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,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* merge-tracker.mjs — Merge batch tracker additions into 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
|
+
* Handles multiple TSV formats:
|
|
10
|
+
* - 9-col: num\tdate\tcompany\trole\tstatus\tscore\tpdf\treport\tnotes
|
|
11
|
+
* - 8-col: num\tdate\tcompany\trole\tstatus\tscore\tpdf\treport (no notes)
|
|
12
|
+
* - Pipe-delimited (markdown table row): | col | col | ... |
|
|
13
|
+
*
|
|
14
|
+
* Dedup: company normalized + role fuzzy match + report number match
|
|
15
|
+
* If duplicate with higher score → update in-place, update report link
|
|
16
|
+
* Validates status against templates/states.yml when present (else built-in labels)
|
|
17
|
+
*
|
|
18
|
+
* Run: node merge-tracker.mjs [--dry-run] [--verify] (from repo root)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync, writeFileSync, readdirSync, mkdirSync, renameSync, existsSync } from 'fs';
|
|
22
|
+
import { join, relative, dirname } from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import {
|
|
25
|
+
PROJECT_DIR, DATA_APPS_DIR, DATA_APPS_FILE, ROOT_APPS_FILE,
|
|
26
|
+
usesDayFiles, ensureDayDir, getHeader, formatAppLine, parseAppLine,
|
|
27
|
+
readAllEntries, writeToDayFiles, listDayFiles, dayFilePath,
|
|
28
|
+
} from './tracker-lib.mjs';
|
|
29
|
+
|
|
30
|
+
const ADDITIONS_DIR = join(PROJECT_DIR, 'batch/tracker-additions');
|
|
31
|
+
const MERGED_DIR = join(ADDITIONS_DIR, 'merged');
|
|
32
|
+
const DRY_RUN = process.argv.includes('--dry-run');
|
|
33
|
+
const VERIFY = process.argv.includes('--verify');
|
|
34
|
+
|
|
35
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
36
|
+
console.log(`merge-tracker.mjs — merge batch/tracker-additions/*.tsv into the tracker
|
|
37
|
+
|
|
38
|
+
Supports day-based (data/applications/YYYY-MM-DD.md) and single-file layouts.
|
|
39
|
+
Moves processed files to batch/tracker-additions/merged/.
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
node merge-tracker.mjs [--dry-run] [--verify]
|
|
43
|
+
npm run merge [-- --dry-run] [--verify]
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
--dry-run Show actions without writing the tracker or moving TSVs
|
|
47
|
+
--verify After merge, run verify-pipeline.mjs (ignored with --dry-run)
|
|
48
|
+
|
|
49
|
+
If the tracker file is missing but TSVs exist, creates the tracker
|
|
50
|
+
with an empty table header. If batch/tracker-additions/ is missing or has
|
|
51
|
+
no .tsv files, exits successfully with nothing to do.
|
|
52
|
+
|
|
53
|
+
Run from the repository root.`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const STATES_FILE = existsSync(join(PROJECT_DIR, 'templates/states.yml'))
|
|
58
|
+
? join(PROJECT_DIR, 'templates/states.yml')
|
|
59
|
+
: join(PROJECT_DIR, 'states.yml');
|
|
60
|
+
|
|
61
|
+
function loadCanonicalLabelsFromStatesYaml(filePath) {
|
|
62
|
+
if (!existsSync(filePath)) return null;
|
|
63
|
+
const text = readFileSync(filePath, 'utf-8');
|
|
64
|
+
const labels = [];
|
|
65
|
+
for (const line of text.split('\n')) {
|
|
66
|
+
const m = line.match(/^\s+label:\s*(.+)$/);
|
|
67
|
+
if (!m) continue;
|
|
68
|
+
let v = m[1].trim().replace(/^['"]|['"]$/g, '');
|
|
69
|
+
if (v) labels.push(v);
|
|
70
|
+
}
|
|
71
|
+
return labels.length > 0 ? labels : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const CANONICAL_STATES = loadCanonicalLabelsFromStatesYaml(STATES_FILE) || [
|
|
75
|
+
'Evaluated', 'Applied', 'Contacted', 'Responded', 'Interview', 'Offer', 'Rejected', 'Discarded', 'SKIP',
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function validateStatus(status) {
|
|
79
|
+
const clean = status.replace(/\*\*/g, '').replace(/\s+\d{4}-\d{2}-\d{2}.*$/, '').trim();
|
|
80
|
+
const lower = clean.toLowerCase();
|
|
81
|
+
|
|
82
|
+
for (const valid of CANONICAL_STATES) {
|
|
83
|
+
if (valid.toLowerCase() === lower) return valid;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const aliases = {
|
|
87
|
+
'hold': 'Evaluated',
|
|
88
|
+
'applied': 'Applied', 'sent': 'Applied',
|
|
89
|
+
'skip': 'SKIP',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (aliases[lower]) return aliases[lower];
|
|
93
|
+
|
|
94
|
+
if (/^(dup(licate)?|repost)/i.test(lower)) return 'Discarded';
|
|
95
|
+
|
|
96
|
+
console.warn(`⚠️ Non-canonical status "${status}" → defaulting to "Evaluated"`);
|
|
97
|
+
return 'Evaluated';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeCompany(name) {
|
|
101
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function roleFuzzyMatch(a, b) {
|
|
105
|
+
const wordsA = a.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
106
|
+
const wordsB = b.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
107
|
+
const overlap = wordsA.filter(w => wordsB.some(wb => wb.includes(w) || w.includes(wb)));
|
|
108
|
+
return overlap.length >= 2;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractReportNum(reportStr) {
|
|
112
|
+
const m = reportStr.match(/\[(\d+)\]/);
|
|
113
|
+
return m ? parseInt(m[1]) : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseScore(s) {
|
|
117
|
+
const m = s.replace(/\*\*/g, '').match(/([\d.]+)/);
|
|
118
|
+
return m ? parseFloat(m[1]) : 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse a TSV file content into a structured addition object.
|
|
123
|
+
*/
|
|
124
|
+
function parseTsvContent(content, filename) {
|
|
125
|
+
content = content.trim();
|
|
126
|
+
if (!content) return null;
|
|
127
|
+
|
|
128
|
+
let parts;
|
|
129
|
+
let addition;
|
|
130
|
+
|
|
131
|
+
if (content.startsWith('|')) {
|
|
132
|
+
parts = content.split('|').map(s => s.trim()).filter(Boolean);
|
|
133
|
+
if (parts.length < 8) {
|
|
134
|
+
console.warn(`⚠️ Skipping malformed pipe-delimited ${filename}: ${parts.length} fields`);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
addition = {
|
|
138
|
+
num: parseInt(parts[0]),
|
|
139
|
+
date: parts[1],
|
|
140
|
+
company: parts[2],
|
|
141
|
+
role: parts[3],
|
|
142
|
+
score: parts[4],
|
|
143
|
+
status: validateStatus(parts[5]),
|
|
144
|
+
pdf: parts[6],
|
|
145
|
+
report: parts[7],
|
|
146
|
+
notes: parts[8] || '',
|
|
147
|
+
};
|
|
148
|
+
} else {
|
|
149
|
+
parts = content.split('\t');
|
|
150
|
+
if (parts.length < 8) {
|
|
151
|
+
console.warn(`⚠️ Skipping malformed TSV ${filename}: ${parts.length} fields`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const col4 = parts[4].trim();
|
|
156
|
+
const col5 = parts[5].trim();
|
|
157
|
+
const col4LooksLikeScore = /^\d+\.?\d*\/5$/.test(col4) || col4 === 'N/A' || col4 === 'DUP';
|
|
158
|
+
const col5LooksLikeScore = /^\d+\.?\d*\/5$/.test(col5) || col5 === 'N/A' || col5 === 'DUP';
|
|
159
|
+
const col4LooksLikeStatus = /^(evaluated|applied|contacted|responded|interview|offer|rejected|discarded|skip|duplicate|repost|hold)/i.test(col4);
|
|
160
|
+
const col5LooksLikeStatus = /^(evaluated|applied|contacted|responded|interview|offer|rejected|discarded|skip|duplicate|repost|hold)/i.test(col5);
|
|
161
|
+
|
|
162
|
+
let statusCol, scoreCol;
|
|
163
|
+
if (col4LooksLikeStatus && !col4LooksLikeScore) {
|
|
164
|
+
statusCol = col4; scoreCol = col5;
|
|
165
|
+
} else if (col4LooksLikeScore && col5LooksLikeStatus) {
|
|
166
|
+
statusCol = col5; scoreCol = col4;
|
|
167
|
+
} else if (col5LooksLikeScore && !col4LooksLikeScore) {
|
|
168
|
+
statusCol = col4; scoreCol = col5;
|
|
169
|
+
} else {
|
|
170
|
+
statusCol = col4; scoreCol = col5;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
addition = {
|
|
174
|
+
num: parseInt(parts[0]),
|
|
175
|
+
date: parts[1],
|
|
176
|
+
company: parts[2],
|
|
177
|
+
role: parts[3],
|
|
178
|
+
status: validateStatus(statusCol),
|
|
179
|
+
score: scoreCol,
|
|
180
|
+
pdf: parts[6],
|
|
181
|
+
report: parts[7],
|
|
182
|
+
notes: parts[8] || '',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (isNaN(addition.num) || addition.num === 0) {
|
|
187
|
+
console.warn(`⚠️ Skipping ${filename}: invalid entry number`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return addition;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- Main ----
|
|
195
|
+
|
|
196
|
+
if (!existsSync(ADDITIONS_DIR)) {
|
|
197
|
+
console.log('✅ No pending additions to merge.');
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const tsvFiles = readdirSync(ADDITIONS_DIR).filter(f => f.endsWith('.tsv'));
|
|
202
|
+
if (tsvFiles.length === 0) {
|
|
203
|
+
console.log('✅ No pending additions to merge.');
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Initialize tracker
|
|
208
|
+
const layout = usesDayFiles() ? 'day' : 'single';
|
|
209
|
+
let appLines;
|
|
210
|
+
let existingApps;
|
|
211
|
+
let maxNum;
|
|
212
|
+
|
|
213
|
+
if (layout === 'day') {
|
|
214
|
+
ensureDayDir();
|
|
215
|
+
({ entries: existingApps, maxNum } = readAllEntries());
|
|
216
|
+
} else {
|
|
217
|
+
// Single-file mode
|
|
218
|
+
const APPS_FILE = existsSync(DATA_APPS_FILE) ? DATA_APPS_FILE : ROOT_APPS_FILE;
|
|
219
|
+
const appsDisplay = relative(PROJECT_DIR, APPS_FILE).replace(/\\/g, '/');
|
|
220
|
+
|
|
221
|
+
if (!existsSync(APPS_FILE)) {
|
|
222
|
+
if (DRY_RUN) {
|
|
223
|
+
console.log('(dry-run) Would create data/applications.md with empty tracker header.');
|
|
224
|
+
} else {
|
|
225
|
+
console.log('No tracker file yet; creating data/applications.md with empty header.');
|
|
226
|
+
mkdirSync(join(PROJECT_DIR, 'data'), { recursive: true });
|
|
227
|
+
writeFileSync(DATA_APPS_FILE, getHeader() + '\n', 'utf-8');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const filePath = existsSync(DATA_APPS_FILE) ? DATA_APPS_FILE : ROOT_APPS_FILE;
|
|
232
|
+
appLines = readFileSync(filePath, 'utf-8').split('\n');
|
|
233
|
+
existingApps = [];
|
|
234
|
+
maxNum = 0;
|
|
235
|
+
for (const line of appLines) {
|
|
236
|
+
if (line.startsWith('|') && !line.includes('---') && !line.includes('Company')) {
|
|
237
|
+
const app = parseAppLine(line);
|
|
238
|
+
if (app) {
|
|
239
|
+
existingApps.push(app);
|
|
240
|
+
if (app.num > maxNum) maxNum = app.num;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const appsDisplay = layout === 'day' ? relative(PROJECT_DIR, DATA_APPS_DIR) : relative(PROJECT_DIR, existsSync(DATA_APPS_FILE) ? DATA_APPS_FILE : ROOT_APPS_FILE);
|
|
247
|
+
console.log(`📊 ${appsDisplay}: ${existingApps.length} existing entries, max #${maxNum}`);
|
|
248
|
+
|
|
249
|
+
tsvFiles.sort((a, b) => {
|
|
250
|
+
const numA = parseInt(a.replace(/\D/g, '')) || 0;
|
|
251
|
+
const numB = parseInt(b.replace(/\D/g, '')) || 0;
|
|
252
|
+
return numA - numB;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
console.log(`📥 Found ${tsvFiles.length} pending additions`);
|
|
256
|
+
|
|
257
|
+
let added = 0;
|
|
258
|
+
let updated = 0;
|
|
259
|
+
let skipped = 0;
|
|
260
|
+
const newEntries = [];
|
|
261
|
+
|
|
262
|
+
for (const file of tsvFiles) {
|
|
263
|
+
const content = readFileSync(join(ADDITIONS_DIR, file), 'utf-8').trim();
|
|
264
|
+
const addition = parseTsvContent(content, file);
|
|
265
|
+
if (!addition) { skipped++; continue; }
|
|
266
|
+
|
|
267
|
+
const reportNum = extractReportNum(addition.report);
|
|
268
|
+
let duplicate = null;
|
|
269
|
+
|
|
270
|
+
if (reportNum) {
|
|
271
|
+
duplicate = existingApps.find(app => {
|
|
272
|
+
const existingReportNum = extractReportNum(app.report);
|
|
273
|
+
return existingReportNum === reportNum;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!duplicate) {
|
|
278
|
+
duplicate = existingApps.find(app => app.num === addition.num);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!duplicate) {
|
|
282
|
+
const normCompany = normalizeCompany(addition.company);
|
|
283
|
+
duplicate = existingApps.find(app => {
|
|
284
|
+
if (normalizeCompany(app.company) !== normCompany) return false;
|
|
285
|
+
return roleFuzzyMatch(addition.role, app.role);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (duplicate) {
|
|
290
|
+
const newScore = parseScore(addition.score);
|
|
291
|
+
const oldScore = parseScore(duplicate.score);
|
|
292
|
+
|
|
293
|
+
if (newScore > oldScore) {
|
|
294
|
+
console.log(`🔄 Update: #${duplicate.num} ${addition.company} — ${addition.role} (${oldScore}→${newScore})`);
|
|
295
|
+
|
|
296
|
+
if (layout === 'day') {
|
|
297
|
+
// Update in existing entries list for later write
|
|
298
|
+
duplicate.date = addition.date;
|
|
299
|
+
duplicate.company = addition.company;
|
|
300
|
+
duplicate.role = addition.role;
|
|
301
|
+
duplicate.score = addition.score;
|
|
302
|
+
duplicate.report = addition.report;
|
|
303
|
+
duplicate.notes = `Re-eval ${addition.date} (${oldScore}→${newScore}). ${addition.notes}`;
|
|
304
|
+
} else {
|
|
305
|
+
const lineIdx = appLines.indexOf(duplicate.raw);
|
|
306
|
+
if (lineIdx >= 0) {
|
|
307
|
+
appLines[lineIdx] = `| ${duplicate.num} | ${addition.date} | ${addition.company} | ${addition.role} | ${addition.score} | ${duplicate.status} | ${duplicate.pdf} | ${addition.report} | Re-eval ${addition.date} (${oldScore}→${newScore}). ${addition.notes} |`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
updated++;
|
|
311
|
+
} else {
|
|
312
|
+
console.log(`⏭️ Skip: ${addition.company} — ${addition.role} (existing #${duplicate.num} ${oldScore} >= new ${newScore})`);
|
|
313
|
+
skipped++;
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
const entryNum = addition.num > maxNum ? addition.num : ++maxNum;
|
|
317
|
+
if (addition.num > maxNum) maxNum = addition.num;
|
|
318
|
+
|
|
319
|
+
newEntries.push({
|
|
320
|
+
...addition,
|
|
321
|
+
num: entryNum,
|
|
322
|
+
});
|
|
323
|
+
added++;
|
|
324
|
+
console.log(`➕ Add #${entryNum}: ${addition.company} — ${addition.role} (${addition.score})`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Write new entries
|
|
329
|
+
if (!DRY_RUN) {
|
|
330
|
+
if (layout === 'day') {
|
|
331
|
+
// Merge new entries into existing, then write day files
|
|
332
|
+
existingApps.push(...newEntries);
|
|
333
|
+
writeToDayFiles(existingApps);
|
|
334
|
+
} else {
|
|
335
|
+
// Single-file: insert new lines after header
|
|
336
|
+
if (newEntries.length > 0) {
|
|
337
|
+
let insertIdx = -1;
|
|
338
|
+
for (let i = 0; i < appLines.length; i++) {
|
|
339
|
+
if (appLines[i].includes('---') && appLines[i].startsWith('|')) {
|
|
340
|
+
insertIdx = i + 1;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (insertIdx >= 0) {
|
|
345
|
+
appLines.splice(insertIdx, 0, ...newEntries.map(formatAppLine));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const APPS_FILE = existsSync(DATA_APPS_FILE) ? DATA_APPS_FILE : ROOT_APPS_FILE;
|
|
350
|
+
writeFileSync(APPS_FILE, appLines.join('\n'));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Move processed files to merged/
|
|
354
|
+
if (!existsSync(MERGED_DIR)) mkdirSync(MERGED_DIR, { recursive: true });
|
|
355
|
+
for (const file of tsvFiles) {
|
|
356
|
+
renameSync(join(ADDITIONS_DIR, file), join(MERGED_DIR, file));
|
|
357
|
+
}
|
|
358
|
+
console.log(`\n✅ Moved ${tsvFiles.length} TSVs to merged/`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log(`\n📊 Summary: +${added} added, 🔄${updated} updated, ⏭️${skipped} skipped`);
|
|
362
|
+
if (DRY_RUN) console.log('(dry-run — no changes written)');
|
|
363
|
+
|
|
364
|
+
// Optional verify — resolve verify-pipeline.mjs relative to this file (works whether
|
|
365
|
+
// installed as a package in node_modules or run from the repo root).
|
|
366
|
+
if (VERIFY && !DRY_RUN) {
|
|
367
|
+
console.log('\n--- Running verification ---');
|
|
368
|
+
const { execSync } = await import('child_process');
|
|
369
|
+
const { fileURLToPath } = await import('url');
|
|
370
|
+
const { dirname } = await import('path');
|
|
371
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
372
|
+
try {
|
|
373
|
+
execSync(`node ${join(here, 'verify-pipeline.mjs')}`, { stdio: 'inherit' });
|
|
374
|
+
} catch (e) {
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
}
|
package/modes/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Modes
|
|
2
|
+
|
|
3
|
+
Markdown prompts used with opencode together with the root [`OPENCODE.md`](../OPENCODE.md). Each file aligns with a `/job-forge …` entry point or shared behavior described there.
|
|
4
|
+
|
|
5
|
+
- **`_shared.md`** — Archetypes, scoring dimensions, negotiation scaffolding. Edit this first when you change how offers are classified or weighted.
|
|
6
|
+
- **Per-command files** — Each `*.md` here pairs with a `/job-forge …` entry in [`OPENCODE.md`](../OPENCODE.md). How modes connect to batch, tracker, and scripts is spelled out in [**Architecture — Modes**](../docs/ARCHITECTURE.md#modes-modes).
|
|
7
|
+
|
|
8
|
+
| File | Role |
|
|
9
|
+
|------|------|
|
|
10
|
+
| [`_shared.md`](_shared.md) | Shared archetypes, scoring, negotiation scaffolding |
|
|
11
|
+
| [`auto-pipeline.md`](auto-pipeline.md) | Default path when the user pastes a JD or URL — full evaluate → report → PDF → tracker flow |
|
|
12
|
+
| [`offer.md`](offer.md) | Explicit full evaluation (blocks A–F) for a single offer |
|
|
13
|
+
| [`compare.md`](compare.md) | Side-by-side comparison of multiple offers |
|
|
14
|
+
| [`contact.md`](contact.md) | LinkedIn or email outreach drafts |
|
|
15
|
+
| [`deep.md`](deep.md) | Deeper company / role research |
|
|
16
|
+
| [`pdf.md`](pdf.md) | Tailored CV and PDF generation |
|
|
17
|
+
| [`training.md`](training.md) | Evaluate a course, cert, or learning path |
|
|
18
|
+
| [`project.md`](project.md) | Evaluate a portfolio project for job fit |
|
|
19
|
+
| [`tracker.md`](tracker.md) | Application tracker hygiene and status questions |
|
|
20
|
+
| [`apply.md`](apply.md) | Application forms and long-form answers |
|
|
21
|
+
| [`scan.md`](scan.md) | Portal and job-board scanning |
|
|
22
|
+
| [`pipeline.md`](pipeline.md) | Work through pending URLs in `data/pipeline.md` |
|
|
23
|
+
| [`batch.md`](batch.md) | Batch evaluation workflow and TSV-oriented runs |
|
|
24
|
+
| [`followup.md`](followup.md) | What to follow up on next |
|
|
25
|
+
| [`rejection.md`](rejection.md) | Log or process a rejection |
|
|
26
|
+
| [`negotiation.md`](negotiation.md) | Offer received — negotiation framing |
|
|
27
|
+
|
|
28
|
+
To tailor profile-driven settings, portals, and templates, see [`docs/CUSTOMIZATION.md`](../docs/CUSTOMIZATION.md).
|
|
29
|
+
|
|
30
|
+
Contributors: see [`CONTRIBUTING.md`](../CONTRIBUTING.md) for branch workflow and the `npm run verify` gate; prefer one cohesive change per PR (for example a single mode or updates under `_shared.md` only).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Scoring Calibration Anchors
|
|
2
|
+
|
|
3
|
+
**This file is Read on-demand, NOT loaded into the global `instructions` prefix.** It lives separately from `_shared.md` because its contents churn as the candidate accumulates reports — keeping it out of the cached prefix means updates here don't bust the prompt cache for every unrelated session.
|
|
4
|
+
|
|
5
|
+
**When to Read this file:** right before assigning a final score during evaluation. Once per evaluation, not per dimension.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
**Use these reference profiles to anchor scores and prevent drift over time.** When evaluating an offer, mentally compare it to these anchors before assigning a final score. Scores MUST be relative to the candidate's actual profile, not absolute.
|
|
10
|
+
|
|
11
|
+
<!-- [CUSTOMIZE] Replace these with real offers you've evaluated, or archetypes
|
|
12
|
+
that represent clear score levels for YOUR situation. The examples below
|
|
13
|
+
are generic starting points — after 10-20 evaluations, replace them with
|
|
14
|
+
actual reports from your pipeline (e.g., "Report #045 — Anthropic — 4.7/5"). -->
|
|
15
|
+
|
|
16
|
+
| Score | What it looks like | Example anchor |
|
|
17
|
+
|-------|--------------------|----------------|
|
|
18
|
+
| **5.0** | Dream role. Exact archetype, 90%+ CV match, top-quartile comp, remote, strong brand, fast process. You'd accept immediately. | _Replace with your highest-scored report once you have one_ |
|
|
19
|
+
| **4.0** | Strong match. Right archetype, 75%+ match, fair comp, minor gaps that are easy to frame. Worth a tailored application. | _Replace with a real ~4.0 report_ |
|
|
20
|
+
| **3.0** | Moderate match. Adjacent archetype, 50-60% match, 2-4 hard gaps, comp unknown or median. Worth evaluating but not a priority. | _Replace with a real ~3.0 report_ |
|
|
21
|
+
| **2.0** | Weak match. Wrong seniority or archetype, major gaps, below-market comp signals. Discourage unless specific reason. | _Replace with a real ~2.0 report_ |
|
|
22
|
+
| **1.0** | No fit. Unrelated domain, entry-level, relocation-only, or red flags. Skip. | _Replace with a real ~1.0 report_ |
|
|
23
|
+
|
|
24
|
+
**Recalibration trigger:** After every 50 evaluations (or when you notice scores clustering — e.g., everything is 3.5-4.2), review the anchors table. Replace generic descriptions with actual reports. If your best offer so far is a 4.3, that's your effective ceiling — adjust the 5.0 anchor to reflect what a true dream role would actually look like for you.
|
|
25
|
+
|
|
26
|
+
**How to use during evaluation:** After computing the weighted score, sanity-check it against the anchors. "Is this really a 4.5? Is it as strong as [anchor report]?" If not, adjust. The anchors prevent both inflation (everything is 4+) and deflation (nothing breaks 3.5).
|