Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compatibility and upstream port

jj-acp is a derived work. Most of its design and a substantial part of its logic come from the upstream buzz-acp crate in the block/buzz repository, which is Apache-2.0. This chapter governs how that relationship stays auditable so the port can be updated rather than rewritten when upstream moves.

Upstream source is never committed here. It is pinned by commit, materialized into ignored scratch storage on demand, verified against recorded digests and compared through a per-file disposition map. References are not runtime dependencies.

Requirements

IDRequirement
JAC-COMPAT-001The upstream port source MUST be pinned by repository, path and commit in docs/upstream-lock.md together with a per-file disposition map.
JAC-COMPAT-002Adopted upstream attribution MUST be recorded in docs/licenses/NOTICE.md, and upstream repository-level agent instructions MUST NOT be copied into this repository.
JAC-COMPAT-003Upstream drift MUST be detectable by a script that materializes the pinned source into ignored scratch storage and verifies recorded digests.
JAC-COMPAT-004The Rust toolchain MUST be pinned and formatting MUST be checked by both the normal and the specification check lanes.
JAC-COMPAT-005Integration tests MUST reach a terminal outcome without a model provider, a network endpoint or a credential.
JAC-COMPAT-006Qualification against a real installed runner MUST be recorded separately and MUST NOT be inferred from fixture-peer tests.
JAC-COMPAT-007Every disposition in the port map MUST be one of ported, ported-with-changes, dropped or not-ported, and a dropped region MUST name the reason.

Upstream lock

The pin is block/buzz crate crates/buzz-acp at commit 2664d14316790a57ea44b3f97c57440b70e43436, Apache-2.0, audited in the upstream lock record. Two files carry the record and they have different authors. docs/upstream/disposition.json is curated by hand and is the authority on intent: it names the pin, the owner vocabulary, and a disposition and note for every upstream region. docs/upstream/lock.json is generated by joining that map with observed SHA-256 digests, so a reviewer can see the exact bytes the port claims to derive from.

Because upstream concentrates logic in very large files and this repository decomposes them under a counted-line budget, the map is recorded per region rather than per whole file: a whole-file mapping would make every upstream comparison a diff against a file that no longer exists here. Each region names an owning crate or chapter drawn from a declared vocabulary, so a re-port has a target and an invented owner fails validation.

The two files may not disagree. The lock validator rejects a digest that is not 64 lowercase hexadecimal characters, a region present in one file and absent from the other, and a lock whose disposition or owner drifted from the curated map, so regenerating the lock cannot quietly rewrite the port’s intent.

scripts/upstream-sync.mjs has three modes. --self-test is a normal check lane and never touches the network: it validates the pin and the map, and reconciles them with the generated lock when one exists. --write and --fetch are explicit opt-in network lanes. --write resolves the pinned crate tree, downloads every region, computes digests and regenerates the lock. --fetch re-downloads and reports drift. Both refuse to proceed when upstream holds a region the curated map does not classify, or when the map classifies a region upstream no longer has, so a new upstream file cannot enter the pin silently.

Materialized upstream source lands only in the ignored .local/upstream/<slug>@<commit> subtree. It is never committed, never edited in place and never referenced by a build. Drift is resolved by re-reviewing each changed region against its owning module and updating its note, not by regenerating the lock.

Disposition

Every classified region carries exactly one disposition and the vocabulary is closed. At the pinned commit the 40 regions divide into 1 ported, 18 ported-with-changes, 14 dropped and 7 not-ported.

Upstream regionDisposition
ACP client: framing, correlation, session methodsported-with-changes
Turn pool, isolated execution, prompt framingported-with-changes
Shared runtime, pool lifecycle, task session modelported-with-changes
One-shot task runner and task document contractported-with-changes
Deterministic ACP peer fixtureported-with-changes
Binary entry pointported
Session scopingported-with-changes: one opaque caller-named scope
Base prompt and standing instruction proseported-with-changes: rewritten without relay references
Relay intake, subscriptions, recovery, REST discoverydropped: Nostr intake
Author gate, owner resolution, workflow provenancedropped: Nostr identity
Git signing, credential helpers, ephemeral keyfiledropped: Nostr signing
Memory fetch, observer publication, usage publicationdropped: relay publication
Setup and onboarding modedropped: relay onboarding
Channel and thread session modelsdropped: relay conversation concepts
Inbound queue, batching, deduplication and replaynot-ported: persistent transport
Channel history prompt projectionnot-ported: persistent transport
Standing pool lifecycle tests and harness-specific promptsnot-ported: persistent transport

A dropped region names the reason it was refused and has no owner. A not-ported region names an owning crate and a deferred concern, and is a deliberate deferral tracked in the ledger rather than an oversight.

A ported-with-changes note records what changed, not merely that something did. The ACP client region is the clearest case: it is decomposed into twelve bounded modules, its automatic approval of every permission request is replaced by a caller-supplied decision that defaults to cancellation, and its advertisement of terminal-native authentication and vendor notification extensions is removed because this harness holds no credentials and publishes no telemetry. Each of those is a boundary this repository holds, so it is recorded in the port map where a re-port will surface it rather than left in a commit message where it will not.

