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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# JobForge Profile Configuration
|
|
2
|
+
# Copy this file to config/profile.yml and fill in your details.
|
|
3
|
+
# This is the single source of truth for your personal data across all modes.
|
|
4
|
+
|
|
5
|
+
candidate:
|
|
6
|
+
full_name: "Jane Smith"
|
|
7
|
+
email: "jane@example.com"
|
|
8
|
+
phone: "+1-555-0123"
|
|
9
|
+
location: "San Francisco, CA"
|
|
10
|
+
linkedin: "linkedin.com/in/janesmith"
|
|
11
|
+
portfolio_url: "https://janesmith.dev"
|
|
12
|
+
github: "github.com/janesmith"
|
|
13
|
+
twitter: "https://x.com/janesmith"
|
|
14
|
+
|
|
15
|
+
target_roles:
|
|
16
|
+
# Your North Star roles — what you're optimizing for
|
|
17
|
+
primary:
|
|
18
|
+
- "Senior AI Engineer"
|
|
19
|
+
- "Staff ML Engineer"
|
|
20
|
+
# Archetypes help the evaluation system score fit
|
|
21
|
+
archetypes:
|
|
22
|
+
- name: "AI/ML Engineer"
|
|
23
|
+
level: "Senior/Staff"
|
|
24
|
+
fit: "primary" # primary = dream role, secondary = good fit, adjacent = stretch
|
|
25
|
+
- name: "AI Product Manager"
|
|
26
|
+
level: "Senior"
|
|
27
|
+
fit: "secondary"
|
|
28
|
+
- name: "Solutions Architect"
|
|
29
|
+
level: "Mid-Senior"
|
|
30
|
+
fit: "adjacent"
|
|
31
|
+
|
|
32
|
+
narrative:
|
|
33
|
+
# Your professional headline (1 line)
|
|
34
|
+
headline: "ML Engineer turned AI product builder"
|
|
35
|
+
# Your exit story — what makes you unique
|
|
36
|
+
exit_story: "Built and sold my SaaS after 5 years. Now focused on applied AI at scale."
|
|
37
|
+
# Your top 3-5 superpowers
|
|
38
|
+
superpowers:
|
|
39
|
+
- "End-to-end ML pipelines"
|
|
40
|
+
- "Fast prototyping (idea to prod in 2 weeks)"
|
|
41
|
+
- "Cross-functional communication"
|
|
42
|
+
# Proof points — projects, articles, case studies with measurable impact
|
|
43
|
+
proof_points:
|
|
44
|
+
- name: "Project Alpha"
|
|
45
|
+
url: "https://janesmith.dev/project-alpha"
|
|
46
|
+
hero_metric: "Reduced inference latency 40%"
|
|
47
|
+
- name: "Open Source Tool"
|
|
48
|
+
url: "https://github.com/janesmith/tool"
|
|
49
|
+
hero_metric: "2K+ GitHub stars"
|
|
50
|
+
# Optional: dashboard/demo URL with credentials
|
|
51
|
+
# dashboard:
|
|
52
|
+
# url: "https://janesmith.dev/demo"
|
|
53
|
+
# password: "demo-2026"
|
|
54
|
+
|
|
55
|
+
compensation:
|
|
56
|
+
target_range: "$150K-200K" # Your target total comp
|
|
57
|
+
currency: "USD"
|
|
58
|
+
minimum: "$120K" # Walk-away number
|
|
59
|
+
location_flexibility: "Remote preferred, 1 week/month on-site possible"
|
|
60
|
+
|
|
61
|
+
location:
|
|
62
|
+
country: "United States"
|
|
63
|
+
city: "San Francisco"
|
|
64
|
+
timezone: "PST"
|
|
65
|
+
visa_status: "No sponsorship needed"
|
|
66
|
+
# For remote roles outside your country:
|
|
67
|
+
# onsite_availability: "1 week/month in any city"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cv-sync-check.mjs — Validates that the job-forge setup is consistent.
|
|
5
|
+
*
|
|
6
|
+
* Checks:
|
|
7
|
+
* 1. cv.md exists
|
|
8
|
+
* 2. config/profile.yml exists and has required fields
|
|
9
|
+
* 3. No hardcoded metrics in _shared.md or batch/batch-prompt.md
|
|
10
|
+
* 4. article-digest.md freshness (if exists)
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node cv-sync-check.mjs
|
|
14
|
+
* npm run sync-check
|
|
15
|
+
* npm run sync-check -- --help
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
19
|
+
import { join, dirname } from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
// Consumer's project root. When installed as a package, we operate on cwd.
|
|
24
|
+
const projectRoot = process.env.JOB_FORGE_PROJECT || process.cwd();
|
|
25
|
+
// Harness dir (where shipped files like modes/_shared.md live). When installed
|
|
26
|
+
// as a package, the consumer has modes/ as a symlink, so either path resolves.
|
|
27
|
+
const harnessRoot = __dirname;
|
|
28
|
+
|
|
29
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
30
|
+
console.log(`cv-sync-check.mjs — optional setup lint for a personalized clone
|
|
31
|
+
|
|
32
|
+
Checks that cv.md and config/profile.yml exist, scans modes/_shared.md and
|
|
33
|
+
batch/batch-prompt.md for lines that look like hardcoded metrics, and warns
|
|
34
|
+
if article-digest.md is stale when present.
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
node cv-sync-check.mjs
|
|
38
|
+
npm run sync-check
|
|
39
|
+
|
|
40
|
+
Exits with code 1 if cv.md or config/profile.yml is missing; exits 0 when
|
|
41
|
+
those exist (warnings only). Not part of the default PR gate — see CONTRIBUTING.md.
|
|
42
|
+
|
|
43
|
+
Run from the repository root.`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const warnings = [];
|
|
48
|
+
const errors = [];
|
|
49
|
+
|
|
50
|
+
// 1. Check cv.md exists
|
|
51
|
+
const cvPath = join(projectRoot, 'cv.md');
|
|
52
|
+
if (!existsSync(cvPath)) {
|
|
53
|
+
errors.push('cv.md not found in project root. Create it with your CV in markdown format.');
|
|
54
|
+
} else {
|
|
55
|
+
const cvContent = readFileSync(cvPath, 'utf-8');
|
|
56
|
+
if (cvContent.trim().length < 100) {
|
|
57
|
+
warnings.push('cv.md seems too short. Make sure it contains your full CV.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Check profile.yml exists
|
|
62
|
+
const profilePath = join(projectRoot, 'config', 'profile.yml');
|
|
63
|
+
if (!existsSync(profilePath)) {
|
|
64
|
+
errors.push('config/profile.yml not found. Copy from config/profile.example.yml and fill in your details.');
|
|
65
|
+
} else {
|
|
66
|
+
const profileContent = readFileSync(profilePath, 'utf-8');
|
|
67
|
+
const requiredFields = ['full_name', 'email', 'location'];
|
|
68
|
+
for (const field of requiredFields) {
|
|
69
|
+
if (!profileContent.includes(field) || profileContent.includes(`"Jane Smith"`)) {
|
|
70
|
+
warnings.push(`config/profile.yml may still have example data. Check field: ${field}`);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. Check for hardcoded metrics in prompt files
|
|
77
|
+
const filesToCheck = [
|
|
78
|
+
{ path: join(projectRoot, 'modes', '_shared.md'), name: '_shared.md' },
|
|
79
|
+
{ path: join(projectRoot, 'batch', 'batch-prompt.md'), name: 'batch-prompt.md' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// Pattern: numbers that look like hardcoded metrics (e.g., "170+ hours", "90% self-service")
|
|
83
|
+
const metricPattern = /\b\d{2,4}\+?\s*(hours?|%|evals?|layers?|tests?|fields?|bases?)\b/gi;
|
|
84
|
+
|
|
85
|
+
for (const { path, name } of filesToCheck) {
|
|
86
|
+
if (!existsSync(path)) continue;
|
|
87
|
+
const content = readFileSync(path, 'utf-8');
|
|
88
|
+
|
|
89
|
+
// Skip lines that are clearly instructions (contain "NEVER hardcode" etc.)
|
|
90
|
+
const lines = content.split('\n');
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
const line = lines[i];
|
|
93
|
+
if (line.includes('NEVER hardcode') || line.includes('NUNCA hardcode') || line.startsWith('#') || line.startsWith('<!--')) continue;
|
|
94
|
+
const matches = line.match(metricPattern);
|
|
95
|
+
if (matches) {
|
|
96
|
+
warnings.push(`${name}:${i + 1} — Possible hardcoded metric: "${matches[0]}". Should this be read from cv.md/article-digest.md?`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 4. Check article-digest.md freshness
|
|
102
|
+
const digestPath = join(projectRoot, 'article-digest.md');
|
|
103
|
+
if (existsSync(digestPath)) {
|
|
104
|
+
const stats = statSync(digestPath);
|
|
105
|
+
const daysSinceModified = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24);
|
|
106
|
+
if (daysSinceModified > 30) {
|
|
107
|
+
warnings.push(`article-digest.md is ${Math.round(daysSinceModified)} days old. Consider updating if your projects have new metrics.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Output results
|
|
112
|
+
console.log('\n=== job-forge sync check ===\n');
|
|
113
|
+
|
|
114
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
115
|
+
console.log('All checks passed.');
|
|
116
|
+
} else {
|
|
117
|
+
if (errors.length > 0) {
|
|
118
|
+
console.log(`ERRORS (${errors.length}):`);
|
|
119
|
+
errors.forEach(e => console.log(` ERROR: ${e}`));
|
|
120
|
+
}
|
|
121
|
+
if (warnings.length > 0) {
|
|
122
|
+
console.log(`\nWARNINGS (${warnings.length}):`);
|
|
123
|
+
warnings.forEach(w => console.log(` WARN: ${w}`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('');
|
|
128
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dedup-tracker.mjs — Remove duplicate entries from the application tracker
|
|
4
|
+
*
|
|
5
|
+
* Supports both layouts:
|
|
6
|
+
* - Day-based: data/applications/YYYY-MM-DD.md (preferred)
|
|
7
|
+
* - Single-file: data/applications.md or applications.md (legacy)
|
|
8
|
+
*
|
|
9
|
+
* Groups by normalized company + fuzzy role match.
|
|
10
|
+
* Keeps entry with highest score. If discarded entry had more advanced status,
|
|
11
|
+
* preserves that status. Merges notes.
|
|
12
|
+
*
|
|
13
|
+
* Run: node dedup-tracker.mjs [--dry-run] (from repo root)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, copyFileSync, existsSync } from 'fs';
|
|
17
|
+
import { join, relative, dirname } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import {
|
|
20
|
+
PROJECT_DIR, DATA_APPS_DIR, DATA_APPS_FILE, ROOT_APPS_FILE,
|
|
21
|
+
usesDayFiles, ensureDayDir, getHeader, formatAppLine, parseAppLine,
|
|
22
|
+
readAllEntries, writeToDayFiles, listDayFiles, dayFilePath,
|
|
23
|
+
} from './tracker-lib.mjs';
|
|
24
|
+
|
|
25
|
+
const DRY_RUN = process.argv.includes('--dry-run');
|
|
26
|
+
|
|
27
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
28
|
+
console.log(`dedup-tracker.mjs — remove duplicate tracker rows by company and role
|
|
29
|
+
|
|
30
|
+
Supports day-based (data/applications/YYYY-MM-DD.md) and single-file layouts.
|
|
31
|
+
Keeps the highest-scoring row per cluster; may promote status when a removed
|
|
32
|
+
row was further along in the pipeline. Merges notes where applicable.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
node dedup-tracker.mjs [--dry-run]
|
|
36
|
+
npm run dedup [-- --dry-run]
|
|
37
|
+
|
|
38
|
+
Exits successfully when no tracker exists (nothing to do).
|
|
39
|
+
Creates a .bak copy next to the tracker before writing (single-file mode).
|
|
40
|
+
|
|
41
|
+
Run from the repository root.`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const STATUS_RANK = {
|
|
46
|
+
'skip': 0,
|
|
47
|
+
'discarded': 0,
|
|
48
|
+
'rejected': 1,
|
|
49
|
+
'evaluated': 2,
|
|
50
|
+
'applied': 3,
|
|
51
|
+
'contacted': 3.5,
|
|
52
|
+
'responded': 4,
|
|
53
|
+
'interview': 5,
|
|
54
|
+
'offer': 6,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function normalizeCompany(name) {
|
|
58
|
+
return name.toLowerCase()
|
|
59
|
+
.replace(/[()]/g, '')
|
|
60
|
+
.replace(/\s+/g, ' ')
|
|
61
|
+
.replace(/[^a-z0-9 ]/g, '')
|
|
62
|
+
.trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeRole(role) {
|
|
66
|
+
return role.toLowerCase()
|
|
67
|
+
.replace(/[()]/g, ' ')
|
|
68
|
+
.replace(/\s+/g, ' ')
|
|
69
|
+
.replace(/[^a-z0-9 /]/g, '')
|
|
70
|
+
.trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function roleMatch(a, b) {
|
|
74
|
+
const wordsA = normalizeRole(a).split(/\s+/).filter(w => w.length > 3);
|
|
75
|
+
const wordsB = normalizeRole(b).split(/\s+/).filter(w => w.length > 3);
|
|
76
|
+
const overlap = wordsA.filter(w => wordsB.some(wb => wb.includes(w) || w.includes(wb)));
|
|
77
|
+
return overlap.length >= 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseScore(s) {
|
|
81
|
+
const m = s.replace(/\*\*/g, '').match(/([\d.]+)/);
|
|
82
|
+
return m ? parseFloat(m[1]) : 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Read entries
|
|
86
|
+
const { entries, source } = readAllEntries();
|
|
87
|
+
if (entries.length === 0) {
|
|
88
|
+
console.log('No tracker entries found. Nothing to dedup.');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`📊 ${entries.length} entries loaded from ${source === 'day' ? 'day files' : 'single file'}`);
|
|
93
|
+
|
|
94
|
+
// Group by company+role
|
|
95
|
+
const groups = new Map();
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
const key = normalizeCompany(entry.company);
|
|
98
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
99
|
+
groups.get(key).push(entry);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Find duplicates
|
|
103
|
+
let removed = 0;
|
|
104
|
+
const toRemove = new Set(); // entry.num values to remove
|
|
105
|
+
const statusUpdates = new Map(); // num → new status
|
|
106
|
+
|
|
107
|
+
for (const [company, companyEntries] of groups) {
|
|
108
|
+
if (companyEntries.length < 2) continue;
|
|
109
|
+
|
|
110
|
+
const processed = new Set();
|
|
111
|
+
for (let i = 0; i < companyEntries.length; i++) {
|
|
112
|
+
if (processed.has(i)) continue;
|
|
113
|
+
const cluster = [companyEntries[i]];
|
|
114
|
+
processed.add(i);
|
|
115
|
+
|
|
116
|
+
for (let j = i + 1; j < companyEntries.length; j++) {
|
|
117
|
+
if (processed.has(j)) continue;
|
|
118
|
+
if (roleMatch(companyEntries[i].role, companyEntries[j].role)) {
|
|
119
|
+
cluster.push(companyEntries[j]);
|
|
120
|
+
processed.add(j);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (cluster.length < 2) continue;
|
|
125
|
+
|
|
126
|
+
// Keep the one with highest score
|
|
127
|
+
cluster.sort((a, b) => parseScore(b.score) - parseScore(a.score));
|
|
128
|
+
const keeper = cluster[0];
|
|
129
|
+
|
|
130
|
+
// Check if any removed entry has more advanced status
|
|
131
|
+
let bestStatusRank = STATUS_RANK[keeper.status.toLowerCase()] || 0;
|
|
132
|
+
let bestStatus = keeper.status;
|
|
133
|
+
for (let k = 1; k < cluster.length; k++) {
|
|
134
|
+
const rank = STATUS_RANK[cluster[k].status.toLowerCase()] || 0;
|
|
135
|
+
if (rank > bestStatusRank) {
|
|
136
|
+
bestStatusRank = rank;
|
|
137
|
+
bestStatus = cluster[k].status;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (bestStatus !== keeper.status) {
|
|
142
|
+
statusUpdates.set(keeper.num, bestStatus);
|
|
143
|
+
console.log(` 📝 #${keeper.num}: status promoted to "${bestStatus}" (from #${cluster.find(e => e.status === bestStatus)?.num})`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Mark duplicates for removal
|
|
147
|
+
for (let k = 1; k < cluster.length; k++) {
|
|
148
|
+
const dup = cluster[k];
|
|
149
|
+
toRemove.add(dup.num);
|
|
150
|
+
removed++;
|
|
151
|
+
console.log(`🗑️ Remove #${dup.num} (${dup.company} — ${dup.role}, ${dup.score}) → kept #${keeper.num} (${keeper.score})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(`\n📊 ${removed} duplicates found`);
|
|
157
|
+
|
|
158
|
+
if (!DRY_RUN && (removed > 0 || statusUpdates.size > 0)) {
|
|
159
|
+
if (source === 'day') {
|
|
160
|
+
// Filter out removed entries and apply status updates, then rewrite
|
|
161
|
+
const kept = entries
|
|
162
|
+
.filter(e => !toRemove.has(e.num))
|
|
163
|
+
.map(e => {
|
|
164
|
+
if (statusUpdates.has(e.num)) {
|
|
165
|
+
return { ...e, status: statusUpdates.get(e.num) };
|
|
166
|
+
}
|
|
167
|
+
return e;
|
|
168
|
+
});
|
|
169
|
+
writeToDayFiles(kept);
|
|
170
|
+
console.log(`✅ Written to day files`);
|
|
171
|
+
} else {
|
|
172
|
+
// Single-file mode
|
|
173
|
+
const APPS_FILE = existsSync(DATA_APPS_FILE) ? DATA_APPS_FILE : ROOT_APPS_FILE;
|
|
174
|
+
const appsDisplay = relative(PROJECT_DIR, APPS_FILE).replace(/\\/g, '/');
|
|
175
|
+
copyFileSync(APPS_FILE, APPS_FILE + '.bak');
|
|
176
|
+
|
|
177
|
+
let content = readFileSync(APPS_FILE, 'utf-8');
|
|
178
|
+
const lines = content.split('\n');
|
|
179
|
+
|
|
180
|
+
const updatedLines = [];
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
const app = parseAppLine(line);
|
|
183
|
+
if (app && toRemove.has(app.num)) continue; // skip removed
|
|
184
|
+
if (app && statusUpdates.has(app.num)) {
|
|
185
|
+
const newStatus = statusUpdates.get(app.num);
|
|
186
|
+
const parts = line.split('|').map(s => s.trim());
|
|
187
|
+
parts[6] = newStatus;
|
|
188
|
+
updatedLines.push('| ' + parts.slice(1, -1).join(' | ') + ' |');
|
|
189
|
+
} else {
|
|
190
|
+
updatedLines.push(line);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
writeFileSync(APPS_FILE, updatedLines.join('\n'));
|
|
195
|
+
console.log(`✅ Written to ${appsDisplay} (backup: ${appsDisplay}.bak)`);
|
|
196
|
+
}
|
|
197
|
+
} else if (DRY_RUN) {
|
|
198
|
+
console.log('(dry-run — no changes written)');
|
|
199
|
+
} else {
|
|
200
|
+
console.log('✅ No duplicates found');
|
|
201
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Package architecture (v2.0.0+)
|
|
4
|
+
|
|
5
|
+
JobForge ships as an npm package. There are two kinds of repo involved:
|
|
6
|
+
|
|
7
|
+
- **Harness** — this repo, `razroo/JobForge`. Installable via `github:razroo/JobForge` (no npm registry). Contains modes, scripts, skill router, templates, fonts, dashboard, and bin entries.
|
|
8
|
+
- **Consumer project** — what users interact with day-to-day. Scaffolded via `npx create-job-forge <dir>`, or hand-authored with `job-forge` listed in `package.json` dependencies.
|
|
9
|
+
|
|
10
|
+
The consumer's project root contains only personal data:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
my-search/
|
|
14
|
+
├── package.json # depends on "job-forge"
|
|
15
|
+
├── opencode.json # instructions: ["templates/states.yml"]
|
|
16
|
+
├── cv.md # personal
|
|
17
|
+
├── config/profile.yml # personal
|
|
18
|
+
├── portals.yml # personal
|
|
19
|
+
├── data/ # personal (gitignored)
|
|
20
|
+
├── reports/ # personal (gitignored)
|
|
21
|
+
├── modes/ # → symlink to node_modules/job-forge/modes/
|
|
22
|
+
├── templates/ # → symlink to node_modules/job-forge/templates/
|
|
23
|
+
├── .opencode/skills/job-forge.md # → symlink
|
|
24
|
+
├── batch/batch-prompt.md # → symlink
|
|
25
|
+
├── batch/batch-runner.sh # → symlink
|
|
26
|
+
└── node_modules/job-forge/ # harness, fetched from github
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Symlinks are created by the harness's `postinstall` hook (`bin/sync.mjs`) on every `npm install`. They are gitignored in the scaffolder template. Real files at those paths are preserved — if a user locally customizes a mode file, the sync skips that symlink and warns.
|
|
30
|
+
|
|
31
|
+
The consumer's `opencode.json` loads a small set of stable files as always-present instructions: `AGENTS.harness.md` (harness operational rules), `templates/states.yml` (canonical application states), `modes/_shared.md` (scoring model), and `cv.md` (the candidate's CV). Caching these in the prefix means agents never Read them as tool calls. Churning content (score calibration anchors, specific mode files) stays out of `instructions` and is Read on demand.
|
|
32
|
+
|
|
33
|
+
The skill router (`.opencode/skills/job-forge.md`) loads mode and data files on demand, keeping per-session input tokens low (~20-40K for most modes instead of ~130-170K when everything was force-loaded).
|
|
34
|
+
|
|
35
|
+
**Cost-tiered subagents** live in `.opencode/agents/` (`general-free`, `general-paid`, `glm-minimal`) — the orchestrator delegates procedural work to free-tier models and reserves paid models for quality-sensitive writing. See [MODEL-ROUTING.md](MODEL-ROUTING.md) for the routing architecture, why it exists, and how to customize.
|
|
36
|
+
|
|
37
|
+
**Upgrading** the harness in a consumer project is `npm run update-harness` — fetches the latest harness (`github:razroo/JobForge`) and `@razroo/opencode-model-fallback` plugin, re-runs symlink sync, and prints the resolved commit SHA.
|
|
38
|
+
|
|
39
|
+
## System Overview
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
┌─────────────────────────────────┐
|
|
43
|
+
│ opencode Agent │
|
|
44
|
+
│ (reads OPENCODE.md + modes/*.md) │
|
|
45
|
+
└──────────┬──────────────────────┘
|
|
46
|
+
│
|
|
47
|
+
┌──────────────────┼──────────────────────┐
|
|
48
|
+
│ │ │
|
|
49
|
+
┌──────▼──────┐ ┌──────▼──────┐ ┌───────────▼────────┐
|
|
50
|
+
│ Single Eval │ │ Portal Scan │ │ Batch Process │
|
|
51
|
+
│ (auto-pipe) │ │ (scan.md) │ │ (batch-runner) │
|
|
52
|
+
└──────┬──────┘ └──────┬──────┘ └───────────┬────────┘
|
|
53
|
+
│ │ │
|
|
54
|
+
│ ┌─────────▼─────────┐ ┌────▼─────┐
|
|
55
|
+
│ │ data/pipeline.md │ │ N workers│
|
|
56
|
+
│ │ (URL inbox) │ │ (opencode run)
|
|
57
|
+
│ └─────────┬─────────┘ └────┬─────┘
|
|
58
|
+
│ │
|
|
59
|
+
┌──────▼──────────────────────────────────────────▼──────┐
|
|
60
|
+
│ Output Pipeline │
|
|
61
|
+
│ ┌──────────┐ ┌────────────┐ ┌───────────────────┐ │
|
|
62
|
+
│ │ Report.md│ │ PDF (HTML │ │ Tracker TSV │ │
|
|
63
|
+
│ │ (A-F eval)│ │ → Geometra) │ │ (merge-tracker) │ │
|
|
64
|
+
│ └──────────┘ └────────────┘ └───────────────────┘ │
|
|
65
|
+
└────────────────────────────────────────────────────────┘
|
|
66
|
+
│
|
|
67
|
+
┌──────────▼──────────┐
|
|
68
|
+
│ data/applications/ │
|
|
69
|
+
│ (day-based tracker) │
|
|
70
|
+
└──────────────────────┘
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Modes (`modes/`)
|
|
74
|
+
|
|
75
|
+
Markdown mode files in `modes/` define how the opencode workflow behaves together with the root `OPENCODE.md`. **`_shared.md`** is the shared layer (archetypes, scoring dimensions, negotiation scaffolding); the rest align with `/job-forge` command entry points listed in `OPENCODE.md`.
|
|
76
|
+
|
|
77
|
+
| File | Focus |
|
|
78
|
+
|------|--------|
|
|
79
|
+
| `_shared.md` | Archetypes, evaluation axes, shared prompts |
|
|
80
|
+
| `auto-pipeline.md` | Default path: evaluate, report, PDF, tracker |
|
|
81
|
+
| `offer.md` | Single-offer analysis |
|
|
82
|
+
| `compare.md` | Comparing multiple offers |
|
|
83
|
+
| `contact.md` | Outreach (e.g. LinkedIn) |
|
|
84
|
+
| `deep.md` | Company research |
|
|
85
|
+
| `pdf.md` | CV / PDF generation |
|
|
86
|
+
| `training.md` | Courses and certifications |
|
|
87
|
+
| `project.md` | Portfolio projects |
|
|
88
|
+
| `tracker.md` | Application tracker review |
|
|
89
|
+
| `apply.md` | Application forms |
|
|
90
|
+
| `scan.md` | Portal / job-board scanning |
|
|
91
|
+
| `pipeline.md` | Pending URL inbox |
|
|
92
|
+
| `batch.md` | Parallel batch runs (`batch/batch-runner.sh`) |
|
|
93
|
+
| `followup.md` | Follow-up triage |
|
|
94
|
+
| `rejection.md` | Rejection handling |
|
|
95
|
+
| `negotiation.md` | Offer negotiation |
|
|
96
|
+
|
|
97
|
+
For customization (archetypes, weights, tone), start with `_shared.md` and [CUSTOMIZATION.md](CUSTOMIZATION.md).
|
|
98
|
+
|
|
99
|
+
## Evaluation Flow (Single Offer)
|
|
100
|
+
|
|
101
|
+
1. **Input**: User pastes JD text or URL
|
|
102
|
+
2. **Extract**: Geometra MCP/WebFetch extracts JD from URL
|
|
103
|
+
3. **Classify**: Detect archetype (one row from the archetype table in `modes/_shared.md`)
|
|
104
|
+
4. **Evaluate**: 6 blocks (A-F).
|
|
105
|
+
- A: Role summary.
|
|
106
|
+
- B: CV match (gaps + mitigation).
|
|
107
|
+
- C: Level strategy.
|
|
108
|
+
- D: Comp research (WebSearch).
|
|
109
|
+
- E: CV personalization plan.
|
|
110
|
+
- F: Interview prep (STAR stories).
|
|
111
|
+
5. **Score**: Weighted average across 10 dimensions (1-5)
|
|
112
|
+
6. **Report**: Save as `reports/{num}-{company}-{date}.md`
|
|
113
|
+
7. **PDF**: Generate ATS-optimized CV (`generate-pdf.mjs`)
|
|
114
|
+
8. **Track**: Write one TSV per evaluation under `batch/tracker-additions/` (see [OPENCODE.md](../OPENCODE.md) TSV layout); fold rows into `data/applications.md` with `npm run merge` / `merge-tracker.mjs` when you are ready (not automatic in every workflow)
|
|
115
|
+
|
|
116
|
+
## Batch Processing
|
|
117
|
+
|
|
118
|
+
The batch system processes multiple offers in parallel:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
batch-input.tsv → batch-runner.sh → N × opencode run workers
|
|
122
|
+
(id, url, source, notes) (orchestrator) (self-contained prompt)
|
|
123
|
+
│
|
|
124
|
+
batch-state.tsv
|
|
125
|
+
(tracks progress)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Each worker is a headless opencode instance (`opencode run`) that receives the full `batch-prompt.md` as context. Workers produce:
|
|
129
|
+
- Report .md
|
|
130
|
+
- PDF
|
|
131
|
+
- Tracker TSV line
|
|
132
|
+
|
|
133
|
+
The orchestrator manages parallelism, state, retries, and resume.
|
|
134
|
+
|
|
135
|
+
**Local batch artifacts:** `batch/batch-input.tsv`, `batch/batch-state.tsv`, `batch/logs/`, and `batch/tracker-additions/*.tsv` are created when you run the runner; they are gitignored (with `.gitkeep` in `batch/logs/` and `batch/tracker-additions/`). A fresh clone ships `batch/batch-runner.sh` and `batch/batch-prompt.md` only until you add an input file — see [`batch/README.md`](../batch/README.md) and `batch/batch-runner.sh --help` for the TSV layout and workflow.
|
|
136
|
+
|
|
137
|
+
## Data Flow
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
cv.md → Evaluation context
|
|
141
|
+
article-digest.md → Proof points for matching
|
|
142
|
+
config/profile.yml → Candidate identity
|
|
143
|
+
portals.yml → Scanner configuration
|
|
144
|
+
data/pipeline.md → Pending URLs and `local:jds/...` inbox (see modes/pipeline.md)
|
|
145
|
+
jds/*.md → Saved job descriptions referenced from the pipeline (`local:jds/{file}`)
|
|
146
|
+
templates/states.yml → Canonical status values
|
|
147
|
+
templates/cv-template.html → PDF generation template
|
|
148
|
+
examples/*.md → Fictional layouts only (not read by scripts; see examples/README.md)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Create `data/pipeline.md` when you start using the URL inbox (`/job-forge pipeline`); format and `local:jds/...` lines are described in [`modes/pipeline.md`](../modes/pipeline.md).
|
|
152
|
+
|
|
153
|
+
## File Naming Conventions
|
|
154
|
+
|
|
155
|
+
- Reports: `{###}-{company-slug}-{YYYY-MM-DD}.md` (3-digit zero-padded)
|
|
156
|
+
- PDFs: `cv-candidate-{company-slug}-{YYYY-MM-DD}.pdf`
|
|
157
|
+
- Tracker TSVs: `batch/tracker-additions/{num}-{company-slug}.tsv` (one file per evaluation; merged files move under `batch/tracker-additions/merged/`)
|
|
158
|
+
|
|
159
|
+
## Pipeline Integrity
|
|
160
|
+
|
|
161
|
+
From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify-pipeline.mjs`. When a tracker file exists, it validates canonical statuses (using `templates/states.yml` when that file is present and parseable), warns on probable duplicate company/role rows, checks that report column markdown links resolve to files in the repo, validates score column format (`X.X/5`, `N/A`, or `DUP`), rejects table rows with too few columns, flags markdown bold inside the score column, and warns if any `batch/tracker-additions/*.tsv` files are still waiting to be merged. It also compares state ids from `templates/states.yml` to an internal fallback list and warns when the two sets drift. **Fresh clone:** the command exits successfully when neither `data/applications.md` nor root `applications.md` exists yet; pending-TSV and states-drift checks still run so contributors see unmerged batch output early. Optional setup validation after you add `cv.md` and `config/profile.yml`: `npm run sync-check` (`cv-sync-check.mjs`).
|
|
162
|
+
|
|
163
|
+
**`verify-pipeline.mjs` checks (same order as the script header):**
|
|
164
|
+
|
|
165
|
+
1. Status column uses canonical ids (from `templates/states.yml` when parseable, else built-in ids and aliases), with no markdown bold and no dates embedded in the status cell.
|
|
166
|
+
2. Warn when multiple rows share the same normalized company + role (possible duplicates).
|
|
167
|
+
3. Report column markdown links resolve to files under the repo root.
|
|
168
|
+
4. Score column matches `X.X/5`, `N/A`, or `DUP`.
|
|
169
|
+
5. Table data rows have enough pipe-delimited columns.
|
|
170
|
+
6. No unmerged `batch/tracker-additions/*.tsv` files (warns if any remain).
|
|
171
|
+
7. Score column has no markdown bold.
|
|
172
|
+
8. Warn when state ids in `templates/states.yml` drift from the script’s built-in fallback list (or when the file exists but ids failed to parse).
|
|
173
|
+
|
|
174
|
+
When the tracker file is missing, checks 1–5 and 7 are skipped; checks 6 and 8 still run.
|
|
175
|
+
|
|
176
|
+
## Contributing touchpoints
|
|
177
|
+
|
|
178
|
+
Prefer one focused change per pull request: a single mode under `modes/`, one repository-root `.mjs` utility, documentation under `docs/`, fictional samples under [`examples/`](../examples/README.md), templates such as [`templates/portals.example.yml`](../templates/portals.example.yml), the batch flow described in [`batch/README.md`](../batch/README.md), or the Go TUI under `dashboard/` — not a repo-wide refactor across 3+ of those at once. Branch workflow, the verify + dashboard build gate, and starter ideas are in [CONTRIBUTING.md](../CONTRIBUTING.md) (**What to Contribute** and **Development**). To look for in-repo `TODO`, `FIXME`, or `HACK` markers before choosing a task, use the `rg` one-liner in [CONTRIBUTING.md — Optional: scripted agent iterations](../CONTRIBUTING.md#optional-scripted-agent-iterations). Upstream PRs MUST stay generic: do not commit real candidate data (`cv.md`, `config/profile.yml`, personalized `portals.yml`, `data/applications.md`, `reports/`, or similar paths called out in CONTRIBUTING and `.gitignore`).
|
|
179
|
+
|
|
180
|
+
**PR / maintainer gate:** Before opening a pull request against `razroo/JobForge`, run `npm run verify` and `npm run build:dashboard` (or `(cd dashboard && go build .)`) from the harness repo root (same as [CONTRIBUTING.md](../CONTRIBUTING.md#development)). For optional scripted iterations that repeat that gate and commit one small change per pass, see [`scripts/cursor-agent-loop.sh`](../scripts/cursor-agent-loop.sh) (environment variables and usage in the script header; overview in [CONTRIBUTING.md](../CONTRIBUTING.md#optional-scripted-agent-iterations)).
|
|
181
|
+
|
|
182
|
+
Scripts maintain data consistency. In a consumer project they're invoked via the `job-forge` CLI (`npx job-forge <cmd>`); in the harness repo they're also directly runnable as `node <script>.mjs`.
|
|
183
|
+
|
|
184
|
+
| Script (in harness) | CLI | Purpose |
|
|
185
|
+
|---------------------|-----|---------|
|
|
186
|
+
| `merge-tracker.mjs` | `npx job-forge merge` | Merges TSV rows from `batch/tracker-additions/` into day files under `data/applications/`, or `data/applications.md` when the directory is absent |
|
|
187
|
+
| `verify-pipeline.mjs` | `npx job-forge verify` | Health check — see the verify paragraph above |
|
|
188
|
+
| `dedup-tracker.mjs` | `npx job-forge dedup` | Removes duplicate entries by company+role |
|
|
189
|
+
| `normalize-statuses.mjs` | `npx job-forge normalize` | Maps status aliases to canonical values |
|
|
190
|
+
| `generate-pdf.mjs` | `npx job-forge pdf` | Renders HTML to PDF via Geometra MCP (`geometra_generate_pdf`) or standalone Playwright/Chromium (`npx job-forge pdf <input.html> <output.pdf>`) |
|
|
191
|
+
| `cv-sync-check.mjs` | `npx job-forge sync-check` | Setup lint: `cv.md` + `config/profile.yml`, hardcoded-metric scan on `modes/_shared.md` and `batch/batch-prompt.md`, optional `article-digest.md` freshness |
|
|
192
|
+
| `scripts/token-usage-report.mjs` | `npx job-forge tokens` | Per-session opencode token/cost report from the SQLite DB |
|
|
193
|
+
| `tracker-lib.mjs` | _(library)_ | Shared helpers for reading/writing day-based tracker files — imported by merge/dedup/verify/normalize |
|
|
194
|
+
| `bin/sync.mjs` | `npx job-forge sync` | Creates the harness symlinks in a consumer project (also runs as `postinstall`) |
|
|
195
|
+
| `bin/create-job-forge.mjs` | `npx create-job-forge <dir>` | Scaffolds a new personal project |
|
|
196
|
+
|
|
197
|
+
All scripts resolve the consumer project dir via `process.env.JOB_FORGE_PROJECT || process.cwd()`, so running the CLI from anywhere in the consumer project Just Works.
|
|
198
|
+
|
|
199
|
+
## Dashboard TUI
|
|
200
|
+
|
|
201
|
+
The `dashboard/` directory contains a standalone Go TUI application that visualizes the pipeline.
|
|
202
|
+
|
|
203
|
+
**Repo root:** The program needs the path to the JobForge checkout (the directory that contains `modes/`, `reports/`, and the tracker). Flag `-path` sets that directory (default `.`, i.e. the process working directory). If you run the binary from inside `dashboard/` after `go build`, use `-path ..` so the tracker is found.
|
|
204
|
+
|
|
205
|
+
**Tracker file:** Day-based directory `data/applications/` (preferred) with `YYYY-MM-DD.md` files. Falls back to single-file `data/applications.md` or root `applications.md` for legacy setups.
|
|
206
|
+
|
|
207
|
+
**Build / run** (see also [SETUP.md](SETUP.md#build-dashboard-optional)):
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
cd dashboard && go build -o job-forge-dashboard .
|
|
211
|
+
./job-forge-dashboard -path ..
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**UI:**
|
|
215
|
+
|
|
216
|
+
- Filter tabs: All, Evaluated, Applied, Interview, Top ≥4, SKIP
|
|
217
|
+
- Sort modes: Score, Date, Company, Status
|
|
218
|
+
- Grouped/flat view
|
|
219
|
+
- Lazy-loaded report previews
|
|
220
|
+
- Inline status picker; on-screen key hints at the bottom of the pipeline view
|