fix(missions): give repo-less container missions the workspace they are promised

Every agent on a research_only mission refused to work, each reporting it was
"in Claude Code", had no /mission/repo, and only had Read/Edit/Bash. All three
statements were true. The run still recorded completed — 5 turns, 7.4k tokens,
0 artifacts, no error.

The machinery is correct when a repo IS bound (verified on a live prod
per-mission container: /mission/repo present, all 5 agents pinned). Only the
repo-less path was broken, in three layers that disagreed by construction:

- sync_in no-oped without a host checkout and copy mode does not bind /mission,
  so NOTHING created /mission/repo. The microVM tier already creates it, for the
  stated reason that "the guest needs the workspace to exist before the agent
  writes into it". Creating it host-side also un-breaks sync_out, equally a
  no-op before, so work survives across phases instead of being wiped.
- pin_agent_workspaces returned Ok after pinning ZERO agents, so the
  deliberately-fatal guard in mission_orchestrator could never fire. Its error
  text already described the exact outcome we got.
- The prompt advertised ZeroClaw tool names and explicitly denied `bash`, while
  every executor ends in `claude -p`: microVM passes Read/Edit/Write/Bash/Agent,
  session passes Read/Edit/Write/Bash, and claude_cli agents get Claude Code's
  native toolset — ZeroClaw's gating never reaches the subprocess. It was
  telling agents to use missing tools and avoid present ones.

And it went green because mission_outputs logged the failed collect and
continued — with the fail-empty rule and the NO-OUTPUT marker both BELOW that
continue, so the phase was retried forever and never failed. The retry is now
bounded by a grace window off completed_at.

Verified end to end: mission completed, agent wrote
/mission/repo/research/firecracker_vs_docker.md, collected and registered as a
document artifact (6.6 kB of real content).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-13 10:46:45 -07:00
co-authored by Claude Opus 5
parent af89020dfd
commit 4dec77ae6d
4 changed files with 134 additions and 44 deletions
+18 -3
View File
@@ -249,12 +249,27 @@ fn host_repo(mission_id: uuid::Uuid) -> std::path::PathBuf {
/// Push the host checkout into the container before a phase runs.
///
/// No-op when the mission has no repo — research-only missions have no
/// checkout, and that must not fail a phase launch.
/// A repo-less mission has no checkout to push, but it still needs
/// `/mission/repo` to EXIST inside the container: the phase prompt tells the
/// agent that is its working directory, `mission_orchestrator` pins every
/// claw's `workspace.path` to it, and `mission_outputs` copies it back out to
/// register artifacts. This used to return early instead, so none of those three
/// were true — the pin resolved to nothing, ZeroClaw fell back to each agent's
/// own sandbox, and the agents (correctly) reported they had no such directory
/// and refused to work. Creating it empty is what the microVM tier already does,
/// for the same reason: see `microvm_executor::inject` ("the guest needs the
/// workspace to exist before the agent writes into it").
///
/// Creating it host-side rather than `mkdir`-ing in the container keeps the copy
/// cycle symmetric — `sync_out` unpacks over this same path, so work written by
/// one phase survives into the next instead of being wiped by the next
/// `sync_in`.
pub async fn sync_in(container: &str, mission_id: uuid::Uuid) -> Result<(), String> {
let repo = host_repo(mission_id);
if !repo.is_dir() {
return Ok(());
tokio::fs::create_dir_all(&repo)
.await
.map_err(|e| format!("create empty workspace {}: {e}", repo.display()))?;
}
let docker = crate::container_exec::connect()?;
copy_in(&docker, container, &repo, "repo").await