Commit Graph
8 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 548f977212 style: rustfmt the four files the repo-less mission fix touched
Found while removing .github/workflows/ci.yml: three of the four files in that
change were unformatted, and four of the diffs were newly introduced (the new
prompt tests and the tool_preamble format! call). Formatting only the files that
change already touched — a repo-wide `cargo fmt` would be 63 files of unrelated
churn and belongs in its own commit.

Mechanical; `cargo test -p cm-api --lib` stays at 322 passed.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 12:19:43 -07:00
Omar SobhandClaude Opus 5 4dec77ae6d 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]>
2026-08-13 10:46:45 -07:00
Omar SobhandClaude Opus 5 768e106614 fix(microvm): a mission with no repository can run in a VM, and its work comes back
Two halves, and the first was worse than the plan assumed. `run_phase_in_vm`
packed `<missions_root>/<mission>/repo` unconditionally — a directory a
repo-less mission does not have — and then required `/mission/repo/.git` inside
the guest before spending a turn. So a repo-less microVM phase did not merely
go uncaptured: it failed before the agent ran.

A repo-less mission now gets an EMPTY workspace at the same guest path, created
host-side so the collect unpacks back over it with no special case, and the
readiness probe asks for what was actually sent — the directory rather than a
`.git` that was never going to be there.

`mission_outputs` then drops its `runtime_kind <> 'microvm'` exclusion, whose
stated reason ("a microVM mission always has a checkout") is exactly what
stopped being true. Where the files come from now depends on the runtime, and
the difference is not cosmetic: a container mission's output is still inside a
running container, while a VM's has already been unpacked onto the host by the
end-of-turn collect. Asking docker for a VM mission's files would query a
container that never existed.

The recursive copy skips symlinks rather than following them — a link out of
the tree would publish whatever it points at.

`research-vm` is the proof, added to the suite as well as the dispatch: the same
assertions as `research-only` with `runtime_kind: microvm`. A scenario nobody
runs is a scenario that does not exist.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-09 19:04:00 -07:00
Omar SobhandClaude Opus 5 91fbd2dc88 refactor: the missions root has one definition, not five
`mission_workspace::missions_root()` is now the only place that answers "where
does mission state live". It had fragmented into five: this function, private
`env::var("CLAWMATES_MISSIONS_ROOT")` copies in security_scan, benchmark_runner
and mission_outputs, and a hardcoded `MISSIONS_HOST_ROOT` const in
mission_runtime that read no env at all.

They agree on the deployed value, so nothing has broken. The risk is entirely
in what comes next: anything that sweeps or reclaims this tree has to be
sweeping the same tree the writers use, and five definitions cannot promise
that — a reaper written against one would silently leave the others' directories
behind forever, which is how the orphans got there in the first place.

A source-walk test fails any module outside `mission_workspace` that reads the
env var itself.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-09 06:18:01 -07:00
Omar Sobh 821cbb8622 feat(missions): hold every producing phase to delivering, and read markdown instead of PDFs
Two changes the portal review asked for.

1. `benchmark` and `security_hardening` had no delivery guarantee.

`empty_delivery_is_a_failure` tested `kind == "coding"`, on the reasoning that
"research phases legitimately write nothing to the tree" — which the research
directive three modules over contradicts, since it tells the agent to save
findings under /mission/repo/research/. The cost: a `benchmark` mission is ONE
benchmark phase, and with that phase exempt nothing in the platform could fail
it. Same for `security_hardening`, whose first two phases are security_scan and
research.

Now keyed on PRODUCING_KINDS = coding, research, benchmark, security_scan.
`review` stays exempt — a reviewing phase that changes nothing has done its job,
the same distinction `vm_stop_gate::per_node` makes. The test that encoded the
old rule is rewritten rather than deleted, with the reasoning that replaced it.
All 8 harness fixtures are coding phases, so harness behaviour is unchanged.

2. PDFs are dropped; markdown is the deliverable.

Rendering a PDF meant asking an LLM to convert markdown to HTML — a paid API
call per document, on the critical path of "let me read my research", which
failed on depleted Gemini credits and left every artifact unreadable. Styling at
render time is free, offline, instant and cannot 429.

- `mission_outputs` no longer requests a render.
- New `GET /api/missions/{id}/artifacts/{artifact_id}/content`. The frontend had
  no way to READ an artifact at all: it listed paths and offered a PDF preview
  that never rendered (and whose `rendered_pdf_path` had no route serving it).
  Two containment rules, both enforced: the artifact must belong to a mission in
  the caller's workspace, and the CANONICALISED path must stay under `_outputs`
  — canonicalise first, because checking the string before resolving `..` is the
  classic hole.
- `MarkdownBlock` now uses react-markdown + remark-gfm + rehype-slug. It was a
  deliberate zero-dep renderer for "the subset the refiner emits", and that
  subset stopped matching reality: agent briefs are largely GFM pipe tables,
  which it showed as literal pipes. MissionOutputReader and RefineDiffModal use
  the same component and gain tables for free.
- Heading ids come from rehype-slug and `outlineOf` slugs with the same
  GithubSlugger, so the outline rail's anchors still resolve. A test pins that
  invariant, including duplicate headings.

Styles live in globals.css under `.md-view`: the markup is generated so there
are no class hooks, and this project has no styled-jsx registry — the app-router
requirement is documented in next/dist/docs/01-app/02-guides/css-in-js.md, which
frontend/AGENTS.md exists to make me read.

