A task can be created pairing a local-only repository (no clone URL) with an executor that can only obtain its workspace by cloning. Nothing refuses the combination, so the task is created, an environment is provisioned on the remote host, and the run dies in the prepare script with a message that does not name the cause.
Observed on SSH:
ERROR failed to launch agent
{"error": "failed to create execution: ssh: prepare script failed"}
The UI shows "Environment preparation failed / Failed at: Running prepare script", "Agent startup failed", and "The agent finished without producing any output." Nothing says the repository could not be reached from the remote host.
Reproduced on remote_docker as well, where the container starts with an empty /workspace.
Affected executors
RequiresCloneURL() == true for ssh, local_docker, remote_docker, k8s and sprites. Of those, only local_docker can use a local checkout: it bind-mounts the host clone read-only, which works because the daemon shares the backend's filesystem. The other four have no such path and none is possible over SSH or to a remote daemon.
Why nothing catches it
- No create-time gate. The task-create dialog's only compatibility logic is agent-profile-vs-executor (
filterCompatibleAgentProfiles). No code path filters repositories by executor or executors by repository. task-create-dialog-repository-sets-control.test.tsx states the current direction outright: "the repository selection constrains which executor profiles are offered, not the reverse" - but that constraint is not implemented for cloneability.
- The existing check runs too late and elsewhere.
resolveRunnerCompatibility (internal/task/service/service_runner_switch.go) already calls RequiresCloneURL and looks up whether the attached repository has a clone URL. It only runs when switching an existing task's runner, not at task creation.
- The launch-time guard skips the first repository.
remoteWorkspaceProjectionFromLaunch (internal/agent/runtime/lifecycle/workspace_materialization.go) raises remote repository %q has no clone URL, but its loop does continue on index == 0, so it only guards sibling repositories. A single-repo task never reaches it.
launchResolveWorkspacePath correctly blanks the host path for every clone-based runtime, so the wrong directory is never forwarded. It does not reject, which is why the failure surfaces later as a prepare-script error instead.
Suggested direction
Reject the combination rather than copying the checkout to the remote host. Copying means choosing a transport, deciding what happens to uncommitted and unpushed work, and handling write-back; that is a feature, not a fix. local_docker only avoids it by sharing a filesystem.
Two layers, because the UI cannot be the only gate:
- Backend, authoritative. Reuse the existing
RequiresCloneURL plus clone-URL lookup at task create and session launch, so API, MCP create_task_kandev, and workflow-created tasks are covered too. Extend the workspace_materialization guard to include index == 0. One rule fixes all five executors.
- Frontend, preventive. Follow the direction already established in the dialog: constrain the offered executor profiles by the attached repositories' cloneability, and mirror the existing
selected-incompatible treatment used for agent profiles when a newly added local repository invalidates the current executor choice.
Whichever way the selection is constrained, the message should name the cause: a local-only repository cannot be reached from a remote environment, and the user's options are to give the repository a remote origin or choose a local executor.
Reproduce
- Add a workspace repository as a local folder with no remote origin.
- Create a task selecting that repository and an SSH (or Remote Docker) executor profile.
- Start the task. It fails during the prepare script with no indication that the repository was the problem.
A task can be created pairing a local-only repository (no clone URL) with an executor that can only obtain its workspace by cloning. Nothing refuses the combination, so the task is created, an environment is provisioned on the remote host, and the run dies in the prepare script with a message that does not name the cause.
Observed on SSH:
The UI shows "Environment preparation failed / Failed at: Running prepare script", "Agent startup failed", and "The agent finished without producing any output." Nothing says the repository could not be reached from the remote host.
Reproduced on
remote_dockeras well, where the container starts with an empty/workspace.Affected executors
RequiresCloneURL() == trueforssh,local_docker,remote_docker,k8sandsprites. Of those, onlylocal_dockercan use a local checkout: it bind-mounts the host clone read-only, which works because the daemon shares the backend's filesystem. The other four have no such path and none is possible over SSH or to a remote daemon.Why nothing catches it
filterCompatibleAgentProfiles). No code path filters repositories by executor or executors by repository.task-create-dialog-repository-sets-control.test.tsxstates the current direction outright: "the repository selection constrains which executor profiles are offered, not the reverse" - but that constraint is not implemented for cloneability.resolveRunnerCompatibility(internal/task/service/service_runner_switch.go) already callsRequiresCloneURLand looks up whether the attached repository has a clone URL. It only runs when switching an existing task's runner, not at task creation.remoteWorkspaceProjectionFromLaunch(internal/agent/runtime/lifecycle/workspace_materialization.go) raisesremote repository %q has no clone URL, but its loop doescontinueonindex == 0, so it only guards sibling repositories. A single-repo task never reaches it.launchResolveWorkspacePathcorrectly blanks the host path for every clone-based runtime, so the wrong directory is never forwarded. It does not reject, which is why the failure surfaces later as a prepare-script error instead.Suggested direction
Reject the combination rather than copying the checkout to the remote host. Copying means choosing a transport, deciding what happens to uncommitted and unpushed work, and handling write-back; that is a feature, not a fix.
local_dockeronly avoids it by sharing a filesystem.Two layers, because the UI cannot be the only gate:
RequiresCloneURLplus clone-URL lookup at task create and session launch, so API, MCPcreate_task_kandev, and workflow-created tasks are covered too. Extend theworkspace_materializationguard to includeindex == 0. One rule fixes all five executors.selected-incompatibletreatment used for agent profiles when a newly added local repository invalidates the current executor choice.Whichever way the selection is constrained, the message should name the cause: a local-only repository cannot be reached from a remote environment, and the user's options are to give the repository a remote origin or choose a local executor.
Reproduce