Skip to content

Regression: MCP handoff no longer inherits supervisor working directory #389

Description

@alexmercenary

Symptom

Handoff workers start in the cao-server process working directory (typically the CAO repository root) instead of inheriting the supervisor's project directory. For example, launching with:

cao launch --working-directory /path/to/my-project --agents supervisor

...the supervisor correctly starts in /path/to/my-project, but any worker created via the handoff MCP tool starts in the CAO checkout directory.

Investigation

Traced the complete execution path:

MCP handoff
  → _handoff_impl()
  → POST /terminals/run-step
  → run_agent_step()
  → create_terminal()
  → TmuxClient.create_window()
  → _resolve_and_validate_working_directory()

working_directory is conditionally omitted from the run-step payload when it is None, so it propagates unchanged to TmuxClient, which correctly falls back to os.getcwd().

In _handoff_impl:

if working_directory:
    payload["working_directory"] = working_directory

Previous behaviour

_create_terminal() (still used by assign) automatically resolved the supervisor terminal's current working directory via GET /terminals/{id}/working-directory whenever working_directory was None. This inherited the supervisor's pane CWD so workers started in the same project directory.

Current behaviour

During the _handoff_impl() refactor in #320, the handoff flow was simplified to a single POST /terminals/run-step. The supervisor working-directory inheritance step was no longer performed. _resolve_handoff_provider() still resolves session_name, caller_id, and allowed_tools, but not working_directory.

Root cause

In _handoff_impl():

  1. The supervisor working directory is no longer resolved when working_directory is None.
  2. working_directory is only included in the payload when truthy, so the field is omitted entirely when unset.

Potential fix

I tested a local fix that:

  1. Restores supervisor CWD auto-resolution in _handoff_impl() (mirroring _create_terminal()):
if working_directory is None:
    current_terminal_id = os.environ.get("CAO_TERMINAL_ID")
    if current_terminal_id:
        try:
            response = requests.get(
                f"{API_BASE_URL}/terminals/{current_terminal_id}/working-directory",
                timeout=_mcp_timeout(),
            )
            if response.status_code == 200:
                working_directory = response.json().get("working_directory")
        except Exception:
            pass
  1. Always includes working_directory in the run-step payload:
payload["working_directory"] = working_directory

This is confined to server.py and preserves the existing fallback behaviour if CWD resolution fails. RunStepRequest.working_directory already defaults to Optional[str] = None, so the unconditional inclusion is safe.

I haven't opened a PR yet because I wanted to confirm this aligns with the intended architecture first, but I'm happy to submit one if this approach makes sense.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions