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():
- The supervisor working directory is no longer resolved when
working_directory is None.
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:
- 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
- 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.
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:
...the supervisor correctly starts in
/path/to/my-project, but any worker created via thehandoffMCP tool starts in the CAO checkout directory.Investigation
Traced the complete execution path:
working_directoryis conditionally omitted from the run-step payload when it isNone, so it propagates unchanged toTmuxClient, which correctly falls back toos.getcwd().In
_handoff_impl:Previous behaviour
_create_terminal()(still used byassign) automatically resolved the supervisor terminal's current working directory viaGET /terminals/{id}/working-directorywheneverworking_directorywasNone. 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 singlePOST /terminals/run-step. The supervisor working-directory inheritance step was no longer performed._resolve_handoff_provider()still resolvessession_name,caller_id, andallowed_tools, but notworking_directory.Root cause
In
_handoff_impl():working_directoryisNone.working_directoryis only included in the payload when truthy, so the field is omitted entirely when unset.Potential fix
I tested a local fix that:
_handoff_impl()(mirroring_create_terminal()):working_directoryin the run-step payload:This is confined to
server.pyand preserves the existing fallback behaviour if CWD resolution fails.RunStepRequest.working_directoryalready defaults toOptional[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.