The previous post covered recording the starting environment and keeping findings attached to an attempt. Those checks and review results are now available in both interfaces. The new work also covers switching between active projects, choosing the worker team and bringing GitHub feedback back into a codemod.
Keeping work running across projects
The Mac app talks to a bundled Rust executable over local stdin and stdout. It sends typed commands with request IDs. Each response includes the current state and an acknowledgement or error. Swift handles rendering and editing focus. Rust still owns scheduling, permissions, saved work and the decisions about which actions are available.
That includes navigation. Each open project has its own engine, with its workers, drafts and ownership lock. Switching to another project moves the previous engine into the background rather than closing it. Coming back returns to that same running session.
The polling code keeps processing every opened project:
pub(crate) fn poll_workers(&mut self) -> rusqlite::Result<()> {
for engine in std::iter::once(&mut self.foreground).chain(self.background.values_mut()) {
// A failed project must not stop other projects or replace their input.
engine.poll_error = engine
.poll_workers()
.err()
.map(|error| format!("Project update failed: {error}"));
}
self.refresh_projects()
}A worker can finish or ask for help while I’m looking at another project. Its status appears in the project list, and its dialog stays with that project instead of taking over the editor I’m using.
The terminal and Mac app share the saved plans, conversations, drafts and results. I still need to close one before opening the same project in the other. A project lock prevents two controllers from running its workers at once; simultaneous control from both interfaces isn’t implemented.
Choosing the team and handling quota limits
I can now choose a model and reasoning effort for the planner and reviewer, then set the execution team by model and count. For example, one Codex executor and two Muse executors allow three execution workers to run at once, subject to task dependencies.
Saving a different team affects new assignments. Workers already running keep their model, and reducing the parallel limit lets existing work finish. The selectable limit is 1–16 executors, but adding workers doesn’t increase the VM’s CPU or memory budget.
Those choices also define the available quota fallbacks. When a provider explicitly reports that its quota is exhausted, the affected worker stops and its checkpoint is checked before the controller tries another compatible model already selected in the team. The replacement gets the task checkpoint, accepted instructions and acknowledged messages. A task that requires a particular provider still has that restriction.
The distinction between quota exhaustion and another failure is deliberate. Switching providers requires a structured quota signal. Even an error message saying “quota exhausted” is insufficient on its own. This test excerpt covers errors that should keep the same worker assigned:
for error in [
json!({"message":"quota exhausted","status":429}),
json!({"codexErrorInfo":"rateLimitExceeded"}),
json!({"codexErrorInfo":"httpConnectionFailed"}),
] {
assert!(from_error("codex", &error, Some("gpt-6.1-sol")).is_none());
}If it’s unclear whether an instruction was delivered, work pauses for receipt recovery. Replaying it on another provider could repeat something that already happened. If no selected model can continue, I need to change the team or refresh models after quota resets, then resume the saved work. Cooldowns persist across restarts and are shared by projects using the same database.
Bringing GitHub feedback back into the work
An open PR can now supply feedback to the same workspace as the local checks and review. The GitHub reader collects the PR’s reviews, unresolved review threads and comments, along with failing checks on the latest head commit. It only reads them; it doesn’t post replies or resolve threads.
Once the current source has a clean review, I can select feedback to bring into the finding ledger. Imported items start as needing investigation and trigger a fresh review before becoming repair work. A review comment may describe an earlier version, and a failed GitHub check may need more investigation before there is a code change to make.
After importing selected items, the action starts a review:
if items.is_empty() || self.store.import_pr_feedback(&self.mods[index], &items)? == 0 {
self.mods[index].agent_review = self.store.review_state(id)?;
return Ok(false);
}
self.mods[index].agent_review = self.store.review_state(id)?;
self.begin_agent_review(index)?;
self.view = View::Findings(0);
Ok(true)Confirmed findings then use the existing repair and verification flow. Importing the same feedback again doesn’t create another finding, and the imported evidence keeps its GitHub reference.
There’s a related change when publishing the result. If the repository has a PR template, the controller places its contents between the generated summary and the recorded verification results. The writer doesn’t rewrite its checklist or mark requirements as completed. The template’s outer whitespace is trimmed, but its contents stay intact.
The GitHub reader currently caps reviews, comments, review threads and check contexts at 50 each. Large PR discussions still need pagination.
Running the app
The app builds from the repository with Xcode 26 or newer and Rust:
./macos/build.sh
open target/macos/Sprowt.appThe current bundle is locally signed for development. It isn’t a notarized release. The interface runs on macOS, but workers still build and check source in Linux, so this doesn’t add worker support for native macOS or iOS builds.
