santifer/career-ops · warning
MODE_MISSING
MODE_MISSING
Error message
AI search needs a newer career-ops — update to enable it.
What it means
Returned (HTTP 400, JSON code MODE_MISSING) by the /api/explore/ai route when fs.readFileSync of modes/discover.md throws ENOENT. The route reads the canonical discover mode at request time rather than bundling a prompt, so an older career-ops core that predates the discover mode — or a checkout where modes/discover.md was deleted — degrades gracefully and tells the client to update. The Scan tab stays usable; only the AI explore feature is unavailable.
Source
Thrown at web/src/app/api/explore/ai/route.ts:51
body = await req.json();
} catch {
return Response.json({ error: "bad json" }, { status: 400 });
}
const query = (body.query || "").trim();
const cliId = body.cliId;
if (!query || !cliId) return Response.json({ error: "query and cliId required" }, { status: 400 });
const resolved = resolveCli(cliId);
if (!resolved) return Response.json({ error: `CLI '${cliId}' not found on this machine` }, { status: 404 });
const { spec, binPath } = resolved;
// Read the CANONICAL mode at request time — single source of truth, never a
// homegrown prompt. Missing (older core) → graceful 400 so the Scan tab stays usable.
let mode: string;
try {
mode = fs.readFileSync(path.join(careerOpsRoot(), "modes", "discover.md"), "utf8");
} catch {
return Response.json({ code: "MODE_MISSING", error: "AI search needs a newer career-ops — update to enable it." }, { status: 400 });
}
const { lines } = assembleDedupContext();
const memory = readMemory();
const memoryLine = memory.trim() ? `\n\nWHAT YOU KNOW ABOUT THE USER (persistent memory):\n${memory.trim()}` : "";
const knownBlock = lines.length ? `\n\n--- ALREADY KNOWN (dedup — do NOT propose these) ---\n${lines.join("\n")}` : "";
const prompt = `${mode}${OUTPUT_CONTRACT}${memoryLine}${knownBlock}\n\n--- USER INTENT ---\n${query}\n`;
const isClaude = cliId === "claude";
const args = isClaude
? [
"-p",
prompt,
"--output-format",
"stream-json",
"--verbose",
"--include-partial-messages",
"--permission-mode",View on GitHub (pinned to 9b17a8ac97)
Solutions
- Update the core: `node update-system.mjs apply` to pull the release that ships modes/discover.md, then restart the web server.
- If careerOpsRoot() is overridden (env var / CLI flag), point it at a current, complete career-ops checkout that contains modes/discover.md.
- Verify the file is present post-update: `ls modes/discover.md`; if still missing, the update didn't apply fully — re-run apply and check its output.
- Until updated, use the non-AI Scan tab — the route's 400 is intentionally non-blocking so scanning still works.
- If you intentionally removed discover.md, restore it from git: `git checkout HEAD -- modes/discover.md`.
Example fix
// before: 400 { code: 'MODE_MISSING' } on every AI explore call
// (older core, no modes/discover.md)
// after: pull the release that ships the mode
// $ node update-system.mjs apply
// $ ls modes/discover.md # confirm present
// (restart web server) Defensive patterns
Strategy: validation
Validate before calling
// Client-side: probe the endpoint's precondition before showing the AI tab,
// or check the file directly if you share the filesystem with the server.
import { existsSync } from 'node:fs';
import path from 'node:path';
function aiExploreAvailable(careerOpsRoot) {
return existsSync(path.join(careerOpsRoot, 'modes', 'discover.md'));
}
// UI gate: if (!aiExploreAvailable(root)) disable the AI tab / show 'update to enable'.
// Server-side alternative: expose the check via a tiny /api/explore/ai/status route. Try / catch
// Fetch handler: treat MODE_MISSING as a feature-gate, not a hard error.
const res = await fetch('/api/explore/ai', { method: 'POST', body: form });
const body = await res.json();
if (res.status === 400 && body?.code === 'MODE_MISSING') {
setAiTabState('unavailable-needs-update'); // graceful UI, Scan tab still works
return;
}
if (!res.ok) throw new Error(body?.error || `explore failed (${res.status})`); Prevention
- Keep the web dashboard and the career-ops core on the same release — version skew is the usual cause.
- After `node update-system.mjs apply`, confirm `modes/discover.md` exists before restarting the web server.
- If you override careerOpsRoot(), point it at a complete, current checkout.
- Surface MODE_MISSING in the UI as 'update available' rather than a generic error.
When it happens
Trigger: A GET/POST to /api/explore/ai with valid query+cliId on a career-ops install whose modes/ directory lacks discover.md. Concretely: an out-of-date checkout (discover.md was added in a later release), a partial/corrupt update that removed modes/, or the careerOpsRoot() pointing at a non-standard/older tree.
Common situations: User upgraded the web dashboard but not the core (dashboard and career-ops core version skew); running against a fork that pruned modes/; careerOpsRoot() misconfigured (env/flag) to point at a stale clone; an interrupted `node update-system.mjs apply` left modes/ short.
Related errors
- Validation failed (${err.message}) and revert also failed ($
- LOCK_TIMEOUT
- USAGE
- ROW_NOT_FOUND
- Cannot read benchmarks at ${path}: ${err.message}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/a252302310660f4a.
Report an issue: GitHub.