Toolchain and dependency pins

The toolchain is pinned by rust-toolchain.toml with the cargo, clippy and rustfmt components, so formatting and lint results are reproducible and the pinned component is what the check lanes invoke. Cargo.lock is committed and every cargo invocation in a check lane passes --locked, so a dependency change is an explicit reviewed edit rather than a silent resolution.

Third-party crates come from the registry under normal semver ranges. Nothing is vendored as committed source, and no dependency is patched, aliased or shimmed to work around a defect; a defect is fixed in its owning repository.

Test qualification

Two levels of qualification are kept distinct and neither substitutes for the other.

The fixture peer is a deterministic scripted ACP agent. It answers the protocol methods with canned frames and can be told to hang at startup, hang during session creation, refuse, exceed a deadline or spawn a long-lived descendant. It proves framing, deadlines, cancellation, cleanup, sanitization and exit codes with no model provider, no network and no credential, which is why every automated lane uses it.

The real installed runner is an actual agent adapter on the operator’s machine. It proves that the ported protocol surface matches what a shipping adapter actually speaks, including capability negotiation and configuration options the fixture only approximates. That qualification is recorded as its own ledger task with its own evidence kind, and the progress checker refuses to mark it done on fixture evidence. A passing fixture suite is not a claim about any real agent.

The ported fixture peer

The fixture is a Python script at tests/fixtures/fixture_agent.py, ported from upstream with all ten failure modes intact: normal, startup hang, session hang, turn hang, ignored cancellation, permission request, agent error, and three descendant-spawning variants. Its launch-record assertions are retargeted from the upstream relay URL, agent key and Nostr git keyfile to the harness-reserved variables this repository actually injects, so the record proves injection rather than identity.

It advertises model and reasoning-effort catalogues, honours a protocol version override and a stop reason override, streams one content chunk per turn, and appends every frame it receives to a log a test can read back. jj-acp-testkit owns locating it, building its argv and environment, and parsing that log.

Two findings came from running it rather than from reading the port map. A permission request answered by semantic kind completes the turn, which exercises a path no in-memory pipe test reaches. And killing the fixture’s direct child leaves its spawned descendant alive, which is the concrete hazard behind the process-group cleanup requirement in session and turn lifecycle rather than an abstract precaution.

Ported regions as of this revision

The ACP client region is ported and decomposed into twelve modules, the largest 228 counted lines. The one-shot task runner region is ported with its document contract, terminal record, exit codes and bounded cleanup. The shared runtime, isolated execution and prompt framing regions are ported in the form the runner and control crates need, which is smaller than upstream because the relay-derived prompt assembly is gone.

Session scoping is ported in reduced form: a one-shot invocation has exactly one scope and one turn, so the opaque-scope machinery upstream needs for channels and threads has no work to do yet and arrives with the persistent transport.

The fixture peer is ported with all ten failure modes and is exercised through the real binary, not only through the protocol crate. Two defects surfaced that way and neither would have been caught by reading the port map: reserving the whole JJ_ACP_ environment namespace stripped legitimate launch options, and letting initialize keep a fixed request timeout gave a hung adapter an unbounded startup phase that the lifecycle chapter explicitly forbids.

Failure modes the fixture must cover

The fixture’s value is the range of ways an agent can misbehave, so its mode list grows when a requirement needs evidence rather than when a test is convenient. Alongside the ten ported modes it now provides two crash modes: one that exits abruptly instead of answering initialize, and one that exits instead of answering session/prompt. Each records a marker before exiting, so a test can distinguish “the agent crashed here” from “the agent never got that far”.

The three descendant modes exist to make process-group containment observable. Without them a cleanup bug is invisible, because killing the direct child always appears to succeed and the orphaned grandchild is nobody’s problem until it holds a port or a file lock. The containment tests read the recorded descendant pid, confirm it is alive mid-run, and confirm it is gone afterwards, which is a stronger claim than asserting the harness exited cleanly.

The log reader also counts dispatched methods. That count is what turns “the crash was reported” into “the crash was reported and the turn was not re-sent”, which is the part of the requirement a record assertion alone cannot prove.

Real installed-runner qualification

Two adapters have been qualified. The first is cursor-agent acp version 2026.09.26-dd393fe, authenticated through the operator’s existing cursor-agent login session. The harness performed no authentication and never called the authenticate method the adapter offers, which is the boundary JAC-SEC-001 states and the reason an adapter that requires interactive login is usable here only when the operator has already logged in.

Two real turns completed, both reporting status: completed with stopReason: end_turn and exit 0, one of them reading a file in the working directory and leaving it unmodified. The adapter’s initialize and session/new payloads are committed as a fixture under the protocol crate’s tests.

