Nexus Factory is AllCode’s new autonomous software factory: you create a Jira ticket, associate a label, and a pipeline of AI agents plans, codes, reviews, and merges the change back as a pull request, with no human in the loop. When a system runs itself, “boring” is the highest compliment you can pay it. The problem is there’s this little thing called Back Pressure, which if not addressed correctly can wreak havoc.
The Agent Architecture of Nexus
Before diving into our recent modifications, let's dive into the agents.
At the center is the Reactor: not an agent itself, but the conductor. It is a pure event router. It receives events (“the planner finished,” “a coder pushed,” “a review passed”), asks the Work Graph what to do next, and dispatches the agent the work graph names. It holds no lifecycle logic of its own, so the agents never talk to each other directly. All of the agents talk through the Reactor, which hopefully makes the pipeline observable and recoverable.
The agents themselves each do exactly one job:
- Planner reads the requirement and the target codebase. After some thought, the Planner decomposes tasks into discrete recipes. Each recipe carries a files list and a dependency graph, so independent recipes can run in parallel.
- Coders run one per recipe, in parallel. Each clones the GitHub repo, implements just its recipe, and pushes a per-task feature branch to GitHub. A coder never sees another coder's work in progress; isolation is the point.
- Reviewers verify a coder's output for correctness (not style). Trivial and low-complexity tasks get a single reviewer. Medium and high-complexity tasks go through a Task Arena: three specialized reviewers (security, correctness, style) whose verdicts are synthesized by a Judge. If review fails, the work graph sends the recipe back to a coder for rework. (For a related, simpler pattern, see our post on building an AI code review agent for GitHub.)
- Merger runs once all recipes are approved. It combines the approved branches, writes a clean PR description, and opens the pull request.
Here is how work flows between them:
Requirement
│
▼
┌─────────┐ decompose into recipes
│ Planner │──────────────┐
└─────────┘ │
▼
┌──────────── recipes (parallel) ────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Coder A │ │ Coder B │ … │ Coder N │ push per-task branches
└────┬────┘ └────┬────┘ └────┬────┘
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ Reviewer (Task Arena for medium/high complexity: │
│ security + correctness + style → Judge synthesizes) │
└───────────────┬─────────────────────┬──────────────────┘
approved │ │ needs changes
▼ └────────► back to Coder (rework)
┌──────────┐
│ Merger │ combine approved branches → open PR
└────┬─────┘
▼
Pull Request
Every arrow is mediated by the REACTOR (event router) + WORK GRAPH (state authority).
Agents never call each other directly.
The whole run is autonomous and typically finishes in ~3–5 minutes.
Here is what landed this week, with real code.
1. Deploys stop killing the reactor mid-flight
Our orchestrator runs on AWS ECS Fargate. ECS sends SIGTERM when it wants a task to stop, escalating to SIGKILL only after a timeout. The problem: our serve loop only listened for SIGINT (Ctrl-C). Oops. So every deploy hard-killed the reactor mid-tick, sometimes mid-dispatch or mid-merge.
The fix is a small signal helper that catches both signals and lets the existing graceful drain run:
/// Await an orchestrator-initiated stop.
///
/// ECS (and most container schedulers) send `SIGTERM` to request a graceful
/// stop, escalating to `SIGKILL` only after `stopTimeout`. A process that only
/// listens for `SIGINT` (Ctrl-C) never observes `SIGTERM`, so the default
/// disposition hard-kills it immediately, killing the reactor mid-tick,
/// possibly mid-dispatch or mid-merge. This future resolves on EITHER signal so
/// the caller can run a graceful drain before exiting.
#[cfg(unix)]
async fn shutdown_signal() -> std::io::Result<()> {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
tokio::select! {
_ = sigterm.recv() => {
tracing::info!("received SIGTERM; beginning graceful shutdown");
}
_ = sigint.recv() => {
tracing::info!("received SIGINT; beginning graceful shutdown");
}
}
Ok(())
}
Wire it into the serve loop and, on shutdown, tell the reactor to drain before the process exits:
if !reactor_completed {
// Ask the reactor to stop ticking/dispatching and drain (abort
// forwarders/recoveries, emit Stopped) before the process exits, so a
// SIGTERM-driven deploy never kills it mid-tick.
let _ = command_tx.send(FactoryReactorCommand::Shutdown);
reactor_handle.await;
}
Paired with a controller stopTimeout of 60s, deploys now drain cleanly instead of severing work in progress.
2. Publishing as the actual user, not a shared token
Multi-tenancy is core to the factory: each user’s repos are cloned and pushed with their connected GitHub OAuth (3LO) token, not a shared static PAT. But three publication operations, the push, the pull-request open, and the merge, were still passing an expired shared FACTORY_PUBLICATION_TOKEN. GitHub rejected the push with HTTP 400, which wedged every merge.
The fix resolves the per-user token for external-provider repos and threads it through all three operations, falling back to the shared token, then None:
fix(repo-service): publish/PR/merge use per-user 3LO token, not the dead shared PAT
AdvanceTrunk's publish, open_pull_request, and merge_branch all passed
FACTORY_PUBLICATION_TOKEN (an expired static PAT) -> GitHub rejected the
push with HTTP 400, wedging every merge. Resolve the per-user connected
OAuth (3LO) token for ExternalProvider repos and use it for all three
publication operations, falling back to the shared token then None.
Alongside this, multi-repo publication stopped reading a single global env var and now uses the per-task repo and trunk, so a run always publishes to the right place.
3. A diff viewer reviewers actually want to use
The Work Orders page renders the real added and removed lines each task produced. Two changes this week made it fast and navigable.
Clickable file names. Every file in a diff is now a link that opens the file on GitHub at the run's exact commit, so a reviewer can jump straight from the diff to the source:
{file.blob_url
? <a href={file.blob_url} target="_blank" rel="noopener noreferrer"
className="diff-file-link" title="Open on GitHub">
{file.filename ?? file.path}
</a>
: <span>{file.filename ?? file.path}</span>}
<span>+{file.additions} −{file.deletions}</span>
Concurrent diff fetching. dark-factory has 100+ factory/run/* branches, and we were fetching each branch's tip commit sequentially, roughly 30 seconds of GitHub round-trips with occasional partial results. We now take the tip SHA straight from the branch-list response and fetch commits with a bounded-concurrency stream:
// Fetch every branch tip commit CONCURRENTLY (bounded fan-out)
// instead of sequentially — 100+ run branches sequentially was
// ~30s of GitHub round-trips. `buffer_unordered` caps in-flight
// requests so we don't hammer the API.
use futures_util::stream::{self, StreamExt};
let commits: Vec<serde_json::Value> = stream::iter(run_branches.into_iter())
.map(|(_name, sha)| {
let token = token.clone();
let owner = owner.clone();
let repo = repo.clone();
async move { github_commit_json(&token, &owner, &repo, &sha).await.ok() }
})
.buffer_unordered(12)
.filter_map(|c| async move { c })
.collect()
.await;
The cap of 12 in-flight requests keeps us fast without hammering GitHub's API.
4. Richer requirement intake: images and documents
A requirement is more than a paragraph of text. This week the composer gained typed attachments, image mockups, PDFs, and text files, and the runner now threads image attachments all the way into the planner's task input so the plan can account for a screenshot or wireframe. PDF text is extracted client-side in the composer, and image attachments are decoded into the sandbox's attachments/ directory for agents to reference.
5. Auth cleanup
We retired the AWS Cognito hosted UI entirely: /auth/login now routes to the custom /signin, unauthenticated users land on our own page instead of Cognito, and marketing/landing routes plus static assets are public. Auth redirects are marked no-store so browsers never cache them.
The theme: earning “boring”
None of these are flashy features. They are the difference between a demo and a system you can leave running. Deploys that drain instead of sever. Publishing that works because it uses the right identity. Diffs that load in under a second and link to the source. Inputs rich enough to describe real work. That is the week: making the autonomous factory boring, in the best possible way.
Nexus Factory is one piece of the broader AllCode Nexus platform, alongside the usage-metering gateway that prices and reconciles what all of this AI traffic actually costs.
Built by AllCode. See the factory live at nexusfactory.allcode.com.