# Codex and Muse can now work together in Sprowt Harness

By Horia Jurcut · 8 October 2026

In [the last post](https://minimumeffort.dev/blog/sprowt-harness-parallel-work), I had two Codex workers making changes in separate folders and checking the combined result. Muse, worker-to-worker messages and an independent reviewer were still on the list. All three are now connected.

Codex and Muse can work on the same codemod, the name I use for a feature or fix. They can ask each other questions, and a failed check can go back to the worker responsible for the task. Once the combined changes pass their checks, I can ask a fresh Codex conversation to review them and send defects back for another round of fixes.

## Giving workers a shared plan

I can describe the feature without deciding how many workers it needs or telling them to communicate. Astra plans the tasks, their file ownership and any shared contracts. Rust assigns work when its dependencies are ready, balancing the available Codex and Muse workers. The limit is still two active executors per codemod, and a small change can stay with one.

For an API and interface change, the plan can define the request and response shapes before either worker starts. The interface worker can build against a stub while the API worker implements the endpoint. A later task checks them together. If both need to edit the same files, or one genuinely needs the other's implementation first, they run in order.

The workers now have saved mailboxes tied to their tasks. They can send an update when an interface changes, ask about a missing detail or tell a peer that a handoff is ready. A message doesn't give its sender permission to edit the recipient's files.

For example, these are the arguments an interface worker could send to `send_worker_message`, using the actual tool schema. Here `api` is an illustrative task ID from the plan:

```json
{
  "to": "api",
  "kind": "ask",
  "body": "Does an empty search return all tasks or no results?",
  "key": "empty-search-behavior",
  "reply_to": null
}
```

The harness supplies the sender's identity. The stable `key` lets it recognize a repeated send; a reply uses the saved question's ID in `reply_to`.

Questions that need my input appear in the composer. The waiting task can finish independent work, but it can't declare itself complete with unanswered questions. Other tasks can keep running. Replies and delivery receipts are saved, so reconnecting a worker includes checking which instructions it already received.

I also changed model routing. Planning now uses Astra at xhigh effort. Jev, a hosted decision model, helps choose the effort for Sol 6.1 executor tasks; Rust handles provider assignment. Those are separate decisions. The scheduler balances active work, without knowing either subscription's remaining quota.

## Getting Muse into the VM

Muse runs its full CLI inside the codemod's Linux VM. It has its own task folder and conversation, and uses the same verification and coordination mechanisms as Codex. Its account credential stays on the Mac. A host broker authenticates allowed model requests without copying that credential into the worker's files.

The two providers take different paths. Production Codex still runs its agent process on the Mac and sends tool execution into the VM. I also built a separate probe for running the full Codex CLI inside Linux, but that hasn't replaced the production path.

Getting a short Muse task to work left another problem in longer conversations. One task kept failing with a JSON decoding error. The VM's process-output buffer retained only the latest 1 MiB, so a large message could lose its beginning before the harness read it.

I changed the bridge to send small chunks and wait for an acknowledgement before sending the next one. I also fixed it to drain the remaining output after a process exits.

Inside the bridge's frame-sending loop, this excerpt from `FramedOutput.send` waits before the next chunk can leave:

```python
if not self.condition.wait_for(
        lambda: self.acknowledged == self.sequence or self.stopped, timeout=45):
    raise BridgeFailure("Muse output acknowledgment timed out.", stage="guest output")
```

Each frame has a sequence number. The matching acknowledgement releases the wait; a stop or timeout ends the transfer instead of letting it keep filling the output buffer.

Large-message tests through the real VM path passed. The interrupted codemod still needed a retry; fixing the transport didn't mean its original work had finished.

## Returning failures to their owners

Previously, a failed verification could leave me to ask the worker to try again. Now Rust sends the failed command and its output back to that worker for one automatic recovery attempt. It keeps the same task, folder and file scope, and must preserve the planned checks. A repeated failure pauses the task and anything that depends on it.

The eligibility query in `recover_verification` makes that limit explicit. This is the SQL from the Rust implementation, reformatted for reading; the placeholders bind the codemod, task attempt and worker:

```sql
SELECT id, attempt FROM task_runs
WHERE mod_id = ?1 AND source = ?2 AND worker_id = ?3
  AND status = 'blocked'
  AND verification_retries = 0
  AND checks != '[]'
```

The same transaction saves the failed checks as feedback, clears the old results and increments `verification_retries`. Restarting the harness doesn't turn that counter back to zero.

There is also a repair path for a problem discovered after another task has finished. An integration worker can identify the responsible task, affected files and a reproducible failing check. Rust validates that request and reopens the original owner's work. The affected tasks are checked again, followed by verification of the combined source. That path has a separate limit of two repair handoffs per plan.

A concrete example in the test fixtures is a greeting script that prints the right output for a supplied name but also accepts an empty name. Its happy-path check passes. The integration check catches the missing rejection and sends the repair back to the script's owner, even when Codex is checking work assigned to Muse.

## Reviewing beyond the passing checks

I can now request a fresh Sol 6.1 xhigh reviewer after the checks pass. It reads the combined diff, source, original request and check results inside the VM. It can't edit source itself. Findings identify the defect, its location and evidence, then return to the existing task owners for fixes.

I tested the review flow with a deliberately introduced bug that the smoke checks missed. The reviewer found it, the owner repaired it, and the checks and a fresh review passed. Automatic review fixes stop after two rounds.

The opening guard in `review_fixes` checks that there are findings, that the review still belongs to the current work, and that its fix budget hasn't been used:

```rust
let Some(state) = self
    .review_state(code_mod.id)?
    .filter(|r| r.status == "findings" && r.rounds < 2 && r.current(code_mod))
else {
    return Ok(false);
};
```

The function then checks the source fingerprint before reopening any owners. A finding from an older version can't start a repair against newer work.

Review remains optional, and I still confirm publication of the PR.

This exposed a lifecycle bug in the harness itself. I had been deleting task folders after final verification. When review reopened only some of the tasks, those workers rebuilt their environments, but the next full check also needed an untouched task's Python environment. It was gone.

Task folders now stay available through review and repairs. A replacement plan or closing the codemod removes them. If a saved check's executable is missing, the original worker gets one recovery attempt to restore its environment before the task and final checks run again.

A review belongs to the source it inspected. New edits invalidate it, and publishing still requires the current files to match the verified version. That leaves me with the combined diff, independently rerun checks and, when I request it, a separate review to inspect before publishing.
