Fixes an intermittent failure of "replays a subagent from its own
transcript file", and with it a broader leak: the unit suite was running
the real Claude CLI hundreds of times per run.
## Cause
The agent runs `claude auth status --json` in the background, without
awaiting it, on every `initialize` and prompt. The scenario suite stubs
it (`CLAUDE_CODE_EXECUTABLE=/usr/bin/false`), but other files didn't.
Logging the probe's executable over one `vitest run` showed about 670
runs of the real CLI:
| File | Real CLI runs |
|---|---|
| `acp-agent.test.ts` | 375 |
| `create-session-options.test.ts` | 14 |
| `incomplete-tools.test.ts` | 12 |
| `session-titles.test.ts` | 8 |
| `file-change-audit-integration.test.ts` | 5 |
Each run uses the machine's own Claude config, unless a test points
`CLAUDE_CONFIG_DIR` elsewhere.
That flaky test points `CLAUDE_CONFIG_DIR` at a temp dir. The CLI it
started then wrote `.claude.json`, `.claude.json.lock` and `backups/`
into that dir about a second later, after the test body had finished.
Under full-suite load, that overlapped the `rm` in the test's `finally`,
which failed with `ENOTEMPTY`. Its assertions always passed.
## Change
`vitest.config.ts` sets `CLAUDE_CODE_EXECUTABLE=/usr/bin/false` in
`test.env`:
- It's the override `claudeCliPath()` already honors, and the value the
scenario suite already uses, so production code doesn't change.
- `test.env` is the base environment, so it survives the
`vi.unstubAllEnvs()` that three files call.
- Runs that exercise the real CLI keep it: with `RUN_INTEGRATION_TESTS`
(`npm run test:integration`) or `RUN_LIVE_CAPABILITY_AUDIT=true`, the
variable is left unset.
Tests that mock `execFile` or set their own executable are unaffected.
## Evidence
- After the change, the same logging shows 0 runs of the real CLI: 688
probes run `/usr/bin/false`, and 108 hit files that already mock
`execFile`.
- The flaky test's temp dir now holds only the test's own `projects/`
after the test, with no `.claude.json`.
- Before the change, it failed in about 2 of 6 full runs on my machine.
After, 8 of 8 full runs passed.
- A full run takes about 4.8 s instead of 8.4 s.
- The variable is `/usr/bin/false` in a default run and unset with
either opt-in flag.
- `npm run build`, `npm run check`, `npm run test:run`.
Today nothing reaches npm between two release-please releases, so trying
a merged
but unreleased change means building from source. This adds a
continuously
published preview channel.
Every CI-green push to `main` that is not a release merge publishes
`<package.json version, patch bumped>-preview.N` under the `preview`
dist-tag,
tags the commit `v<version>`, and dispatches the same agent registry
update a
stable release does. With `main` at 0.73.0 that gives
`0.73.1-preview.1`,
`0.73.1-preview.2`, and so on; `N` restarts at 1 whenever a release
moves the
base. `latest` is untouched.
## Approach
- **Added to `publish.yml` rather than a new workflow.** npm binds a
trusted
publisher to one repo + workflow filename, and a package gets only one.
A
separate file would fail OIDC and cost the stable path its publisher.
- **Gated on the `CI` workflow succeeding** (`workflow_run`), so a red
commit is
never published. release-please's release merge is excluded by the head
commit's author and subject — its `releases_created` output is not
reachable
from a different workflow run.
- **`N` is the max of the npm registry and local git tags.** npm
reserves
versions forever, even after `unpublish`, so a reused `N` is a hard
failure;
but the registry is CDN-served and lags, which the tags cover. A
registry read
failure aborts rather than guessing.
- **Base is patch+1, not release-please's prediction**, which moves
mid-flight
and would reset `N` under already-published previews.
- **The version is applied to the CI working tree only**, never
committed —
`package.json` and the release-please manifest stay release-please's to
own.
- **`--tag preview` is mandatory:** `npm publish` defaults to `latest`
even for a
semver prerelease.
- **Three jobs.** `publish-npm-preview` mirrors `publish-npm` and only
publishes
(no repo write access); `publish-tag-preview` only tags, so a failed tag
is
retryable with _Re-run failed jobs_ without re-publishing an immutable
version;
`trigger-registry-update` is shared with the stable path. Both
downstream jobs
gate on a `published` output the publish step sets, not on job status —
the tag
push happens after the publish, so a red job can still mean npm has the
version.
## Next step
The publish is `npm publish --dry-run` for now. The `workflow_run`
trigger and
the `release` environment's protected-branch policy mean this job cannot
run off
the default branch at all, so merging this is the only way to exercise
the
trigger, the gate and the version calculation — and with `published`
unset, the
rehearsal neither tags nor touches the registry.
Once one run looks right, a follow-up drops `--dry-run` and adds
`echo "published=true" >> "$GITHUB_OUTPUT"` to that step. Nothing else
changes.