job-forge 2.14.31 → 2.14.32

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.
@@ -17,7 +17,7 @@ same: produce the best final writing you can from the context you were given.
17
17
  - Write cover letters, Section G draft answers, "Why X?" responses.
18
18
  - Compose STAR+R interview stories and the story bank (`modes/offer.md` Block F).
19
19
  - Draft LinkedIn outreach messages (`modes/contact.md`).
20
- - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, then the narrative report.
20
+ - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, validate it with `job-forge score:check` when it drives PDF/apply/batch decisions, then write the narrative report.
21
21
  - Drive a single high-stakes application form only when the orchestrator explicitly dispatches you in `apply` mode. In that case, follow `modes/apply.md` exactly and use the same Geometra/Gmail flow as `@general-free`.
22
22
 
23
23
  ## Skip These Tasks
@@ -16,7 +16,7 @@ same: produce the best final writing you can from the context you were given.
16
16
  - Write cover letters, Section G draft answers, "Why X?" responses.
17
17
  - Compose STAR+R interview stories and the story bank (`modes/offer.md` Block F).
18
18
  - Draft LinkedIn outreach messages (`modes/contact.md`).
19
- - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, then the narrative report.
19
+ - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, validate it with `job-forge score:check` when it drives PDF/apply/batch decisions, then write the narrative report.
20
20
  - Drive a single high-stakes application form only when the orchestrator explicitly dispatches you in `apply` mode. In that case, follow `modes/apply.md` exactly and use the same Geometra/Gmail flow as `@general-free`.
21
21
 
22
22
  ## Skip These Tasks
@@ -30,7 +30,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
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.
31
31
  why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
32
32
 
