docs: correct OpenClaw acpx config path — openclaw.json (two keys)#19
Merged
firstintent merged 31 commits intomainfrom Apr 15, 2026
Merged
docs: correct OpenClaw acpx config path — openclaw.json (two keys)#19firstintent merged 31 commits intomainfrom
firstintent merged 31 commits intomainfrom
Conversation
The previous docs said to edit a separate ~/.acpx/config.json with a top-level agents key. Actual OpenClaw structure is a single openclaw.json with two nested locations: - acp.allowedAgents gates which agent names /acp spawn accepts - plugins.entries.acpx.config.agents maps agent name → command Updated join.md Step 2 (same-machine + remote-server blocks) and README's OpenClaw rows to reflect this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Workspace routing is a special case of agent-kind routing where the kind is 'claude' and the instance distinguishes sessions. Merged the two separate roadmap items into one — 'Multi-target routing via --target' — with concrete acpx config examples showing how OpenClaw registers multiple bridge entries to reach different CC workspaces and peer adapters through one daemon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extracted the full Multi-target routing design into a dedicated design doc (docs/design/multi-target-routing.md) and shrank the roadmap entry to a one-paragraph summary that points at it. The design covers: - TargetId model (kind:id) - Deployment scenarios (single-dev multi-project, bidirectional, cross-machine multi-tenant) - Routing rules (inbound ACP, inbound A2A, outbound CC→peer, peer→CC) - id conflict policy (reject by default, --force to kick) - CLI surface (claude, codex, acp --target, daemon targets) - Wire-format extensions (claude_connect target, rejection frames) - Backward compatibility - Explicitly out of scope (dynamic discovery, auto-id) Not yet implemented — v0.2 scheduled work. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 atomic tasks implementing the kind:id target model: - P10.1 TargetId type + parser - P10.2 Plugin workspace-id derivation + claude_connect extension - P10.3 Daemon rooms map keyed by TargetId - P10.4 a2a-bridge acp --target flag - P10.5 a2a-bridge daemon targets subcommand - P10.6 Attach conflict policy (reject + --force) - P10.7 A2A contextId → TargetId routing - P10.8 Outbound CC→peer via reply tool target field - P10.9 a2a-bridge codex --id flag - P10.10 Cross-target integration test - P10.11 Documentation sweep + CHANGELOG 0.2.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces the TargetId model for v0.2 multi-target routing:
every agent instance is identified by a branded 'kind:id' string.
- parseTarget(input) returns a Result with parsed {kind, id} or
a descriptive error. Accepts bare kind (id defaults to "default")
or full "kind:id" form.
- formatTarget({kind, id}) is the inverse, throws on invalid parts.
- assertTarget(input) returns the branded TargetId or throws.
- Validation: both segments match /^[a-z0-9_-]+$/ — same constraint
as identifier-safe channel meta keys, so TargetId strings survive
every transport layer without silent mangling.
18 unit tests cover happy paths (kind:id, bare kind, digits,
underscores, hyphens), rejection cases (empty, only colon, leading/
trailing colon, multiple colons, uppercase, space, dot, slash),
format round-trips, and assertTarget behaviour. 402 pass total
(was 384).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…(P10.2) Plugin now sends its TargetId to the daemon on attach so v0.2's multi-target router can place it in the right Room. Backward compat: when no env vars are set, derives `claude:default` (today's behaviour). - src/shared/workspace-id.ts: resolveWorkspaceId() walks the priority chain documented in docs/design/multi-target-routing.md (A2A_BRIDGE_WORKSPACE_ID → A2A_BRIDGE_STATE_DIR basename → conversationId prefix → "default") with sanitisation that guarantees identifier-safe output. resolveClaudeTarget() builds the full `claude:<id>` TargetId. - src/transport/control-protocol.ts: claude_connect frame gains optional `target?: string`. Round-trip test covers both forms. - src/runtime-plugin/daemon-client/daemon-client.ts: attachClaude() now accepts an optional target argument; sends it on the frame when present. - src/runtime-plugin/bridge.ts: computes CLAUDE_TARGET at startup from stateDir.dir + env, passes it to attachClaude(). 15 new tests (workspace-id sanitisation, priority chain, claude_connect round-trip with/without target). 416 pass total (was 402). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…(P10.3) RoomRouter gains TargetId-typed helpers: - getOrCreateByTarget(target) returns/mints the Room for a TargetId. Internally just forwards to getOrCreate(target as RoomId) since a TargetId is structurally a RoomId, but the typed wrapper makes call sites declarative and avoids unsafe casts at every use. - getByTarget(target) is the non-creating accessor. daemon.ts attach handler now stores a Map<TargetId, Connection> in parallel with the existing single attachedClaude pointer: - claude_connect carries optional `target`. Validated via parseTarget; malformed or non-claude targets are rejected with a log line. - Default target is "claude:default" (v0.1 behaviour preserved). - attachClaude updates both the per-target map and the back-compat attachedClaude singleton. - detachClaude cleans up the per-target entry first; if the global pointer was this conn, it falls through to "any other attached" so emitToClaude / broadcast keep a destination. Per-target routing through the gateway (so different inbound turns reach different attached CCs) lands in P10.4 / P10.7. 3 new RoomRouter tests for the TargetId methods. 419 pass total (was 416). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Routes ACP turns to a specific daemon Room via a kind:id TargetId: - transport/control-protocol.ts: acp_turn_start gains optional `target` field (string). - runtime-daemon/inbound/acp/daemon-proxy-gateway.ts: new `target` ctor option; subprocess stamps it on every acp_turn_start frame when set. - cli/acp.ts: parseAcpArgs recognises --target / -t / --target=, validates via parseTarget, plumbs through RunAcpOptions.target → DaemonProxyGateway. Invalid values exit 1 with a clear message. - runtime-daemon/inbound/acp/turn-handler.ts: new `isTargetAttached` predicate ctor option. When supplied, every turn is gated on the predicate; rejection emits acp_turn_error with "target X not attached" and never touches the gateway. Missing `target` field defaults to claude:default. No predicate → all targets accepted (v0.1 backward compat). - runtime-daemon/daemon.ts: wires the predicate to attachedClaudeByTarget (P10.3 map). claude:default also accepts the legacy attachedClaude pointer so single-CC v0.1 flows work without a target field. 4 new turn-handler tests (happy/missing/default/no-predicate). 423 pass total (was 419). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds `a2a-bridge daemon targets` that opens a WebSocket to the control plane, sends a new `list_targets` client message, and prints a plain-text table of every TargetId the daemon currently tracks. Daemon side: - `ControlClientMessage` / `ControlServerMessage` grow the `list_targets` / `targets_response` pair plus a `TargetEntry` struct (target, attached, clientId, attachedAt). - `runtime-daemon/daemon.ts` snapshots `attachedClaudeByTarget` and surfaces the legacy singleton `attachedClaude` as `claude:default` for v0.1 wire compatibility. An `attachedAtByTarget` Map tracks per-target attach wall-clock time for uptime rendering. CLI side: - `RunDaemonOptions.queryTargets` is injectable so tests stub the WebSocket round-trip. - `formatTargetsTable` renders a 4-column table (target, attached, client, uptime); empty input prints a friendly hint. - Usage/help text updated; `cli.ts` lists the new subcommand. Tests: - `daemon.test.ts` covers not-running short-circuit, successful query + table rendering, and error surface. A separate `formatTargetsTable` suite locks the layout. Backward compat preserved: the subcommand is additive, the RPC is optional on the wire, and v0.1 single-CC setups render as a single `claude:default` row. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the v0.1 last-wins behaviour on `claude_connect` with an
explicit conflict policy. When a new CC tries to attach to a
TargetId that is already owned by another connection, the daemon
now rejects the new attach by default and surfaces a descriptive
error. Re-running with `--force` tells the daemon to kick the old
attach, notifying it via a new `claude_connect_replaced` frame.
Wire:
- `claude_connect` grows an optional `force?: boolean` field.
- Two new server-bound frames:
- `claude_connect_rejected { target, reason }` — sent to the new
conn when a conflict is detected without force.
- `claude_connect_replaced { target }` — sent to the old conn
just before the daemon closes its socket under force.
- Round-trip tests lock both variants.
Daemon:
- `attachClaude(conn, target?, force=false)` applies the new policy.
The reason string includes the old conn's client id and a human
uptime (`2h ago`, `3m ago`) pulled from `attachedAtByTarget`.
- `sendProtocolMessage` deliveries are unchanged otherwise; the
existing map bookkeeping stays correct for both outcomes.
Plugin:
- `DaemonClient.attachClaude(target, force)` threads the flag
through. New `connectRejected` / `connectReplaced` events fire
on incoming frames.
- `runtime-plugin/bridge.ts` reads `A2A_BRIDGE_FORCE_ATTACH=1` once
at startup and passes it to the initial attach. On either conflict
event we enter the existing `disabledState` so we stop reconnect
loops and push a CC-visible notification.
- Drive-by fix: the disabled-state recovery path was passing no
target to `attachClaude()`, which would silently route a
recovering CC to `claude:default` instead of its real workspace.
It now passes `CLAUDE_TARGET` (and never forces).
CLI:
- `a2a-bridge claude --force` strips the flag before spawning
`claude` and sets `A2A_BRIDGE_FORCE_ATTACH=1` on the child env.
- `extractForceFlag` helper + unit tests lock the parser.
Tests:
- `src/cli/claude-conflict.test.ts` boots a real daemon, attaches
two `DaemonClient`s to the same TargetId, and asserts both
outcomes: without force → second gets `connectRejected`, first
stays owner; with force → second wins, first receives
`connectReplaced` and its socket closes.
- `daemon-client.test.ts` covers the wire flag + event emission.
- `control-protocol.test.ts` locks the two new server frames.
Scope note: Codex peer conflict handling is deferred to P10.9,
which introduces target ids on codex. This change covers claude
only, matching the attach surface that exists today.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`startA2AServer` gains a `contextRoutes: Record<string, string>`
config knob. When supplied, every inbound `message/stream` turn
resolves its target by looking up `params.message.contextId` in the
map; unmapped contexts fall back to `claude:default`. The resolved
TargetId keys the Room via `RoomRouter.getOrCreateByTarget`, so
multi-CC deployments can carve A2A traffic across distinct CC
instances without each contextId spawning its own Room.
- Startup validates every route value via `parseTarget` — a typo
fails fast rather than 500ing the first inbound call. Supplying
`contextRoutes` without a `roomRouter` is also a startup error.
- When `contextRoutes` is omitted, the server keeps the v0.1
`deriveRoomId({ contextId })` behaviour so existing deployments
and tests are unaffected.
- Daemon wiring: `A2A_BRIDGE_CONTEXT_ROUTES` env var (a JSON object
literal) is parsed via a new exported `parseContextRoutes`
helper. Malformed input is logged and dropped — a bad env var
degrades to v0.1 routing rather than bringing the daemon down.
Tests:
- Integration: three contextIds (two mapped, one stranger) hit
three distinct Rooms keyed by TargetId; stranger routes to
`claude:default`, which notably did NOT mint its own Room.
- Startup: malformed TargetId and missing roomRouter both throw.
- Unit: `parseContextRoutes` handles empty/invalid/partial JSON.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The plugin's `reply` tool accepts an optional `target` field. When supplied, the daemon forwards the reply to that TargetId's Room instead of the inbound turn's originator. Absent target keeps v0.1 behaviour (try inbound intercept → else Codex injection). Wire: - `claude_to_codex` grows an optional `target?: string` field. Round-trip tests lock both the present and absent variants. Plugin: - `reply` tool schema gains a `target` property (description covers the intended hand-off pattern — `claude:project-b`, `codex:default`). - `ReplySender` type, `bridge.ts` adapter, and `DaemonClient.sendReply` all thread `target` through without changing defaults. - Response text names the target when present; preserves the "Reply sent to Codex." phrasing when absent so dual-mode tests stay green. Daemon: - `case "claude_to_codex"` parses `target` via `parseTarget` at the top of the handler. `claude:*` targets deliver directly to the named attached CC (falling back to the legacy `attachedClaude` singleton for `claude:default`). `codex:default` falls through to the existing injection path; other codex ids are deferred to P10.9 and rejected with a descriptive error. Self-delivery is also rejected — replies must not loop back. Tests: - New `src/cli/reply-target.test.ts` spawns a real daemon, attaches two stub CCs, and covers forward / unknown / omit / invalid paths in a single integration run (plus a dedicated test for malformed TargetId strings). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Maintainer decision (2026-04-15): v0.2 ships with multi-claude routing only. Codex peer-id support is out of scope for v0.2 — it would require refactoring the daemon's peer hosting model (per-id registry + port allocation + singleton teardown), too heavy for this phase. P10.10 and P10.11 proceed with claude-only cross-target coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
End-to-end multi-claude routing: two concurrent ACP subprocesses
targeting `claude:ws-a` and `claude:ws-b` now each receive only
their own CC's reply, with no cross-talk. This was the missing
plumbing between the P10.4 target-rejection predicate and actual
target-aware delivery.
Daemon (src/runtime-daemon/daemon.ts):
- New `emitToClaudeTarget(target, message)` helper. Looks up
`attachedClaudeByTarget[target]`; for `claude:default` falls
back to the legacy singleton `attachedClaude` for v0.1 compat.
Drops with a log when no CC is attached for the target — no
cross-target buffering, which would leak data.
- `inboundRoomRouter` factory now mints a per-Room
`DaemonClaudeCodeGateway` whose `sendToClaude` routes to the
Room's TargetId via `emitToClaudeTarget`. `defaultRoom` keeps
the shared `inboundGateway` so v0.1 `"default"` routing is
unchanged.
- `claude_to_codex` handler's interceptReply path now picks the
sender's target (via `claudeConnTarget`) and delegates to that
Room's gateway — so a reply from CC-A can only close CC-A's
in-flight ACP turn, never CC-B's.
AcpTurnHandler (src/runtime-daemon/inbound/acp/turn-handler.ts):
- Constructor takes a `GatewayForTarget` function instead of a
fixed gateway. Daemon wires it to
`inboundRoomRouter.getOrCreateByTarget(target).gateway`.
- `handleTurnStart` becomes async. Resolves the target's gateway
after the `isTargetAttached` check; missing gateway surfaces
`acp_turn_error { "no gateway for target ..." }` — mirrors the
existing not-attached error path.
- Existing 19 unit tests updated to wrap the stub gateway in
`() => gw` and await `handleTurnStart` (all still green).
P10.10 test (src/cli/multi-target.test.ts):
- Boots real daemon, attaches two stub CCs, drives two concurrent
ACP subprocesses with distinct `--target` values, asserts each
subprocess sees ONLY its CC's prefixed reply. Also asserts no
fallback to the echo executor (proves the router saw the CC).
Scope note: codex peer-id routing remains deferred to v0.3
(P10.9) per the maintainer decision on 2026-04-15 — this test
covers the multi-claude axis only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- `docs/design/multi-target-routing.md` — status flipped from "v0.2 design (approved, not yet implemented)" to "implemented in v0.2.0 (multi-claude axis); codex peer-id deferred to v0.3". - `docs/guides/rooms.md` — new TargetId section explains the `kind:id` model, the updated precedence table (ACP `--target`, A2A `contextRoutes`, legacy deriveRoomId path), CC attach id derivation, conflict policy, and the outbound reply target. - `docs/join.md` — new "Multi-workspace (v0.2, optional)" section covers the per-workspace STATE_DIR + `--target` pattern. - `README.md` — new "Multi-workspace routing (v0.2)" section; env var table grows `A2A_BRIDGE_WORKSPACE_ID`, `A2A_BRIDGE_FORCE_ATTACH`, and `A2A_BRIDGE_CONTEXT_ROUTES`; connect table gets an OpenClaw multi-CC row. - `CHANGELOG.md` — 0.2.0 block drafted: TargetId model, plugin workspace-id derivation, `--target`, `contextRoutes`, outbound reply target, attach conflict policy, `daemon targets` subcommand; per-target inbound gateway; wire additions; deferred items (codex peer-id, hot-reload, dynamic discovery). Scope note: architecture.md, positioning.md, and roadmap.md are read-only under the autonomous-mode rules and were not touched. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 10 close-out status: [x] P10.1 TargetId type + parser [x] P10.2 Plugin-side workspace id derivation [x] P10.3 Daemon rooms map keyed by TargetId [x] P10.4 a2a-bridge acp --target flag [x] P10.5 a2a-bridge daemon targets subcommand [x] P10.6 Attach conflict policy (reject + --force) [x] P10.7 A2A contextId → TargetId routing [x] P10.8 Outbound CC → peer via reply target [~] P10.9 codex --id flag (deferred to v0.3) [x] P10.10 Cross-target integration test [x] P10.11 Documentation sweep Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ten-step hands-on checklist the maintainer runs locally before bumping the version and cutting the release. Covers every P10.x capability end-to-end: backward compat smoke, multi-workspace attach + conflict reject, --force, ACP --target, A2A contextRoutes, reply.target, cross-target isolation, docs review. Also includes the post-checklist bump + publish pointer to the existing publish.md runbook and a 72h rollback note. Scope note: codex peer-id (P10.9) is explicitly marked deferred to v0.3 so testers skip it — codex remains single-instance in v0.2.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…icit targets `listTargetEntries` surfaced the legacy `attachedClaude` singleton as a `claude:default` row whenever no per-target entry matched that key — but the singleton is ALWAYS set to the most-recent attach, so for any v0.2 setup (e.g. CC-A as claude:proj-a + CC-B as claude:proj-b) `a2a-bridge daemon targets` printed three rows (proj-a, proj-b, and a phantom claude:default aliasing whichever proj-X attached last). The "no explicit default" guard now also checks that the singleton's connection isn't already listed under a different target, so the legacy default only shows up when a real v0.1 attach (no target field) landed. Regression test in claude-conflict.test.ts drives `list_targets` directly against a real daemon with two explicit-target attaches and asserts the snapshot contains exactly those two (no ghost row). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements ACP `session/load` so clients in "persistent session"
mode (OpenClaw acpx, Zed's restartable agents) can reuse a
sessionId after our subprocess respawns. Without it, acpx's next
prompt after a subprocess restart would fail with "agent does not
support session/load" — the ACP compat gap surfaced during v0.2
OpenClaw smoke testing.
Our ACP server is stateless across subprocess lifetimes — every
turn runs fresh through the daemon's gateway. There is no turn
history to restore, so `loadSession` just registers the sessionId
in `activeSessions` and returns empty, which is enough for acpx to
proceed with its next `session/prompt`. Clients get the same
behaviour as a fresh `newSession` without the caller having to
reconcile ids across restarts.
- `initialize` → `agentCapabilities.loadSession: true`.
- `loadSession(params)` → register, return `{}`.
- Unit test: `service.sessionCount()` bumps by one after a
`loadSession` call on a previously-unknown id.
Also commits docs/release/v0.2.0-openclaw-test.md (the test
prompt handed to OpenClaw, written earlier for the smoke pass).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pass over the skill prompt that CC / OpenClaw read when they self-install each side. Reshapes around what landed in v0.2 and what the v0.2.0 OpenClaw smoke pass exposed as gotchas. - Drops the hardcoded `a2a-bridge v0.1.0` from the verify steps — now just asserts the binary prints any version. Avoids having to bump the skill on every release. - Promotes `--url`-based remote setup into a top-level "Remote server" subsection for each client (OpenClaw, Zed, VS Code). Notes the `A2A_BRIDGE_CONTROL_HOST=0.0.0.0` env var on the CC side, matching what v0.2 cross-host actually needs. - Adds a separate "Advanced — multiple Claude Code workspaces (v0.2)" chapter with the `--target` pattern, the `--force` conflict policy, and a concrete OpenClaw acpx config that registers one agent per target. - Expands Troubleshooting with the real failure shapes we hit during the pre-release smoke: `ACP_SESSION_INIT_FAILED` (acpx PATH), `target <id> not attached`, echo-fallback as a routing-failure signal. - Drops the v0.1-specific "draft release" troubleshooting line — releases have been public for a while. - Small rephrasing throughout (imperative voice, split args into `command` + `args` arrays so acpx doesn't depend on shell parsing). No behavioural change; pure documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 10 shipped, pre-release smoke passed (multi-claude routing end-to-end through OpenClaw), and the two follow-up fixes surfaced during that pass are landed: - no phantom `claude:default` row in `daemon targets` - `session/load` implemented as a stateless no-op so acpx persistent-session mode respawns cleanly Bumps all three version manifests to 0.2.0: - `package.json` - `plugins/a2a-bridge/.claude-plugin/plugin.json` - `.claude-plugin/marketplace.json` Flips the CHANGELOG header from `## [0.2.0] — unreleased` to `## [0.2.0] — 2026-04-16` and appends the two post-Phase-10 fixes to the changelog body. Drafts `docs/release/v0.2.0-github-release.md` as the body for `gh release create --notes-file`, so the GitHub release ships with a proper changelog block this time (previous release had the changelog only in `CHANGELOG.md`). Publishing itself is a human step — see `docs/release/publish.md`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Owner
Author
|
@copilot resolve the merge conflicts in this pull request |
Copilot stopped work on behalf of
firstintent due to an error
April 15, 2026 17:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brief description of the changes.
Motivation
Why is this change needed?
Changes
Testing
bun run check:cipasses (typecheck + lint:deps + test:unit+ build:plugin + version-alignment + smoke-tarball + smoke-e2e)
Checklist