The artifacts tab moved to `MissionArtifacts.tsx`. MissionCanvas was 1341 lines
against a 1250 limit BEFORE this change — already failing lint; it is now 1248.

238 backend lib tests, 20 backend test binaries, 89 frontend tests, clean tsc,
clean eslint on every file touched, production build succeeds.
2026-08-07 12:19:06 -07:00
Omar Sobh da889f83ab fix(missions): an empty repo-less phase was re-processed on every tick forever
The guard added in ceab28b fails a repo-less phase that produced nothing. It
does not record that it looked — and the selection query asks "no artifact of
this kind exists", which stays true forever for a phase with no output. So the
phase matched on every sweep: a docker copy_out per tick, and with BATCH = 5,
five such phases would occupy every slot permanently and no repo-less mission
would ever be captured again.

Measured on the first live negative control: 4 occurrences of the guard's log
line, then 8 45 seconds later.

This is a bug this codebase has already fixed once. `record_uncapturable` exists
because "five reaped phases from earlier runs blocked the batch while a freshly
finished coding phase went untouched" — its own comment. I wrote the same defect
into new code on the same sweep, which is the argument for the marker being part
of the pattern rather than something each capture path remembers separately.

Same fix as the precedent: a real file (`NO-OUTPUT.md`) behind a real artifact
row, because a row pointing at nothing turns every reader into an unexplained
404. It carries `metadata.empty = true`, the convention `mission_delivery`
already uses for its "No code changes" artifact, so "captured, and there was
nothing" is distinguishable from "captured eight documents".

The guard itself was proven correct on that same run before this was noticed:
mission failed, phase failed, artifacts 0, with the reason and the
`allow_empty` escape hatch named in the log.

237 lib tests pass.
2026-08-07 11:48:23 -07:00
Omar Sobh 89bc53b53d fix(missions): repo-less capture was publishing the agent's own identity files
First live run of `capture_repo_less_phases`: 9 artifacts, of which 2 were the
user's research. The other 7 were AGENTS.md, HEARTBEAT.md, IDENTITY.md,
MEMORY.md, SOUL.md, TOOLS.md and USER.md — the agent runtime's identity
scaffolding, seeded into the workspace root because that root is pinned to the
repository root.

The codebase already knew about these files and already had the list. What it
did not have is a defence that works without a repo: `ignore_agent_scaffolding`
writes them to `.git/info/exclude`, and a mission with no repository has no
`.git`. So the exact files that once got committed into a user's repo and
pushed (the reason that list exists) came back through a new channel.

`AGENT_SCAFFOLDING` is now `pub(crate)` and `mission_outputs` filters on it
directly — one list, two consumers, so the next file the runtime starts seeding
is excluded from both at once rather than from whichever was remembered.

Negative control: replace the filter with `&& true` and
`research_documents_are_kept_and_scaffolding_is_not` fails.

Found by running it against a live mission, not by reading it. The unit tests
passed the whole time — they seeded a tree that did not contain the scaffolding,
because I did not know it would be there.
2026-08-07 11:35:00 -07:00
Omar Sobh ceab28b902 fix(missions): a repo-less mission threw away everything its agents wrote
`capture_finished_coding_phases` selects `AND m.repo_id IS NOT NULL`. Every
`research_only` mission is repo-less by design (`requires_repo = false`), so the
whole capture path — including the `sync_out` that copies the agent's work OUT
of the container — never ran, and the container was reaped unread.

Measured on the real mission `019fdc35` ("ClawHDF5 Research"): four agents, 9.5
minutes, EIGHT research documents — an HDF5 parser design, a Rust ecosystem
survey, a seven-crate dependency map, tracing and fuzzing strategy. Result:
`mission_artifacts` = 0, mission `completed`. Not recoverable: no container, no
volume, nothing under the missions root.

The platform did not merely fail to save the work — it INSTRUCTED it. The task
preamble tells every agent "/mission/repo ... is the mission's git checkout",
whether or not one exists, and the research directive says to save findings
there. One agent recorded the contradiction verbatim: "No git repo — file is
written." It looked, saw no repo, complied anyway.

Three changes, one per link in that chain:

1. `mission_outputs::capture_repo_less_phases` — copies `/mission/repo` out of
   the container and registers each file as an artifact under `_outputs/`,
   which is a SIBLING of the mission dir and survives `teardown_container`.
   This is also the code that finally reads `produces`, until now an inert key:
   `produces = ["md","pdf"]` now drives `render_pdf` into the existing
   pdf_renderer worker.

2. The preamble is conditional. A repo-less mission is told its workspace is
   scratch, that git_operations has nothing to act on, and — the part that
   matters — that files left there ARE collected and published. An agent told
   only "there is no repo" has no reason to write anything to disk.

3. A repo-less phase that produced no files is FAILED, unless it declares
   `allow_empty`. The same rule `empty_delivery_is_a_failure` applies to coding,
   for the only channel these phases have. Note this is NOT that guard widened:
   it keys on `files_changed`, which is meaningless with no checkout, and would
   not have saved the ClawHDF5 documents.

Negative controls, each ablated and confirmed failing: ignore `has_repo` and the
preamble test fails; empty the skip-list and the capture test keeps `.git` and
`node_modules`; write artifacts inside the mission dir and the survives-the-reap
test fails.

236 lib tests pass.
2026-08-07 11:28:10 -07:00