67 Commits

Author SHA1 Message Date
Julius Brussee 3e62063e40 fix(codex): compress array-shaped tool output, record route eligibility, stop config self-corruption and hook stacking
Live-tested Codex 0.149 through the proxy and found four defects:

- proxy: custom_tool_call_output.output is an ARRAY of input_text parts on
  Codex 0.149+. The Responses extractor accepted only the string form, so
  every real Codex tool turn had an empty live zone and the subscription
  route compressed nothing (75-116 KB requests, zero savings). The captured
  real request now compresses; regression test pins the array shape.
- proxy: the /chatgpt route never recorded compression eligibility, so the
  CLI told every Codex subscription user "your agent never reached the
  compression layer" even when routing worked.
- cli: codexNativeConfig ran the legacy table strippers before removing its
  own marker blocks; the stripper swallowed the tables end marker, so the
  block enable had just written failed its own re-parse as "corrupted" on
  the next wrap and doctor reported drift forever. Markers come off first.
- cli: nativeHooksDocument matched caveman hook entries by exact command
  string, so a binary path change (upgrade, moved install) appended a new
  hook set per event instead of replacing it. Match on managed identity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2f7Lf7G9erA7YNn5SXPEj
2026-09-04 01:57:14 +02:00
Julius Brussee 9911e5fd0d test(cli): kilo install assertion derives from the profile pin
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2f7Lf7G9erA7YNn5SXPEj
2026-09-03 20:54:29 +02:00
Julius Brussee 3b74643f4d fix(cli): wait for a freshly started proxy's run state before judging its owner
A proxy caveman just started binds its port before its run state is
readable, so the immediate probe could report a foreign owner on slower
runners. Poll briefly after start and restart. Test fixes for CI: pin
XDG_CONFIG_HOME for in-process Kilo assertions, import the profile scope
checker through a file URL so Windows can load it, and skip the sh-stub
gh reporter tests on Windows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 15:48:49 +02:00
Julius Brussee 14b1c15f2a fix(cli): fail closed on unverifiable proxy owners, strip attributed routes on direct launch
Review follow-ups to #945: a listener caveman did not start this run and
cannot prove it owns (foreign process, or a proxy binary too old to
answer) now launches direct, and the direct-launch strip removes the
agent-attributed /w/<agent> route as well as the bare gateway URL, so a
leaked outer route never reaches the child. Qwen's ACP and extension
listing guards accept yargs camelCase spellings. Proxy-aware fetch no
longer sets an IP literal as TLS servername (rejected on Node 26). The
conformance-pin test fixture follows the pinned-upstream-binary job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 15:39:01 +02:00
Julius Brussee d6c1cd39af fix(cli): launch direct when a foreign process owns the proxy port
A foreign listener on 127.0.0.1:8787 used to get the banner but still
receive the routed launch, which would send the operator's provider
keys to a process caveman does not own (#945). Treat it like a gate
mismatch and fall back to a direct launch.

Also: Claude Code refuses Remote Control behind any non-first-party
ANTHROPIC_BASE_URL, so `caveman claude remote-control` launches direct
and the limitation is documented (#947); caveman-compress reads the
first text block instead of content[0] so a leading tool_use block no
longer crashes after a paid call (closes #932's report); hook checksum
manifest regenerated after #794.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 15:25:28 +02:00
Julius Brussee c24123dd2b Merge remote-tracking branch 'origin/main'
# Conflicts:
#	README.md
#	docs/technical/agent-profile-registry.md
#	packages/cli/src/index.ts
#	packages/cli/tests/subscription-recovery.runtime.mjs
#	proxy/internal/gateway/chatgpt.go
#	proxy/internal/gateway/proxy.go
#	proxy/internal/gateway/retrieve_tool.go
#	proxy/internal/gateway/server.go
#	proxy/internal/gateway/subscription_live_zone_test.go
2026-09-02 15:19:57 +02:00
Julius Brussee 7684fb1cef fix(cli): name per-session recovery and lock Qwen/Kilo control surfaces
Bare start never stamps global recovery; the banner now says which
sessions compress (those whose agent carries caveman_retrieve) and the
status mcp-missing row keys off any installed MCP marker.

Qwen: refuse extension, ACP, session-restore, control-command, and
enforced-auth surfaces that could reactivate a provider route outside
the locked session; replace modelProviders/providerProtocol wholesale
in the routed profile; keep web search and workflows off via env.
Kilo: daemon and console launch direct since they outlive the wrap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 15:18:19 +02:00
Julius Brussee be2f788161 fix(cli): guard Kilo managed routing 2026-08-30 06:07:35 +02:00
Julius Brussee 9978a02b7e fix(cli): parse Kilo boolean arity 2026-08-30 06:01:04 +02:00
Julius Brussee df6251dbe2 fix(cli): honor Kilo config directory 2026-08-30 05:58:49 +02:00
Julius Brussee bc71040245 fix(cli): parse Kilo command arity 2026-08-30 05:56:28 +02:00
Julius Brussee 9377d1f062 fix(cli): follow Kilo config paths 2026-08-30 05:53:13 +02:00
Julius Brussee ca51a04ed2 fix(cli): guard Kilo route overrides 2026-08-30 05:46:42 +02:00
Julius Brussee 8e3f72a4a9 feat(registry): allow optional Kilo upstream keys 2026-08-30 05:35:07 +02:00
Julius Brussee facc597029 fix(cli): guard Qwen route overrides 2026-08-30 05:29:20 +02:00
Julius Brussee 15d08edd83 fix(ci): close profile validation gaps 2026-08-30 05:13:07 +02:00
Julius Brussee fb58e4acbb fix(cli): honor MCP surface opt-outs 2026-08-30 05:08:27 +02:00
Julius Brussee bedde358a5 fix(cli): transact agent MCP ownership 2026-08-30 05:04:13 +02:00
Julius Brussee 55992b71c5 fix(cli): detect Qwen late relaunch suppressors 2026-08-30 03:48:10 +02:00
Julius Brussee 86fb3a6506 fix(cli): guard Qwen late credential resolution 2026-08-30 03:45:01 +02:00
Julius Brussee 35ad321ccd fix(cli): match Qwen JSON and merge safety 2026-08-30 03:41:39 +02:00
Julius Brussee 30bf2e5182 fix(cli): mirror Qwen settings loader 2026-08-30 03:30:59 +02:00
Julius Brussee 4bc4cec8f5 fix(cli): mirror Qwen credential policy 2026-08-30 03:22:18 +02:00
Julius Brussee 6aac0628d6 fix(cli): respect Qwen workspace trust 2026-08-30 03:04:56 +02:00
Julius Brussee 20df023da3 fix(cli): honor Qwen effective upstream key 2026-08-30 02:57:05 +02:00
Julius Brussee 9cd2703f02 test(cli): cover Qwen stored-key fallback 2026-08-30 02:51:53 +02:00
Julius Brussee 522e549ee3 fix(registry): scope optional credential token 2026-08-30 02:51:38 +02:00
Julius Brussee 5c2c517d92 feat(cli): omit unavailable profile credentials 2026-08-30 02:46:36 +02:00
Julius Brussee fd99c98f7c fix(cli): verify effective MCP recovery 2026-08-30 02:41:38 +02:00
Julius Brussee 620ece12ee feat(cli): manage Qwen MCP recovery 2026-08-30 01:38:31 +02:00
Julius Brussee cb485a1285 test(cli): cover Qwen wrap isolation 2026-08-30 01:23:55 +02:00
Julius Brussee b34c6d2724 fix(registry): price Qwen provider models 2026-08-30 01:21:42 +02:00
Julius Brussee e831b58b05 feat(cli): preserve platform agent settings 2026-08-30 01:20:52 +02:00
Julius Brussee 4a5e6dfcf1 feat(cli): manage Kilo MCP recovery 2026-08-30 01:11:12 +02:00
Julius Brussee 3d5fa5be85 fix(proxy): align profile IDs with slug limit 2026-08-30 01:02:24 +02:00
Julius Brussee da7f3caf2b fix(cli): resolve published agent aliases 2026-08-30 00:59:18 +02:00
Julius Brussee 5ec62c0d5d test(cli): derive agent coverage from registry 2026-08-30 00:54:19 +02:00
Julius Brussee a42f383fdf fix(cli): support documented version flag 2026-08-29 23:33:18 +02:00
Julius Brussee 8f0fe44049 test: wait for asynchronous shutdown evidence 2026-08-29 22:32:22 +02:00
Julius Brussee 6e6b8ae633 fix: close weekly review gaps 2026-08-29 22:26:31 +02:00
Julius Brussee e11d0029f9 fix: harden weekly integrations and prepare v2.4.0 2026-08-29 21:57:23 +02:00
Julius Brussee 93a6255c36 Merge remote-tracking branch 'origin/pr/889' into codex/weekly-triage-20260829 2026-08-29 21:34:31 +02:00
Julius Brussee 1b7ff8fae9 fix(proxy): scope repo intelligence to project source and require direct evidence (#901)
* fix(proxy): skip vendor trees in repo intel

BM25 was indexing .pixi/envs and other dependency
dirs, then injecting those file:line guesses every
turn. Exclude known vendor trees and drop the path
block when no local scout is configured.

Closes #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): detect dep trees by content, not name

Name denylist missed conda envs, undotted pixi, and
virtualenvs under any name, while dropping real
first-party build/ and vendor/ dirs.

- listFiles prefers `git ls-files --exclude-standard`,
  so the project's own .gitignore defines vendored
- walk fallback identifies environments by contents
  (conda-meta, pyvenv.cfg, site-packages child),
  catching the interpreter stdlib they carry
- ambiguous build/dist/target/vendor need a sibling
  manifest before exclusion
- symlink and junction dirs are never followed

Refs #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): treat the indexed repo as untrusted input

Second review found the git listing both unsafe and
incomplete.

- gitCommand pins core.fsmonitor and other exec
  vectors off and strips GIT_*; fsmonitor runs during
  an index read, so `ls-files` in a hostile clone ran
  attacker code at session start
- git-listed paths now run the same content filter as
  the walk, so an un-ignored conda prefix or committed
  vendor tree no longer rides in on git's say-so
- an empty-but-successful listing means the cwd is
  ignored by an enclosing repo; fall back to the walk
  instead of reporting a complete map
- Direct matches the file name, not the whole path:
  a term like "src" marked every file beneath it
- ListingBasis discloses which path ran, hashed in

Refs #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): log declined evidence, document invariants

A declined turn stays silent to the model but now
records repository_evidence_declined_no_direct_match,
so weak evidence is distinguishable from a broken or
disabled mechanism.

Adds a positive control to the suppression test and
records the gating, filtering, and git-hardening
invariants in proxy/CLAUDE.md.

Refs #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(installer): make prune path flavour explicit, not host-derived

pruneOrphanedManagedHooks guards against pruning a hook whose path is
absolute on the other platform. That guard is POSIX-only by construction:
win32's absolute set is a superset of posix's, so a Windows host reads
C:\... natively and prunes it. The test asserting the guard hard-coded a
Windows path and so failed on the windows CI runner.

Select the path flavour from an explicit platform arg (default
process.platform, same as rewriteLegacyManagedHookCommands) so the POSIX
side stays reachable from a Windows runner. Host behaviour unchanged.

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* test(cli): escape stub preload path in NODE_OPTIONS

Node parses NODE_OPTIONS itself and treats a backslash inside a quoted
value as an escape, so the raw Windows path reached --require with every
separator eaten:

  Cannot find module 'C:UsersRUNNER~1AppDataLocalTempcave-pi-enableXbinstub-dispatch.cjs'

Every stub-backed process died in the preload, failing all six pi enable
and wrap round trips. The quotes must stay for profile names with spaces,
so double the separators instead. POSIX is unaffected — stubEnv returns
before this on non-Windows.

Never caught before because npm test failed earlier in the same job, so
test:windows has not run since these tests joined the gate.

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): harden every untrusted git call, not just repo intelligence

`repointel` hardened its own `git ls-files` and left three other call sites
raw. `nativehook.currentRepositoryState` runs `git status` on EVERY hook
event, and `status` refreshes the index, so it triggers `core.fsmonitor`
harder than `ls-files` does — verified: the sentinel hook fires. Same for
`store/detect_outcomes` and the two `execFileSync` sites in the CLI.

- `proxy/internal/gitsafe` and `packages/cli/src/git-safe.ts` now own the
  `-c` override list; all five call sites route through them.
- `verify_repo.py` fails the build on a NEW raw `git -C <dir>` call site,
  which is the regression class this bug actually belongs to.
- `Bundle.DirectOnly()` strips metadata-only items BEFORE the evidence
  object is written. Filtering only at render time left the BM25-proximity
  guesses stored behind the `ccr://` handle, one retrieval from the model,
  which is what the block refuses to render.
- `gitListBudget` caps git at half the caller's remaining warm budget. 2s
  of a 3s window left the fallback walk — the MORE expensive path — unable
  to finish, so the map failed closed in exactly the case the fallback
  exists for.
- `NormalizeTerms` floors terms at 3 characters, matching the hook that
  produces them: direct evidence is a substring test on a file's own name,
  so `go`/`py`/`js` marked every file of that language a direct hit.

Regression tests verified to fail without their fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(build): stop wedge generation racing itself on Windows

mcp, shrink, and browse each run gen-wedge-installer from their own
prepack/pretest, and every run writes all four targets, not just its own.
`pnpm -r test` runs those packages in parallel, so several processes
copy onto the same file at once and Windows fails the loser:

  EBUSY: resource busy or locked, copyfile
  packages/shared/binary-installer/installer.mjs
  -> mcp/bin/binary-installer.generated.mjs

The bytes are identical every time and the outputs are committed, so the
race was over writes that change nothing. Skip when the destination
already matches, and land a real change through a temp file + rename so
no concurrent reader sees a partial file. Verified with a 12-way
concurrent storm, and against corrupted outputs to prove the write path
still restores them.

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): keep system git config readable while hardening

GIT_CONFIG_NOSYSTEM defends against nothing here — /etc/gitconfig is
root-owned, the threat is the repository's own .git/config, and the `-c`
overrides beat every config level anyway. What it does cost is org-wide
`safe.directory` allowances: without them `git status` fails "dubious
ownership" on shared and CI machines, repository state comes back empty,
and reuse silently stops. Dropped from both wrappers, with a test so it
does not come back.

Also names the fsmonitor tradeoff at the hot call site: disabling it taxes
the people who configured it because their repo is huge, and status has a
100ms budget. That degrades safely; moving the budget is a latency
decision for every user and needs a measurement this fix does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(build): pin wedge generation against the concurrency it now survives

The EBUSY fix has no committed regression proof, and the failure only
appears when several packages run the script at once — which is exactly
what `pnpm -r test` does and what a single-process check never reproduces.
Eight concurrent runs must all succeed and leave the shared outputs
byte-identical; a drifted output must still be repaired, so skip-if-
identical cannot regress into skip-always.

Runs in root `npm test`, which is the gate the Windows job failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
Co-authored-by: Julius Brussee <juliusbrussee@getlockedin.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 01:12:21 +02:00
Julius Brussee 6808703feb fix(cli): revive a dead local proxy mid-session, not just at SessionStart
A wrap-owned proxy idle-exits ~30m after its wrap dies, but native routing
keeps pointing every later turn of a plain session at 127.0.0.1:8787 — so the
next prompt hard-failed with ConnectionRefused and nothing was left to restart
it. Only the SessionStart hook ever revived the proxy.

nativeHook() now runs the (already idempotent, port-checked) revive block on
UserPromptSubmit and PostCompact too, and the fast native hook delegates a
UserPromptSubmit to the full CLI when the loopback gateway PORT is dead. The
trigger is the port, not the runtime socket, so proxy-up/socket-down states
(record pass-through, noProxy, slow runtime) keep the zero-spawn fast path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:03:32 +02:00
Julius Brussee a7fc75c114 feat(cli): surface learn spend, the savings ledger, and holdout reports
Add the CLI half of learn v3: a window-cost and effective-input line on the
default report, `learn savings`, `learn experiment start|arm|report|list|stop`,
`learn export`, and `learn reconcile --usage-export <csv>`.

The rendering enforces the same rules the analyzer does structurally. Savings
rows are grouped by measurement method with no blended headline, confounders and
provenance print in the default view rather than behind a flag, and a confirmed
row without attribution is shown as unattributed rather than attributed by
default. Money is formatted down to five decimals so real sub-cent spend does not
round to $0.00 and read as nothing.

`export` and `reconcile` stay machine-readable: the digest is a file to read
before deciding to share it, and a reconciliation is a table, not a headline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:00:45 +02:00
Dylan Neve 7d4dd245bb fix(cli): honour HTTP_PROXY/HTTPS_PROXY for outbound requests
Node's global fetch ignores the conventional proxy variables, so on a host
whose egress only leaves through an HTTP proxy every request the CLI makes
fails while curl and npm succeed. The first thing a new user hits is:

    binary download unreachable — agents still launch; traffic is NOT
    compressed or metered
    fix: caveman setup --install

which points back at the command that just failed, with nothing to suggest a
proxy is involved. The same blind spot affects the npm version check, login,
and every cloud call.

Node 24 can opt in with NODE_USE_ENV_PROXY=1 and 22 has it behind an
experimental warning, but it is off by default and a process cannot enable it
for itself after start-up, so the CLI has to implement the variables.

Add packages/cli/src/proxy-fetch.ts: resolve the proxy per target
(HTTPS_PROXY/HTTP_PROXY with ALL_PROXY fallback, lower case winning, NO_PROXY
with wildcard, suffix and port rules), then dispatch over node:http and
node:https — CONNECT tunnel for https so TLS still terminates at the target,
absolute-form for http. Redirects are followed with the proxy re-resolved per
hop, so a hop into a NO_PROXY host goes direct.

main() installs the wrapper before any request. It is skipped when nothing is
configured, and when NODE_USE_ENV_PROXY is set so a runtime that already
handles proxying is not proxied twice. No new dependencies.

Verified on a proxy-only host: setup --install reported the download as
unreachable before and installs all six binaries with checksums verified
after. Tests cover variable precedence, NO_PROXY matching, tunnelling,
bypass, redirects, bodies, abort, and the install guard.

Signed-off-by: Dylan Neve <dylanneve1@users.noreply.github.com>
2026-08-21 12:16:07 +01:00
Julius Brussee 7a03e6ffbc test(cli): run the pi round trips on Windows, and Go on macOS
pi-enable and pi-wrap could not join the Windows gate: their fixtures built
PATH with `:` and wrote extensionless `#!/bin/sh` stubs, which Windows cannot
execute at all. tests/harness/stub-bin.mjs gives them two stub shapes, one
per way the CLI launches things — a .cmd Node shim for commands routed
through portableInvocation (`pi`), and, for the Go binaries the CLI
execFile's directly, a real .exe: node.exe hardlinked under the binary's
name, answering from a --require preload that runs before Node resolves
argv[1] as a script. A .cmd cannot serve there, since Node refuses to spawn
one without a shell (CVE-2024-27980). Both fixtures also set USERPROFILE
beside HOME — os.homedir() reads it on Windows, and the Pi extension
directory hangs off it — and retry cleanup for handles Windows still holds.

update.runtime.mjs stays out, and not for a harness reason: its committed
checksum manifest carries only darwin and linux artifacts, so on win32
`update` looks up caveman-proxy_win32_amd64 and fails before downloading
anything. Adding those rows needs the release signing key; the regeneration
recipe is recorded at the fixture read.

The macOS job also runs `go test ./engine/... ./proxy/... ./shared/...` —
without it the runner touched no Go at all, so a BSD-only path, spawn or
timer regression shipped green on the platform this job exists to cover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmbqWDtNexzDfH5V3BMRz
2026-08-20 04:14:49 +02:00
Julius Brussee 8fe0631dcc engine: unwrap agent <file> wrappers before content classification
In-flight work that was already in the working tree, committed alongside the
cross-platform review at the user's request. NOT reviewed as part of that
review — recorded separately so it stays attributable.

Generalizes unwrapListing into unwrapInput (engine/filewrap.go) so agent
<file> tags are stripped alongside read-tool line-number gutters before
Detect classifies content, and rewrapped afterwards. Carries matching
packages/cli changes and their runtime tests.

Verified before committing: `go test ./engine/` passes and the
packages/cli suite is 612 passed / 0 failed / 17 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
2026-08-20 04:14:48 +02:00
Julius Brussee 216e685646 feat(cli): wire pi as a native host — wrap, enable, doctor, conformance
Pi joins every host surface: NativeAgent unions, capability matrix (incl.
post_tool_rewrite; PostToolUseFailure now returns raw responses for pi and
opencode), native-hook output contracts, Go validAgent +
directOutputRewriteAgent, doctor/status/enable/disable/expectedRoute rows,
and the porcelain agent list (visible cap 7 → 8: '+1 more' rendered longer
than the id it hid).

Wrap door: buildPiWrapArgs prepends the profile-declared loader flag plus the
bundled extension (CAVEMAN_PI_EXTENSION → dist/caveman-pi-extension.mjs),
stamps CAVEMAN_PI_HOOK_CMD + CAVE_GATEWAY_URL, and skips the generic base-URL
env spray — pi routing is extension-side, behind its recovery gate. Missing
asset fails open to a direct launch with a warning; the bundler script fails
the build instead of silently shipping without the asset.

Enable door: journaled write of banner + baked hook invocation + bundle to
~/.pi/agent/extensions/caveman-native.js (Pi auto-discovers only .ts/.js;
.mjs would journal fine and never load). settings.json stays byte-identical.
Status/doctor: pi route health = on-disk artifact ends with the currently
shipped bundle (catches stale bundles after CLI upgrades; the gateway is
resolved at runtime by the extension through the same chain as gatewayURL(),
so enable-time route recording would only manufacture false drift).
hooks install/uninstall pi exit 0 and point at enable/disable — the rewrite
surface ships inside the native extension.

Conformance: pi joins the matrix — a real wrap through the real proxy build
compresses via /w/pi/openai/v1 with a CCR handle booked under agent_slug=pi,
and the stub refuses to run if wrap failed open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
2026-08-20 04:14:48 +02:00
Julius Brussee 664268a4f8 test: force-exit runtime suites so no leaked handle can ever hang CI again
typescript hung a third time (2h+ where a completing run takes ~2min).
The telemetry-stub unref fixed one instance of the class, but ~55 runtime
files hold stub servers/processes, and ANY load-dependent failure that
skips its cleanup pins that file's event loop and node --test waits
forever. --test-force-exit (Node 22.5+) ends each test-file process once
its tests finish, leaked handles or not — a failure now fails, on every
file, permanently. Also gave the hand-rolled noisy-stdout stub the same
10s token-read budget as stubProxyStats (the remaining 400ms flake under
concurrency). Mixed batch at concurrency 4: 61 pass x3 consecutive runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
2026-08-19 10:27:07 +02:00