Switch the bundled ACP tooling from full-CLI bundling to bridge-only
bundling. The staging scripts now install the bridge JS trees with
`npm --omit=optional`, which skips the SDK/codex platform packages that
vendor the native claude/codex CLIs — the bundled resource drops from
~586MB to ~56MB of pure JS, and ad-hoc codesigning of vendored Mach-O
binaries is no longer needed.
The bridges instead run the user's own harness CLI: at spawn time the
desktop resolves `claude`/`codex` from PATH and exports
CLAUDE_CODE_EXECUTABLE / CODEX_PATH (neither bridge falls back to PATH
itself), driven by a new `bridge_cli_env_var` catalog field. A value
already present in the desktop's environment wins, and per-agent env
overrides still apply afterwards.
Because the app once again depends on a user-installed CLI, this
reinstates the machinery that 55a80e5c retired: the CliMissing
availability gate, PATH-based auth probes, curl CLI install commands,
and the Doctor's CLI-missing copy and CLI-path row. The Doctor's
"bundled" badge now reads an explicit `adapter_ships_with_app` catalog
field, since inferring it from empty install-command lists breaks once
claude/codex regain CLI install commands.
The lock drops the native*/npmOs/npmCpu/npmLibc fields (trees are
platform-independent, but stay per-target so pins can be bumped
independently), install validation asserts no platform package slipped
into the tree, and freshness stamps gain STAMP_INSTALL_MODE=bridge-only
so stale full-CLI caches are reinstalled rather than reused. The
harness-clis.json manifest and its resolution path are removed; the
prepare script deletes the stale file from previously staged resource
dirs.
Verified: desktop cargo tests (1421), buzz-acp tests, clippy + fmt on
both, tsc, biome, desktop unit tests (2791), doctor-states +
doctor-cta-screenshots Playwright specs, file-size/px guards, and
end-to-end staging on aarch64-apple-darwin (bridge wrappers report
0.58.1/1.1.2, no Mach-O in the tree, idempotent re-run).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Since the bundling series emptied every runtime's
adapter_install_commands, three pieces of machinery in the earlier
cleanup plan were dead code. Delete them, and fix the Phase-3 install
verification hint that the emptying broke for goose:
- node_required install gate: runtime_needs_npm was constant false, so
the node_required computation never fired and Doctor's amber
"Node.js is required to install this adapter" callout could never
render. Deleted the trigger (discovery.rs), the wire field (types.rs,
tauri.ts, types.ts), the callout component + the nodeRequired half of
the Install-button gate (DoctorSettingsPanel.tsx), and the fixture
fields (e2eBridge.ts, agentReadiness.test.mjs, doctor-states.spec.ts).
E2e test 04-node-required force-fed the retired state and is deleted;
the bundled bridges' real Node.js requirement bites at spawn, not
install, and is covered by the node-runtime Doctor section
(07-node-runtime-warn). The cleanup plan's per-row spawn-time callout
derived from nodeRuntimeCheck remains open as a follow-up.
- npm preflight/EACCES machinery: Phase 2 of
install_acp_runtime_blocking applied npm handling only to
adapter_install_commands — empty everywhere. Deleted
is_npm_global_install (+7 tests), npm_eacces_hint/npm_eacces_guidance/
NPM_MISSING_HINT (+5 tests), npm_preflight_check/resolve_npm_prefix/
npm_install_target_is_writable/unix_is_writable (+3 tests), the
Phase-2 npm branches, and the stale Phase-1 preflight note.
- codex adapter-availability drift stamp: built to catch a manual npm
install/downgrade flipping the adapter under a running agent —
impossible now that bare-name resolution prefers the bundle. Deleted
the cache + availability_drift predicate (+5 tests), the codex-only
discovery warm, the spawn-time stamp, the availability_drift half of
needs_restart (hash_drift stays), and the ManagedAgentProcess field
with its finish_spawn plumbing.
Goose verify hint fix: adapter_verification_step received
bundled = adapter_install_commands.is_empty(), now true for all four
runtimes — a goose curl install that succeeded but left `goose`
unresolvable claimed "The Goose ACP adapter ships with the Buzz desktop
app… Reinstall Buzz", wrong on both counts. The new
runtime_adapter_is_bundled seam requires cli_install_commands AND
adapter_install_commands to be empty (goose has a curl CLI installer;
claude/codex/buzz-agent have neither), with catalog-driven tests
pinning goose to the check-the-step-output hint. That hint also drops
its "and your npm global prefix" tail — no npm-installed adapters
remain in the catalog.
File-size ledger ratcheted down to bank the deletions:
agent_discovery.rs 1410 -> 1021, discovery.rs 1147 -> 1046,
runtime.rs 2216 -> 2079, tauri.ts 1342 -> 1340, types.ts 1066 -> 1064.
Covers sections 1 and 3 of the post-bundling cleanup note; the
adapter_missing retirement (section 2) and copy/fixture staleness
(section 4) are separate follow-ups.
Verification:
- cargo test --lib (desktop/src-tauri): 1423 passed, 0 failed
(1436 at head - 15 deleted npm/drift tests + 2 new bundled-flag tests)
- cargo clippy --lib --tests -D warnings: clean; cargo fmt --check: clean
- tsc --noEmit: clean; biome: no new diagnostics
- pnpm test (desktop): 2792 passed, 0 failed
- playwright doctor-states.spec.ts: 8 passed;
doctor-cta-screenshots.spec.ts: 3 passed
- node scripts/check-file-sizes.mjs + check-px-text.mjs: pass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The Doctor runtime row's status line for a bundled adapter read
"ACP bridge bundled with Buzz." — the protocol jargon adds nothing in
a row that already names the runtime, and the sentence only needs to
say where the tool comes from. Shorten it to "Bundled with Buzz."
(keeping the trailing period to match the sibling "Available via ..."
line).
Updated in step: the DoctorSettingsPanel comment quoting the line, the
09-bundled-adapter e2e assertion and its doc comment, and the
AcpRuntimeCatalogEntry::adapter_bundled doc comment that cites the UI
copy. No other test or fixture asserts the old string; the node-runtime
check's backend-sourced messages keep their ACP wording per the
row-detail/page-framing split from the de-jargon pass.
Verification:
- tsc --noEmit: clean; biome check on both touched TS files: clean
- cargo fmt --check (desktop/src-tauri): clean (doc-comment-only change)
- playwright doctor-states.spec.ts: 9 passed, including
09-bundled-adapter against the new copy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Since the ACP bundles vendor full native CLIs (Claude Code inside
@anthropic-ai/claude-agent-sdk-*, codex inside @openai/codex-*), agent
sessions never touch a user-installed claude/codex. Yet the auth probes
still resolved those CLIs from the user's PATH, and a missing user CLI
put the runtime behind a CliMissing availability gate — blocking agents
that would have run fine on the bundle. Point the probes at the vendored
CLIs and delete the gate plus everything downstream of it (curl-pipe CLI
install commands, Doctor's user-CLI path row, the cli_missing nudge card).
The vendored CLIs are deliberately NOT staged into resources/acp/bin:
that dir is the highest-priority segment of the agent-spawn PATH, and
CLIs there would shadow the user's (possibly newer) claude/codex inside
every session. Instead a probe-only manifest maps CLI names to paths
inside the staged node trees.
- prepare-acp-tools-resource.sh reads nativePackage/nativeExecutable
from acp-tools.lock.json, verifies the vendored binary exists in the
staged tree (chmod +x), and writes harness-clis.json next to
node-runtime.json: {"clis":[{id,cli,path}]} with resource-root-relative
paths. The manifest is gitignored like its sibling.
- acp_tools::bundled_harness_cli() resolves a CLI name through the
manifest — bare names only, relative non-escaping paths only, must be
an executable file. Absent manifest/entry degrades to None.
- discovery::resolve_probe_binary() tries the bundled CLI first, then
the user's PATH — auth probes in the discovery sweep and
readiness::cli_login_requirements both go through it. Auth state now
reflects the CLI the sessions actually use, while dev builds without
a staged bundle keep working via the PATH fallback.
- classify_runtime: a resolving adapter is Available unconditionally;
underlying_cli now only disambiguates AdapterMissing vs NotInstalled.
CliMissing is removed from the desktop enum (runtime-only, never
persisted) and from the TS union; buzz-acp's wire mirror keeps the
variant so payloads from older desktops still parse (AdapterOutdated
precedent), while the FE nudge validator rejects the retired literal
so stale JSON can't render a card the UI has no branch for.
- claude/codex catalog entries drop underlying_cli and the curl-pipe
cli_install_commands; login provisioning copy stays as-is.
- UI: cli_missing branches removed from Doctor (StatusIcon, RuntimeRow,
user-CLI path row), SetupStep, persona pickers ("(CLI missing)"
suffix + sort rank), AgentDefinitionDialog warning, and the nudge
card; retired the cli_missing nudge screenshot spec.
Verification:
- cargo test --lib (desktop/src-tauri): 1436 passed, 0 failed (new
bundled_harness_cli manifest/escape/absent tests, probe-runs-without-
underlying-CLI readiness test, adapter-implies-Available discovery test)
- cargo test -p buzz-acp --lib: 497 passed (cli_missing round-trip kept)
- cargo clippy --lib --tests -D warnings (both crates): clean;
cargo fmt --check (both crates): clean
- tsc --noEmit: clean; biome check + file-size/px-text/pubkey guards: pass
- pnpm test (desktop): 2792 passed, 0 failed (new validator-rejects-
cli_missing test)
- playwright doctor-states.spec.ts + doctor-cta-screenshots.spec.ts:
12 passed (Doctor shows no CLI path row; nudge cards unaffected)
- prepare-acp-tools-resource.sh: manifest written; vendored
claude --version → 2.1.205 (Claude Code); vendored
codex login status → exit 0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The claude catalog entry still listed the retired Zed-era
claude-code-acp adapter as a resolution fallback after
claude-agent-acp. Since the bundling series, the bundled
claude-agent-acp always resolves first, so the fallback is
unreachable in the discovery sweep, readiness classification, and
install verification — and were the bundle ever broken, silently
spawning a stray install of the deprecated package is exactly the
drift the series exists to prevent. Reporting adapter_missing (with
the reinstall-Buzz hint) is the correct outcome there.
Move the name from the claude entry's commands to its aliases:
known_acp_runtime still maps stored records, override pins, and
avatar lookups carrying the legacy command to the claude runtime
(the override tests feeding claude-code-acp all pass unchanged), but
no resolution path sweeps for it anymore. The commands/aliases split
this makes load-bearing is now documented on the KnownAcpRuntime
fields, and comments claiming discovery "may select an installed
alias" are reworded to the stored-record reality. The
default_agent_args normalization arm and the frontend/buzz-acp
normalization paths (agentReuse.ts, buzz-acp config.rs) deliberately
keep the name for stored records, as does KNOWN_AGENT_BINARIES for
orphan-process cleanup.
Completes item B.3 of the post-bundling cleanup plan. The
discovery.rs file-size ledger is ratcheted 1134 -> 1139 for the five
new comment lines (the file sat exactly at its limit).
Verification:
- cargo test --lib (desktop/src-tauri): 1423 passed, 0 failed,
11 ignored — including the avatar-url and override-pin tests that
exercise claude-code-acp identity mapping via the alias path
- cargo clippy --lib --tests -D warnings: clean
- cargo fmt --check: clean
- node desktop/scripts/check-file-sizes.mjs: pass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
For the bundled claude/codex bridges, the Doctor panel rendered the
resolved adapter path — a deep resource-dir path like
/Applications/Buzz.app/Contents/Resources/resources/acp/bin/… — plus
an "Available via installed on PATH" description that is wrong for a
bundled tool. Replace both with the plain statement "ACP bridge bundled
with Buzz." and keep showing only the user's CLI path.
- AcpRuntimeCatalogEntry gains adapter_bundled, computed in the
discovery sweep via the new acp_tools::path_is_in_bundled_dir
(component-wise starts_with against the registered bundled tools bin
dir, false when no bundle is registered). It covers both states that
surface an adapter path: available and cli_missing.
- Doctor RuntimeRow: when adapter_bundled, the "Available via …" line
becomes "ACP bridge bundled with Buzz.", the mono adapter-path line
is dropped (the CLI path stays), and the cli_missing copy reads
"ACP bridge bundled with Buzz, but the <label> CLI is not installed."
instead of quoting the bundle path.
- adapter_bundled is optional on the raw TS type (mapped ?? false) so
existing e2e fixtures stay valid; the field is always sent by the
backend.
The file-size ledger bumps for discovery.rs / tauri.ts / types.ts
landed with the preceding gate-repair commit (lefthook checks out the
staged half of partially staged files, so the ledger could not be
split across the two commits).
Verification:
- cargo test --lib (desktop/src-tauri): 1423 passed, 0 failed
(new component-wise path_is_in_dir test)
- cargo clippy --lib --tests -D warnings: clean; cargo fmt --check: clean
- tsc --noEmit: clean; biome check: no new diagnostics
- pnpm test (desktop): 2744 passed, 0 failed
- playwright doctor-states.spec.ts: 9 passed, including new
09-bundled-adapter asserting the copy renders, the bundle path does
not, and the CLI path still does
- node scripts/check-file-sizes.mjs + check-px-text.mjs: pass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
install_acp_runtime_blocking previously reported success whenever every
install step it ran succeeded. For the bundled bridges (claude, codex)
the adapter install plan is empty, so a broken bundle would run zero
steps and report success — discovery would immediately classify the
runtime as not installed again, an install-succeeds/still-broken loop
with nothing actionable surfaced to the user.
Add a Phase 3 verification gate after the final resolve-cache clear: a
pure adapter_verification_step seam re-resolves the runtime's adapter
commands and, when none resolves, appends a failed synthetic "verify"
step and flips the result to success:false. The hint points at
reinstalling Buzz when the adapter is bundled (the catalog carries no
install commands) and at the install step output for npm-installed
adapters like goose.
Ports the install-verification gate from the berd donor series
("require the bundled bridge in install verification", berd 4028e34c,
squashed into berd 09388d7): post-fix verification must apply the same
resolved-bundled-bridge gate as readiness, otherwise an install can
verify as success while the card immediately flips back to
not-installed. The readiness half of that gate is already structural
in Buzz — classify_runtime yields AdapterMissing/NotInstalled when the
bundle-first resolution finds no adapter.
Verification:
- cargo test --lib (desktop/src-tauri): 1385 passed, 0 failed
(4 new adapter_verification_step tests: resolves -> None, bundled
failure carries the reinstall-Buzz hint, unbundled failure does not,
empty command list -> nothing to verify)
- cargo clippy --lib --tests -D warnings: clean
- cargo fmt --check: clean
- node desktop/scripts/check-file-sizes.mjs: pass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
With the ACP bridges (@agentclientprotocol/claude-agent-acp,
@agentclientprotocol/codex-acp) bundled with the app at pinned versions
and resolved ahead of user installs, three pieces of machinery are dead
code for those two runtimes:
- The codex 0.16.x version gate: probe_codex_acp_major_version,
codex_adapter_availability / codex_adapter_is_outdated, and the
AdapterOutdated availability status (Rust enum variant plus the
frontend "adapter_outdated" union member and all its UI branches).
The bundled adapter is always the pinned 1.x package, so probing for
the deprecated @zed-industries/codex-acp package has no trigger left.
- The EEXIST uninstall-then-reinstall two-step in plan_adapter_install,
which existed only to swap the deprecated codex package for the new
one. The plan is now the simple missing -> catalog-commands mapping,
and the function drops its runtime_id parameter.
- The in-app `npm install -g` flow for the two bundled bridges: their
catalog entries now carry empty adapter_install_commands and a hint
that the adapter ships with the Buzz desktop app. Goose keeps its
npm install flow untouched — its adapter is not bundled.
cli_login_requirements now classifies availability purely via
classify_runtime (which resolves bundle-first), replacing the codex
version-probe special case.
The configNudge validator rejects the retired "adapter_outdated"
literal, so stale nudge JSON emitted by an older app version cannot
render a card the current UI has no branch for (regression test
inverted accordingly).
This is a Buzz-specific retirement enabled by the bundling series; the
donor series (block/builderbot apps/staged, berd) had no equivalent
version gate to remove. File-size ledger entries for discovery.rs
(1178 -> 1056), discovery/tests.rs (1029 -> 825), and readiness.rs
(1754 -> 1583) are ratcheted down to bank the deletions, per the note
left in the bundled-resolution commit.
Verification:
- cargo test --lib (desktop/src-tauri): 1381 passed, 0 failed
- cargo clippy --lib --tests -D warnings: clean
- cargo fmt --check: clean
- tsc --noEmit: clean; biome check src: no new diagnostics
- pnpm test (desktop): 2744 passed, 0 failed
- node scripts/check-file-sizes.mjs: pass with ratcheted limits
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The bundled ACP bridges are shell shims that exec node; without a
suitable Node.js on the spawn PATH the first agent session dies with a
bare exit 127. Surface the requirement in the Doctor panel instead:
- New managed_agents::node_runtime module reads the staged
resources/acp/node-runtime.json manifest (one entry per npm-sourced
bridge, each with its own required Node major), resolves node from
the same augmented PATH the agent spawn and CLI auth probes use (so
the check cannot disagree with what the wrapper shims find at spawn
time), probes `node -p process.versions.node` with a 10s timeout,
and reports pass/warn with a per-bridge satisfied/unmet/unknown
verdict list. A missing manifest keeps the check silent (no
npm-sourced bridges bundled); an unreadable one warns instead of
hiding a packaging break.
- New check_acp_node_runtime Tauri command plus a Doctor panel section
(message, node path, per-bridge requirements, Install Node.js fix
link on warn) that the Re-run button refreshes alongside the runtime
rows. The e2e mock bridge grows a nodeRuntimeCheck fixture knob.
- tokio gains the "process" feature for the async node probe.
Readiness gating for bundled bridges needs no code here: Buzz's
classify_runtime already reports adapter_missing when find_command
(which prefers the bundle since the previous commit) cannot resolve
the bridge, and cli_login_requirements resolves the adapter the same
way — so a broken bundle reads not_installed instead of ready. This
is the behaviour squareup/berd 17c5e9e5 had to add explicitly.
Ports block/builderbot apps/staged adea4017 (node-runtime doctor
manifest), itself a port of squareup/berd 07087303.
Verification:
- cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib:
1392 passed, 0 failed, 11 ignored (10 new tests: version parsing,
requirement labels, all four check states, manifest missing/invalid/
loaded including the exact staging-script JSON shape)
- cargo clippy --all-targets: clean; cargo fmt --check: clean
- desktop: tsc --noEmit clean; biome clean; pnpm test 2744 passed
- playwright doctor-states.spec.ts (smoke): 8 passed, including new
06-node-runtime-pass / 07-node-runtime-warn /
08-node-runtime-hidden-when-not-bundled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Register the staged ACP tools bin dir (BUZZ_ACP_TOOLS_DIR dev override,
else the resources/acp/bin Tauri resource dir) once at app setup, and
consult it everywhere bridge commands are resolved:
- New managed_agents::acp_tools module: OnceLock registration plus
command_in_bundled_dir, which resolves bare command names only —
path-like commands name a specific binary the user picked and are
never redirected into the bundle (Path::join with an absolute path
would replace the bundled dir entirely).
- resolve_command_uncached consults the bundled dir first, so the
discovery sweep, readiness find_command, and spawn-time agent-command
resolution (BUZZ_ACP_AGENT_COMMAND) all prefer the pinned bridges.
Registration runs at the top of .setup() because resolutions are
cached for the app lifetime.
- build_augmented_path gains the bundled dir as its highest-priority
segment, covering both the agent spawn PATH and the CLI auth-probe
PATH, and now joins entries best-effort: an entry embedding the PATH
separator (legal in macOS paths) is dropped with a log line instead
of collapsing the entire augmented PATH to None.
The donor's GOOSE_SEARCH_PATHS pinning is deliberately not ported —
Buzz spawns bridges via its own buzz-acp harness, which takes the
resolved command from BUZZ_ACP_AGENT_COMMAND and the augmented PATH.
The discovery.rs file-size ratchet is bumped 1171 -> 1178 for the new
sweep check; the codex version-gate retirement later in this series
shrinks the file and ratchets it back down.
Ports block/builderbot apps/staged 55eb2772 (resolve bundled ACP tools
at runtime) and afa524e7 (best-effort PATH join), themselves ports of
squareup/berd 2add3727 and bb912c96.
Verification:
- cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib:
1382 passed, 0 failed, 11 ignored (14 new tests cover env-override
precedence, bare-name-only bundled resolution, executable checks,
bundled-dir PATH priority, and un-joinable entry handling)
- cargo clippy --all-targets: clean; cargo fmt --check: clean
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Buzz desktop spawned the claude-agent-acp and codex-acp bridges from
whatever the user had installed globally — an unpinned `npm install -g`
surface with no integrity checking, which produced stale-bridge drift
(the deprecated @zed-industries/codex-acp 0.16.x gate) and
missing-tool failures. Bundle both bridges as app resources instead,
pinned per target:
- desktop/acp-tools.lock.json pins @agentclientprotocol/claude-agent-acp
0.58.1 and @agentclientprotocol/codex-acp 1.1.2 (npm `latest` at time
of commit) for the four supported targets, with integrity hashes and
Block Artifactory tarballs for the package, its claude-agent-sdk /
@openai/codex dependency, and the per-target native package.
- desktop/scripts/update-acp-tools-lock.mjs regenerates the lock from
the registry's `latest` dist-tags, failing loudly on any unresolvable
package — never silently pinning an older version. Ranged
dependencies (codex-acp's ^0.144.0) resolve to the highest matching
version when `npm view` returns an array.
- desktop/scripts/ensure-acp-tools.sh installs the locked tools into a
shared dev cache (~/Library/Caches/buzz-dev/acp-tools), validates
versions + integrity against the lock, and stamps staged binaries
next to the shared bin dir so any lock change — including a revert —
forces a re-stage; binaries no longer in the lock are pruned.
- desktop/scripts/prepare-acp-tools-resource.sh stages the vendored npm
trees + node wrapper shims into desktop/src-tauri/resources/acp,
writes the node-runtime.json manifest for the app's Node.js doctor
check, and ad-hoc signs every nested Mach-O (file(1) scan — the codex
package vendors rg, zsh, and codex-code-mode-host beyond the main
CLIs) so Gatekeeper doesn't kill them in local builds.
- desktop/scripts/lib/acp-node-wrapper.sh is the single wrapper-shim
generator shared by both scripts, so the dev-cache and bundled
wrappers (and the Node major they enforce) cannot drift.
- Wiring: `just dev` / `just staging` stage the resources and export
BUZZ_ACP_TOOLS_DIR; `just desktop-release-build` stages per target
before `tauri build`; `just bump-acp-tools` reruns the lock updater;
tauri.conf.json bundles resources/acp; staged artifacts are
gitignored with a .gitkeep placeholder.
Runtime resolution of the staged dir follows in the next commit. The
sprout-releases internal pipeline will need the same staging step
before its `tauri build`; that lives in a separate repo.
Ports the build-time half of the Staged implementation in
block/builderbot#876 (branch commits 16b115a7 bundle feature, f5c6bba9
re-stage after lock revert, 288fa7eb shared node wrapper lib, adea4017
node-runtime manifest, 5124302a sign all nested Mach-Os, de6a9782
ranged-dependency updater fix), itself ported from squareup/berd
f07df1d2 + 1db993fb + 24d7518b + 07087303 + 737e33a5.
Verified: update-acp-tools-lock.mjs regenerated the lock against the
Block registry (byte-identical pins to the donor lock; both packages
confirmed at npm `latest`); fresh stage installs both tools and the
staged wrappers report 0.58.1 / 1.1.2; no-op re-run performs zero npm
installs; a stamp/lock mismatch forces a re-stage and stray binaries
are pruned; all five staged Mach-Os pass `codesign --verify`; cargo
check on desktop/src-tauri passes with the new resources entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>