Calyx 持續工作階段功能一天經歷七輪 AI 程式碼審查,2,048 項測試全過仍未能啟動
Seven rounds of AI code review in one day, 2,048 tests green, and the feature had never started once
Calyx 作者回顧七個月開發紀錄,指出持續工作階段功能在一天內經歷七輪 AI 程式碼審查,測試執行數增至 2,048 且零失敗,卻仍未能在應用程式內啟動。實際啟動後才揭露 Calyx 命令與 ghostty 的 shell 包裝造成重複 exec;修正後功能隨 v0.29.0 發佈,沒有用戶收到故障版本。
Since March 2026 I have been building Calyx, a native macOS terminal for running and supervising coding agents in parallel. I work on it alone, and Claude Code writes most of the code. As of October 7, 2026 the repository has 880 commits (752 excluding merges) and 110 release tags, v0.1.0 through v0.46.1.
I run development under one self-imposed rule: every finding from the review subagent gets fixed. This post reads seven months of commit history back and pulls out what that review found and what it did not. There is no control group. Nothing here was built twice, once with review and once without, so this is not a measurement of how well AI review works. It is a record with numbers, and the last section separates what the record supports from what it does not.
The workflow
In my current setup, Claude Code subagents write all of the code. The main conversation does not write code; it plans, approves and coordinates. Every change goes through the same stages:
- Plan in Plan Mode. I approve the plan.
- From a clean tree,
git worktree adda worktree. A test-writing subagent writes the tests and confirms they fail before any implementation exists. - Implement. A test-running subagent confirms everything passes.
- A review subagent reviews the worktree's diff. Commit in the worktree. I approve.
- Merge into main, then delete the worktree and the branch.
Two rules sit on top. Every review finding is fixed, none silently skipped, and I am asked only when a finding is genuinely unclear. And symptoms are never patched: when a defect shows up, the fix goes back to the primary source and addresses the root cause.
Those rules live in my local configuration, not in the repository, and the file has no history. I cannot show when each rule took effect. The oldest trace in a commit message is 7ec033794 on June 22, which fixed even suggestion-level findings and gave the reason as "ALL @code-reviewer findings MUST be fixed".
What I can use as a record is the commit bodies. Twenty-nine non-merge commits identify themselves in the subject line as responses to review or audit findings. I counted subjects containing "review", "audit" or "batch N", then removed the unrelated ones (the diff-review feature, "preview", README updates) and the comment-label cleanup discussed later. Some review fixes never say so in the subject (5f3606b94 is one), so 29 is a floor. Many of those bodies also record how many unit tests (the CalyxTests suite) ran and how many failed at that point.
July 5: seven rounds
Calyx has persistent sessions: terminal sessions that survive quitting the app. A small Rust daemon owns each shell's PTY, and each pane runs a client that attaches to the daemon. I built it in phases. Phase 3, which integrated the daemon into the app, was merged into main at 00:51 on July 5.
That afternoon and evening, the persistent-session code went through review after review. These are the rounds whose numbers made it into commit subjects:
| Round | Commit | Time | CalyxTests | Diff (lines) |
|---|---|---|---|---|
| 4 | 77e50735d |
13:41 | 2019 run, 0 failed | +1419 / -101 |
| 5 | b7ad88913 |
15:51 | 2028 run, 0 failed | +1479 / -212 |
| 7 | d5ae8c9cd |
17:25 | 2032 run, 0 failed | +916 / -215 |
| 9 | a941b245d |
18:23 | 2036 run, 0 failed | +536 / -64 |
| 11 | f1868a8a5 |
19:08 | 2042 run, 0 failed | +906 / -192 |
| 13 | 3c32054da |
20:03 | 2047 run, 0 failed | +1252 / -144 |
| 15 | 1948d43a6 |
20:35 | 2048 run, 0 failed | +224 / -105 |
Only rounds 4, 5, 7, 9, 11, 13 and 15 (and round 18, early the next morning) appear in commits. The commits do not say what happened in the other rounds.
The findings were real: reentrancy in the quit-confirmation flow, events dropped while the confirmation modal was up, cancellation not reaching child processes. Each round added a few tests and nothing ever failed. The fixes did not shrink steadily (rounds 5 and 13 were each larger than the round before), but round 15 came in at +224/-105 lines.
Through all seven rounds, a persistent pane had never once started inside the app.
The symptom had been visible from the beginning: panes were created and destroyed at full speed, and the window flashed. At 21:45, c842b18f1 fixed that flashing as a reconnect bug. The reconnect attempt counter was reset to zero the moment a replacement pane was created, so the backoff never grew and the retries never stopped. After that fix the retries were bounded, but the panes still died instantly.
The cause turned up only when the app was actually run and the error printed in the pane was read. At 23:00, 0040b282e fixed one problem: the HOME=<root> word at the front of the pane command was being executed as a path relative to the pane's working directory. The fix kept exec as the first word and moved the HOME assignment into arguments to /usr/bin/env. It locked a "first word is exec" contract into the tests, and 2,057 tests passed. Nineteen minutes later, cd89aab20 recorded this from the running app:
bash: line 0: exec: exec: not found
On macOS, the ghostty in the submodule (v1.3.1) runs a shell-form command by wrapping it as /usr/bin/login -flp <user> /bin/bash --noprofile --norc -c "exec -l <command>" (execCommand in ghostty/src/termio/Exec.zig). Calyx's command already started with its own exec, so the result was a double exec, and bash went looking for a program literally named exec. The commit body says the Phase 3 form had never worked inside the app at all, and that this was the real root of the pane churn that had been there from the start.
The tests did execute the synthesized command and checked the environment the child process saw. They ran it through a plain /bin/sh -c, without ghostty's exec -l wrapper, and without that wrapper a leading exec of your own is perfectly fine. ghostty's source sits in the repository as a submodule, but the review was reading the worktree's diff. Today the tests run the command through /bin/sh -c "exec <command>", emulating ghostty's wrapping, and the whole story is kept in exactly one place, a test file header:
// This also RETIRES the entire ghostty-exec-wrapping saga the old
// HomeStampTests file fought through, in three steps:
// - A bare `HOME=<root>` first word resolves as a cwd-relative
// path, field-verified broken as `bash: .../HOME=/tmp/cxpane: No such
// file or directory`.
// - A redundant leading `exec` is PATH-searched as a literal
// nonexistent program, field-verified broken as `bash: line 0: exec:
// exec: not found`.
// - Routing through `/usr/bin/env` finally makes the `HOME=`
// word an actual env assignment rather than a literal argv word.
(CalyxTests/Sessions/SessionCommandSynthesizerRuntimeStateDirFlagsTests.swift)
Persistent sessions first shipped in v0.29.0 on July 7, which includes the fix. No user ever received the broken form.
Fixes that introduced defects
In several places, the commit that fixed a finding produced the next round's finding. The clearest case is a small helper that carries Swift task cancellation through to a child process.
Round 11 (f1868a8a5) added a lock-guarded bridge to SystemCommandRunner so that polling a hung daemon would stop piling up orphaned processes. Round 13 (3c32054da) adopted the same bridge for Git subprocesses and added a second bridge to propagate cancellation into time-bounded daemon queries. Round 15 (1948d43a6) fixed what those two rounds had introduced. The Git adoption was a copy, so two byte-identical classes lived in two files. The branch for "already cancelled before launch" called process.terminate() directly and never set the flag that prevents a double terminate. And the time-bound bridge could fire onTimeout twice if cancellation arrived before registration. The file that exists today explains why it is one file:
// Shared by SystemCommandRunner.runInternal and GitService.run --
// was two byte-identical `private final class CancellationBridge`
// copies, one per file, consolidated here so a future fix to the
// terminate-once discipline only has to land in one place.
(Calyx/Helpers/ProcessCancellationBridge.swift)
The LSP feature shows the same shape three times in a little over half an hour on June 22. At 09:40, b649a9336 fixed seven issues the reviewer found after fix batches 1 to 3. The first was a race between stop() and startLSP(); the fix made the teardown re-read the bridge after pending startup work drained. Twenty minutes later, 2eeb6d366 fixed the fact that this re-read could also tear down the new bridge installed by a subsequent start(). Twelve minutes after that, 7ec033794 fixed the test for that fix, which would have passed against the old code too: an await between stop() and the injection let the teardown run first and bail out harmlessly, so the test could not see the regression it was written for.
One introduced defect stayed hidden for three months. LSP session shutdown waits for pending persistence work, with a two-second cap. Item 6 of b649a9336 added that cap using two unstructured Tasks, a drain observer and a timer, with only the first to arrive waking the caller. Its comment explained the choice: Task.value does not honour cancellation, so a naive TaskGroup race-and-cancel would still wait for the drain.
Two days later, on June 24, cc80ebbd3, the response to an audit that surfaced about 70 issues, rewrote that code. To stop the losing timer from living for two seconds while holding self, it switched to a withTaskGroup race followed by cancelAll(). The same diff deleted the comment warning that this shape cannot enforce the bound, and replaced it with one explaining that shutdown latency stays bounded by whichever child wins. The cap was not restored until 7edef08f1 on September 26. The current comment reads:
// Bounded wait: cap the drain at 2 seconds. `Task.value` does not
// honour cancellation, so a structured race (`withTaskGroup`)
// cannot enforce the bound: the group implicitly awaits every
// child, including a cancelled `task.value` child whose persist
// worker is stuck in cancellation-blind work (e.g. decoding a
// huge file).
(Calyx/Features/LSP/LSPSession.swift, excerpt)
The regression test's comment records that with the withTaskGroup version, a 2,000,000-entry persistence file kept shutdown busy for about 21 seconds. The same comment suggests why it went unnoticed for three months: there is no test hook for the persistence worker's timing, so the regression test manufactures slowness by writing a real 1,000,000-entry JSON file. The June 24 diff contained the deleted warning, but no test could say which of the two comments was right.
On July 6, 87d77f130 swept a batch of fixes (H1 to H11) to the experimental daemon handoff feature and found four defects those fixes had introduced. In one, an adopted session was left Running permanently, impossible to kill or reattach.
All of these are concurrency or lifecycle code. Fixes there close a problem by adding a state or a path, and the added path lands in the next round's diff, which is where most of these were caught. The two-second cap was different. No test could create its condition, so which of the two explanations held was decided only by whoever wrote the comment.
What review caught
Review also caught defects that had slipped past the tests. Round 9 (a941b245d) found that the production snapshot builder never saved tab.sessionRefs. The completeness tests exercised a different snapshot path that production did not use, and according to the commit, persistent sessions therefore never survived a real save and relaunch. Round 11 (f1868a8a5) made the production builder delegate to the tested path, so there are no longer two builders to drift apart.
On August 7, b7a8aa9c1 fixed a close path that set the "termination confirmed" flag without checking whether the Quick Terminal was keeping the app alive. Every other close path did that check. Trusting the flag, each window believed the app was quitting: it detached persistent sessions instead of killing them, and the last window skipped its pre-teardown save.
The test in 7ec033794 that could not fail belongs in the same group. The production snapshot path versus the path the tests checked, one close path versus the other close paths, a test versus the regression it is meant to catch: each is a mismatch between two things inside the repository, and reading both side by side tends to find it.
An audit also overturned an earlier declaration that the work was done. c372964be on June 26 fixed 14 findings and one regression from a second-pass audit, and its body says an earlier "all bugs fixed" claim was premature. The first symptom it names is that Calyx launched from Finder could not start a pyright installed through NVM. PATH augmentation used a hardcoded list, and the code that spawned language servers ignored even that augmented environment. This one is also an audit catching a defect that depends on something outside the repository, the environment the app is launched in.
What was found outside review
Like the exec story, several defects surfaced only when the real thing was run.
On August 19, 067fa4b4f fixed Codex rows in the sidebar that stayed idle forever after Codex exited. Calyx never installed a SessionEnd hook for Codex, on a premise written in the config manager's own doc comment: "Codex has no SessionEnd hook". For Codex 0.148.0 that was false. The binary's hook event enum lists SessionEnd, and writing the hook into a scratch CODEX_HOME fires it. The same experiment disproved a second recorded premise, that Codex strips Calyx's environment variables before running hooks.
On August 21, 85d84a255 captured a real opencode run that delegated to a subagent and found that session.deleted never fires at all. The code that retired child sessions on session.deleted had never run in practice, and the sets the comments said were cleaned on that same signal were never cleaned either.
5f3606b94 on June 27 started with a pyright crash I hit in real use. Calyx advertised refreshSupport: true but never registered the matching handler, and according to the commit, pyright treats the resulting -32601 reply as a fatal protocol violation. A sweep for the same class of bug found 25 of them. After those fixes, a 148-cell check running 70 tools against pyright and rust-analyzer passed in full. LSP fixes still continued afterward: 15a4a03fa in August, bc177c7f9 and 7edef08f1 in September.
bcedbc4e3 on July 7 is the commit tagged v0.29.0. Eight production call sites referenced test seams that exist only in Debug builds, so the Release configuration did not compile. The commit notes that 80 hours of Debug-only verification never tripped it.
In each of these, the fact that decided correctness lived outside the diff: how another program actually behaves (ghostty, Codex, OpenCode, pyright), or the build configuration. In the Codex case the wrong premise was written into a code comment; in the OpenCode case a comment described cleanup that never happened; and 0040b282e locked a wrong premise into the tests as a contract. A premise written into comments or tests is likely to become the yardstick a diff review checks consistency against, rather than something it checks. Checking the premise itself took running the other program.
What a shrinking review diff measures
July 5 is one feature on one day, so it is worth checking whether the same shape shows up elsewhere. The MCP Apps host landed at 14:48 on September 24 in bbff0a049: 249 files, 49,027 lines added. The commits for review passes 2 through 4 followed the same day, each smaller than the last:
| Pass | Commit | Time | Files changed |
|---|---|---|---|
| 2 | de6df4d14 |
16:13 | 36 |
| 3 | 27a1dd857 |
19:36 | 13 |
| 4 | 9e224b836 |
22:18 | 6 |
Pass 4 was edge conditions: a negative pane width should yield a zero dock width, and the dock divider's hit area must not overlap a split divider's. In a little over two hours after it, three more fixes landed. Two of them matter here. At 00:05, c098c4f0d fixed the open-link consent prompt showing only the tail of a long URL: the text view measured itself before it had a width and stayed scrolled to the bottom. At 00:40, cac63bc88 worked around Swift 6.2.3 aborting the Release build (-O, whole-module) with a SIL ownership error inside the compiler.
Both show up as soon as you put a long URL on screen or run a Release build, and neither came out of four review passes. A shrinking review diff tells you the reviewer is finding less in the diff. It does not tell you the feature is done.
Traces the workflow left behind
Fixing every finding also left marks in the code itself. On August 20, 1c783299d removed 496 labels spread across 86 files. They named review rounds, task IDs and internal spec documents that are not in the repository, so the next reader had no way to follow them. Sentences that existed only to carry a label were rewritten to state, in the present tense, the invariant the code or test actually holds. The commit verifies the cleanup touched nothing else: with string literals masked and comment lines stripped, every file is byte-identical to before.
For a subagent writing a fix, the natural explanation is "which finding this fixes". That context exists only in the review conversation; the next person reading the code does not have it. My reading is that 496 labels piled up because that mismatch played out on every single fix.
Workflow state also ended up in commit messages. The body of a3f729d57 on July 5 says: "NOT yet code-reviewed; review before merging to main". And the round 5 and round 7 commits each include removing em dashes from lines the previous round had added. In rounds 5 and 7, the fixes from the previous round had added new violations for the next round to clean up.
What this record supports, and what it does not
First, what it cannot tell you. With no control, I cannot say how many defects review prevented overall, or whether it was worth the time. I cannot say whether human review would have done differently. I cannot say how faithfully the tests-fail-first step was followed: of 752 non-merge commits, only seven mention the RED step in their subject or body. And because the rules file has no history, I cannot say when each rule took effect.
Within the record, three things hold:
- Neither an all-green unit test suite nor review fixes that end up small showed that the feature worked. The same shape appears for persistent sessions on July 5 and for MCP Apps on September 24.
- Fixes to concurrency and lifecycle code introduced new defects. Most were caught in the next round. The one whose condition no test could create stayed for three months.
- In the examples above, mismatches between two things inside the repository were caught by review. Apart from the Finder PATH defect the audit picked up, defects whose deciding fact lived outside the repository were caught by running the real thing, not by another review round.
Practices the record suggests
If I set up the same workflow again, the record points to these:
- When a change integrates with an external program, run it in the app once before stacking review rounds. The July 5 defect would very likely have shown up in the first error message a pane printed.
- When a comment states a premise about an external program, record how and against which version it was checked.
067fa4b4fchecked Codex 0.148.0's binary and a scratch config. An unchecked premise is an open question, not a comment. - Run tests through the wrapping the external program actually applies.
- Build Release early, with the exact invocation the release script uses.
- Do not treat a claim as established until a test can create its condition. Time bounds are the typical case.
- Record "all findings fixed" as the state of the review, and do not read it as the state of the feature.
- Keep comments to invariants the next reader can follow. Round numbers and finding IDs belong in the commit or the review record.
The end-to-end tests had their own share of places that passed while checking nothing; that is a separate post.
About Calyx
Calyx is a native macOS terminal (MIT) for running and supervising coding agents in parallel: Claude Code, Codex, OpenCode, Hermes, Grok and pi. It has an approval inbox that collects permission prompts in one place, agent status driven by each CLI's own hooks, the persistent sessions described above, command history that agents can read, and in-terminal diff review with line comments sent back to the agent. It requires macOS 26 or later on Apple Silicon.
brew install --cask calyx
Source: https://github.com/yuuichieguchi/Calyx
Docs: https://help.getcalyx.app
Every commit cited here is in the public repository, so you can read the messages and diffs yourself.
來源:DEV Community · dev.to