nfltr orch refactored its own CLI
A large refactor is a test of whether several coding agents can share one job without stepping on each other: the parts must not touch, they must land on one branch, and nothing may break. nfltr connects machines through an outbound-only relay, and nfltr orch runs Claude Code agents on your own machines, driven by a hub that is your own Claude Code session. We gave a hub the same kind of goal twice in one day: split a large package of the nfltr CLI's own code into smaller ones, moving code without changing behavior.
What we found: in round 1 the hub and its agents split the orchestration commands into seven packages, and the operator had to fix three things before the result passed the repository's checks. In round 2 they split the CLI's root package into eight, the root package's own tests got 37 % faster, and the operator fixed nothing. Round 2 also took longer (61.1 against 43.6 minutes), made one test gate about 3 s slower, and found three gaps, all fixed since. Every number comes from the two run logs.
Setup
Both rounds used the same fleet: four nodes on one Mac, each joined with --max-agents 2, so 8 agent slots. The hub and every agent used the same Claude model. The spending cap was $30 in round 1 and $25 in round 2.
The plan was made without a model. A reference graph of the package's declarations shows what each command reaches; what two or more commands reach is a shared base, split out first, and with it out the parts no longer touch each other. The goal named the parts, their order and the rules: move code without changing behavior, keep every test, push only to the run's own branches. It pointed every agent at the plan.
Both rounds started the hub with the nfltr orch "<goal>" launcher. That launcher has since been removed (v2.1.20, October 22, 2026); the same hub now runs as your own Claude Code session with the hub tools added through claude mcp add nfltr -- nfltr mcp --toolset hub.
What the hub did
In round 2 the hub followed the plan's shape, with no message from the operator:
- At the start it spawned the base and one part that does not depend on it, together.
- At 14.8 minutes, after the base reported, it spawned the six remaining parts in one call, each on its own branch off the base's commit.
- At 36.5 minutes, after all six reported, it spawned one agent to merge the parts. Every conflict was in import, handler or setter lines of files that stay in the root package, none in moved code.
- At 61.3 minutes it wrote its final answer.
Every part reported its build, vet and short tests passing, and test names identical before and after. All 8 parts and the merge were pushed. The operator rebased the result onto main and ran the repository's gates; all passed.
| Round 1: orchestration commands | Round 2: root package | |
|---|---|---|
| Packages split out | 7 | 8 |
| Agent turns | 8 | 11: 9 work turns, 1 stray placeholder, 1 follow-up message |
| Agent processes | 12, 4 of them gave up unclaimed | 11, none unclaimed |
| Most agents at once | 6 of 8 slots | 6 of 8 slots |
| Makespan | 43.6 min | 61.1 min |
| Utilization, overall / parallel phase | 0.22 / 0.35 | 0.30 / 0.60 |
| Model spend | $8.50 | about $5.95 ($6.81 reported, see below) |
| Operator steers | 0 | 0 |
| Operator code fixes | 3 | 0 |
Evidence: two refactor runs on one fleet
- Setup
- Four nodes on one Mac,
--max-agents 2each (8 slots), nfltr.xyz as the relay, the same Claude model for the hub and every agent, caps of $30 (round 1) and $25 (round 2). One run per round, 2026-10-02, on the nfltr CLI's own source. - Numbers
- Round 2: 8 packages, 0 operator steers and 0 operator code fixes, makespan 61.1 min, utilization 0.30 overall and 0.60 in the parallel phase, about $5.95 of spend. Round 1: 7 packages, 3 operator code fixes, 43.6 min, 0.22 / 0.35, $8.50.
- Reproduce
- (a) With the released binaries you can run the same shape on your own repository:
nfltr node join --max-agents 2on each machine, then a refactor goal in a Claude Code session with the hub tools. Live runs cost real model spend. (b) One-off live runs on the closed nfltr repository; no checked-in harness reproduces them, and the target code is not public. - Limits
- One run per round; the two rounds split different packages, so the makespans are not a like-for-like comparison. All four nodes shared one Mac's CPUs, which slowed every agent's checks (see What got slower). No single-agent run of the same refactor was made.
What the split bought
The root package went from 11,998 to 4,945 source lines, 59 % smaller, and from 225 to 83 top-level tests. No test was lost: 2,190 test names across the CLI before and after. The root package's own tests went from 28.9 s to 18.4 s, 37 % faster.
In round 1 the orchestration commands' package went from 22,769 to 9,778 source lines, 57 % smaller.
Evidence: the split's size and test-time effect
- Setup
- The root package before and after round 2's merge, on the same Mac; source lines and test names counted over the CLI's Go files.
- Numbers
- 11,998 to 4,945 source lines (59 % smaller); 225 to 83 top-level tests; 2,190 test names before and after; the root package's tests 28.9 s to 18.4 s (37 % faster). Round 1: 22,769 to 9,778 lines (57 %).
- Reproduce
- (a) On your own repository: count a package's source lines and
go test -list '.*' ./...names before and after a split. (b) Our check, in the closed nfltr repository: the same comparison on the two commits; no make target wraps it. - Limits
- The test times are one figure before and one after, on one machine. The line counts measure the root package only: the code moved, it did not disappear.
What got slower
The CLI's orchestration test gate got about 3 s slower: 36.7 s and 35.2 s after the split, against 32.5 s and 32.8 s before. The gate now covers the 8 new packages. Three packages in it, the root and two of the new ones, each build the full CLI binary for those tests, about 6 s of link each, and their relay tests now run beside one another. Inside the gate they take 26 to 31 s each, against 14 to 18 s alone. Two remedies were open when this was written: build the binary once per gate run, or keep the packages that are not orchestration commands out of this gate (they still run in the full suite).
Update: the first remedy has landed. The test processes of one gate run now share one CLI binary, linked once by whichever needs it first. Measured afterwards with a warm cache and nothing else running, the gate took 31.4 s and 31.6 s against 32.4 s and 33.0 s before; it is now bound by one package that takes 27 to 29 s on its own, the next target. Building the binary in the gate's recipe before the tests was also measured and rejected: 38.6 s and 38.4 s.
Evidence: the orchestration test gate's time
- Setup
- The CLI's orchestration test gate on one Mac, two runs per variant: before the split, after it, and after the shared-binary fix (warm cache, nothing else running, after one warm-up run).
- Numbers
- 32.5 / 32.8 s before the split, 36.7 / 35.2 s after it. The later fix measurement: 32.4 / 33.0 s before, 31.4 / 31.6 s with one shared binary, 38.6 / 38.4 s with the binary built first in the recipe (rejected).
- Reproduce
- (a) Not with the released binaries: the gate tests the CLI's own source. (b) Our check, in the closed nfltr repository:
make test-orchestration-nfltr-cliat the commits before and after. - Limits
- Two runs per variant on one machine. The fix measurement is a separate session from the split measurement, so its "before" (32.4 / 33.0 s) is not the same pair as the split's (32.5 / 32.8 s).
The run took longer than round 1, 61.1 minutes against 43.6, because each agent worked longer, not because agents waited. Parts ran 10 to 21 minutes, against 5 to 12 in round 1, and the merge 24.3 minutes against 9.2. In round 2 every agent ran the root package's tests and the repository's commit hook (vet, a Windows build, a dead-code check) while five others did the same on one Mac; one agent saw the root package's short tests take 233 s. Agents on separate machines, or checks scoped to what a part changed, would shorten it.
Running the hook was the brief's change after round 1. There, agents skipped it, and the operator's three fixes were two things the hook catches (an unused copied helper and three missing markers) and one bug in the hook itself, which read moved code as new. In round 2 every commit went through the fixed hook, and nothing needed fixing.
The round 1 fixes, measured live
- A slow clone no longer stalls other agents. In round 1 the hub held a hub-wide lock while it waited for a worker to accept a turn, and a worker first clones the repository, which took 1.5 to 3.5 minutes, so every other spawn waited behind that clone. 4 of 12 agent processes gave up after their 2-minute claim timeout, and one part started 6.7 minutes after its spawn. Now no call to a node or worker runs under a hub-wide lock. In round 2 the six parts' agents launched in the same second and each started 3.9 to 18.3 s later. The wait in the parallel phase fell from 6.7 minutes to 0.3.
- One repository mirror per machine. In round 1 each of the four nodes cloned its own mirror, about 330 MB each. In round 2 one 341 MB mirror served all four. Its one clone took 2.5 minutes; the other first-wave agent waited on the mirror's lock and started 3 s after the clone finished, and the second wave found the mirror ready.
The 3 s steps between the six starts came from that lock: each agent refreshed the mirror in turn. Update: that fix has landed too. An agent that waited for the lock and finds a refresh that finished after it began waiting uses it instead of fetching again, so six agents racing on a warm mirror make one fetch, not six. It has a regression test; it has not been measured in a live run yet.
Evidence: spawn waits before and after the lock fix
- Setup
- The two refactor runs above: four nodes on one Mac, 8 slots, one run each.
- Numbers
- Round 1: 4 of 12 agent processes gave up unclaimed, one part started 6.7 min after its spawn. Round 2: six agents launched in the same second and started 3.9 to 18.3 s later; the parallel phase's wait fell from 6.7 min to 0.3. Mirrors: four of about 330 MB in round 1, one of 341 MB in round 2, its clone 2.5 min.
- Reproduce
- (a) With the released binaries: join one machine with
nfltr node join --max-agents 4, have the hub spawn several agents on one repository at once withworkspaceset, and compare their start times; the node keeps one mirror per repository. The live numbers are from one-off runs. (b) Our check, in the closed nfltr repository:make test-orch-hub-fake-claude(a slow provision delays only its own agent) andmake bench-orch-node-mirror(disk and clone time of four agents cloning from one node's mirror). - Limits
- One run before and one after. The 3 s refresh steps were measured before the later mirror fix; that fix is covered by a test, not by a live run.
Gaps round 2 found
All three were being fixed when this post was written; all three have landed since (see the update after the list).
- A wake-up turn replaced the final answer. The merging agent gave its full report, then a timer it had started in the background finished, and Claude Code woke the session for one more turn. That turn's one line, that the notification was only a finished timer, became the agent's result. The hub noticed, sent one message asking for the full report and got it 9 s later.
- A resumed session's usage was counted twice. The follow-up turn reported the whole session's totals, and the hub added them to the first turn's. About $0.86 was counted twice, so the run reported $6.81 for about $5.95 of spend, and a spending cap would trip early.
- The hub spawned a placeholder. Between its two spawn calls it started one agent whose whole objective was a template placeholder. The agent refused in 4 s for $0.05, and the hub called it a mistake and stopped it. This was the hub's error, and nfltr does not judge objectives.
Update: within one session the first answer is now the result; a later turn replaces it only when it answers a new message, such as a steer. A resumed session reports its usage net of what it had already reported, so caps and spend lines see per-turn figures. And spawn_agent's brief now carries the fact that the agent receives the brief exactly as sent, with nothing filled in for a placeholder; nfltr still does not judge objectives. Each has a regression test.
At the end, before they were stopped, the idle nodes had used 0.3 to 0.4 s of CPU each in 72 minutes, and the relay 5.8 s.
Is it for you?
Fits if you have a large change that splits into parts that do not touch each other, a plan for that split (here, from a reference graph rather than a model), and checks that judge each part by exit status. The hub runs the parts in parallel and an agent merges them; the plan and the checks are what made round 2 need no operator fixes.
Not ready for refactors whose parts overlap: nothing here measured how the hub handles parts that conflict in moved code. Agents sharing one machine run each other's checks slowly, so parallel parts on one host may not finish sooner. nfltr orch runs Claude Code agents only; nfltr.xyz runs on one VM, and a hub and its nodes belong to one account; and the nfltr repository is closed source, so this run cannot be repeated on the same code (the repositories that are public since 2026-10-29 are in Four nfltr repositories are now open source).
Try next: join each machine you want agents on, then add the hub to your own Claude Code session and give it a goal:
nfltr node join --max-agents 2
claude mcp add nfltr -- nfltr mcp --toolset hub
claude -p "<goal>"
The guides: Join machines as nodes, Use nfltr from Claude Code, and the hub tools reference. What a hub is useful for, measured on other work, is in What nfltr orch is useful for, measured; how it compares with Claude Code's own agent teams is in part 5. The design and the tests behind the hub are in How distributed Claude Code is built with nfltr, and the process we use for the rest of our Go code in How we build reliable Go software.