job-forge 2.14.20 → 2.14.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/main.mdc +5 -2
- package/AGENTS.md +5 -2
- package/CLAUDE.md +5 -2
- package/README.md +4 -2
- package/bin/job-forge.mjs +37 -0
- package/docs/SETUP.md +2 -0
- package/iso/instructions.md +5 -2
- package/lib/jobforge-cache.mjs +105 -0
- package/modes/auto-pipeline.md +3 -1
- package/package.json +13 -2
- package/scripts/cache.mjs +313 -0
- package/templates/capabilities.json +3 -1
- package/templates/context.json +10 -1
package/.cursor/rules/main.mdc
CHANGED
|
@@ -30,7 +30,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
30
30
|
- [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
|
|
31
31
|
why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
|
|
32
32
|
|
|
33
|
-
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv
|
|
33
|
+
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`.
|
|
34
34
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
35
35
|
|
|
36
36
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
@@ -71,11 +71,14 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
71
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
72
|
why: deterministic context bundles prevent reference-file drift and accidental token bloat without adding MCP/tool-schema tokens
|
|
73
73
|
|
|
74
|
+
- [D12] Use `job-forge cache:*` for deterministic local artifact reuse when available. For URL inputs, check `npx job-forge cache:has --url "..."` / `cache:get` before browser or network JD fetches; after a successful fetch, store the exact JD text with `npx job-forge cache:put --url "..." --ttl 14d --input @file` when it is already on disk.
|
|
75
|
+
why: `iso-cache` is not an MCP and adds no prompt/tool-schema tokens; it avoids repeated JD fetch/render passes and lets future sessions reuse stable content from `.jobforge-cache/`
|
|
76
|
+
|
|
74
77
|
## Procedure
|
|
75
78
|
|
|
76
79
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
77
80
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
78
|
-
3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; decide inline vs delegated work [D1].
|
|
81
|
+
3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; check cached artifacts before URL/JD refetches [D12]; decide inline vs delegated work [D1].
|
|
79
82
|
4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
|
|
80
83
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
|
|
81
84
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
package/AGENTS.md
CHANGED
|
@@ -25,7 +25,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
25
25
|
- [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
|
|
26
26
|
why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
|
|
27
27
|
|
|
28
|
-
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv
|
|
28
|
+
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`.
|
|
29
29
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
30
30
|
|
|
31
31
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
@@ -66,11 +66,14 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
66
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
67
|
why: deterministic context bundles prevent reference-file drift and accidental token bloat without adding MCP/tool-schema tokens
|
|
68
68
|
|
|
69
|
+
- [D12] Use `job-forge cache:*` for deterministic local artifact reuse when available. For URL inputs, check `npx job-forge cache:has --url "..."` / `cache:get` before browser or network JD fetches; after a successful fetch, store the exact JD text with `npx job-forge cache:put --url "..." --ttl 14d --input @file` when it is already on disk.
|
|
70
|
+
why: `iso-cache` is not an MCP and adds no prompt/tool-schema tokens; it avoids repeated JD fetch/render passes and lets future sessions reuse stable content from `.jobforge-cache/`
|
|
71
|
+
|
|
69
72
|
## Procedure
|
|
70
73
|
|
|
71
74
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
72
75
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
73
|
-
3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; decide inline vs delegated work [D1].
|
|
76
|
+
3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; check cached artifacts before URL/JD refetches [D12]; decide inline vs delegated work [D1].
|
|
74
77
|
4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
|
|
75
78
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
|
|
76
79
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
package/CLAUDE.md
CHANGED
|
@@ -25,7 +25,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
25
25
|
- [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
|
|
26
26
|
why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
|
|
27
27
|
|
|
28
|
-
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv
|
|
28
|
+
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`.
|
|
29
29
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
30
30
|
|
|
31
31
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
@@ -66,11 +66,14 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
66
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
67
|
why: deterministic context bundles prevent reference-file drift and accidental token bloat without adding MCP/tool-schema tokens
|
|
68
68
|
|
|
69
|
+
- [D12] Use `job-forge cache:*` for deterministic local artifact reuse when available. For URL inputs, check `npx job-forge cache:has --url "..."` / `cache:get` before browser or network JD fetches; after a successful fetch, store the exact JD text with `npx job-forge cache:put --url "..." --ttl 14d --input @file` when it is already on disk.
|
|
70
|
+
why: `iso-cache` is not an MCP and adds no prompt/tool-schema tokens; it avoids repeated JD fetch/render passes and lets future sessions reuse stable content from `.jobforge-cache/`
|
|
71
|
+
|
|
69
72
|
## Procedure
|
|
70
73
|
|
|
71
74
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
72
75
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
73
|
-
3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; decide inline vs delegated work [D1].
|
|
76
|
+
3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; check cached artifacts before URL/JD refetches [D12]; decide inline vs delegated work [D1].
|
|
74
77
|
4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
|
|
75
78
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
|
|
76
79
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
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`, `templates/capabilities.json` defines role capability boundaries via `@razroo/iso-capabilities`, `templates/context.json` defines deterministic mode/reference bundles via `@razroo/iso-context`,
|
|
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`, `.jobforge-ledger/events.jsonl` records duplicate/status events via `@razroo/iso-ledger`, and `.jobforge-cache/` stores reusable JD/artifact content via `@razroo/iso-cache`. 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 + 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,
|
|
81
|
+
| **Trace + Telemetry + Guard + Contract + Ledger + Capabilities + Context + Cache** | `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, `job-forge context:*` plans mode/reference context bundles, and `job-forge cache:*` reuses fetched JD/artifact content 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
|
|
@@ -146,6 +146,7 @@ my-search/
|
|
|
146
146
|
├── config/profile.yml # your identity, target roles (personal)
|
|
147
147
|
├── data/ # applications, pipeline, scan history (personal, gitignored)
|
|
148
148
|
├── .jobforge-ledger/ # append-only local workflow events (personal, gitignored)
|
|
149
|
+
├── .jobforge-cache/ # content-addressed local JD/artifact cache (personal, gitignored)
|
|
149
150
|
├── reports/ # generated evaluation reports (personal, gitignored)
|
|
150
151
|
├── batch/{batch-input,batch-state}.tsv, tracker-additions/, logs/ # personal
|
|
151
152
|
├── .jobforge-runs/ # durable batch workflow records (generated)
|
|
@@ -197,6 +198,7 @@ JobForge/
|
|
|
197
198
|
│ ├── ledger.mjs # iso-ledger-backed workflow-state CLI
|
|
198
199
|
│ ├── capabilities.mjs # iso-capabilities-backed role policy CLI
|
|
199
200
|
│ ├── context.mjs # iso-context-backed context bundle CLI
|
|
201
|
+
│ ├── cache.mjs # iso-cache-backed local artifact cache CLI
|
|
200
202
|
│ ├── token-usage-report.mjs # opencode cost analyzer
|
|
201
203
|
│ └── release/check-source.mjs # version gate for npm publish
|
|
202
204
|
├── tracker-lib.mjs / merge-tracker.mjs / dedup-tracker.mjs / verify-pipeline.mjs
|
package/bin/job-forge.mjs
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* ledger:* Query local deterministic workflow state via iso-ledger
|
|
24
24
|
* capabilities:* Query role capability policy via iso-capabilities
|
|
25
25
|
* context:* Query/render deterministic context bundles via iso-context
|
|
26
|
+
* cache:* Reuse local deterministic artifacts via iso-cache
|
|
26
27
|
* sync Re-run the harness symlink sync (bin/sync.mjs)
|
|
27
28
|
* help, --help Show this message
|
|
28
29
|
*/
|
|
@@ -103,6 +104,18 @@ const contextAliases = {
|
|
|
103
104
|
'context:path': 'path',
|
|
104
105
|
};
|
|
105
106
|
|
|
107
|
+
const cacheAliases = {
|
|
108
|
+
'cache:key': 'key',
|
|
109
|
+
'cache:status': 'status',
|
|
110
|
+
'cache:has': 'has',
|
|
111
|
+
'cache:get': 'get',
|
|
112
|
+
'cache:put': 'put',
|
|
113
|
+
'cache:list': 'list',
|
|
114
|
+
'cache:verify': 'verify',
|
|
115
|
+
'cache:prune': 'prune',
|
|
116
|
+
'cache:path': 'path',
|
|
117
|
+
};
|
|
118
|
+
|
|
106
119
|
const [, , cmd, ...rest] = process.argv;
|
|
107
120
|
|
|
108
121
|
function printHelp() {
|
|
@@ -142,6 +155,12 @@ Commands:
|
|
|
142
155
|
context:plan Estimate files/tokens for one context bundle
|
|
143
156
|
context:check Fail if a context bundle exceeds its budget
|
|
144
157
|
context:render Render context bundle content as markdown/json
|
|
158
|
+
cache:status Show local artifact cache status
|
|
159
|
+
cache:key Print deterministic cache key for a job URL
|
|
160
|
+
cache:has Check whether a job URL or cache key is cached
|
|
161
|
+
cache:get Read cached JD/artifact content
|
|
162
|
+
cache:put Store JD/artifact content
|
|
163
|
+
cache:verify Validate local artifact cache integrity
|
|
145
164
|
sync Re-create harness symlinks in the current project
|
|
146
165
|
|
|
147
166
|
Deterministic helpers (prefer these over LLM-derived values):
|
|
@@ -175,6 +194,9 @@ Pass --help after a command to see its own flags, e.g.:
|
|
|
175
194
|
job-forge capabilities:check general-free --tool browser --mcp geometra --command "npx job-forge merge" --filesystem write
|
|
176
195
|
job-forge context:plan apply
|
|
177
196
|
job-forge context:check apply --budget 23000
|
|
197
|
+
job-forge cache:has --url https://example.test/jobs/123
|
|
198
|
+
job-forge cache:get --url https://example.test/jobs/123
|
|
199
|
+
job-forge cache:put --url https://example.test/jobs/123 --input @jds/example.md
|
|
178
200
|
|
|
179
201
|
Project directory resolves to $JOB_FORGE_PROJECT or cwd.`);
|
|
180
202
|
}
|
|
@@ -274,6 +296,21 @@ if (cmd === 'context' || contextAliases[cmd]) {
|
|
|
274
296
|
process.exit(result.status ?? 1);
|
|
275
297
|
}
|
|
276
298
|
|
|
299
|
+
if (cmd === 'cache' || cacheAliases[cmd]) {
|
|
300
|
+
const cacheArgs = cmd === 'cache'
|
|
301
|
+
? (rest.length === 0 ? ['help'] : rest)
|
|
302
|
+
: [cacheAliases[cmd], ...rest];
|
|
303
|
+
|
|
304
|
+
const scriptPath = join(PKG_ROOT, 'scripts/cache.mjs');
|
|
305
|
+
const result = spawnSync(process.execPath, [scriptPath, ...cacheArgs], {
|
|
306
|
+
stdio: 'inherit',
|
|
307
|
+
cwd: PROJECT_DIR,
|
|
308
|
+
env: process.env,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
process.exit(result.status ?? 1);
|
|
312
|
+
}
|
|
313
|
+
|
|
277
314
|
const rel = commands[cmd];
|
|
278
315
|
if (!rel) {
|
|
279
316
|
console.error(`Unknown command: ${cmd}\n`);
|
package/docs/SETUP.md
CHANGED
|
@@ -128,6 +128,7 @@ From your project root, these commands maintain the tracker and pipeline checks.
|
|
|
128
128
|
| Inspect tracker row contract | `npx iso-contract explain jobforge.tracker-row --contracts templates/contracts.json` | _(none)_ |
|
|
129
129
|
| Inspect role capabilities | `npx job-forge capabilities:explain general-free` | `npm run capabilities:explain -- general-free` |
|
|
130
130
|
| Inspect context bundle budget | `npx job-forge context:plan apply` | `npm run context:plan -- apply` |
|
|
131
|
+
| Inspect local JD/artifact cache | `npx job-forge cache:status` | `npm run cache:status` |
|
|
131
132
|
| Map status column to canonical labels | `npx job-forge normalize` | `npm run normalize` |
|
|
132
133
|
| Merge duplicate company/role rows | `npx job-forge dedup` | `npm run dedup` |
|
|
133
134
|
| Generate ATS-optimized CV PDF | `npx job-forge pdf` | `npm run pdf` |
|
|
@@ -144,6 +145,7 @@ From your project root, these commands maintain the tracker and pipeline checks.
|
|
|
144
145
|
| Show local workflow ledger status | `npx job-forge ledger:status` | `npm run ledger:status` |
|
|
145
146
|
| Rebuild local workflow ledger from tracker/pipeline files | `npx job-forge ledger:rebuild` | `npm run ledger:rebuild` |
|
|
146
147
|
| Check duplicate/status event without loading tracker files | `npx job-forge ledger:has --company "Acme" --role "Staff Engineer" --status Applied` | `npm run ledger:has -- --company ...` |
|
|
148
|
+
| Check/reuse cached JD content | `npx job-forge cache:has --url <url>` / `npx job-forge cache:get --url <url>` | `npm run cache:has -- --url ...` |
|
|
147
149
|
| Re-create harness symlinks | `npx job-forge sync` | `npm run sync` |
|
|
148
150
|
| Build optional dashboard TUI (Go on `PATH`) | `(cd node_modules/job-forge/dashboard && go build .)` | `npm run build:dashboard` (harness repo only) |
|
|
149
151
|
|
package/iso/instructions.md
CHANGED
|
@@ -25,7 +25,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
25
25
|
- [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
|
|
26
26
|
why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
|
|
27
27
|
|
|
28
|
-
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv
|
|
28
|
+
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`.
|
|
29
29
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
30
30
|
|
|
31
31
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
@@ -66,11 +66,14 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
66
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
67
|
why: deterministic context bundles prevent reference-file drift and accidental token bloat without adding MCP/tool-schema tokens
|
|
68
68
|
|
|
69
|
+
- [D12] Use `job-forge cache:*` for deterministic local artifact reuse when available. For URL inputs, check `npx job-forge cache:has --url "..."` / `cache:get` before browser or network JD fetches; after a successful fetch, store the exact JD text with `npx job-forge cache:put --url "..." --ttl 14d --input @file` when it is already on disk.
|
|
70
|
+
why: `iso-cache` is not an MCP and adds no prompt/tool-schema tokens; it avoids repeated JD fetch/render passes and lets future sessions reuse stable content from `.jobforge-cache/`
|
|
71
|
+
|
|
69
72
|
## Procedure
|
|
70
73
|
|
|
71
74
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
72
75
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
73
|
-
3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; decide inline vs delegated work [D1].
|
|
76
|
+
3. Read the active mode file [D3]; use context bundle checks when changing context loads [D11]; check cached artifacts before URL/JD refetches [D12]; decide inline vs delegated work [D1].
|
|
74
77
|
4. Prepare Geometra dispatches: cleanup [H3], ledger prefilter when present [D8], dedupe [H2], location filter [D5], routing [D2, D10], proxy prompt hygiene [H8].
|
|
75
78
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
|
|
76
79
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
cacheKey,
|
|
5
|
+
hasCacheEntry,
|
|
6
|
+
listCacheEntries,
|
|
7
|
+
pruneCache,
|
|
8
|
+
putCacheEntry,
|
|
9
|
+
readCacheContent,
|
|
10
|
+
resolveCacheDir,
|
|
11
|
+
verifyCache,
|
|
12
|
+
} from '@razroo/iso-cache';
|
|
13
|
+
|
|
14
|
+
export const CACHE_DIR = '.jobforge-cache';
|
|
15
|
+
export const JD_CACHE_NAMESPACE = 'jobforge.jd';
|
|
16
|
+
export const JD_CACHE_VERSION = '1';
|
|
17
|
+
export const JD_CACHE_KIND = 'jd';
|
|
18
|
+
export const DEFAULT_JD_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
|
|
21
|
+
return projectDir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function jobForgeCacheDir(projectDir = resolveProjectDir()) {
|
|
25
|
+
return process.env.JOB_FORGE_CACHE || join(projectDir, CACHE_DIR);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function jobForgeCacheSummary(projectDir = resolveProjectDir()) {
|
|
29
|
+
const root = resolveCacheDir(jobForgeCacheDir(projectDir));
|
|
30
|
+
const entries = listCacheEntries(root, { includeExpired: true });
|
|
31
|
+
return {
|
|
32
|
+
root,
|
|
33
|
+
exists: existsSync(root),
|
|
34
|
+
entries: entries.length,
|
|
35
|
+
active: entries.filter((entry) => !entry.expiresAt || new Date(entry.expiresAt).getTime() > Date.now()).length,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function jobDescriptionCacheKey(url) {
|
|
40
|
+
return cacheKey({
|
|
41
|
+
namespace: JD_CACHE_NAMESPACE,
|
|
42
|
+
version: JD_CACHE_VERSION,
|
|
43
|
+
parts: { url: normalizeJobUrl(url) },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function putJobDescriptionCache(url, content, options = {}, projectDir = resolveProjectDir()) {
|
|
48
|
+
const normalizedUrl = normalizeJobUrl(url);
|
|
49
|
+
return putCacheEntry(
|
|
50
|
+
jobForgeCacheDir(projectDir),
|
|
51
|
+
jobDescriptionCacheKey(normalizedUrl),
|
|
52
|
+
content,
|
|
53
|
+
{
|
|
54
|
+
kind: options.kind || JD_CACHE_KIND,
|
|
55
|
+
contentType: options.contentType || 'text/markdown',
|
|
56
|
+
ttlMs: options.expiresAt ? undefined : (options.ttlMs ?? DEFAULT_JD_TTL_MS),
|
|
57
|
+
expiresAt: options.expiresAt,
|
|
58
|
+
metadata: {
|
|
59
|
+
url: normalizedUrl,
|
|
60
|
+
source: options.source || 'job-forge',
|
|
61
|
+
...(options.metadata || {}),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readJobDescriptionCache(url, options = {}, projectDir = resolveProjectDir()) {
|
|
68
|
+
return readCacheContent(jobForgeCacheDir(projectDir), jobDescriptionCacheKey(url), options);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function hasJobDescriptionCache(url, options = {}, projectDir = resolveProjectDir()) {
|
|
72
|
+
return hasCacheEntry(jobForgeCacheDir(projectDir), jobDescriptionCacheKey(url), options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readJobForgeCache(key, options = {}, projectDir = resolveProjectDir()) {
|
|
76
|
+
return readCacheContent(jobForgeCacheDir(projectDir), key, options);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function putJobForgeCache(key, content, options = {}, projectDir = resolveProjectDir()) {
|
|
80
|
+
return putCacheEntry(jobForgeCacheDir(projectDir), key, content, options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function listJobForgeCache(options = {}, projectDir = resolveProjectDir()) {
|
|
84
|
+
return listCacheEntries(jobForgeCacheDir(projectDir), options);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function verifyJobForgeCache(projectDir = resolveProjectDir()) {
|
|
88
|
+
return verifyCache(jobForgeCacheDir(projectDir));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function pruneJobForgeCache(options = {}, projectDir = resolveProjectDir()) {
|
|
92
|
+
return pruneCache(jobForgeCacheDir(projectDir), options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function normalizeJobUrl(url) {
|
|
96
|
+
const text = String(url || '').trim();
|
|
97
|
+
if (!text) throw new Error('url is required');
|
|
98
|
+
try {
|
|
99
|
+
const parsed = new URL(text);
|
|
100
|
+
parsed.hash = '';
|
|
101
|
+
return parsed.toString();
|
|
102
|
+
} catch {
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
105
|
+
}
|
package/modes/auto-pipeline.md
CHANGED
|
@@ -21,7 +21,9 @@ Fetch the JD content once. If the input is a **URL** (not pasted JD text), fetch
|
|
|
21
21
|
|
|
22
22
|
**If the input is JD text** (not a URL): use it directly, no fetching needed.
|
|
23
23
|
|
|
24
|
-
**Local artifacts before Step 0 methods:** Grep `reports/` for the URL or stable company+role slug; if a report already embeds the full JD, Read it and skip network fetch entirely. If the pipeline row or `jds/` references `local:jds/{file}`, Read that file first. This stacks with the rule above: one fetch per URL per session, and **zero** if the JD is already on disk
|
|
24
|
+
**Local artifacts before Step 0 methods:** Grep `reports/` for the URL or stable company+role slug; if a report already embeds the full JD, Read it and skip network fetch entirely. If the pipeline row or `jds/` references `local:jds/{file}`, Read that file first. For URL inputs, run `npx job-forge cache:has --url "{url}"` and then `npx job-forge cache:get --url "{url}"` on a hit; cached JD text is authoritative local content and avoids a browser/network fetch. This stacks with the rule above: one fetch per URL per session, and **zero** if the JD is already on disk or in `.jobforge-cache/`.
|
|
25
|
+
|
|
26
|
+
**Cache after a successful fetch:** If the JD text is written to `jds/` or embedded in a report, store that exact text with `npx job-forge cache:put --url "{url}" --ttl 14d --input @path/to/jd.md`. Do not refetch just to populate the cache.
|
|
25
27
|
|
|
26
28
|
## Step 1 — Run Evaluation A-F
|
|
27
29
|
Execute exactly as in the `offer` mode (read `modes/offer.md` for all blocks A-F).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.22",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,6 +41,14 @@
|
|
|
41
41
|
"context:plan": "node bin/job-forge.mjs context:plan",
|
|
42
42
|
"context:check": "node bin/job-forge.mjs context:check",
|
|
43
43
|
"context:render": "node bin/job-forge.mjs context:render",
|
|
44
|
+
"cache:key": "node bin/job-forge.mjs cache:key",
|
|
45
|
+
"cache:has": "node bin/job-forge.mjs cache:has",
|
|
46
|
+
"cache:get": "node bin/job-forge.mjs cache:get",
|
|
47
|
+
"cache:put": "node bin/job-forge.mjs cache:put",
|
|
48
|
+
"cache:status": "node bin/job-forge.mjs cache:status",
|
|
49
|
+
"cache:list": "node bin/job-forge.mjs cache:list",
|
|
50
|
+
"cache:verify": "node bin/job-forge.mjs cache:verify",
|
|
51
|
+
"cache:prune": "node bin/job-forge.mjs cache:prune",
|
|
44
52
|
"plan": "iso plan .",
|
|
45
53
|
"lint:agentmd": "agentmd lint iso/instructions.md",
|
|
46
54
|
"lint:modes": "isolint lint modes/",
|
|
@@ -58,7 +66,9 @@
|
|
|
58
66
|
"bin/",
|
|
59
67
|
"iso/",
|
|
60
68
|
"models.yaml",
|
|
61
|
-
".claude/",
|
|
69
|
+
".claude/settings.json",
|
|
70
|
+
".claude/iso-route.resolved.json",
|
|
71
|
+
".claude/agents/",
|
|
62
72
|
".cursor/mcp.json",
|
|
63
73
|
".cursor/rules/",
|
|
64
74
|
".cursor/iso-route.md",
|
|
@@ -106,6 +116,7 @@
|
|
|
106
116
|
},
|
|
107
117
|
"dependencies": {
|
|
108
118
|
"@razroo/iso-capabilities": "^0.1.0",
|
|
119
|
+
"@razroo/iso-cache": "^0.1.0",
|
|
109
120
|
"@razroo/iso-context": "^0.1.0",
|
|
110
121
|
"@razroo/iso-contract": "^0.1.0",
|
|
111
122
|
"@razroo/iso-guard": "^0.1.0",
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import {
|
|
5
|
+
formatCacheEntries,
|
|
6
|
+
formatPruneResult,
|
|
7
|
+
formatVerifyResult,
|
|
8
|
+
} from '@razroo/iso-cache';
|
|
9
|
+
import { PROJECT_DIR } from '../tracker-lib.mjs';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_JD_TTL_MS,
|
|
12
|
+
JD_CACHE_KIND,
|
|
13
|
+
jobDescriptionCacheKey,
|
|
14
|
+
jobForgeCacheDir,
|
|
15
|
+
jobForgeCacheSummary,
|
|
16
|
+
listJobForgeCache,
|
|
17
|
+
pruneJobForgeCache,
|
|
18
|
+
putJobDescriptionCache,
|
|
19
|
+
putJobForgeCache,
|
|
20
|
+
readJobDescriptionCache,
|
|
21
|
+
readJobForgeCache,
|
|
22
|
+
verifyJobForgeCache,
|
|
23
|
+
} from '../lib/jobforge-cache.mjs';
|
|
24
|
+
|
|
25
|
+
const USAGE = `job-forge cache - local deterministic artifact cache
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
job-forge cache:key --url <url> [--json]
|
|
29
|
+
job-forge cache:status [--json]
|
|
30
|
+
job-forge cache:has (--url <url> | --key <key>) [--allow-expired] [--json]
|
|
31
|
+
job-forge cache:get (--url <url> | --key <key>) [--allow-expired] [--output <file>] [--json]
|
|
32
|
+
job-forge cache:put (--url <url> | --key <key>) --input <text|@file|-> [--ttl 14d] [--kind <kind>] [--content-type <type>] [--meta <json|@file>] [--json]
|
|
33
|
+
job-forge cache:list [--kind <kind>] [--include-expired] [--json]
|
|
34
|
+
job-forge cache:verify [--json]
|
|
35
|
+
job-forge cache:prune [--expired] [--dry-run] [--json]
|
|
36
|
+
job-forge cache:path
|
|
37
|
+
|
|
38
|
+
Default path is .jobforge-cache/ unless JOB_FORGE_CACHE is set. This is local
|
|
39
|
+
project state, not an MCP and not prompt context.`;
|
|
40
|
+
|
|
41
|
+
const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
|
|
42
|
+
const opts = parseArgs(rawArgs);
|
|
43
|
+
|
|
44
|
+
if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
45
|
+
console.log(USAGE);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
if (cmd === 'path') {
|
|
51
|
+
console.log(jobForgeCacheDir(PROJECT_DIR));
|
|
52
|
+
} else if (cmd === 'key') {
|
|
53
|
+
key(opts);
|
|
54
|
+
} else if (cmd === 'status') {
|
|
55
|
+
status(opts);
|
|
56
|
+
} else if (cmd === 'has') {
|
|
57
|
+
has(opts);
|
|
58
|
+
} else if (cmd === 'get') {
|
|
59
|
+
get(opts);
|
|
60
|
+
} else if (cmd === 'put') {
|
|
61
|
+
put(opts);
|
|
62
|
+
} else if (cmd === 'list') {
|
|
63
|
+
list(opts);
|
|
64
|
+
} else if (cmd === 'verify') {
|
|
65
|
+
verify(opts);
|
|
66
|
+
} else if (cmd === 'prune') {
|
|
67
|
+
prune(opts);
|
|
68
|
+
} else {
|
|
69
|
+
console.error(`unknown cache command "${cmd}"\n`);
|
|
70
|
+
console.error(USAGE);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseArgs(args) {
|
|
79
|
+
const opts = {
|
|
80
|
+
json: false,
|
|
81
|
+
help: false,
|
|
82
|
+
allowExpired: false,
|
|
83
|
+
includeExpired: false,
|
|
84
|
+
dryRun: false,
|
|
85
|
+
expired: false,
|
|
86
|
+
ttlMs: DEFAULT_JD_TTL_MS,
|
|
87
|
+
metadata: {},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < args.length; i++) {
|
|
91
|
+
const arg = args[i];
|
|
92
|
+
if (arg === '--json') {
|
|
93
|
+
opts.json = true;
|
|
94
|
+
} else if (arg === '--url') {
|
|
95
|
+
opts.url = valueAfter(args, ++i, '--url');
|
|
96
|
+
} else if (arg.startsWith('--url=')) {
|
|
97
|
+
opts.url = arg.slice('--url='.length);
|
|
98
|
+
} else if (arg === '--key') {
|
|
99
|
+
opts.key = valueAfter(args, ++i, '--key');
|
|
100
|
+
} else if (arg.startsWith('--key=')) {
|
|
101
|
+
opts.key = arg.slice('--key='.length);
|
|
102
|
+
} else if (arg === '--input') {
|
|
103
|
+
opts.input = valueAfter(args, ++i, '--input');
|
|
104
|
+
} else if (arg.startsWith('--input=')) {
|
|
105
|
+
opts.input = arg.slice('--input='.length);
|
|
106
|
+
} else if (arg === '--output') {
|
|
107
|
+
opts.output = valueAfter(args, ++i, '--output');
|
|
108
|
+
} else if (arg.startsWith('--output=')) {
|
|
109
|
+
opts.output = arg.slice('--output='.length);
|
|
110
|
+
} else if (arg === '--kind') {
|
|
111
|
+
opts.kind = valueAfter(args, ++i, '--kind');
|
|
112
|
+
} else if (arg.startsWith('--kind=')) {
|
|
113
|
+
opts.kind = arg.slice('--kind='.length);
|
|
114
|
+
} else if (arg === '--content-type') {
|
|
115
|
+
opts.contentType = valueAfter(args, ++i, '--content-type');
|
|
116
|
+
} else if (arg.startsWith('--content-type=')) {
|
|
117
|
+
opts.contentType = arg.slice('--content-type='.length);
|
|
118
|
+
} else if (arg === '--ttl') {
|
|
119
|
+
opts.ttlMs = parseDuration(valueAfter(args, ++i, '--ttl'));
|
|
120
|
+
} else if (arg.startsWith('--ttl=')) {
|
|
121
|
+
opts.ttlMs = parseDuration(arg.slice('--ttl='.length));
|
|
122
|
+
} else if (arg === '--expires-at') {
|
|
123
|
+
opts.expiresAt = valueAfter(args, ++i, '--expires-at');
|
|
124
|
+
opts.ttlMs = undefined;
|
|
125
|
+
} else if (arg.startsWith('--expires-at=')) {
|
|
126
|
+
opts.expiresAt = arg.slice('--expires-at='.length);
|
|
127
|
+
opts.ttlMs = undefined;
|
|
128
|
+
} else if (arg === '--meta') {
|
|
129
|
+
opts.metadata = parseMetadata(valueAfter(args, ++i, '--meta'));
|
|
130
|
+
} else if (arg.startsWith('--meta=')) {
|
|
131
|
+
opts.metadata = parseMetadata(arg.slice('--meta='.length));
|
|
132
|
+
} else if (arg === '--allow-expired') {
|
|
133
|
+
opts.allowExpired = true;
|
|
134
|
+
} else if (arg === '--include-expired') {
|
|
135
|
+
opts.includeExpired = true;
|
|
136
|
+
} else if (arg === '--dry-run') {
|
|
137
|
+
opts.dryRun = true;
|
|
138
|
+
} else if (arg === '--expired') {
|
|
139
|
+
opts.expired = true;
|
|
140
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
141
|
+
opts.help = true;
|
|
142
|
+
} else {
|
|
143
|
+
throw new Error(`unknown flag "${arg}"`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return opts;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function valueAfter(values, index, flag) {
|
|
151
|
+
const value = values[index];
|
|
152
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function key(opts) {
|
|
157
|
+
const keyValue = keyFromOptions(opts);
|
|
158
|
+
if (opts.json) {
|
|
159
|
+
console.log(JSON.stringify({ key: keyValue, url: opts.url || null }, null, 2));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.log(keyValue);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function status(opts) {
|
|
166
|
+
const summary = jobForgeCacheSummary(PROJECT_DIR);
|
|
167
|
+
if (opts.json) {
|
|
168
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
console.log(`cache: ${summary.root}`);
|
|
172
|
+
console.log(`exists: ${summary.exists ? 'yes' : 'no'}`);
|
|
173
|
+
console.log(`entries: ${summary.active} active, ${summary.entries - summary.active} expired`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function has(opts) {
|
|
177
|
+
const hit = read(opts);
|
|
178
|
+
if (opts.json) {
|
|
179
|
+
console.log(JSON.stringify({ hit: Boolean(hit?.hit), stale: Boolean(hit?.stale), key: keyFromOptions(opts) }, null, 2));
|
|
180
|
+
} else {
|
|
181
|
+
console.log(hit?.hit ? `HIT${hit.stale ? ' stale' : ''}` : 'MISS');
|
|
182
|
+
}
|
|
183
|
+
process.exit(hit?.hit ? 0 : 1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function get(opts) {
|
|
187
|
+
const hit = read(opts);
|
|
188
|
+
if (!hit?.hit || hit.content === undefined) {
|
|
189
|
+
if (opts.json) {
|
|
190
|
+
console.log(JSON.stringify({ hit: false, stale: Boolean(hit?.stale), key: keyFromOptions(opts) }, null, 2));
|
|
191
|
+
} else {
|
|
192
|
+
console.log('MISS');
|
|
193
|
+
}
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
if (opts.output) {
|
|
197
|
+
writeFileSync(opts.output, hit.content, 'utf8');
|
|
198
|
+
}
|
|
199
|
+
if (opts.json) {
|
|
200
|
+
console.log(JSON.stringify({ hit: true, stale: hit.stale, entry: hit.entry, content: opts.output ? undefined : hit.content }, null, 2));
|
|
201
|
+
} else if (opts.output) {
|
|
202
|
+
console.log(`WROTE ${opts.output}`);
|
|
203
|
+
} else {
|
|
204
|
+
process.stdout.write(hit.content);
|
|
205
|
+
if (!hit.content.endsWith('\n')) process.stdout.write('\n');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function put(opts) {
|
|
210
|
+
if (!opts.input) throw new Error('cache:put requires --input <text|@file|->');
|
|
211
|
+
const content = readInput(opts.input);
|
|
212
|
+
const entry = opts.url
|
|
213
|
+
? putJobDescriptionCache(opts.url, content, {
|
|
214
|
+
kind: opts.kind || JD_CACHE_KIND,
|
|
215
|
+
contentType: opts.contentType,
|
|
216
|
+
ttlMs: opts.ttlMs,
|
|
217
|
+
expiresAt: opts.expiresAt,
|
|
218
|
+
metadata: opts.metadata,
|
|
219
|
+
}, PROJECT_DIR)
|
|
220
|
+
: putJobForgeCache(requiredKey(opts), content, {
|
|
221
|
+
kind: opts.kind,
|
|
222
|
+
contentType: opts.contentType,
|
|
223
|
+
ttlMs: opts.expiresAt ? undefined : opts.ttlMs,
|
|
224
|
+
expiresAt: opts.expiresAt,
|
|
225
|
+
metadata: opts.metadata,
|
|
226
|
+
}, PROJECT_DIR);
|
|
227
|
+
if (opts.json) {
|
|
228
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
console.log(`STORED ${entry.key} ${entry.contentHash}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function list(opts) {
|
|
235
|
+
const entries = listJobForgeCache({
|
|
236
|
+
kind: opts.kind,
|
|
237
|
+
includeExpired: opts.includeExpired,
|
|
238
|
+
}, PROJECT_DIR);
|
|
239
|
+
if (opts.json) {
|
|
240
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
console.log(formatCacheEntries(entries));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function verify(opts) {
|
|
247
|
+
const result = verifyJobForgeCache(PROJECT_DIR);
|
|
248
|
+
if (opts.json) {
|
|
249
|
+
console.log(JSON.stringify(result, null, 2));
|
|
250
|
+
} else {
|
|
251
|
+
console.log(formatVerifyResult(result));
|
|
252
|
+
}
|
|
253
|
+
process.exit(result.ok ? 0 : 1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function prune(opts) {
|
|
257
|
+
const result = pruneJobForgeCache({
|
|
258
|
+
expired: opts.expired || undefined,
|
|
259
|
+
dryRun: opts.dryRun,
|
|
260
|
+
}, PROJECT_DIR);
|
|
261
|
+
if (opts.json) {
|
|
262
|
+
console.log(JSON.stringify(result, null, 2));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
console.log(formatPruneResult(result));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function read(opts) {
|
|
269
|
+
if (opts.url) {
|
|
270
|
+
return readJobDescriptionCache(opts.url, { allowExpired: opts.allowExpired }, PROJECT_DIR);
|
|
271
|
+
}
|
|
272
|
+
return readJobForgeCache(requiredKey(opts), { allowExpired: opts.allowExpired }, PROJECT_DIR);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function keyFromOptions(opts) {
|
|
276
|
+
if (opts.url) return jobDescriptionCacheKey(opts.url);
|
|
277
|
+
return requiredKey(opts);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function requiredKey(opts) {
|
|
281
|
+
if (!opts.key) throw new Error('expected --url or --key');
|
|
282
|
+
return opts.key;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function readInput(input) {
|
|
286
|
+
if (input === '-') return readFileSync(0, 'utf8');
|
|
287
|
+
if (input.startsWith('@')) return readFileSync(input.slice(1), 'utf8');
|
|
288
|
+
return input;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseMetadata(raw) {
|
|
292
|
+
const text = raw.startsWith('@') ? readFileSync(raw.slice(1), 'utf8') : raw;
|
|
293
|
+
const parsed = JSON.parse(text);
|
|
294
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
295
|
+
throw new Error('--meta must be a JSON object');
|
|
296
|
+
}
|
|
297
|
+
return parsed;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function parseDuration(raw) {
|
|
301
|
+
const match = /^(\d+)(ms|s|m|h|d)?$/.exec(String(raw).trim());
|
|
302
|
+
if (!match) throw new Error('--ttl must be a duration like 14d, 2h, 30m, 10s, or 500ms');
|
|
303
|
+
const value = Number(match[1]);
|
|
304
|
+
const unit = match[2] || 'ms';
|
|
305
|
+
const multipliers = {
|
|
306
|
+
ms: 1,
|
|
307
|
+
s: 1000,
|
|
308
|
+
m: 60 * 1000,
|
|
309
|
+
h: 60 * 60 * 1000,
|
|
310
|
+
d: 24 * 60 * 60 * 1000,
|
|
311
|
+
};
|
|
312
|
+
return value * multipliers[unit];
|
|
313
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"npx job-forge ledger:*",
|
|
12
12
|
"npx job-forge capabilities:*",
|
|
13
13
|
"npx job-forge context:*",
|
|
14
|
+
"npx job-forge cache:*",
|
|
14
15
|
"rg *"
|
|
15
16
|
],
|
|
16
17
|
"deny": [
|
|
@@ -56,7 +57,8 @@
|
|
|
56
57
|
"npx job-forge merge",
|
|
57
58
|
"npx job-forge verify",
|
|
58
59
|
"npx job-forge ledger:*",
|
|
59
|
-
"npx job-forge capabilities:*"
|
|
60
|
+
"npx job-forge capabilities:*",
|
|
61
|
+
"npx job-forge cache:*"
|
|
60
62
|
],
|
|
61
63
|
"deny": [
|
|
62
64
|
"task *"
|
package/templates/context.json
CHANGED
|
@@ -9,11 +9,20 @@
|
|
|
9
9
|
"description": "Shared JobForge orchestration contract.",
|
|
10
10
|
"tokenBudget": 4000,
|
|
11
11
|
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "AGENTS.harness.md",
|
|
14
|
+
"maxTokens": 3500,
|
|
15
|
+
"required": false,
|
|
16
|
+
"notes": [
|
|
17
|
+
"Consumer-project symlink to the shared harness contract."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
12
20
|
{
|
|
13
21
|
"path": "iso/instructions.md",
|
|
14
22
|
"maxTokens": 3500,
|
|
23
|
+
"required": false,
|
|
15
24
|
"notes": [
|
|
16
|
-
"
|
|
25
|
+
"Harness-source fallback; mode/reference files should be selected through narrower bundles."
|
|
17
26
|
]
|
|
18
27
|
}
|
|
19
28
|
]
|