Why T25 orchestrates agent CLIs instead of being an in-process framework
The architecture decision that defines T25: every pipeline role runs a real, headless CLI in an isolated git worktree, and the factory governs what happens between stages.
The decision in one sentence
When we started building T25, the architecture question was not "which model to use". It was: is the agent code inside our process, or a process we govern? We chose the second option, and it explains almost everything about the product, from how we isolate tasks to how we audit decisions.
An in-process agent framework runs inside your application: you call a function, the agent loop executes in your memory, and the result comes back as a return value. A factory that orchestrates CLIs does the opposite: every pipeline role, research, planner, dev, QA, reviewer, security, is a real CLI, authenticated with the user's own subscription, spawned as a separate process, fed a prompt assembled by the factory, and returning output the factory parses before deciding anything.
Between the two models there is a difference of posture. In the first, the agent is a part of you. In the second, the agent is a vendor with a contract, and the contract is what makes governance possible.
What happens between stages when you orchestrate agent CLIs
The central question is who controls what happens between one agent's output and the next dispatch. In T25, the answer is FactoryService (src/factory/service.ts): a loop over task.state where each iteration picks the adapter, runs the role, parses the artifact, and calls transition(). The state machine (src/core/state-machine.ts) centralizes legal transitions via assertTransition, the service never assigns state directly.
This means the model does not walk the pipeline on its own. It produces text in one stage; code decides whether that text is a valid artifact, what it authorizes, and what the next state is. A parse failure is not silently retried: it fails the task with a descriptive error. A diff that exceeds limits.max_files_changed or limits.max_diff_lines is stopped by checkDiffLimits() after implementation. A plan only asks for human approval if requiresPlanApproval() says so, based on the risk configured in t25.yaml.
This design is what let us argue, in an earlier post, that the model's APPROVE is not the merge: the reviewer's verdict is recomputed by evaluateReview() from the findings, and merging remains a human action after the required checks pass.
Isolation by worktree, isolation by process
Every task runs in its own git worktree under .factory/worktrees/<taskId>, on branch factory/<taskId>, never in the main checkout. We covered the details elsewhere: path guards against symlink and .. escape, ownership checks via .factory-metadata, per-PID locks, and a cleanup() that refuses a dirty worktree and never force-removes.
Orchestrating CLIs adds a second isolation on top: process isolation. Each CLI adapter (src/adapters/base.ts) spawns with a plain argv, no shell, branch names and paths are never interpolated into a command string. Prompts over 100KB are materialized into a temp file instead of going through argv, to stay under ARG_MAX. There is a timeout, abort, and quota detection, and an optional Docker sandbox provider runs commands with network none.
The practical effect on blast radius: when an agent misbehaves, an edit loop, a giant diff, an attempt to write outside the directory, the damage is contained in a disposable directory, on a branch nobody merged, in a process that can be killed. None of that depends on the model being in a good mood.
Auditability as a consequence
Processes that finish leave a trail. Every agent run in T25 records a log, SSE events, and an artifact persisted in the store addressed by SHA-256; loose Markdown in .factory/artifacts/ is no longer the source of truth. Operator decisions, approve, merge, cancel, retry, archive, go to an append-only audit.jsonl with actor and timestamp, exposed through GET /audit in the dashboard. In production, tasks, runs, and leases live in Postgres, with atomic leasing via FOR UPDATE SKIP LOCKED and worker heartbeats.
When a post-incident review asks "who wrote this, with which prompt, from which state", the answer is a query, not an act of recollection. In an in-process architecture, getting the equivalent means instrumenting every call yourself, which most teams defer forever.
Swapping agents without a rewrite
t25.yaml defines, per role, an ordered list of adapters, a fallback chain, not a fixed adapter. If Claude Code is not on the PATH, or answers with quota exhausted, selectAdapter() tries the next one in the list. A new adapter is a thin CliAdapterSpec (binary name plus buildArgs), not a runtime reimplementation: spawn, timeout, and logging machinery is shared in src/adapters/base.ts.
This benefit only exists because the agent is an external process.
Cost and credentials stay with you
Each CLI runs authenticated with the user's own session and subscription, on their machine. T25 asks for no provider API key, does not proxy model traffic, and never sees conversation content beyond the output it parses. The usage events adapters emit at the end of each run feed a per-run cost estimate (src/core/cost.ts) with configurable rates, for visibility, not billing.
For a team already paying for these CLIs, that changes the project's math: the factory adds orchestration cost; it does not replace the agents' contract. And for security, it means no new credential is created, stored, or transmitted.
What the choice costs
No architecture is free. Three costs are real and live with us:
- Spawn latency. Every stage pays the boot of a CLI process. For short tasks this is proportionally expensive. Our answer is to batch work per stage, the whole pipeline runs few dispatches per task, and to treat machine time as cheap compared to human review time.
- Dependency on third-party CLIs. Version changes, flag changes, and output format changes break adapters. We mitigate with tolerant parsers (legacy output compatibility), version detection at boot, and contract tests, but the fragility is real and shows up from time to time.
- Less control over the model loop. When the agent is a library in your process, you see every tool call, every token, and can intervene mid-reasoning. When it is a process, you govern the borders, the prompt going in, the output and logs coming out, and the middle belongs to the CLI. For governance that is enough; for research into model behavior, it is not.
There are cases where an in-process framework is the right call: prototypes, orchestration experiments, products whose core is the agent itself. If your problem is "I need a custom agent loop inside my service", a library serves you better. If the problem is "I need a team to ship software with agents without giving up review, scope, isolation, and a human merge", the external-process factory is the design that holds.
FAQ
Isn't orchestrating CLIs slower than calling an API directly?
It is, per stage: a process has to boot and a prompt has to be materialized. The fair comparison is not per call, it is per delivered task. A cheap API call that produces code with no spec, no isolated worktree, and no auditable verdict costs more in review than a dispatch with spawn overhead.
What if the CLI I use goes away or changes radically?
The per-role fallback chain exists for exactly that. Because the contract between the factory and the agent is the parsed output, swapping one stage's executor does not change policy, the same argument as in our post on what a software factory is: the factory decides what happens to the code; who writes it is replaceable.
T25 runs agents with my credentials. Is that safe?
T25 runs locally, on your machine, with the CLIs you already authenticated. No credential is sent to a server of ours, the control plane is local, the Postgres is yours. What the factory centralizes is policy, not secrets. The worktree and workspace rules cover the defensive side in detail.
Can I use T25 as a library inside my service?
That is not the design. T25 is a standalone harness with a REST/SSE API, a CLI, and a dashboard over the same FactoryService. If you need to embed an agent loop in your own process, our recommendation is to use that loop where it shines and leave governance, worktrees, gates, merge, to the factory.
Where do I read more about the architecture?
The documentation has the architecture guide and the domain model, and the blog already covers worktree isolation and the approval gate in depth. T25's code is closed; the documentation describes the behavior cited here.