job-forge 2.14.18 → 2.14.20

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.
@@ -65,12 +65,18 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
65
65
  - [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
66
66
  why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
67
67
 
68
+ - [D10] Treat `templates/capabilities.json` as the source of truth for role capability boundaries. Use `npx job-forge capabilities:explain <role>` or `npx job-forge capabilities:check <role> ...` when changing or validating subagent tool/MCP/filesystem/command permissions; do not paste the full capability matrix into task prompts.
69
+ why: executable local policy prevents role-permission drift without adding MCP/tool-schema tokens or loading a capability matrix into the shared prefix
70
+
71
+ - [D11] Treat `templates/context.json` as the source of truth for mode/reference context bundles. Use `npx job-forge context:plan <mode>` or `npx job-forge context:check <mode>` when changing or validating what a mode loads; do not paste the full context matrix into prompts.
72
+ why: deterministic context bundles prevent reference-file drift and accidental token bloat without adding MCP/tool-schema tokens
73
+
68
74
  ## Procedure
69
75
 
70
76
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
71
77
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
72
- 3. Read the active mode file [D3]; decide inline vs delegated work [D1].
73
- 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
78
+ 3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; decide inline vs delegated work [D1].
79
+ 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
74
80
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
75
81
  6. Keep multi-job form-filling out of the orchestrator [H4].
76
82
  7. Cross-check subagent facts against authoritative files [H7].
@@ -76,6 +76,14 @@ Local workflow ledger (terminal, outside opencode):
76
76
  Artifact contracts (terminal, outside opencode):
77
77
  npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json
78
78
  npx job-forge tracker-line ... --write # renders + validates tracker TSV locally
79
+
80
+ Role capabilities (terminal, outside opencode):
81
+ npx job-forge capabilities:explain general-free
82
+ npx job-forge capabilities:check general-free --tool browser --mcp geometra --filesystem write
83
+
84
+ Context bundles (terminal, outside opencode):
85
+ npx job-forge context:plan apply
86
+ npx job-forge context:check apply --budget 23000
79
87
  ```
80
88
 
81
89
  ---
package/AGENTS.md CHANGED
@@ -60,12 +60,18 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
60
60
  - [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
61
61
  why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
62
62
 
63
+ - [D10] Treat `templates/capabilities.json` as the source of truth for role capability boundaries. Use `npx job-forge capabilities:explain <role>` or `npx job-forge capabilities:check <role> ...` when changing or validating subagent tool/MCP/filesystem/command permissions; do not paste the full capability matrix into task prompts.
64
+ why: executable local policy prevents role-permission drift without adding MCP/tool-schema tokens or loading a capability matrix into the shared prefix
65
+
66
+ - [D11] Treat `templates/context.json` as the source of truth for mode/reference context bundles. Use `npx job-forge context:plan <mode>` or `npx job-forge context:check <mode>` when changing or validating what a mode loads; do not paste the full context matrix into prompts.
67
+ why: deterministic context bundles prevent reference-file drift and accidental token bloat without adding MCP/tool-schema tokens
68
+
63
69
  ## Procedure
64
70
 
65
71
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
66
72
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
67
- 3. Read the active mode file [D3]; decide inline vs delegated work [D1].
68
- 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
73
+ 3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; decide inline vs delegated work [D1].
74
+ 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
69
75
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
70
76
  6. Keep multi-job form-filling out of the orchestrator [H4].
71
77
  7. Cross-check subagent facts against authoritative files [H7].
package/CLAUDE.md CHANGED
@@ -60,12 +60,18 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
60
60
  - [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
61
61
  why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
62
62
 
63
+ - [D10] Treat `templates/capabilities.json` as the source of truth for role capability boundaries. Use `npx job-forge capabilities:explain <role>` or `npx job-forge capabilities:check <role> ...` when changing or validating subagent tool/MCP/filesystem/command permissions; do not paste the full capability matrix into task prompts.
64
+ why: executable local policy prevents role-permission drift without adding MCP/tool-schema tokens or loading a capability matrix into the shared prefix
65
+
66
+ - [D11] Treat `templates/context.json` as the source of truth for mode/reference context bundles. Use `npx job-forge context:plan <mode>` or `npx job-forge context:check <mode>` when changing or validating what a mode loads; do not paste the full context matrix into prompts.
67
+ why: deterministic context bundles prevent reference-file drift and accidental token bloat without adding MCP/tool-schema tokens
68
+
63
69
  ## Procedure
64
70
 
65
71
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
66
72
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
67
- 3. Read the active mode file [D3]; decide inline vs delegated work [D1].
68
- 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
73
+ 3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; decide inline vs delegated work [D1].
74
+ 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
69
75
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
70
76
  6. Keep multi-job form-filling out of the orchestrator [H4].
71
77
  7. Cross-check subagent facts against authoritative files [H7].
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/contracts.json` defines tracker/apply artifact shapes via `@razroo/iso-contract`, and `.jobforge-ledger/events.jsonl` records deterministic duplicate/status events via `@razroo/iso-ledger`. Neither adds prompt or tool-schema tokens.
34
+ JobForge also keeps MCP-free local workflow state: `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`, and `.jobforge-ledger/events.jsonl` records deterministic duplicate/status events via `@razroo/iso-ledger`. 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 + Ledger** | `job-forge trace:*` exposes local OpenCode transcripts, `job-forge telemetry:*` summarizes runs, `job-forge guard:*` audits deterministic policy rules, `templates/contracts.json` enforces artifact shape with `iso-contract`, and `job-forge ledger:*` queries append-only workflow state without MCP/token overhead. |
81
+ | **Trace + Telemetry + Guard + Contract + Ledger + Capabilities + Context** | `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 ledger:*` queries append-only workflow state, `job-forge capabilities:*` checks role boundaries, and `job-forge context:*` plans mode/reference context bundles 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
@@ -162,7 +162,7 @@ my-search/
162
162
  ├── .opencode/skills/job-forge.md # → skill router
163
163
  ├── .opencode/agents/ # → @general-free, @general-paid, @glm-minimal
164
164
  ├── modes/ # → _shared.md + skill modes
165
- ├── templates/ # → states.yml, portals.example.yml, cv-template.html
165
+ ├── templates/ # → states.yml, portals.example.yml, cv-template.html, capabilities.json, context.json
166
166
  ├── batch/batch-prompt.md # → batch worker prompt
167
167
  ├── batch/batch-runner.sh # → parallel orchestrator
168
168
 
@@ -188,13 +188,15 @@ JobForge/
188
188
  │ ├── sync.mjs # postinstall: creates symlinks in consumer project
189
189
  │ └── create-job-forge.mjs # scaffolder
190
190
  ├── modes/ # _shared.md + 16 skill modes
191
- ├── templates/ # cv-template.html, portals.example.yml, states.yml
191
+ ├── templates/ # cv-template.html, portals.example.yml, states.yml, capabilities.json, context.json
192
192
  ├── config/profile.example.yml # template for consumer's profile.yml
193
193
  ├── batch/{batch-prompt.md,batch-runner.sh} # batch orchestrator
194
194
  ├── scripts/
195
195
  │ ├── batch-orchestrator.mjs # iso-orchestrator-backed batch control loop
196
196
  │ ├── tracker-line.mjs # iso-contract-backed tracker TSV renderer
197
197
  │ ├── ledger.mjs # iso-ledger-backed workflow-state CLI
198
+ │ ├── capabilities.mjs # iso-capabilities-backed role policy CLI
199
+ │ ├── context.mjs # iso-context-backed context bundle CLI
198
200
  │ ├── token-usage-report.mjs # opencode cost analyzer
199
201
  │ └── release/check-source.mjs # version gate for npm publish
200
202
  ├── tracker-lib.mjs / merge-tracker.mjs / dedup-tracker.mjs / verify-pipeline.mjs
package/bin/job-forge.mjs CHANGED
@@ -21,6 +21,8 @@
21
21
  * telemetry:* Summarize JobForge pipeline status from traces + tracker files
22
22
  * guard:* Audit JobForge trace policy with iso-guard
23
23
  * ledger:* Query local deterministic workflow state via iso-ledger
24
+ * capabilities:* Query role capability policy via iso-capabilities
25
+ * context:* Query/render deterministic context bundles via iso-context
24
26
  * sync Re-run the harness symlink sync (bin/sync.mjs)
25
27
  * help, --help Show this message
26
28
  */
@@ -84,6 +86,23 @@ const ledgerAliases = {
84
86
  'ledger:path': 'path',
85
87
  };
86
88
 
89
+ const capabilitiesAliases = {
90
+ 'capabilities:list': 'list',
91
+ 'capabilities:explain': 'explain',
92
+ 'capabilities:check': 'check',
93
+ 'capabilities:render': 'render',
94
+ 'capabilities:path': 'path',
95
+ };
96
+
97
+ const contextAliases = {
98
+ 'context:list': 'list',
99
+ 'context:explain': 'explain',
100
+ 'context:plan': 'plan',
101
+ 'context:check': 'check',
102
+ 'context:render': 'render',
103
+ 'context:path': 'path',
104
+ };
105
+
87
106
  const [, , cmd, ...rest] = process.argv;
88
107
 
89
108
  function printHelp() {
@@ -114,6 +133,15 @@ Commands:
114
133
  ledger:rebuild Rebuild .jobforge-ledger/events.jsonl from tracker/pipeline files
115
134
  ledger:has Check URL or company+role state without loading tracker files
116
135
  ledger:verify Validate the local workflow ledger
136
+ capabilities:list List JobForge role capability policies
137
+ capabilities:explain Explain one role capability policy
138
+ capabilities:check Validate requested tool/MCP/command/fs/network access
139
+ capabilities:render Render compact role guidance for an agent harness
140
+ context:list List JobForge context bundles
141
+ context:explain Explain one context bundle
142
+ context:plan Estimate files/tokens for one context bundle
143
+ context:check Fail if a context bundle exceeds its budget
144
+ context:render Render context bundle content as markdown/json
117
145
  sync Re-create harness symlinks in the current project
118
146
 
119
147
  Deterministic helpers (prefer these over LLM-derived values):
@@ -143,6 +171,10 @@ Pass --help after a command to see its own flags, e.g.:
143
171
  job-forge guard:audit
144
172
  job-forge guard:explain
145
173
  job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied
174
+ job-forge capabilities:explain general-free
175
+ job-forge capabilities:check general-free --tool browser --mcp geometra --command "npx job-forge merge" --filesystem write
176
+ job-forge context:plan apply
177
+ job-forge context:check apply --budget 23000
146
178
 
147
179
  Project directory resolves to $JOB_FORGE_PROJECT or cwd.`);
148
180
  }
@@ -212,6 +244,36 @@ if (cmd === 'ledger' || ledgerAliases[cmd]) {
212
244
  process.exit(result.status ?? 1);
213
245
  }
214
246
 
247
+ if (cmd === 'capabilities' || capabilitiesAliases[cmd]) {
248
+ const capabilitiesArgs = cmd === 'capabilities'
249
+ ? (rest.length === 0 ? ['help'] : rest)
250
+ : [capabilitiesAliases[cmd], ...rest];
251
+
252
+ const scriptPath = join(PKG_ROOT, 'scripts/capabilities.mjs');
253
+ const result = spawnSync(process.execPath, [scriptPath, ...capabilitiesArgs], {
254
+ stdio: 'inherit',
255
+ cwd: PROJECT_DIR,
256
+ env: process.env,
257
+ });
258
+
259
+ process.exit(result.status ?? 1);
260
+ }
261
+
262
+ if (cmd === 'context' || contextAliases[cmd]) {
263
+ const contextArgs = cmd === 'context'
264
+ ? (rest.length === 0 ? ['help'] : rest)
265
+ : [contextAliases[cmd], ...rest];
266
+
267
+ const scriptPath = join(PKG_ROOT, 'scripts/context.mjs');
268
+ const result = spawnSync(process.execPath, [scriptPath, ...contextArgs], {
269
+ stdio: 'inherit',
270
+ cwd: PROJECT_DIR,
271
+ env: process.env,
272
+ });
273
+
274
+ process.exit(result.status ?? 1);
275
+ }
276
+
215
277
  const rel = commands[cmd];
216
278
  if (!rel) {
217
279
  console.error(`Unknown command: ${cmd}\n`);
@@ -163,6 +163,7 @@ data/pipeline.md → Pending URLs and `local:jds/...` inbox (see modes/p
163
163
  .jobforge-ledger/events.jsonl → Append-only workflow events for cheap local duplicate/status checks
164
164
  jds/*.md → Saved job descriptions referenced from the pipeline (`local:jds/{file}`)
165
165
  templates/states.yml → Canonical status values
166
+ templates/context.json → Deterministic mode/reference context bundle policy
166
167
  templates/cv-template.html → PDF generation template
167
168
  examples/*.md → Fictional layouts only (not read by scripts; see examples/README.md)
168
169
  ```
@@ -175,6 +176,8 @@ Create `data/pipeline.md` when you start using the URL inbox (`/job-forge pipeli
175
176
  - PDFs: `cv-candidate-{company-slug}-{YYYY-MM-DD}.pdf`
176
177
  - Tracker TSVs: `batch/tracker-additions/{num}-{company-slug}.tsv` (one file per evaluation; merged files move under `batch/tracker-additions/merged/`; shape enforced by `templates/contracts.json`)
177
178
  - Ledger: `.jobforge-ledger/events.jsonl` (created by `job-forge ledger:rebuild`, `tracker-line --write`, or `merge`; gitignored personal state)
179
+ - Capabilities: `templates/capabilities.json` (role boundary policy inspected with `job-forge capabilities:*`)
180
+ - Context: `templates/context.json` (mode/reference file bundles inspected with `job-forge context:*`)
178
181
 
179
182
  ## Pipeline Integrity
180
183
 
@@ -216,6 +219,7 @@ Scripts maintain data consistency. In a consumer project they're invoked via the
216
219
  | `scripts/telemetry.mjs` | `npx job-forge telemetry:status` / `telemetry:show` | JobForge operational telemetry derived from OpenCode traces plus tracker TSV state |
217
220
  | `scripts/guard.mjs` | `npx job-forge guard:audit` / `guard:explain` | Deterministic `@razroo/iso-guard` policy audits over local OpenCode traces |
218
221
  | `scripts/ledger.mjs` | `npx job-forge ledger:status` / `ledger:has` / `ledger:rebuild` | Deterministic `@razroo/iso-ledger` state over tracker, TSV, and pipeline files |
222
+ | `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 |
219
223
  | `tracker-lib.mjs` | _(library)_ | Shared helpers for reading/writing day-based tracker files — imported by merge/dedup/verify/normalize |
220
224
  | `bin/sync.mjs` | `npx job-forge sync` | Creates the harness symlinks in a consumer project (also runs as `postinstall`) |
221
225
  | `bin/create-job-forge.mjs` | `npx create-job-forge <dir>` | Scaffolds a new personal project |
@@ -142,6 +142,14 @@ npx job-forge ledger:verify
142
142
 
143
143
  Machine-readable artifact shapes live in `templates/contracts.json` and are enforced by `@razroo/iso-contract`. `job-forge tracker-line` renders tracker additions through the `jobforge.tracker-row` contract, `merge` validates pending TSV/table rows before writing tracker files, and `verify` validates existing tracker rows against the same contract. Custom forks can extend `templates/contracts.json`, but keep the tracker status enum aligned with `templates/states.yml`.
144
144
 
145
+ ## JobForge role capabilities
146
+
147
+ Role capability boundaries live in `templates/capabilities.json` and are enforced locally by `@razroo/iso-capabilities`. Use `job-forge capabilities:explain <role>` to inspect a role and `job-forge capabilities:check <role> ...` to validate a tool, MCP, command, filesystem, or network boundary before changing agent frontmatter. Custom forks can extend the policy, but keep it aligned with `.opencode/agents/` and the routing rules in `iso/instructions.md`.
148
+
149
+ ## JobForge context bundles
150
+
151
+ Mode/reference context bundles live in `templates/context.json` and are planned locally by `@razroo/iso-context`. Use `job-forge context:plan <mode>` to see the files and estimated tokens, `job-forge context:check <mode>` to fail on budget drift, and `job-forge context:render <mode>` when you intentionally need a compact markdown or JSON context bundle. This is not an MCP and does not add tool-schema tokens; rendered context only consumes prompt tokens when a workflow deliberately asks for it.
152
+
145
153
  ## JobForge guard audits
146
154
 
147
155
  Guard audits run deterministic `@razroo/iso-guard` policies over the same local OpenCode traces. The default policy lives at `templates/guards/jobforge-baseline.yaml` and checks rules that are reliable from transcript data, including max two task dispatches per assistant message, no task-status polling via `task`, no raw proxy configuration in task prompts, and no child session task recursion.
@@ -53,6 +53,10 @@ The orchestrator can only dispatch to these three agents. Accidental self-calls
53
53
  ```
54
54
  Disables ~30 MCP tool schemas globally; each agent re-enables only what it needs in its own `.opencode/agents/<name>.md` frontmatter. Saves ~2-3K input tokens per request in the orchestrator.
55
55
 
56
+ `templates/capabilities.json` is the executable source for intended role boundaries. Inspect it with `job-forge capabilities:explain general-free`, or validate a boundary with `job-forge capabilities:check general-free --tool browser --mcp geometra --command "npx job-forge merge" --filesystem write --network restricted`. Native harness frontmatter still enforces permissions where supported; `iso-capabilities` gives JobForge a local audit/check contract for changes and reviews.
57
+
58
+ `templates/context.json` is the executable source for intended mode/reference context loads. Inspect a bundle with `job-forge context:plan apply`, fail CI or local reviews on drift with `job-forge context:check apply`, or render the bundle only when a workflow deliberately needs it. `iso-context` is local CLI/library policy, not an MCP, so it avoids tool-schema overhead and prevents accidental "load every mode file" prompt bloat.
59
+
56
60
  **3. Thinking budgets** (`reasoningEffort` in agent frontmatter):
57
61
  - `@general-free`: `minimal` — procedural work shouldn't need chain-of-thought
58
62
  - `@general-paid`: `medium` — writing quality benefits from thinking
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`, `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`, `context`, `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
@@ -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
+ | Inspect role capabilities | `npx job-forge capabilities:explain general-free` | `npm run capabilities:explain -- general-free` |
130
+ | Inspect context bundle budget | `npx job-forge context:plan apply` | `npm run context:plan -- apply` |
129
131
  | Map status column to canonical labels | `npx job-forge normalize` | `npm run normalize` |
130
132
  | Merge duplicate company/role rows | `npx job-forge dedup` | `npm run dedup` |
131
133
  | Generate ATS-optimized CV PDF | `npx job-forge pdf` | `npm run pdf` |
@@ -79,6 +79,14 @@ Local workflow ledger (terminal, outside opencode):
79
79
  Artifact contracts (terminal, outside opencode):
80
80
  npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json
81
81
  npx job-forge tracker-line ... --write # renders + validates tracker TSV locally
82
+
83
+ Role capabilities (terminal, outside opencode):
84
+ npx job-forge capabilities:explain general-free
85
+ npx job-forge capabilities:check general-free --tool browser --mcp geometra --filesystem write
86
+
87
+ Context bundles (terminal, outside opencode):
88
+ npx job-forge context:plan apply
89
+ npx job-forge context:check apply --budget 23000
82
90
  ```
83
91
 
84
92
  ---
@@ -60,12 +60,18 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
60
60
  - [D9] Treat `templates/contracts.json` as the source of truth for machine-readable artifacts. Prefer `npx job-forge tracker-line ... --write` for tracker additions; if emitting TSV manually, inspect `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` first. `merge` and `verify` enforce the tracker-row contract locally.
61
61
  why: deterministic code owns the exact tracker TSV/table shape; repeated prose gets re-tokenized and agents occasionally misremember it
62
62
 
63
+ - [D10] Treat `templates/capabilities.json` as the source of truth for role capability boundaries. Use `npx job-forge capabilities:explain <role>` or `npx job-forge capabilities:check <role> ...` when changing or validating subagent tool/MCP/filesystem/command permissions; do not paste the full capability matrix into task prompts.
64
+ why: executable local policy prevents role-permission drift without adding MCP/tool-schema tokens or loading a capability matrix into the shared prefix
65
+
66
+ - [D11] Treat `templates/context.json` as the source of truth for mode/reference context bundles. Use `npx job-forge context:plan <mode>` or `npx job-forge context:check <mode>` when changing or validating what a mode loads; do not paste the full context matrix into prompts.
67
+ why: deterministic context bundles prevent reference-file drift and accidental token bloat without adding MCP/tool-schema tokens
68
+
63
69
  ## Procedure
64
70
 
65
71
  1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
66
72
  2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
67
- 3. Read the active mode file [D3]; decide inline vs delegated work [D1].
68
- 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
73
+ 3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; decide inline vs delegated work [D1].
74
+ 4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
69
75
  5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
70
76
  6. Keep multi-job form-filling out of the orchestrator [H4].
71
77
  7. Cross-check subagent facts against authoritative files [H7].
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { dirname, join, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import {
5
+ checkRoleCapability,
6
+ formatCheckResult,
7
+ formatResolvedRole,
8
+ loadCapabilityPolicy,
9
+ renderRole,
10
+ resolveRole,
11
+ roleNames,
12
+ } from '@razroo/iso-capabilities';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const PKG_ROOT = resolve(__dirname, '..');
16
+ export const CAPABILITIES_RELATIVE_PATH = 'templates/capabilities.json';
17
+
18
+ export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
19
+ return projectDir;
20
+ }
21
+
22
+ export function jobForgeCapabilitiesPath(projectDir = resolveProjectDir()) {
23
+ const projectPath = join(projectDir, CAPABILITIES_RELATIVE_PATH);
24
+ if (existsSync(projectPath)) return projectPath;
25
+ return join(PKG_ROOT, CAPABILITIES_RELATIVE_PATH);
26
+ }
27
+
28
+ export function loadJobForgeCapabilityPolicy(projectDir = resolveProjectDir()) {
29
+ const path = jobForgeCapabilitiesPath(projectDir);
30
+ return loadCapabilityPolicy(JSON.parse(readFileSync(path, 'utf-8')));
31
+ }
32
+
33
+ export function listJobForgeCapabilityRoles(projectDir = resolveProjectDir()) {
34
+ return roleNames(loadJobForgeCapabilityPolicy(projectDir));
35
+ }
36
+
37
+ export function resolveJobForgeCapabilityRole(name, projectDir = resolveProjectDir()) {
38
+ return resolveRole(loadJobForgeCapabilityPolicy(projectDir), name);
39
+ }
40
+
41
+ export function checkJobForgeCapability(name, request, projectDir = resolveProjectDir()) {
42
+ return checkRoleCapability(loadJobForgeCapabilityPolicy(projectDir), name, request);
43
+ }
44
+
45
+ export function formatJobForgeCapabilityCheck(result) {
46
+ return formatCheckResult(result);
47
+ }
48
+
49
+ export function formatJobForgeCapabilityRole(role) {
50
+ return formatResolvedRole(role);
51
+ }
52
+
53
+ export function renderJobForgeCapabilityRole(role, target = 'markdown') {
54
+ return renderRole(role, target);
55
+ }
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { dirname, join, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import {
5
+ bundleNames,
6
+ formatContextPlan,
7
+ formatResolvedContextBundle,
8
+ loadContextPolicy,
9
+ planContext,
10
+ renderContextPlan,
11
+ resolveContextBundle,
12
+ } from '@razroo/iso-context';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const PKG_ROOT = resolve(__dirname, '..');
16
+ export const CONTEXT_RELATIVE_PATH = 'templates/context.json';
17
+
18
+ export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
19
+ return projectDir;
20
+ }
21
+
22
+ export function jobForgeContextPath(projectDir = resolveProjectDir()) {
23
+ const projectPath = join(projectDir, CONTEXT_RELATIVE_PATH);
24
+ if (existsSync(projectPath)) return projectPath;
25
+ return join(PKG_ROOT, CONTEXT_RELATIVE_PATH);
26
+ }
27
+
28
+ export function loadJobForgeContextPolicy(projectDir = resolveProjectDir()) {
29
+ const path = jobForgeContextPath(projectDir);
30
+ return loadContextPolicy(JSON.parse(readFileSync(path, 'utf-8')));
31
+ }
32
+
33
+ export function listJobForgeContextBundles(projectDir = resolveProjectDir()) {
34
+ return bundleNames(loadJobForgeContextPolicy(projectDir));
35
+ }
36
+
37
+ export function resolveJobForgeContextBundle(name, projectDir = resolveProjectDir()) {
38
+ return resolveContextBundle(loadJobForgeContextPolicy(projectDir), name);
39
+ }
40
+
41
+ export function planJobForgeContextBundle(name, options = {}, projectDir = resolveProjectDir()) {
42
+ return planContext(loadJobForgeContextPolicy(projectDir), name, options);
43
+ }
44
+
45
+ export function formatJobForgeContextPlan(plan) {
46
+ return formatContextPlan(plan);
47
+ }
48
+
49
+ export function formatJobForgeContextBundle(bundle) {
50
+ return formatResolvedContextBundle(bundle);
51
+ }
52
+
53
+ export function renderJobForgeContextPlan(plan, target = 'markdown') {
54
+ return renderContextPlan(plan, target);
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-forge",
3
- "version": "2.14.18",
3
+ "version": "2.14.20",
4
4
  "description": "AI-powered job search pipeline built on opencode",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,15 @@
32
32
  "ledger:verify": "node bin/job-forge.mjs ledger:verify",
33
33
  "ledger:has": "node bin/job-forge.mjs ledger:has",
34
34
  "ledger:query": "node bin/job-forge.mjs ledger:query",
35
+ "capabilities:list": "node bin/job-forge.mjs capabilities:list",
36
+ "capabilities:explain": "node bin/job-forge.mjs capabilities:explain",
37
+ "capabilities:check": "node bin/job-forge.mjs capabilities:check",
38
+ "capabilities:render": "node bin/job-forge.mjs capabilities:render",
39
+ "context:list": "node bin/job-forge.mjs context:list",
40
+ "context:explain": "node bin/job-forge.mjs context:explain",
41
+ "context:plan": "node bin/job-forge.mjs context:plan",
42
+ "context:check": "node bin/job-forge.mjs context:check",
43
+ "context:render": "node bin/job-forge.mjs context:render",
35
44
  "plan": "iso plan .",
36
45
  "lint:agentmd": "agentmd lint iso/instructions.md",
37
46
  "lint:modes": "isolint lint modes/",
@@ -96,6 +105,8 @@
96
105
  "node": ">=20.6.0"
97
106
  },
98
107
  "dependencies": {
108
+ "@razroo/iso-capabilities": "^0.1.0",
109
+ "@razroo/iso-context": "^0.1.0",
99
110
  "@razroo/iso-contract": "^0.1.0",
100
111
  "@razroo/iso-guard": "^0.1.0",
101
112
  "@razroo/iso-ledger": "^0.1.0",
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { PROJECT_DIR } from '../tracker-lib.mjs';
4
+ import {
5
+ checkJobForgeCapability,
6
+ formatJobForgeCapabilityCheck,
7
+ formatJobForgeCapabilityRole,
8
+ jobForgeCapabilitiesPath,
9
+ listJobForgeCapabilityRoles,
10
+ renderJobForgeCapabilityRole,
11
+ resolveJobForgeCapabilityRole,
12
+ } from '../lib/jobforge-capabilities.mjs';
13
+
14
+ const USAGE = `job-forge capabilities - deterministic role capability policy
15
+
16
+ Usage:
17
+ job-forge capabilities:list [--json]
18
+ job-forge capabilities:explain <role> [--json]
19
+ job-forge capabilities:check <role> [--tool <name>] [--mcp <name>] [--command <cmd>] [--filesystem read|write] [--network off|restricted|on] [--json]
20
+ job-forge capabilities:render <role> [--target markdown|claude|codex|cursor|opencode|json] [--json]
21
+ job-forge capabilities:path
22
+
23
+ The policy is templates/capabilities.json. It is local project state, not an
24
+ MCP and not prompt context.`;
25
+
26
+ const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
27
+ const { positional, opts } = parseArgs(rawArgs);
28
+
29
+ if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
30
+ console.log(USAGE);
31
+ process.exit(0);
32
+ }
33
+
34
+ try {
35
+ if (cmd === 'path') {
36
+ console.log(jobForgeCapabilitiesPath(PROJECT_DIR));
37
+ } else if (cmd === 'list') {
38
+ list(opts);
39
+ } else if (cmd === 'explain') {
40
+ explain(positional, opts);
41
+ } else if (cmd === 'check') {
42
+ check(positional, opts);
43
+ } else if (cmd === 'render') {
44
+ render(positional, opts);
45
+ } else {
46
+ console.error(`unknown capabilities command "${cmd}"\n`);
47
+ console.error(USAGE);
48
+ process.exit(2);
49
+ }
50
+ } catch (error) {
51
+ console.error(error instanceof Error ? error.message : String(error));
52
+ process.exit(1);
53
+ }
54
+
55
+ function parseArgs(args) {
56
+ const positional = [];
57
+ const opts = {
58
+ json: false,
59
+ help: false,
60
+ target: 'markdown',
61
+ tools: [],
62
+ mcp: [],
63
+ commands: [],
64
+ filesystem: [],
65
+ };
66
+
67
+ for (let i = 0; i < args.length; i++) {
68
+ const arg = args[i];
69
+ if (arg === '--json') {
70
+ opts.json = true;
71
+ } else if (arg === '--tool') {
72
+ opts.tools.push(valueAfter(args, ++i, '--tool'));
73
+ } else if (arg.startsWith('--tool=')) {
74
+ opts.tools.push(arg.slice('--tool='.length));
75
+ } else if (arg === '--mcp') {
76
+ opts.mcp.push(valueAfter(args, ++i, '--mcp'));
77
+ } else if (arg.startsWith('--mcp=')) {
78
+ opts.mcp.push(arg.slice('--mcp='.length));
79
+ } else if (arg === '--command') {
80
+ opts.commands.push(valueAfter(args, ++i, '--command'));
81
+ } else if (arg.startsWith('--command=')) {
82
+ opts.commands.push(arg.slice('--command='.length));
83
+ } else if (arg === '--filesystem') {
84
+ opts.filesystem.push(parseFilesystem(valueAfter(args, ++i, '--filesystem')));
85
+ } else if (arg.startsWith('--filesystem=')) {
86
+ opts.filesystem.push(parseFilesystem(arg.slice('--filesystem='.length)));
87
+ } else if (arg === '--network') {
88
+ opts.network = parseNetwork(valueAfter(args, ++i, '--network'));
89
+ } else if (arg.startsWith('--network=')) {
90
+ opts.network = parseNetwork(arg.slice('--network='.length));
91
+ } else if (arg === '--target') {
92
+ opts.target = parseTarget(valueAfter(args, ++i, '--target'));
93
+ } else if (arg.startsWith('--target=')) {
94
+ opts.target = parseTarget(arg.slice('--target='.length));
95
+ } else if (arg === '--help' || arg === '-h') {
96
+ opts.help = true;
97
+ } else if (arg.startsWith('--')) {
98
+ throw new Error(`unknown flag "${arg}"`);
99
+ } else {
100
+ positional.push(arg);
101
+ }
102
+ }
103
+
104
+ return { positional, opts };
105
+ }
106
+
107
+ function valueAfter(values, index, flag) {
108
+ const value = values[index];
109
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
110
+ return value;
111
+ }
112
+
113
+ function list(opts) {
114
+ const names = listJobForgeCapabilityRoles(PROJECT_DIR);
115
+ if (opts.json) {
116
+ console.log(JSON.stringify(names, null, 2));
117
+ return;
118
+ }
119
+ console.log(names.join('\n'));
120
+ }
121
+
122
+ function explain(positional, opts) {
123
+ const role = readRole(positional);
124
+ if (opts.json) {
125
+ console.log(JSON.stringify(role, null, 2));
126
+ return;
127
+ }
128
+ console.log(formatJobForgeCapabilityRole(role));
129
+ }
130
+
131
+ function check(positional, opts) {
132
+ const roleName = positional[0];
133
+ if (!roleName) throw new Error('missing role name');
134
+ const request = requestFromOptions(opts);
135
+ if (!hasRequest(request)) {
136
+ throw new Error('capabilities:check requires at least one --tool, --mcp, --command, --filesystem, or --network');
137
+ }
138
+ const result = checkJobForgeCapability(roleName, request, PROJECT_DIR);
139
+ if (opts.json) {
140
+ console.log(JSON.stringify(result, null, 2));
141
+ } else {
142
+ console.log(formatJobForgeCapabilityCheck(result));
143
+ }
144
+ process.exit(result.ok ? 0 : 1);
145
+ }
146
+
147
+ function render(positional, opts) {
148
+ const role = readRole(positional);
149
+ const text = renderJobForgeCapabilityRole(role, opts.target);
150
+ if (opts.json) {
151
+ console.log(JSON.stringify({ target: opts.target, role, text }, null, 2));
152
+ return;
153
+ }
154
+ console.log(text);
155
+ }
156
+
157
+ function readRole(positional) {
158
+ const roleName = positional[0];
159
+ if (!roleName) throw new Error('missing role name');
160
+ return resolveJobForgeCapabilityRole(roleName, PROJECT_DIR);
161
+ }
162
+
163
+ function requestFromOptions(opts) {
164
+ return {
165
+ tools: opts.tools.length ? opts.tools : undefined,
166
+ mcp: opts.mcp.length ? opts.mcp : undefined,
167
+ commands: opts.commands.length ? opts.commands : undefined,
168
+ filesystem: opts.filesystem.length ? opts.filesystem : undefined,
169
+ network: opts.network,
170
+ };
171
+ }
172
+
173
+ function hasRequest(request) {
174
+ return Boolean(
175
+ request.tools?.length ||
176
+ request.mcp?.length ||
177
+ request.commands?.length ||
178
+ request.filesystem?.length ||
179
+ request.network,
180
+ );
181
+ }
182
+
183
+ function parseFilesystem(value) {
184
+ if (value === 'read' || value === 'write') return value;
185
+ throw new Error('--filesystem must be read or write');
186
+ }
187
+
188
+ function parseNetwork(value) {
189
+ if (value === 'off' || value === 'restricted' || value === 'on') return value;
190
+ throw new Error('--network must be off, restricted, or on');
191
+ }
192
+
193
+ function parseTarget(value) {
194
+ if (
195
+ value === 'markdown' ||
196
+ value === 'claude' ||
197
+ value === 'codex' ||
198
+ value === 'cursor' ||
199
+ value === 'opencode' ||
200
+ value === 'json'
201
+ ) {
202
+ return value;
203
+ }
204
+ throw new Error('--target must be markdown, claude, codex, cursor, opencode, or json');
205
+ }
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { PROJECT_DIR } from '../tracker-lib.mjs';
4
+ import {
5
+ formatJobForgeContextBundle,
6
+ formatJobForgeContextPlan,
7
+ jobForgeContextPath,
8
+ listJobForgeContextBundles,
9
+ planJobForgeContextBundle,
10
+ renderJobForgeContextPlan,
11
+ resolveJobForgeContextBundle,
12
+ } from '../lib/jobforge-context.mjs';
13
+
14
+ const USAGE = `job-forge context - deterministic mode/reference context policy
15
+
16
+ Usage:
17
+ job-forge context:list [--json]
18
+ job-forge context:explain <bundle> [--json]
19
+ job-forge context:plan <bundle> [--root <dir>] [--budget N] [--chars-per-token N] [--json]
20
+ job-forge context:check <bundle> [--root <dir>] [--budget N] [--chars-per-token N] [--json]
21
+ job-forge context:render <bundle> [--root <dir>] [--target markdown|json] [--json]
22
+ job-forge context:path
23
+
24
+ The policy is templates/context.json. It is local project policy, not an MCP
25
+ and not always-loaded prompt context.`;
26
+
27
+ const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
28
+ const { positional, opts } = parseArgs(rawArgs);
29
+
30
+ if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
31
+ console.log(USAGE);
32
+ process.exit(0);
33
+ }
34
+
35
+ try {
36
+ if (cmd === 'path') {
37
+ console.log(jobForgeContextPath(PROJECT_DIR));
38
+ } else if (cmd === 'list') {
39
+ list(opts);
40
+ } else if (cmd === 'explain') {
41
+ explain(positional, opts);
42
+ } else if (cmd === 'plan') {
43
+ plan(positional, opts, false);
44
+ } else if (cmd === 'check') {
45
+ check(positional, opts);
46
+ } else if (cmd === 'render') {
47
+ render(positional, opts);
48
+ } else {
49
+ console.error(`unknown context command "${cmd}"\n`);
50
+ console.error(USAGE);
51
+ process.exit(2);
52
+ }
53
+ } catch (error) {
54
+ console.error(error instanceof Error ? error.message : String(error));
55
+ process.exit(1);
56
+ }
57
+
58
+ function parseArgs(args) {
59
+ const positional = [];
60
+ const opts = {
61
+ json: false,
62
+ help: false,
63
+ target: 'markdown',
64
+ };
65
+
66
+ for (let i = 0; i < args.length; i++) {
67
+ const arg = args[i];
68
+ if (arg === '--json') {
69
+ opts.json = true;
70
+ } else if (arg === '--root') {
71
+ opts.root = valueAfter(args, ++i, '--root');
72
+ } else if (arg.startsWith('--root=')) {
73
+ opts.root = arg.slice('--root='.length);
74
+ } else if (arg === '--budget' || arg === '--token-budget') {
75
+ opts.tokenBudget = parsePositiveInteger(valueAfter(args, ++i, arg), arg);
76
+ } else if (arg.startsWith('--budget=')) {
77
+ opts.tokenBudget = parsePositiveInteger(arg.slice('--budget='.length), '--budget');
78
+ } else if (arg.startsWith('--token-budget=')) {
79
+ opts.tokenBudget = parsePositiveInteger(arg.slice('--token-budget='.length), '--token-budget');
80
+ } else if (arg === '--chars-per-token') {
81
+ opts.charsPerToken = parsePositiveInteger(valueAfter(args, ++i, '--chars-per-token'), '--chars-per-token');
82
+ } else if (arg.startsWith('--chars-per-token=')) {
83
+ opts.charsPerToken = parsePositiveInteger(arg.slice('--chars-per-token='.length), '--chars-per-token');
84
+ } else if (arg === '--target') {
85
+ opts.target = parseTarget(valueAfter(args, ++i, '--target'));
86
+ } else if (arg.startsWith('--target=')) {
87
+ opts.target = parseTarget(arg.slice('--target='.length));
88
+ } else if (arg === '--help' || arg === '-h') {
89
+ opts.help = true;
90
+ } else if (arg.startsWith('--')) {
91
+ throw new Error(`unknown flag "${arg}"`);
92
+ } else {
93
+ positional.push(arg);
94
+ }
95
+ }
96
+
97
+ return { positional, opts };
98
+ }
99
+
100
+ function valueAfter(values, index, flag) {
101
+ const value = values[index];
102
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
103
+ return value;
104
+ }
105
+
106
+ function list(opts) {
107
+ const names = listJobForgeContextBundles(PROJECT_DIR);
108
+ if (opts.json) {
109
+ console.log(JSON.stringify(names, null, 2));
110
+ return;
111
+ }
112
+ console.log(names.join('\n'));
113
+ }
114
+
115
+ function explain(positional, opts) {
116
+ const bundle = readBundle(positional);
117
+ if (opts.json) {
118
+ console.log(JSON.stringify(bundle, null, 2));
119
+ return;
120
+ }
121
+ console.log(formatJobForgeContextBundle(bundle));
122
+ }
123
+
124
+ function plan(positional, opts, includeContent) {
125
+ const bundleName = readBundleName(positional);
126
+ const result = planJobForgeContextBundle(bundleName, planOptions(opts, includeContent), PROJECT_DIR);
127
+ if (opts.json) {
128
+ console.log(JSON.stringify(result, null, 2));
129
+ return result;
130
+ }
131
+ console.log(formatJobForgeContextPlan(result));
132
+ return result;
133
+ }
134
+
135
+ function check(positional, opts) {
136
+ const result = plan(positional, opts, false);
137
+ process.exit(result.ok ? 0 : 1);
138
+ }
139
+
140
+ function render(positional, opts) {
141
+ const bundleName = readBundleName(positional);
142
+ const result = planJobForgeContextBundle(bundleName, planOptions(opts, true), PROJECT_DIR);
143
+ const text = renderJobForgeContextPlan(result, opts.target);
144
+ if (opts.json) {
145
+ console.log(JSON.stringify({ target: opts.target, plan: result, text }, null, 2));
146
+ } else {
147
+ console.log(text);
148
+ }
149
+ process.exit(result.ok ? 0 : 1);
150
+ }
151
+
152
+ function readBundle(positional) {
153
+ return resolveJobForgeContextBundle(readBundleName(positional), PROJECT_DIR);
154
+ }
155
+
156
+ function readBundleName(positional) {
157
+ const bundleName = positional[0];
158
+ if (!bundleName) throw new Error('missing bundle name');
159
+ return bundleName;
160
+ }
161
+
162
+ function planOptions(opts, includeContent) {
163
+ return {
164
+ root: opts.root || PROJECT_DIR,
165
+ includeContent,
166
+ tokenBudget: opts.tokenBudget,
167
+ charsPerToken: opts.charsPerToken,
168
+ };
169
+ }
170
+
171
+ function parseTarget(value) {
172
+ if (value === 'markdown' || value === 'json') return value;
173
+ throw new Error('--target must be markdown or json');
174
+ }
175
+
176
+ function parsePositiveInteger(value, flag) {
177
+ const number = Number(value);
178
+ if (!Number.isInteger(number) || number <= 0) throw new Error(`${flag} must be a positive integer`);
179
+ return number;
180
+ }
@@ -0,0 +1,110 @@
1
+ {
2
+ "roles": [
3
+ {
4
+ "name": "jobforge-base",
5
+ "description": "Safe local JobForge role for reading project state and running deterministic verification commands.",
6
+ "tools": ["read", "search", "shell"],
7
+ "mcp": [],
8
+ "commands": {
9
+ "allow": [
10
+ "npx job-forge verify",
11
+ "npx job-forge ledger:*",
12
+ "npx job-forge capabilities:*",
13
+ "npx job-forge context:*",
14
+ "rg *"
15
+ ],
16
+ "deny": [
17
+ "rm -rf *"
18
+ ]
19
+ },
20
+ "filesystem": "read-only",
21
+ "network": "off",
22
+ "notes": [
23
+ "Do not paste secrets from config/profile.yml into prompts, traces, or summaries.",
24
+ "Prefer deterministic JobForge commands over re-reading growing tracker files when a local command can answer the question."
25
+ ]
26
+ },
27
+ {
28
+ "name": "orchestrator",
29
+ "description": "Primary session role that routes work, checks policy, and delegates tool-heavy browser/application work.",
30
+ "extends": "jobforge-base",
31
+ "tools": ["task"],
32
+ "commands": {
33
+ "allow": [
34
+ "npx job-forge merge",
35
+ "npx job-forge guard:*",
36
+ "npx job-forge telemetry:*",
37
+ "npx job-forge trace:*"
38
+ ]
39
+ },
40
+ "filesystem": "read-only",
41
+ "network": "off",
42
+ "notes": [
43
+ "Delegate multi-job Geometra work to subagents; do not run browser-heavy application loops inline.",
44
+ "Use capabilities checks when changing or auditing role boundaries."
45
+ ]
46
+ },
47
+ {
48
+ "name": "general-free",
49
+ "description": "Procedural worker for application form fills, TSV writes, merges, verification, OTP retrieval, and portal metadata extraction.",
50
+ "extends": "jobforge-base",
51
+ "tools": ["browser", "write"],
52
+ "mcp": ["geometra", "gmail"],
53
+ "commands": {
54
+ "allow": [
55
+ "npx job-forge tracker-line *",
56
+ "npx job-forge merge",
57
+ "npx job-forge verify",
58
+ "npx job-forge ledger:*",
59
+ "npx job-forge capabilities:*"
60
+ ],
61
+ "deny": [
62
+ "task *"
63
+ ]
64
+ },
65
+ "filesystem": "project-write",
66
+ "network": "restricted",
67
+ "notes": [
68
+ "Use for procedural, tool-heavy work where output shape is validated by JobForge commands.",
69
+ "Do not spawn nested tasks."
70
+ ]
71
+ },
72
+ {
73
+ "name": "general-paid",
74
+ "description": "Quality-sensitive writing and evaluation worker for reports, cover letters, interview stories, and outreach.",
75
+ "extends": "jobforge-base",
76
+ "tools": ["write"],
77
+ "commands": {
78
+ "allow": [
79
+ "npx job-forge render-report-header *",
80
+ "npx job-forge pdf",
81
+ "npx job-forge tracker-line *",
82
+ "npx job-forge verify"
83
+ ]
84
+ },
85
+ "filesystem": "project-write",
86
+ "network": "restricted",
87
+ "notes": [
88
+ "Use for narrative quality, personalization, and scoring explanations rather than browser automation."
89
+ ]
90
+ },
91
+ {
92
+ "name": "glm-minimal",
93
+ "description": "Narrow one-shot extractor/classifier role for small structured transforms.",
94
+ "extends": "jobforge-base",
95
+ "tools": [],
96
+ "commands": {
97
+ "allow": [
98
+ "npx job-forge slugify *",
99
+ "npx job-forge today",
100
+ "npx job-forge next-num"
101
+ ]
102
+ },
103
+ "filesystem": "read-only",
104
+ "network": "off",
105
+ "notes": [
106
+ "Keep input small and output structured; no browser, MCP, or nested task work."
107
+ ]
108
+ }
109
+ ]
110
+ }
@@ -0,0 +1,283 @@
1
+ {
2
+ "defaults": {
3
+ "tokenBudget": 12000,
4
+ "charsPerToken": 4
5
+ },
6
+ "bundles": [
7
+ {
8
+ "name": "base",
9
+ "description": "Shared JobForge orchestration contract.",
10
+ "tokenBudget": 4000,
11
+ "files": [
12
+ {
13
+ "path": "iso/instructions.md",
14
+ "maxTokens": 3500,
15
+ "notes": [
16
+ "Shared contract; mode/reference files should be selected through narrower bundles."
17
+ ]
18
+ }
19
+ ]
20
+ },
21
+ {
22
+ "name": "shared",
23
+ "extends": "base",
24
+ "description": "Shared scoring model plus JobForge orchestration contract.",
25
+ "tokenBudget": 8000,
26
+ "files": [
27
+ {
28
+ "path": "modes/_shared.md",
29
+ "maxTokens": 4500,
30
+ "notes": [
31
+ "Loaded for evaluation-style modes that need archetypes, scoring, and profile references."
32
+ ]
33
+ }
34
+ ]
35
+ },
36
+ {
37
+ "name": "auto-pipeline",
38
+ "extends": "shared",
39
+ "description": "Default JD/URL pipeline context.",
40
+ "tokenBudget": 12000,
41
+ "files": [
42
+ {
43
+ "path": "modes/auto-pipeline.md",
44
+ "maxTokens": 2000
45
+ },
46
+ {
47
+ "path": "modes/reference-setup.md",
48
+ "maxTokens": 2600,
49
+ "required": false,
50
+ "notes": [
51
+ "Load when onboarding, tracker layout, or setup rules are the blocker."
52
+ ]
53
+ }
54
+ ]
55
+ },
56
+ {
57
+ "name": "offer",
58
+ "extends": "shared",
59
+ "description": "Single-offer evaluation context.",
60
+ "tokenBudget": 11000,
61
+ "files": [
62
+ {
63
+ "path": "modes/offer.md",
64
+ "maxTokens": 2400
65
+ },
66
+ {
67
+ "path": "modes/_shared-calibration.md",
68
+ "maxTokens": 2500,
69
+ "required": false,
70
+ "notes": [
71
+ "Load when score calibration or anchor examples are needed."
72
+ ]
73
+ }
74
+ ]
75
+ },
76
+ {
77
+ "name": "compare",
78
+ "extends": "shared",
79
+ "description": "Offer comparison context.",
80
+ "tokenBudget": 8500,
81
+ "files": [
82
+ {
83
+ "path": "modes/compare.md",
84
+ "maxTokens": 600
85
+ }
86
+ ]
87
+ },
88
+ {
89
+ "name": "contact",
90
+ "extends": "shared",
91
+ "description": "LinkedIn outreach context.",
92
+ "tokenBudget": 9000,
93
+ "files": [
94
+ {
95
+ "path": "modes/contact.md",
96
+ "maxTokens": 1200
97
+ }
98
+ ]
99
+ },
100
+ {
101
+ "name": "pdf",
102
+ "extends": "shared",
103
+ "description": "CV/PDF generation context.",
104
+ "tokenBudget": 10000,
105
+ "files": [
106
+ {
107
+ "path": "modes/pdf.md",
108
+ "maxTokens": 2200
109
+ }
110
+ ]
111
+ },
112
+ {
113
+ "name": "apply",
114
+ "extends": "shared",
115
+ "description": "Application form-fill context with Geometra recovery rules.",
116
+ "tokenBudget": 23000,
117
+ "files": [
118
+ {
119
+ "path": "modes/apply.md",
120
+ "maxTokens": 7800
121
+ },
122
+ {
123
+ "path": "modes/reference-geometra.md",
124
+ "maxTokens": 5600
125
+ },
126
+ {
127
+ "path": "modes/reference-portals.md",
128
+ "maxTokens": 2500,
129
+ "required": false,
130
+ "notes": [
131
+ "Load when OTP, proxy, or portal configuration is the blocker."
132
+ ]
133
+ }
134
+ ]
135
+ },
136
+ {
137
+ "name": "pipeline",
138
+ "extends": "shared",
139
+ "description": "Pending URL inbox context.",
140
+ "tokenBudget": 12000,
141
+ "files": [
142
+ {
143
+ "path": "modes/pipeline.md",
144
+ "maxTokens": 1500
145
+ },
146
+ {
147
+ "path": "modes/reference-setup.md",
148
+ "maxTokens": 2600,
149
+ "required": false
150
+ }
151
+ ]
152
+ },
153
+ {
154
+ "name": "scan",
155
+ "extends": "shared",
156
+ "description": "Portal scanning context.",
157
+ "tokenBudget": 18000,
158
+ "files": [
159
+ {
160
+ "path": "modes/scan.md",
161
+ "maxTokens": 6500
162
+ },
163
+ {
164
+ "path": "modes/reference-portals.md",
165
+ "maxTokens": 2500
166
+ }
167
+ ]
168
+ },
169
+ {
170
+ "name": "batch",
171
+ "extends": "shared",
172
+ "description": "Batch processing and durable runner context.",
173
+ "tokenBudget": 16000,
174
+ "files": [
175
+ {
176
+ "path": "modes/batch.md",
177
+ "maxTokens": 1500
178
+ },
179
+ {
180
+ "path": "batch/batch-prompt.md",
181
+ "maxTokens": 4800,
182
+ "required": false,
183
+ "notes": [
184
+ "Needed when invoking or auditing the standalone batch runner prompt."
185
+ ]
186
+ },
187
+ {
188
+ "path": "modes/reference-setup.md",
189
+ "maxTokens": 2600,
190
+ "required": false
191
+ }
192
+ ]
193
+ },
194
+ {
195
+ "name": "tracker",
196
+ "extends": "base",
197
+ "description": "Tracker/status context with setup reference.",
198
+ "tokenBudget": 7000,
199
+ "files": [
200
+ {
201
+ "path": "modes/tracker.md",
202
+ "maxTokens": 500
203
+ },
204
+ {
205
+ "path": "modes/reference-setup.md",
206
+ "maxTokens": 2600
207
+ }
208
+ ]
209
+ },
210
+ {
211
+ "name": "deep",
212
+ "extends": "base",
213
+ "description": "Company research context.",
214
+ "tokenBudget": 5500,
215
+ "files": [
216
+ {
217
+ "path": "modes/deep.md",
218
+ "maxTokens": 1200
219
+ }
220
+ ]
221
+ },
222
+ {
223
+ "name": "training",
224
+ "extends": "base",
225
+ "description": "Course/certification evaluation context.",
226
+ "tokenBudget": 4500,
227
+ "files": [
228
+ {
229
+ "path": "modes/training.md",
230
+ "maxTokens": 400
231
+ }
232
+ ]
233
+ },
234
+ {
235
+ "name": "project",
236
+ "extends": "base",
237
+ "description": "Portfolio project evaluation context.",
238
+ "tokenBudget": 4500,
239
+ "files": [
240
+ {
241
+ "path": "modes/project.md",
242
+ "maxTokens": 400
243
+ }
244
+ ]
245
+ },
246
+ {
247
+ "name": "followup",
248
+ "extends": "base",
249
+ "description": "Follow-up triage context.",
250
+ "tokenBudget": 5500,
251
+ "files": [
252
+ {
253
+ "path": "modes/followup.md",
254
+ "maxTokens": 1000
255
+ }
256
+ ]
257
+ },
258
+ {
259
+ "name": "rejection",
260
+ "extends": "base",
261
+ "description": "Rejection logging and pattern review context.",
262
+ "tokenBudget": 5500,
263
+ "files": [
264
+ {
265
+ "path": "modes/rejection.md",
266
+ "maxTokens": 1100
267
+ }
268
+ ]
269
+ },
270
+ {
271
+ "name": "negotiation",
272
+ "extends": "base",
273
+ "description": "Offer negotiation context.",
274
+ "tokenBudget": 6500,
275
+ "files": [
276
+ {
277
+ "path": "modes/negotiation.md",
278
+ "maxTokens": 1600
279
+ }
280
+ ]
281
+ }
282
+ ]
283
+ }