{"record":{"id":"84172acef61fb107","repo":"JuliusBrussee/caveman","slug":"cave-harness-plan-mismatch","errorCode":"cave_harness_plan_mismatch","errorMessage":"cave_harness_plan_mismatch","messagePattern":"cave_harness_plan_mismatch","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/execution-kernel.ts","lineNumber":82,"sourceCode":"  build: CaveBuildLock;\n  harness: string;\n  adapterVersion: string;\n  upstreamVersion: string;\n  agentId?: string;\n  contextIR: ContextIR;\n  plan: CavePlan;\n}): LockedHarnessPreparation {\n  const build = parseCaveBuildLock(input.build);\n  if (build.harness.id !== input.harness ||\n      build.harness.adapter_version !== input.adapterVersion ||\n      build.harness.upstream_version !== input.upstreamVersion) {\n    throw new Error(\"cave_harness_build_mismatch\");\n  }\n  if (input.agentId !== undefined && build.agent_id !== input.agentId) {\n    throw new Error(\"cave_harness_agent_mismatch\");\n  }\n  if (stableStringify(input.plan) !== stableStringify(build.selected_plan)) {\n    throw new Error(\"cave_harness_plan_mismatch\");\n  }\n  const planSHA256 = sha256(stableStringify(input.plan));\n  if (planSHA256 !== build.plan_sha256) {\n    throw new Error(\"cave_harness_plan_digest_mismatch\");\n  }\n  const contextIRSHA256 = sha256(stableStringify(contextIRToWire(input.contextIR)));\n  if (contextIRSHA256 !== build.context_ir_sha256) {\n    throw new Error(\"cave_harness_context_ir_mismatch\");\n  }\n  const [provider, ...modelParts] = input.plan.model.split(\"/\");\n  const model = modelParts.join(\"/\");\n  if (!provider || !model) throw new Error(\"cave_harness_model_invalid\");\n  return Object.freeze({\n    build,\n    plan: build.selected_plan,\n    planSHA256,\n    contextIRSHA256,\n    provider,","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/execution-kernel.ts#L64-L100","documentation":"The kernel compares a stable serialization of the runtime CavePlan against the plan embedded in the build lock. It throws when the two plans differ in any serialized field, meaning the execution request's plan is not the plan that was locked at build time.","triggerScenarios":"Calling prepared or streamAgentInternal with an input.plan whose stableStringify output differs from build.selected_plan — any added/removed/changed plan field (model, reasoning, tools, etc.) triggers it.","commonSituations":"Mutating the plan after building the lock (adding a tool or changing reasoning); constructing a fresh plan object with defaults differing from the locked one; JSON round-tripping that changes field order only if not using the stable stringify contract; schema version bump changing plan shape.","solutions":["Pass the exact plan stored in build.selected_plan into prepareLockedHarnessExecution.","Rebuild the build lock from the current plan if the plan legitimately changed.","Avoid mutating the plan object between lock creation and execution; treat it as frozen.","Diff stableStringify(plan) against stableStringify(build.selected_plan) to see which field drifted."],"exampleFix":"// before\nconst plan = { ...locked.selected_plan, reasoning: \"high\" };\nawait prepared({ build: lock, plan });\n// after\nawait prepared({ build: lock, plan: lock.selected_plan });","handlingStrategy":"try-catch","validationCode":"import { stableStringify } from \"./execution-kernel\";\nif (stableStringify(plan) !== stableStringify(lock.selected_plan)) {\n  lock = await buildCaveBuildLock({ plan });\n}","typeGuard":"function planMatchesLock(plan: object, lock: { selected_plan: object }): boolean {\n  return stableStringify(plan) === stableStringify(lock.selected_plan);\n}","tryCatchPattern":"try {\n  await prepared({ build: lock, plan });\n} catch (err) {\n  if (err instanceof Error && err.message === \"cave_harness_plan_mismatch\") {\n    const fresh = await buildCaveBuildLock({ plan });\n    return prepared({ build: fresh, plan });\n  }\n  throw err;\n}","preventionTips":["Pass lock.selected_plan itself as the plan for locked executions.","Freeze the plan (Object.freeze) after lock creation to prevent accidental mutation.","Avoid JSON round-trips of the plan before execution; keep the original object.","When plan shape evolves, rebuild all outstanding locks rather than patching them."],"tags":["execution-kernel","build-lock","plan","serialization"],"backgroundTag":"schema-validation-failed","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}