The second is @agentclientprotocol/codex-acp version 1.13.1, resolved from the local npm cache and launched the way ai-host launches it through acpx. One read-only turn completed with the target file unmodified and no file created. Its captured payload is committed alongside Cursor’s, and the two are deliberately different: Codex reports agentInfo where Cursor reports none, offers bare model values where Cursor embeds parameterized overrides, exposes a thought_level option under the adapter-defined identifier reasoning_effort where Cursor exposes none, and emits an _auth/status_update notification during the handshake that is not part of the exchange being waited for.

Asserting against both is what makes the capability-parsing tests meaningful. An explicit false capability is only a real regression risk because a shipping adapter sends one, and discovery of an adapter-defined option identifier is only proven by an adapter whose name for it nobody would have guessed.

This closes the fixture-versus-real gap for two adapters. It is not evidence about claude-agent-acp, opencode or goose, which remain unlaunched, and neither run raised a permission request, so the default deny policy is still proven only against the fixture peer.

Reasoning effort and execution mode are still not applied. The captured payload shows why the generic shape does not transfer: this adapter carries effort inside parameterized model values such as grok-4.7[context=256k,reasoning_effort=high, fast=true] and exposes a mode option with agent, plan and ask, while the upstream harness discovered a separate thought_level option for a different adapter. Neither is a superset of the other, so JAC-RUN-006 stays Planned and owns its own milestone rather than being marked done on a model-only implementation.

Advertised surfaces are not stable across sessions

Qualifying session configuration corrected a claim this chapter previously made. An earlier capture of codex-acp 1.13.1 recorded a thought_level option under the adapter-defined identifier reasoning_effort, and a model_config option for fast-mode. A later session with the same adapter version advertised neither, had one more model candidate, and reported a different current model.

The cause is that the adapter’s configuration surface depends on the selected model. With one model it exposes reasoning effort as a separate option; with another the effort is carried inside the model identifier itself. A committed capture is therefore evidence about one session, not a property of an adapter version, and the fixture is a snapshot whose authority expires.

Two consequences are load-bearing. Resolution must happen against the current session’s advertisement and never against a cached catalogue, which is what the runner does. And a test that asserts a specific adapter offers a specific option is asserting something that can stop being true without any code changing, so the committed fixtures are used to prove parsing behaviour against real shapes rather than to promise what a live adapter will advertise.

Live configuration qualification

Effort applied through a parameterized model identifier, and mode application, were both qualified against a live adapter: --effort high and --mode plan each landed with no shortfall reported, and a narrowed request for a model that carries no effort reported both settings as unavailable rather than guessing. A live --mode plan against the other adapter was correctly refused with a diagnostic naming the collaboration_mode category that does offer it.

The thought_level path is proven against the committed capture but was not reproduced live, because the adapter no longer advertises that option. It is recorded as fixture-proven rather than live-proven rather than being claimed outright.

Interactive session integration

The interactive fixture proves persistent session identity, streamed tool/text/plan activity, permission handling and cancellation. It is distinct from installed-adapter interactive qualification.

The fleet fixture records initialization, distinct adapter sessions and prompt counts. It proves application scope fencing and zero warmup prompts separately from installed adapter qualification. Control version 3 requires fresh real Codex/Cursor acceptance; earlier version-2 evidence is unchanged.

E-M12-002 qualifies separate prompt context with payload, control-wire and fleet fixture assertions. The fixture advertises embeddedContext and records block boundaries while preserving session fencing. These results do not qualify installed-adapter resource interpretation, permissions or other platforms. Text fallback remains available when the capability is absent or false.

Trusted tool declaration fixtures

The fleet fixture records the trusted mcpServers received by both distinct session/new calls. Warm initialization opens neither a session nor coding tools. Its inert tool declaration never executes a command. CLI rejection tests cover bounded input, duplicate names, relative commands and reserved task identities. These are source/fixture proofs only; local model coding tools and installed permission/cancellation exchange remain controlling-application qualification.

Public release preparation

Publication relocates all Python fixture peers into crates/jj-acp-testkit/tests/fixtures. fixture_script resolves from CARGO_MANIFEST_DIR, so an extracted testkit remains usable without the original workspace. Historical root fixture paths are retained in evidence with explicit removal records. Platform release gates use deterministic fixtures; M12 live-adapter gates remain open.

Parallel CLI fixtures allocate process-local serials with exclusive directory creation. Timestamp collisions must never share state, and peer teardown must preserve live test directories. The scratch regression reproduced 31 directories for 32 simultaneous callers before this fix. Signal exit-code and record assertions remain unchanged.

Changesets prepares coordinated Cargo metadata and changelog history through development tooling. Publication still uses immutable version tags, the existing native qualification profile and the configured crates.io trusted publishers. Initial 0.1.0 artifacts remain attached to their original source.

On a cold checkout, locked Cargo dependency bootstrap precedes offline Changesets preparation. A missing dependency cache preserves the pending note and previous coordinator metadata; it is not a reason to disable package verification.

Release-preparation acceptance verifies that versioning consumes pending notes and synchronizes metadata without modifying canonical chapters or the ledger. The documented automation exception preserves deterministic/native product qualification and does not extend installed-runner compatibility claims.