job-forge 2.14.27 → 2.14.29

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.
@@ -83,18 +83,24 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
83
83
  - [D15] Treat `templates/canon.json` as the source of truth for URL/company/role identity keys. Use `npx job-forge canon:key ...` or `npx job-forge canon:compare ...` before broad duplicate checks when a stable key or same/possible/different decision is useful.
84
84
  why: `iso-canon` is not an MCP and adds no prompt/tool-schema tokens; it centralizes duplicate-key rules so agents do not repeatedly derive inconsistent slugs for aliases, suffixes, remote/location noise, or tracking URLs
85
85
 
86
+ - [D16] Treat `templates/preflight.json` as the source of truth for multi-apply dispatch safety. After candidate facts and gates are materialized from authoritative files, run `npx job-forge preflight:plan --candidates <file>` or `npx job-forge preflight:check --candidates <file>` before task dispatch; follow the emitted rounds and pre/post steps. This does not replace H2 four-source grep until those facts are materialized into the candidate JSON.
87
+ why: `iso-preflight` is not an MCP and adds no prompt/tool-schema tokens; it turns file-backed facts, duplicate/location gates, max-two rounds, and cleanup/merge/verify steps into an executable local plan instead of repeated prose
88
+
89
+ - [D17] Treat `templates/postflight.json` as the source of truth for multi-apply dispatch settlement. Save the JSON preflight plan and per-round observed dispatch/outcome/artifact records, then run `npx job-forge postflight:status --plan <plan.json> --outcomes <outcomes.json>` after each round and `npx job-forge postflight:check ...` after merge/verify. Follow its next action instead of inferring completion from subagent prose.
90
+ why: `iso-postflight` is not an MCP and adds no prompt/tool-schema tokens; it makes "round complete", missing TSVs, failed candidates, replacements, merge, and verify an executable local gate instead of repeated orchestration prose
91
+
86
92
  ## Procedure
87
93
 
88
94
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
89
95
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
90
96
  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 canonical identity keys for duplicate checks [D15]. Use migration checks for harness drift [D14]. Decide inline vs delegated work [D1].
91
- 4. Prepare Geometra dispatches: cleanup [H3], canon/index/ledger prefilter when useful [D8, D13, D15], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
92
- 5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
97
+ 4. Prepare Geometra dispatches: cleanup [H3], canon/index/ledger prefilter when useful [D8, D13, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
98
+ 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].
93
99
  6. Keep multi-job form-filling out of the orchestrator [H4].
94
100
  7. Cross-check subagent facts against authoritative files [H7].
95
101
  8. Apply score gate [D4].
96
102
  9. Merge contract-validated TSV outcomes [H6, D9].
97
- 10. Verify tracker before ending [H6].
103
+ 10. Verify tracker and run postflight check before ending [H6, D17].
98
104
 
99
105
  ## Routing
100
106
 
@@ -82,6 +82,14 @@ Identity keys (terminal, outside opencode):
82
82
  npx job-forge canon:key company-role --company "Acme" --role "Staff Engineer"
83
83
  npx job-forge canon:compare company "OpenAI, Inc." "Open AI"
84
84
 
85
+ Preflight dispatch plans (terminal, outside opencode):
86
+ npx job-forge preflight:plan --candidates batch/preflight-candidates.json
87
+ npx job-forge preflight:check --candidates batch/preflight-candidates.json
88
+
89
+ Postflight dispatch settlement (terminal, outside opencode):
90
+ npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
91
+ npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
92
+
85
93
  Consumer migrations (terminal, outside opencode):
86
94
  npx job-forge migrate:plan # preview package.json/.gitignore drift
87
95
  npx job-forge migrate:apply # apply safe harness upgrade migrations
@@ -194,7 +202,19 @@ Step 3 — Pre-flight cleanup (once, before the loop)
194
202
  - geometra_list_sessions()
195
203
  - geometra_disconnect({ closeBrowser: true })
196
204
 
