job-forge 2.14.43 → 2.14.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/main.mdc +4 -1
- package/AGENTS.md +4 -1
- package/CLAUDE.md +4 -1
- package/bin/create-job-forge.mjs +1 -0
- package/bin/geometra-mcp-launcher.mjs +191 -3
- package/docs/SETUP.md +2 -0
- package/iso/instructions.md +4 -1
- package/modes/apply.md +15 -6
- package/modes/reference-geometra.md +2 -0
- package/modes/reference-portals.md +16 -0
- package/package.json +2 -2
- package/scripts/check-iso-smoke.mjs +2 -0
- package/templates/migrations.json +1 -0
package/.cursor/rules/main.mdc
CHANGED
|
@@ -36,6 +36,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
36
36
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true` and `stealth: true` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
37
37
|
why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP opens visible Chromium unless `headless: true` is explicit, and Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`; both flags belong with JobForge portal sessions instead of stock visible Playwright Chromium
|
|
38
38
|
|
|
39
|
+
- [H9] If Geometra MCP disappears, becomes unresponsive, or returns a cascade of `Not connected` after a live form-fill, inspect `.jobforge-mcp/geometra-mcp.jsonl` before guessing. Report the last `launcher_start`, `child_spawn`, `heartbeat`, `signal_received`, `child_stderr`, and `child_exit` events plus the timestamp gap from the last heartbeat. If the last event is an old heartbeat with no `signal_received` / `child_exit`, treat it as likely host SIGKILL or external process death.
|
|
40
|
+
why: OpenCode or the OS can kill the MCP server without stderr, crash logs, or core dumps. JobForge's MCP launcher writes durable lifecycle events outside MCP stdout, so silent disappearances still leave enough evidence to distinguish host kill, child crash, stderr failure, and wrapper health
|
|
41
|
+
|
|
39
42
|
## Defaults
|
|
40
43
|
|
|
41
44
|
- [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
|
|
@@ -67,7 +70,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
67
70
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
68
71
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
69
72
|
3. Read the active mode file [D3]. Use local helpers when they can replace broad file reads, prose math, manual policy checks, or artifact reuse decisions [D8]. Decide inline vs delegated work [D1].
|
|
70
|
-
4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8].
|
|
73
|
+
4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8], MCP lifecycle log awareness [H9].
|
|
71
74
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D8].
|
|
72
75
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
73
76
|
7. Cross-check subagent facts against authoritative files [H7].
|
package/AGENTS.md
CHANGED
|
@@ -31,6 +31,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
31
31
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true` and `stealth: true` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
32
32
|
why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP opens visible Chromium unless `headless: true` is explicit, and Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`; both flags belong with JobForge portal sessions instead of stock visible Playwright Chromium
|
|
33
33
|
|
|
34
|
+
- [H9] If Geometra MCP disappears, becomes unresponsive, or returns a cascade of `Not connected` after a live form-fill, inspect `.jobforge-mcp/geometra-mcp.jsonl` before guessing. Report the last `launcher_start`, `child_spawn`, `heartbeat`, `signal_received`, `child_stderr`, and `child_exit` events plus the timestamp gap from the last heartbeat. If the last event is an old heartbeat with no `signal_received` / `child_exit`, treat it as likely host SIGKILL or external process death.
|
|
35
|
+
why: OpenCode or the OS can kill the MCP server without stderr, crash logs, or core dumps. JobForge's MCP launcher writes durable lifecycle events outside MCP stdout, so silent disappearances still leave enough evidence to distinguish host kill, child crash, stderr failure, and wrapper health
|
|
36
|
+
|
|
34
37
|
## Defaults
|
|
35
38
|
|
|
36
39
|
- [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
|
|
@@ -62,7 +65,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
62
65
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
63
66
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
64
67
|
3. Read the active mode file [D3]. Use local helpers when they can replace broad file reads, prose math, manual policy checks, or artifact reuse decisions [D8]. Decide inline vs delegated work [D1].
|
|
65
|
-
4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8].
|
|
68
|
+
4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8], MCP lifecycle log awareness [H9].
|
|
66
69
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D8].
|
|
67
70
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
68
71
|
7. Cross-check subagent facts against authoritative files [H7].
|
package/CLAUDE.md
CHANGED
|
@@ -31,6 +31,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
31
31
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true` and `stealth: true` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
32
32
|
why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP opens visible Chromium unless `headless: true` is explicit, and Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`; both flags belong with JobForge portal sessions instead of stock visible Playwright Chromium
|
|
33
33
|
|
|
34
|
+
- [H9] If Geometra MCP disappears, becomes unresponsive, or returns a cascade of `Not connected` after a live form-fill, inspect `.jobforge-mcp/geometra-mcp.jsonl` before guessing. Report the last `launcher_start`, `child_spawn`, `heartbeat`, `signal_received`, `child_stderr`, and `child_exit` events plus the timestamp gap from the last heartbeat. If the last event is an old heartbeat with no `signal_received` / `child_exit`, treat it as likely host SIGKILL or external process death.
|
|
35
|
+
why: OpenCode or the OS can kill the MCP server without stderr, crash logs, or core dumps. JobForge's MCP launcher writes durable lifecycle events outside MCP stdout, so silent disappearances still leave enough evidence to distinguish host kill, child crash, stderr failure, and wrapper health
|
|
36
|
+
|
|
34
37
|
## Defaults
|
|
35
38
|
|
|
36
39
|
- [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
|
|
@@ -62,7 +65,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
62
65
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
63
66
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
64
67
|
3. Read the active mode file [D3]. Use local helpers when they can replace broad file reads, prose math, manual policy checks, or artifact reuse decisions [D8]. Decide inline vs delegated work [D1].
|
|
65
|
-
4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8].
|
|
68
|
+
4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8], MCP lifecycle log awareness [H9].
|
|
66
69
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D8].
|
|
67
70
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
68
71
|
7. Cross-check subagent facts against authoritative files [H7].
|
package/bin/create-job-forge.mjs
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
|
|
8
|
-
const DEFAULT_FALLBACK_PACKAGE = '@geometra/mcp@1.62.
|
|
8
|
+
const DEFAULT_FALLBACK_PACKAGE = '@geometra/mcp@1.62.2';
|
|
9
9
|
const RESOLVE_ONLY_FLAG = '--job-forge-resolve-target';
|
|
10
|
+
const DEFAULT_LOG_RELATIVE_PATH = '.jobforge-mcp/geometra-mcp.jsonl';
|
|
11
|
+
const DEFAULT_HEARTBEAT_MS = 15_000;
|
|
12
|
+
const MAX_STDERR_LOG_CHARS = 4_000;
|
|
13
|
+
const SIGNAL_EXIT_CODES = {
|
|
14
|
+
SIGHUP: 129,
|
|
15
|
+
SIGINT: 130,
|
|
16
|
+
SIGQUIT: 131,
|
|
17
|
+
SIGTERM: 143,
|
|
18
|
+
};
|
|
10
19
|
|
|
11
20
|
function normalizeEnv(value) {
|
|
12
21
|
if (typeof value !== 'string') return null;
|
|
@@ -26,6 +35,94 @@ function readJsonFile(filePath) {
|
|
|
26
35
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
function boolEnvDisabled(value) {
|
|
39
|
+
if (typeof value !== 'string') return false;
|
|
40
|
+
return ['0', 'false', 'no', 'off'].includes(value.trim().toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function positiveIntEnv(value, fallback) {
|
|
44
|
+
if (typeof value !== 'string' || value.trim() === '') return fallback;
|
|
45
|
+
const parsed = Number.parseInt(value, 10);
|
|
46
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createLifecycleLogger(projectDir) {
|
|
50
|
+
if (boolEnvDisabled(process.env.JOB_FORGE_GEOMETRA_MCP_LOG)) {
|
|
51
|
+
return {
|
|
52
|
+
enabled: false,
|
|
53
|
+
logPath: null,
|
|
54
|
+
write() {},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const configuredPath = normalizeEnv(process.env.JOB_FORGE_GEOMETRA_MCP_LOG_PATH);
|
|
59
|
+
const logPath = configuredPath ? resolve(configuredPath) : join(projectDir, DEFAULT_LOG_RELATIVE_PATH);
|
|
60
|
+
|
|
61
|
+
function write(event, detail = {}) {
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
64
|
+
appendFileSync(logPath, `${JSON.stringify({
|
|
65
|
+
ts: new Date().toISOString(),
|
|
66
|
+
event,
|
|
67
|
+
pid: process.pid,
|
|
68
|
+
ppid: process.ppid,
|
|
69
|
+
projectDir,
|
|
70
|
+
...detail,
|
|
71
|
+
})}\n`);
|
|
72
|
+
} catch {
|
|
73
|
+
// Logging must never break MCP startup or stdio protocol handling.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
enabled: true,
|
|
79
|
+
logPath,
|
|
80
|
+
write,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function targetForLog(target) {
|
|
85
|
+
return {
|
|
86
|
+
source: target.source,
|
|
87
|
+
command: target.command,
|
|
88
|
+
args: target.args,
|
|
89
|
+
resolvedPath: target.resolvedPath,
|
|
90
|
+
packageSpec: target.packageSpec,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function envForLog() {
|
|
95
|
+
return {
|
|
96
|
+
node: process.version,
|
|
97
|
+
platform: process.platform,
|
|
98
|
+
arch: process.arch,
|
|
99
|
+
geometraStealth: process.env.GEOMETRA_STEALTH ?? null,
|
|
100
|
+
geometraBrowser: process.env.GEOMETRA_BROWSER ?? null,
|
|
101
|
+
explicitMcpPath: Boolean(normalizeEnv(process.env.JOB_FORGE_GEOMETRA_MCP_PATH)),
|
|
102
|
+
explicitMcpPackage: Boolean(normalizeEnv(process.env.JOB_FORGE_GEOMETRA_MCP_PACKAGE)),
|
|
103
|
+
explicitLogPath: Boolean(normalizeEnv(process.env.JOB_FORGE_GEOMETRA_MCP_LOG_PATH)),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function processHealthForLog() {
|
|
108
|
+
const memory = process.memoryUsage();
|
|
109
|
+
const resourceUsage = typeof process.resourceUsage === 'function' ? process.resourceUsage() : null;
|
|
110
|
+
return {
|
|
111
|
+
uptimeMs: Math.round(process.uptime() * 1000),
|
|
112
|
+
memory,
|
|
113
|
+
resourceUsage,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stderrChunkForLog(chunk) {
|
|
118
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
|
|
119
|
+
return {
|
|
120
|
+
text: text.slice(0, MAX_STDERR_LOG_CHARS),
|
|
121
|
+
bytes: Buffer.byteLength(text),
|
|
122
|
+
truncated: text.length > MAX_STDERR_LOG_CHARS,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
29
126
|
function readProjectPathFromPackageJson(projectDir) {
|
|
30
127
|
const packagePath = join(projectDir, 'package.json');
|
|
31
128
|
if (!existsSync(packagePath)) return null;
|
|
@@ -114,18 +211,109 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
114
211
|
return;
|
|
115
212
|
}
|
|
116
213
|
|
|
214
|
+
const projectDir = process.env.JOB_FORGE_PROJECT || process.cwd();
|
|
215
|
+
const logger = createLifecycleLogger(projectDir);
|
|
216
|
+
const heartbeatMs = boolEnvDisabled(process.env.JOB_FORGE_GEOMETRA_MCP_HEARTBEAT)
|
|
217
|
+
? 0
|
|
218
|
+
: positiveIntEnv(process.env.JOB_FORGE_GEOMETRA_MCP_HEARTBEAT_MS, DEFAULT_HEARTBEAT_MS);
|
|
219
|
+
|
|
220
|
+
logger.write('launcher_start', {
|
|
221
|
+
argv,
|
|
222
|
+
target: targetForLog(target),
|
|
223
|
+
env: envForLog(),
|
|
224
|
+
logPath: logger.logPath,
|
|
225
|
+
heartbeatMs,
|
|
226
|
+
});
|
|
227
|
+
|
|
117
228
|
const child = spawn(target.command, [...target.args, ...argv], {
|
|
118
|
-
stdio: 'inherit',
|
|
229
|
+
stdio: ['inherit', 'inherit', 'pipe'],
|
|
119
230
|
env: process.env,
|
|
120
231
|
});
|
|
232
|
+
let exiting = false;
|
|
233
|
+
let heartbeat = null;
|
|
234
|
+
|
|
235
|
+
logger.write('child_spawn', {
|
|
236
|
+
childPid: child.pid,
|
|
237
|
+
target: targetForLog(target),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (child.stderr) {
|
|
241
|
+
child.stderr.on('data', (chunk) => {
|
|
242
|
+
process.stderr.write(chunk);
|
|
243
|
+
logger.write('child_stderr', {
|
|
244
|
+
childPid: child.pid,
|
|
245
|
+
...stderrChunkForLog(chunk),
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (heartbeatMs > 0) {
|
|
251
|
+
heartbeat = setInterval(() => {
|
|
252
|
+
logger.write('heartbeat', {
|
|
253
|
+
childPid: child.pid,
|
|
254
|
+
childKilled: child.killed,
|
|
255
|
+
health: processHealthForLog(),
|
|
256
|
+
});
|
|
257
|
+
}, heartbeatMs);
|
|
258
|
+
heartbeat.unref();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const signalNames = ['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGTERM'];
|
|
262
|
+
for (const signal of signalNames) {
|
|
263
|
+
process.once(signal, () => {
|
|
264
|
+
if (exiting) return;
|
|
265
|
+
exiting = true;
|
|
266
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
267
|
+
logger.write('signal_received', {
|
|
268
|
+
signal,
|
|
269
|
+
childPid: child.pid,
|
|
270
|
+
childKilled: child.killed,
|
|
271
|
+
health: processHealthForLog(),
|
|
272
|
+
});
|
|
273
|
+
if (!child.killed) child.kill(signal);
|
|
274
|
+
process.exit(SIGNAL_EXIT_CODES[signal] ?? 1);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
process.once('uncaughtException', (error) => {
|
|
279
|
+
logger.write('uncaught_exception', {
|
|
280
|
+
message: error instanceof Error ? error.message : String(error),
|
|
281
|
+
stack: error instanceof Error ? error.stack : null,
|
|
282
|
+
health: processHealthForLog(),
|
|
283
|
+
});
|
|
284
|
+
throw error;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
process.once('unhandledRejection', (reason) => {
|
|
288
|
+
logger.write('unhandled_rejection', {
|
|
289
|
+
message: reason instanceof Error ? reason.message : String(reason),
|
|
290
|
+
stack: reason instanceof Error ? reason.stack : null,
|
|
291
|
+
health: processHealthForLog(),
|
|
292
|
+
});
|
|
293
|
+
});
|
|
121
294
|
|
|
122
295
|
child.on('error', (error) => {
|
|
296
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
297
|
+
logger.write('child_error', {
|
|
298
|
+
childPid: child.pid,
|
|
299
|
+
message: error instanceof Error ? error.message : String(error),
|
|
300
|
+
stack: error instanceof Error ? error.stack : null,
|
|
301
|
+
health: processHealthForLog(),
|
|
302
|
+
});
|
|
123
303
|
console.error(error instanceof Error ? error.message : String(error));
|
|
124
304
|
process.exit(1);
|
|
125
305
|
});
|
|
126
306
|
|
|
127
307
|
child.on('exit', (code, signal) => {
|
|
308
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
309
|
+
logger.write('child_exit', {
|
|
310
|
+
childPid: child.pid,
|
|
311
|
+
code,
|
|
312
|
+
signal,
|
|
313
|
+
health: processHealthForLog(),
|
|
314
|
+
});
|
|
128
315
|
if (signal) {
|
|
316
|
+
for (const signalName of signalNames) process.removeAllListeners(signalName);
|
|
129
317
|
process.kill(process.pid, signal);
|
|
130
318
|
return;
|
|
131
319
|
}
|
package/docs/SETUP.md
CHANGED
|
@@ -216,6 +216,8 @@ Use it to identify which sessions or models are consuming the most tokens. The `
|
|
|
216
216
|
**PDF generation fails**
|
|
217
217
|
The scaffolded `opencode.json` already registers Geometra MCP; if it's not running, check `opencode mcp list` and verify the scaffolded config under the `mcp.geometra` key — its `command` MUST be `["npx", "--no-install", "job-forge", "mcp:geometra"]`, `enabled: true`, and its `environment` should include `GEOMETRA_STEALTH=1` (or equivalently `GEOMETRA_BROWSER=stealth`) so proxy-backed portal sessions default to CloakBrowser's patched Chromium. `job-forge mcp:geometra` resolves Geometra in this order: `JOB_FORGE_GEOMETRA_MCP_PATH`, then a consumer-project override from `package.json -> jobForge.geometraMcpPath`, then `opencode.json -> mcp.geometra.environment.JOB_FORGE_GEOMETRA_MCP_PATH`, then a sibling `../geometra/mcp/dist/index.js` checkout for local JobForge development, and finally the pinned npm package. Geometra manages Chromium via its built-in proxy. JobForge still passes `headless: true` and `stealth: true` for portal sessions explicitly; the env block keeps the default aligned for auto-spawned sessions and local debugging. For standalone CLI usage (outside opencode), `generate-pdf.mjs` also works with standalone Playwright/Chromium — install with `npx playwright install chromium`.
|
|
218
218
|
|
|
219
|
+
`job-forge mcp:geometra` writes MCP launcher diagnostics to `.jobforge-mcp/geometra-mcp.jsonl`. If the server silently vanishes with empty stderr, run `tail -40 .jobforge-mcp/geometra-mcp.jsonl`. A final `signal_received` event means the host sent a catchable signal, a final `child_exit` means Geometra exited, and an old final `heartbeat` with no exit/signal usually means SIGKILL or external process death. Set `JOB_FORGE_GEOMETRA_MCP_LOG_PATH` to move the log, `JOB_FORGE_GEOMETRA_MCP_LOG=0` to disable it, or `JOB_FORGE_GEOMETRA_MCP_HEARTBEAT_MS` to tune the heartbeat interval.
|
|
220
|
+
|
|
219
221
|
For consumer projects that should always use a local Geometra checkout across Opencode, Codex, Cursor, and Claude, prefer a local `package.json` override instead of editing symlinked MCP configs:
|
|
220
222
|
|
|
221
223
|
```json
|
package/iso/instructions.md
CHANGED
|
@@ -31,6 +31,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
31
31
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true` and `stealth: true` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
32
32
|
why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions. Geometra MCP opens visible Chromium unless `headless: true` is explicit, and Geometra MCP >=1.61.3 can launch CloakBrowser stealth Chromium via `stealth: true`; both flags belong with JobForge portal sessions instead of stock visible Playwright Chromium
|
|
33
33
|
|
|
34
|
+
- [H9] If Geometra MCP disappears, becomes unresponsive, or returns a cascade of `Not connected` after a live form-fill, inspect `.jobforge-mcp/geometra-mcp.jsonl` before guessing. Report the last `launcher_start`, `child_spawn`, `heartbeat`, `signal_received`, `child_stderr`, and `child_exit` events plus the timestamp gap from the last heartbeat. If the last event is an old heartbeat with no `signal_received` / `child_exit`, treat it as likely host SIGKILL or external process death.
|
|
35
|
+
why: OpenCode or the OS can kill the MCP server without stderr, crash logs, or core dumps. JobForge's MCP launcher writes durable lifecycle events outside MCP stdout, so silent disappearances still leave enough evidence to distinguish host kill, child crash, stderr failure, and wrapper health
|
|
36
|
+
|
|
34
37
|
## Defaults
|
|
35
38
|
|
|
36
39
|
- [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
|
|
@@ -62,7 +65,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
62
65
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
63
66
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
64
67
|
3. Read the active mode file [D3]. Use local helpers when they can replace broad file reads, prose math, manual policy checks, or artifact reuse decisions [D8]. Decide inline vs delegated work [D1].
|
|
65
|
-
4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8].
|
|
68
|
+
4. Prepare Geometra dispatches: cleanup [H3], local-helper prefilters when useful [D8], dedupe [H2], location filter [D5], file-backed preflight plan/check [D8], routing [D2], proxy/headless/stealth prompt hygiene [H8], MCP lifecycle log awareness [H9].
|
|
66
69
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D8].
|
|
67
70
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
68
71
|
7. Cross-check subagent facts against authoritative files [H7].
|
package/modes/apply.md
CHANGED
|
@@ -4,8 +4,8 @@ Live application assistant. Reads the active application form in Chrome (via Geo
|
|
|
4
4
|
|
|
5
5
|
## Hard limits
|
|
6
6
|
|
|
7
|
-
- [H1] Submit the form
|
|
8
|
-
why: Greenhouse-style forms regenerate internal field IDs after any DOM-mutating action (especially file uploads); multi-call sequences see stale IDs, enter a retry loop, and burn tens of thousands of tokens
|
|
7
|
+
- [H1] Submit the form with one stable `geometra_run_actions` action array that chains upload + fill + pick + submit. Set `softTimeoutMs: 45000`, `output: "final"`, and `includeSteps: false`; if Geometra returns `paused: true` with `resumeFromIndex`, immediately continue with the exact same `actions` array and that `resumeFromIndex`. Never split upload / fill / submit across separate direct tools or rebuilt action arrays.
|
|
8
|
+
why: Greenhouse-style forms regenerate internal field IDs after any DOM-mutating action (especially file uploads); multi-call sequences see stale IDs, enter a retry loop, and burn tens of thousands of tokens. The soft-timeout continuation keeps the MCP call under host timeout without changing the action array or restarting the application
|
|
9
9
|
|
|
10
10
|
- [H2] Never auto-retry a failed submit. On recovery failure, report the error to the orchestrator and stop. The orchestrator decides whether to re-dispatch.
|
|
11
11
|
why: duplicate applications are worse than a missed retry — ATS portals often accept a submit whose response was dropped mid-flight, so a retry double-submits. A human must decide.
|
|
@@ -64,7 +64,7 @@ Live application assistant. Reads the active application form in Chrome (via Geo
|
|
|
64
64
|
9. Route high-stakes applications through `@general-paid` [D8].
|
|
65
65
|
10. Extract form questions; classify each Section-G vs new.
|
|
66
66
|
11. Generate answers from Block B + Block F + Section G + JD.
|
|
67
|
-
12. Submit
|
|
67
|
+
12. Submit via one stable `run_actions` action array [H1] using labels [D6], `imeFriendly: true` [D4], `softTimeoutMs: 45000`, `output: "final"`, and `includeSteps: false`.
|
|
68
68
|
13. On session error, run the 4-step recovery; only one retry [H2].
|
|
69
69
|
14. On provider failure, stop and inspect telemetry before any retry [D9].
|
|
70
70
|
15. On OTP prompt, fetch the code from Gmail via `gmail_get_message`.
|
|
@@ -308,13 +308,16 @@ Notes:
|
|
|
308
308
|
|
|
309
309
|
When the candidate asks you to actually submit (or when running in auto-pipeline mode at score ≥ 3.0), follow these rules **strictly**. Greenhouse-style forms regenerate internal field IDs after any DOM-mutating action, especially file uploads. That breaks multi-call fill sequences and forces the model into a retry loop that burns tens of thousands of tokens.
|
|
310
310
|
|
|
311
|
-
### Use one `run_actions`
|
|
311
|
+
### Use one stable `run_actions` action array (Rule A — never split)
|
|
312
312
|
|
|
313
|
-
Do the entire submission
|
|
313
|
+
Do the entire submission with one stable `geometra_run_actions` `actions` array that chains all steps. Never split upload / fill / submit across separate direct tools, and never rebuild the action array between continuations.
|
|
314
314
|
|
|
315
315
|
```
|
|
316
316
|
geometra_run_actions({
|
|
317
317
|
sessionId: "...",
|
|
318
|
+
softTimeoutMs: 45000,
|
|
319
|
+
output: "final",
|
|
320
|
+
includeSteps: false,
|
|
318
321
|
actions: [
|
|
319
322
|
{ type: "upload_files", fieldLabel: "Resume/CV", paths: ["/abs/path/cv.pdf"] },
|
|
320
323
|
{ type: "fill_fields", imeFriendly: true,
|
|
@@ -326,6 +329,8 @@ geometra_run_actions({
|
|
|
326
329
|
})
|
|
327
330
|
```
|
|
328
331
|
|
|
332
|
+
If the response contains `paused: true`, `pauseReason: "soft-timeout"`, and `resumeFromIndex`, call `geometra_run_actions` again immediately with the same `sessionId`, the exact same `actions` array, `softTimeoutMs: 45000`, `output: "final"`, `includeSteps: false`, and `resumeFromIndex` set to the returned value. This is a continuation, not a retry, and it is the host-safe path for long Samsara/Greenhouse-style embeds. Stop only if the continued call fails, not merely because it paused.
|
|
333
|
+
|
|
329
334
|
**Always pass `imeFriendly: true` on `fill_fields` for Ashby** (and safe as a default everywhere). Ashby's React form swallows programmatic text input silently — visible value looks correct, `invalidCount` stays >0, and Submit fails with "field required" or "flagged as possible spam." `imeFriendly: true` fires proper composition events that clear React's internal validity state. Confirmed fix: Supabase #793 (2026-04-19). Zero cost on other portals; no reason to leave it off.
|
|
330
335
|
|
|
331
336
|
### Use `fieldLabel` over `fieldId` (Rule B)
|
|
@@ -356,6 +361,9 @@ Call 3: geometra_connect({
|
|
|
356
361
|
})
|
|
357
362
|
Call 4: geometra_run_actions({
|
|
358
363
|
sessionId: "<new sessionId from Call 3>",
|
|
364
|
+
softTimeoutMs: 45000,
|
|
365
|
+
output: "final",
|
|
366
|
+
includeSteps: false,
|
|
359
367
|
actions: [... the EXACT same actions array you used before ...]
|
|
360
368
|
})
|
|
361
369
|
```
|
|
@@ -365,7 +373,8 @@ Call 4: geometra_run_actions({
|
|
|
365
373
|
1. **Always run all 4 calls.** Do not skip Call 1 or Call 2 even if Call 1 shows an empty pool.
|
|
366
374
|
2. **Do not re-fetch the form schema.** Do not call `geometra_form_schema` between Call 3 and Call 4. Your labels haven't changed, so the same `actions` array still works.
|
|
367
375
|
3. **Do not edit the actions array.** Copy it verbatim from your first attempt. Do not re-pick fieldIds. Do not add or remove actions. Same array in, same array out.
|
|
368
|
-
4. **
|
|
376
|
+
4. **Soft-timeout pause is not failure.** If Call 4 returns `paused: true`, continue with the returned `resumeFromIndex` and the exact same action array. Do not count this as the one retry.
|
|
377
|
+
5. **Only ONE retry.** If Call 4 ALSO fails, STOP. Return this exact message to the orchestrator:
|
|
369
378
|
|
|
370
379
|
```
|
|
371
380
|
APPLY FAILED AFTER RECOVERY: <URL>
|
|
@@ -159,6 +159,8 @@ Subagents launched via the `task` tool start with a fresh context and cannot aut
|
|
|
159
159
|
|
|
160
160
|
**Fix in one sentence:** ALWAYS run `geometra_list_sessions` + `geometra_disconnect` BEFORE `geometra_connect`. Every time. No exceptions except the one explicit exception below.
|
|
161
161
|
|
|
162
|
+
If Geometra MCP itself disappears or becomes unresponsive, inspect `.jobforge-mcp/geometra-mcp.jsonl` before escalating. `signal_received` means the MCP host or parent process sent a catchable signal, `child_exit` means the Geometra child exited, `child_stderr` preserves stderr that may not show in the agent transcript, and a final stale `heartbeat` with no later event usually means SIGKILL / host reap / OS kill. Report the final event type and timestamp rather than saying only "MCP crashed."
|
|
163
|
+
|
|
162
164
|
---
|
|
163
165
|
|
|
164
166
|
#### Rule 1 — Orchestrator pre-dispatch cleanup (DO THIS EVERY TIME)
|
|
@@ -145,4 +145,20 @@ Use `npx job-forge portal:snapshot --url "{url}" --json` or `npx job-forge porta
|
|
|
145
145
|
|
|
146
146
|
`job-forge mcp:geometra` resolves Geometra in this order: `JOB_FORGE_GEOMETRA_MCP_PATH`, then `package.json -> jobForge.geometraMcpPath`, then `opencode.json -> mcp.geometra.environment.JOB_FORGE_GEOMETRA_MCP_PATH`, then a sibling local `../geometra/mcp/dist/index.js` checkout for maintainers working across both repos, then the pinned npm fallback.
|
|
147
147
|
|
|
148
|
+
JobForge's Geometra launcher writes a durable lifecycle log at `.jobforge-mcp/geometra-mcp.jsonl` in the consumer project. The log is JSONL and does not use stdout, so it does not interfere with the MCP stdio protocol. Expected events include `launcher_start`, `child_spawn`, periodic `heartbeat`, `child_stderr`, `signal_received`, `child_error`, and `child_exit`. Override the location with `JOB_FORGE_GEOMETRA_MCP_LOG_PATH`, disable it with `JOB_FORGE_GEOMETRA_MCP_LOG=0`, or tune the heartbeat with `JOB_FORGE_GEOMETRA_MCP_HEARTBEAT_MS`.
|
|
149
|
+
|
|
148
150
|
To check or modify MCP settings, edit `opencode.json` in the project root.
|
|
151
|
+
|
|
152
|
+
## Silent MCP Death Diagnostics
|
|
153
|
+
|
|
154
|
+
If Geometra MCP vanishes with no stderr, no crash log, and subsequent calls return `Not connected`, inspect the lifecycle log before making a claim:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
tail -40 .jobforge-mcp/geometra-mcp.jsonl
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- Last event is `signal_received`: the MCP host or parent process sent a catchable signal such as `SIGTERM`.
|
|
161
|
+
- Last event is `child_exit`: the Geometra child process exited and the log should show its code or signal.
|
|
162
|
+
- Last event is `child_stderr`: preserve the stderr text; it is the best upstream bug report payload.
|
|
163
|
+
- Last event is an old `heartbeat` with no later `signal_received` or `child_exit`: likely host `SIGKILL`, OS kill, or wrapper process death. No process can log after `SIGKILL`, so report the heartbeat timestamp and the missing exit event.
|
|
164
|
+
- No `launcher_start`: OpenCode never started JobForge's Geometra launcher, or it used a different MCP command/config.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.45",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"@agent-pattern-labs/iso-score": "^0.1.1",
|
|
200
200
|
"@agent-pattern-labs/iso-timeline": "^0.1.1",
|
|
201
201
|
"@agent-pattern-labs/iso-trace": "^0.5.1",
|
|
202
|
-
"@geometra/mcp": "1.62.
|
|
202
|
+
"@geometra/mcp": "1.62.2",
|
|
203
203
|
"playwright": "^1.58.1"
|
|
204
204
|
},
|
|
205
205
|
"devDependencies": {
|
|
@@ -22,12 +22,14 @@ const checks = [
|
|
|
22
22
|
["H6 requires merge and verify", () => every(files.instructions, ["batch/tracker-additions/*.tsv", "npx job-forge merge", "npx job-forge verify"])],
|
|
23
23
|
["H7 distrusts subagent prose", () => every(files.instructions, ["must originate from a file", "not from prior subagent prose"])],
|
|
24
24
|
["H8 keeps proxy secret and requires headless stealth", () => every(files.instructions, ["[H8]", "Do not transcribe `server`, `username`, `password`, or `bypass`", "`headless: true`", "`stealth: true`"])],
|
|
25
|
+
["H9 points to Geometra MCP lifecycle logs", () => every(files.instructions, ["[H9]", ".jobforge-mcp/geometra-mcp.jsonl", "launcher_start", "heartbeat", "child_exit"])],
|
|
25
26
|
["OpenCode addendum exists for task semantics", () => every(files.instructionsOpencode, ["OpenCode", "`task`", "launch acknowledgement", "Do not use `task` to poll status"])],
|
|
26
27
|
["root points to consolidated helper reference", () => every(files.instructions, ["[D8]", "modes/reference-local-helpers.md", "deterministic local helpers"])],
|
|
27
28
|
["helper reference covers score/timeline/prioritize/lineage", () => every(files.helpers, ["templates/score.json", "npx job-forge score:*", "templates/timeline.json", "npx job-forge timeline:*", "templates/prioritize.json", "npx job-forge prioritize:*", ".jobforge-lineage.json", "npx job-forge lineage:*"])],
|
|
28
29
|
["root helper defaults are consolidated", () => !/\[D(?:9|1\d|2[0-9])\]/.test(files.instructions)],
|
|
29
30
|
["shared prompt points to on-demand references", () => every(files.instructions, ["modes/{mode}.md", "modes/reference-setup.md", "modes/reference-portals.md", "modes/reference-geometra.md"])],
|
|
30
31
|
["apply mode requires headless stealth Geometra", () => every(files.apply, ["`headless: true`", "`stealth: true`", "`isolated: true`", "every Geometra auto-connect call"])],
|
|
32
|
+
["apply mode uses host-safe run_actions continuations", () => every(files.apply, ["softTimeoutMs: 45000", "resumeFromIndex", "pauseReason: \"soft-timeout\"", "This is a continuation, not a retry"])],
|
|
31
33
|
["apply mode owns high-stakes upgrade", () => every(files.apply, ["[D8]", "@general-paid", "4.0/5", "high-stakes"])],
|
|
32
34
|
["apply mode blocks provider auto-downgrade", () => every(files.apply, ["[D9]", "do not auto-downgrade", "inspect telemetry before retrying"])],
|
|
33
35
|
["models policy pins OpenCode to DeepSeek V4 Flash", () => /extends:\s*standard/.test(files.models) && count(files.models, "opencode-go/deepseek-v4-flash") >= 4],
|