{"record":{"id":"a252302310660f4a","repo":"santifer/career-ops","slug":"mode-missing","errorCode":"MODE_MISSING","errorMessage":"AI search needs a newer career-ops — update to enable it.","messagePattern":"AI search needs a newer career-ops — update to enable it\\.","errorType":"error_code","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"web/src/app/api/explore/ai/route.ts","lineNumber":51,"sourceCode":"    body = await req.json();\n  } catch {\n    return Response.json({ error: \"bad json\" }, { status: 400 });\n  }\n  const query = (body.query || \"\").trim();\n  const cliId = body.cliId;\n  if (!query || !cliId) return Response.json({ error: \"query and cliId required\" }, { status: 400 });\n\n  const resolved = resolveCli(cliId);\n  if (!resolved) return Response.json({ error: `CLI '${cliId}' not found on this machine` }, { status: 404 });\n  const { spec, binPath } = resolved;\n\n  // Read the CANONICAL mode at request time — single source of truth, never a\n  // homegrown prompt. Missing (older core) → graceful 400 so the Scan tab stays usable.\n  let mode: string;\n  try {\n    mode = fs.readFileSync(path.join(careerOpsRoot(), \"modes\", \"discover.md\"), \"utf8\");\n  } catch {\n    return Response.json({ code: \"MODE_MISSING\", error: \"AI search needs a newer career-ops — update to enable it.\" }, { status: 400 });\n  }\n\n  const { lines } = assembleDedupContext();\n  const memory = readMemory();\n  const memoryLine = memory.trim() ? `\\n\\nWHAT YOU KNOW ABOUT THE USER (persistent memory):\\n${memory.trim()}` : \"\";\n  const knownBlock = lines.length ? `\\n\\n--- ALREADY KNOWN (dedup — do NOT propose these) ---\\n${lines.join(\"\\n\")}` : \"\";\n  const prompt = `${mode}${OUTPUT_CONTRACT}${memoryLine}${knownBlock}\\n\\n--- USER INTENT ---\\n${query}\\n`;\n\n  const isClaude = cliId === \"claude\";\n  const args = isClaude\n    ? [\n        \"-p\",\n        prompt,\n        \"--output-format\",\n        \"stream-json\",\n        \"--verbose\",\n        \"--include-partial-messages\",\n        \"--permission-mode\",","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/web/src/app/api/explore/ai/route.ts#L33-L69","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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`."],"exampleFix":"// before: 400 { code: 'MODE_MISSING' } on every AI explore call\n// (older core, no modes/discover.md)\n// after: pull the release that ships the mode\n//   $ node update-system.mjs apply\n//   $ ls modes/discover.md   # confirm present\n//   (restart web server)","handlingStrategy":"validation","validationCode":"// Client-side: probe the endpoint's precondition before showing the AI tab,\n// or check the file directly if you share the filesystem with the server.\nimport { existsSync } from 'node:fs';\nimport path from 'node:path';\nfunction aiExploreAvailable(careerOpsRoot) {\n  return existsSync(path.join(careerOpsRoot, 'modes', 'discover.md'));\n}\n// UI gate: if (!aiExploreAvailable(root)) disable the AI tab / show 'update to enable'.\n// Server-side alternative: expose the check via a tiny /api/explore/ai/status route.","typeGuard":null,"tryCatchPattern":"// Fetch handler: treat MODE_MISSING as a feature-gate, not a hard error.\nconst res = await fetch('/api/explore/ai', { method: 'POST', body: form });\nconst body = await res.json();\nif (res.status === 400 && body?.code === 'MODE_MISSING') {\n  setAiTabState('unavailable-needs-update'); // graceful UI, Scan tab still works\n  return;\n}\nif (!res.ok) throw new Error(body?.error || `explore failed (${res.status})`);","preventionTips":["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."],"tags":["web","api","version-skew","update-system","graceful-degradation","filesystem"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}