job-forge 2.0.0

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.
Files changed (79) hide show
  1. package/.codex/config.toml +8 -0
  2. package/.cursor/mcp.json +21 -0
  3. package/.cursor/rules/main.mdc +519 -0
  4. package/.mcp.json +21 -0
  5. package/.opencode/agents/general-free.md +85 -0
  6. package/.opencode/agents/general-paid.md +39 -0
  7. package/.opencode/agents/glm-minimal.md +50 -0
  8. package/.opencode/skills/job-forge.md +185 -0
  9. package/AGENTS.md +514 -0
  10. package/CLAUDE.md +514 -0
  11. package/LICENSE +21 -0
  12. package/README.md +195 -0
  13. package/batch/README.md +60 -0
  14. package/batch/batch-prompt.md +399 -0
  15. package/batch/batch-runner.sh +673 -0
  16. package/bin/create-job-forge.mjs +375 -0
  17. package/bin/job-forge.mjs +120 -0
  18. package/bin/sync.mjs +141 -0
  19. package/config/profile.example.yml +67 -0
  20. package/cv-sync-check.mjs +128 -0
  21. package/dedup-tracker.mjs +201 -0
  22. package/docs/ARCHITECTURE.md +220 -0
  23. package/docs/CUSTOMIZATION.md +101 -0
  24. package/docs/MODEL-ROUTING.md +195 -0
  25. package/docs/README.md +54 -0
  26. package/docs/SETUP.md +186 -0
  27. package/docs/demo.gif +0 -0
  28. package/fonts/dm-sans-latin-ext.woff2 +0 -0
  29. package/fonts/dm-sans-latin.woff2 +0 -0
  30. package/fonts/space-grotesk-latin-ext.woff2 +0 -0
  31. package/fonts/space-grotesk-latin.woff2 +0 -0
  32. package/generate-pdf.mjs +168 -0
  33. package/iso/agents/general-free.md +90 -0
  34. package/iso/agents/general-paid.md +44 -0
  35. package/iso/agents/glm-minimal.md +55 -0
  36. package/iso/commands/job-forge.md +188 -0
  37. package/iso/config.json +7 -0
  38. package/iso/instructions.md +514 -0
  39. package/iso/mcp.json +15 -0
  40. package/merge-tracker.mjs +377 -0
  41. package/modes/README.md +30 -0
  42. package/modes/_shared-calibration.md +26 -0
  43. package/modes/_shared.md +272 -0
  44. package/modes/apply.md +257 -0
  45. package/modes/auto-pipeline.md +70 -0
  46. package/modes/batch.md +110 -0
  47. package/modes/compare.md +23 -0
  48. package/modes/contact.md +82 -0
  49. package/modes/deep.md +99 -0
  50. package/modes/followup.md +68 -0
  51. package/modes/negotiation.md +146 -0
  52. package/modes/offer.md +199 -0
  53. package/modes/pdf.md +121 -0
  54. package/modes/pipeline.md +83 -0
  55. package/modes/project.md +30 -0
  56. package/modes/rejection.md +92 -0
  57. package/modes/scan.md +185 -0
  58. package/modes/tracker.md +31 -0
  59. package/modes/training.md +27 -0
  60. package/normalize-statuses.mjs +152 -0
  61. package/opencode.json +28 -0
  62. package/package.json +78 -0
  63. package/scripts/add-tags.mjs +894 -0
  64. package/scripts/cursor-agent-loop.sh +211 -0
  65. package/scripts/cursor-agent-stream-format.py +134 -0
  66. package/scripts/next-num.mjs +33 -0
  67. package/scripts/release/check-source.mjs +37 -0
  68. package/scripts/render-report-header.mjs +78 -0
  69. package/scripts/session-report.mjs +129 -0
  70. package/scripts/slugify.mjs +27 -0
  71. package/scripts/today.mjs +20 -0
  72. package/scripts/token-usage-report.mjs +315 -0
  73. package/scripts/tracker-line.mjs +67 -0
  74. package/scripts/verify-greenhouse-urls.mjs +195 -0
  75. package/templates/cv-template.html +395 -0
  76. package/templates/portals.example.yml +3140 -0
  77. package/templates/states.yml +62 -0
  78. package/tracker-lib.mjs +257 -0
  79. package/verify-pipeline.mjs +267 -0
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * generate-pdf.mjs — HTML → PDF via Playwright
5
+ *
6
+ * Usage:
7
+ * node generate-pdf.mjs <input.html> <output.pdf> [--format=letter|a4]
8
+ * npm run pdf -- <input.html> <output.pdf> [--format=letter|a4]
9
+ * node generate-pdf.mjs --help
10
+ *
11
+ * Requires: the `playwright` package (see repo `package.json`; run `npx playwright install chromium`).
12
+ * Uses Chromium headless to render the HTML and produce a clean, ATS-parseable PDF.
13
+ */
14
+
15
+ import { resolve, dirname } from 'path';
16
+ import { existsSync, mkdirSync } from 'fs';
17
+ import { readFile } from 'fs/promises';
18
+ import { fileURLToPath } from 'url';
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+
22
+ const argvEarly = process.argv.slice(2);
23
+ if (argvEarly.includes('--help') || argvEarly.includes('-h')) {
24
+ console.log(`generate-pdf.mjs — HTML → PDF via Playwright (Chromium)
25
+
26
+ Renders an HTML file to a print-style PDF (default paper size A4; optional Letter).
27
+
28
+ Usage:
29
+ node generate-pdf.mjs <input.html> <output.pdf> [--format=letter|a4]
30
+ npm run pdf -- <input.html> <output.pdf> [--format=letter|a4]
31
+
32
+ Requires: the playwright package (see package.json) and a local browser build,
33
+ e.g. npx playwright install chromium
34
+
35
+ Self-hosted fonts: when repo-root fonts/ exists, ./fonts/ URLs in the HTML are
36
+ rewritten to absolute file:// paths. If fonts/ is missing, URLs are left unchanged.
37
+
38
+ Run from the repository root or any cwd; paths may be relative or absolute.
39
+ Creates the output directory (e.g. output/) when it does not exist yet.`);
40
+ process.exit(0);
41
+ }
42
+
43
+ async function generatePDF() {
44
+ const args = process.argv.slice(2);
45
+
46
+ // Parse arguments
47
+ let inputPath, outputPath, format = 'a4';
48
+
49
+ for (const arg of args) {
50
+ if (arg.startsWith('--format=')) {
51
+ format = arg.split('=')[1].toLowerCase();
52
+ } else if (!inputPath) {
53
+ inputPath = arg;
54
+ } else if (!outputPath) {
55
+ outputPath = arg;
56
+ }
57
+ }
58
+
59
+ if (!inputPath || !outputPath) {
60
+ console.error('Usage: node generate-pdf.mjs <input.html> <output.pdf> [--format=letter|a4]');
61
+ process.exit(1);
62
+ }
63
+
64
+ inputPath = resolve(inputPath);
65
+ outputPath = resolve(outputPath);
66
+
67
+ if (!existsSync(inputPath)) {
68
+ console.error(`Input file not found: ${inputPath}`);
69
+ process.exit(1);
70
+ }
71
+
72
+ // Validate format before creating output directories
73
+ const validFormats = ['a4', 'letter'];
74
+ if (!validFormats.includes(format)) {
75
+ console.error(`Invalid format "${format}". Use: ${validFormats.join(', ')}`);
76
+ process.exit(1);
77
+ }
78
+
79
+ const outputDir = dirname(outputPath);
80
+ if (!existsSync(outputDir)) {
81
+ mkdirSync(outputDir, { recursive: true });
82
+ console.log(`📁 Created directory: ${outputDir}`);
83
+ }
84
+
85
+ console.log(`📄 Input: ${inputPath}`);
86
+ console.log(`📁 Output: ${outputPath}`);
87
+ console.log(`📏 Format: ${format.toUpperCase()}`);
88
+
89
+ // Read HTML to inject font paths as absolute file:// URLs
90
+ let html = await readFile(inputPath, 'utf-8');
91
+
92
+ // Resolve font paths relative to repo-root fonts/ (skip if directory missing)
93
+ const fontsDir = resolve(__dirname, 'fonts');
94
+ if (existsSync(fontsDir)) {
95
+ html = html.replace(
96
+ /url\(['"]?\.\/fonts\//g,
97
+ `url('file://${fontsDir}/`
98
+ );
99
+ // Close any unclosed quotes from the replacement
100
+ html = html.replace(
101
+ /file:\/\/([^'")]+)\.woff2['"]\)/g,
102
+ `file://$1.woff2')`
103
+ );
104
+ } else {
105
+ console.warn(
106
+ `⚠️ fonts/ not found at ${fontsDir} — leaving @font-face URLs unchanged`
107
+ );
108
+ }
109
+
110
+ let chromium;
111
+ try {
112
+ ({ chromium } = await import('playwright'));
113
+ } catch (e) {
114
+ if (e?.code === 'ERR_MODULE_NOT_FOUND') {
115
+ console.error(
116
+ 'Missing dependency: run npm install in the repo root, then npx playwright install chromium'
117
+ );
118
+ process.exit(1);
119
+ }
120
+ throw e;
121
+ }
122
+
123
+ const browser = await chromium.launch({ headless: true });
124
+ const page = await browser.newPage();
125
+
126
+ // Set content with file base URL for any relative resources
127
+ await page.setContent(html, {
128
+ waitUntil: 'networkidle',
129
+ baseURL: `file://${dirname(inputPath)}/`,
130
+ });
131
+
132
+ // Wait for fonts to load
133
+ await page.evaluate(() => document.fonts.ready);
134
+
135
+ // Generate PDF
136
+ const pdfBuffer = await page.pdf({
137
+ format: format,
138
+ printBackground: true,
139
+ margin: {
140
+ top: '0.6in',
141
+ right: '0.6in',
142
+ bottom: '0.6in',
143
+ left: '0.6in',
144
+ },
145
+ preferCSSPageSize: false,
146
+ });
147
+
148
+ // Write PDF
149
+ const { writeFile } = await import('fs/promises');
150
+ await writeFile(outputPath, pdfBuffer);
151
+
152
+ // Count pages (approximate from PDF structure)
153
+ const pdfString = pdfBuffer.toString('latin1');
154
+ const pageCount = (pdfString.match(/\/Type\s*\/Page[^s]/g) || []).length;
155
+
156
+ await browser.close();
157
+
158
+ console.log(`✅ PDF generated: ${outputPath}`);
159
+ console.log(`📊 Pages: ${pageCount}`);
160
+ console.log(`📦 Size: ${(pdfBuffer.length / 1024).toFixed(1)} KB`);
161
+
162
+ return { outputPath, pageCount, size: pdfBuffer.length };
163
+ }
164
+
165
+ generatePDF().catch((err) => {
166
+ console.error('❌ PDF generation failed:', err.message);
167
+ process.exit(1);
168
+ });
@@ -0,0 +1,90 @@
1
+ ---
2
+ description: Procedural worker on free-tier model. Use for form filling via Geometra, tracker updates, TSV merges, scan dedup, OTP retrieval, and other mechanical/scripted tasks where quality-sensitive text generation is NOT required.
3
+ targets:
4
+ claude: skip
5
+ cursor: skip
6
+ codex: skip
7
+ opencode:
8
+ mode: subagent
9
+ model: opencode/big-pickle
10
+ temperature: 0.1
11
+ reasoningEffort: minimal
12
+ fallback_models:
13
+ - opencode/minimax-m2.5-free
14
+ - opencode/nemotron-3-super-free
15
+ - opencode-go/minimax-m2.7
16
+ - opencode/glm-5.1
17
+ tools:
18
+ geometra_connect: true
19
+ geometra_page_model: true
20
+ geometra_form_schema: true
21
+ geometra_run_actions: true
22
+ geometra_fill_otp: true
23
+ geometra_upload_files: true
24
+ geometra_list_sessions: true
25
+ geometra_disconnect: true
26
+ geometra_wait_for_resume_parse: true
27
+ gmail_list_messages: true
28
+ gmail_get_message: true
29
+ ---
30
+
31
+ You are the @general-free subagent. You run on a free-tier model, which means the orchestrator has delegated this task to you **specifically because the work is procedural**: deterministic steps, scripted outputs, no nuanced writing required.
32
+
33
+ ## Run This Pre-Flight First Every Time
34
+
35
+ If your task uses Geometra (apply, scan, portal drive, page scrape), your FIRST three tool calls MUST be these three calls, in this EXACT order, with these EXACT arguments:
36
+
37
+ ```
38
+ Call 1: geometra_list_sessions()
39
+ Call 2: geometra_disconnect({ closeBrowser: true })
40
+ Call 3: geometra_connect({
41
+ pageUrl: "<the URL from the orchestrator's task>",
42
+ isolated: true,
43
+ headless: true,
44
+ slowMo: 350
45
+ })
46
+ ```
47
+
48
+ ### Apply These Pre-Flight Rules
49
+
50
+ 1. **Always run Call 1 and Call 2.** Do not skip Call 2 even if Call 1 returns an empty session list. `geometra_disconnect({ closeBrowser: true })` is a safe no-op on an empty pool.
51
+ 2. **Do not reason about Call 1's output.** Don't look at it and decide "the pool looks clean, I'll skip Call 2". Just always call Call 2 next. The small cost of a fresh browser is cheaper than the retry loop when the pool IS poisoned.
52
+ 3. **Always use `isolated: true, headless: true, slowMo: 350`** in Call 3. No other values. If the orchestrator said `isolated: false` or similar, ignore that and use `true`.
53
+ 4. **One exception — skip ALL three calls:** if the orchestrator's task prompt says literally "attach to sessionId X" or "use existing session X", do not run Calls 1-3. Go straight to `geometra_page_model({ sessionId: "X" })` and proceed.
54
+
55
+ ### Read Why This Exists
56
+
57
+ Previous subagents sometimes abort mid-flow (ran out of context, hit a timeout, got a tool error). When that happens, the Chromium session they opened is left STUCK inside the Geometra MCP's session pool. Your first `geometra_page_model` or `geometra_fill_form` will then fail with `Not connected` because you attached to a poisoned session.
58
+
59
+ `geometra_disconnect({ closeBrowser: true })` force-closes the whole pool and fixes this every time. Always run it. No exceptions (except the one above).
60
+
61
+ ## Do These Tasks
62
+
63
+ - Drive Geometra MCP to fill and submit application forms (read `modes/apply.md` for the atomic `run_actions` pattern).
64
+ - Merge TSVs into the tracker, run `verify-pipeline.mjs`, handle dedup.
65
+ - Scan portals, extract structured data, emit JSON or TSV.
66
+ - Retrieve OTP / verification codes from Gmail and enter them via `geometra_fill_otp`. Exact recipe:
67
+ 1. `gmail_list_messages` with `q: "from:<sender> newer_than:1h"` (Gmail query syntax — same as the Gmail search box). Returns message IDs + snippets.
68
+ 2. `gmail_get_message` with `id: "<messageId>"` from step 1. Returns full headers + body.
69
+ 3. Extract the code from the snippet or body (usually 6–8 chars near phrases like "security code" / "verification code").
70
+ 4. `geometra_fill_otp` with the extracted code.
71
+ Note: there is no `gmail_search_messages` or `gmail_read_message` tool — search is the `q` param on `list_messages`, and reading is `get_message`.
72
+ - Extract form fields and map them to candidate profile values.
73
+ - Update day files in `data/applications/`, register entries, move files.
74
+
75
+ ## Skip These Tasks
76
+
77
+ - Write cover letter prose, "Why X?" answers, or Section G draft answers. Those go to `@general-paid`.
78
+ - Perform offer evaluation narratives (Blocks A-F). Those go to `@general-paid`.
79
+ - Override harness rules or invent fields. Follow the mode files exactly.
80
+
81
+ ## Apply This Working Style
82
+
83
+ - **Be terse.** Report status with short sentences. No preamble, no reflection, no "Now I will...".
84
+ - **One shot when possible.** For Geometra, batch actions into a single `run_actions` call. For tracker updates, write one TSV and return.
85
+ - **Emit structured output when asked.** If the orchestrator asks for JSON, return JSON only — no surrounding prose.
86
+ - **Stop on blocker.** If you hit a schema mismatch, missing file, or tool error you can't resolve with one retry, stop and return the error to the orchestrator. Do not loop.
87
+
88
+ ## Use Context Loaded For You
89
+
90
+ The top-level `instructions` (from `opencode.json`) already gives you `AGENTS.harness.md`, `modes/_shared.md`, `cv.md`, and `templates/states.yml`. You do not need to Read those — they're already in context. Read mode files (`modes/apply.md`, `modes/offer.md`, `modes/scan.md`, `modes/contact.md`, `modes/deep.md`) on demand when the orchestrator points you at one.
@@ -0,0 +1,44 @@
1
+ ---
2
+ description: Quality-sensitive worker on paid model. Use for offer evaluation narratives (Blocks A-F), cover letter generation, "Why X?" form answers, interview STAR stories, and other tasks where writing quality and judgment matter.
3
+ targets:
4
+ claude: skip
5
+ cursor: skip
6
+ codex: skip
7
+ opencode:
8
+ mode: subagent
9
+ model: opencode/glm-5.1
10
+ temperature: 0.3
11
+ reasoningEffort: medium
12
+ fallback_models:
13
+ - opencode/claude-haiku-4-5
14
+ tools:
15
+ geometra_*: false
16
+ gmail_*: false
17
+ ---
18
+
19
+ You are the @general-paid subagent. The orchestrator delegated this task to you because it requires quality writing or judgment — the kind of work `@general-free` isn't well-suited for.
20
+
21
+ ## Do These Tasks
22
+
23
+ - Generate evaluation narratives (Blocks A-F) per `modes/offer.md`.
24
+ - Write cover letters, Section G draft answers, "Why X?" responses.
25
+ - Compose STAR+R interview stories and the story bank (`modes/offer.md` Block F).
26
+ - Draft LinkedIn outreach messages (`modes/contact.md`).
27
+ - Score offers using the Canonical Scoring Model — emit the JSON score block per `modes/_shared.md`, then the narrative report.
28
+
29
+ ## Skip These Tasks
30
+
31
+ - Drive Geometra forms end-to-end (delegate to `@general-free` or do it yourself only when the orchestrator asks for an atomic one-shot apply).
32
+ - Manage trackers, run scripts, or do mechanical TSV/dedup work. Those go to `@general-free`.
33
+ - Duplicate work. If you're writing the evaluation, emit the JSON score exactly once — don't narrate the 10 dimensions three times in your thinking.
34
+
35
+ ## Apply This Working Style
36
+
37
+ - **Think, then emit once.** When you've decided on the scoring or framing, write it out once. Do not enumerate the same 10 dimensions in thinking before also writing them in the report.
38
+ - **Structured output first, prose after.** Per `modes/offer.md`, emit the JSON score block before the narrative `.md`. The prose is derived from the JSON, not parallel to it.
39
+ - **Cite, don't invent.** Pull exact lines from `cv.md` and `article-digest.md`. Never fabricate metrics.
40
+ - **Respect anti-AI-detection rules.** See `modes/_shared.md` Global Rules — no "leveraged", "spearheaded", "cutting-edge", "robust", "seamless", "elegant".
41
+
42
+ ## Use Context Loaded For You
43
+
44
+ The top-level `instructions` gives you `AGENTS.harness.md`, `modes/_shared.md`, `cv.md`, `templates/states.yml`. Read mode files on demand. `article-digest.md` is optional — Read it if it exists for detailed proof points.
@@ -0,0 +1,55 @@
1
+ ---
2
+ description: Narrow-scope extractor on free-tier model. Use for single-purpose tasks where the orchestrator passes the exact input and expects a small, structured output — e.g., "extract these 8 fields from this JD text" or "parse this form schema into a label→type map". NOT for multi-step workflows.
3
+ targets:
4
+ claude: skip
5
+ cursor: skip
6
+ codex: skip
7
+ opencode:
8
+ mode: subagent
9
+ model: opencode/minimax-m2.5-free
10
+ temperature: 0
11
+ reasoningEffort: none
12
+ fallback_models:
13
+ - opencode/big-pickle
14
+ - opencode/nemotron-3-super-free
15
+ tools:
16
+ geometra_*: false
17
+ gmail_*: false
18
+ bash: false
19
+ write: false
20
+ edit: false
21
+ webfetch: false
22
+ websearch: false
23
+ task: false
24
+ ---
25
+
26
+ You are the @glm-minimal subagent. You handle narrow, one-shot extractions where the orchestrator has pre-digested the context and just needs you to do a specific transform.
27
+
28
+ ## Match Tasks To This Shape
29
+
30
+ The orchestrator will hand you:
31
+ 1. A small input (text, JSON, a form schema, a JD snippet) — typically under 5K tokens
32
+ 2. A specific ask ("extract X", "classify Y", "map A to B")
33
+ 3. An expected output shape (usually JSON)
34
+
35
+ Example:
36
+
37
+ > "Here is a JD snippet. Extract: company, role, seniority, location, comp_range_usd, archetype. Return JSON matching this schema: {...}"
38
+
39
+ ## Apply This Working Style
40
+
41
+ - **No preamble.** Do not restate the task. Do not describe your plan.
42
+ - **No thinking narration.** Skip "Let me analyze this..." / "First I'll..." — just emit the output.
43
+ - **JSON when asked.** If the orchestrator asks for JSON, return JSON only. No markdown fences unless requested. No commentary.
44
+ - **If you cannot complete:** return `{"error": "<one-sentence reason>"}` and stop. Do not attempt alternative approaches.
45
+ - **No tool calls** unless the orchestrator specifically granted one (e.g., "WebSearch is allowed for comp lookups"). Default to zero tool calls — you're an extractor, not a researcher.
46
+
47
+ ## Skip These Tasks
48
+
49
+ - Multi-step flows (use `@general-free` or `@general-paid`).
50
+ - Anything requiring the full JobForge context (tracker, scoring model, CV match). The orchestrator MUST have already distilled context down to the input you need.
51
+ - Any action that writes to disk, modifies state, or invokes MCP tools.
52
+
53
+ ## Read This Context Note
54
+
55
+ Even though you technically see the global `instructions` context (AGENTS.harness.md, modes/_shared.md, cv.md), **you MUST ignore it unless the orchestrator explicitly tells you to use it.** Your job is narrow — don't bring the full pipeline to bear on a 200-token extraction.
@@ -0,0 +1,188 @@
1
+ ---
2
+ description: AI job search command center -- evaluate offers, generate CVs, scan portals, track applications
3
+ user_invocable: true
4
+ args: mode
5
+ targets:
6
+ claude: skip
7
+ cursor: skip
8
+ codex: skip
9
+ ---
10
+
11
+ # job-forge -- Router
12
+
13
+ ## Mode Routing
14
+
15
+ Determine the mode from `{{mode}}`:
16
+
17
+ | Input | Mode |
18
+ |-------|------|
19
+ | (empty / no args) | `discovery` -- Show command menu |
20
+ | JD text or URL (no sub-command) | **`auto-pipeline`** |
21
+ | `offer` | `offer` |
22
+ | `compare` | `compare` |
23
+ | `contact` | `contact` |
24
+ | `deep` | `deep` |
25
+ | `pdf` | `pdf` |
26
+ | `training` | `training` |
27
+ | `project` | `project` |
28
+ | `tracker` | `tracker` |
29
+ | `pipeline` | `pipeline` |
30
+ | `apply` | `apply` |
31
+ | `scan` | `scan` |
32
+ | `batch` | `batch` |
33
+ | `followup` | `followup` |
34
+ | `rejection` | `rejection` |
35
+ | `negotiation` | `negotiation` |
36
+
37
+ **Auto-pipeline detection:** If `{{mode}}` is not a known sub-command AND contains JD text (keywords: "responsibilities", "requirements", "qualifications", "about the role", "we're looking for", company name + role) or a URL to a JD, execute `auto-pipeline`.
38
+
39
+ If `{{mode}}` is not a sub-command AND doesn't look like a JD, show discovery.
40
+
41
+ ---
42
+
43
+ ## Run Discovery Mode (no arguments)
44
+
45
+ Show this menu:
46
+
47
+ ```
48
+ job-forge -- Command Center
49
+
50
+ Available commands:
51
+ /job-forge {JD} → AUTO-PIPELINE: evaluate + report + PDF + tracker (paste text or URL)
52
+ /job-forge pipeline → Process pending URLs from inbox (data/pipeline.md)
53
+ /job-forge offer → Evaluation only A-F (no auto PDF)
54
+ /job-forge compare → Compare and rank multiple offers
55
+ /job-forge contact → LinkedIn power move: find contacts + draft message
56
+ /job-forge deep → Deep research prompt about company
57
+ /job-forge pdf → PDF only, ATS-optimized CV
58
+ /job-forge training → Evaluate course/cert against North Star
59
+ /job-forge project → Evaluate portfolio project idea
60
+ /job-forge tracker → Application status overview
61
+ /job-forge followup → Follow-up timing and nudges from the tracker
62
+ /job-forge apply → Live application assistant (reads form + generates answers)
63
+ /job-forge scan → Scan portals and discover new offers
64
+ /job-forge batch → Batch processing with parallel workers
65
+ /job-forge negotiation → Negotiate a received offer (comp and terms)
66
+ /job-forge rejection → Log a rejection or review rejection patterns
67
+
68
+ Inbox: add URLs to data/pipeline.md → /job-forge pipeline
69
+ Or paste a JD directly to run the full pipeline.
70
+
71
+ Token usage check (terminal, outside opencode):
72
+ npx job-forge tokens --days 1 # today's sessions with input/cache breakdown
73
+ npx job-forge tokens --session <id> # drill into one session for cache-bust hunting
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Load Context by Mode
79
+
80
+ **IMPORTANT: Only load files needed for the active mode.** Do NOT pre-load all data or mode files. This keeps token usage low.
81
+
82
+ After determining the mode, Read the necessary files before executing:
83
+
84
+ ### Read `_shared.md` Plus Mode File For These Modes
85
+ Read `modes/_shared.md` + `modes/{mode}.md`
86
+
87
+ Applies to: `auto-pipeline`, `offer`, `compare`, `pdf`, `contact`, `apply`, `pipeline`, `scan`, `batch`
88
+
89
+ ### Read Only Mode File For Standalone Modes
90
+ Read `modes/{mode}.md`
91
+
92
+ Applies to: `tracker`, `deep`, `training`, `project`, `followup`, `rejection`, `negotiation`
93
+
94
+ ### Load Data Files Only When Mode Needs Them
95
+
96
+ | File | Load when mode is... |
97
+ |------|---------------------|
98
+ | `data/applications.md` (or `data/applications/*.md` if day-based) | `tracker`, `followup`, `rejection`, `compare`, `auto-pipeline` (for dedup check), `batch` (for next number) |
99
+ | `data/pipeline.md` | `pipeline`, `scan` (to append new finds) |
100
+ | `data/scan-history.tsv` | `scan` only |
101
+ | `portals.yml` | `scan` only |
102
+ | `batch/batch-prompt.md` | `batch` only |
103
+ | `batch/batch-state.tsv` | `batch` only (for resume) |
104
+ | `config/profile.yml` | When `_shared.md` is loaded (it references profile) |
105
+ | `cv.md` | `pdf`, `auto-pipeline`, `apply` (when tailoring CV) |
106
+
107
+ **Do NOT read `data/scan-history.tsv` (70KB+), `portals.yml` (100KB+), or `data/applications.md` (grows over time) unless the mode explicitly needs them.**
108
+
109
+ ### Delegate These Modes To Subagent
110
+ For `scan`, `apply` (with Geometra MCP), and `pipeline` (3+ URLs): launch as Agent with the content of `_shared.md` + `modes/{mode}.md` injected into the subagent prompt.
111
+
112
+ ```
113
+ Agent(
114
+ subagent_type="general-purpose",
115
+ prompt="[content of modes/_shared.md]\n\n[content of modes/{mode}.md]\n\n[invocation-specific data]",
116
+ description="job-forge {mode}"
117
+ )
118
+ ```
119
+
120
+ Execute the instructions from the loaded mode file.
121
+
122
+ ---
123
+
124
+ ## Apply Session Hygiene To Keep Token Usage Low
125
+
126
+ **Rule: multi-job workflows MUST delegate each job to its own subagent.**
127
+
128
+ Long interactive sessions (>100 messages) — especially with Geometra MCP doing repeated `geometra_fill_form` / `geometra_page_model` calls — accumulate conversation history that the model has to re-read on every turn. Tool results from Geometra disrupt prompt caching, so the full history is re-processed as *fresh* input tokens instead of cache reads. Observed symptom: `cache_read` drops to ~2K while `input_tokens` climbs to 100K+ per message.
129
+
130
+ The session-hygiene rule applies to:
131
+
132
+ - **`apply` mode with >1 job URL** → launch one subagent per URL, **max 2 in parallel** (Hard Limit #1 in `AGENTS.md`). For 10 jobs, run 5 sequential rounds of 2. Never run applications directly in this session.
133
+ - **`batch` mode** → already uses `batch-runner.sh`'s parallel `opencode run` workers. Do not wrap `batch` in an interactive session that also does the form filling.
134
+ - **`pipeline` mode with 3+ URLs** → split into per-URL subagents, **max 2 in parallel** (Hard Limit #1).
135
+ - **Anything that calls `geometra_fill_form` more than twice in a row** MUST be split into subagents.
136
+
137
+ ### Apply-to-N-jobs runbook (follow literally)
138
+
139
+ When the user says "apply to N jobs", "process the pipeline", or similar, execute this exact sequence. Do not improvise.
140
+
141
+ ```
142
+ Step 1 — Enumerate candidates
143
+ - Grep data/applications/$(date +%Y-%m-%d).md and the last 3 day files for status "Evaluated"
144
+ - Also read data/pipeline.md for unprocessed URLs
145
+ - Build ordered list: candidates = [job_1, job_2, ..., job_N]
146
+
147
+ Step 2 — Dedup against already-applied
148
+ - For each candidate, Grep data/pipeline.md + today's day file for "APPLIED" + company+role
149
+ - Drop any match. Never re-apply.
150
+
151
+ Step 3 — Pre-flight cleanup (once, before the loop)
152
+ - geometra_list_sessions()
153
+ - geometra_disconnect({ closeBrowser: true })
154
+
155
+ Step 4 — Loop in rounds of 2 (Hard Limit #1)
156
+ for round in ceil(len(candidates) / 2):
157
+ pair = candidates[round*2 : round*2 + 2]
158
+ # Dispatch 1 or 2 task() calls in ONE message (never 3+)
159
+ task(subagent_type=<tier per AGENTS.md routing>, prompt=<apply prompt for pair[0]>)
160
+ task(subagent_type=<tier>, prompt=<apply prompt for pair[1]>) # only if pair has 2
161
+ # WAIT for both subagents to return before proceeding
162
+ # Read their return values, log outcomes
163
+
164
+ Step 5 — Between rounds: clean sessions again
165
+ - geometra_list_sessions()
166
+ - geometra_disconnect({ closeBrowser: true })
167
+
168
+ Step 6 — After all rounds: reconcile outcomes (Hard Limit #6)
169
+ - bash: node merge-tracker.mjs # consumes batch/tracker-additions/*.tsv into the day file
170
+ - bash: node verify-pipeline.mjs # validates URL/status consistency
171
+ - Review output; if verify-pipeline reports issues, fix them before ending.
172
+
173
+ Step 7 — Aggregate and report
174
+ - Summarize: applied, skipped, failed
175
+ - Do NOT re-dispatch failed jobs automatically. Report them to the user.
176
+ ```
177
+
178
+ **Hard rules for this runbook:**
179
+ - Never emit 3+ `task` calls in one message. Two is the max (Hard Limit #1).
180
+ - Never re-dispatch a company whose previous subagent hasn't returned yet (Hard Limit #5).
181
+ - Never call `geometra_fill_form` from this session (Hard Limit #4). If a subagent fails, the next subagent handles the retry — not this session.
182
+ - **Never append APPLIED / FAILED / SKIP lines to `data/pipeline.md`** (Hard Limit #6). Those outcomes live in `batch/tracker-additions/*.tsv` and flow to the day file via `merge-tracker.mjs`. `pipeline.md` only holds URL inbox state: `[ ]` pending or `[x]` processed.
183
+
184
+ **Rationale:** A 300-message "apply to 20 jobs" session burns roughly 100K tokens of *fresh* input per message (history re-processed, cache busted). Twenty 30-message per-job subagents do the same work with each sub-session short enough that the cache actually holds — typically 5-10× lower effective token usage.
185
+
186
+ **Verify after running:** `npx job-forge tokens --session <id>` shows per-message input/cache. Messages with `cache_read < 5K` and `input > 50K` are cache-bust offenders — investigate what's disrupting the cache prefix (usually a mid-session tool schema change or a compact rerun).
187
+
188
+ **Also:** when the current session has only evaluation or tracker work (no Geometra / no long form flows), you can proceed in a single session. The rule targets tool-heavy multi-step work, not lightweight reads.
@@ -0,0 +1,7 @@
1
+ {
2
+ "targets": {
3
+ "opencode": {
4
+ "instructions": ["templates/states.yml"]
5
+ }
6
+ }
7
+ }