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.
- package/.codex/config.toml +8 -0
- package/.cursor/mcp.json +21 -0
- package/.cursor/rules/main.mdc +519 -0
- package/.mcp.json +21 -0
- package/.opencode/agents/general-free.md +85 -0
- package/.opencode/agents/general-paid.md +39 -0
- package/.opencode/agents/glm-minimal.md +50 -0
- package/.opencode/skills/job-forge.md +185 -0
- package/AGENTS.md +514 -0
- package/CLAUDE.md +514 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/batch/README.md +60 -0
- package/batch/batch-prompt.md +399 -0
- package/batch/batch-runner.sh +673 -0
- package/bin/create-job-forge.mjs +375 -0
- package/bin/job-forge.mjs +120 -0
- package/bin/sync.mjs +141 -0
- package/config/profile.example.yml +67 -0
- package/cv-sync-check.mjs +128 -0
- package/dedup-tracker.mjs +201 -0
- package/docs/ARCHITECTURE.md +220 -0
- package/docs/CUSTOMIZATION.md +101 -0
- package/docs/MODEL-ROUTING.md +195 -0
- package/docs/README.md +54 -0
- package/docs/SETUP.md +186 -0
- package/docs/demo.gif +0 -0
- package/fonts/dm-sans-latin-ext.woff2 +0 -0
- package/fonts/dm-sans-latin.woff2 +0 -0
- package/fonts/space-grotesk-latin-ext.woff2 +0 -0
- package/fonts/space-grotesk-latin.woff2 +0 -0
- package/generate-pdf.mjs +168 -0
- package/iso/agents/general-free.md +90 -0
- package/iso/agents/general-paid.md +44 -0
- package/iso/agents/glm-minimal.md +55 -0
- package/iso/commands/job-forge.md +188 -0
- package/iso/config.json +7 -0
- package/iso/instructions.md +514 -0
- package/iso/mcp.json +15 -0
- package/merge-tracker.mjs +377 -0
- package/modes/README.md +30 -0
- package/modes/_shared-calibration.md +26 -0
- package/modes/_shared.md +272 -0
- package/modes/apply.md +257 -0
- package/modes/auto-pipeline.md +70 -0
- package/modes/batch.md +110 -0
- package/modes/compare.md +23 -0
- package/modes/contact.md +82 -0
- package/modes/deep.md +99 -0
- package/modes/followup.md +68 -0
- package/modes/negotiation.md +146 -0
- package/modes/offer.md +199 -0
- package/modes/pdf.md +121 -0
- package/modes/pipeline.md +83 -0
- package/modes/project.md +30 -0
- package/modes/rejection.md +92 -0
- package/modes/scan.md +185 -0
- package/modes/tracker.md +31 -0
- package/modes/training.md +27 -0
- package/normalize-statuses.mjs +152 -0
- package/opencode.json +28 -0
- package/package.json +78 -0
- package/scripts/add-tags.mjs +894 -0
- package/scripts/cursor-agent-loop.sh +211 -0
- package/scripts/cursor-agent-stream-format.py +134 -0
- package/scripts/next-num.mjs +33 -0
- package/scripts/release/check-source.mjs +37 -0
- package/scripts/render-report-header.mjs +78 -0
- package/scripts/session-report.mjs +129 -0
- package/scripts/slugify.mjs +27 -0
- package/scripts/today.mjs +20 -0
- package/scripts/token-usage-report.mjs +315 -0
- package/scripts/tracker-line.mjs +67 -0
- package/scripts/verify-greenhouse-urls.mjs +195 -0
- package/templates/cv-template.html +395 -0
- package/templates/portals.example.yml +3140 -0
- package/templates/states.yml +62 -0
- package/tracker-lib.mjs +257 -0
- package/verify-pipeline.mjs +267 -0
package/generate-pdf.mjs
ADDED
|
@@ -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.
|