33
- - [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, and materialized fact records returned by `npx job-forge facts:query ...`.
33
+ - [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, and materialized fact records returned by `npx job-forge facts:query ...`.
34
34
  why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
35
35
 
36
36
  - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
@@ -95,16 +95,19 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
95
95
  - [D18] Treat `templates/redact.json` as the source of truth before exporting local traces, prompts, reports, or fixtures outside the project. Use `npx job-forge redact:scan --input <file>`, `redact:apply --input <file> --output .jobforge-redacted/<file>`, or `redact:verify --input <file>` instead of hand-redacting with prose. This complements H8; it does not make it acceptable to paste secrets into prompts.
96
96
  why: `iso-redact` is not an MCP and adds no prompt/tool-schema tokens; it gives deterministic safe-export checks whose findings do not print matched secret values
97
97
 
98
+ - [D19] Treat `templates/score.json` as the source of truth for offer scoring weights, bands, and gates. After emitting a report score JSON that will drive PDF/application/batch decisions, run `npx job-forge score:check --input <file>`; for apply decisions run `npx job-forge score:gate --input <file> --gate apply`. Do not recalculate weighted totals or thresholds manually when the local helper can check them.
99
+ why: `iso-score` is not an MCP and adds no prompt/tool-schema tokens; it makes scoring math, recommendation bands, and threshold booleans executable local policy instead of repeated model prose
100
+
98
101
  ## Procedure
99
102
 
100
103
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
101
104
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
102
- 3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
105
+ 3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
103
106
  4. Prepare Geometra dispatches: cleanup [H3], canon/index/facts/ledger prefilter when useful [D8, D13, D13b, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
104
107
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D17].
105
108
  6. Keep multi-job form-filling out of the orchestrator [H4].
106
109
  7. Cross-check subagent facts against authoritative files [H7].
107
- 8. Apply score gate [D4].
110
+ 8. Apply score gate [D4, D19].
108
111
  9. Merge contract-validated TSV outcomes [H6, D9].
109
112
  10. Verify tracker and run postflight check before ending [H6, D17].
110
113
 
@@ -32,7 +32,7 @@ same: produce the best final writing you can from the context you were given.
32
32
  - Write cover letters, Section G draft answers, "Why X?" responses.
33
33
  - Compose STAR+R interview stories and the story bank (`modes/offer.md` Block F).
34
34
  - Draft LinkedIn outreach messages (`modes/contact.md`).
35
- - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, then the narrative report.
35
+ - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, validate it with `job-forge score:check` when it drives PDF/apply/batch decisions, then write the narrative report.
36
36
  - Drive a single high-stakes application form only when the orchestrator explicitly dispatches you in `apply` mode. In that case, follow `modes/apply.md` exactly and use the same Geometra/Gmail flow as `@general-free`.
37
37
 
38
38
  ## Skip These Tasks
@@ -99,6 +99,11 @@ Artifact contracts (terminal, outside opencode):
99
99
  npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json
100
100
  npx job-forge tracker-line ... --write # renders + validates tracker TSV locally
101
101
 
102
+ Score policy (terminal, outside opencode):
103
+ npx job-forge score:check --input /tmp/score.json
104
+ npx job-forge score:gate --input /tmp/score.json --gate apply
105
+ npx job-forge score:explain
106
+
102
107
  Role capabilities (terminal, outside opencode):
103
108
  npx job-forge capabilities:explain general-free
104
109
  npx job-forge capabilities:check general-free --tool browser --mcp geometra --filesystem write
@@ -207,6 +212,9 @@ Step 4 — Materialize and check the dispatch plan
207
212
  (or another explicit JSON file). Include source paths for company, role,
208
213
  companyRoleKey, URL, score, duplicate/location gates, and any skip/block
209
214
  decision.
215
+ - If the candidate came from a fresh evaluation score JSON, run npx job-forge
216
+ score:check --input <score.json> and npx job-forge score:gate --input
217
+ <score.json> --gate apply before using that score as an apply gate.
210
218
  - Run npx job-forge preflight:check --candidates <file> to fail on missing
211
219
  sources or blocked gates, then npx job-forge preflight:plan --candidates
212
220
  <file> --json > batch/preflight-plan.json to get the bounded round list.
package/AGENTS.md CHANGED
@@ -25,7 +25,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
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.
26
26
  why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
27
27
 
28
- - [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, and materialized fact records returned by `npx job-forge facts:query ...`.
28
+ - [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, and materialized fact records returned by `npx job-forge facts:query ...`.
29
29
  why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
30
30
 
31
31
  - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
@@ -90,16 +90,19 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
90
90
  - [D18] Treat `templates/redact.json` as the source of truth before exporting local traces, prompts, reports, or fixtures outside the project. Use `npx job-forge redact:scan --input <file>`, `redact:apply --input <file> --output .jobforge-redacted/<file>`, or `redact:verify --input <file>` instead of hand-redacting with prose. This complements H8; it does not make it acceptable to paste secrets into prompts.
91
91
  why: `iso-redact` is not an MCP and adds no prompt/tool-schema tokens; it gives deterministic safe-export checks whose findings do not print matched secret values
92
92
 
93
+ - [D19] Treat `templates/score.json` as the source of truth for offer scoring weights, bands, and gates. After emitting a report score JSON that will drive PDF/application/batch decisions, run `npx job-forge score:check --input <file>`; for apply decisions run `npx job-forge score:gate --input <file> --gate apply`. Do not recalculate weighted totals or thresholds manually when the local helper can check them.
94
+ why: `iso-score` is not an MCP and adds no prompt/tool-schema tokens; it makes scoring math, recommendation bands, and threshold booleans executable local policy instead of repeated model prose
95
+
93
96
  ## Procedure
94
97
 
95
98
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
96
99
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
97
- 3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
100
+ 3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
98
101
  4. Prepare Geometra dispatches: cleanup [H3], canon/index/facts/ledger prefilter when useful [D8, D13, D13b, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
99
102
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D17].
100
103
  6. Keep multi-job form-filling out of the orchestrator [H4].
101
104
  7. Cross-check subagent facts against authoritative files [H7].
102
- 8. Apply score gate [D4].
105
+ 8. Apply score gate [D4, D19].
103
106
  9. Merge contract-validated TSV outcomes [H6, D9].
104
107
  10. Verify tracker and run postflight check before ending [H6, D17].
105
108
 
package/CLAUDE.md CHANGED
@@ -25,7 +25,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
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.
26
26
  why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
27
27
 
28
- - [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, and materialized fact records returned by `npx job-forge facts:query ...`.
28
+ - [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, and materialized fact records returned by `npx job-forge facts:query ...`.
29
29
  why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
30
30
 
31
31
  - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
@@ -90,16 +90,19 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
90
90
  - [D18] Treat `templates/redact.json` as the source of truth before exporting local traces, prompts, reports, or fixtures outside the project. Use `npx job-forge redact:scan --input <file>`, `redact:apply --input <file> --output .jobforge-redacted/<file>`, or `redact:verify --input <file>` instead of hand-redacting with prose. This complements H8; it does not make it acceptable to paste secrets into prompts.
91
91
  why: `iso-redact` is not an MCP and adds no prompt/tool-schema tokens; it gives deterministic safe-export checks whose findings do not print matched secret values
92
92
 
93
+ - [D19] Treat `templates/score.json` as the source of truth for offer scoring weights, bands, and gates. After emitting a report score JSON that will drive PDF/application/batch decisions, run `npx job-forge score:check --input <file>`; for apply decisions run `npx job-forge score:gate --input <file> --gate apply`. Do not recalculate weighted totals or thresholds manually when the local helper can check them.
94
+ why: `iso-score` is not an MCP and adds no prompt/tool-schema tokens; it makes scoring math, recommendation bands, and threshold booleans executable local policy instead of repeated model prose
95
+
93
96
  ## Procedure
94
97
 
95
98
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
96
99
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
97
- 3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
100
+ 3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
98
101
  4. Prepare Geometra dispatches: cleanup [H3], canon/index/facts/ledger prefilter when useful [D8, D13, D13b, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
99
102
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D17].
100
103
  6. Keep multi-job form-filling out of the orchestrator [H4].
101
104
  7. Cross-check subagent facts against authoritative files [H7].
102
- 8. Apply score gate [D4].
105
+ 8. Apply score gate [D4, D19].
103
106
  9. Merge contract-validated TSV outcomes [H6, D9].
104
107
  10. Verify tracker and run postflight check before ending [H6, D17].
105
108
 
package/README.md CHANGED
@@ -31,7 +31,7 @@ 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/canon.json` defines URL/company/role identity keys via `@razroo/iso-canon`, `templates/contracts.json` defines tracker/apply artifact shapes via `@razroo/iso-contract`, `templates/capabilities.json` defines role capability boundaries via `@razroo/iso-capabilities`, `templates/context.json` defines deterministic mode/reference bundles via `@razroo/iso-context`, `templates/preflight.json` defines safe dispatch rounds/gates via `@razroo/iso-preflight`, `templates/postflight.json` defines safe dispatch settlement via `@razroo/iso-postflight`, `templates/redact.json` defines safe-export redaction rules via `@razroo/iso-redact`, `templates/migrations.json` defines safe consumer-project upgrades via `@razroo/iso-migrate`, `templates/facts.json` defines source-backed fact extraction via `@razroo/iso-facts`, `.jobforge-ledger/events.jsonl` records duplicate/status events via `@razroo/iso-ledger`, `.jobforge-cache/` stores reusable JD/artifact content via `@razroo/iso-cache`, `.jobforge-index.json` indexes artifact source pointers via `@razroo/iso-index`, and `.jobforge-facts.json` materializes queryable facts with provenance. None of these add always-on prompt or tool-schema tokens.
34
+ JobForge also keeps MCP-free local workflow state and policy: `templates/canon.json` defines URL/company/role identity keys via `@razroo/iso-canon`, `templates/contracts.json` defines tracker/apply artifact shapes via `@razroo/iso-contract`, `templates/score.json` defines weighted scoring and gates via `@razroo/iso-score`, `templates/capabilities.json` defines role capability boundaries via `@razroo/iso-capabilities`, `templates/context.json` defines deterministic mode/reference bundles via `@razroo/iso-context`, `templates/preflight.json` defines safe dispatch rounds/gates via `@razroo/iso-preflight`, `templates/postflight.json` defines safe dispatch settlement via `@razroo/iso-postflight`, `templates/redact.json` defines safe-export redaction rules via `@razroo/iso-redact`, `templates/migrations.json` defines safe consumer-project upgrades via `@razroo/iso-migrate`, `templates/facts.json` defines source-backed fact extraction via `@razroo/iso-facts`, `.jobforge-ledger/events.jsonl` records duplicate/status events via `@razroo/iso-ledger`, `.jobforge-cache/` stores reusable JD/artifact content via `@razroo/iso-cache`, `.jobforge-index.json` indexes artifact source pointers via `@razroo/iso-index`, and `.jobforge-facts.json` materializes queryable facts with provenance. None of these add always-on prompt or tool-schema tokens.
35
35
 
36
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.
37
37
 
@@ -63,7 +63,7 @@ JobForge turns opencode into a full job search command center. Instead of manual
63
63
  | Feature | Description |
64
64
  |---------|-------------|
65
65
  | **Auto-Pipeline** | Paste a URL, get a full evaluation + PDF + tracker entry |
66
- | **Unified Scoring** | 10 weighted dimensions, consistent across all modes, with calibration anchors |
66
+ | **Unified Scoring** | 10 weighted dimensions, consistent across all modes, with calibration anchors and deterministic `job-forge score:*` checks |
67
67
  | **Anti-AI-Detection CVs** | Writing rules that avoid ATS filters on Indeed, LinkedIn, Workday |
68
68
  | **6-Block Evaluation** | Role summary, CV match, level strategy, comp research, personalization, interview prep (STAR+R) |
69
69
  | **Interview Story Bank** | Curated bank of 10-12 stories with match counts, archetype tags, and automatic pruning |
@@ -78,7 +78,7 @@ JobForge turns opencode into a full job search command center. Instead of manual
78
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/`. |
79
79
  | **Pipeline Integrity** | Automated merge, dedup, status normalization, health checks |
80
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. |
81
- | **Trace + Telemetry + Guard + Contract + Canon + Ledger + Capabilities + Context + Cache + Index + Facts + Preflight + Postflight + Redact + Migrate** | `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`, `job-forge canon:*` derives stable URL/company/role identity keys, `job-forge ledger:*` queries append-only workflow state, `job-forge capabilities:*` checks role boundaries, `job-forge context:*` plans mode/reference context bundles, `job-forge cache:*` reuses fetched JD/artifact content, `job-forge index:*` queries compact source pointers, `job-forge facts:*` materializes source-backed job/application/candidate facts, `job-forge preflight:*` plans bounded apply dispatch rounds from file-backed candidate facts, `job-forge postflight:*` settles dispatch outcomes/artifacts/post-steps, `job-forge redact:*` sanitizes local exports, and `job-forge migrate:*` applies safe consumer-project upgrades without MCP/tool-schema overhead. |
81
+ | **Trace + Telemetry + Guard + Contract + Score + Canon + Ledger + Capabilities + Context + Cache + Index + Facts + Preflight + Postflight + Redact + Migrate** | `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`, `job-forge score:*` computes/checks weighted offer scores, `job-forge canon:*` derives stable URL/company/role identity keys, `job-forge ledger:*` queries append-only workflow state, `job-forge capabilities:*` checks role boundaries, `job-forge context:*` plans mode/reference context bundles, `job-forge cache:*` reuses fetched JD/artifact content, `job-forge index:*` queries compact source pointers, `job-forge facts:*` materializes source-backed job/application/candidate facts, `job-forge preflight:*` plans bounded apply dispatch rounds from file-backed candidate facts, `job-forge postflight:*` settles dispatch outcomes/artifacts/post-steps, `job-forge redact:*` sanitizes local exports, and `job-forge migrate:*` applies safe consumer-project upgrades without MCP/tool-schema overhead. |
82
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. |
83
83
 
84
84
  ## Usage
@@ -118,7 +118,7 @@ You paste a job URL or description
118
118
 
119
119
  ┌────────▼─────────┐
120
120
  │ A-F Evaluation │ Match, gaps, comp research, STAR stories
121
- │ (reads cv.md) │ Unified 10-dimension scoring model
121
+ │ (reads cv.md) │ Unified 10-dimension scoring model + iso-score checks
122
122
  └────────┬─────────┘
123
123
 
124
124
  ┌────┼────┐
@@ -166,7 +166,7 @@ my-search/
166
166
  ├── .opencode/skills/job-forge.md # → skill router
167
167
  ├── .opencode/agents/ # → @general-free, @general-paid, @glm-minimal
168
168
  ├── modes/ # → _shared.md + skill modes
169
- ├── templates/ # → states.yml, portals.example.yml, cv-template.html, canon.json, capabilities.json, context.json, index.json, facts.json, preflight.json, postflight.json, redact.json, migrations.json
169
+ ├── templates/ # → states.yml, portals.example.yml, cv-template.html, canon.json, score.json, capabilities.json, context.json, index.json, facts.json, preflight.json, postflight.json, redact.json, migrations.json
170
170
  ├── batch/batch-prompt.md # → batch worker prompt
171
171
  ├── batch/batch-runner.sh # → parallel orchestrator
172
172
 
@@ -192,7 +192,7 @@ JobForge/
192
192
  │ ├── sync.mjs # postinstall: creates symlinks in consumer project
193
193
  │ └── create-job-forge.mjs # scaffolder
194
194
  ├── modes/ # _shared.md + 16 skill modes
195
- ├── templates/ # cv-template.html, portals.example.yml, states.yml, canon.json, capabilities.json, context.json, facts.json, preflight.json, postflight.json, redact.json, migrations.json
195
+ ├── templates/ # cv-template.html, portals.example.yml, states.yml, canon.json, score.json, capabilities.json, context.json, facts.json, preflight.json, postflight.json, redact.json, migrations.json
196
196
  ├── config/profile.example.yml # template for consumer's profile.yml
197
197
  ├── batch/{batch-prompt.md,batch-runner.sh} # batch orchestrator
198
198
  ├── scripts/
@@ -204,6 +204,7 @@ JobForge/
204
204
  │ ├── cache.mjs # iso-cache-backed local artifact cache CLI
205
205
  │ ├── index.mjs # iso-index-backed artifact lookup CLI
206
206
  │ ├── facts.mjs # iso-facts-backed local fact materialization
207
+ │ ├── score.mjs # iso-score-backed offer scoring CLI
207
208
  │ ├── canon.mjs # iso-canon-backed identity normalization CLI
208
209
  │ ├── preflight.mjs # iso-preflight-backed dispatch planning CLI
209
210
  │ ├── postflight.mjs # iso-postflight-backed dispatch settlement CLI
package/bin/job-forge.mjs CHANGED
@@ -26,6 +26,7 @@
26
26
  * cache:* Reuse local deterministic artifacts via iso-cache
27
27
  * index:* Query local artifacts via iso-index
28
28
  * facts:* Materialize source-backed local facts via iso-facts
29
+ * score:* Compute/check deterministic offer scores via iso-score
29
30
  * canon:* Compute deterministic identity keys via iso-canon
30
31
  * preflight:* Plan safe dispatch rounds via iso-preflight
31
32
  * postflight:* Settle dispatch outcomes via iso-postflight
@@ -144,6 +145,16 @@ const factsAliases = {
144
145
  'facts:path': 'path',
145
146
  };
146
147
 
148
+ const scoreAliases = {
149
+ 'score:compute': 'compute',
150
+ 'score:verify': 'verify',
151
+ 'score:check': 'check',
152
+ 'score:gate': 'gate',
153
+ 'score:compare': 'compare',
154
+ 'score:explain': 'explain',
155
+ 'score:path': 'path',
156
+ };
157
+
147
158
  const canonAliases = {
148
159
  'canon:normalize': 'normalize',
149
160
  'canon:key': 'key',
@@ -238,6 +249,11 @@ Commands:
238
249
  facts:query Query materialized facts with source path/line provenance
239
250
  facts:verify Validate local fact set integrity
240
251
  facts:check Check configured fact requirements
252
+ score:compute Compute canonical weighted score from report score JSON
253
+ score:check Validate score math, thresholds, rationales, and dimensions
254
+ score:gate Evaluate one score gate (apply, pdf, draft_answers, strong)
255
+ score:compare Compare two score JSON files deterministically
256
+ score:explain Show the active scoring rubric from templates/score.json
241
257
  canon:key Print stable URL/company/role/company-role keys
242
258
  canon:compare Compare two identifiers as same/possible/different
243
259
  canon:explain Show the active identity canonicalization policy
@@ -295,6 +311,8 @@ Pass --help after a command to see its own flags, e.g.:
295
311
  job-forge index:query "acme"
296
312
  job-forge facts:has --fact application.status --key "company-role:acme:staff-engineer"
297
313
  job-forge facts:query --fact job.url --tag report
314
+ job-forge score:check --input /tmp/score.json
315
+ job-forge score:gate --input /tmp/score.json --gate apply
298
316
  job-forge canon:key company-role --company "Acme, Inc." --role "Senior SWE - Remote US"
299
317
  job-forge canon:compare company "OpenAI, Inc." "Open AI"
300
318
  job-forge preflight:plan --candidates batch/preflight-candidates.json
@@ -449,6 +467,21 @@ if (cmd === 'facts' || factsAliases[cmd]) {
449
467
  process.exit(result.status ?? 1);
450
468
  }
451
469
 
470
+ if (cmd === 'score' || scoreAliases[cmd]) {
471
+ const scoreArgs = cmd === 'score'
472
+ ? (rest.length === 0 ? ['help'] : rest)
473
+ : [scoreAliases[cmd], ...rest];
474
+
475
+ const scriptPath = join(PKG_ROOT, 'scripts/score.mjs');
476
+ const result = spawnSync(process.execPath, [scriptPath, ...scoreArgs], {
477
+ stdio: 'inherit',
478
+ cwd: PROJECT_DIR,
479
+ env: process.env,
480
+ });
481
+
482
+ process.exit(result.status ?? 1);
483
+ }
484
+
452
485
  if (cmd === 'canon' || canonAliases[cmd]) {
453
486
  const canonArgs = cmd === 'canon'
454
487
  ? (rest.length === 0 ? ['help'] : rest)
@@ -32,7 +32,7 @@ my-search/
32
32
  ├── .opencode/skills/job-forge.md # → skill router
33
33
  ├── .opencode/agents/ # → @general-free, @general-paid, @glm-minimal
34
34
  ├── modes/ # → mode files
35
- ├── templates/ # → states.yml, portals.example.yml, cv-template.html, preflight.json, postflight.json
35
+ ├── templates/ # → states.yml, portals.example.yml, cv-template.html, score.json, preflight.json, postflight.json
36
36
  ├── batch/batch-prompt.md # → batch worker prompt
37
37
  ├── batch/batch-runner.sh # → parallel orchestrator
38
38
  └── node_modules/job-forge/ # harness, installed from npm
@@ -40,7 +40,7 @@ my-search/
40
40
 
41
41
  Symlinks are created by the harness's `postinstall` hook (`bin/sync.mjs`) on every `npm install`. Real files at those paths are preserved — if a user locally customizes a mode file, the sync skips that symlink and warns.
42
42
 
43
- The consumer's `opencode.json` loads a small set of stable files as always-present instructions: `AGENTS.harness.md` (harness operational rules), `templates/states.yml` (canonical application states), `modes/_shared.md` (scoring model), and `cv.md` (the candidate's CV). Caching these in the prefix means agents never Read them as tool calls. Churning content (score calibration anchors, specific mode files) stays out of `instructions` and is Read on demand.
43
+ The consumer's `opencode.json` loads a small set of stable files as always-present instructions: `AGENTS.harness.md` (harness operational rules), `templates/states.yml` (canonical application states), `modes/_shared.md` (scoring model), and `cv.md` (the candidate's CV). The executable scoring rubric lives in `templates/score.json` and is checked on demand with `job-forge score:*`, so agents do not need to repeat scoring math in the prompt. Caching stable prose in the prefix means agents never Read it as tool calls. Churning content (score calibration anchors, specific mode files) stays out of `instructions` and is Read on demand.
44
44
 
45
45
  The skill router (`.opencode/skills/job-forge.md`) loads mode and data files on demand, keeping per-session input tokens low (~20-40K for most modes instead of ~130-170K when everything was force-loaded).
46
46
 
@@ -122,7 +122,7 @@ For customization (archetypes, weights, tone), start with `_shared.md` and [CUST
122
122
  - D: Comp research (WebSearch).
123
123
  - E: CV personalization plan.
124
124
  - F: Interview prep (STAR stories).
125
- 5. **Score**: Weighted average across 10 dimensions (1-5)
125
+ 5. **Score**: Weighted average across 10 dimensions (1-5), computed and gated from `templates/score.json`
126
126
  6. **Report**: Save as `reports/{num}-{company}-{date}.md`
127
127
  7. **PDF**: Generate ATS-optimized CV (`generate-pdf.mjs`)
128
128
  8. **Track**: Write one TSV per evaluation under `batch/tracker-additions/` (see [AGENTS.md](../AGENTS.md) TSV layout); fold rows into `data/applications.md` with `npm run merge` / `merge-tracker.mjs` when you are ready (not automatic in every workflow)
@@ -166,6 +166,7 @@ data/pipeline.md → Pending URLs and `local:jds/...` inbox (see modes/p
166
166
  jds/*.md → Saved job descriptions referenced from the pipeline (`local:jds/{file}`)
167
167
  templates/states.yml → Canonical status values
168
168
  templates/canon.json → Canonical URL/company/role identity keys
169
+ templates/score.json → Canonical weighted scoring rubric and gates
169
170
  templates/context.json → Deterministic mode/reference context bundle policy
170
171
  templates/facts.json → Source-backed fact extraction policy
171
172
  templates/preflight.json → Safe apply dispatch rounds/gates policy
@@ -186,6 +187,7 @@ Create `data/pipeline.md` when you start using the URL inbox (`/job-forge pipeli
186
187
  - Index: `.jobforge-index.json` (created on demand by `job-forge index:*`; gitignored local lookup state)
187
188
  - Facts: `.jobforge-facts.json` (created on demand by `job-forge facts:*`; gitignored local fact state)
188
189
  - Canon: `templates/canon.json` (identity rules inspected with `job-forge canon:*`)
190
+ - Score: `templates/score.json` (weighted rubric and gates inspected with `job-forge score:*`)
189
191
  - Preflight: `templates/preflight.json` (dispatch rounds/gates inspected with `job-forge preflight:*`)
190
192
  - Postflight: `templates/postflight.json` (dispatch outcomes/artifacts/post-steps inspected with `job-forge postflight:*`)
191
193
  - Migrations: `templates/migrations.json` (applied by `job-forge sync` and inspectable with `job-forge migrate:*`)
@@ -236,6 +238,7 @@ Scripts maintain data consistency. In a consumer project they're invoked via the
236
238
  | `scripts/ledger.mjs` | `npx job-forge ledger:status` / `ledger:has` / `ledger:rebuild` | Deterministic `@razroo/iso-ledger` state over tracker, TSV, and pipeline files |
237
239
  | `scripts/index.mjs` | `npx job-forge index:status` / `index:has` / `index:query` | Deterministic `@razroo/iso-index` lookup over reports, tracker rows, TSVs, pipeline, scan history, and ledger events |
238
240
  | `scripts/facts.mjs` | `npx job-forge facts:status` / `facts:has` / `facts:query` | Deterministic `@razroo/iso-facts` materialization over job URLs, scores, application statuses, preflight candidates, scan history, and ledger events |
241
+ | `scripts/score.mjs` | `npx job-forge score:check` / `score:gate` / `score:explain` | Deterministic `@razroo/iso-score` checks for weighted offer scores, threshold booleans, recommendations, and score gates |
239
242
  | `scripts/canon.mjs` | `npx job-forge canon:normalize` / `canon:key` / `canon:compare` | Deterministic `@razroo/iso-canon` identity normalization for URLs, companies, roles, and company+role pairs |
240
243
  | `scripts/context.mjs` | `npx job-forge context:list` / `context:plan` / `context:check` / `context:render` | Deterministic `@razroo/iso-context` mode/reference context bundle planning and rendering |
241
244
  | `scripts/preflight.mjs` | `npx job-forge preflight:plan` / `preflight:check` / `preflight:explain` | Deterministic `@razroo/iso-preflight` dispatch planning for file-backed candidate facts and gates |
@@ -158,6 +158,10 @@ Artifact lookup policy lives in `templates/index.json` and is built locally by `
158
158
 
159
159
  Fact extraction policy lives in `templates/facts.json` and is built locally by `@razroo/iso-facts`. Use `job-forge facts:query --fact job.url`, `job-forge facts:has --fact application.status --key "company-role:acme:staff-engineer"`, and `job-forge facts:verify` to work with compact source-backed facts instead of rereading reports, tracker day files, TSVs, candidate JSON, scan history, and ledger events. Query, has, verify, and check rebuild `.jobforge-facts.json` on demand, so scaffolded projects need no setup. JobForge canonicalizes company/role and URL fact keys through `templates/canon.json` before writing the fact set. This is not an MCP and does not add prompt or tool-schema tokens.
160
160
 
161
+ ## JobForge scoring policy
162
+
163
+ Weighted scoring policy lives in `templates/score.json` and is enforced locally by `@razroo/iso-score`. Use `job-forge score:check --input <score.json>` to validate emitted report score JSON, `job-forge score:gate --input <score.json> --gate apply` to check an application threshold, and `job-forge score:explain` to inspect the active dimensions, weights, bands, and gates. Custom forks can change weights or thresholds in `templates/score.json`, but keep the dimension ids aligned with `modes/_shared.md` and report rendering.
164
+
161
165
  ## JobForge identity canonicalization
162
166
 
163
167
  URL, company, role, and company+role identity rules live in `templates/canon.json` and are enforced locally by `@razroo/iso-canon`. Use `job-forge canon:key company-role --company "OpenAI, Inc." --role "Senior SWE, AI Platform"` to derive the same duplicate key used by ledger/index helpers, and `job-forge canon:compare company "OpenAI, Inc." "Open AI"` to explain whether two values resolve to the same entity. Custom forks can extend aliases, suffixes, stop words, and match thresholds in `templates/canon.json`. This is not an MCP and does not add prompt or tool-schema tokens.
package/docs/SETUP.md CHANGED
@@ -126,6 +126,8 @@ From your project root, these commands maintain the tracker and pipeline checks.
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
128
  | Inspect tracker row contract | `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` | _(none)_ |
129
+ | Validate emitted score JSON | `npx job-forge score:check --input /tmp/score.json` | `npm run score:check -- --input /tmp/score.json` |
130
+ | Check application score gate | `npx job-forge score:gate --input /tmp/score.json --gate apply` | `npm run score:gate -- --input /tmp/score.json --gate apply` |
129
131
  | Derive canonical company/role key | `npx job-forge canon:key company-role --company "Acme" --role "Staff Engineer"` | `npm run canon:key -- company-role --company ...` |
130
132
  | Compare identity values | `npx job-forge canon:compare company "OpenAI, Inc." "Open AI"` | `npm run canon:compare -- company ...` |
131
133
  | Inspect role capabilities | `npx job-forge capabilities:explain general-free` | `npm run capabilities:explain -- general-free` |
@@ -39,7 +39,7 @@ same: produce the best final writing you can from the context you were given.
39
39
  - Write cover letters, Section G draft answers, "Why X?" responses.
40
40
  - Compose STAR+R interview stories and the story bank (`modes/offer.md` Block F).
41
41
  - Draft LinkedIn outreach messages (`modes/contact.md`).
42
- - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, then the narrative report.
42
+ - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, validate it with `job-forge score:check` when it drives PDF/apply/batch decisions, then write the narrative report.
43
43
  - Drive a single high-stakes application form only when the orchestrator explicitly dispatches you in `apply` mode. In that case, follow `modes/apply.md` exactly and use the same Geometra/Gmail flow as `@general-free`.
44
44
 
45
45
  ## Skip These Tasks
@@ -102,6 +102,11 @@ Artifact contracts (terminal, outside opencode):
102
102
  npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json
103
103
  npx job-forge tracker-line ... --write # renders + validates tracker TSV locally
104
104
 
105
+ Score policy (terminal, outside opencode):
106
+ npx job-forge score:check --input /tmp/score.json
107
+ npx job-forge score:gate --input /tmp/score.json --gate apply
108
+ npx job-forge score:explain
109
+
105
110
  Role capabilities (terminal, outside opencode):
106
111
  npx job-forge capabilities:explain general-free
107
112
  npx job-forge capabilities:check general-free --tool browser --mcp geometra --filesystem write
@@ -210,6 +215,9 @@ Step 4 — Materialize and check the dispatch plan
210
215
  (or another explicit JSON file). Include source paths for company, role,
211
216
  companyRoleKey, URL, score, duplicate/location gates, and any skip/block
212
217
  decision.
218
+ - If the candidate came from a fresh evaluation score JSON, run npx job-forge
219
+ score:check --input <score.json> and npx job-forge score:gate --input
220
+ <score.json> --gate apply before using that score as an apply gate.
213
221
  - Run npx job-forge preflight:check --candidates <file> to fail on missing
214
222
  sources or blocked gates, then npx job-forge preflight:plan --candidates
215
223
  <file> --json > batch/preflight-plan.json to get the bounded round list.
@@ -25,7 +25,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
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.
26
26
  why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
27
27
 
28
- - [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, and materialized fact records returned by `npx job-forge facts:query ...`.
28
+ - [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, and materialized fact records returned by `npx job-forge facts:query ...`.
29
29
  why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
30
30
 
31
31
  - [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
@@ -90,16 +90,19 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
90
90
  - [D18] Treat `templates/redact.json` as the source of truth before exporting local traces, prompts, reports, or fixtures outside the project. Use `npx job-forge redact:scan --input <file>`, `redact:apply --input <file> --output .jobforge-redacted/<file>`, or `redact:verify --input <file>` instead of hand-redacting with prose. This complements H8; it does not make it acceptable to paste secrets into prompts.
91
91
  why: `iso-redact` is not an MCP and adds no prompt/tool-schema tokens; it gives deterministic safe-export checks whose findings do not print matched secret values
92
92
 
93
+ - [D19] Treat `templates/score.json` as the source of truth for offer scoring weights, bands, and gates. After emitting a report score JSON that will drive PDF/application/batch decisions, run `npx job-forge score:check --input <file>`; for apply decisions run `npx job-forge score:gate --input <file> --gate apply`. Do not recalculate weighted totals or thresholds manually when the local helper can check them.
94
+ why: `iso-score` is not an MCP and adds no prompt/tool-schema tokens; it makes scoring math, recommendation bands, and threshold booleans executable local policy instead of repeated model prose
95
+
93
96
  ## Procedure
94
97
 
95
98
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
96
99
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
97
- 3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
100
+ 3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
98
101
  4. Prepare Geometra dispatches: cleanup [H3], canon/index/facts/ledger prefilter when useful [D8, D13, D13b, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
99
102
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D17].
100
103
  6. Keep multi-job form-filling out of the orchestrator [H4].
101
104
  7. Cross-check subagent facts against authoritative files [H7].
102
- 8. Apply score gate [D4].
105
+ 8. Apply score gate [D4, D19].
103
106
  9. Merge contract-validated TSV outcomes [H6, D9].
104
107
  10. Verify tracker and run postflight check before ending [H6, D17].
105
108
 
@@ -0,0 +1,212 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import {
4
+ compareScoreResults,
5
+ computeScore,
6
+ evaluateGate,
7
+ loadScoreConfig,
8
+ parseJson,
9
+ scoreResultId,
10
+ verifyScoreResult,
11
+ } from '@razroo/iso-score';
12
+
13
+ export const SCORE_CONFIG_FILE = 'templates/score.json';
14
+ export const SCORE_PROFILE = 'jobforge';
15
+
16
+ export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
17
+ return projectDir;
18
+ }
19
+
20
+ export function jobForgeScoreConfigPath(projectDir = resolveProjectDir()) {
21
+ return process.env.JOB_FORGE_SCORE_CONFIG || join(projectDir, SCORE_CONFIG_FILE);
22
+ }
23
+
24
+ export function readJobForgeScoreConfig(projectDir = resolveProjectDir()) {
25
+ const path = jobForgeScoreConfigPath(projectDir);
26
+ return loadScoreConfig(parseJson(readFileSync(path, 'utf8'), path));
27
+ }
28
+
29
+ export function readJsonFile(path) {
30
+ return parseJson(readFileSync(path, 'utf8'), path);
31
+ }
32
+
33
+ export function normalizeJobForgeScoreInput(input) {
34
+ if (!isObject(input)) throw new Error('score input must be a JSON object');
35
+
36
+ if ('dimensions' in input) {
37
+ return {
38
+ ...input,
39
+ profile: stringOr(input.profile, SCORE_PROFILE),
40
+ };
41
+ }
42
+
43
+ if (!isObject(input.scores)) {
44
+ throw new Error('score input must contain either dimensions or JobForge scores');
45
+ }
46
+
47
+ const dimensions = {};
48
+ for (const [id, raw] of Object.entries(input.scores)) {
49
+ if (!isObject(raw)) throw new Error(`scores.${id} must be a JSON object`);
50
+ dimensions[id] = {
51
+ score: Number(raw.score),
52
+ note: stringOr(raw.rationale, stringOr(raw.note, '')),
53
+ evidence: Array.isArray(raw.evidence) ? raw.evidence.filter((item) => typeof item === 'string') : [],
54
+ };
55
+ }
56
+
57
+ return stripUndefined({
58
+ subject: [input.company, input.role].filter((value) => typeof value === 'string' && value.length > 0).join(' - ') || undefined,
59
+ profile: SCORE_PROFILE,
60
+ dimensions,
61
+ facts: stripUndefined({
62
+ report_num: jsonScalar(input.report_num),
63
+ company: jsonScalar(input.company),
64
+ role: jsonScalar(input.role),
65
+ archetype: jsonScalar(input.archetype),
66
+ url: jsonScalar(input.url),
67
+ date: jsonScalar(input.date),
68
+ }),
69
+ meta: stripUndefined({
70
+ sourceShape: 'jobforge-score-json',
71
+ expectedWeightedTotal: jsonScalar(input.weighted_total),
72
+ recommendation: jsonScalar(input.recommendation),
73
+ pdf_threshold_met: typeof input.pdf_threshold_met === 'boolean' ? input.pdf_threshold_met : undefined,
74
+ draft_answers_threshold_met: typeof input.draft_answers_threshold_met === 'boolean' ? input.draft_answers_threshold_met : undefined,
75
+ }),
76
+ });
77
+ }
78
+
79
+ export function computeJobForgeScore(input, options = {}, projectDir = resolveProjectDir()) {
80
+ const config = readJobForgeScoreConfig(projectDir);
81
+ const normalized = normalizeJobForgeScoreInput(input);
82
+ const result = computeScore(config, normalized, { profile: options.profile || normalized.profile || SCORE_PROFILE });
83
+ return withJobForgeIssues(result, normalized);
84
+ }
85
+
86
+ export function checkJobForgeScore(input, options = {}, projectDir = resolveProjectDir()) {
87
+ const result = computeJobForgeScore(input, options, projectDir);
88
+ const errors = result.issues.filter((issue) => issue.severity === 'error').length;
89
+ const warnings = result.issues.filter((issue) => issue.severity === 'warn').length;
90
+ return {
91
+ ok: errors === 0,
92
+ errors,
93
+ warnings,
94
+ result,
95
+ issues: result.issues,
96
+ };
97
+ }
98
+
99
+ export function evaluateJobForgeScoreGate(input, options = {}, projectDir = resolveProjectDir()) {
100
+ const config = readJobForgeScoreConfig(projectDir);
101
+ const normalized = normalizeJobForgeScoreInput(input);
102
+ const base = evaluateGate(config, normalized, {
103
+ profile: options.profile || normalized.profile || SCORE_PROFILE,
104
+ gate: options.gate,
105
+ });
106
+ const result = withJobForgeIssues(base.result, normalized);
107
+ const errors = result.issues.some((issue) => issue.severity === 'error');
108
+ const gate = errors
109
+ ? { ...base.gate, pass: false, reason: `${base.gate.reason}; score has error issues` }
110
+ : base.gate;
111
+ return {
112
+ ok: gate.pass,
113
+ gate,
114
+ result,
115
+ };
116
+ }
117
+
118
+ export function verifyJobForgeScoreResult(result) {
119
+ return verifyScoreResult(result);
120
+ }
121
+
122
+ export function compareJobForgeScores(leftInput, rightInput, options = {}, projectDir = resolveProjectDir()) {
123
+ const left = computeJobForgeScore(leftInput, options, projectDir);
124
+ const right = computeJobForgeScore(rightInput, options, projectDir);
125
+ return compareScoreResults(left, right);
126
+ }
127
+
128
+ function withJobForgeIssues(result, normalized) {
129
+ const issues = [...result.issues, ...jobForgeShapeIssues(result, normalized)];
130
+ if (issues.length === result.issues.length) return result;
131
+ const updated = { ...result, issues };
132
+ updated.id = scoreResultId(updated);
133
+ return updated;
134
+ }
135
+
136
+ function jobForgeShapeIssues(result, normalized) {
137
+ if (normalized.meta?.sourceShape !== 'jobforge-score-json') return [];
138
+
139
+ const issues = [];
140
+ const expectedTotal = normalized.meta.expectedWeightedTotal;
141
+ if (typeof expectedTotal === 'number' && Math.abs(round1(expectedTotal) - result.score) > 0.0001) {
142
+ issues.push(error('weighted-total-mismatch', `weighted_total ${expectedTotal} does not match computed score ${result.score}`));
143
+ }
144
+
145
+ const expectedRecommendation = recommendationFor(result.score);
146
+ if (normalized.meta.recommendation !== undefined && normalized.meta.recommendation !== expectedRecommendation) {
147
+ issues.push(error('recommendation-mismatch', `recommendation must be "${expectedRecommendation}" for score ${result.score}`));
148
+ }
149
+
150
+ const expectedPdf = result.score >= 3;
151
+ if (normalized.meta.pdf_threshold_met !== undefined && normalized.meta.pdf_threshold_met !== expectedPdf) {
152
+ issues.push(error('pdf-threshold-mismatch', `pdf_threshold_met must be ${expectedPdf} for score ${result.score}`));
153
+ }
154
+
155
+ const expectedDraft = result.score >= 3.5;
156
+ if (normalized.meta.draft_answers_threshold_met !== undefined && normalized.meta.draft_answers_threshold_met !== expectedDraft) {
157
+ issues.push(error('draft-answers-threshold-mismatch', `draft_answers_threshold_met must be ${expectedDraft} for score ${result.score}`));
158
+ }
159
+
160
+ for (const dimension of result.dimensions) {
161
+ if (!isHalfStep(dimension.score)) {
162
+ issues.push(error('invalid-score-step', `dimension "${dimension.id}" score must use 0.5 increments`, dimension.id));
163
+ }
164
+ if (!dimension.note || dimension.note.trim().length === 0) {
165
+ issues.push(error('missing-rationale', `dimension "${dimension.id}" rationale is required`, dimension.id));
166
+ } else if (dimension.note.length > 80) {
167
+ issues.push(error('rationale-too-long', `dimension "${dimension.id}" rationale must be <= 80 characters`, dimension.id));
168
+ } else if (hasMarkdown(dimension.note)) {
169
+ issues.push(error('rationale-markdown', `dimension "${dimension.id}" rationale must not contain markdown`, dimension.id));
170
+ }
171
+ }
172
+
173
+ return issues;
174
+ }
175
+
176
+ function recommendationFor(score) {
177
+ if (score >= 3.5) return 'apply';
178
+ if (score >= 3) return 'apply_with_caveats';
179
+ return 'skip';
180
+ }
181
+
182
+ function isHalfStep(value) {
183
+ return Number.isFinite(value) && Number.isInteger(value * 2);
184
+ }
185
+
186
+ function hasMarkdown(value) {
187
+ return /(`|\*\*|__|\[[^\]]+\]\(|^#{1,6}\s)/.test(value);
188
+ }
189
+
190
+ function error(code, message, dimension) {
191
+ return stripUndefined({ severity: 'error', code, message, dimension });
192
+ }
193
+
194
+ function stringOr(value, fallback) {
195
+ return typeof value === 'string' ? value : fallback;
196
+ }
197
+
198
+ function jsonScalar(value) {
199
+ return value === null || ['string', 'number', 'boolean'].includes(typeof value) ? value : undefined;
200
+ }
201
+
202
+ function round1(value) {
203
+ return Math.round(Number(value) * 10) / 10;
204
+ }
205
+
206
+ function isObject(value) {
207
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
208
+ }
209
+
210
+ function stripUndefined(value) {
211
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
212
+ }
package/modes/_shared.md CHANGED
@@ -147,7 +147,7 @@ If the candidate has a live demo/dashboard (check profile.yml), offer access in
147
147
 
148
148
  ## Use this canonical scoring model (SINGLE SOURCE OF TRUTH)
149
149
 
150
- **ALL evaluation modes MUST use this exact model.** Whether the offer is evaluated via `offer`, `auto-pipeline`, `batch`, or compared via `compare`, the score is computed the same way. This ensures scores are comparable across the entire pipeline.
150
+ **ALL evaluation modes MUST use this exact model.** Whether the offer is evaluated via `offer`, `auto-pipeline`, `batch`, or compared via `compare`, the score is computed the same way. The executable source of truth is `templates/score.json`; use `npx job-forge score:check --input <score.json>` and `score:gate --gate apply` when validating emitted score JSON instead of recalculating gates in prose. This ensures scores are comparable across the entire pipeline.
151
151
 
152
152
  | # | Dimension | Weight | 1 | 3 | 5 |
153
153
  |---|-----------|--------|---|---|---|
@@ -209,8 +209,9 @@ If the candidate has a live demo/dashboard (check profile.yml), offer access in
209
209
  **After emitting the JSON:**
210
210
 
211
211
  1. Embed the same JSON block verbatim in the report `.md` under a `## Score` section (fenced as ` ```json `).
212
- 2. Write Blocks A-F **referencing** the scores by key (e.g., "Seniority fit: 3/5 Senior IC, no formal management"). Do NOT re-list all 10 dimensions in prose. Do NOT repeat the rationales verbatim.
213
- 3. Do NOT narrate the scoring process in thinking before emitting the JSON. Decide, emit, move on.
212
+ 2. Save/check the emitted block when it will drive a PDF, application gate, comparison, or batch dispatch: `npx job-forge score:check --input <score.json>` and, for apply decisions, `npx job-forge score:gate --input <score.json> --gate apply`.
213
+ 3. Write Blocks A-F **referencing** the scores by key (e.g., "Seniority fit: 3/5 Senior IC, no formal management"). Do NOT re-list all 10 dimensions in prose. Do NOT repeat the rationales verbatim.
214
+ 4. Do NOT narrate the scoring process in thinking before emitting the JSON. Decide, emit, move on.
214
215
 
215
216
  **Score interpretation (use consistently everywhere):**
216
217
  - **4.5-5.0** — Strong match. Generate PDF + draft answers. Apply promptly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-forge",
3
- "version": "2.14.31",
3
+ "version": "2.14.32",
4
4
  "description": "AI-powered job search pipeline built on opencode",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,6 +62,12 @@
62
62
  "facts:has": "node bin/job-forge.mjs facts:has",
63
63
  "facts:query": "node bin/job-forge.mjs facts:query",
64
64
  "facts:explain": "node bin/job-forge.mjs facts:explain",
65
+ "score:compute": "node bin/job-forge.mjs score:compute",
66
+ "score:verify": "node bin/job-forge.mjs score:verify",
67
+ "score:check": "node bin/job-forge.mjs score:check",
68
+ "score:gate": "node bin/job-forge.mjs score:gate",
69
+ "score:compare": "node bin/job-forge.mjs score:compare",
70
+ "score:explain": "node bin/job-forge.mjs score:explain",
65
71
  "canon:normalize": "node bin/job-forge.mjs canon:normalize",
66
72
  "canon:key": "node bin/job-forge.mjs canon:key",
67
73
  "canon:compare": "node bin/job-forge.mjs canon:compare",
@@ -160,6 +166,7 @@
160
166
  "@razroo/iso-postflight": "^0.1.0",
161
167
  "@razroo/iso-preflight": "^0.1.0",
162
168
  "@razroo/iso-redact": "^0.1.0",
169
+ "@razroo/iso-score": "^0.1.0",
163
170
  "@razroo/iso-trace": "^0.4.0",
164
171
  "playwright": "^1.58.1"
165
172
  },
@@ -19,6 +19,7 @@ const checks = [
19
19
  ["H5 blocks same-company concurrent retry", () => every(files.instructions, ["Re-dispatch the same company only AFTER", "previous subagent returns"])],
20
20
  ["H6 requires merge and verify", () => every(files.instructions, ["batch/tracker-additions/*.tsv", "npx job-forge merge", "npx job-forge verify"])],
21
21
  ["H7 distrusts subagent prose", () => every(files.instructions, ["must originate from a file", "not from prior subagent prose"])],
22
+ ["score policy points to local helper", () => every(files.instructions, ["[D19]", "templates/score.json", "npx job-forge score:check", "npx job-forge score:gate"])],
22
23
  ["shared prompt points to on-demand references", () => every(files.instructions, ["modes/{mode}.md", "modes/reference-setup.md", "modes/reference-portals.md", "modes/reference-geometra.md"])],
23
24
  ["apply mode owns high-stakes upgrade", () => every(files.apply, ["[D8]", "@general-paid", "4.0/5", "high-stakes"])],
24
25
  ["apply mode blocks provider auto-downgrade", () => every(files.apply, ["[D9]", "do not auto-downgrade", "inspect telemetry before retrying"])],
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFileSync } from 'fs';
4
+ import { relative, resolve } from 'path';
5
+ import {
6
+ formatCheckResult,
7
+ formatComparison,
8
+ formatConfigSummary,
9
+ formatGateResult,
10
+ formatScoreResult,
11
+ formatVerifyResult,
12
+ } from '@razroo/iso-score';
13
+ import { PROJECT_DIR } from '../tracker-lib.mjs';
14
+ import {
15
+ checkJobForgeScore,
16
+ compareJobForgeScores,
17
+ computeJobForgeScore,
18
+ evaluateJobForgeScoreGate,
19
+ jobForgeScoreConfigPath,
20
+ readJobForgeScoreConfig,
21
+ readJsonFile,
22
+ verifyJobForgeScoreResult,
23
+ } from '../lib/jobforge-score.mjs';
24
+
25
+ const USAGE = `job-forge score - deterministic JobForge offer scoring
26
+
27
+ Usage:
28
+ job-forge score:compute --input <file> [--out <file>] [--profile jobforge] [--json]
29
+ job-forge score:check --input <file> [--profile jobforge] [--json]
30
+ job-forge score:gate --input <file> [--gate apply] [--profile jobforge] [--json]
31
+ job-forge score:verify --score <file> [--json]
32
+ job-forge score:compare --left <file> --right <file> [--profile jobforge] [--json]
33
+ job-forge score:explain [--profile jobforge] [--json]
34
+ job-forge score:path
35
+
36
+ Default config is templates/score.json. The input may be native iso-score
37
+ JSON or JobForge's existing report score JSON shape with a top-level scores map.`;
38
+
39
+ const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
40
+ const opts = parseArgs(rawArgs);
41
+
42
+ if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
43
+ console.log(USAGE);
44
+ process.exit(0);
45
+ }
46
+
47
+ try {
48
+ if (cmd === 'path') {
49
+ console.log(jobForgeScoreConfigPath(PROJECT_DIR));
50
+ } else if (cmd === 'compute') {
51
+ compute(opts);
52
+ } else if (cmd === 'check') {
53
+ check(opts);
54
+ } else if (cmd === 'gate') {
55
+ gate(opts);
56
+ } else if (cmd === 'verify') {
57
+ verify(opts);
58
+ } else if (cmd === 'compare') {
59
+ compare(opts);
60
+ } else if (cmd === 'explain') {
61
+ explain(opts);
62
+ } else {
63
+ console.error(`unknown score command "${cmd}"\n`);
64
+ console.error(USAGE);
65
+ process.exit(2);
66
+ }
67
+ } catch (error) {
68
+ console.error(error instanceof Error ? error.message : String(error));
69
+ process.exit(1);
70
+ }
71
+
72
+ function parseArgs(args) {
73
+ const opts = {
74
+ json: false,
75
+ help: false,
76
+ profile: '',
77
+ gate: '',
78
+ input: '',
79
+ score: '',
80
+ left: '',
81
+ right: '',
82
+ out: '',
83
+ };
84
+
85
+ for (let i = 0; i < args.length; i++) {
86
+ const arg = args[i];
87
+ if (arg === '--json') {
88
+ opts.json = true;
89
+ } else if (arg === '--profile') {
90
+ opts.profile = valueAfter(args, ++i, '--profile');
91
+ } else if (arg.startsWith('--profile=')) {
92
+ opts.profile = arg.slice('--profile='.length);
93
+ } else if (arg === '--gate') {
94
+ opts.gate = valueAfter(args, ++i, '--gate');
95
+ } else if (arg.startsWith('--gate=')) {
96
+ opts.gate = arg.slice('--gate='.length);
97
+ } else if (arg === '--input') {
98
+ opts.input = valueAfter(args, ++i, '--input');
99
+ } else if (arg.startsWith('--input=')) {
100
+ opts.input = arg.slice('--input='.length);
101
+ } else if (arg === '--score') {
102
+ opts.score = valueAfter(args, ++i, '--score');
103
+ } else if (arg.startsWith('--score=')) {
104
+ opts.score = arg.slice('--score='.length);
105
+ } else if (arg === '--left') {
106
+ opts.left = valueAfter(args, ++i, '--left');
107
+ } else if (arg.startsWith('--left=')) {
108
+ opts.left = arg.slice('--left='.length);
109
+ } else if (arg === '--right') {
110
+ opts.right = valueAfter(args, ++i, '--right');
111
+ } else if (arg.startsWith('--right=')) {
112
+ opts.right = arg.slice('--right='.length);
113
+ } else if (arg === '--out') {
114
+ opts.out = valueAfter(args, ++i, '--out');
115
+ } else if (arg.startsWith('--out=')) {
116
+ opts.out = arg.slice('--out='.length);
117
+ } else if (arg === '--help' || arg === '-h') {
118
+ opts.help = true;
119
+ } else {
120
+ throw new Error(`unknown flag "${arg}"`);
121
+ }
122
+ }
123
+
124
+ return opts;
125
+ }
126
+
127
+ function valueAfter(values, index, flag) {
128
+ const value = values[index];
129
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
130
+ return value;
131
+ }
132
+
133
+ function compute(opts) {
134
+ if (!opts.input) throw new Error('score:compute requires --input');
135
+ const result = computeJobForgeScore(readJsonFile(resolve(opts.input)), { profile: opts.profile }, PROJECT_DIR);
136
+ if (opts.out) writeFileSync(resolve(opts.out), `${JSON.stringify(result, null, 2)}\n`, 'utf8');
137
+ if (opts.json) {
138
+ console.log(JSON.stringify(result, null, 2));
139
+ } else {
140
+ console.log(formatScoreResult(result));
141
+ }
142
+ }
143
+
144
+ function check(opts) {
145
+ if (!opts.input) throw new Error('score:check requires --input');
146
+ const result = checkJobForgeScore(readJsonFile(resolve(opts.input)), { profile: opts.profile }, PROJECT_DIR);
147
+ if (opts.json) {
148
+ console.log(JSON.stringify(result, null, 2));
149
+ } else {
150
+ console.log(formatCheckResult(result));
151
+ }
152
+ process.exit(result.ok ? 0 : 1);
153
+ }
154
+
155
+ function gate(opts) {
156
+ if (!opts.input) throw new Error('score:gate requires --input');
157
+ const result = evaluateJobForgeScoreGate(readJsonFile(resolve(opts.input)), {
158
+ gate: opts.gate || 'apply',
159
+ profile: opts.profile,
160
+ }, PROJECT_DIR);
161
+ if (opts.json) {
162
+ console.log(JSON.stringify(result, null, 2));
163
+ } else {
164
+ console.log(formatGateResult(result));
165
+ }
166
+ process.exit(result.ok ? 0 : 1);
167
+ }
168
+
169
+ function verify(opts) {
170
+ if (!opts.score) throw new Error('score:verify requires --score');
171
+ const result = verifyJobForgeScoreResult(readJsonFile(resolve(opts.score)));
172
+ if (opts.json) {
173
+ console.log(JSON.stringify(result, null, 2));
174
+ } else {
175
+ console.log(formatVerifyResult(result));
176
+ }
177
+ process.exit(result.ok ? 0 : 1);
178
+ }
179
+
180
+ function compare(opts) {
181
+ if (!opts.left) throw new Error('score:compare requires --left');
182
+ if (!opts.right) throw new Error('score:compare requires --right');
183
+ const result = compareJobForgeScores(
184
+ readJsonFile(resolve(opts.left)),
185
+ readJsonFile(resolve(opts.right)),
186
+ { profile: opts.profile },
187
+ PROJECT_DIR,
188
+ );
189
+ if (opts.json) {
190
+ console.log(JSON.stringify(result, null, 2));
191
+ } else {
192
+ console.log(formatComparison(result));
193
+ }
194
+ const hasErrors = [...result.left.issues, ...result.right.issues].some((issue) => issue.severity === 'error');
195
+ process.exit(hasErrors ? 1 : 0);
196
+ }
197
+
198
+ function explain(opts) {
199
+ const config = readJobForgeScoreConfig(PROJECT_DIR);
200
+ if (opts.json) {
201
+ const value = opts.profile
202
+ ? { ...config, profiles: config.profiles.filter((profile) => profile.name === opts.profile) }
203
+ : config;
204
+ console.log(JSON.stringify(value, null, 2));
205
+ } else {
206
+ console.log(`config: ${relative(PROJECT_DIR, jobForgeScoreConfigPath(PROJECT_DIR))}`);
207
+ console.log(formatConfigSummary(config, opts.profile || undefined));
208
+ }
209
+ }
@@ -40,6 +40,12 @@
40
40
  "facts:has": "job-forge facts:has",
41
41
  "facts:query": "job-forge facts:query",
42
42
  "facts:explain": "job-forge facts:explain",
43
+ "score:compute": "job-forge score:compute",
44
+ "score:verify": "job-forge score:verify",
45
+ "score:check": "job-forge score:check",
46
+ "score:gate": "job-forge score:gate",
47
+ "score:compare": "job-forge score:compare",
48
+ "score:explain": "job-forge score:explain",
43
49
  "canon:normalize": "job-forge canon:normalize",
44
50
  "canon:key": "job-forge canon:key",
45
51
  "canon:compare": "job-forge canon:compare",
@@ -0,0 +1,128 @@
1
+ {
2
+ "version": 1,
3
+ "profiles": [
4
+ {
5
+ "name": "jobforge",
6
+ "description": "Canonical JobForge job-fit rubric. Agents choose dimension scores; iso-score owns weighted math, bands, and gates.",
7
+ "scale": {
8
+ "min": 1,
9
+ "max": 5,
10
+ "precision": 1
11
+ },
12
+ "dimensions": [
13
+ {
14
+ "id": "north_star",
15
+ "label": "North Star alignment",
16
+ "weight": 0.25,
17
+ "required": true
18
+ },
19
+ {
20
+ "id": "cv_match",
21
+ "label": "CV match",
22
+ "weight": 0.15,
23
+ "required": true
24
+ },
25
+ {
26
+ "id": "seniority_fit",
27
+ "label": "Seniority fit",
28
+ "weight": 0.15,
29
+ "required": true
30
+ },
31
+ {
32
+ "id": "comp",
33
+ "label": "Comp estimate",
34
+ "weight": 0.1,
35
+ "required": true
36
+ },
37
+ {
38
+ "id": "growth",
39
+ "label": "Growth trajectory",
40
+ "weight": 0.1,
41
+ "required": true
42
+ },
43
+ {
44
+ "id": "remote",
45
+ "label": "Remote quality",
46
+ "weight": 0.05,
47
+ "required": true
48
+ },
49
+ {
50
+ "id": "company",
51
+ "label": "Company reputation",
52
+ "weight": 0.05,
53
+ "required": true
54
+ },
55
+ {
56
+ "id": "stack",
57
+ "label": "Tech stack modernity",
58
+ "weight": 0.05,
59
+ "required": true
60
+ },
61
+ {
62
+ "id": "speed",
63
+ "label": "Speed to offer",
64
+ "weight": 0.05,
65
+ "required": true
66
+ },
67
+ {
68
+ "id": "culture",
69
+ "label": "Cultural signals",
70
+ "weight": 0.05,
71
+ "required": true
72
+ }
73
+ ],
74
+ "bands": [
75
+ {
76
+ "id": "strong",
77
+ "label": "Strong match",
78
+ "min": 4.5
79
+ },
80
+ {
81
+ "id": "apply",
82
+ "label": "Apply",
83
+ "min": 3.5
84
+ },
85
+ {
86
+ "id": "apply_with_caveats",
87
+ "label": "Apply with caveats",
88
+ "min": 3
89
+ },
90
+ {
91
+ "id": "skip",
92
+ "label": "Skip",
93
+ "min": 1
94
+ }
95
+ ],
96
+ "gates": [
97
+ {
98
+ "id": "pdf",
99
+ "label": "Generate PDF",
100
+ "min": 3,
101
+ "blockOnMissingRequired": true,
102
+ "blockOnIssues": true
103
+ },
104
+ {
105
+ "id": "draft_answers",
106
+ "label": "Draft application answers",
107
+ "min": 3.5,
108
+ "blockOnMissingRequired": true,
109
+ "blockOnIssues": true
110
+ },
111
+ {
112
+ "id": "apply",
113
+ "label": "Apply or queue application",
114
+ "min": 3,
115
+ "blockOnMissingRequired": true,
116
+ "blockOnIssues": true
117
+ },
118
+ {
119
+ "id": "strong",
120
+ "label": "Strong match",
121
+ "min": 4.5,
122
+ "requireBand": "strong",
123
+ "blockOnIssues": true
124
+ }
125
+ ]
126
+ }
127
+ ]
128
+ }