💻 The Laptop Is the Last Bottleneck: From Coding Agents to Shared Workflows
on July 26, 2026
One coding agent in a terminal is manageable. You ask it to change a file, glance at the diff, run the tests you remember, and move on. The invisible checklist still fits in one head.
Then you open a second agent. Then a third. One works on the API. Another rewrites the docs. A third tries a security pass. Suddenly you are no longer only programming. You are scheduling, reviewing, and remembering. Did this run include static analysis? Did that branch get a security look? What did yesterday’s failure teach us that this chat has already forgotten?
The agents are not necessarily incapable. The process around them is still personal. That is the thesis of this article, grounded in a small PHP experiment: expose an engineering “change review” checklist as an HTTP API with Framework X as the asynchronous runtime and Darkwood Flow as the orchestration layer.
The central technical idea is simple:
Framework X receives and executes requests. Flow describes and coordinates the work triggered by those requests.
They are not competitors. They sit on different layers of the same problem.
The hidden workflow
Software work already is a workflow, even when nobody draws the boxes:
Change code
↓
Run tests
↓
Run static analysis
↓
Review security
↓
Update documentation
↓
Review result
When that sequence lives only as habit—“I always run PHPUnit before I push”—it is fragile. A tired Friday omits a step. A new teammate never learned it. An agent chat that rolled out of context cannot reconstruct it.
The missing step is not a smarter prompt. It is making the process explicit: named steps, inputs and outputs, failure modes, and a trace of what actually ran.
Why the laptop becomes the bottleneck
Saying “the laptop is the bottleneck” is easy to mishear as a hardware complaint. The interesting claim is organizational.
The laptop holds more than CPU. It holds identity and credentials, repository access, private prompts and skills, local scripts, model configuration, agent history, project conventions, and the lessons from last week’s failures. It is both a runtime and a knowledge silo.
When several agents run in parallel, everything that compensates for their limits remains personal:
- remember tests, static analysis, security, docs, review, deploy constraints;
- remember what previous runs taught;
- keep the secrets and the allowlists on one machine.
That does not scale to a team. When the person—or the laptop—leaves, the automation and its memory leave too.
From agent commands to workflows
The conceptual shift is from:
Give one agent a large task
to:
Define a sequence of bounded steps
Each step may later be PHP code, a shell command, an API call, a coding agent, another model, a human approval, or a deploy service. In the experiment described below, the steps are deterministic local PHP services—on purpose. The point is the shape of the process, not another LLM demo.
The workflow is the durable layer. The current model or agent is replaceable.
The Framework X experiment
The prototype lives in a small project, framework-x-flow. It is not a product. It is a laboratory for one question: can we turn a private engineering checklist into something HTTP-visible, inspectable, and reproducible—without pretending we have built a cloud agent platform?
Architecture in one diagram:
HTTP request
↓
Framework X route + middleware
↓
Flow workflow (jobs over Ip payloads)
↓
Steps (validate → analyse → quality → docs → review → retrospective)
↓
Trace + retrospective
↓
JSON response (+ atomic file under var/runs/)
Responsibility split, as implemented:
| Concern | Framework X | Darkwood Flow | This demo |
|---|---|---|---|
| HTTP requests / routing | Yes | No | Handlers |
| Long-running ReactPHP server | Yes (HttpServerRunner) |
Embedded via driver | — |
| Workflow definition | No | Yes (FlowFactory jobs) |
Change-review chain |
| Step execution | Handler-level only | Yes (jobs / Ips) | Step classes |
| Workflow context | Request attributes | Ip data pipeline |
ChangeReviewContext |
| Async primitives | Promises, generators, Fibers | Drivers (ReactDriver, FiberDriver, …) |
EmbeddedReactDriver adapter |
| Ip concurrency | — | MaxIpStrategy |
CLI / HTTP experiment |
| Intra-run parallel quality checks | — | Not a native DAG | ReactPHP Promise::all adapter |
| Run persistence / traces | Access logs | No first-class run store | JSON files + step traces |
| Safe JSON errors | Default HTML 500s | — | JsonErrorHandler |
| Secure sandbox | No | No | No |
How Framework X works (just enough)
Framework X (clue/framework-x, currently 0.17.x) is a microframework on ReactPHP. The same App can run as a long-lived CLI HTTP server or behind a traditional PHP SAPI.
Runner selection is SAPI-aware: on CLI, HttpServerRunner binds a socket (default 127.0.0.1:8080, overridable with X_LISTEN) and runs ReactPHP’s event loop; behind FPM or Apache, SapiRunner handles one request from globals. For tests and embedding, App::__invoke() processes a single PSR-7 request and awaits promises.
Handlers may return a synchronous PSR-7 response, a PromiseInterface, or a generator that yields promises. On PHP 8.1+ in CLI mode, HttpServerRunner also wraps each request in a FiberHandler, so React\Async\await() can look synchronous without blocking the mental model of the code. Errors are meant to become HTTP responses rather than crash the process: ErrorHandler normalizes throwables, rejected promises, and invalid return types.
Dependency injection is a small Container: array config, factories, ENV-shaped parameters, optional PSR-11. Class-string controllers resolve lazily.
That is why Framework X matters for this article: it can expose workflows over HTTP and keep an asynchronous process alive. It is not a scheduler, queue, multi-tenant sandbox, or agent runtime.
It also has rough edges worth naming. The stock ErrorHandler always embeds exception class, message, and file location in HTML 500 bodies—there is no debug/production switch. RouteHandler keeps its own HTML 404/405 path. The package is still 0.x. A long-lived CLI process shares memory across requests: mutable singletons, blocking calls, and nested Loop::stop() are footguns. An async runtime does not make blocking PHP non-blocking; sleep(), synchronous HTTP clients, and heavy filesystem work still stall the loop.
How Flow fits
Darkwood Flow (darkwood/flow v8.1.1, PHP ≥ 8.5) is a driver-based job pipeline. You build a chain with FlowFactory::create(), push work as Ip values, and call await() until the stream drains. A job receives $ip->data and returns the next data value. Failures become Flow\Exception\RuntimeException; an optional per-stage errorJob runs, and the Ip does not continue to the next stage.
In the demo, the change-review workflow is assembled roughly like this (closures wrap invokable step objects—Flow accepts Closure|JobInterface, not bare invokables):
return (new FlowFactory($driver))->create(static function () use (…) {
yield [static fn (ChangeReviewContext $ctx) => $validate($ctx), $errorJob];
yield [static fn (ChangeReviewContext $ctx) => $analyse($ctx), $errorJob];
// concurrent quality adapter, or sequential tests / static / security stages
yield [static fn (ChangeReviewContext $ctx) => $docs($ctx), $errorJob];
yield [static fn (ChangeReviewContext $ctx) => $final($ctx), $errorJob];
yield [static fn (ChangeReviewContext $ctx) => $retro($ctx), $errorJob];
}, ['driver' => $driver]);
Conceptual steps:
validate
↓
analyse
↓
tests | static-analysis | security-review
↓
documentation
↓
final-review
↓
retrospective
HTTP surface (Framework X routes):
$app->get('/health', HealthHandler::class);
$app->get('/workflows/change-review', DescribeWorkflowHandler::class);
$app->post('/workflows/change-review/runs', RunWorkflowHandler::class);
$app->get('/workflows/change-review/runs/{runId}', GetWorkflowRunHandler::class);
$app->get('/analysis/framework-x-flow', AnalysisHandler::class);
$app->get('/experiments/sequential-vs-concurrent', ExperimentHandler::class);
What Flow provides: job chains, Ips, drivers, strategies such as MaxIpStrategy, and errorJob hooks.
What the demo adds: HTTP representation, run IDs, step timing traces, atomic JSON persistence under var/runs/, retrospective formatting, controlled failure flags, and—critically—an EmbeddedReactDriver. Native Flow\Driver\ReactDriver::await() calls Loop::run() and Loop::stop(). Framework X’s HttpServerRunner already owns the loop; calling native await() inside a request would stop the HTTP server. The embedded driver keeps React async/delay semantics but completes a Promise without stopping the shared loop. That adapter is project code, not upstream Flow.
Default quality mode packs three checks into one Flow job via ReactPHP Promise::all (ConcurrentQualityChecksStep). That is also an adapter. Flow’s native concurrency model is many Ips through a stage with MaxIpStrategy, not a parallel DAG of child steps inside one Ip.
Sequential versus concurrent execution
The CLI experiment (php bin/experiment) compares three Ips, each delaying 0.05s:
| Mode | Strategy | Observed elapsed (local, 2026-07-26) |
|---|---|---|
| Sequential | MaxIpStrategy(1) |
152.446 ms |
| Concurrent | MaxIpStrategy(3) |
50.088 ms |
| Speedup | — | ~3.04× |
These timings are illustrative and non-scientific. They match the expected shape: three serial delays versus three overlapping delays.
A subtle finding: LinearIpStrategy alone still overlaps async jobs under ReactDriver, because the pull loop can start the next Ip while prior work is in flight. True sequential control needed MaxIpStrategy(1). That is the kind of detail that only shows up when you measure.
Concurrency helps only when tasks are independent and non-blocking. Wrapping blocking work in a Promise does not free the event loop.
Failure is part of the workflow
Orchestration is not only ordering happy paths. The demo accepts controlled failure flags in the POST body (exception, promise, async, security), rejects invalid JSON with a safe 400, and maps step failures into a structured run report.
Because Framework X’s default errors are HTML and verbose, the app binds a JsonErrorHandler that extends ErrorHandler. That subclassing matters: a plain middleware placed “in front” still gets a stock HTML ErrorHandler unshifted by App bootstrap. Access log, when present, must be immediately followed by an ErrorHandler instance. Clients see codes like internal_error or invalid_json; details go to a local log file.
A failed security check, for example, produced a report with:
{
"code": "quality_check_failed",
"message": "Security check failed: no dependency audit result was supplied."
}
and continued far enough for a retrospective to propose a concrete workflow change—not a stack trace in the browser.
The retrospective step
The last job builds a deterministic retrospective: slow steps, failed checks, warnings, lessons, and suggestedWorkflowChanges. On the security failure above, the proposal was:
{
"target": "security-review",
"reason": "The security check failed because no dependency audit result was supplied.",
"proposal": "Add a dependency audit step before final review."
}
The note attached to every retrospective is intentional:
Proposals only — this step does not rewrite application code or workflow definitions.
Propose a workflow change is not the same as silently rewrite the workflow. Reviewable self-improvement beats uncontrolled self-modification. Automatic learning, automatic proposal generation, and autonomous mutation of production automation are three different claims; this prototype only practices the middle one.
What moving to the cloud really means
Putting the same PHP on a server is not collaboration. A team workflow platform still needs shared execution, durable history, identity and permissions, secret isolation, sandboxing, network policy, quotas, auditability, cleanup, and often queues or distributed scheduling.
Products in the industry—Upsun Dispatch is one example of the broader conversation about remote agent execution—exist because those problems are hard. Mentioning that context is not an endorsement of any vendor. The point for Darkwood is narrower: orchestration (what Flow explores) and secure multi-tenant agent hosting (what this prototype does not attempt) are separate layers. Framework X and Flow do not currently provide a secure sandbox; claiming otherwise would be false.
What this prototype does not prove
- No secure cloud sandbox, credential broker, or command allowlist.
- No distributed scheduler or durable message queue.
- No multi-tenant isolation or automatic team governance.
- No safe autonomous self-modification of workflows.
- No evidence that every coding task should become a formal workflow.
- No evidence that Framework X is the only or best runtime for this model.
- No proof that encoding a checklist on one laptop automatically makes it a team workflow—sharing still needs deployment, shared storage, and social process.
It does show that an explicit run report is easier to inspect than a private chat transcript, that async runtimes and workflow engines solve different problems, and that honest adapters (EmbeddedReactDriver, concurrent quality via Promise::all) are better than inventing features the libraries do not have.
Conclusion
Models will change. Coding agents will change. Vendors will rename the same infrastructure problems.
What remains useful is the process a team can name, run, inspect, fail safely inside, and improve on purpose: validate, analyse, check quality, document, review, retrospect—encoded as data, not as folklore on one laptop.
Framework X can be the door that receives the request. Flow can be the graph that coordinates the work. Neither replaces the hard parts of cloud agent execution. Together, in this small experiment, they make the checklist visible.
The useful artifact is no longer the private prompt. It is the workflow you can run again tomorrow—and argue about in a pull request.