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

  1. Update the core: `node update-system.mjs apply` to pull the release that ships modes/discover.md, then restart the web server.
  2. If careerOpsRoot() is overridden (env var / CLI flag), point it at a current, complete career-ops checkout that contains modes/discover.md.
  3. 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.
  4. Until updated, use the non-AI Scan tab — the route's 400 is intentionally non-blocking so scanning still works.
  5. 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

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


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/a252302310660f4a. Report an issue: GitHub.