197
- Step 4 — Loop in rounds of 2 (Hard Limit #1)
205
+ Step 4 — Materialize and check the dispatch plan
206
+ - Write file-backed candidate facts/gates to batch/preflight-candidates.json
207
+ (or another explicit JSON file). Include source paths for company, role,
208
+ companyRoleKey, URL, score, duplicate/location gates, and any skip/block
209
+ decision.
210
+ - Run npx job-forge preflight:check --candidates <file> to fail on missing
211
+ sources or blocked gates, then npx job-forge preflight:plan --candidates
212
+ <file> --json > batch/preflight-plan.json to get the bounded round list.
213
+ - Follow the emitted rounds. Do not dispatch blocked candidates, and do not
214
+ replace H2's four-source grep with preflight unless those grep results are
215
+ present in the candidate JSON.
216
+
217
+ Step 5 — Loop in rounds of 2 (Hard Limit #1)
198
218
  for round in ceil(len(candidates) / 2):
199
219
  pair = candidates[round*2 : round*2 + 2]
200
220
  # If proxy is configured, do not paste proxy values into prompts.
@@ -208,18 +228,24 @@ Step 4 — Loop in rounds of 2 (Hard Limit #1)
208
228
  # A returned task/session id is only a launch receipt, not completion.
209
229
  # Do not create a "check task status" task; inspect tracker files or
210
230
  # iso-trace if the user asks for status later.
211
- # Read their return values, log outcomes
231
+ # Read their return values, log outcomes in batch/postflight-outcomes.json
232
+ # with candidateId, status, and a tracker-tsv artifact path for every
233
+ # terminal outcome.
234
+ npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
235
+ # Follow the emitted next action before dispatching the next round.
212
236
 
213
- Step 5 — Between rounds: clean sessions again
237
+ Step 6 — Between rounds: clean sessions again
214
238
  - geometra_list_sessions()
215
239
  - geometra_disconnect({ closeBrowser: true })
216
240
 
217
- Step 6 — After all rounds: reconcile outcomes (Hard Limit #6)
241
+ Step 7 — After all rounds: reconcile outcomes (Hard Limit #6)
218
242
  - bash: npx job-forge merge # consumes batch/tracker-additions/*.tsv into the day file
219
243
  - bash: npx job-forge verify # validates URL/status consistency
244
+ - Add merge/verify step observations to batch/postflight-outcomes.json
245
+ - bash: npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
220
246
  - Review output; if verify-pipeline reports issues, fix them before ending.
221
247
 
222
- Step 7 — Aggregate and report
248
+ Step 8 — Aggregate and report
223
249
  - Summarize: applied, skipped, failed
224
250
  - Do NOT re-dispatch failed jobs automatically. Report them to the user.
225
251
  ```
package/AGENTS.md CHANGED
@@ -78,18 +78,24 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
78
78
  - [D15] Treat `templates/canon.json` as the source of truth for URL/company/role identity keys. Use `npx job-forge canon:key ...` or `npx job-forge canon:compare ...` before broad duplicate checks when a stable key or same/possible/different decision is useful.
79
79
  why: `iso-canon` is not an MCP and adds no prompt/tool-schema tokens; it centralizes duplicate-key rules so agents do not repeatedly derive inconsistent slugs for aliases, suffixes, remote/location noise, or tracking URLs
80
80
 
81
+ - [D16] Treat `templates/preflight.json` as the source of truth for multi-apply dispatch safety. After candidate facts and gates are materialized from authoritative files, run `npx job-forge preflight:plan --candidates <file>` or `npx job-forge preflight:check --candidates <file>` before task dispatch; follow the emitted rounds and pre/post steps. This does not replace H2 four-source grep until those facts are materialized into the candidate JSON.
82
+ why: `iso-preflight` is not an MCP and adds no prompt/tool-schema tokens; it turns file-backed facts, duplicate/location gates, max-two rounds, and cleanup/merge/verify steps into an executable local plan instead of repeated prose
83
+
84
+ - [D17] Treat `templates/postflight.json` as the source of truth for multi-apply dispatch settlement. Save the JSON preflight plan and per-round observed dispatch/outcome/artifact records, then run `npx job-forge postflight:status --plan <plan.json> --outcomes <outcomes.json>` after each round and `npx job-forge postflight:check ...` after merge/verify. Follow its next action instead of inferring completion from subagent prose.
85
+ why: `iso-postflight` is not an MCP and adds no prompt/tool-schema tokens; it makes "round complete", missing TSVs, failed candidates, replacements, merge, and verify an executable local gate instead of repeated orchestration prose
86
+
81
87
  ## Procedure
82
88
 
83
89
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
84
90
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
85
91
  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 canonical identity keys for duplicate checks [D15]. Use migration checks for harness drift [D14]. Decide inline vs delegated work [D1].
86
- 4. Prepare Geometra dispatches: cleanup [H3], canon/index/ledger prefilter when useful [D8, D13, D15], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
87
- 5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
92
+ 4. Prepare Geometra dispatches: cleanup [H3], canon/index/ledger prefilter when useful [D8, D13, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
93
+ 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].
88
94
  6. Keep multi-job form-filling out of the orchestrator [H4].
89
95
  7. Cross-check subagent facts against authoritative files [H7].
90
96
  8. Apply score gate [D4].
91
97
  9. Merge contract-validated TSV outcomes [H6, D9].
92
- 10. Verify tracker before ending [H6].
98
+ 10. Verify tracker and run postflight check before ending [H6, D17].
93
99
 
94
100
  ## Routing
95
101
 
package/CLAUDE.md CHANGED
@@ -78,18 +78,24 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
78
78
  - [D15] Treat `templates/canon.json` as the source of truth for URL/company/role identity keys. Use `npx job-forge canon:key ...` or `npx job-forge canon:compare ...` before broad duplicate checks when a stable key or same/possible/different decision is useful.
79
79
  why: `iso-canon` is not an MCP and adds no prompt/tool-schema tokens; it centralizes duplicate-key rules so agents do not repeatedly derive inconsistent slugs for aliases, suffixes, remote/location noise, or tracking URLs
80
80
 
81
+ - [D16] Treat `templates/preflight.json` as the source of truth for multi-apply dispatch safety. After candidate facts and gates are materialized from authoritative files, run `npx job-forge preflight:plan --candidates <file>` or `npx job-forge preflight:check --candidates <file>` before task dispatch; follow the emitted rounds and pre/post steps. This does not replace H2 four-source grep until those facts are materialized into the candidate JSON.
82
+ why: `iso-preflight` is not an MCP and adds no prompt/tool-schema tokens; it turns file-backed facts, duplicate/location gates, max-two rounds, and cleanup/merge/verify steps into an executable local plan instead of repeated prose
83
+
84
+ - [D17] Treat `templates/postflight.json` as the source of truth for multi-apply dispatch settlement. Save the JSON preflight plan and per-round observed dispatch/outcome/artifact records, then run `npx job-forge postflight:status --plan <plan.json> --outcomes <outcomes.json>` after each round and `npx job-forge postflight:check ...` after merge/verify. Follow its next action instead of inferring completion from subagent prose.
85
+ why: `iso-postflight` is not an MCP and adds no prompt/tool-schema tokens; it makes "round complete", missing TSVs, failed candidates, replacements, merge, and verify an executable local gate instead of repeated orchestration prose
86
+
81
87
  ## Procedure
82
88
 
83
89
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
84
90
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
85
91
  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 canonical identity keys for duplicate checks [D15]. Use migration checks for harness drift [D14]. Decide inline vs delegated work [D1].
86
- 4. Prepare Geometra dispatches: cleanup [H3], canon/index/ledger prefilter when useful [D8, D13, D15], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
87
- 5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
92
+ 4. Prepare Geometra dispatches: cleanup [H3], canon/index/ledger prefilter when useful [D8, D13, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
93
+ 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].
88
94
  6. Keep multi-job form-filling out of the orchestrator [H4].
89
95
  7. Cross-check subagent facts against authoritative files [H7].
90
96
  8. Apply score gate [D4].
91
97
  9. Merge contract-validated TSV outcomes [H6, D9].
92
- 10. Verify tracker before ending [H6].
98
+ 10. Verify tracker and run postflight check before ending [H6, D17].
93
99
 
94
100
  ## Routing
95
101
 
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/migrations.json` defines safe consumer-project upgrades via `@razroo/iso-migrate`, `.jobforge-ledger/events.jsonl` records duplicate/status events via `@razroo/iso-ledger`, `.jobforge-cache/` stores reusable JD/artifact content via `@razroo/iso-cache`, and `.jobforge-index.json` indexes artifact source pointers via `@razroo/iso-index`. None of these add always-on prompt or tool-schema tokens.
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/migrations.json` defines safe consumer-project upgrades via `@razroo/iso-migrate`, `.jobforge-ledger/events.jsonl` records duplicate/status events via `@razroo/iso-ledger`, `.jobforge-cache/` stores reusable JD/artifact content via `@razroo/iso-cache`, and `.jobforge-index.json` indexes artifact source pointers via `@razroo/iso-index`. 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
 
@@ -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 + 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, and `job-forge migrate:*` applies safe consumer-project upgrades without MCP/tool-schema overhead. |
81
+ | **Trace + Telemetry + Guard + Contract + Canon + Ledger + Capabilities + Context + Cache + Index + Preflight + Postflight + 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 preflight:*` plans bounded apply dispatch rounds from file-backed candidate facts, `job-forge postflight:*` settles dispatch outcomes/artifacts/post-steps, 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
@@ -164,7 +164,7 @@ my-search/
164
164
  ├── .opencode/skills/job-forge.md # → skill router
165
165
  ├── .opencode/agents/ # → @general-free, @general-paid, @glm-minimal
166
166
  ├── modes/ # → _shared.md + skill modes
167
- ├── templates/ # → states.yml, portals.example.yml, cv-template.html, canon.json, capabilities.json, context.json, index.json, migrations.json
167
+ ├── templates/ # → states.yml, portals.example.yml, cv-template.html, canon.json, capabilities.json, context.json, index.json, preflight.json, postflight.json, migrations.json
168
168
  ├── batch/batch-prompt.md # → batch worker prompt
169
169
  ├── batch/batch-runner.sh # → parallel orchestrator
170
170
 
@@ -190,7 +190,7 @@ JobForge/
190
190
  │ ├── sync.mjs # postinstall: creates symlinks in consumer project
191
191
  │ └── create-job-forge.mjs # scaffolder
192
192
  ├── modes/ # _shared.md + 16 skill modes
193
- ├── templates/ # cv-template.html, portals.example.yml, states.yml, canon.json, capabilities.json, context.json, migrations.json
193
+ ├── templates/ # cv-template.html, portals.example.yml, states.yml, canon.json, capabilities.json, context.json, preflight.json, postflight.json, migrations.json
194
194
  ├── config/profile.example.yml # template for consumer's profile.yml
195
195
  ├── batch/{batch-prompt.md,batch-runner.sh} # batch orchestrator
196
196
  ├── scripts/
@@ -202,6 +202,8 @@ JobForge/
202
202
  │ ├── cache.mjs # iso-cache-backed local artifact cache CLI
203
203
  │ ├── index.mjs # iso-index-backed artifact lookup CLI
204
204
  │ ├── canon.mjs # iso-canon-backed identity normalization CLI
205
+ │ ├── preflight.mjs # iso-preflight-backed dispatch planning CLI
206
+ │ ├── postflight.mjs # iso-postflight-backed dispatch settlement CLI
205
207
  │ ├── migrate.mjs # iso-migrate-backed consumer-project migrations
206
208
  │ ├── token-usage-report.mjs # opencode cost analyzer
207
209
  │ └── release/check-source.mjs # version gate for npm publish
@@ -151,6 +151,12 @@ const consumerPkg = {
151
151
  'canon:key': 'job-forge canon:key',
152
152
  'canon:compare': 'job-forge canon:compare',
153
153
  'canon:explain': 'job-forge canon:explain',
154
+ 'preflight:plan': 'job-forge preflight:plan',
155
+ 'preflight:check': 'job-forge preflight:check',
156
+ 'preflight:explain': 'job-forge preflight:explain',
157
+ 'postflight:status': 'job-forge postflight:status',
158
+ 'postflight:check': 'job-forge postflight:check',
159
+ 'postflight:explain': 'job-forge postflight:explain',
154
160
  'migrate:plan': 'job-forge migrate:plan',
155
161
  'migrate:apply': 'job-forge migrate:apply',
156
162
  'migrate:check': 'job-forge migrate:check',
@@ -257,6 +263,8 @@ Before doing any work, remember where things live in *this* project:
257
263
  | Local workflow ledger | \`.jobforge-ledger/events.jsonl\` | Deterministic append-only state; use \`job-forge ledger:*\` |
258
264
  | Local artifact index | \`.jobforge-index.json\` | Deterministic file/line lookup; use \`job-forge index:*\` |
259
265
  | Identity canonicalization | \`templates/canon.json\` | Stable URL/company/role keys; use \`job-forge canon:*\` |
266
+ | Dispatch preflight policy | \`templates/preflight.json\` | Safe apply rounds/gates; use \`job-forge preflight:*\` |
267
+ | Dispatch postflight policy | \`templates/postflight.json\` | Safe apply settlement; use \`job-forge postflight:*\` |
260
268
  | Consumer migrations | \`templates/migrations.json\` | Safe script/gitignore upgrades; use \`job-forge migrate:*\` |
261
269
  | Scanner config | \`portals.yml\` (project root) | Company configs |
262
270
  | Profile / identity | \`config/profile.yml\` | Candidate name, email, target roles |
@@ -269,7 +277,7 @@ Before doing any work, remember where things live in *this* project:
269
277
  | Batch input / state | \`batch/batch-input.tsv\`, \`batch/batch-state.tsv\` | Personal data |
270
278
  | Generated reports | \`reports/{###}-{company-slug}-{YYYY-MM-DD}.md\` | Gitignored |
271
279
  | Generated PDFs | \`output/\` | Gitignored |
272
- | Templates | \`templates/\` (symlink) | \`cv-template.html\`, \`portals.example.yml\`, \`states.yml\` |
280
+ | Templates | \`templates/\` (symlink) | \`cv-template.html\`, \`portals.example.yml\`, \`states.yml\`, runtime policies |
273
281
  | Harness rules | \`AGENTS.harness.md\` (symlink) | Shared operational guide, loaded via \`opencode.json:instructions\` |
274
282
  | Harness source | \`node_modules/job-forge/\` | Read this for harness internals |
275
283
 
@@ -354,6 +362,9 @@ reports/
354
362
  batch/batch-state.tsv
355
363
  batch/batch-state.tsv.bak
356
364
  batch/batch-input.tsv
365
+ batch/preflight-candidates.json
366
+ batch/preflight-plan.json
367
+ batch/postflight-outcomes.json
357
368
  batch/tracker-additions/
358
369
  !batch/tracker-additions/.gitkeep
359
370
  batch/logs/
@@ -408,6 +419,8 @@ job-forge verify # verify pipeline integrity
408
419
  job-forge ledger:status # local deterministic workflow ledger status
409
420
  job-forge index:status # local artifact index status
410
421
  job-forge canon:key company-role --company "Acme, Inc." --role "Senior SWE"
422
+ job-forge preflight:plan --candidates batch/preflight-candidates.json
423
+ job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
411
424
  job-forge migrate:check # verify consumer package scripts/gitignore are current
412
425
  job-forge pdf cv.md out.pdf
413
426
  job-forge tokens --days 1 # per-session opencode token usage
package/bin/job-forge.mjs CHANGED
@@ -26,6 +26,8 @@
26
26
  * cache:* Reuse local deterministic artifacts via iso-cache
27
27
  * index:* Query local artifacts via iso-index
28
28
  * canon:* Compute deterministic identity keys via iso-canon
29
+ * preflight:* Plan safe dispatch rounds via iso-preflight
30
+ * postflight:* Settle dispatch outcomes via iso-postflight
29
31
  * migrate:* Apply deterministic consumer-project migrations via iso-migrate
30
32
  * sync Re-run the harness symlink sync (bin/sync.mjs)
31
33
  * help, --help Show this message
@@ -137,6 +139,20 @@ const canonAliases = {
137
139
  'canon:path': 'path',
138
140
  };
139
141
 
142
+ const preflightAliases = {
143
+ 'preflight:plan': 'plan',
144
+ 'preflight:check': 'check',
145
+ 'preflight:explain': 'explain',
146
+ 'preflight:path': 'path',
147
+ };
148
+
149
+ const postflightAliases = {
150
+ 'postflight:status': 'status',
151
+ 'postflight:check': 'check',
152
+ 'postflight:explain': 'explain',
153
+ 'postflight:path': 'path',
154
+ };
155
+
140
156
  const migrateAliases = {
141
157
  'migrate:plan': 'plan',
142
158
  'migrate:apply': 'apply',
@@ -198,6 +214,12 @@ Commands:
198
214
  canon:key Print stable URL/company/role/company-role keys
199
215
  canon:compare Compare two identifiers as same/possible/different
200
216
  canon:explain Show the active identity canonicalization policy
217
+ preflight:plan Build bounded dispatch plan from candidate JSON
218
+ preflight:check Fail if preflight candidates are blocked
219
+ preflight:explain Show the active preflight workflow policy
220
+ postflight:status Reconcile dispatch plan, outcomes, artifacts, and post-steps
221
+ postflight:check Fail unless a dispatched workflow is fully settled
222
+ postflight:explain Show the active postflight workflow policy
201
223
  migrate:plan Preview deterministic consumer-project migrations
202
224
  migrate:apply Apply deterministic consumer-project migrations
203
225
  migrate:check Fail if migrations are pending
@@ -242,6 +264,10 @@ Pass --help after a command to see its own flags, e.g.:
242
264
  job-forge index:query "acme"
243
265
  job-forge canon:key company-role --company "Acme, Inc." --role "Senior SWE - Remote US"
244
266
  job-forge canon:compare company "OpenAI, Inc." "Open AI"
267
+ job-forge preflight:plan --candidates batch/preflight-candidates.json
268
+ job-forge preflight:check --candidates batch/preflight-candidates.json
269
+ job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
270
+ job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
245
271
  job-forge migrate:check
246
272
  job-forge migrate:apply
247
273
 
@@ -388,6 +414,36 @@ if (cmd === 'canon' || canonAliases[cmd]) {
388
414
  process.exit(result.status ?? 1);
389
415
  }
390
416
 
417
+ if (cmd === 'preflight' || preflightAliases[cmd]) {
418
+ const preflightArgs = cmd === 'preflight'
419
+ ? (rest.length === 0 ? ['help'] : rest)
420
+ : [preflightAliases[cmd], ...rest];
421
+
422
+ const scriptPath = join(PKG_ROOT, 'scripts/preflight.mjs');
423
+ const result = spawnSync(process.execPath, [scriptPath, ...preflightArgs], {
424
+ stdio: 'inherit',
425
+ cwd: PROJECT_DIR,
426
+ env: process.env,
427
+ });
428
+
429
+ process.exit(result.status ?? 1);
430
+ }
431
+
432
+ if (cmd === 'postflight' || postflightAliases[cmd]) {
433
+ const postflightArgs = cmd === 'postflight'
434
+ ? (rest.length === 0 ? ['help'] : rest)
435
+ : [postflightAliases[cmd], ...rest];
436
+
437
+ const scriptPath = join(PKG_ROOT, 'scripts/postflight.mjs');
438
+ const result = spawnSync(process.execPath, [scriptPath, ...postflightArgs], {
439
+ stdio: 'inherit',
440
+ cwd: PROJECT_DIR,
441
+ env: process.env,
442
+ });
443
+
444
+ process.exit(result.status ?? 1);
445
+ }
446
+
391
447
  if (cmd === 'migrate' || migrateAliases[cmd]) {
392
448
  const migrateArgs = cmd === 'migrate'
393
449
  ? (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
35
+ ├── templates/ # → states.yml, portals.example.yml, cv-template.html, 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
@@ -166,6 +166,8 @@ jds/*.md → Saved job descriptions referenced from the pipelin
166
166
  templates/states.yml → Canonical status values
167
167
  templates/canon.json → Canonical URL/company/role identity keys
168
168
  templates/context.json → Deterministic mode/reference context bundle policy
169
+ templates/preflight.json → Safe apply dispatch rounds/gates policy
170
+ templates/postflight.json → Safe apply dispatch settlement policy
169
171
  templates/migrations.json → Safe consumer-project upgrade policy
170
172
  templates/cv-template.html → PDF generation template
171
173
  examples/*.md → Fictional layouts only (not read by scripts; see examples/README.md)
@@ -181,6 +183,8 @@ Create `data/pipeline.md` when you start using the URL inbox (`/job-forge pipeli
181
183
  - Ledger: `.jobforge-ledger/events.jsonl` (created by `job-forge ledger:rebuild`, `tracker-line --write`, or `merge`; gitignored personal state)
182
184
  - Index: `.jobforge-index.json` (created on demand by `job-forge index:*`; gitignored local lookup state)
183
185
  - Canon: `templates/canon.json` (identity rules inspected with `job-forge canon:*`)
186
+ - Preflight: `templates/preflight.json` (dispatch rounds/gates inspected with `job-forge preflight:*`)
187
+ - Postflight: `templates/postflight.json` (dispatch outcomes/artifacts/post-steps inspected with `job-forge postflight:*`)
184
188
  - Migrations: `templates/migrations.json` (applied by `job-forge sync` and inspectable with `job-forge migrate:*`)
185
189
  - Capabilities: `templates/capabilities.json` (role boundary policy inspected with `job-forge capabilities:*`)
186
190
  - Context: `templates/context.json` (mode/reference file bundles inspected with `job-forge context:*`)
@@ -229,6 +233,8 @@ Scripts maintain data consistency. In a consumer project they're invoked via the
229
233
  | `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 |
230
234
  | `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 |
231
235
  | `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 |
236
+ | `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 |
237
+ | `scripts/postflight.mjs` | `npx job-forge postflight:status` / `postflight:check` / `postflight:explain` | Deterministic `@razroo/iso-postflight` settlement for dispatch outcomes, required tracker TSV artifacts, and merge/verify post-steps |
232
238
  | `scripts/migrate.mjs` | `npx job-forge migrate:plan` / `migrate:apply` / `migrate:check` | Deterministic `@razroo/iso-migrate` consumer-project upgrades for scripts and generated-artifact ignores |
233
239
  | `tracker-lib.mjs` | _(library)_ | Shared helpers for reading/writing day-based tracker files — imported by merge/dedup/verify/normalize |
234
240
  | `bin/sync.mjs` | `npx job-forge sync` | Creates the harness symlinks in a consumer project and applies safe migrations (also runs as `postinstall`) |
@@ -158,6 +158,14 @@ Artifact lookup policy lives in `templates/index.json` and is built locally by `
158
158
 
159
159
  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.
160
160
 
161
+ ## JobForge preflight plans
162
+
163
+ Application dispatch policy lives in `templates/preflight.json` and is planned locally by `@razroo/iso-preflight`. After candidate facts and gate results have been materialized into JSON, use `job-forge preflight:plan --candidates <file>` to get bounded rounds and required pre/post steps, or `job-forge preflight:check --candidates <file>` to fail on missing source facts or blocked gates. This is not an MCP and does not add prompt or tool-schema tokens; it consumes only the candidate JSON you deliberately pass to it.
164
+
165
+ ## JobForge postflight settlement
166
+
167
+ Application settlement policy lives in `templates/postflight.json` and is checked locally by `@razroo/iso-postflight`. After each dispatch round, record observed candidate outcomes and tracker TSV artifact paths in `batch/postflight-outcomes.json`, then run `job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json` to get the next safe action. After `merge` and `verify`, add post-step observations and run `job-forge postflight:check ...` to fail unless the workflow is complete. This is not an MCP and does not add prompt or tool-schema tokens.
168
+
161
169
  ## JobForge consumer migrations
162
170
 
163
171
  Consumer-project migrations live in `templates/migrations.json` and are applied locally by `@razroo/iso-migrate`. `job-forge sync` applies safe migrations automatically after refreshing symlinks; use `JOB_FORGE_SKIP_MIGRATIONS=1` to opt out. Use `job-forge migrate:plan`, `job-forge migrate:apply`, and `job-forge migrate:check` to inspect or enforce script/gitignore drift explicitly. This is not an MCP and does not add prompt or tool-schema tokens.
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`, `ledger`, `canon`, `context`, `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`, `canon`, `context`, `preflight`, `postflight`, `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
@@ -132,6 +132,10 @@ From your project root, these commands maintain the tracker and pipeline checks.
132
132
  | Inspect context bundle budget | `npx job-forge context:plan apply` | `npm run context:plan -- apply` |
133
133
  | Inspect local JD/artifact cache | `npx job-forge cache:status` | `npm run cache:status` |
134
134
  | Inspect local artifact index | `npx job-forge index:status` | `npm run index:status` |
135
+ | Plan safe application dispatch rounds | `npx job-forge preflight:plan --candidates batch/preflight-candidates.json` | `npm run preflight:plan -- --candidates ...` |
136
+ | Fail on blocked preflight candidates | `npx job-forge preflight:check --candidates batch/preflight-candidates.json` | `npm run preflight:check -- --candidates ...` |
137
+ | Settle dispatch outcomes after a round | `npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json` | `npm run postflight:status -- --plan ... --outcomes ...` |
138
+ | Fail unless dispatch outcomes and post-steps are complete | `npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json` | `npm run postflight:check -- --plan ... --outcomes ...` |
135
139
  | Inspect pending consumer migrations | `npx job-forge migrate:plan` | `npm run migrate:plan` |
136
140
  | Map status column to canonical labels | `npx job-forge normalize` | `npm run normalize` |
137
141
  | Merge duplicate company/role rows | `npx job-forge dedup` | `npm run dedup` |
@@ -85,6 +85,14 @@ Identity keys (terminal, outside opencode):
85
85
  npx job-forge canon:key company-role --company "Acme" --role "Staff Engineer"
86
86
  npx job-forge canon:compare company "OpenAI, Inc." "Open AI"
87
87
 
88
+ Preflight dispatch plans (terminal, outside opencode):
89
+ npx job-forge preflight:plan --candidates batch/preflight-candidates.json
90
+ npx job-forge preflight:check --candidates batch/preflight-candidates.json
91
+
92
+ Postflight dispatch settlement (terminal, outside opencode):
93
+ npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
94
+ npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
95
+
88
96
  Consumer migrations (terminal, outside opencode):
89
97
  npx job-forge migrate:plan # preview package.json/.gitignore drift
90
98
  npx job-forge migrate:apply # apply safe harness upgrade migrations
@@ -197,7 +205,19 @@ Step 3 — Pre-flight cleanup (once, before the loop)
197
205
  - geometra_list_sessions()
198
206
  - geometra_disconnect({ closeBrowser: true })
199
207
 
200
- Step 4 — Loop in rounds of 2 (Hard Limit #1)
208
+ Step 4 — Materialize and check the dispatch plan
209
+ - Write file-backed candidate facts/gates to batch/preflight-candidates.json
210
+ (or another explicit JSON file). Include source paths for company, role,
211
+ companyRoleKey, URL, score, duplicate/location gates, and any skip/block
212
+ decision.
213
+ - Run npx job-forge preflight:check --candidates <file> to fail on missing
214
+ sources or blocked gates, then npx job-forge preflight:plan --candidates
215
+ <file> --json > batch/preflight-plan.json to get the bounded round list.
216
+ - Follow the emitted rounds. Do not dispatch blocked candidates, and do not
217
+ replace H2's four-source grep with preflight unless those grep results are
218
+ present in the candidate JSON.
219
+
220
+ Step 5 — Loop in rounds of 2 (Hard Limit #1)
201
221
  for round in ceil(len(candidates) / 2):
202
222
  pair = candidates[round*2 : round*2 + 2]
203
223
  # If proxy is configured, do not paste proxy values into prompts.
@@ -211,18 +231,24 @@ Step 4 — Loop in rounds of 2 (Hard Limit #1)
211
231
  # A returned task/session id is only a launch receipt, not completion.
212
232
  # Do not create a "check task status" task; inspect tracker files or
213
233
  # iso-trace if the user asks for status later.
214
- # Read their return values, log outcomes
234
+ # Read their return values, log outcomes in batch/postflight-outcomes.json
235
+ # with candidateId, status, and a tracker-tsv artifact path for every
236
+ # terminal outcome.
237
+ npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
238
+ # Follow the emitted next action before dispatching the next round.
215
239
 
216
- Step 5 — Between rounds: clean sessions again
240
+ Step 6 — Between rounds: clean sessions again
217
241
  - geometra_list_sessions()
218
242
  - geometra_disconnect({ closeBrowser: true })
219
243
 
220
- Step 6 — After all rounds: reconcile outcomes (Hard Limit #6)
244
+ Step 7 — After all rounds: reconcile outcomes (Hard Limit #6)
221
245
  - bash: npx job-forge merge # consumes batch/tracker-additions/*.tsv into the day file
222
246
  - bash: npx job-forge verify # validates URL/status consistency
247
+ - Add merge/verify step observations to batch/postflight-outcomes.json
248
+ - bash: npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
223
249
  - Review output; if verify-pipeline reports issues, fix them before ending.
224
250
 
225
- Step 7 — Aggregate and report
251
+ Step 8 — Aggregate and report
226
252
  - Summarize: applied, skipped, failed
227
253
  - Do NOT re-dispatch failed jobs automatically. Report them to the user.
228
254
  ```
@@ -78,18 +78,24 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
78
78
  - [D15] Treat `templates/canon.json` as the source of truth for URL/company/role identity keys. Use `npx job-forge canon:key ...` or `npx job-forge canon:compare ...` before broad duplicate checks when a stable key or same/possible/different decision is useful.
79
79
  why: `iso-canon` is not an MCP and adds no prompt/tool-schema tokens; it centralizes duplicate-key rules so agents do not repeatedly derive inconsistent slugs for aliases, suffixes, remote/location noise, or tracking URLs
80
80
 
81
+ - [D16] Treat `templates/preflight.json` as the source of truth for multi-apply dispatch safety. After candidate facts and gates are materialized from authoritative files, run `npx job-forge preflight:plan --candidates <file>` or `npx job-forge preflight:check --candidates <file>` before task dispatch; follow the emitted rounds and pre/post steps. This does not replace H2 four-source grep until those facts are materialized into the candidate JSON.
82
+ why: `iso-preflight` is not an MCP and adds no prompt/tool-schema tokens; it turns file-backed facts, duplicate/location gates, max-two rounds, and cleanup/merge/verify steps into an executable local plan instead of repeated prose
83
+
84
+ - [D17] Treat `templates/postflight.json` as the source of truth for multi-apply dispatch settlement. Save the JSON preflight plan and per-round observed dispatch/outcome/artifact records, then run `npx job-forge postflight:status --plan <plan.json> --outcomes <outcomes.json>` after each round and `npx job-forge postflight:check ...` after merge/verify. Follow its next action instead of inferring completion from subagent prose.
85
+ why: `iso-postflight` is not an MCP and adds no prompt/tool-schema tokens; it makes "round complete", missing TSVs, failed candidates, replacements, merge, and verify an executable local gate instead of repeated orchestration prose
86
+
81
87
  ## Procedure
82
88
 
83
89
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
84
90
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
85
91
  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 canonical identity keys for duplicate checks [D15]. Use migration checks for harness drift [D14]. Decide inline vs delegated work [D1].
86
- 4. Prepare Geometra dispatches: cleanup [H3], canon/index/ledger prefilter when useful [D8, D13, D15], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
87
- 5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
92
+ 4. Prepare Geometra dispatches: cleanup [H3], canon/index/ledger prefilter when useful [D8, D13, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
93
+ 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].
88
94
  6. Keep multi-job form-filling out of the orchestrator [H4].
89
95
  7. Cross-check subagent facts against authoritative files [H7].
90
96
  8. Apply score gate [D4].
91
97
  9. Merge contract-validated TSV outcomes [H6, D9].
92
- 10. Verify tracker before ending [H6].
98
+ 10. Verify tracker and run postflight check before ending [H6, D17].
93
99
 
94
100
  ## Routing
95
101
 
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import {
4
+ loadPostflightConfig,
5
+ parseJson,
6
+ settlePostflight,
7
+ } from '@razroo/iso-postflight';
8
+
9
+ export const POSTFLIGHT_CONFIG_FILE = 'templates/postflight.json';
10
+ export const POSTFLIGHT_WORKFLOW = 'jobforge.apply';
11
+
12
+ export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
13
+ return projectDir;
14
+ }
15
+
16
+ export function jobForgePostflightConfigPath(projectDir = resolveProjectDir()) {
17
+ return process.env.JOB_FORGE_POSTFLIGHT_CONFIG || join(projectDir, POSTFLIGHT_CONFIG_FILE);
18
+ }
19
+
20
+ export function readJobForgePostflightConfig(projectDir = resolveProjectDir()) {
21
+ const path = jobForgePostflightConfigPath(projectDir);
22
+ return loadPostflightConfig(parseJson(readFileSync(path, 'utf8'), path));
23
+ }
24
+
25
+ export function settleJobForgePostflight(planInput, observationsInput, options = {}, projectDir = resolveProjectDir()) {
26
+ return settlePostflight(readJobForgePostflightConfig(projectDir), planInput, observationsInput, {
27
+ workflow: options.workflow || POSTFLIGHT_WORKFLOW,
28
+ });
29
+ }
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import {
4
+ loadPreflightConfig,
5
+ parseJson,
6
+ planPreflight,
7
+ } from '@razroo/iso-preflight';
8
+
9
+ export const PREFLIGHT_CONFIG_FILE = 'templates/preflight.json';
10
+ export const PREFLIGHT_WORKFLOW = 'jobforge.apply';
11
+
12
+ export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
13
+ return projectDir;
14
+ }
15
+
16
+ export function jobForgePreflightConfigPath(projectDir = resolveProjectDir()) {
17
+ return process.env.JOB_FORGE_PREFLIGHT_CONFIG || join(projectDir, PREFLIGHT_CONFIG_FILE);
18
+ }
19
+
20
+ export function readJobForgePreflightConfig(projectDir = resolveProjectDir()) {
21
+ const path = jobForgePreflightConfigPath(projectDir);
22
+ return loadPreflightConfig(parseJson(readFileSync(path, 'utf8'), path));
23
+ }
24
+
25
+ export function planJobForgePreflight(candidateInput, options = {}, projectDir = resolveProjectDir()) {
26
+ return planPreflight(readJobForgePreflightConfig(projectDir), candidateInput, {
27
+ workflow: options.workflow || PREFLIGHT_WORKFLOW,
28
+ });
29
+ }
package/modes/apply.md CHANGED
@@ -188,11 +188,18 @@ Step 4 — For round in ceil(N/2):
188
188
  task(apply to pair[1]) # only if pair has 2
189
189
  # WAIT for both final outcomes. A session id is not completion.
190
190
  # Do not dispatch round N+1 while round N is still in flight.
191
- Step 5 — Between rounds: geometra_list_sessions() + geometra_disconnect({closeBrowser: true})
192
- Step 6 — Reconcile outcomes (Hard Limit #6):
191
+ Step 5 — After each round:
192
+ write/update batch/postflight-outcomes.json with candidateId, status,
193
+ and tracker-tsv artifact path for every terminal outcome.
194
+ bash: npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
195
+ follow the emitted next action before the next dispatch.
196
+ Step 6 — Between rounds: geometra_list_sessions() + geometra_disconnect({closeBrowser: true})
197
+ Step 7 — Reconcile outcomes (Hard Limit #6):
193
198
  bash: npx job-forge merge # TSVs → day file
194
199
  bash: npx job-forge verify # validate
195
- Step 7 — Summarize outcomes; do NOT auto-retry failures.
200
+ add merge/verify step observations to batch/postflight-outcomes.json
201
+ bash: npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
202
+ Step 8 — Summarize outcomes; do NOT auto-retry failures.
196
203
  ```
197
204
 
198
205
  If a subagent fails, report it in the summary and let the user decide whether to retry. Never auto-retry — re-running a submit step risks duplicate applications. If a subagent returns SKIP because it discovered a duplicate, treat that as a missed preflight check: finish the current round, then choose a replacement candidate only after re-running dedupe against all four sources.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-forge",
3
- "version": "2.14.27",
3
+ "version": "2.14.29",
4
4
  "description": "AI-powered job search pipeline built on opencode",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,6 +59,12 @@
59
59
  "canon:key": "node bin/job-forge.mjs canon:key",
60
60
  "canon:compare": "node bin/job-forge.mjs canon:compare",
61
61
  "canon:explain": "node bin/job-forge.mjs canon:explain",
62
+ "preflight:plan": "node bin/job-forge.mjs preflight:plan",
63
+ "preflight:check": "node bin/job-forge.mjs preflight:check",
64
+ "preflight:explain": "node bin/job-forge.mjs preflight:explain",
65
+ "postflight:status": "node bin/job-forge.mjs postflight:status",
66
+ "postflight:check": "node bin/job-forge.mjs postflight:check",
67
+ "postflight:explain": "node bin/job-forge.mjs postflight:explain",
62
68
  "migrate:plan": "node bin/job-forge.mjs migrate:plan",
63
69
  "migrate:apply": "node bin/job-forge.mjs migrate:apply",
64
70
  "migrate:check": "node bin/job-forge.mjs migrate:check",
@@ -129,9 +135,9 @@
129
135
  "node": ">=20.6.0"
130
136
  },
131
137
  "dependencies": {
132
- "@razroo/iso-capabilities": "^0.1.0",
133
138
  "@razroo/iso-cache": "^0.1.0",
134
139
  "@razroo/iso-canon": "^0.1.0",
140
+ "@razroo/iso-capabilities": "^0.1.0",
135
141
  "@razroo/iso-context": "^0.1.0",
136
142
  "@razroo/iso-contract": "^0.1.0",
137
143
  "@razroo/iso-guard": "^0.1.0",
@@ -139,6 +145,8 @@
139
145
  "@razroo/iso-ledger": "^0.1.0",
140
146
  "@razroo/iso-migrate": "^0.1.0",
141
147
  "@razroo/iso-orchestrator": "^0.1.0",
148
+ "@razroo/iso-postflight": "^0.1.0",
149
+ "@razroo/iso-preflight": "^0.1.0",
142
150
  "@razroo/iso-trace": "^0.4.0",
143
151
  "playwright": "^1.58.1"
144
152
  },
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'fs';
4
+ import { isAbsolute, relative, resolve } from 'path';
5
+ import {
6
+ formatConfigSummary,
7
+ formatPostflightResult,
8
+ loadPostflightConfig,
9
+ parseJson,
10
+ settlePostflight,
11
+ } from '@razroo/iso-postflight';
12
+ import { PROJECT_DIR } from '../tracker-lib.mjs';
13
+ import {
14
+ jobForgePostflightConfigPath,
15
+ readJobForgePostflightConfig,
16
+ settleJobForgePostflight,
17
+ } from '../lib/jobforge-postflight.mjs';
18
+
19
+ const USAGE = `job-forge postflight - deterministic dispatch settlement for JobForge
20
+
21
+ Usage:
22
+ job-forge postflight:status --plan <file> --outcomes <file> [--workflow jobforge.apply] [--json]
23
+ job-forge postflight:check --plan <file> --outcomes <file> [--workflow jobforge.apply] [--json]
24
+ job-forge postflight:explain [--json]
25
+ job-forge postflight:path
26
+
27
+ Plan files are JSON objects with rounds, such as the JSON output from
28
+ job-forge preflight:plan. Outcome files are JSON objects with dispatches,
29
+ outcomes, and post-step observations. The policy is templates/postflight.json.
30
+ This is local project state, not an MCP and not prompt context.`;
31
+
32
+ const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
33
+ const opts = parseArgs(rawArgs);
34
+
35
+ if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
36
+ console.log(USAGE);
37
+ process.exit(0);
38
+ }
39
+
40
+ try {
41
+ if (cmd === 'path') {
42
+ console.log(configPath(opts));
43
+ } else if (cmd === 'status' || cmd === 'check') {
44
+ runSettlement(cmd, opts);
45
+ } else if (cmd === 'explain') {
46
+ explain(opts);
47
+ } else {
48
+ console.error(`unknown postflight command "${cmd}"\n`);
49
+ console.error(USAGE);
50
+ process.exit(2);
51
+ }
52
+ } catch (error) {
53
+ console.error(error instanceof Error ? error.message : String(error));
54
+ process.exit(1);
55
+ }
56
+
57
+ function parseArgs(args) {
58
+ const opts = {
59
+ json: false,
60
+ help: false,
61
+ };
62
+
63
+ for (let i = 0; i < args.length; i++) {
64
+ const arg = args[i];
65
+ if (arg === '--json') {
66
+ opts.json = true;
67
+ } else if (arg === '--plan' || arg === '-p') {
68
+ opts.plan = valueAfter(args, ++i, arg);
69
+ } else if (arg.startsWith('--plan=')) {
70
+ opts.plan = arg.slice('--plan='.length);
71
+ } else if (arg === '--outcomes' || arg === '--observations' || arg === '-o') {
72
+ opts.outcomes = valueAfter(args, ++i, arg);
73
+ } else if (arg.startsWith('--outcomes=')) {
74
+ opts.outcomes = arg.slice('--outcomes='.length);
75
+ } else if (arg.startsWith('--observations=')) {
76
+ opts.outcomes = arg.slice('--observations='.length);
77
+ } else if (arg === '--workflow') {
78
+ opts.workflow = valueAfter(args, ++i, '--workflow');
79
+ } else if (arg.startsWith('--workflow=')) {
80
+ opts.workflow = arg.slice('--workflow='.length);
81
+ } else if (arg === '--config') {
82
+ opts.config = valueAfter(args, ++i, '--config');
83
+ } else if (arg.startsWith('--config=')) {
84
+ opts.config = arg.slice('--config='.length);
85
+ } else if (arg === '--help' || arg === '-h') {
86
+ opts.help = true;
87
+ } else {
88
+ throw new Error(`unknown flag "${arg}"`);
89
+ }
90
+ }
91
+
92
+ return opts;
93
+ }
94
+
95
+ function runSettlement(mode, opts) {
96
+ if (!opts.plan) throw new Error(`${mode} requires --plan <file>`);
97
+ if (!opts.outcomes) throw new Error(`${mode} requires --outcomes <file>`);
98
+ const plan = readJsonFile(resolveInputPath(opts.plan));
99
+ const observations = readJsonFile(resolveInputPath(opts.outcomes));
100
+ const result = opts.config
101
+ ? settlePostflight(readConfig(opts), plan, observations, { workflow: opts.workflow })
102
+ : settleJobForgePostflight(plan, observations, { workflow: opts.workflow }, PROJECT_DIR);
103
+
104
+ if (opts.json) {
105
+ console.log(JSON.stringify(result, null, 2));
106
+ } else {
107
+ console.log(formatPostflightResult(result, mode));
108
+ }
109
+
110
+ if (mode === 'check' && !result.ok) process.exit(1);
111
+ }
112
+
113
+ function explain(opts) {
114
+ const config = readConfig(opts);
115
+ if (opts.json) {
116
+ console.log(JSON.stringify(config, null, 2));
117
+ return;
118
+ }
119
+ console.log(`config: ${relativePath(configPath(opts))}`);
120
+ console.log(formatConfigSummary(config));
121
+ }
122
+
123
+ function readConfig(opts) {
124
+ if (opts.config) {
125
+ const path = resolveInputPath(opts.config);
126
+ return loadPostflightConfig(readJsonFile(path));
127
+ }
128
+ return readJobForgePostflightConfig(PROJECT_DIR);
129
+ }
130
+
131
+ function configPath(opts) {
132
+ return opts.config ? resolveInputPath(opts.config) : jobForgePostflightConfigPath(PROJECT_DIR);
133
+ }
134
+
135
+ function readJsonFile(path) {
136
+ return parseJson(readFileSync(path, 'utf8'), path);
137
+ }
138
+
139
+ function valueAfter(values, index, flag) {
140
+ const value = values[index];
141
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
142
+ return value;
143
+ }
144
+
145
+ function resolveInputPath(path) {
146
+ return isAbsolute(path) ? path : resolve(PROJECT_DIR, path);
147
+ }
148
+
149
+ function relativePath(path) {
150
+ return relative(PROJECT_DIR, path) || '.';
151
+ }
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'fs';
4
+ import { isAbsolute, relative, resolve } from 'path';
5
+ import {
6
+ formatConfigSummary,
7
+ formatPreflightPlan,
8
+ loadPreflightConfig,
9
+ parseJson,
10
+ planPreflight,
11
+ } from '@razroo/iso-preflight';
12
+ import { PROJECT_DIR } from '../tracker-lib.mjs';
13
+ import {
14
+ jobForgePreflightConfigPath,
15
+ planJobForgePreflight,
16
+ readJobForgePreflightConfig,
17
+ } from '../lib/jobforge-preflight.mjs';
18
+
19
+ const USAGE = `job-forge preflight - deterministic dispatch planning for JobForge
20
+
21
+ Usage:
22
+ job-forge preflight:plan --candidates <file> [--workflow jobforge.apply] [--json]
23
+ job-forge preflight:check --candidates <file> [--workflow jobforge.apply] [--json]
24
+ job-forge preflight:explain [--json]
25
+ job-forge preflight:path
26
+
27
+ Candidate files are JSON arrays, or objects with a candidates array. The policy
28
+ is templates/preflight.json. This is local project state, not an MCP and not
29
+ prompt context.`;
30
+
31
+ const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
32
+ const opts = parseArgs(rawArgs);
33
+
34
+ if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
35
+ console.log(USAGE);
36
+ process.exit(0);
37
+ }
38
+
39
+ try {
40
+ if (cmd === 'path') {
41
+ console.log(configPath(opts));
42
+ } else if (cmd === 'plan' || cmd === 'check') {
43
+ runPlan(cmd, opts);
44
+ } else if (cmd === 'explain') {
45
+ explain(opts);
46
+ } else {
47
+ console.error(`unknown preflight command "${cmd}"\n`);
48
+ console.error(USAGE);
49
+ process.exit(2);
50
+ }
51
+ } catch (error) {
52
+ console.error(error instanceof Error ? error.message : String(error));
53
+ process.exit(1);
54
+ }
55
+
56
+ function parseArgs(args) {
57
+ const opts = {
58
+ json: false,
59
+ help: false,
60
+ };
61
+
62
+ for (let i = 0; i < args.length; i++) {
63
+ const arg = args[i];
64
+ if (arg === '--json') {
65
+ opts.json = true;
66
+ } else if (arg === '--candidates' || arg === '-c') {
67
+ opts.candidates = valueAfter(args, ++i, arg);
68
+ } else if (arg.startsWith('--candidates=')) {
69
+ opts.candidates = arg.slice('--candidates='.length);
70
+ } else if (arg === '--workflow') {
71
+ opts.workflow = valueAfter(args, ++i, '--workflow');
72
+ } else if (arg.startsWith('--workflow=')) {
73
+ opts.workflow = arg.slice('--workflow='.length);
74
+ } else if (arg === '--config') {
75
+ opts.config = valueAfter(args, ++i, '--config');
76
+ } else if (arg.startsWith('--config=')) {
77
+ opts.config = arg.slice('--config='.length);
78
+ } else if (arg === '--help' || arg === '-h') {
79
+ opts.help = true;
80
+ } else {
81
+ throw new Error(`unknown flag "${arg}"`);
82
+ }
83
+ }
84
+
85
+ return opts;
86
+ }
87
+
88
+ function runPlan(mode, opts) {
89
+ if (!opts.candidates) throw new Error(`${mode} requires --candidates <file>`);
90
+ const candidates = readJsonFile(resolveInputPath(opts.candidates));
91
+ const result = opts.config
92
+ ? planPreflight(readConfig(opts), candidates, { workflow: opts.workflow })
93
+ : planJobForgePreflight(candidates, { workflow: opts.workflow }, PROJECT_DIR);
94
+
95
+ if (opts.json) {
96
+ console.log(JSON.stringify(result, null, 2));
97
+ } else {
98
+ console.log(formatPreflightPlan(result, mode));
99
+ }
100
+
101
+ if (mode === 'check' && !result.ok) process.exit(1);
102
+ }
103
+
104
+ function explain(opts) {
105
+ const config = readConfig(opts);
106
+ if (opts.json) {
107
+ console.log(JSON.stringify(config, null, 2));
108
+ return;
109
+ }
110
+ console.log(`config: ${relativePath(configPath(opts))}`);
111
+ console.log(formatConfigSummary(config));
112
+ }
113
+
114
+ function readConfig(opts) {
115
+ if (opts.config) {
116
+ const path = resolveInputPath(opts.config);
117
+ return loadPreflightConfig(readJsonFile(path));
118
+ }
119
+ return readJobForgePreflightConfig(PROJECT_DIR);
120
+ }
121
+
122
+ function configPath(opts) {
123
+ return opts.config ? resolveInputPath(opts.config) : jobForgePreflightConfigPath(PROJECT_DIR);
124
+ }
125
+
126
+ function readJsonFile(path) {
127
+ return parseJson(readFileSync(path, 'utf8'), path);
128
+ }
129
+
130
+ function valueAfter(values, index, flag) {
131
+ const value = values[index];
132
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
133
+ return value;
134
+ }
135
+
136
+ function resolveInputPath(path) {
137
+ return isAbsolute(path) ? path : resolve(PROJECT_DIR, path);
138
+ }
139
+
140
+ function relativePath(path) {
141
+ return relative(PROJECT_DIR, path) || '.';
142
+ }
@@ -14,6 +14,7 @@
14
14
  "npx job-forge cache:*",
15
15
  "npx job-forge index:*",
16
16
  "npx job-forge canon:*",
17
+ "npx job-forge preflight:*",
17
18
  "rg *"
18
19
  ],
19
20
  "deny": [
@@ -37,7 +38,8 @@
37
38
  "npx job-forge merge",
38
39
  "npx job-forge guard:*",
39
40
  "npx job-forge telemetry:*",
40
- "npx job-forge trace:*"
41
+ "npx job-forge trace:*",
42
+ "npx job-forge preflight:*"
41
43
  ]
42
44
  },
43
45
  "filesystem": "read-only",
@@ -62,7 +64,8 @@
62
64
  "npx job-forge capabilities:*",
63
65
  "npx job-forge cache:*",
64
66
  "npx job-forge index:*",
65
- "npx job-forge canon:*"
67
+ "npx job-forge canon:*",
68
+ "npx job-forge preflight:*"
66
69
  ],
67
70
  "deny": [
68
71
  "task *"
@@ -37,6 +37,12 @@
37
37
  "canon:key": "job-forge canon:key",
38
38
  "canon:compare": "job-forge canon:compare",
39
39
  "canon:explain": "job-forge canon:explain",
40
+ "preflight:plan": "job-forge preflight:plan",
41
+ "preflight:check": "job-forge preflight:check",
42
+ "preflight:explain": "job-forge preflight:explain",
43
+ "postflight:status": "job-forge postflight:status",
44
+ "postflight:check": "job-forge postflight:check",
45
+ "postflight:explain": "job-forge postflight:explain",
40
46
  "migrate:plan": "job-forge migrate:plan",
41
47
  "migrate:apply": "job-forge migrate:apply",
42
48
  "migrate:check": "job-forge migrate:check",
@@ -58,7 +64,10 @@
58
64
  ".jobforge-runs/",
59
65
  ".jobforge-ledger/",
60
66
  ".jobforge-cache/",
61
- ".jobforge-index.json"
67
+ ".jobforge-index.json",
68
+ "batch/preflight-candidates.json",
69
+ "batch/preflight-plan.json",
70
+ "batch/postflight-outcomes.json"
62
71
  ]
63
72
  }
64
73
  ]
@@ -0,0 +1,79 @@
1
+ {
2
+ "version": 1,
3
+ "workflows": [
4
+ {
5
+ "name": "jobforge.apply",
6
+ "description": "Settle JobForge application dispatch rounds after subagents return outcomes.",
7
+ "terminalStatuses": [
8
+ "APPLIED",
9
+ "Applied",
10
+ "APPLY FAILED",
11
+ "Apply Failed",
12
+ "FAILED",
13
+ "Failed",
14
+ "SKIP",
15
+ "Skip",
16
+ "Skipped",
17
+ "Discarded"
18
+ ],
19
+ "successStatuses": [
20
+ "APPLIED",
21
+ "Applied"
22
+ ],
23
+ "failureStatuses": [
24
+ "APPLY FAILED",
25
+ "Apply Failed",
26
+ "FAILED",
27
+ "Failed"
28
+ ],
29
+ "skipStatuses": [
30
+ "SKIP",
31
+ "Skip",
32
+ "Skipped",
33
+ "Discarded"
34
+ ],
35
+ "inFlightStatuses": [
36
+ "running",
37
+ "in-flight",
38
+ "started",
39
+ "pending"
40
+ ],
41
+ "replacementStatuses": [
42
+ "APPLY FAILED",
43
+ "Apply Failed",
44
+ "FAILED",
45
+ "Failed"
46
+ ],
47
+ "requiredArtifacts": [
48
+ {
49
+ "id": "tracker-tsv",
50
+ "label": "Tracker TSV outcome",
51
+ "statuses": [
52
+ "APPLIED",
53
+ "Applied",
54
+ "APPLY FAILED",
55
+ "Apply Failed",
56
+ "FAILED",
57
+ "Failed",
58
+ "SKIP",
59
+ "Skip",
60
+ "Skipped",
61
+ "Discarded"
62
+ ]
63
+ }
64
+ ],
65
+ "postSteps": [
66
+ {
67
+ "id": "merge",
68
+ "label": "Merge tracker TSV outcomes",
69
+ "command": "npx job-forge merge"
70
+ },
71
+ {
72
+ "id": "verify",
73
+ "label": "Verify tracker integrity",
74
+ "command": "npx job-forge verify"
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "version": 1,
3
+ "workflows": [
4
+ {
5
+ "name": "jobforge.apply",
6
+ "description": "Plan safe JobForge application dispatch rounds before Geometra/task work starts.",
7
+ "roundSize": 2,
8
+ "idFact": "id",
9
+ "conflictFact": "companyRoleKey",
10
+ "requiredFacts": [
11
+ "id",
12
+ "company",
13
+ "role",
14
+ "companyRoleKey",
15
+ "url",
16
+ "score"
17
+ ],
18
+ "sourceRequiredFacts": [
19
+ "company",
20
+ "role",
21
+ "companyRoleKey",
22
+ "url",
23
+ "score"
24
+ ],
25
+ "requireGateSources": true,
26
+ "gatePolicy": {
27
+ "skipStatuses": [
28
+ "skip",
29
+ "skipped"
30
+ ],
31
+ "blockStatuses": [
32
+ "block",
33
+ "blocked",
34
+ "fail",
35
+ "failed"
36
+ ]
37
+ },
38
+ "preSteps": [
39
+ {
40
+ "id": "geometra-cleanup",
41
+ "label": "Disconnect stale Geometra sessions before dispatch",
42
+ "command": "geometra_list_sessions && geometra_disconnect({ closeBrowser: true })"
43
+ }
44
+ ],
45
+ "postSteps": [
46
+ {
47
+ "id": "merge",
48
+ "label": "Merge tracker TSV outcomes",
49
+ "command": "npx job-forge merge"
50
+ },
51
+ {
52
+ "id": "verify",
53
+ "label": "Verify tracker integrity",
54
+ "command": "npx job-forge verify"
55
+ }
56
+ ]
57
+ }
58
+ ]
59
+ }