{"record":{"id":"b7ecb40a246e2e63","repo":"Yeachan-Heo/oh-my-codex","slug":"autopilot-workingdirectory-must-match-the-selected","errorCode":null,"errorMessage":"autopilot.workingDirectory must match the selected writable workspace","messagePattern":"autopilot\\.workingDirectory must match the selected writable workspace","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/modes/base.ts","lineNumber":351,"sourceCode":"      ? await readModeStateForExplicitSession(mode, explicitSessionId, projectRoot)\n      : await readModeState(mode, projectRoot);\n  if (!current) throw new Error(`Mode ${mode} not found`);\n  await mkdir(scope.stateDir, { recursive: true });\n\n  if (mode === 'ralph') {\n    assertRalphUpdateMatchesSession(current, scope.sessionId);\n  }\n\n  const updatedBase = { ...current, ...updates };\n  if (mode === 'autopilot') {\n    const submittedSessionId = typeof updates.session_id === 'string' ? updates.session_id.trim() : '';\n    if (submittedSessionId && scope.sessionId && submittedSessionId !== scope.sessionId) {\n      throw new Error('autopilot.session_id must match the selected writable session scope');\n    }\n    const canonicalWorkspace = projectRoot ?? process.cwd();\n    const submittedWorkingDirectory = typeof updates.workingDirectory === 'string' ? updates.workingDirectory.trim() : '';\n    if (submittedWorkingDirectory && submittedWorkingDirectory !== canonicalWorkspace) {\n      throw new Error('autopilot.workingDirectory must match the selected writable workspace');\n    }\n    if (scope.sessionId) updatedBase.session_id = scope.sessionId;\n    updatedBase.workingDirectory = canonicalWorkspace;\n    // Shared invariant, not a local copy: see src/state/handoff-carrier.ts for why a supplied\n    // malformed carrier must be rejected before any merge normalizes it away.\n    const suppliedHandoffs = updates.handoff_artifacts;\n    assertValidHandoffCarriersIn(updates as Record<string, unknown>, 'supplied');\n    // Also the PERSISTED state: a stored `state.handoff_artifacts` array survives this shallow merge\n    // and the gate would read it, so validating only the incoming payload left it fail-open.\n    assertValidHandoffCarriersIn(current as Record<string, unknown>, 'stored');\n    const currentHandoffs = requirePersistedHandoffCarrier(current.handoff_artifacts, 'handoff_artifacts carrier');\n    const nextHandoffs = requirePersistedHandoffCarrier(suppliedHandoffs, 'supplied handoff_artifacts carrier');\n    if (Object.keys(currentHandoffs).length > 0 || Object.keys(nextHandoffs).length > 0) {\n      updatedBase.handoff_artifacts = { ...currentHandoffs, ...nextHandoffs };\n    }\n  }\n  delete updatedBase.trustedPipelineProgress;\n  if (!Object.prototype.hasOwnProperty.call(updates, 'run_outcome')) {","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/modes/base.ts#L333-L369","documentation":"Autopilot state updates enforce that a submitted workingDirectory matches the canonical workspace (projectRoot, falling back to process.cwd()). This keeps autopilot state anchored to the repository it was created for.","triggerScenarios":"Calling updateModeState/updateAutopilotPipelineState in autopilot mode with a non-empty updates.workingDirectory that differs from projectRoot ?? process.cwd().","commonSituations":"Running the CLI from a different directory than the project root, passing an absolute path with different casing or a trailing slash, symlinks resolving differently, or a stale hardcoded path.","solutions":["Drop workingDirectory from updates — it is canonicalized and set for you","Set projectRoot explicitly when constructing the mode manager so the canonical workspace is deterministic","Normalize your path (path.resolve, realpath) before comparing/passing it"],"exampleFix":"// before\nawait updateModeState('autopilot', { workingDirectory: '/repo/../repo' });\n// after\nawait updateModeState('autopilot', { }); // workingDirectory set from projectRoot ?? cwd","handlingStrategy":"validation","validationCode":"const wd = (updates.workingDirectory ?? '').trim();\nconst canonical = projectRoot ?? process.cwd();\nif (wd && wd !== canonical) updates = { ...updates, workingDirectory: canonical };","typeGuard":null,"tryCatchPattern":"try { await updateModeState('autopilot', updates); } catch (e) { if (e instanceof Error && e.message.includes('autopilot.workingDirectory')) { delete updates.workingDirectory; await updateModeState('autopilot', updates); } throw e; }","preventionTips":["Always construct mode managers with an explicit projectRoot","Compare paths with path.resolve to avoid trailing-slash/casing mismatches"],"tags":["autopilot","working-directory","path-validation"],"backgroundTag":"path-mismatch","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}