The slowest part of an AI-assisted verification tool is usually the model you bring to it. Shipmoor does not host that model, choose its provider, or control how long it takes to answer. But Shipmoor does decide whether independent calls wait in a line, whether the same frozen question gets asked again, and whether you can see which phase is actually taking the time.
Shipmoor 0.9.0 is a latency and observability release for Code Review and Claim Check. It removes avoidable serial work, runs independent model calls concurrently under explicit bounds, reuses decompositions whose inputs cannot change, and exposes the remaining critical path. It does that without turning completion order into product behavior and without loosening the gate.
This post covers the performance program inside 0.9.0. It is not the complete 0.9.0 release note.
First: see where the time is going
Code Review now has phase-level timing:
shipmoor review . --agent cursor --verbose
The timing lines go to stderr and name four local phases:
plan: the optional planning pass for a file;main: the file’s complete review loop;review_filter: the final per-path pass that removes comments the agent no longer stands behind;relocation: a model-assisted attempt to place a comment whose code did not resolve directly.
At the end, Review prints a count, total, and mean for every phase it observed. JSON, SARIF, findings, and exit behavior do not change. You can turn the timing on when a run feels slow without creating a different review artifact.
Claim Check gets the same treatment at the gate level:
shipmoor claim-check . \
--agent 'cursor agent -p --model gpt-5.4-mini-low --mode ask --trust' \
--verbose \
--trace-out .shipmoor/claim-check.trace.json
--verbose shows derive progress, bind progress when an acceptance set exists, and full-mode producer and judge progress on stderr. The trace adds run-relative timing for phases, producers, model calls, and decomposition-cache hits or misses. It records prompt sizes, never prompt contents. Timing is observational and stays outside the verdict, the VSA’s canonical bytes, and its self-digest.
That distinction matters. A performance tool should explain a slow gate; it should not create a different gate.
Review no longer serializes independent filtering
After the per-file review loops finish, Shipmoor asks the agent to filter its comments once more. Before 0.9.0, seven target paths meant seven independent filter calls in a row. They had no dependency on one another, but they still queued.
Those path filters now run concurrently under the same concurrency limit you already set for Review. Each worker calculates what it would remove without mutating the shared comment store. Shipmoor waits for the complete bounded batch and publishes removals in deterministic path order.
The failure rule stays conservative: if one path’s agent call fails, its comments stay. If the executor itself fails, Shipmoor drains the submitted work and publishes no partial set of removals.
In the controlled seven-path test, with a fixed 120ms delay and concurrency three, the filter span moved from a median 0.895s to 0.387s. That is a test of queueing and overlap, not a promise that every review is 56.7% faster.
Relocation moves off the file’s MAIN loop
A review comment has to point at real code. Most comments resolve deterministically from the existing_code the agent returned. When that snippet has drifted, Shipmoor can ask the agent to relocate it.
That relocation used to happen inline. The file’s MAIN loop could not process its next tool call or reach task_done until the relocation call returned.
0.9.0 wires the existing comment worker pool into every real diff and whole-file review. The file loop can continue while relocation runs. On a normally completed run, Shipmoor drains the pool before final filtering or dedup reads the comments, so downstream processing sees the completed relocation set.
Concurrency introduced a second problem: a fast second relocation could publish before a slow first one, changing finding order from run to run. The final implementation separates execution order from publication order. Relocations execute concurrently, then each file publishes completed batches in original tool-call order. Publication happens after releasing the bounded worker slot, so a concurrency-one schedule cannot deadlock while one batch waits for another.
One existing timeout boundary remains. Python cannot force-kill an inner MAIN thread. If a per-file hard deadline abandons that daemon, the outer review returns on time but the daemon can still publish comments, timing, or accounting later. Review uses a serial filter fallback when it detects that state; the normal drain guarantee does not claim to kill an abandoned thread.
The controlled unequal-delay test saw two relocations overlap, let the next MAIN call start before either was done, and reduced the measured critical-path median from 0.174s to 0.135s. The public JSON digest stayed byte-identical. Relocation itself did not get faster; it stopped blocking unrelated work in the same turn.
Planning is now an explicit choice
The PLAN pass helps the reviewer build focus before MAIN. It also costs a model call for every file over the planning threshold. 0.9.0 makes that tradeoff explicit:
shipmoor review . --agent cursor --no-plan
Or in .shipmoor.yaml:
review:
skip_plan: true
The default remains false. MAIN always runs. The option applies to diff review and whole-file review.
This is not a free speed switch. Removing the plan may reduce review focus, and Shipmoor does not claim findings parity between planned and unplanned runs. Use it when you deliberately prefer one fewer call per qualifying file, for example during a fast exploratory pass.
In the fixed-delay 60-line test, the option removed exactly one PLAN call and moved the median from 0.309s to 0.156s. That result proves the call was removed. It does not prove that every model or every repository will halve its review time.
Claim Check’s judge now works vertically
Claim Check can have several judgment obligations, and each obligation can decompose into several atomic questions, each sampled multiple times. Those dimensions are independent enough to overlap, but they cannot be unbounded.
0.9.0 runs independent judgment items and sibling atomic checks concurrently under one shared judge semaphore. Every live decomposition, sample, and retry consumes that same budget. Results still publish in the frozen acceptance/decomposition order, never the order in which a provider happened to finish.
The default internal budget is bounded between two and eight based on available CPU. It is not a new tuning flag. K remains three, the agreement floor remains two, and pointer confirmation stays serial and non-speculative.
If the judge needs a different timeout from standalone Review, set one without changing Review:
review:
timeout: 120
claim_check:
judge_timeout: 240
When claim_check.judge_timeout is absent, it inherits review.timeout as before.
In the controlled four-item test, a fixed 20ms transport moved from 0.781s serial to 0.201s at a budget of four, with measured peak concurrency exactly four. That is evidence that the model-call waves overlap under the cap, not a claim that every full Claim Check is 3.88 times faster.
Ask the binder once, not once per residue item
Before evidence producers start, Claim Check binds deterministic obligations to checks. Author hints win first, then the tag join, then deterministic heuristics. An agent gets only the unresolved residue.
That last rung used to make one blocking model call per item. Four unresolved obligations meant four calls before any producer could start, even when all four answers were declines.
0.9.0 sends the residue in frozen-order chunks of at most 20. Four items now need one request. Forty items need two. Every returned binding still goes through the same per-item validation, and a malformed or missing item response declines that item rather than inventing a binding.
With four fixed-delay items, the controlled median moved from 0.160s across four calls to 0.040s in one batch, with equivalent binding output.
Frozen decompositions are reused, but judgments stay live
The text of an approved acceptance obligation cannot change between Claim Check rounds without changing its identity. The decomposition prompt and calibration also have explicit versions. Asking the agent to decompose the same frozen item again buys model variance and latency, not fresh evidence.
0.9.0 stores one decomposition memo per canonical key under:
.shipmoor/judge-decompositions/
A warm hit skips decomposition. It does not skip the judgment:
- all K audit samples still run live;
- retries still run;
- agreement is still computed;
- pointer confirmation still runs;
- a
BLOCKEDverdict still needs a resolving witness.
The cache is not trusted evidence. Unreadable JSON, a key or schema mismatch, malformed checks, or duplicate IDs are all misses. Deleting the directory simply makes the next run decompose again.
The controlled cache test removes one call wave: cold makes one decomposition plus three audit calls; warm makes the three live audit calls and no decomposition. Verdict payloads remain byte-identical under the fixed transport.
The adapter does less work before your agent starts
Each built-in Review turn starts a small Shipmoor adapter that translates between the review protocol and the coding-agent CLI you already use. That hidden command used to import modules owned by scan, licensing, output rendering, skills, and upgrade paths before it could dispatch the turn.
Those imports are now deferred to the commands that own them. In seven fresh Python 3.14 processes, cumulative shipmoor.cli import time moved from a median 89,404us to 75,473us, a 15.58% reduction in Shipmoor-controlled source startup.
This is the narrowest number in the release. It does not say Cursor, Claude, or Codex starts 15.58% faster, and it says nothing about provider latency or model reasoning. It removes work Shipmoor owned before your agent could begin.
The controlled results, side by side
The model-shaped benchmarks isolate one scheduling or call-count boundary with a fixed-delay or fixed-response transport. Adapter startup is measured separately across seven fresh Python processes. In both cases, the before/after ratios show that the intended mechanism moved; they are not interchangeable estimates of full-product latency.
| Controlled mechanism | Before median | After median | Observed delta |
|---|---|---|---|
| Claim Check judge vertical | 0.7805s | 0.2014s | 74.2% lower elapsed, a 3.88x before/after ratio |
| Binder batching | 0.1604s | 0.0401s | 75.0% lower elapsed, a 4.00x before/after ratio |
| Review adapter startup | 89,404us | 75,473us | 15.58% lower Shipmoor-controlled import time |
| Review filtering | 0.8947s | 0.3870s | 56.7% lower elapsed, a 2.31x before/after ratio |
Opt-in --no-plan | 0.3087s | 0.1557s | 49.6% lower elapsed, a 1.98x before/after ratio |
| Comment relocation | 0.1743s | 0.1354s | 22.3% lower elapsed, a 1.29x before/after ratio |
The decomposition cache has a call-count result rather than a comparable elapsed-time ratio: the controlled cold path made one decomposition call plus three live audit calls; the warm path made the same three live audit calls and no decomposition call.
Do not add or multiply these percentages into a product-wide speedup. The mechanisms run on different fixtures and affect different portions of the critical path.
The release panel
We ran the full public Review producer into the public Claim Check assembly rather than timing either engine in isolation.
The fixed setup was:
cursor agent -p --model gpt-5.4-mini-low --mode ask --trust;- Apple M3 Pro, arm64, 36 GiB;
- Python 3.14.2;
- five synthetic repositories per panel, including a four-item judge residue.
Across two panels:
- 20 of 20 full-mode rounds completed;
- every Review producer completed and carried an evidence reference;
- all ten case runs reached
READYin two rounds; - both deliberately unfaithful-check negative controls were caught;
- strict nearest-rank combined p95 was
84.83s, below the300srelease threshold.
The release evidence reports no full-assembly before/after baseline, so 84.83s is an absolute release result, not the basis for an overall percentage-improvement claim.
A supplemental unchanged-contract run recorded four decomposition misses on the cold round and four hits on the warm round while keeping live audit calls in both. Each round replayed its own collected evidence byte-identically; the live cold and warm verdicts differed, so this is not a cold-versus-warm verdict-parity claim.
Earlier attempts through Claude coincided with active provider incidents and are retained as failed, nonqualifying attempts. We switched the fixed release panel to an exact Cursor command and model rather than mixing providers into one distribution.
The panel is evidence for that command, model, host, and workload. It is not a latency guarantee for your repository or provider.
What did not change
Performance work is easy to get wrong when it changes the meaning of completion. These boundaries remain:
shipmoor reviewis still advisory and never blocks on findings;- PLAN is still the default;
- filter and relocation workers drain before normal output on non-abandoned runs; a timed-out inner MAIN daemon can still publish late state;
- finding and verdict order is deterministic rather than completion-ordered;
- timing never enters Review JSON/SARIF or Claim Check VSA bytes;
- a failed Review path keeps evidence rather than deleting comments;
- an invalid decomposition cache entry is a miss, never a pass;
- cache warmth never skips live votes or confirmation;
- floor-only Claim Check still makes zero model calls;
- Shipmoor still uses the agent and provider relationship you bring.
One resource caveat is worth calling out. Review file dispatch and comment relocation use separate pools, each sized from review concurrency, and they may overlap. Peak external-agent subprocesses can approach twice the configured review concurrency. If your provider or workstation needs a lower ceiling, lower review.concurrency and use the new timing to see the result.
Try it
# Observe the normal, plan-guided review.
shipmoor review . --agent cursor --verbose
# Deliberately trade PLAN guidance for one fewer call per qualifying file.
shipmoor review . --agent cursor --no-plan --verbose
# Observe the complete intent gate and retain timing.
shipmoor claim-check . \
--agent 'cursor agent -p --model gpt-5.4-mini-low --mode ask --trust' \
--verbose \
--trace-out .shipmoor/claim-check.trace.json
-
0.9.0 does not make your model intrinsically faster. It makes Shipmoor stop asking independent work to wait, stop asking the same frozen decomposition twice, and show you the work that remains.
-
From the
shipmoor.devEngineering Team