[BUG]

Status Open
Reported on v2.1.218
Maintainer reply None cached
Activity 0 comments · opened Jul 25, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Harness escalation — Workflow agent isolation, panel aggregation, quota degradation, and concurrency

Reported: 2026-07-20 · updated 2026-07-25
Context: A multi-agent code-review workflow used as a merge gate on a private TypeScript monorepo
Severity: High — three of the four findings produce a fabricated verdict while reporting success

---

The common class

All four findings share one shape: a harness default that is wrong for how the tool is actually
used, and that fails quietly rather than loudly.

  • Findings 1–3 each cause a workflow to return a result-shaped artifact that a caller cannot

distinguish from a genuine clean review. In a code-review/merge-gate context, that is not a
degraded result — it is a fabricated one.

  • Finding 4 causes a ~4× slowdown by deriving agent concurrency from a resource the agents do not

consume.

Findings 1–3 were each caught only because a lane refused to read a convenient result as a pass.
That should not be the mechanism that protects a caller.

Note on local mitigation

We built an execution-ledger protocol that parses launched-vs-returned from panel output and
refuses to derive a "clean" report when the two disagree. It works, and it is why findings 1–3
were caught rather than acted on.

But it is a detector wrapped around the harness, not a fix inside it. It cannot restore the
isolation guarantee in finding 1 — it can see that agents died, not that the survivors were
reviewing each other's in-flight mutations. It cannot prevent quota being spent mid-run in
finding 3. It does nothing at all for finding 4. It protects one project while the documentation
continues to recommend the pattern in finding 2 to everyone else. And it carries its own cost:
the detector needed its own hardening round after review found a laundering path inside it, and
it currently over-blocks in the opposite direction — a findings-bearing commit is permanently
blocked with no accept-in-place verb, so two copy-only nits forced an extra review round on a
merge that was already correctly authorised.

We needed this badly enough to build the workaround. The above is what the workaround cannot
cover.

---

1. isolation: 'worktree' degrades silently into a verdict-producing run

What happened

A code-review workflow declared isolation: 'worktree' on the agents permitted to apply
mutants — the standard remedy so that one reviewer cannot observe another's in-flight mutation
and report it as a finding about the commit.

Eleven of fifteen agents failed to start, each with:

Cannot create agent worktree: not in a git repository and no WorktreeCreate hooks are
configured. Configure WorktreeCreate/WorktreeRemove hooks in settings.json to use worktree
isolation with other VCS systems.

The workflow's cwd was /home/<user> (the session's primary working directory), which is not a
git repository; the repository under review was /home/<user>/streams/<worktree>. Nothing in the
launch path signalled that the declared isolation could not be honoured.

The workflow then ran to completion and returned an APPROVE verdict. The dead agents were
the @tester lens, the @ui lens, and the entire adversarial-verification phase — 9 of 9
verifiers. Four agents produced the verdict that was presented as the output of fifteen.

Why this is worse than not offering the capability

A capability that is absent forces the caller to solve the problem another way. A capability
that is accepted and then silently unavailable lets the caller believe the problem is
solved. Here the failure mode is not a crashed run — it is a successful-looking run whose
conclusion is fabricated
. The task notification reported status: completed with a verdict
payload; the agent errors appeared only in a separate <failures> block that a caller acting on
the result may never correlate.

Requested fix (either is acceptable)

  • Make it work regardless of cwd — resolve the repository from the agent's working directory

or an explicit parameter, rather than the workflow's cwd; or

  • Fail the whole workflow at launch. Validate that isolation can be honoured before any

agent starts, and refuse the run with a clear error.

What must not happen is the current behaviour: per-agent failures accumulating mid-run while
the workflow proceeds to synthesise and return a result.

Suggested additional guard

If a declared capability cannot be honoured for any agent, the workflow's return value should
carry an explicit degraded: true / agentsFailed: N field that a caller cannot miss — not
only a side-channel failures list.

---

2. The panel aggregation contract belongs in the harness primitive

The bug every lane will independently write

The documented fan-out → vote → synthesise pattern encourages:

const votes = (await parallel(verifiers)).filter(Boolean)
const upheld = votes.filter(v => !v.refuted).length
const survives = upheld >= 2          // <-- the defect

When every verifier dies, votes is empty, upheld is 0, 0 >= 2 is false, and the finding
is classified refuted. A finding that nobody examined is reported to the synthesiser under
"refuted — do not re-raise". Symmetrically, reviews.filter(Boolean) makes a lens that died
byte-identical to a lens that found nothing clean.

Both idioms come straight from the tool documentation's own examples (.filter(Boolean) before
using results). They are correct for tolerating a failure and wrong for reporting one.

