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
| ID | Requirement |
|---|---|
| JAC-COMPAT-001 | The 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-002 | Adopted 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-003 | Upstream drift MUST be detectable by a script that materializes the pinned source into ignored scratch storage and verifies recorded digests. |
| JAC-COMPAT-004 | The Rust toolchain MUST be pinned and formatting MUST be checked by both the normal and the specification check lanes. |
| JAC-COMPAT-005 | Integration tests MUST reach a terminal outcome without a model provider, a network endpoint or a credential. |
| JAC-COMPAT-006 | Qualification against a real installed runner MUST be recorded separately and MUST NOT be inferred from fixture-peer tests. |
| JAC-COMPAT-007 | Every 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 region | Disposition |
|---|---|
| ACP client: framing, correlation, session methods | ported-with-changes |
| Turn pool, isolated execution, prompt framing | ported-with-changes |
| Shared runtime, pool lifecycle, task session model | ported-with-changes |
| One-shot task runner and task document contract | ported-with-changes |
| Deterministic ACP peer fixture | ported-with-changes |
| Binary entry point | ported |
| Session scoping | ported-with-changes: one opaque caller-named scope |
| Base prompt and standing instruction prose | ported-with-changes: rewritten without relay references |
| Relay intake, subscriptions, recovery, REST discovery | dropped: Nostr intake |
| Author gate, owner resolution, workflow provenance | dropped: Nostr identity |
| Git signing, credential helpers, ephemeral keyfile | dropped: Nostr signing |
| Memory fetch, observer publication, usage publication | dropped: relay publication |
| Setup and onboarding mode | dropped: relay onboarding |
| Channel and thread session models | dropped: relay conversation concepts |
| Inbound queue, batching, deduplication and replay | not-ported: persistent transport |
| Channel history prompt projection | not-ported: persistent transport |
| Standing pool lifecycle tests and harness-specific prompts | not-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.