Measured impact

Same commit, same script logic, only the aggregation changed:

| aggregation | agents run | verdict |
| --- | --- | --- |
| filter(Boolean) + upheld >= 2 | 4 of 15 (11 died) | APPROVE |
| unverified-is-not-refuted + dead-lens accounting | 27 of 27 | REQUEST_CHANGES, incl. a MAJOR |

The MAJOR the first run erased: an empty-state body on a regulated compliance register could be
swapped to claim "nothing has been raised on this tenant, in any status" while the tenant held
resolved corrective-action history — with the full test suite green.

Requested contract (in the harness's panel/vote primitive)

  1. Unverified ≠ refuted. Below-quorum returns ⇒ the finding survives, tagged

unverified.

  1. Launched vs returned is always reported. The primitive surfaces the count of agents that

did not return, and the synthesis step receives it.

  1. A silent agent is distinguishable from a clean agent by construction — the caller should

not have to remember to check.

  1. Prefer an API where results arrive as {status: 'ok'|'failed', value} rather than

value | null, so filter(Boolean) is not the ergonomic choice.

Documentation change worth making regardless

The Workflow tool description currently advises: *"A thunk that throws … resolves to null
so .filter(Boolean) before using the results."* For aggregation that produces a judgement,
that advice is actively harmful. Recommend adding: *absence is a distinct state from a negative
result; never let a filtered-out failure reach a threshold comparison.*

---

3. A usage limit degrades a live panel into a verdict-shaped incomplete

What happened

Observed twice, in two separate lanes.

Instance A (2026-07-21). A merge-gate panel returned 0 confirmed — no findings, no
errors surfaced at the call site. The run had been killed by a weekly usage limit partway
through; the agents never completed. The payload was shape-identical to a genuine clean panel.
The caller correctly refused to read it as a pass — but nothing in the return value distinguished
"reviewed and found nothing" from "never reviewed anything."

Instance B (2026-07-22, a separate workflow). A live panel hit a usage limit mid-run and
returned a verdict-shaped incomplete. Same shape, same ambiguity.

Why it matters

This is finding 1's failure mode arriving by a different route. A quota boundary is an
expected operating condition, not an exceptional one — panels are a capped resource and lanes
will routinely approach the cap. So the degradation path here is not a rare edge case; it is a
path the tool will take regularly, and it currently terminates in a payload that reads as a
clean review.

A caller acting on 0 findings — merging a PR, closing a gate — has no signal that the review
did not occur.

Requested fix

  • A quota-terminated run must not return a verdict payload. Return an explicit terminal

state (incomplete, quota_exhausted) that is not shaped like a result.

  • If a partial payload is returned at all, it must carry launched vs returned counts (the

same field requested in finding 2), so that returned < launched is visible without the
caller having to remember to check.

  • Ideally, fail before spending: if the remaining quota cannot cover the declared fan-out,

refuse at launch rather than exhausting mid-run.

---

4. Agent-execution concurrency is derived from CPU core count, throttling inference-bound agents

What was measured

Agent execution funnels through a single semaphore whose width is computed once at startup from
the host's core count:

function dBy(e){ return Math.min(16, Math.max(2, e - 2)) }   // clamp(cores - 2, 2, 16)
pBy = dBy(os.cpus().length)                                   // module-level, once at startup
B   = oB(pBy, K)                                              // the ONE agent-execution pool

On a 4-core host this yields a cap of 2. Every agent() call — whether inside parallel(),
pipeline(), or sequential — passes through B.

Measured across 29 real panels: max observed concurrency was 2, every time, while panels
were 6–87 agents wide. A 51-agent merge gate therefore runs as ~25 serial waves.

The agents are inference-bound, not CPU-bound. Actual local command execution (Bash) averaged
0.8–4.7s per call and accounted for only 9–30% of agent time; the remainder is model
inference, during which the host CPU is idle. Host state during panels: ~96% idle CPU, no
memory pressure, no OOM events. The cap is protecting a resource the agents do not consume.

Impact

| panel | agents | wall clock at width 2 | projected at width 8 |
| --- | --- | --- | --- |
| 6-lens verify | 6 | 574s | — |
| 45-agent gate | 45 | 2,937s | — |
| 51-agent merge gate | 51 | 4,769s (~80 min) | ~1,100s (~18 min) |

Launch overhead is seconds; the synthesis tail is 1–3% of wall clock. The agent phase is ~97% of
the run, and it is throttled by the cap alone.

There is no override

pBy is a compiled module constant. There is no environment variable, no per-workflow setting,
and no per-agent() argument that affects it. Userland can only go narrower than 2, never
wider. (CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY governs in-turn tool use, not agent(). The
second pool, oB(50, …), is the safety-classifier limiter, not an execution lane.)

Related: no cross-session exclusivity primitive

pBy/B are per-process. Nothing spans Claude sessions, so two sessions on one host can each
run panels concurrently with no coordination — observed 47 temporal overlaps where 2–3 panels
ran at once. This is currently benign because the agents are inference-bound, but it means a
heavy agent (full build, e2e, a mutation gate) has nothing preventing it from colliding with
another session's heavy agent. The only available mitigation today is an OS-level flock inside
the agent's own Bash command.

Requested fix

Decouple agent-execution concurrency from core count. Either:

  • An override on the cap (env var or launch flag) — the minimal change; or
  • Better: a per-agent() lane tag routed to a separate semaphore, so that inference-bound

reviewers can run wide (~8) while heavy gate/e2e/mutation agents remain serialized (≤2, or 1).

The second form is preferred because the current cap conflates two genuinely different agent
kinds. The serialization requirement is real for heavy agents — it should be kept, and expressed
directly, rather than approximated by a core-count formula that also throttles agents which use
no CPU.

A cross-session lane primitive (or documented guidance that flock is the intended mechanism)
would close the related gap above.

---

Reproduction

Finding 1. Workflow cwd: any non-git directory (e.g. a session whose primary working dir is
not the repo). Any agent with isolation: 'worktree'. Observe: agent fails, workflow continues,
verdict returned, status: completed.

Finding 2. Any fan-out → vote aggregation using .filter(Boolean) per the documented
example, with a below-quorum threshold comparison downstream.

Finding 3. Launch a panel whose fan-out exceeds remaining quota. Observe: run terminates
mid-flight, payload returns shaped as a completed verdict with zero findings.

Finding 4. Any host with ≤4 cores. Launch a panel of >2 agents. Observe max concurrency 2
regardless of declared fan-out, with host CPU largely idle throughout.

What Should Happen?

A workflow should never return a result-shaped payload when the agents that
would have produced that result did not run.

Specifically:

  1. Worktree isolation — if isolation: 'worktree' cannot be honoured, either

resolve the repository from the agent's working directory rather than the
workflow's cwd, or fail the whole workflow at launch. It should not proceed
with the surviving agents and synthesise a verdict from them.

  1. Aggregation — below-quorum returns should leave a finding surviving and

tagged unverified, never classified as refuted. Launched-vs-returned counts
should always be present in the return value, so a silent agent is
distinguishable from a clean one by construction rather than by the caller
remembering to check.

  1. Quota termination — a run killed by a usage limit should return an explicit

terminal state (incomplete / quota_exhausted), not a verdict payload with
zero findings. Ideally, refuse at launch if remaining quota cannot cover the
declared fan-out.

  1. Concurrency — agent-execution concurrency should be settable independently of

host core count, either via an override or (preferably) a per-agent lane tag
routed to a separate semaphore, so inference-bound reviewers can run wide
while heavy gate/e2e agents stay serialized.

In all four cases the current behaviour fails quietly. Failures should be loud
and present in the return value the caller acts on.

Error Messages/Logs

Steps to Reproduce

  1. Worktree isolation silently no-ops

Launch a workflow whose cwd is a non-git directory (e.g. the session's home
directory) while the repository under review is a subdirectory or worktree.
Declare isolation: 'worktree' on the agents.
Observe: agents fail to start with "Cannot create agent worktree: not in a git
repository", the workflow continues with the survivors, and returns a verdict
with status: completed. The failures appear only in a separate block, not in
the verdict payload.

  1. Aggregation treats unexamined as refuted

Build any fan-out -> vote -> synthesise step using the documented
.filter(Boolean) example on agent results, with a quorum threshold
downstream (e.g. upheld >= 2).
Observe: when all verifiers die, the array is empty, the threshold comparison
is false, and the finding is reported as refuted rather than unexamined. A
dead lens is byte-identical to a lens that returned clean.

  1. Quota termination returns a verdict shape

Launch a panel whose fan-out exceeds the remaining usage quota.
Observe: the run terminates mid-flight and returns a payload shaped like a
completed verdict with zero findings — indistinguishable from a genuine clean
review.

  1. Concurrency capped by core count

On a host with 4 cores, launch a workflow with more than 2 agents, whether via
parallel(), pipeline(), or sequential calls.
Observe: maximum concurrency is 2 regardless of declared fan-out, while host
CPU sits largely idle throughout (agents are inference-bound; local command
execution is roughly 9-30% of agent time). No env var or per-agent setting
raises it; userland can only go narrower.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.1.218

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

VS Code integrated terminal

Additional Information

_No response_

View original on GitHub ↗