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

jj-acp specification

This book is the canonical behavior contract for jj-acp, a pluggable Agent Client Protocol harness that runs coding agents on behalf of a controlling application. Read architecture, governance, milestones and verification before implementing a slice.

jj-acp is a port of the upstream buzz-acp harness with its Nostr intake, identity, author gating, key custody and relay publication removed. The controlling application is the trust boundary. The pinned upstream source and the per-file disposition of the port are recorded in docs/upstream-lock.md, and adopted-source attribution lives in docs/licenses/NOTICE.md.

The first delivery is a one-shot prepared task runner. Persistent version 1 adds scoped scheduling; explicit version 2 adds warm interactive sessions. The jj-store controlling-application integration remains separately qualified through the progress ledger.

Architecture

jj-acp is a Cargo workspace of layered crates. Each layer owns one concern and may depend only on the layers beneath it. The controlling application sits outside the workspace and owns everything jj-acp deliberately does not: identity, authorization, durable state, repository authority and prompt policy.

controlling app (jj-store, a script, an operator)
        │  versioned task document / control frames, newline-delimited JSON
        ▼
jj-acp-cli ── jj-acp-control ── jj-acp-runner ── jj-acp-agent
                                            │                   │
                                            └── jj-acp-protocol ──┘
                                                        │ stdio JSON-RPC
                                                        ▼
                                                 ACP agent subprocess

The layering is not advisory. scripts/check-boundaries.mjs reads cargo metadata --no-deps --locked and fails on an edge that is not in the declared matrix, on any crate absent from the matrix, and on any excluded dependency. The excluded set encodes the port’s deliberate omissions, so a future re-port that reintroduces Nostr, a transport, a TLS stack or a database fails the gate instead of review.

Requirements

IDRequirement
JAC-ARCH-001The repository MUST be a Cargo workspace of layered crates whose internal dependencies match the matrix enforced by scripts/check-boundaries.mjs.
JAC-ARCH-002jj-acp MUST NOT depend on Nostr, transport, HTTP, TLS or database crates, and the boundary checker MUST fail closed when one appears.
JAC-ARCH-003Production sources MUST stay at or below 300 counted lines and test sources at or below 500, with exceptions only as explicit baseline entries.
JAC-ARCH-004Production modules MUST NOT define test functions inline or import the testkit crate; unit tests MUST use a tests.rs sibling module and integration tests MUST use a crate tests directory.
JAC-ARCH-005The controlling application MUST own runner selection, working directory, credentials and prompt content; jj-acp MUST NOT derive them on its own.
JAC-ARCH-006A crate MUST own its behaviour; the CLI crate MUST compose and MUST NOT reimplement protocol, scheduling or transport logic.

Crate ownership

CrateOwnsMust not know
jj-acp-protocolFrame codec, correlation, capability negotiation, session methods, stop reasons, typed ACP payloadsProcesses, scheduling, credentials
jj-acp-agentRunner definitions, catalog tiers, executable resolution, launch environment merge and reserved-key strippingTurn lifecycle, transport
jj-acp-runnerSubprocess launch, process-group ownership, session and turn lifecycle, deadlines, cancellation, cleanupHow work arrives
jj-acp-controlTask document, terminal record, control transport abstraction and the stdio transportACP framing details
jj-acp-cliArgument parsing, launch configuration assembly, exit codesAny behaviour it can delegate
jj-acp-testkitDeterministic ACP peer fixtures and failure modes for testsProduction use of any kind

Counted-line budget

The budget in JAC-ARCH-003 is a deliberate divergence from jj-store, which budgets only web and script sources and carries Rust files past a thousand lines. Upstream buzz-acp concentrates its ACP client, pool, queue and relay in single files of two hundred kilobytes or more. Porting those files intact would make every future upstream comparison a whole-file diff, so decomposition is a portability requirement rather than a style preference.

scripts/rust-architecture.mjs measures counted lines with blanks and comments excluded, classifies each file as production, test or script, and compares against scripts/rust-architecture-baseline.json. The baseline ships empty. An entry may be added only when an extraction cannot lower a budget, and the gate reports a stale entry whose file no longer exists, so the baseline cannot quietly retain dead exceptions.

Repository tooling

The governance suite includes automation recognition and selective spec-first exemptions for Dependabot and Changesets. This CI setup reads GitHub PR metadata; normal source gates remain offline. Runtime crates and their dependency matrix are unchanged.

Validation is first-party Node automation under scripts/, not a crate. It sits outside the Cargo workspace on purpose: the gates must be able to inspect the workspace as data, read cargo metadata, and fail on a crate that does not exist yet. Each checker exports its decision function so a governance test can exercise it against synthetic input, and each has a thin command-line entry so a lane can invoke it directly.

ScriptOwns
scripts/check-boundaries.mjsCrate layer matrix, excluded dependencies, testkit isolation
scripts/rust-architecture.mjsCounted-line budget, test placement, testkit imports
scripts/check-progress.mjsLedger parsing, evidence integrity, completion gates
scripts/upstream-lock.mjsPort map and lock validation, digests, drift classification
scripts/upstream-sync.mjsPin self-test and the opt-in networked materialization
scripts/governance.test.mjsLedger and boundary regressions
scripts/rust-architecture.test.mjsBudget and placement regressions
scripts/upstream-lock.test.mjsPort map, lock and drift regressions

Tooling changes are protected and mapped to this chapter’s governance owner, so a gate cannot be weakened without a same-change specification edit. Adding a checker means adding its decision function, its tests and its lane together.

Dependency declaration

Third-party versions are declared once in the workspace dependency table and referenced by name from each crate, so a version change is one reviewed edit rather than a sweep across manifests. Cargo.lock is committed and every cargo invocation in a check lane passes --locked.

Each crate enables only the facilities it owns. jj-acp-protocol takes io-util, sync and time from the async runtime and nothing else, because it speaks over a caller-supplied byte stream and has no business spawning a process or opening a socket. jj-acp-agent takes no async runtime at all: resolving a runner is a synchronous filesystem question. Process ownership, signal handling and scheduling arrive with the runner and CLI crates, which is where the corresponding facilities are enabled.

That split is enforced rather than documented. A governance test reads the protocol crate’s manifest and fails if a process or network facility appears in it, and the boundary checker fails on any crate absent from the layer matrix and on any excluded dependency, so a crate cannot quietly acquire a capability its layer does not own.

Layer status

All six crates carry implementation except where the ledger says otherwise. The protocol, agent, runner, control and CLI crates are populated; the testkit crate owns fixture location, launch configuration and log parsing. The dependency matrix in scripts/check-boundaries.mjs is unchanged from the bootstrap slice, which is the point: filling five crates with real code required no new layer edge, so the original decomposition was adequate rather than merely tidy.

The one dependency the matrix does express and the bootstrap could not exercise is the testkit’s. It depends on the protocol and agent crates because it configures a fixture through the same types a production caller uses, and it must never depend on the runner, control or CLI crates, or a test helper would become a composition path around them.

The layering held under implementation without amendment. Five crates were filled with real code across four slices and the dependency matrix recorded at bootstrap needed no new edge, which is the evidence that the decomposition was adequate rather than merely tidy. The one refinement was to the testkit’s own row: it depends on the protocol and agent crates because it configures a fixture through the same types a production caller uses, and it must never depend on the runner, control or CLI crates, or a test helper would become a composition path around them.

Qualifying a real adapter exercised every layer at once and found three defects that no fixture test had reached, in the protocol crate’s capability probing, the CLI’s subcommand option handling and the compiled-in runner declarations. That is the argument for keeping the real-runner gate separate from the fixture suite rather than treating the fixture as sufficient: the fixture proves this code agrees with itself, and only a shipping adapter proves it agrees with anyone else.

Repository identity

The repository, its crates, its binary and its requirement identifiers are named jj-acp and JAC-, after the repository it exists to serve. The npm scope is unchanged: @lapismd/jj-acp sits alongside @lapismd/jj-store, and the crate layer names keep their acp-<layer> suffixes so the owning chapter of any crate is readable from its path alone.

Renaming is a governance event rather than a search-and-replace, because three contracts travel with the name. The launch-option environment namespace moved to JJ_ACP_*, the harness-reserved task identity to JJ_ACP_TASK_*, and the requirement identifier prefix to JAC-. All three are read by other programs: a controlling application that set LAPIS_ACP_MODEL would silently get the default instead, which is why the rename belongs in one atomic change with its ledger entry rather than being spread across slices.

Interactive session integration

Interactive control version 2 is implemented within the existing protocol, runner and control layers. It adds no network, database or identity dependency and retains the one-shot interface.

Qualified controlling application

JAC-ARCH-005 is qualified against jj-store on macOS ARM64 through M10. The host owns authorization, runner selection, exact isolated JJ workspaces, private request journals, sanitized live delivery and AgentStore publication. It consumes the rebuilt executable over explicit control version 2, without crate or sibling source imports. Real Codex/Cursor edits in registered Git-backed JJ folders and native real-RustFS repositories, cold production Chromium/restart and managed owner lifetime are separately recorded in E-M10-001. These live turns raised no permission requests; exact permission/cancellation/deadline proof remains fixture evidence. Packaging, additional platforms/adapters and live permission qualification remain separate from this host profile.

Trusted MCP declaration parsing stays in the CLI composition layer and uses existing Serde data types. Protocol/runner/control receive fixed declarations without introducing any network, authentication, storage or tool-policy dependencies.

Public release preparation

The CLI Cargo package is publicly named jj-acp; its source directory remains crates/jj-acp-cli and its library remains jj_acp_cli. All internal path dependencies also specify a registry version. Each crate packages its own README, full license and attribution. The testkit fixtures and fixture lookup live inside the published crate rather than depending on a repository root.

Registry existence is established by the version metadata endpoint before downloading an archive. An unpublished CDN object can return HTTP 403; that is not a credential failure and must not be used to decide existence. Metadata 404 admits publication, while any error downloading a known version blocks publication and any checksum mismatch still refuses replacement.

Changesets versions the private development coordinator for the six Cargo crates. Its Node dependencies are development tooling and never enter the Rust runtime or npm publication. A version consistency check covers workspace inheritance, internal dependency ranges, Cargo.lock, changelog and installation examples before release preparation or publication.

ACP wire protocol

jj-acp-protocol owns the Agent Client Protocol surface that jj-acp speaks to an agent subprocess over stdin and stdout. It is a hand-rolled JSON-RPC implementation, matching upstream: there is no ACP crate dependency to vendor, and no network transport of any kind.

Framing is newline-delimited JSON with an explicit per-frame byte cap. A frame that exceeds the cap is a protocol error, never a silent truncation and never an unbounded allocation. Bounded reads are what allow the harness to survive a malfunctioning agent that emits an endless line.

Requirements

IDRequirement
JAC-PROTO-001The protocol crate MUST encode and decode ACP JSON-RPC frames over newline-delimited stdio with an explicit per-frame byte cap and MUST fail closed on overflow.
JAC-PROTO-002The client MUST request protocol version 2 at initialize and MUST fall back to version 1 when the agent reports it, recording the negotiated version.
JAC-PROTO-003The client MUST support session/new, session/prompt, session/cancel, session/set_model, session/set_config_option, inbound session/request_permission and session/update.
JAC-PROTO-004A turn MUST terminate in exactly one stop reason drawn from end_turn, max_tokens, max_turn_requests, refusal and cancelled.
JAC-PROTO-005An agent error MUST be surfaced as a typed protocol error preserving the agent’s numeric code, and MUST substitute the whole error object when no message is present so provider detail survives.
JAC-PROTO-006The protocol crate MUST remain free of process spawning, scheduling, credential handling and transport policy.
JAC-PROTO-007Session loading MUST use the advertised loadSession capability and the caller-pinned working directory; permission waits MUST remain bounded by their request and turn lifetimes.
JAC-PROTO-008Inbound session updates and permission requests MUST expose their adapter session identity to caller validation before delivery; default handlers MUST preserve existing single-session behavior.

Method direction

MethodDirectionRole
initializeclient→agentNegotiate protocol version and read agent capabilities
session/newclient→agentOpen a session with working directory and MCP server list
session/promptclient→agentSubmit one prepared turn
session/cancelclient→agentRequest cooperative cancellation of an in-flight turn
session/set_modelclient→agentSelect a model when the agent advertises the capability
session/set_config_optionclient→agentSelect a configuration option such as a reasoning effort
session/updateagent→clientStream turn progress, tool calls and content chunks
session/request_permissionagent→clientAsk the controlling side to approve or refuse an action

Capability negotiation is advisory in one direction only. When an agent does not advertise a capability, a request for it is rejected locally with a reason naming the absent capability rather than being sent and timing out. When an agent advertises a capability the harness does not model, the harness ignores it and records that the session is narrower than the agent.

Port relationship

This chapter governs the ported, decomposed derivative of the upstream ACP client. Upstream concentrates framing, correlation, session setup, model and permission handling and legacy first-turn framing in one 215 KB file. Here those concerns are twelve modules, the largest of which is 228 counted lines, and docs/upstream-lock.md records which upstream region each came from so a re-port can be compared region by region instead of file by file.

Implemented surface

ModuleOwns
frameBounded newline-delimited reader and writer, skip counting
envelopeRequest, notification and response construction, inbound classification, id matching
errorTyped wire failures and agent error extraction
budgetConnection limits and the two-bound turn budget
initializeClient identity, advertised client capabilities, version negotiation
contentText content blocks and MCP server declarations
sessionsession/new, session/prompt and session/cancel payloads and stop reason parsing
config_optionModel and reasoning-effort discovery and selection
updatesession/update classification with raw pass-through
permissionsession/request_permission normalization and response construction
handlerThe inbound-traffic hook and a recording default
clientThe connection: correlation, budgets and servicing agent traffic while awaiting a response

Connection is generic over a reader and a writer rather than owning a child process, which is what allows the whole surface to be tested over an in-memory pipe. Process ownership, respawn and cleanup belong to session and turn lifecycle.

A turn budget carries an idle bound that resets on every frame and an absolute bound that never resets; the shorter remaining bound always wins. Both are clamped to a nonzero floor so a zero value cannot produce a deadline that has already passed and a confusing immediate timeout.

Deliberate divergences

Three behaviours differ from upstream on purpose, and each is a boundary this repository holds rather than an oversight in the port.

Approval is not automatic. Upstream answered every session/request_permission by selecting the option whose kind is allow_once, falling back to reject_once, because it ran unattended in an auto mode. Here the default answer is cancellation and a caller that wants unattended approval must choose it explicitly, select by semantic kind rather than by identifier, and record that it did so. An unanswered approval request must never silently become an approval.

Client capabilities are advertised as absent. This crate implements no fs/read_text_file, fs/write_text_file or terminal/* method, so it advertises all three as false. Upstream also advertised terminal-native authentication and vendor-specific notification extensions; both are omitted because this harness holds no credentials and publishes no telemetry, and advertising a facility that does not exist would only produce requests that fail.

Skipped frames are counted rather than only logged. A blank line is a frame separator and is not counted; an unparseable line is counted and exposed through skipped_frames, so a caller can distinguish an agent that emits stray diagnostics from one that emits nothing but noise. Skipping is tolerance, not acceptance: a stream of only garbage yields no frames and eventually trips the idle bound.

Budget-scoped negotiation

initialize and session/new each have a _within variant taking an explicit turn budget. A caller executing one prepared turn passes the turn’s own budget, so adapter startup and session setup share the idle and absolute bounds with the turn instead of getting a separate unbounded phase. A hung adapter is then reported as the bound that actually elapsed: an adapter that emits nothing trips the idle window, and one that streams without answering trips the absolute cap.

The default variants keep a fixed request limit for callers that have no turn budget yet. This distinction was not free: until it existed, a hanging adapter consumed a fixed 60 second request timeout and reported a deadline rather than silence, which is a different diagnosis and a much slower one.

send_request and await_response are separate operations so a caller can race the wait against a cancellation signal without abandoning the request. The agent still owes an answer for that id, and abandoning it would leave the answer unread. This is safe because the frame reader is resumable: bytes already consumed from the underlying reader survive a cancelled read and complete the same frame on the next one.

What a real adapter changed

Qualification against cursor-agent acp corrected one behaviour and confirmed two design choices.

The correction: advertises tested for presence, and adapters send an explicit false for facilities they do not support. "audio": false therefore read as advertised, which would have sent audio content to an agent that had already said it cannot take it. A boolean leaf is now honoured as a boolean; a non-boolean, non-null leaf is an object or marker whose presence is the advertisement, which is how loadSession: true and mcpCapabilities: { http: true } continue to read.

The first confirmation is version negotiation. The client asks for protocol version 2 and this adapter answers 1. Supporting both is not defensive over-engineering; it is the only reason the exchange worked at all.

The second is the dual option identifier. The ACP schema says configId and this adapter sends id, exactly as upstream recorded for a different adapter. Accepting both is what makes its 43 model candidates selectable rather than invisible.

The captured initialize and session/new payloads are committed as a test fixture, so these assertions are re-checked against real adapter output on every run rather than against a synthetic payload that only proves the parser agrees with itself.

Two adapters, two shapes

Both captured payloads are committed as fixtures, and they disagree in ways that matter. Neither is a superset of the other, so parsing has to be driven by what a session actually advertises rather than by an expected shape.

Surfacecursor-agent acpcodex-acp 1.13.1
Negotiated protocol version11
agentInfoabsentname, title and version
Option identifier keyidid
Model valuesparameterized, bracketedbare names
thought_level optionabsentpresent, as reasoning_effort
Session modesagent, plan, askread-only, agent and others
MCP transportshttp and ssehttp only
Extra capabilitiessessionCapabilities.listauth.logout, providers, _meta.authStatus, resume, fork, subagents

Three consequences are load-bearing.

Capability flags must be read as booleans, not as presence, because both adapters send explicit false for facilities they do not support and one of them sends it for an MCP transport. Option identifiers must be discovered per session, because reasoning_effort is a name no parser would guess and Cursor has no equivalent option at all. Model values must be matched exactly against the advertised candidates, because a bare name is valid for one adapter and meaningless for the other.

Both adapters also emitted a notification that is not part of the exchange being waited for — Codex sends _auth/status_update during the handshake. A client that read only responses would stall here, which is why unmodelled notifications are routed to the handler and skipped rather than treated as a protocol violation.

Normalized matching without a closed vocabulary

The thinking module normalizes reasoning effort for matching only. It defines a portable level set — off, low, medium, high, xhigh — plus the attribute keys that carry a level in real adapter output: effort, reasoning_effort, variant, and thinking as a weaker marker that reasoning happens without naming how much.

It is deliberately not an enum that constrains what can be selected. A real adapter advertises max and ultra, which no portable vocabulary names, so a closed type would make them unreachable. effort_matches therefore tries an exact comparison first and falls back to normalization, and the value sent back is always the adapter’s own spelling.

Parameterized model identifiers are parsed rather than string-matched: model_id_base separates the name from its parameters, model_id_attrs reads the key-value pairs, and advertised_effort applies a fixed key precedence so model[thinking=true,effort=high] reports the level regardless of the order the adapter emitted the attributes. A malformed or keyless bracket yields nothing rather than a guess.

Interactive session integration

Interactive loading calls session/load only after loadSession was advertised. Loading never prompts. Permission callback waits remain inside the overall request bound, including when no new frame arrives.

Warm worker handlers validate sessionId before inbound session updates or permission requests are delivered. Single-session handlers default to accepting the existing traffic. A scope mismatch is a protocol failure, never permission approval or activity reassignment.

Version 3 ignores delayed updates from exact known inactive adapter sessions and automatically cancels their permission requests. Unknown or mismatched active identities still invalidate the worker; inactive traffic never enters the current application transcript. Only fixed failure classifications and numeric agent codes are exposed, never untrusted diagnostic details.

Prepared prompt context is an ordered content block independent of authored text. Embedded resource support is selected from negotiated capabilities by the caller. The protocol helper emits an inline text resource or separate text block, then the unchanged authored text. It performs no URI or file access.

Trusted coding-tool declarations

The controlling application may select a bounded JSON file of stdio MCP declarations through --tools or JJ_ACP_TOOLS. This is trusted launch configuration, independent of tasks and v2/v3 control input. Session creation and supported loading receive the same declarations. Warm initialization does not start a tool session. The file is bounded to 64 KiB and eight unique stdio servers; only absolute executable paths are accepted, with bounded arguments/environment and no reserved harness identity names. Errors disclose stable codes, not file contents or credentials. ACP continues to own no tool policy, authentication, network or durable settings.

Public release packaging

The published protocol crate includes registry-versioned dependencies and its own README, Apache-2.0 license and upstream notice. Wire contracts and framing limits are unchanged by packaging.

Runner catalog

A runner is an external ACP-speaking agent that jj-acp can spawn. Runners are data, not code: a catalog entry names an executable, its arguments, an environment floor, an MCP transport and an install hint. Adding support for a new agent MUST be a catalog change.

The catalog is tiered so that trust decreases as flexibility increases, matching the model the upstream harness uses and the model @lapismd/ai-host already exposes to Lapis consumers.

TierSourceEditableAuto-install
1Compiled-in defaultsNoNever
2Catalog file supplied by launch configurationNoNever
3Caller-supplied definition in a task or control frameNoNever

No tier may auto-install. Availability is determined by resolving the declared executable, and an unavailable runner reports its install hint rather than attempting a download or executing a shell command.

Requirements

IDRequirement
JAC-AGENT-001A runner MUST be declared as data carrying identifier, label, command, arguments, environment floor, MCP transport and install hint.
JAC-AGENT-002Adding a runner MUST NOT require a code change, an install shell command or a network fetch.
JAC-AGENT-003Reserved harness environment keys MUST be stripped before a definition or caller override is merged, and MUST NOT be settable by any tier.
JAC-AGENT-004A disabled or unknown runner identifier MUST fail resolution and MUST NOT fall back to a default runner.
JAC-AGENT-005Runner identifiers MUST be validated against a fixed pattern and MUST be unique within a resolved catalog.
JAC-AGENT-006The launch environment MUST be assembled from the inherited environment, the definition floor and caller overrides in that precedence order.

Reserved environment

The harness injects identity and correlation variables into a spawned agent so that tools it provides can attribute work correctly. Those names are reserved. A runner definition, a catalog file or a caller-supplied override that sets one is stripped before merge, and the strip is recorded so the operator can see that a definition attempted to claim harness identity.

This is the only place jj-acp performs anything resembling an authorization check, and it is a namespace guard rather than an identity system. There is no keypair, no signature, no token and no peer credential anywhere in this repository. The controlling application decided to run this runner with this environment; jj-acp only refuses to let the runner definition contradict that decision.

Default catalog

The compiled-in tier names the adapters that are already in use elsewhere in the workspace, so an operator who has one of them installed needs no configuration. Identifiers are stable and reserved: a lower tier cannot shadow them.

IdentifierAdapterMCP transport
codex@agentclientprotocol/codex-acpstdio
claudeclaude-agent-acp, legacy claude-code-acpstdio
opencodeopencodestdio
goosegoose acpstdio
cursorcursor-agent acphttp

A runner that speaks ACP over stdio and satisfies the protocol requirements in ACP wire protocol works without being listed here. The catalog exists for discovery and labelling, not as an allowlist.

Implemented surface

ModuleOwns
definitionRunnerDefinition, CommandSpec, McpTransport, identifier and label validation
catalogTiers, collision rules, resolution and listing
defaultsThe compiled-in tier and its reserved identifiers
configCatalog document parsing
environmentReserved-key stripping and three-layer merge
availabilityThe CommandProbe trait, a PATH probe, a fixed probe and availability reports
errorTyped refusal reasons

A candidate argv is a CommandSpec, and a definition may list several in preference order. The first whose executable resolves wins, which is how one definition covers both a current and a legacy adapter command name without inventing two runners.

Resolution refuses rather than substitutes. An unknown identifier and a disabled one are distinct errors, and neither falls back to a default, because silently running a different agent than the one requested would execute untrusted prompt content with capabilities the caller did not choose.

Collisions are refused for the whole batch rather than skipping the offending entry. A catalog file that fails to load therefore cannot produce a quietly smaller runner set than the operator wrote.

CommandProbe is a trait so availability is testable against a synthetic search path. The real probe only tests whether a file exists; it never executes the candidate to see whether it works, because executing an untrusted binary to probe it would be worse than not probing at all.

Environment precedence

The three layers apply in order: the inherited environment, the definition floor, then caller overrides. A later layer replaces an earlier one for the same name, and the first appearance fixes its position so the result is deterministic.

Every layer is stripped of names under the JJ_ACP_TASK_ prefix before merging, and the stripped names are returned so an operator can see that a definition tried to claim harness identity. The harness then adds its own reserved variables through a separate function that no catalog file and no caller override can reach. The prefix is matched rather than an enumerated list, so a future harness variable is covered without a rule change.

Qualification status

The compiled-in entrypoints are declared from the published adapter package names but are not yet qualified against installed adapters. Declaring an argv is not evidence that it launches, which is why the real installed-runner gate is a separate ledger task and why an unqualified default is a convenience rather than a claim.

Qualified entrypoints

The default catalog table above is a convenience, not a claim. This records which entrypoints have actually been launched and spoken to.

IdentifierEntrypointQualified against
cursorcursor-agent acp2026.09.26-dd393fe, two completed turns
codexcodex-acp1.13.1, one completed read-only turn
claudeclaude-agent-acpnot launched
opencodeopencode acpexecutable present, entrypoint unverified
goosegoose acpexecutable present, entrypoint unverified

The codex entry needs a qualification of its own. The codex CLI is not an ACP agent and has no acp subcommand; the ACP surface is a separate adapter package, @agentclientprotocol/codex-acp, which is how ai-host reaches it through acpx. That launcher uses npx -y, which fetches the package into the npm cache on demand and never puts it on PATH. So codex-acp can be present and working on a machine where command -v codex-acp finds nothing, and this harness’s compiled-in declaration resolves only when the binary really is on PATH. A catalog entry naming npx with the adapter in its arguments is the portable form.

opencode and goose report available: true on a machine that has them installed, and that means only that the executable exists. Probing never runs a candidate, because executing an untrusted binary to see whether it works is worse than not probing. Their ACP entrypoints remain unverified.

Public release packaging

The published agent crate includes registry-versioned dependencies and its own README, license and notice. Runner selection, environment precedence and reserved IDs retain their existing contracts.

Session and turn lifecycle

jj-acp-runner executes prepared turns. It does not decide what to run, when to run it, or what to do with the result; intake and disposition belong to jj-acp-control and to the controlling application above it.

A session scope is the unit of serialization. One scope has at most one turn in flight. Distinct scopes may run concurrently when more than one agent subprocess exists. This mirrors the upstream per-channel guarantee without inheriting its channel concept: the controlling application names the scope, and jj-acp treats it as an opaque key.

Requirements

IDRequirement
JAC-RUN-001The runner MUST execute one prepared turn per session scope with at most one turn in flight per scope.
JAC-RUN-002Cancellation MUST be cooperative and bounded: request cancellation, wait a bounded grace period, then kill the process group and reap within a bounded interval.
JAC-RUN-003An idle timeout reset by agent output and an absolute wall-clock turn deadline MUST both apply, and the shorter remaining bound MUST win.
JAC-RUN-004A timeout MUST be reported as an unknown outcome and MUST NOT be recorded as success or as a proven failure.
JAC-RUN-005A crashed agent MUST be reported as a distinct outcome from a bound that elapsed, and a restart MUST NOT silently re-dispatch a turn that was already sent.
JAC-RUN-006Session creation MUST apply a configured model, reasoning effort and execution mode where the adapter advertises them, MUST resolve each against the current session rather than a cached one, and MUST retain the adapter default rather than fail the turn when a setting is unsupported or unavailable.
JAC-RUN-007On Unix, cleanup MUST signal the whole process group before reaping its leader so ordinary descendants cannot outlive the turn, and MUST report whether the stop was confirmed.
JAC-RUN-008Interactive sessions MUST negotiate once, apply configuration before prompting, load only advertised sessions, preserve cancellation and absolute deadlines during permission waits, and close their process ownership within bounded intervals.
JAC-RUN-009Warm workers MUST negotiate once, preserve distinct session configuration, invalidate all sessions after uncertain execution, never replay work, and bound both active and lifetime session counts.

Deadline composition

Two independent bounds apply to a turn. The idle timeout resets on any agent stdout activity, so a long-running but visibly progressing turn is not killed. The absolute turn duration never resets, so a turn that emits output forever still ends. A task document may lower the absolute bound; it can never raise it above the launch configuration cap, because the document is untrusted input and the cap is trusted policy.

Deadline accounting covers adapter startup, session setup and the turn itself. There is no separate unbounded startup phase. A launch configuration read has its own short bound and reports a distinct outcome, because a blocked configuration read must not be indistinguishable from a blocked agent.

Cancellation and cleanup

Cancellation is a sequence, not an event. The runner requests cooperative cancellation through the protocol, waits a bounded grace period for the agent to return a stop reason, then kills the process group and waits a further bounded interval to reap it. Each stage reports what actually happened.

Cleanup is best-effort and this is stated rather than hidden. A harness killed with SIGKILL, or a machine that loses power, cannot run cleanup at all. A descendant that deliberately escapes its process group is outside the guarantee. An unreaped child is reported, not assumed stopped.

Descendant containment is a Unix property. On other platforms cleanup addresses the direct child only, and the reduced guarantee is reported rather than silently assumed to be equivalent.

Recovery

JAC-RUN-009 distinguishes a valid RPC rejection of session preparation from a failed connection. Rejected open/configure reports a classified error without retiring unrelated warm scopes. Uncertain prompt execution still invalidates the worker, with no replay or automatic fallback.

A turn that ends without a stop reason is an unknown outcome. The runner records what it observed and stops. It does not retry, because a retry can execute the same side effects twice, and it does not fabricate a terminal state to make the record tidier. Recovery from an unknown outcome is the controlling application’s decision, made with its own durable state.

Implemented surface

ModuleOwns
launchLaunchSpec, spawning with piped stdio, inherited stderr and a Unix process group
processProcessGuard, bounded grace, process-group kill, bounded reap, Termination
turnAgent, TurnRequest, and run_turn for one negotiated, prompted, stopped turn
outcomeTurnOutcome, TurnStatus, Failure and the protocol error classification

The child’s stderr is inherited rather than piped, so agent diagnostics reach the operator directly and can never be mistaken for protocol frames on stdout.

Failure carries a classified kind and an untrusted detail. Only the kind and the agent’s numeric error code may be published; the detail is diagnostic text for stderr and can quote prompt content or a credential the agent was given. The split is structural, so sanitization cannot be forgotten by a later caller.

A timeout is classified by which bound elapsed, because the two mean different things. An idle timeout says the agent went silent. An absolute timeout says the cap was reached, possibly while the agent was still visibly working. Both are unknown outcomes, and neither is retried.

The descendant hazard

Killing the agent’s direct child does not stop a child the agent spawned. The fixture peer’s descendant mode proves this rather than asserting it: after the direct child is killed the recorded descendant pid is still alive. That is why ProcessGuard kills the process group on Unix, and why Termination reports whether the stop was confirmed instead of assuming it.

Termination::confirmed_stopped is false when the reap bound elapsed first. A caller that needs certainty must treat that as an unknown outcome and report it, which is what the control layer does through its diagnostics rather than through the terminal record.

Orderly shutdown before a kill

Termination is the fallback, not the first move. When a turn ends the agent’s stdin is released before the grace window starts, because closing a pipe write end is what produces the end of input a well-behaved agent waits for. Flushing is not sufficient: an async shutdown on a child’s stdin leaves the descriptor open, so an agent blocked reading it never learns the exchange is over.

The observable difference is that a completed turn now stops its agent without a kill. That matters for a real adapter that flushes session state on exit, and it removes a misleading diagnostic from the normal path: a group kill is reported because it is worth knowing about, and reporting it after every successful task would train an operator to ignore it.

Releasing the connection also means the process guard is the only remaining owner, so the cleanup sequence cannot race against a further protocol write.

Signal ordering and why it is not interchangeable

A process group id is only unambiguously ours while the group leader is unreaped, because a pid cannot be recycled while its process is still a zombie. Both group signals are therefore sent before, or in the absence of, any successful reap: a cooperative SIGTERM to the group first, then a bounded wait, then SIGKILL to the group only when the leader is still outstanding. Reaping first and signalling after would risk killing an unrelated group whose leader happens to have taken the recycled pid.

The cooperative group signal is what makes containment hold on the graceful path too. Closing an agent’s stdin lets it exit on its own, but a child it spawned has no reason to notice, and an agent that exits voluntarily leaves that child running. Signalling the group before the grace window means a descendant is stopped whether the agent cooperated or had to be killed.

The residual case is stated rather than hidden: a descendant that ignores SIGTERM, whose leader then exits voluntarily inside the grace window, is not reached, because at that point the leader is reaped and the group id is no longer safely ours. Containment is guaranteed for the fatal path and best-effort for the cooperative one. Termination reports which happened through group_signalled, group_killed and escalated, so a caller can tell them apart instead of assuming.

A side benefit is speed. Agents that previously sat out the whole grace window after a completed turn now stop on the cooperative signal, which cut the task contract suite’s wall clock by roughly a factor of six without weakening any assertion.

Crash is not a timeout

An agent that exits mid-turn and an agent that stops answering are different facts with different consequences, and they get different records. A crash is failed with agent_exited and exit 1: the agent is gone, so its state is knowable and a caller may decide to start a new one. A bound that elapsed is timed_out with exit 124: the agent may still be running and still producing side effects, so its state is unknown.

Neither is retried. The distinction exists so a controlling application can make that decision itself with its own durable state, not so this harness can make it automatically. Re-dispatching a turn whose outcome is unknown can execute its side effects twice, which is why the crash tests assert the dispatched prompt count is exactly one rather than merely asserting the record.

A crash during startup is reported the same way and is distinguishable by what never happened: no session/new and no session/prompt reached the agent, so nothing was dispatched and nothing can have partially executed.

Session configuration

Three settings are applied after session/new and before the turn: model, reasoning effort and execution mode. Each is resolved against the current session’s advertisement, never against a cached catalogue or a per-adapter table, because the surface a given adapter exposes varies — including between two sessions of the same adapter version.

ModuleOwns
configurationSessionConfiguration, SettingOutcome, ConfigurationReport and apply

Order matters. The model is selected first, because an adapter rebuilds its option catalogue for the new model and may expose a different reasoning-effort identifier afterwards; the capabilities used for effort and mode resolution are taken from the switch response when it carries any, and only fall back to the original session result. Applying effort before the model would resolve it against a catalogue the switch is about to invalidate.

Configuration error classification

One distinction governs all three settings, and it is the reason a preference is safe to request at all.

A transport-class failure — I/O, write timeout, request timeout, protocol violation, agent exit — aborts and propagates, because the stream may be corrupt and continuing to speak over it would produce nonsense that looks like a result. An application-level rejection, meaning the adapter answered with a JSON-RPC error, is recorded and the session continues on the adapter’s own default.

The distinction is what keeps a preference from being able to fail a task. Asking for a model an adapter does not have is a question, not a fault, and treating it as one would punish a caller for asking.

Outcomes are classified rather than collapsed, because they send an operator looking in different places: unsupported means the adapter has no such control, unavailable means it has one that does not offer this value, rejected means it was asked and refused, and not-this-setting means the value exists under a different category so applying it here would have been a guess.

Effort has three encodings in the wild

Reasoning effort is not one mechanism. Across two adapters and three observed sessions it appeared as:

  1. A separate configuration option in the thought_level category, with an adapter-defined identifier that must be discovered.
  2. An attribute inside a parameterized model identifier, under any of effort, reasoning_effort or variant, where applying it means selecting a different model.
  3. A bare positional bracket token such as [medium], which is not read as an effort.

The third is refused deliberately. The token usually is a reasoning level, so a closed vocabulary would make the inference mostly right — and mostly right is the worst possible property here, because the failure mode is silently selecting a model the operator never asked for. Matching only what an adapter explicitly keyed keeps the behaviour predictable, and the shortfall is reported so the operator learns the adapter has no settable effort rather than wondering why nothing changed.

Matching is exact first and normalized second. Exact match is what keeps adapter-specific levels selectable when they exist in no portable vocabulary; normalization is what lets a caller asking for off reach an adapter that advertises none. The value sent is always the adapter’s own spelling.

Interactive session integration

Interactive open and load retain their agent connection. Follow-up prompts reuse the same session and serialized process; close, protocol loss and timeout release process ownership. Configuration and permission handling use bounded application callbacks rather than the accumulating one-shot recording handler.

Queued prompt cancellation in explicit control v2 reports sessionUsable while retaining the warm session. The next explicit follow-up reuses it; connection shutdown still closes it. Version-1 command and terminal record shapes remain unchanged. E-M11-003 records the binary regression and source gates.

Warm worker initialization is independent of session creation. Serial sessions share one initialized connection but retain separate negotiated mode state. Cancellation, absolute deadlines and bounded process-group cleanup apply to opening and every turn. Unknown outcomes invalidate every session; no implicit restart or prompt replay occurs. Multi-session fixture qualification is separate from qualification of each real installed adapter.

Interactive PromptInput retains authored bytes separately from caller-prepared context. One turn uses ordered context and text blocks, with embedded-resource selection supplied from the negotiated session. Existing text-only prompt callers, deadlines, cancellation, quiescence and unusable-session outcomes are unchanged.

Trusted coding-tool declarations

The controlling application may select a bounded JSON file of stdio MCP declarations through --tools or JJ_ACP_TOOLS. This is trusted launch configuration, independent of tasks and v2/v3 control input. Session creation and supported loading receive the same declarations. Warm initialization does not start a tool session. The file is bounded to 64 KiB and eight unique stdio servers; only absolute executable paths are accepted, with bounded arguments/environment and no reserved harness identity names. Errors disclose stable codes, not file contents or credentials. ACP continues to own no tool policy, authentication, network or durable settings.

Public release packaging

The published runner crate includes registry-versioned dependencies and its own README, license and notice. Native release tests retain bounded process-group cleanup, signal and unknown-outcome contracts.

Controlling application boundary

jj-acp has no message bus, no relay, no subscription and no author gate. Work arrives from a controlling application that spawned the process, over stdin, and results leave over stdout. There is no listener, no port, no token and no authentication, because there is no peer: the parent process is the trust boundary and it already decided to run this executable with this environment.

This chapter replaces the upstream relay intake entirely. The upstream author gate, membership discovery, owner resolution and workflow-provenance checking have no analogue here and are not partially reimplemented.

Requirements

IDRequirement
JAC-CTRL-001The controlling application MUST drive jj-acp over a versioned newline-delimited JSON control protocol with no authentication and no network listener.
JAC-CTRL-002jj-acp MUST accept one prepared task document of at most 1 MiB from a file or stdin and MUST reject unknown fields, duplicate fields, wrong types, trailing documents and malformed JSON.
JAC-CTRL-003Every attempted invocation MUST emit exactly one terminal record on stdout and MUST NOT include the prompt, credentials, agent final text or raw adapter errors.
JAC-CTRL-004Diagnostics and child stderr MUST stay on stderr so that stdout remains unconditionally parseable by the controlling application.
JAC-CTRL-005The task document MUST NOT change the working directory, executable, tools or credentials, which come only from trusted launch configuration.
JAC-CTRL-006A control frame MUST carry an explicit integer version, and an unsupported version MUST be rejected before any agent subprocess is started.
JAC-CTRL-007Reading a task document from a file or stdin MUST have its own bounded interval, separate from the execution deadline.
JAC-CTRL-008Interactive control version 2 MUST retain one caller-configured ACP session across ordered turns, stream correlated bounded activity and permission requests, and accept only current permission decisions without changing the version-1 record contract.
JAC-CTRL-009Explicit control version 3 MUST initialize a worker without opening or prompting, serialize bounded caller-scoped sessions under its launch directory, and fence cancellation, activity and permissions by exact session identity.

Task document, version 1

One UTF-8 JSON object, at most 1 MiB including whitespace, supplied as a path or as - for stdin. Stdin MUST be closed after the document.

{
  "version": 1,
  "taskId": "example-001",
  "agent": "codex",
  "prompt": "Inspect the working copy and report findings.",
  "maxDurationMs": 120000
}
FieldContract
versionInteger 1. Any other version fails before agent launch.
taskIdNonblank string, 1 to 256 UTF-8 bytes, no control characters. Correlation only; not a deduplication key and not a claim.
agentRunner identifier that MUST resolve to an enabled catalog entry. This is a mis-execution guard, not an authorship check.
promptNonblank prepared task text. Context, evidence and reporting instructions belong here.
maxDurationMsPositive integer. The shorter of this value and the trusted absolute cap wins.

All fields are required. The document is executable task input rather than a sandboxed message, so only supply tasks trusted with that runner’s capabilities.

Terminal record

One JSON line per attempted invocation. taskId is null until the document validates. status is completed, failed, invalid, timed_out or cancelled. stopReason appears only on normal completion. Failures carry a stable sanitized error code such as invalid_task_json, agent_spawn_failed, runner_mismatch or configuration_timeout.

{
  "version": 1,
  "taskId": "example-001",
  "status": "completed",
  "stopReason": "end_turn"
}

A record is not a durable ledger. A killed harness or a broken stdout can leave no record at all, and launching the same task twice can execute it twice. Durability belongs to the controlling application.

Transport pluggability

The control transport is a trait with one implementation in the first delivery: newline-delimited JSON over the inherited stdin and stdout. A controlling application that wants a Unix socket, a local HTTP endpoint or an in-process link adds a transport rather than editing the runner. The trait boundary is the reason the one-shot task runner and a future persistent service can share the same session and turn machinery.

Implemented surface

ModuleOwns
taskDocument version 1, strict validation, the stable rejection code vocabulary
inputBounded reads from a path or stdin, capped while reading rather than after
recordTerminalRecord, Status, ExitCode and the record-level outcome codes
invocationResolving a runner, launching, executing one turn, stopping, and reducing to a record
transportThe ControlTransport trait and the stdio implementation

Rejection happens before any agent is spawned. A document that names an unknown runner, a version this build does not accept, or a runner other than the one this process was launched as never reaches a launch attempt, and the fixture log stays empty for every refusal case.

Reserved namespace

Only the task identity the harness injects is reserved, under the JJ_ACP_TASK_ prefix. An earlier design reserved the whole JJ_ACP_ tree and the fixture peer caught the consequence: it also stripped the launch options an operator sets on purpose, such as JJ_ACP_RUNNER and JJ_ACP_MODEL, breaking configuration in the name of protecting identity. Reserving narrowly is still sufficient, because the only thing a runner definition must not be able to forge is which task the harness says it is running.

Exit code resolution

The invocation reports the exit code for every outcome except a signal-driven cancellation, where it sets a flag instead. The CLI owns signal handling and therefore owns the choice between 130 and 143, because only it observed which signal arrived. Keeping that decision out of the control crate is what lets a controlling application drive the same invocation over a different transport and apply its own interruption semantics.

Configuration shortfalls are diagnostics, not record fields

A setting that did not land is reported on stderr with its classified reason and, for an adapter rejection, the numeric error code. It is never added to the terminal record. Two reasons: the record schema is fixed so a controlling application can parse it unconditionally, and an adapter’s own rejection text is untrusted and can quote prompt content, so only the classification and the code are safe to publish.

The turn outcome still carries the full configuration report, so a caller embedding the crate can act on each setting individually. The one-shot CLI reduces it to diagnostics because its contract is one record and one exit code.

The persistent control protocol

jj-acp serve reads newline-delimited JSON frames on stdin and writes frames on stdout. Version 1 carries four requests and six responses.

RequestFieldsAnswered by
submitrequestId, taskone record frame, plus diagnostic frames
cancelrequestId, targetack, or error with not_in_flight
runnersrequestIdrunners
shutdownrequestIdack, then the loop drains and exits

Every frame carries version, and the harness opens each session with a ready frame carrying the resolved runner list, so a controlling application learns what it can ask for before asking. An unsupported version is refused before any agent is started, which is what makes the version field meaningful rather than decorative.

A refused task document is delivered as a record frame with status: invalid rather than as an error frame. One shape for every task outcome means a controlling application handles success, failure and refusal through the same code path, and cannot miss a refusal because it was only listening for records. Frame-level problems — bad JSON, a missing version, an unknown type — are error frames, because there is no task to report against.

Diagnostics travel as diagnostic frames rather than stderr. The one-shot path writes them to stderr because a human is reading it; the persistent path has a machine on the other end, and a machine that has to watch two streams to know what happened will eventually miss one. Stdout still carries nothing but frames.

Concurrency, scopes and the queue

Submissions carry a caller-named scope. A scope has at most one turn in flight; distinct scopes run concurrently up to the worker limit; a bounded queue absorbs bursts and refuses admission when full rather than growing without limit.

The scope is opaque. Upstream scoped by channel and thread because a relay has those concepts. Here the controlling application decides what a scope means and this harness only guarantees non-overlap within one, which is why the default scope exists: a peer that never names one gets the serial behaviour it had before scopes were introduced.

Queue selection is first-fit rather than strictly FIFO, because a submission whose scope is busy must not block an unrelated scope behind it. Order within a scope is preserved, which is the guarantee that actually matters.

A submission queued behind its own scope is not refused. Serializing a scope means the second turn waits, not that it is rejected, and admission is acknowledged with a queued frame so a peer can tell a waiting submission from a dropped one.

RequestFieldsAnswered by
submitrequestId, scope?, taskqueued, then one record, plus diagnostics
cancelrequestId, targetack, or error with not_in_flight
runnersrequestIdrunners
statusrequestIdstatus
shutdownrequestIdack, then drain and exit

status reports the configured worker count and queue bound alongside the live queue depth and the scopes with a turn in flight, so a peer can distinguish “your work is waiting” from “this harness cannot take more work”.

Ordering between a record and the state it implies

A finished submission routes its frames back through the loop rather than emitting them from the task that produced them. The loop releases the scope first and writes the record second.

Emitting directly from the task looked correct and was not: a peer could observe a record and then ask for status that still listed that submission as running, because the loop had not yet processed the completion. That is a contradiction a controlling application has no way to resolve, and it is invisible in a test that does not ask the question.

The transport seam

The loop depends on two traits rather than on stdio: a FrameSource it owns and a FrameSink it shares. They are split because a persistent server has one reader and many writers, and modelling that as one bidirectional object would force both sides to share a lock only the writers need.

An earlier shape of this trait modelled the one-shot path — read a task document, emit a record — and nothing used it, because the persistent protocol exchanges requests and responses rather than documents. It was replaced with the seam the loop actually needs, and the scheduler is now tested in process over an in-memory pair as well as through the spawned binary. A seam that no caller passes through is a claim rather than an abstraction.

Sources return raw lines and sinks write whole lines atomically. A transport that dropped or rewrote a frame it disliked would hide the very thing a peer needs to hear about, and an interleaved frame is indistinguishable from a corrupt one.

Crashes and slots

A worker slot is held by a permit for the whole turn, so releasing it on drop is what makes a crashed agent give its slot back. The crash is reported as agent_exited and the submission is never re-dispatched: a turn whose outcome is unknown may already have produced side effects, and running it again is the controlling application’s decision to make with its own durable state.

Shutdown cancels running submissions and waits for slots to return, bounded by the same cleanup intervals a single invocation uses. When the bound elapses first that is reported as a diagnostic rather than concealed, because a peer told the shutdown was clean has no way to detect an orphan.

Interactive control version 2

One trusted launch owns one session, runner and working directory. Frames carry version 2 and requestId. Requests are open (optional loadSessionId), configure (model, effort, mode), prompt (text, optional context, maxDurationMs), cancel (target), permission (permissionId, optionId or null), status and close. Unknown fields and oversized frames are refused. No request accepts a path, executable or environment.

Responses are ready, queued, opened, configured, activity, permission, record, status, ack, error and closed. Every activity has an increasing sequence and request identity; content is separate from terminal records. The parent owns redaction and durable history. Session and raw adapter capabilities are facts, not publication authority. Configuration reports expose classified outcomes; unsupported requested modes prevent prompting until corrected.

Permission options retain the exact adapter identifiers. Decisions are valid only for that live permission and its advertised options. Timeout, cancellation, EOF and close cancel pending permissions. Queue and event capacities are bounded; output backpressure or overflow ends execution with an explicit unknown outcome. A control connection never retries work. Coverage loss remains latched after the output queue drains. A racing completed turn cannot report success or a reusable session after any activity frame was lost.

Canceling an admitted prompt while it is still queued reports cancelled without discarding a warm session. Its terminal frame explicitly reports sessionUsable; an individual queued cancellation retains usability, while connection shutdown does not. The next explicit prompt can reuse the retained session.

Warm worker control version 3

A trusted launch fixes one runner and a managed directory root. warm only spawns and initializes; it sends neither session/new nor a prompt. open carries an opaque application sessionId and an existing cwd whose canonical path is inside that root. It optionally loads an advertised adapter session. Executables, environment and arbitrary paths remain launch policy.

Configure, prompt, cancel and permission require the application sessionId. Every session response includes that ID; opened additionally includes the adapterSessionId. Inbound updates and permissions from a different adapter session fail the worker. Status and close address the whole worker. close_session releases application participation without stopping other sessions. There is no provider session-delete claim. At most 64 sessions may be opened over one worker lifetime, preventing retained provider sessions from growing without bound. One turn runs at a time; queue capacity and request identity limits match v2. A valid RPC rejection during open/configure reports agent_rejected for that request and preserves the warm worker and other scopes. It does not imply process failure or uncertain execution. Protocol failure, process exit, timeout or interrupted execution closes the worker and invalidates all scopes. The application owns generation fencing, durable identities, history and explicit continuation. No prompt is retried by this protocol.

Version 3 ignores delayed updates from exact known inactive adapter sessions and automatically cancels their permission requests. Unknown or mismatched active identities still invalidate the worker; inactive traffic never enters the current application transcript. Only fixed failure classifications and numeric agent codes are exposed, never untrusted diagnostic details.

Version-2 and version-3 prompts allow empty authored text only when bounded prepared context is nonblank, enabling comments-only submissions without dummy prompt text. Both text and context empty or whitespace remains invalid. Version-2 and version-3 prompts accept bounded optional context separately from nonempty authored text. This data cannot alter launch policy. A single session/prompt sends the context first and authored text last. An advertised promptCapabilities.embeddedContext selects a text resource with an opaque context URI and text/plain MIME type; absent or false capability uses a separate text block. Context is never fetched from the URI. No preliminary turn, retry or session-configuration side effect is introduced. Existing text-only callers retain their wire shape and lifecycle.

Trusted coding-tool declarations

The controlling application may select a bounded JSON file of stdio MCP declarations through --tools or JJ_ACP_TOOLS. This is trusted launch configuration, independent of tasks and v2/v3 control input. Session creation and supported loading receive the same declarations. Warm initialization does not start a tool session. The file is bounded to 64 KiB and eight unique stdio servers; only absolute executable paths are accepted, with bounded arguments/environment and no reserved harness identity names. Errors disclose stable codes, not file contents or credentials. ACP continues to own no tool policy, authentication, network or durable settings.

Public release packaging

The published control crate includes registry-versioned dependencies and its own README, license and notice. Task/record v1 and controls v1/v2/v3 keep their existing shapes; version 3 remains experimental.

CLI and exit codes

The jj-acp binary is the only executable in this repository. It composes the crates and owns nothing else: argument parsing, launch configuration assembly, signal handling and the process exit code.

The first delivery is the one-shot prepared task runner. A persistent service mode is specified but not implemented, and the ledger records it as deferred rather than implying parity.

Requirements

IDRequirement
JAC-CLI-001The binary MUST expose run --task <path|-> executing exactly one prepared task in one fresh agent process and session, then exit.
JAC-CLI-002Exit codes MUST be 0 normal completion, 1 startup, execution or output failure, 2 invalid arguments, launch configuration or task input, 124 deadline exceeded, 130 SIGINT and 143 SIGTERM.
JAC-CLI-003SIGINT and SIGTERM MUST trigger bounded cleanup before exit and MUST NOT leave an unreaped child when cleanup succeeds.
JAC-CLI-004Every launch option MUST be settable by a command-line flag and by a matching environment variable, with the flag taking precedence.
JAC-CLI-005The binary MUST NOT write diagnostics, prompts or agent output to stdout on any path, including startup failure and signal handling.
JAC-CLI-006A task run MUST NOT start a subscription, a standing pool, a heartbeat or any other service facility, and inherited service-only options MUST NOT enable one.
JAC-CLI-007serve –control-version 2 MUST select the interactive protocol explicitly while the default and one-shot commands retain version-1 behavior.
JAC-CLI-008serve –control-version 3 MUST explicitly select warm worker control; versions 1 and 2 MUST retain their existing contracts.

Commands

CommandRoleStatus
jj-acp run --task -Execute one prepared task document read from stdinPlanned
jj-acp run --task PATHExecute one prepared task document read from a filePlanned
jj-acp agentsList resolved runners with availability and install hintsPlanned
jj-acp servePersistent control transport with a standing session poolDeferred

Exit code 0 means the turn returned normally. It is not proof that the task succeeded; the caller MUST inspect stopReason. A refusal is a normal completion with stopReason set to refusal, which is why the two are separate signals.

Working directory and inheritance

The working directory is the launch working directory. A task document cannot change it, and no option in the document is consulted for it. This keeps the untrusted input away from the one decision that determines which files an agent can touch.

The inherited environment is preserved and extended, never replaced. Existing variables that the harness needs to control are overridden deliberately and the override is recorded; everything else passes through so a controlling application can configure provider credentials the way it already does.

Implemented surface

ModuleOwns
argsFlag and environment parsing, defaults, usage text, refusals
run_taskCatalog assembly, prompt file read, launch configuration, one invocation
agentsOne JSON object per line describing each resolved runner and its availability
signalsSIGINT and SIGTERM installation and the cancellation watch
libDispatch, runtime construction, emitting one record, resolving the exit code

Every launch option has a flag and a matching JJ_ACP_* variable, and the flag wins. Defaults for the idle window and the absolute cap are 620 and 7200 seconds, inherited from the upstream harness; the cancellation grace and reap bounds default to five seconds each.

Installing a signal handler replaces the default terminate-immediately behaviour, so an interruption now produces bounded cleanup and a record instead of an orphaned agent and silence. A failure to install is reported rather than ignored, because running without cancellation handling would silently weaken that guarantee.

Stdout contract

Stdout carries terminal records, and for --help, --version and agents the output those commands exist to produce. Nothing else is ever written there. Usage for a malformed command line goes to stderr, so a controlling application that mis-invokes the binary still sees empty stdout rather than prose it cannot parse.

A record is emitted on every path that reaches an invocation, including a spawn failure and an agent error. A broken stdout is the one case with no record, and it exits 1; the record is not a durable ledger and a killed harness can leave none.

Failure classification

The exit code distinguishes causes a controlling application must treat differently. 1 with agent_exited means the agent is gone and a new one can safely be started. 124 means a bound elapsed and the agent’s state is unknown, so starting another one risks two agents working the same task. 2 means nothing was started at all, so the same document can be corrected and resubmitted.

130 and 143 mean the run was interrupted and bounded cleanup ran. Cleanup being bounded is not the same as cleanup being complete, and the record says cancelled with interrupted rather than claiming the agent stopped.

Subcommand option handling

agents parses the same launch options as run, because it inspects a catalog. Building it from defaults instead reported a smaller runner set than the operator configured and looked like a correct answer, which is the worst shape for this class of bug: a catalog file that silently does nothing.

Arguments that no subcommand would read are refused rather than ignored. --help --catalog x is a mistake, and accepting it teaches an operator that unparsed flags are harmless.

Session configuration flags

--model, --effort and --mode request session settings, each with a matching JJ_ACP_MODEL, JJ_ACP_EFFORT and JJ_ACP_MODE variable and the flag winning. All three are requests, not requirements: a value the adapter does not offer is reported on stderr and the adapter’s own default is kept, and the run still completes normally with exit 0.

That asymmetry is intentional. Failing a task because an adapter lacks a preference would make the flags dangerous to pass, and a controlling application that targets several adapters could not set a preference at all. The record schema is unchanged by configuration, so shortfalls travel on stderr where an operator reads them and never in the record a machine parses.

The serve command

jj-acp serve parses the same launch options as run and refuses --task, because a task arrives per control frame and a launch-time document would be meaningless. Sharing the option parser is what stops a flag from meaning one thing in run and something else in serve; the trusted launch configuration is built by one function both commands call.

A signal ends the loop rather than cancelling one task. A controlling application that has stopped reading has nobody to receive a record, so the honest outcome is a bounded drain followed by the signal’s own exit code. Reaching end of input is a clean exit 0, because a closed stdin is how a controlling application says it is finished.

Unusable launch configuration exits 2 with the reason on stderr and nothing on stdout. There is no loop to serve and no peer to frame a refusal for, so the exit code is the only machine-readable signal available.

Pool sizing

--workers sets how many submissions serve executes at once and --queue-capacity how many may wait, each with a matching JJ_ACP_WORKERS and JJ_ACP_QUEUE_CAPACITY variable. Workers default to one, which preserves the serial behaviour from before scopes existed, so raising concurrency is an explicit choice rather than a surprise. Both are validated: zero is refused, and workers are capped at 32.

The one-shot path ignores both, because a process that runs one task and exits has nothing to size.

Interactive session integration

The additive –control-version option accepts 1 or 2 and defaults to 1. Version 2 requires an explicit –runner. Signal and owner EOF request bounded interactive cleanup.

Queued prompt cancellation in explicit control v2 reports sessionUsable while retaining the warm session. The next explicit follow-up reuses it; connection shutdown still closes it. Version-1 command and terminal record shapes remain unchanged. E-M11-003 records the binary regression and source gates.

Warm worker integration explicitly selects –control-version 3 and a fixed managed cwd root. The flag default remains version 1; version 2 remains a single interactive session with no request-supplied paths.

A version-3 adapter rejection of open/configure remains request-scoped. Other opened sessions stay usable on the initialized process. Protocol errors, timeouts and process exits retain worker-wide invalidation (E-M12-004).

Serve controls v2/v3 accept optional prepared context independently of text. This adds no launch flag, filesystem selection or credential surface. The CLI preserves caller session/request fencing and emits one terminal outcome per real prompt; warming and context preparation do not submit preliminary turns.

Interactive v2/v3 callers may submit a comments-only turn using empty text and nonblank prepared context. The CLI retains both fields without inventing user text; an entirely empty prompt remains invalid (E-M12-003).

Trusted coding-tool declarations

The controlling application may select a bounded JSON file of stdio MCP declarations through --tools or JJ_ACP_TOOLS. This is trusted launch configuration, independent of tasks and v2/v3 control input. Session creation and supported loading receive the same declarations. Warm initialization does not start a tool session. The file is bounded to 64 KiB and eight unique stdio servers; only absolute executable paths are accepted, with bounded arguments/environment and no reserved harness identity names. Errors disclose stable codes, not file contents or credentials. ACP continues to own no tool policy, authentication, network or durable settings.

Public release packaging

The public Cargo package name is jj-acp, retaining the jj-acp executable and jj_acp_cli library. Source paths stay under crates/jj-acp-cli. The README covers one-shot tasks, persistent control, trusted tool declarations and installation. Packaging changes do not change the control protocol.

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.

The deadline containment regression allows interpreter startup before testing cleanup of a real descendant. Its configured absolute deadline is ten seconds; the test verifies exit 124, a deadline-exceeded terminal record, exactly one prompt dispatch, bounded completion and disappearance of the recorded process. A two-second delayed interpreter reproduces the previous premature timeout and passes the corrected setup. This is fixture timing, not a runtime limit change.

Changesets version preparation refreshes the root installation example and packaged CLI README together. The CLI Cargo package continues to inherit the coordinated workspace version; version preparation adds no command or protocol.

Security and trust

jj-acp runs an autonomous coding agent with tool access on a local machine. Its security posture is therefore mostly about what it refuses to own. It implements no authentication, holds no keys, signs nothing, opens no listener and keeps no durable state. Every one of those is the controlling application’s responsibility, and blurring that line would create a second, weaker copy of a policy that already exists upstream of this process.

The threat model is a trusted controlling application supplying untrusted task content and an untrusted agent subprocess producing untrusted output.

Requirements

IDRequirement
JAC-SEC-001jj-acp MUST NOT implement authentication, identity, key custody or author gating; the controlling application is the trust boundary.
JAC-SEC-002Credentials MUST NOT appear in process arguments and MUST NOT be written to a log, a diagnostic or a terminal record.
JAC-SEC-003A task document MUST be treated as executable input, and the runner identifier check MUST prevent accidental execution by a different runner without claiming to verify authorship.
JAC-SEC-004jj-acp MUST NOT modify global Git configuration, install a credential helper or persist a signing key.
JAC-SEC-005A failed, cancelled or uncertain invocation MUST NOT be retried automatically.
JAC-SEC-006Agent output, agent error text and agent-supplied paths MUST be treated as untrusted data and MUST NOT be interpreted as instructions or executed.
JAC-SEC-007Resource bounds MUST be enforced on untrusted input: document size, read interval, frame size, turn duration and reap interval.

What is deliberately absent

The upstream harness authenticates to a relay with a keypair, gates inbound events by author, resolves an owner, verifies workflow-message provenance against a relay signing key, signs Git commits and tags, and installs scoped credential helpers with an ephemeral keyfile. None of that is ported, stubbed or feature-flagged. A future need for it belongs in the controlling application, which already has an identity system, rather than in a runner that would have to invent one.

The consequence is explicit: jj-acp grants an agent whatever the launch environment grants. A caller that supplies provider credentials in the environment has given them to the agent. That is the caller’s decision and it is not reversible from inside this process, which is why the task document cannot carry credentials and why reserved environment keys cannot be set by a runner definition.

Sanitization

Terminal records are sanitized by construction rather than by filtering. The record schema has no field that carries prompt text, agent final text, raw adapter error text or environment content. A failure carries a stable code from a fixed set. This is stronger than redaction, because there is no path by which an unexpected value reaches the record.

Diagnostics on stderr are operator-facing and may name a failing executable, a protocol violation or a timeout stage. They MUST NOT include credentials or prompt content. Child stderr is forwarded for diagnosis and is the agent’s own output; a controlling application that treats it as trusted is making an error this repository cannot prevent, and JAC-SEC-006 states the expectation.

Bounds

Untrusted input is bounded before it is parsed rather than during. The document size cap applies to the whole UTF-8 object including whitespace. The read interval is separate from the execution deadline, so a stalled pipe cannot consume the turn budget and a stalled agent cannot consume the read budget. The frame byte cap applies to every line the agent emits, including the first.

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.

Specification governance

Use the parent workspace test escalation ladder. Requirement changes belong here before or with implementation. Generated book output is ignored and non-normative. Only milestones.md carries implementation status; evidence entries hold immutable records rather than competing status summaries. Focused test commands may prove a requested slice without running unrelated suites when the user narrows test scope, but every directly affected boundary still needs named evidence.

jj-acp borrows the jj-store governance regime wholesale: canonical mdBook chapters, unique requirement IDs with exactly one verification row each, a single progress ledger, mapped spec-first enforcement, pinned toolchain and Jujutsu commits per verified slice. It deliberately adds one gate jj-store does not have for Rust, recorded as JAC-GOV-007, because the upstream files this repository ports are far larger than jj-store’s Rust budget would allow.

Requirements

IDRequirement
JAC-GOV-001The canonical specification MUST live under spec/src, build with mdBook, and assign unique JAC-<AREA>-NNN requirement IDs with exactly one verification row each.
JAC-GOV-002Protected implementation, interface, tooling and agent-guidance changes MUST update their mapped canonical chapter in the same Jujutsu change.
JAC-GOV-003Every implementation slice MUST update milestones.md with actual commands, results, failures, affected files, versions, formats, gaps and next action.
JAC-GOV-004Progress checks MUST validate task and dependency references, evidence links and named tests, completion gates and same-change ledger updates.
JAC-GOV-005Verified slices MUST be committed with Jujutsu while unrelated work and local workspace configuration remain uncommitted.
JAC-GOV-006Rust formatting MUST use the pinned toolchain component and tracked rustfmt.toml, and MUST be checked by both normal and specification checks.
JAC-GOV-007Rust and first-party script sources MUST stay within the recorded counted-line budget, and exceptions MUST be explicit baseline entries that do not grow.

Change map

Protected areaRequired chapter
^crates/(?:[a-z0-9]+-)*acp-protocol/protocol.md
^crates/(?:[a-z0-9]+-)*acp-agent/agents.md
^crates/(?:[a-z0-9]+-)*acp-runner/runner.md
^crates/(?:[a-z0-9]+-)*acp-control/control.md
^crates/(?:[a-z0-9]+-)*acp-cli/cli.md
^crates/(?:[a-z0-9]+-)*acp-testkit/compatibility.md
^(?:tests/|docs/)compatibility.md
^(?:scripts/|AGENTS\.md$|spec-validator\.config\.mjs$|spec/book\.toml$|\.gitignore$|\.prettier)spec-governance.md
^rustfmt\.toml$spec-governance.md
^(?:Cargo\.(?:toml|lock)$|rust-toolchain\.toml$|package\.json$|pnpm-lock\.yaml$|README\.md$|LICENSE\.md$)architecture.md

.github/ maps to both this chapter and release.md. CHANGELOG.md, CONTRIBUTING.md, SECURITY.md and .gitattributes map to architecture.md.

A protected path that matches no rule above fails closed as unmapped rather than passing silently. A path that matches more than one rule requires every mapped chapter in the same change, as specified for workflow and Changesets configuration changes.

Passing bookkeeping MUST NOT substitute for passing product tests. A ledger entry records the commands actually run and their real failure counts; it never converts a fixture-peer result into a real-runner qualification or a compile into a behavioral proof.

Generated automation changes

Identified Dependabot and Changesets PRs MAY omit same-change canonical chapter and ledger/evidence updates. CI MUST identify the bot author, reserved branch, same-repository head and main base, and exact checked revision. Explicit bot PR dispatches and merged/tagged qualification use GitHub’s associated PR metadata. Unrecognized or unrelated revisions MUST retain the ordinary gates; lookup errors MUST fail closed. A push spanning multiple changes is not exempt.

Only spec-first and same-change evidence bookkeeping are exempt. Existing ledger integrity, specification validation/build, formatting, boundaries, tests, Clippy, native builds and packaging MUST still run. This follows the Lapis Notes generated Changesets exemption and extends it to Dependabot.

Check lanes

spec-validator.config.mjs declares the lanes that pnpm spec:check runs, and package.json exposes the same checks as individual scripts so a narrowed slice can run one of them. The lanes are rustfmt, prettier, crate boundaries, the counted-line budget, the progress ledger, the upstream pin self-test, the governance and lock test suites, and the cargo workspace tests.

The upstream pin lane is offline by design. scripts/upstream-sync.mjs --self-test validates the curated port map and reconciles it with the generated lock without touching the network, so a normal check run never depends on upstream availability. The two networked modes, --write and --fetch, are explicit opt-in lanes reached through pnpm upstream:write and pnpm upstream:fetch. This mirrors the sibling jj-store split between a harness self-test in the normal lane and a real-storage qualification that stays opt-in, and it preserves the rule that a self-test is not conformance evidence: passing --self-test proves the record is internally consistent, not that the pinned upstream source still matches it.

A lane that needs the network, a credential or a model provider MUST NOT be part of the normal check run. It belongs in an explicitly named opt-in lane whose absent evidence is recorded in the ledger as a gap rather than inferred as a pass.

Test placement and the counted-line gate

The counted-line gate classifies a source file as production, test or script from its path, and a production file that defines a test function inline is rejected. A #[cfg(test)] mod tests; declaration is accepted, because it names a sibling file rather than carrying test bodies; the bodies live in that sibling or in the crate’s tests directory. The distinction matters to the budget: an inline test module inflates a production file’s counted lines with content that is not production, which would either force a spurious baseline exception or push real logic out of the file it belongs in.

The gate exempts the testkit crate from the rule against importing the testkit, because its own documentation examples legitimately name it. The rule exists to keep production crates away from test helpers, not to stop a helper describing itself, and a gate that fires on its own subject teaches reviewers to ignore it.

An over-budget file is extracted, never baselined. The baseline exists for a file whose extraction genuinely cannot lower its budget, and a stale entry whose file no longer exists is itself a failure, so exceptions cannot outlive their justification. When the protocol crate’s connection tests outgrew the test budget, the response was to split them across three test files sharing one harness module rather than to record an exception.

Milestone shape

The ledger rejects a Done task that has an incomplete child, because a <ID>.<N> suffix asserts that the child is part of the parent’s gate rather than work that follows it. Follow-on qualification is therefore a sibling milestone with a dependency on the one it follows, not a sub-milestone.

That distinction was learned by being blocked rather than by anticipating it. The one-shot task runner was originally recorded with process-group containment and real installed-runner qualification as its children, which made the runner impossible to call done until both were also done, even though its own gate was complete and independently evidenced. Renumbering them as siblings with dependencies preserved the ordering without conflating a delivered gate with the hardening that follows it.

One task carries a special evidence rule: the real installed-runner qualification cannot be marked done on evidence whose kind is anything other than a real runner run. A fixture-peer suite is deterministic and model-free by design, and that is exactly why it cannot prove interoperability with a shipping adapter.

Crate rules key on the layer, not the product name

The crate rows above match (?:[a-z0-9]+-)*acp-<layer> rather than a literal product prefix. The layer is what determines the owning chapter: a change under a protocol crate belongs to protocol.md whatever the product is called.

Renaming the repository exposed why the literal form was wrong. A rename produces deletions at the old paths alongside additions at the new ones, and the old paths still match the protected list. With literal patterns they matched no rule, so the rename commit failed as unmapped protected source — correctly by the rule as written, and for a reason that had nothing to do with the specification. The alternatives were worse: carrying the previous product name in the map forever, or adding a temporary rule and remembering to remove it.

The pattern still fails closed for a genuinely new crate. Anything under crates/ whose layer is not one of the six listed matches no rule and is reported as unmapped, so adding a seventh crate still requires a same-change map update.

Public release preparation

GitHub workflows are protected and require this chapter plus release.md. Root contributor, security, changelog and Git attributes configuration map to architecture.md. The progress checker selects explicit JJ_ACP_CHECK_BASE/HEAD in CI and otherwise preserves local Jujutsu revision checks; ordinary Git-only checkouts default to HEAD and its parent, with an empty-tree base for a first commit. Missing specification or evidence changes fail closed. CI checks all canonical lanes plus portable packaging; native Rust lanes run on each supported target. GitHub Actions dependency updates are weekly and Actions use pinned reviewed SHAs.

The canonical CI job installs mdBook through the pinned Rust tooling Action and actionlint 1.7.12 from its official release archive with an explicit SHA-256 check. actionlint is not a supported taiki-e/install-action tool. The initial hosted job failed at tool setup before source gates; corrected setup must pass those gates before release.

Registry existence is established by the version metadata endpoint before downloading an archive. An unpublished CDN object can return HTTP 403; that is not a credential failure and must not be used to decide existence. Metadata 404 admits publication, while any error downloading a known version blocks publication and any checksum mismatch still refuses replacement.

Changesets configuration and guidance map to release.md; configuration also maps to this chapter. Active release notes are prose and do not require a release-chapter edit with every feature. Generated version PRs update manifests, lockfiles, changelog and installation examples without manufactured canonical candidate prose or ledger evidence. Normal check/specification lanes validate coordinated versions and release automation regressions.

Workflow setup fetches the committed Rust dependency graph before offline version-preparation acceptance. Source and fixture checks must not rely on a restored Cargo cache. The local wrapper checks cache completeness before consuming a note or editing version metadata.

Implementation progress

This is the single status ledger. Done requires original-gate evidence; a fixture-peer result cannot satisfy a real installed-runner gate, and compilation cannot satisfy a behavioural gate. Every implementation slice adds evidence here in its enclosing Jujutsu change.

Tasks and gates

IDStatusDependsRequirementsGateNextEvidence
BootstrapDone-JAC-GOV-001,JAC-GOV-002,JAC-GOV-003,JAC-GOV-005,JAC-GOV-006,JAC-ARCH-001,JAC-ARCH-002,JAC-COMPAT-004Canonical specification, ledger, crate skeleton, boundary gate and pinned toolchain green on an empty productMaintain governance each sliceE-BOOT-001
Bootstrap.1Done-JAC-GOV-004,JAC-GOV-007,JAC-ARCH-003,JAC-ARCH-004Boundary, counted-line and progress-checker negative tests passMaintain gates each sliceE-BOOT-002
Bootstrap.2Done-JAC-GOV-001,JAC-GOV-002,JAC-GOV-003,JAC-ARCH-001Repository, crates, binary, requirement prefix and environment namespace renamed from lapis-acp to jj-acp with the change map keyed on crate layerMaintain governance each sliceE-BOOT-003
M1DoneBootstrapJAC-COMPAT-001,JAC-COMPAT-002,JAC-COMPAT-003,JAC-COMPAT-007,JAC-SEC-001Pinned upstream lock, attribution and digest-verified drift detectionImplement M2 protocol crateE-M1-001
M2DoneBootstrapJAC-PROTO-001,JAC-PROTO-002,JAC-PROTO-003,JAC-PROTO-004,JAC-PROTO-005,JAC-PROTO-006Bounded ACP framing, version negotiation and session methods under unit testImplement M3 runner catalogE-M2-001
M3DoneM2JAC-AGENT-001,JAC-AGENT-002,JAC-AGENT-003,JAC-AGENT-004,JAC-AGENT-005,JAC-AGENT-006,JAC-COMPAT-005Tiered runner catalog, reserved-key stripping and deterministic fixture peerImplement M4 task runnerE-M3-001
M4DoneM3JAC-CTRL-002,JAC-CTRL-003,JAC-CTRL-004,JAC-CTRL-005,JAC-CTRL-006,JAC-CTRL-007,JAC-CLI-001,JAC-CLI-002,JAC-CLI-003,JAC-CLI-004,JAC-CLI-005,JAC-CLI-006,JAC-RUN-001,JAC-RUN-002,JAC-RUN-003,JAC-RUN-004,JAC-ARCH-005,JAC-ARCH-006,JAC-SEC-002,JAC-SEC-003,JAC-SEC-004,JAC-SEC-005,JAC-SEC-006,JAC-SEC-007One-shot prepared task runner with sanitized record, published exit codes, signal cleanup and clean stdoutQualify M5 process-group containmentE-M4-001
M5DoneM4JAC-RUN-005,JAC-RUN-007Descendant containment on both the forced and the graceful path, crash reporting distinct from a timeout, and no re-dispatchQualify M6 against a real installed runnerE-M5-001
M6DoneM5JAC-COMPAT-006,JAC-PROTO-002Real installed-runner qualification against cursor-agent acp recorded separately from fixture evidenceImplement M7 session configurationE-M6-001,E-M6-002
M7DoneM6JAC-RUN-006Model, effort and mode resolved per session with transport-versus-rejection classification, qualified live on two adaptersImplement M8 persistent transportE-M7-001
M8DoneM7JAC-CTRL-001Persistent versioned control protocol over stdio with concurrent cancellation, frame-level refusals and one shape for every task outcomeImplement M9 standing poolE-M8-001
M9DoneM8JAC-RUN-001,JAC-RUN-005Caller-named scopes with one turn in flight each, concurrent distinct scopes, bounded queue admission and slot release on crash without re-dispatchImplement M10 consumer acceptanceE-M9-001
M10DoneM9JAC-ARCH-005Controlling-application consumer acceptance in a real hostMaintain qualified macOS jj-store profileE-M10-001
M11DoneM9JAC-CTRL-008,JAC-RUN-008,JAC-PROTO-007,JAC-CLI-007Persistent interactive sessions, streamed activity, permission decisions and bounded cleanupMaintain interactive session contractE-M11-001,E-M11-002,E-M11-003
M12In progressM11JAC-CTRL-009,JAC-RUN-009,JAC-PROTO-008,JAC-CLI-008Warm worker and scoped sessions with real installed-runner acceptanceQualify installed adapters and jj-store integrationE-M12-001,E-M12-004,E-TOOLS-001,E-TOOLS-002
M13DoneM11JAC-REL-001,JAC-REL-002,JAC-REL-003,JAC-REL-004,JAC-REL-005Portable crate packaging, Git CI governance, Pages and native release automationValidate source and archives before publicationE-RELEASE-001,E-CI-CLEANUP-001
M14DoneM13JAC-REL-003,JAC-REL-004,JAC-REL-005Public source, hosted CI/specification, registry installation and four native archivesPublish the verified source and versionE-GITHUB-001,E-PUBLISH-001,E-HANDOFF-001
M15DoneM14JAC-REL-006,JAC-REL-004,JAC-REL-005,JAC-GOV-002,JAC-GOV-003Coordinated Changesets version preparation and protected automation configurationUse Version Packages PR for the next Cargo releaseE-CHANGESETS-001,E-CHANGESETS-COLD-001,E-CI-AUTOMATION-001

Evidence

Each entry records the commands actually run, their real failure counts, the files touched, the named tests that prove the gate, the versions and formats in play, and the gaps that remain. An entry with result: "pass" and a non-empty remaining list is rejected by the progress checker, so a passing claim cannot coexist with an acknowledged gap.

[
  {
    "id": "E-BOOT-001",
    "tasks": ["Bootstrap"],
    "date": "2026-09-30",
    "kind": "governance",
    "files": [
      "spec-validator.config.mjs",
      "spec/book.toml",
      "spec/src/SUMMARY.md",
      "spec/src/index.md",
      "spec/src/architecture.md",
      "spec/src/protocol.md",
      "spec/src/agents.md",
      "spec/src/runner.md",
      "spec/src/control.md",
      "spec/src/cli.md",
      "spec/src/security.md",
      "spec/src/compatibility.md",
      "spec/src/spec-governance.md",
      "spec/src/milestones.md",
      "spec/src/verification.md",
      "Cargo.toml",
      "Cargo.lock",
      "rust-toolchain.toml",
      "rustfmt.toml",
      "package.json",
      "AGENTS.md",
      "README.md",
      "crates/jj-acp-protocol/src/lib.rs",
      "crates/jj-acp-agent/src/lib.rs",
      "crates/jj-acp-runner/src/lib.rs",
      "crates/jj-acp-control/src/lib.rs",
      "crates/jj-acp-cli/src/lib.rs",
      "crates/jj-acp-testkit/src/lib.rs"
    ],
    "tests": ["scripts/governance.test.mjs#accepts_verified_task"],
    "runs": [
      {
        "command": "cargo fmt --all -- --check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/check-boundaries.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "spec-validator validate",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "Rust 1.98.1; cargo 1.98.1; Node v24.10.0; pnpm 10.34.5; @lapismd/spec-validator 0.1.0",
    "formats": "requirement table; verification table; ledger task table and JSON evidence"
  },
  {
    "id": "E-BOOT-002",
    "tasks": ["Bootstrap.1"],
    "date": "2026-09-30",
    "kind": "governance",
    "files": [
      "scripts/check-boundaries.mjs",
      "scripts/check-progress.mjs",
      "scripts/governance.test.mjs",
      "scripts/rust-architecture.mjs",
      "scripts/rust-architecture.test.mjs",
      "scripts/rust-architecture-baseline.json"
    ],
    "tests": [
      "scripts/governance.test.mjs#rejects_excluded_nostr_transport_and_database_dependencies",
      "scripts/governance.test.mjs#testkit_is_dev_only",
      "scripts/governance.test.mjs#rejects_crate_outside_the_boundary_matrix",
      "scripts/governance.test.mjs#rejects_fixture_peer_qualification",
      "scripts/governance.test.mjs#rejects_unchanged_ledger_for_code",
      "scripts/rust-architecture.test.mjs#rejects_production_source_over_budget",
      "scripts/rust-architecture.test.mjs#rejects_inline_tests_in_a_production_module",
      "scripts/rust-architecture.test.mjs#rejects_production_import_of_the_testkit",
      "scripts/rust-architecture.test.mjs#rejects_stale_baseline_entry"
    ],
    "runs": [
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/rust-architecture.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/check-progress.mjs",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "Node v24.10.0; cargo 1.98.1 for cargo metadata --no-deps --locked",
    "formats": "counted-line budget; crate dependency matrix; ledger JSON evidence"
  },
  {
    "id": "E-M1-001",
    "tasks": ["M1"],
    "date": "2026-09-30",
    "kind": "upstream-audit",
    "files": [
      "docs/upstream-lock.md",
      "docs/licenses/NOTICE.md",
      "docs/upstream/disposition.json",
      "docs/upstream/lock.json",
      "scripts/upstream-lock.mjs",
      "scripts/upstream-sync.mjs",
      "scripts/upstream-lock.test.mjs",
      "spec/src/compatibility.md",
      "spec/src/spec-governance.md",
      "spec/src/architecture.md",
      "spec-validator.config.mjs",
      "package.json"
    ],
    "tests": [
      "scripts/upstream-lock.test.mjs#the_curated_port_map_is_valid",
      "scripts/upstream-lock.test.mjs#the_recorded_lock_matches_the_curated_map",
      "scripts/upstream-lock.test.mjs#validate_lock_detects_drift_from_the_curated_map",
      "scripts/upstream-lock.test.mjs#drift_report_classifies_every_observed_file",
      "scripts/upstream-lock.test.mjs#build_lock_refuses_a_classified_region_without_a_digest",
      "scripts/upstream-lock.test.mjs#dropped_regions_carry_a_reason_and_no_owner",
      "scripts/upstream-lock.test.mjs#rejects_an_owner_disposition_mismatch",
      "scripts/upstream-lock.test.mjs#scratch_materialization_stays_in_ignored_storage"
    ],
    "runs": [
      {
        "command": "node --test scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/upstream-sync.mjs --self-test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/upstream-sync.mjs --write",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/upstream-sync.mjs --fetch",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "block/buzz@2664d14316790a57ea44b3f97c57440b70e43436; Node v24.10.0; SHA-256",
    "formats": "upstream disposition map; generated digest lock; drift report"
  },
  {
    "id": "E-M2-001",
    "tasks": ["M2"],
    "date": "2026-09-30",
    "kind": "protocol",
    "files": [
      "crates/jj-acp-protocol/Cargo.toml",
      "crates/jj-acp-protocol/src/lib.rs",
      "crates/jj-acp-protocol/src/budget.rs",
      "crates/jj-acp-protocol/src/client.rs",
      "crates/jj-acp-protocol/src/config_option.rs",
      "crates/jj-acp-protocol/src/content.rs",
      "crates/jj-acp-protocol/src/envelope.rs",
      "crates/jj-acp-protocol/src/error.rs",
      "crates/jj-acp-protocol/src/frame.rs",
      "crates/jj-acp-protocol/src/handler.rs",
      "crates/jj-acp-protocol/src/initialize.rs",
      "crates/jj-acp-protocol/src/permission.rs",
      "crates/jj-acp-protocol/src/session.rs",
      "crates/jj-acp-protocol/src/stop_reason.rs",
      "crates/jj-acp-protocol/src/update.rs",
      "crates/jj-acp-protocol/tests/common/mod.rs",
      "crates/jj-acp-protocol/tests/payload.rs",
      "crates/jj-acp-protocol/tests/wire.rs",
      "crates/jj-acp-protocol/tests/inbound.rs",
      "crates/jj-acp-protocol/tests/bounds.rs",
      "Cargo.toml",
      "Cargo.lock",
      "scripts/governance.test.mjs",
      "scripts/rust-architecture.mjs",
      "scripts/rust-architecture.test.mjs",
      "docs/upstream/disposition.json",
      "docs/upstream/lock.json",
      "spec/src/protocol.md",
      "spec/src/architecture.md",
      "spec/src/compatibility.md",
      "spec/src/spec-governance.md",
      "AGENTS.md"
    ],
    "tests": [
      "crates/jj-acp-protocol/tests/wire.rs#initialize_negotiates_and_records_the_selected_version",
      "crates/jj-acp-protocol/tests/wire.rs#initialize_rejects_a_version_the_client_cannot_speak",
      "crates/jj-acp-protocol/tests/wire.rs#a_prompt_streams_updates_and_then_returns_one_stop_reason",
      "crates/jj-acp-protocol/tests/wire.rs#request_ids_increase_and_stay_correlated",
      "crates/jj-acp-protocol/tests/wire.rs#cancellation_is_a_notification_and_carries_no_id",
      "crates/jj-acp-protocol/tests/inbound.rs#an_unanswered_permission_request_is_cancelled_not_approved",
      "crates/jj-acp-protocol/tests/inbound.rs#an_explicit_approval_selects_the_option_by_kind",
      "crates/jj-acp-protocol/tests/inbound.rs#an_unimplemented_agent_request_is_answered_not_ignored",
      "crates/jj-acp-protocol/tests/bounds.rs#an_oversized_frame_is_refused_before_it_can_be_buffered",
      "crates/jj-acp-protocol/tests/bounds.rs#the_idle_bound_reports_silence_while_the_absolute_bound_caps_noise",
      "crates/jj-acp-protocol/tests/bounds.rs#an_agent_that_stops_reading_is_reported_as_a_write_timeout",
      "crates/jj-acp-protocol/tests/bounds.rs#a_closed_agent_stream_is_reported_not_hung",
      "crates/jj-acp-protocol/tests/payload.rs#a_model_match_comes_from_the_nested_candidates_not_the_current_value",
      "crates/jj-acp-protocol/tests/payload.rs#stop_reason_parsing_tolerates_agent_casing",
      "scripts/governance.test.mjs#the_protocol_crate_declares_no_process_or_network_facility",
      "scripts/rust-architecture.test.mjs#allows_a_cfg_test_sibling_module_declaration"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-protocol --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/rust-architecture.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "Rust 1.98.1; tokio 1.53.1; serde 1.0.229; serde_json 1.0.151; thiserror 2.0.21",
    "formats": "ACP JSON-RPC protocol version 2 with version 1 fallback; newline-delimited frames capped at 10 MB"
  },
  {
    "id": "E-M3-001",
    "tasks": ["M3"],
    "date": "2026-09-30",
    "kind": "fixture-peer",
    "files": [
      "crates/jj-acp-agent/Cargo.toml",
      "crates/jj-acp-agent/src/lib.rs",
      "crates/jj-acp-agent/src/availability.rs",
      "crates/jj-acp-agent/src/catalog.rs",
      "crates/jj-acp-agent/src/config.rs",
      "crates/jj-acp-agent/src/defaults.rs",
      "crates/jj-acp-agent/src/definition.rs",
      "crates/jj-acp-agent/src/environment.rs",
      "crates/jj-acp-agent/src/error.rs",
      "crates/jj-acp-agent/tests/catalog.rs",
      "crates/jj-acp-testkit/Cargo.toml",
      "crates/jj-acp-testkit/src/lib.rs",
      "crates/jj-acp-testkit/src/fixture.rs",
      "crates/jj-acp-testkit/src/log.rs",
      "crates/jj-acp-testkit/tests/fixture.rs",
      "tests/fixtures/fixture_agent.py",
      "Cargo.toml",
      "Cargo.lock",
      "scripts/check-boundaries.mjs",
      "scripts/governance.test.mjs",
      "scripts/rust-architecture.mjs",
      "scripts/rust-architecture.test.mjs",
      "docs/upstream/disposition.json",
      "docs/upstream/lock.json",
      "spec/src/agents.md",
      "spec/src/architecture.md",
      "spec/src/compatibility.md",
      "spec/src/spec-governance.md"
    ],
    "tests": [
      "crates/jj-acp-agent/tests/catalog.rs#a_runner_is_declared_entirely_as_data",
      "crates/jj-acp-agent/tests/catalog.rs#identifiers_are_pattern_checked_and_length_bounded",
      "crates/jj-acp-agent/tests/catalog.rs#the_compiled_in_tier_reserves_its_identifiers",
      "crates/jj-acp-agent/tests/catalog.rs#no_tier_may_claim_an_identifier_twice",
      "crates/jj-acp-agent/tests/catalog.rs#unknown_and_disabled_runners_fail_without_a_default_fallback",
      "crates/jj-acp-agent/tests/catalog.rs#reserved_environment_keys_are_stripped_from_every_layer",
      "crates/jj-acp-agent/tests/catalog.rs#environment_layers_apply_inherited_then_floor_then_overrides",
      "crates/jj-acp-agent/tests/catalog.rs#harness_variables_are_produced_only_by_the_harness",
      "crates/jj-acp-agent/tests/catalog.rs#a_catalog_document_declares_runners_without_executing_anything",
      "crates/jj-acp-agent/tests/catalog.rs#availability_resolves_the_first_candidate_that_exists",
      "crates/jj-acp-testkit/tests/fixture.rs#the_fixture_completes_a_turn_without_a_model_provider",
      "crates/jj-acp-testkit/tests/fixture.rs#a_permission_request_is_answered_and_the_turn_completes",
      "crates/jj-acp-testkit/tests/fixture.rs#an_agent_error_arrives_typed_with_its_marker_intact",
      "crates/jj-acp-testkit/tests/fixture.rs#a_silent_agent_is_bounded_by_the_idle_window",
      "crates/jj-acp-testkit/tests/fixture.rs#a_version_one_agent_is_accepted_and_recorded",
      "crates/jj-acp-testkit/tests/fixture.rs#killing_the_agent_alone_leaves_its_descendant_running",
      "scripts/governance.test.mjs#the_testkit_may_depend_on_the_layers_it_exercises",
      "scripts/rust-architecture.test.mjs#allows_the_testkit_crate_to_name_itself"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-agent --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test -p jj-acp-testkit --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test -p jj-acp-protocol --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/upstream-sync.mjs --self-test",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "Rust 1.98.1; Python 3.14.7; tokio 1.53.1; serde_json 1.0.151",
    "formats": "runner catalog document; fixture record log; ACP protocol versions 1 and 2"
  },
  {
    "id": "E-M4-001",
    "tasks": ["M4"],
    "date": "2026-09-30",
    "kind": "task-runner",
    "files": [
      "crates/jj-acp-protocol/src/client.rs",
      "crates/jj-acp-protocol/src/frame.rs",
      "crates/jj-acp-protocol/tests/bounds.rs",
      "crates/jj-acp-agent/src/config.rs",
      "crates/jj-acp-agent/src/environment.rs",
      "crates/jj-acp-agent/tests/catalog.rs",
      "crates/jj-acp-runner/Cargo.toml",
      "crates/jj-acp-runner/src/lib.rs",
      "crates/jj-acp-runner/src/launch.rs",
      "crates/jj-acp-runner/src/outcome.rs",
      "crates/jj-acp-runner/src/process.rs",
      "crates/jj-acp-runner/src/turn.rs",
      "crates/jj-acp-control/Cargo.toml",
      "crates/jj-acp-control/src/lib.rs",
      "crates/jj-acp-control/src/input.rs",
      "crates/jj-acp-control/src/invocation.rs",
      "crates/jj-acp-control/src/record.rs",
      "crates/jj-acp-control/src/task.rs",
      "crates/jj-acp-control/src/transport.rs",
      "crates/jj-acp-cli/Cargo.toml",
      "crates/jj-acp-cli/src/lib.rs",
      "crates/jj-acp-cli/src/main.rs",
      "crates/jj-acp-cli/src/agents.rs",
      "crates/jj-acp-cli/src/args.rs",
      "crates/jj-acp-cli/src/run_task.rs",
      "crates/jj-acp-cli/src/signals.rs",
      "crates/jj-acp-cli/tests/common/mod.rs",
      "crates/jj-acp-cli/tests/args.rs",
      "crates/jj-acp-cli/tests/run_task.rs",
      "crates/jj-acp-cli/tests/signals.rs",
      "crates/jj-acp-cli/tests/stdio_contract.rs",
      "crates/jj-acp-testkit/tests/fixture.rs",
      "tests/fixtures/fixture_agent.py",
      "Cargo.toml",
      "Cargo.lock",
      "docs/upstream/disposition.json",
      "docs/upstream/lock.json",
      "spec/src/architecture.md",
      "spec/src/protocol.md",
      "spec/src/agents.md",
      "spec/src/runner.md",
      "spec/src/control.md",
      "spec/src/cli.md",
      "spec/src/compatibility.md"
    ],
    "tests": [
      "crates/jj-acp-cli/tests/run_task.rs#a_prepared_task_completes_with_one_terminal_record",
      "crates/jj-acp-cli/tests/run_task.rs#file_and_stdin_input_agree",
      "crates/jj-acp-cli/tests/run_task.rs#a_refusal_is_a_normal_completion_with_its_own_stop_reason",
      "crates/jj-acp-cli/tests/run_task.rs#an_agent_error_never_echoes_the_agent_message",
      "crates/jj-acp-cli/tests/run_task.rs#the_prompt_and_credentials_never_reach_the_terminal_record",
      "crates/jj-acp-cli/tests/run_task.rs#the_fixture_receives_the_harness_reserved_variables",
      "crates/jj-acp-cli/tests/run_task.rs#invalid_documents_are_refused_before_any_agent_starts",
      "crates/jj-acp-cli/tests/run_task.rs#duplicate_and_trailing_documents_are_refused",
      "crates/jj-acp-cli/tests/run_task.rs#an_oversized_document_is_refused_without_being_buffered",
      "crates/jj-acp-cli/tests/run_task.rs#a_task_addressed_to_another_runner_is_refused",
      "crates/jj-acp-cli/tests/run_task.rs#an_unknown_runner_is_refused_without_pinning_one",
      "crates/jj-acp-cli/tests/run_task.rs#an_unresolvable_runner_reports_a_spawn_failure",
      "crates/jj-acp-cli/tests/run_task.rs#a_deadline_produces_timed_out_and_exit_124",
      "crates/jj-acp-cli/tests/run_task.rs#an_idle_window_that_never_sees_a_frame_times_out_separately",
      "crates/jj-acp-cli/tests/signals.rs#sigterm_produces_bounded_cleanup_and_exit_143",
      "crates/jj-acp-cli/tests/signals.rs#sigint_produces_bounded_cleanup_and_exit_130",
      "crates/jj-acp-cli/tests/stdio_contract.rs#an_invalid_command_line_leaves_stdout_empty",
      "crates/jj-acp-cli/tests/stdio_contract.rs#a_failed_invocation_still_emits_exactly_one_record",
      "crates/jj-acp-cli/tests/stdio_contract.rs#the_agents_command_emits_one_json_object_per_line",
      "crates/jj-acp-cli/tests/args.rs#a_flag_overrides_the_matching_environment_variable",
      "crates/jj-acp-cli/tests/args.rs#every_option_is_settable_by_environment",
      "crates/jj-acp-protocol/tests/bounds.rs#a_read_cancelled_mid_frame_resumes_instead_of_losing_bytes",
      "crates/jj-acp-testkit/tests/fixture.rs#killing_the_agent_alone_leaves_its_descendant_running"
    ],
    "runs": [
      {
        "command": "cargo test --workspace --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/rust-architecture.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/check-boundaries.mjs",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "Rust 1.98.1; Python 3.14.7; tokio 1.53.1; nix 0.31; serde_json 1.0.151",
    "formats": "task document version 1; terminal record version 1; exit codes 0/1/2/124/130/143"
  },
  {
    "id": "E-M5-001",
    "tasks": ["M5"],
    "date": "2026-09-30",
    "kind": "lifecycle",
    "files": [
      "crates/jj-acp-runner/src/process.rs",
      "crates/jj-acp-cli/tests/cleanup.rs",
      "crates/jj-acp-testkit/src/fixture.rs",
      "crates/jj-acp-testkit/src/log.rs",
      "tests/fixtures/fixture_agent.py",
      "spec/src/runner.md",
      "spec/src/cli.md",
      "spec/src/compatibility.md"
    ],
    "tests": [
      "crates/jj-acp-cli/tests/cleanup.rs#a_forced_termination_takes_the_agents_descendant_with_it",
      "crates/jj-acp-cli/tests/cleanup.rs#a_completed_turn_still_does_not_leave_a_descendant_behind",
      "crates/jj-acp-cli/tests/cleanup.rs#an_agent_that_crashes_mid_turn_is_reported_and_never_re_dispatched",
      "crates/jj-acp-cli/tests/cleanup.rs#an_agent_that_crashes_during_startup_never_opens_a_session",
      "crates/jj-acp-cli/tests/cleanup.rs#a_crash_is_distinguished_from_a_bound_that_elapsed",
      "crates/jj-acp-cli/tests/cleanup.rs#the_helpers_resolve_the_real_fixture_paths"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-cli --test cleanup --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test --workspace --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "Rust 1.98.1; nix 0.31; Python 3.14.7; tokio 1.53.1; macOS ARM64",
    "formats": "termination report fields group_signalled/group_killed/reaped; fixture crash markers; dispatch counts",
    "note": "The containment test for a gracefully completing turn failed before process.rs signalled the group ahead of the grace window, which is the fix this entry records. Descendant containment on non-Unix platforms is unqualified: the direct child is signalled and the reduced guarantee is reported rather than approximated."
  },
  {
    "id": "E-M6-001",
    "tasks": ["M6"],
    "date": "2026-09-30",
    "kind": "real-runner",
    "files": [
      "crates/jj-acp-agent/src/defaults.rs",
      "crates/jj-acp-agent/tests/catalog.rs",
      "crates/jj-acp-cli/src/args.rs",
      "crates/jj-acp-cli/src/lib.rs",
      "crates/jj-acp-cli/tests/args.rs",
      "crates/jj-acp-protocol/src/initialize.rs",
      "crates/jj-acp-protocol/tests/payload.rs",
      "crates/jj-acp-protocol/tests/real_adapter.rs",
      "crates/jj-acp-protocol/tests/fixtures/cursor-acp-session.json",
      "spec/src/agents.md",
      "spec/src/protocol.md",
      "spec/src/cli.md",
      "spec/src/compatibility.md"
    ],
    "tests": [
      "crates/jj-acp-protocol/tests/real_adapter.rs#a_real_adapter_selects_protocol_version_one_and_is_accepted",
      "crates/jj-acp-protocol/tests/real_adapter.rs#a_real_adapter_names_its_option_id_rather_than_config_id",
      "crates/jj-acp-protocol/tests/real_adapter.rs#model_candidates_are_parameterized_and_matched_exactly",
      "crates/jj-acp-protocol/tests/real_adapter.rs#the_stable_surface_wins_when_both_surfaces_offer_the_model",
      "crates/jj-acp-protocol/tests/real_adapter.rs#a_model_only_on_the_unstable_surface_falls_back_to_set_model",
      "crates/jj-acp-protocol/tests/real_adapter.rs#this_adapter_exposes_no_reasoning_effort_option_of_its_own",
      "crates/jj-acp-protocol/tests/payload.rs#an_explicit_false_capability_is_not_advertised",
      "crates/jj-acp-agent/tests/catalog.rs#cursor_is_declared_with_the_transport_it_actually_advertises",
      "crates/jj-acp-cli/tests/args.rs#the_agents_command_parses_the_same_launch_options_as_run",
      "crates/jj-acp-cli/tests/args.rs#arguments_no_subcommand_would_read_are_refused",
      "crates/jj-acp-cli/tests/args.rs#agents_refuses_a_task_flag"
    ],
    "runs": [
      {
        "command": "cursor-agent acp \u2014 initialize and session/new handshake probe, no prompt",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "jj-acp run --task <scratch>/task.json --runner cursor --idle-timeout 60 --max-turn-duration 180 (reply READY, no tools)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "jj-acp run --task <scratch>/task.json --runner cursor (read notes.md, report first line, no modification)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test --workspace --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "cursor-agent 2026.09.26-dd393fe; ACP protocolVersion 1 negotiated from a request for 2; Rust 1.98.1; macOS ARM64",
    "formats": "captured initialize and session/new payloads; parameterized model values such as composer-2.5[fast=true]; configOption id rather than configId",
    "note": "Two real turns completed with status completed and stopReason end_turn, exit 0, no stderr noise, and the read target left unmodified. Three defects were found by the real adapter and none by the fixture suite: advertises() treated an explicit false as advertised, agents ignored --catalog, and trailing arguments after help were silently dropped. Qualification covers cursor only; codex-acp, claude-agent-acp, opencode and goose remain unlaunched, and an adapter that cannot reuse an existing login cannot be driven because authentication is deliberately unimplemented."
  },
  {
    "id": "E-M6-002",
    "tasks": ["M6"],
    "date": "2026-09-30",
    "kind": "real-runner",
    "files": [
      "crates/jj-acp-protocol/tests/real_adapter.rs",
      "crates/jj-acp-protocol/tests/fixtures/codex-acp-session.json",
      "spec/src/agents.md",
      "spec/src/protocol.md",
      "spec/src/compatibility.md"
    ],
    "tests": [
      "crates/jj-acp-protocol/tests/real_adapter.rs#a_second_adapter_also_selects_version_one_but_does_report_agent_info",
      "crates/jj-acp-protocol/tests/real_adapter.rs#explicit_false_capabilities_are_not_advertised_by_a_real_adapter",
      "crates/jj-acp-protocol/tests/real_adapter.rs#adapters_disagree_about_whether_model_values_are_parameterized",
      "crates/jj-acp-protocol/tests/real_adapter.rs#a_real_adapter_does_expose_a_reasoning_effort_option",
      "crates/jj-acp-protocol/tests/real_adapter.rs#adapters_emit_notifications_that_are_not_part_of_the_exchange",
      "crates/jj-acp-protocol/tests/real_adapter.rs#both_adapters_expose_modes_this_crate_does_not_yet_model"
    ],
    "runs": [
      {
        "command": "codex-acp 1.13.1 handshake probe (initialize + session/new), no prompt",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "PATH=<npx cache bin>:$PATH jj-acp run --task <scratch>/task.json --runner codex --idle-timeout 150 --max-turn-duration 240 (read notes.md, report first line, no modification)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test -p jj-acp-protocol --test real_adapter --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test --workspace --locked",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "@agentclientprotocol/codex-acp 1.13.1 resolved from the local npx cache; codex-cli 0.156.1 present but not an ACP agent; ACP protocolVersion 1 negotiated from a request for 2; ChatGPT login reused from ~/.codex/auth.json",
    "formats": "captured initialize and session/new payloads; bare model values such as gpt-6-astra; configOption categories mode, collaboration_mode, model, thought_level and model_config; _auth/status_update notification",
    "note": "One real read-only turn completed with status completed, stopReason end_turn and exit 0; notes.md was unmodified and no file was created. The codex CLI itself has no acp subcommand, so the ACP path is the adapter that acpx launches through npx on demand and that never appears on PATH. This adapter does expose a thought_level option with the adapter-defined identifier reasoning_effort, which Cursor does not, so JAC-RUN-006 stays Planned and the discovery helper is proven against real data rather than implemented end to end. No permission request was raised, so the default deny policy was not exercised against this adapter."
  },
  {
    "id": "E-M7-001",
    "tasks": ["M7"],
    "date": "2026-09-30",
    "kind": "real-runner",
    "files": [
      "crates/jj-acp-protocol/src/thinking.rs",
      "crates/jj-acp-protocol/src/config_option.rs",
      "crates/jj-acp-protocol/src/lib.rs",
      "crates/jj-acp-protocol/tests/session_config.rs",
      "crates/jj-acp-runner/src/configuration.rs",
      "crates/jj-acp-runner/src/outcome.rs",
      "crates/jj-acp-runner/src/turn.rs",
      "crates/jj-acp-runner/src/lib.rs",
      "crates/jj-acp-control/src/invocation.rs",
      "crates/jj-acp-cli/src/args.rs",
      "crates/jj-acp-cli/src/run_task.rs",
      "crates/jj-acp-cli/tests/args.rs",
      "crates/jj-acp-cli/tests/configuration.rs",
      "tests/fixtures/fixture_agent.py",
      "spec/src/protocol.md",
      "spec/src/runner.md",
      "spec/src/control.md",
      "spec/src/cli.md",
      "spec/src/compatibility.md"
    ],
    "tests": [
      "crates/jj-acp-protocol/tests/session_config.rs#effort_resolves_through_a_discovered_thought_level_option",
      "crates/jj-acp-protocol/tests/session_config.rs#effort_levels_outside_the_portable_vocabulary_stay_selectable",
      "crates/jj-acp-protocol/tests/session_config.rs#effort_resolves_through_parameterized_model_ids_when_no_option_exists",
      "crates/jj-acp-protocol/tests/session_config.rs#a_base_model_with_no_matching_effort_is_reported_honestly",
      "crates/jj-acp-protocol/tests/session_config.rs#a_bare_positional_bracket_is_not_read_as_an_effort",
      "crates/jj-acp-protocol/tests/session_config.rs#a_keyed_effort_wins_over_a_thinking_marker_on_the_same_identifier",
      "crates/jj-acp-protocol/tests/session_config.rs#mode_resolution_uses_both_advertised_surfaces",
      "crates/jj-acp-protocol/tests/session_config.rs#a_mode_offered_under_another_category_is_not_substituted",
      "crates/jj-acp-protocol/tests/session_config.rs#a_mode_present_in_the_option_but_not_in_available_modes_is_refused",
      "crates/jj-acp-cli/tests/configuration.rs#a_requested_model_effort_and_mode_all_reach_the_adapter",
      "crates/jj-acp-cli/tests/configuration.rs#configuration_is_applied_before_the_turn_is_dispatched",
      "crates/jj-acp-cli/tests/configuration.rs#a_value_offered_under_another_category_is_not_substituted",
      "crates/jj-acp-cli/tests/configuration.rs#an_unknown_model_is_reported_and_the_turn_still_completes",
      "crates/jj-acp-cli/tests/configuration.rs#configuration_arrives_by_environment_variable_too",
      "crates/jj-acp-cli/tests/args.rs#every_option_is_settable_by_environment"
    ],
    "runs": [
      {
        "command": "jj-acp run --runner cursor --effort high (live; effort resolved through a parameterized model identifier and accepted)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "jj-acp run --runner cursor --mode plan (live; applied with no shortfall)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "jj-acp run --runner cursor --model composer-2.5 --effort high (live; both reported unavailable, exit 0)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "jj-acp run --runner codex --mode read-only (live; applied)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "jj-acp run --runner codex --mode plan (live; refused naming collaboration_mode)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "jj-acp run --runner codex --effort high (live; reported unsupported because the adapter no longer advertises thought_level for the current model)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test --workspace --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "cursor-agent 2026.09.26-dd393fe; @agentclientprotocol/codex-acp 1.13.1; Rust 1.98.1; macOS ARM64",
    "formats": "normalized thinking levels off/low/medium/high/xhigh for matching only; effort attribute keys effort, reasoning_effort, variant, thinking; parameterized model identifiers",
    "note": "Three defects were found by asserting against real captures rather than synthetic payloads: advertised_effort returned the first attribute in document order so a thinking marker masked a level, the effort-surface decision was made after narrowing to one model so a model without effort reported the adapter as having no effort control at all, and the configuration tests initially resolved against the fixture envelope instead of its session/new result. A live codex session also stopped advertising thought_level between captures at the same adapter version, which corrected an earlier claim in compatibility.md; the thought_level path is fixture-proven but not live-proven. A bare positional bracket such as [medium] is deliberately not read as an effort."
  },
  {
    "id": "E-M8-001",
    "tasks": ["M8"],
    "date": "2026-09-30",
    "kind": "control-protocol",
    "files": [
      "crates/jj-acp-control/src/protocol.rs",
      "crates/jj-acp-control/src/serve.rs",
      "crates/jj-acp-control/src/invocation.rs",
      "crates/jj-acp-control/src/lib.rs",
      "crates/jj-acp-cli/src/serve.rs",
      "crates/jj-acp-cli/src/args.rs",
      "crates/jj-acp-cli/src/lib.rs",
      "crates/jj-acp-cli/src/run_task.rs",
      "crates/jj-acp-cli/tests/serve.rs",
      "crates/jj-acp-cli/tests/signals.rs",
      "spec/src/control.md",
      "spec/src/cli.md"
    ],
    "tests": [
      "crates/jj-acp-cli/tests/serve.rs#startup_announces_the_resolved_runners",
      "crates/jj-acp-cli/tests/serve.rs#a_submitted_task_returns_exactly_one_record",
      "crates/jj-acp-cli/tests/serve.rs#a_second_submission_while_one_is_in_flight_is_refused",
      "crates/jj-acp-cli/tests/serve.rs#cancelling_a_submission_that_is_not_in_flight_is_refused",
      "crates/jj-acp-cli/tests/serve.rs#a_runner_listing_is_answered_without_starting_an_agent",
      "crates/jj-acp-cli/tests/serve.rs#malformed_and_unsupported_frames_are_refused_and_the_loop_continues",
      "crates/jj-acp-cli/tests/serve.rs#an_invalid_task_document_is_reported_as_a_record_not_an_error",
      "crates/jj-acp-cli/tests/serve.rs#end_of_input_ends_the_loop_cleanly"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-cli --test serve --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test --workspace --locked (three consecutive clean runs)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "control protocol version 1; Rust 1.98.1; tokio 1.53.1",
    "formats": "newline-delimited control frames; requests submit/cancel/runners/shutdown; responses ready/record/runners/ack/error/diagnostic",
    "note": "The signals test was intermittently failing under parallel workspace load. The cause was the test, not the product: it raised the idle bound to 90 seconds but left the absolute cap at 60, so a deadline could end the turn before the signal arrived and report exit 124 instead of the signal code. Both bounds now exceed any plausible startup delay and three consecutive full workspace runs are clean. Execution is serial in this slice; a standing pool with caller-named scopes is M9."
  },
  {
    "id": "E-M9-001",
    "tasks": ["M9"],
    "date": "2026-10-01",
    "kind": "scheduler",
    "files": [
      "crates/jj-acp-control/src/scheduler.rs",
      "crates/jj-acp-control/src/serve.rs",
      "crates/jj-acp-control/src/transport.rs",
      "crates/jj-acp-control/src/protocol.rs",
      "crates/jj-acp-control/src/lib.rs",
      "crates/jj-acp-control/tests/serve.rs",
      "crates/jj-acp-cli/src/args.rs",
      "crates/jj-acp-cli/src/serve.rs",
      "crates/jj-acp-cli/tests/serve.rs",
      "spec/src/control.md",
      "spec/src/cli.md",
      "spec/src/milestones.md",
      "spec/src/verification.md"
    ],
    "tests": [
      "crates/jj-acp-control/tests/serve.rs#the_ready_frame_announces_sizing_and_the_catalogue",
      "crates/jj-acp-control/tests/serve.rs#one_scope_serializes_while_distinct_scopes_overlap",
      "crates/jj-acp-control/tests/serve.rs#a_crashed_agent_releases_its_slot_without_re_dispatch",
      "crates/jj-acp-control/tests/serve.rs#a_malformed_frame_never_stops_the_loop",
      "crates/jj-acp-control/tests/serve.rs#a_submission_completes_and_the_loop_keeps_serving",
      "crates/jj-acp-cli/tests/serve.rs#submissions_in_one_scope_serialize_rather_than_overlap",
      "crates/jj-acp-cli/tests/serve.rs#submissions_in_distinct_scopes_run_concurrently",
      "crates/jj-acp-cli/tests/serve.rs#a_full_queue_is_refused_rather_than_grown",
      "crates/jj-acp-cli/tests/serve.rs#a_crashed_agent_releases_its_slot_and_is_never_re_dispatched",
      "crates/jj-acp-cli/tests/args.rs#malformed_command_lines_are_refused_before_any_work"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-control --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test -p jj-acp-cli --test serve --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test --workspace --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/rust-architecture.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "control protocol version 1 with scopes; Rust 1.98.1; tokio 1.53.1",
    "formats": "requests submit/cancel/runners/status/shutdown; responses ready/queued/record/runners/status/ack/error/diagnostic",
    "note": "Two defects were found while building this. Emitting a record directly from the worker task let a peer observe a record and then a status still listing that submission as running, so completions now route through the loop and release the scope first. And serve.rs reached 334 counted lines, which the budget gate rejected; the scheduler was extracted rather than baselined. The M8 contract changed deliberately: a second submission in the same scope is now queued and acknowledged rather than refused with busy, because serializing a scope means the second turn waits. Warm agent reuse across turns is not implemented; each turn spawns and tears down its own agent process. The M8 test asserting a busy refusal was replaced by scope-serialization coverage and the replacement is recorded as a tracked removal.",
    "removed_tests": [
      "crates/jj-acp-cli/tests/serve.rs#a_second_submission_while_one_is_in_flight_is_refused"
    ]
  },
  {
    "id": "E-BOOT-003",
    "tasks": ["Bootstrap.2"],
    "date": "2026-10-01",
    "kind": "governance",
    "files": [
      "Cargo.toml",
      "Cargo.lock",
      "package.json",
      "spec-validator.config.mjs",
      "spec/book.toml",
      "AGENTS.md",
      "README.md",
      "scripts/check-boundaries.mjs",
      "scripts/check-progress.mjs",
      "scripts/rust-architecture.mjs",
      "scripts/governance.test.mjs",
      "scripts/rust-architecture.test.mjs",
      "docs/upstream/disposition.json",
      "docs/upstream/lock.json",
      "spec/src/architecture.md",
      "spec/src/spec-governance.md",
      "crates/jj-acp-cli/tests/signals.rs"
    ],
    "tests": [
      "scripts/governance.test.mjs#accepts_verified_task",
      "scripts/governance.test.mjs#rejects_excluded_nostr_transport_and_database_dependencies",
      "scripts/governance.test.mjs#the_testkit_may_depend_on_the_layers_it_exercises",
      "scripts/governance.test.mjs#the_protocol_crate_declares_no_process_or_network_facility",
      "scripts/rust-architecture.test.mjs#allows_the_testkit_crate_to_name_itself",
      "crates/jj-acp-cli/tests/signals.rs#sigterm_produces_bounded_cleanup_and_exit_143",
      "crates/jj-acp-cli/tests/signals.rs#sigint_produces_bounded_cleanup_and_exit_130"
    ],
    "runs": [
      {
        "command": "cargo build --workspace --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test --workspace --locked (three consecutive full runs)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/governance.test.mjs scripts/rust-architecture.test.mjs scripts/upstream-lock.test.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/check-boundaries.mjs && node scripts/rust-architecture.mjs && node scripts/check-progress.mjs && node scripts/upstream-sync.mjs --self-test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "spec-validator validate && spec-validator first && spec-validator check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "jj-acp --version && jj-acp agents",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "Rust 1.98.1; Node v24.10.0; pnpm 10.34.5; @lapismd/spec-validator 0.1.0; binary jj-acp 0.1.0",
    "formats": "requirement identifiers JAC-<AREA>-NNN; environment namespace JJ_ACP_*; reserved task identity JJ_ACP_TASK_*; crate names jj-acp-<layer>",
    "note": "Renamed the repository, six crates, the binary, the npm package, the requirement prefix and the environment namespace from lapis-acp to jj-acp in one atomic change. The rename exposed a change-map defect: crate rules keyed on the literal product name, so deletions at the old paths matched the protected list but no rule and the rename commit failed as unmapped. The rules now key on the crate layer, which is what actually determines the owning chapter, and still fail closed for a crate whose layer is not listed. It also exposed that the signals test startup bound was tuned to an idle machine; a full workspace run starts roughly fifteen binaries in parallel and the 30 second wait tripped intermittently, so the bound is now set so that only a genuine failure trips it. Three consecutive full workspace runs are clean."
  },
  {
    "id": "E-M11-001",
    "tasks": ["M11"],
    "date": "2026-10-06",
    "kind": "interactive-control",
    "files": [
      "README.md",
      "crates/jj-acp-cli/src/args.rs",
      "crates/jj-acp-cli/src/control_version.rs",
      "crates/jj-acp-cli/src/lib.rs",
      "crates/jj-acp-cli/src/serve.rs",
      "crates/jj-acp-cli/tests/interactive.rs",
      "crates/jj-acp-control/src/interactive.rs",
      "crates/jj-acp-control/src/interactive_handler.rs",
      "crates/jj-acp-control/src/interactive_session.rs",
      "crates/jj-acp-control/src/interactive_wire.rs",
      "crates/jj-acp-control/src/interactive_worker.rs",
      "crates/jj-acp-control/src/lib.rs",
      "crates/jj-acp-control/src/transport.rs",
      "crates/jj-acp-control/tests/interactive_wire.rs",
      "crates/jj-acp-protocol/src/client.rs",
      "crates/jj-acp-protocol/src/handler.rs",
      "crates/jj-acp-runner/src/launch.rs",
      "crates/jj-acp-runner/src/lib.rs",
      "crates/jj-acp-runner/src/persistent.rs",
      "crates/jj-acp-runner/src/process.rs",
      "spec/src/architecture.md",
      "spec/src/cli.md",
      "spec/src/compatibility.md",
      "spec/src/control.md",
      "spec/src/index.md",
      "spec/src/milestones.md",
      "spec/src/protocol.md",
      "spec/src/runner.md",
      "spec/src/verification.md",
      "tests/fixtures/interactive_agent.py"
    ],
    "tests": [
      "crates/jj-acp-cli/tests/interactive.rs#follow_up_reuses_the_session_and_streams_activity",
      "crates/jj-acp-cli/tests/interactive.rs#permissions_validate_exact_options_and_expire_after_resolution",
      "crates/jj-acp-cli/tests/interactive.rs#cancellation_remains_serviceable_during_permission_wait",
      "crates/jj-acp-cli/tests/interactive.rs#permission_wait_is_inside_the_absolute_deadline",
      "crates/jj-acp-cli/tests/interactive.rs#unsupported_modes_block_prompts_and_duplicates_never_run_again",
      "crates/jj-acp-cli/tests/interactive.rs#resume_is_explicit_and_does_not_send_a_prompt",
      "crates/jj-acp-control/tests/interactive_wire.rs#partial_frames_survive_cancelled_reads"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-cli --test serve --locked (before)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test -p jj-acp-cli --test interactive -p jj-acp-control --test interactive_wire --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings (initial)",
        "result": "fail",
        "failures": 4
      },
      {
        "command": "cargo clippy --workspace --all-targets --locked -- -D warnings",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/rust-architecture.mjs (initial)",
        "result": "fail",
        "failures": 1
      },
      {
        "command": "node scripts/rust-architecture.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "Rust 1.98.1; macOS ARM64; control v2; deterministic Python peer",
    "formats": "Explicit interactive control v2; existing task/record/v1 control contracts retained",
    "note": "Before: v1 serve 11/11 and no interactive control. New target: six binary and three transport/input tests pass. Two compile/format repair attempts preceded the passing target; four clippy findings and one 301-line budget failure were repaired without suppressions. Signal/drop containment has a process-group fallback. Cross-layer changes require the full source check/test/build/spec lanes. All four required wider gates passed. Real interactive host qualification remains a distinct M10 gate."
  },
  {
    "id": "E-M11-002",
    "tasks": ["M11"],
    "date": "2026-10-06",
    "kind": "interactive-overflow-regression",
    "files": [
      "crates/jj-acp-control/src/interactive.rs",
      "crates/jj-acp-control/src/interactive_handler.rs",
      "crates/jj-acp-control/src/interactive_worker.rs",
      "crates/jj-acp-control/tests/interactive_overflow.rs",
      "spec/src/control.md",
      "spec/src/milestones.md",
      "spec/src/verification.md"
    ],
    "tests": [
      "crates/jj-acp-control/tests/interactive_overflow.rs#output_overflow_never_reports_completed_or_reusable_session"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-control --test interactive_overflow --locked (initial)",
        "result": "fail",
        "failures": 1
      },
      {
        "command": "cargo test -p jj-acp-control --test interactive_overflow --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check (initial, before ledger update)",
        "result": "fail",
        "failures": 2
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0
      }
    ],
    "remaining": [],
    "versions": "Rust 1.98.1; control version 2",
    "formats": "control/v2; terminal record/v1 unchanged",
    "note": "Consumer integration uncovered a completed-turn race after event output overflow. Coverage loss now remains latched after queue recovery and closes the session; terminal records report outcome_unknown/activity_coverage_lost. watch::send_replace also retains cancellation without a current subscriber. M11 review: fixture gate remains Done after all four required source gates passed; M10 real host acceptance stays separate.",
    "result": "pass"
  },
  {
    "id": "E-M11-003",
    "tasks": ["M11"],
    "date": "2026-10-06",
    "kind": "queued-cancellation-regression",
    "files": [
      "crates/jj-acp-control/src/interactive_worker.rs",
      "crates/jj-acp-cli/tests/interactive.rs",
      "spec/src/control.md",
      "spec/src/milestones.md",
      "spec/src/verification.md",
      "spec/src/cli.md",
      "spec/src/runner.md"
    ],
    "tests": [
      "crates/jj-acp-cli/tests/interactive.rs#cancelling_a_queued_prompt_keeps_the_warm_session_usable"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-cli --test interactive --locked cancelling_a_queued_prompt_keeps_the_warm_session_usable (before)",
        "result": "fail",
        "failures": 1
      },
      {
        "command": "cargo test -p jj-acp-cli --test interactive --locked cancelling_a_queued_prompt_keeps_the_warm_session_usable",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check (before ledger update)",
        "result": "fail",
        "failures": 2
      },
      {
        "command": "pnpm check (partial was not an accepted ledger result)",
        "result": "fail",
        "failures": 1
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:check (before mapped CLI chapter update)",
        "result": "fail",
        "failures": 1
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0
      }
    ],
    "remaining": [],
    "versions": "Rust 1.98.1; control version 2",
    "formats": "control/v2; terminal record/v1 unchanged",
    "note": "Enclosing revision mkmlwqzt. Queued cancellation previously omitted sessionUsable even while retaining the warm session; jj-store would mark that live session interrupted. The terminal frame now reports usability and the next explicit prompt succeeds. M11 review: remains Done after its required wider gates were rerun; M10 real host acceptance remains separate.",
    "result": "pass"
  },
  {
    "id": "E-M10-001",
    "tasks": ["M10"],
    "date": "2026-10-06",
    "kind": "real-consumer",
    "files": [
      "README.md",
      "spec/src/architecture.md",
      "spec/src/verification.md",
      "spec/src/milestones.md"
    ],
    "tests": [],
    "runs": [
      {
        "command": "In jj-store: PATH=/Users/stevejuma/.npm/_npx/bb64e4387f65cc68/node_modules/.bin:$PATH JJ_STORE_AGENT_PRIVACY_CACHE=/private/tmp/jj-store-agent-fixture-cache-20261006 JJ_STORE_AGENT_TEST_ACP=/Users/stevejuma/code/lapismd/jj-acp/target/debug/jj-acp cargo test -p jj-store-cli --test agent_execution --locked real::installed_codex_and_cursor_edit_isolated_workspaces_and_publish_history -- --ignored --exact",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "In jj-store: PATH=/Users/stevejuma/.npm/_npx/bb64e4387f65cc68/node_modules/.bin:$PATH JJ_STORE_AGENT_PRIVACY_CACHE=/private/tmp/jj-store-agent-fixture-cache-20261006 python3 scripts/agent-execution-acceptance.py --server target/debug/jj-store --jj target/debug/jj --rustfs runtime/embedded-rustfs/target/debug/jj-store-rustfs --acp /Users/stevejuma/code/lapismd/jj-acp/target/debug/jj-acp --web web/dist --browser",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "In jj-store: JJ_STORE_DESKTOP_TEST_RESOURCES=$PWD/desktop/resources JJ_STORE_AGENT_PRIVACY_CACHE=/private/tmp/jj-store-agent-fixture-cache-20261006 cargo test --manifest-path desktop/Cargo.toml --test managed_runtime --locked real_managed_runtime_restarts_preserves_origin_and_rejects_occupied_port -- --ignored --exact",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check:progress",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:first",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:validate",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm format:check",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "versions": "macOS ARM64; Rust 1.98.1; JJ 0.45.1; codex-acp 1.13.1 from existing npx cache; Cursor 2026.10.01-e373342; source binary mkmlwqzt/5f7e3fe1; host revision zontovyv with E-EXEC-001/002.",
    "formats": "Interactive control/v2 and terminal record/v1 unchanged. The host owns private device journals, exact JJ workspace selection and sanitized AgentStore publication; no harness persistence or authority was added.",
    "note": "Enclosing source change somnxvvl. M10 is Done after real jj-store consumer qualification: both installed adapters edited isolated ordinary Git-backed JJ and native real-RustFS workspaces, preserved dirty source files and published sanitized repository history. Cold production Chromium saved a named setup, launched/streamed/ended a native session, opened retained review and passed Axe including color contrast. Cold host restart retained exact session/event sequences, and explicit capability-gated resume or labelled saved-context continuation dispatched no prompt. Managed owner-EOF/restart lifetime passed separately. The host repaired newline-bearing Cursor projection aliases, independent saved-setup revisions, stream error lifetime and caption contrast in its owning modules; fixture cancellation usability was repaired in source E-M11-003 and passed all four source gates. 0 live permission requests were raised by these real edits; fixture permission/cancellation/deadline evidence remains distinct. Signed app/DMG/WKWebView, other platforms/adapters, live permission qualification and distribution remain outside this consumer profile. Optional unrelated host clippy diagnostics are recorded in E-EXEC-001, not claimed as a passing lane. M11 stays Done; no milestone status is inferred from compilation."
  },
  {
    "id": "E-M12-001",
    "tasks": ["M12"],
    "date": "2026-10-07",
    "kind": "warm worker control and session fencing",
    "versions": "macOS ARM64; Rust 1.98.1; JJ 0.45.1; installed Codex ACP 1.13.1 from existing npx cache; enclosing change yxwxpvkv",
    "formats": "control version 3; existing versions 1/2 unchanged; terminal record version 1",
    "files": [
      "crates/jj-acp-cli/src/args.rs",
      "crates/jj-acp-cli/src/control_version.rs",
      "crates/jj-acp-cli/src/serve.rs",
      "crates/jj-acp-cli/tests/fleet.rs",
      "crates/jj-acp-control/src/fleet.rs",
      "crates/jj-acp-control/src/fleet_wire.rs",
      "crates/jj-acp-control/src/fleet_worker.rs",
      "crates/jj-acp-control/src/interactive.rs",
      "crates/jj-acp-control/src/interactive_handler.rs",
      "crates/jj-acp-control/src/interactive_worker.rs",
      "crates/jj-acp-control/src/lib.rs",
      "crates/jj-acp-control/src/warm_worker.rs",
      "crates/jj-acp-control/tests/interactive_overflow.rs",
      "crates/jj-acp-protocol/src/client.rs",
      "crates/jj-acp-protocol/src/handler.rs",
      "spec/src/cli.md",
      "spec/src/compatibility.md",
      "spec/src/control.md",
      "spec/src/protocol.md",
      "spec/src/runner.md",
      "spec/src/verification.md",
      "tests/fixtures/fleet_agent.py",
      "spec/src/milestones.md"
    ],
    "tests": [
      "crates/jj-acp-cli/tests/fleet.rs#warm_worker_opens_distinct_sessions_without_reinitializing"
    ],
    "runs": [
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target cargo test -p jj-acp-cli --test fleet --locked (before)",
        "result": "fail",
        "failures": 1
      },
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target cargo test -p jj-acp-cli --test interactive --locked (before)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target cargo test -p jj-acp-cli --test fleet --test interactive --locked",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target cargo test -p jj-acp-cli --test fleet --locked (scope fencing)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo check -p jj-acp-cli --locked (temporary borrow fixes)",
        "result": "fail",
        "failures": 2
      },
      {
        "command": "cargo test -p jj-acp-cli --test fleet --locked (fixture configuration correction)",
        "result": "fail",
        "failures": 1
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check (queue Result lint)",
        "result": "fail",
        "failures": 1
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo test -p jj-acp-cli --test fleet --locked (scoped responses and release cancellation)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "python3 /tmp/jj-acp-fleet-real.py (initial two-session installed adapter run)",
        "result": "fail",
        "failures": 1,
        "note": "Second turn invalidated by delayed update from the known inactive first adapter session; no uncertain prompt replay."
      },
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target cargo test --locked -p jj-acp-cli --test fleet (known inactive session regression)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "python3 /tmp/jj-acp-fleet-real.py",
        "result": "pass",
        "failures": 0,
        "note": "Installed Codex ACP 1.13.1 warm without session/prompt; two distinct application sessions completed read-only turns on one initialized worker; exact scope; close reaped; zero permission requests."
      },
      {
        "command": "PATH=<existing adapter cache>/node_modules/.bin:$PATH JJ_STORE_AGENT_TEST_ACP=<rebuilt jj-acp> CARGO_TARGET_DIR=../jj-store/target cargo test --locked -p jj-store-cli --test agent_execution real::installed_fleet_warms_general_chat_then_reuses_an_attached_workspace -- --ignored --exact --nocapture",
        "result": "pass",
        "failures": 0,
        "note": "Real jj-store consumer: general turn, attached isolated workspace edit, preserved dirty source, canonical publication and exact duplicate without replay; zero permissions."
      },
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target pnpm spec:check and pnpm check (before final Markdown formatting)",
        "result": "fail",
        "failures": 2,
        "note": "control/protocol Markdown required Prettier; formatted in place."
      },
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target pnpm spec:check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "CARGO_TARGET_DIR=../jj-acp/target pnpm build",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pending",
    "remaining": [
      "Cold native jj-store integration, other version-3 adapters/platforms and real permission/cancellation exchange are separate qualification gates."
    ],
    "note": "Enclosing change yxwxpvkv. Warm initialization sends neither session/new nor a prompt. Multiple session directories share one initialized adapter. Exact application scopes fence permissions, cancellation and events. Release cancels active/queued turns in its own scope. The installed Codex regression exposed delayed known-inactive session updates; these are now discarded and stale permissions cancelled, while unknown identities fail the worker. No uncertain prompt is replayed. M12 remains In progress with fixture and installed Codex gates passing; native cold consumer and other adapters remain separately qualified."
  },
  {
    "id": "E-M12-002",
    "tasks": ["M12"],
    "date": "2026-10-07",
    "kind": "separate prepared context blocks",
    "versions": "macOS ARM64; Rust 1.98.1; JJ 0.45.1; enclosing change ylkuyxzx; fixture evidence only",
    "formats": "additive optional context in controls v2/v3; ordered ACP text/resource blocks; terminal record v1 unchanged",
    "files": [
      "crates/jj-acp-control/src/interactive_wire.rs",
      "crates/jj-acp-control/src/interactive_worker.rs",
      "crates/jj-acp-control/src/fleet_worker.rs",
      "crates/jj-acp-runner/src/persistent.rs",
      "crates/jj-acp-protocol/src/session.rs",
      "crates/jj-acp-protocol/tests/payload.rs",
      "crates/jj-acp-control/tests/interactive_wire.rs",
      "crates/jj-acp-cli/tests/fleet.rs",
      "tests/fixtures/fleet_agent.py",
      "spec/src/cli.md",
      "spec/src/compatibility.md",
      "spec/src/runner.md",
      "spec/src/control.md",
      "spec/src/protocol.md",
      "spec/src/verification.md",
      "spec/src/milestones.md"
    ],
    "tests": [
      "crates/jj-acp-protocol/tests/payload.rs#context_blocks_are_capability_gated_and_preserve_authored_bytes",
      "crates/jj-acp-control/tests/interactive_wire.rs#prompt_context_is_optional_bounded_data_without_launch_authority",
      "crates/jj-acp-cli/tests/fleet.rs#warm_worker_opens_distinct_sessions_without_reinitializing"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-protocol --test payload -p jj-acp-control --test interactive_wire --locked",
        "result": "pass",
        "detail": "27 protocol tests and 4 control tests pass.",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "detail": "Full source tests passed before the later fixture assertion addition.",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "detail": "All source binaries built before package cache cleanup.",
        "failures": 0
      },
      {
        "command": "CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo test -p jj-acp-cli --test fleet --locked",
        "result": "pass",
        "failures": 0,
        "note": "Exact resource/authored block assertions and scope fencing pass after cache cleanup."
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0,
        "detail": "Full source formatting, types, boundaries, architecture and clippy all targets pass."
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0,
        "detail": "All 11 specification lanes pass after the runner, CLI and compatibility topic updates."
      },
      {
        "command": "CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 pnpm test",
        "result": "fail",
        "failures": 1,
        "detail": "The forced descendant cleanup fixture missed its PID under load; no product assertion was loosened."
      },
      {
        "command": "CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 cargo test -p jj-acp-cli --test cleanup a_forced_termination_takes_the_agents_descendant_with_it --locked -- --exact",
        "result": "pass",
        "failures": 0,
        "detail": "The exact cleanup regression passes unchanged."
      },
      {
        "command": "CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 pnpm test",
        "result": "pass",
        "failures": 0,
        "detail": "Final complete source test and doctest rerun passes, including resource/authored block fixture assertions."
      },
      {
        "command": "CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2 pnpm build",
        "result": "pass",
        "failures": 0,
        "detail": "Final locked build passes."
      },
      {
        "command": "pnpm spec:check",
        "result": "fail",
        "failures": 1,
        "detail": "Progress validation requires pending while a live interpretation gap remains; restored the scoped pending result without removing that gate."
      }
    ],
    "result": "pending",
    "remaining": [
      "Installed adapter interpretation of embedded context resources remains an independent live qualification gate; these results are fixture and source evidence."
    ],
    "note": "Prepared context is separate from authored text. Embedded resources require advertised support, otherwise context uses an independent text block in the same turn. No preliminary turn or replay. Initial check refused the missing evidence update; the first fixture assertion used a text-only prompt and was repaired to send context. Its next compilation ran out of disk space; generated package caches were cleared. M12 remains In progress and real installed-adapter qualification is not inferred from fixture block assertions. Exact cleanup retry and the final full test rerun pass unchanged. Full check, all specification lanes and final build pass. No package was published, installed binary replaced or real prompt sent."
  },
  {
    "id": "E-M12-003",
    "tasks": ["M12"],
    "date": "2026-10-09",
    "kind": "comments-only prepared prompt context",
    "versions": "macOS ARM64; Rust 1.98.1; enclosing Jujutsu change lwlxyuwu",
    "formats": "control versions 2/3; optional prepared context; record v1 unchanged",
    "files": [
      "crates/jj-acp-control/src/interactive_wire.rs",
      "crates/jj-acp-control/tests/interactive_wire.rs",
      "crates/jj-acp-cli/tests/fleet.rs",
      "spec/src/control.md",
      "spec/src/verification.md",
      "spec/src/milestones.md",
      "spec/src/cli.md"
    ],
    "tests": [
      "crates/jj-acp-control/tests/interactive_wire.rs#context_only_prompt_requires_nonempty_prepared_content",
      "crates/jj-acp-cli/tests/fleet.rs#warm_worker_opens_distinct_sessions_without_reinitializing"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-control --test interactive_wire context_only_prompt_requires_nonempty_prepared_content --locked -- --exact (initial test)",
        "result": "fail",
        "failures": 1,
        "detail": "Test initially referenced a private module; removed that access and kept version-3 coverage in the CLI fleet test."
      },
      {
        "command": "cargo test -p jj-acp-control --test interactive_wire context_only_prompt_requires_nonempty_prepared_content --locked -- --exact",
        "result": "fail",
        "failures": 1,
        "detail": "Empty authored text with nonempty prepared context was refused."
      },
      {
        "command": "cargo test -p jj-acp-control --test interactive_wire --locked",
        "result": "pass",
        "failures": 0,
        "detail": "5 tests."
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0,
        "detail": "Full source tests, governance and doctests including version-3 context-only fixture."
      },
      {
        "command": "pnpm check",
        "result": "fail",
        "failures": 1,
        "detail": "Evidence test references needed file paths; corrected the references."
      },
      {
        "command": "pnpm spec:check",
        "result": "fail",
        "failures": 1,
        "detail": "Evidence test references needed file paths; corrected the references."
      },
      {
        "command": "pnpm check (corrected evidence)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:check (corrected evidence)",
        "result": "fail",
        "failures": 1,
        "detail": "Spec-first requires the owning CLI chapter for its fleet-test change; added comments-only CLI contract."
      },
      {
        "command": "pnpm spec:check (CLI chapter updated)",
        "result": "pass",
        "failures": 0,
        "detail": "11 lanes, including full tests, formatting and spec-first."
      }
    ],
    "result": "pending",
    "remaining": [
      "Other v3 adapters/platforms and real permission/cancellation exchanges remain separate M12 gates."
    ],
    "note": "The jj-store consumer exposed an old staged binary rejecting the optional context field and the current parser rejecting comments-only sends. Rebuilt source accepts nonblank context with empty authored text, still refuses fully empty prompts, and preserves the exact text/context blocks without manufactured message content. M12 remains In progress; no added credentials, launch authority or retries. The rebuilt helper was staged only in the jj-store-chat-body ignored development runtime. Its installed Codex consumer completed a read-only sample prompt and a comments-only turn answering three comments at two anchors, with two committed answer links. Consumer evidence is E-CHAT-LIVE-007; no release or installed CLI replacement."
  },
  {
    "id": "E-M12-004",
    "tasks": ["M12"],
    "date": "2026-10-10",
    "kind": "request-scoped session rejection",
    "versions": "macOS ARM64; Rust 1.98.1; enclosing Jujutsu change mtvyswyw",
    "formats": "control v3 unchanged; version-1 records unchanged",
    "files": [
      "crates/jj-acp-control/src/fleet_worker.rs",
      "crates/jj-acp-cli/tests/fleet.rs",
      "spec/src/control.md",
      "spec/src/runner.md",
      "spec/src/cli.md",
      "spec/src/verification.md",
      "spec/src/milestones.md"
    ],
    "tests": [
      "crates/jj-acp-cli/tests/fleet.rs#warm_worker_opens_distinct_sessions_without_reinitializing"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-cli --test fleet warm_worker_opens_distinct_sessions_without_reinitializing --locked -- --exact (before)",
        "result": "fail",
        "failures": 1,
        "detail": "A valid rejected load invalidated another configured scope; next configure reported session_not_open."
      },
      {
        "command": "cargo test -p jj-acp-cli --test fleet warm_worker_opens_distinct_sessions_without_reinitializing --locked -- --exact",
        "result": "pass",
        "failures": 0,
        "detail": "Rejected load leaves existing configured scope usable; foreign-session uncertainty still invalidates it."
      },
      {
        "command": "pnpm check (initial)",
        "result": "fail",
        "failures": 1,
        "detail": "Three edited canonical Markdown files needed formatting."
      },
      {
        "command": "pnpm exec prettier --write spec/src/control.md spec/src/runner.md spec/src/cli.md spec/src/verification.md spec/src/milestones.md",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check (formatted)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0,
        "detail": "Full governance, crate tests and doctests."
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "jj-store cold UI Send with staged rebuilt helper",
        "result": "pass",
        "failures": 0,
        "detail": "Exact MIT prompt completed, request 2481f6b2b8cd46fcb747e5860b281a55; one LICENSE change, published 435/435. Consumer E-AGENT-RESUME-001."
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0,
        "detail": "All 11 source lanes including full tests, mdBook and spec-first."
      }
    ],
    "result": "pending",
    "remaining": [
      "Other adapters/platforms and independent live permission/cancellation gates remain."
    ],
    "note": "M12 remains In progress. Valid RPC open/configure rejection preserves the initialized worker and unrelated scopes, invalidating only the rejected scope. Uncertain prompt execution, timeout, protocol failure and process exit retain worker-wide invalidation. No prompt retry or raw diagnostic publication."
  },
  {
    "id": "E-TOOLS-001",
    "tasks": ["M12"],
    "requirements": [
      "JAC-CLI-008",
      "JAC-PROTO-008",
      "JAC-RUN-009",
      "JAC-CTRL-009"
    ],
    "date": "2026-10-07",
    "files": [
      "crates/jj-acp-cli/src/tools.rs",
      "crates/jj-acp-control/src/invocation.rs",
      "crates/jj-acp-cli/tests/fleet.rs"
    ],
    "tests": [
      "crates/jj-acp-cli/src/tools/tests.rs#trusted_tools_refuse_duplicate_names_relative_commands_and_reserved_identity",
      "crates/jj-acp-cli/tests/fleet.rs#warm_worker_opens_distinct_sessions_without_reinitializing",
      "crates/jj-acp-cli/src/tools/tests.rs#trusted_tools_are_bounded_and_diagnostics_do_not_echo_values"
    ],
    "runs": [
      {
        "command": "CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/Users/stevejuma/code/lapismd/jj-acp/target pnpm check",
        "result": "fail",
        "failures": 1,
        "note": "Progress checker correctly rejected missing same-change evidence. Added this record before rerunning."
      },
      {
        "command": "CARGO_BUILD_JOBS=2 CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/Users/stevejuma/code/lapismd/jj-acp/target pnpm check",
        "result": "fail",
        "failures": 1,
        "note": "Evidence schema required kind/versions/formats and actual test file paths. Corrected record."
      },
      {
        "command": "CARGO_BUILD_JOBS=2 CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/Users/stevejuma/code/lapismd/jj-acp/target pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "CARGO_BUILD_JOBS=2 CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/Users/stevejuma/code/lapismd/jj-acp/target pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "CARGO_BUILD_JOBS=2 CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/Users/stevejuma/code/lapismd/jj-acp/target pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:check (first run)",
        "result": "fail",
        "failures": 1,
        "note": "All executable lanes passed. Spec-first required compatibility chapter for the modified fleet fixture; added owning explanation."
      },
      {
        "command": "CARGO_BUILD_JOBS=2 CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/Users/stevejuma/code/lapismd/jj-acp/target pnpm spec:check",
        "result": "pass",
        "failures": 0,
        "note": "11 lanes, including cold source tests and mapped canonical chapter check."
      }
    ],
    "result": "pass",
    "remaining": [],
    "note": "Enclosing change mkopntkz. Trusted bounded launch declarations extend control v2/v3 and one-shot session creation/load. Task/control input never selects tools. M12 remains In progress; new source fixtures do not close installed adapter/platform qualification.",
    "kind": "verification",
    "versions": "macOS ARM64; Rust 1.98.1; ACP 1; stdio control v2/v3",
    "formats": "Trusted tool JSON; no durable/public format change"
  },
  {
    "id": "E-TOOLS-002",
    "tasks": ["M12"],
    "date": "2026-10-14",
    "kind": "workspace integration",
    "versions": "Rust 1.98.1; macOS ARM64; JJ 0.45.1",
    "formats": "task/record v1; control v2/v3; trusted tool JSON unchanged",
    "files": [
      "crates/jj-acp-cli/tests/fleet.rs",
      "crates/jj-acp-cli/src/tools.rs",
      "spec/src/cli.md",
      "spec/src/milestones.md"
    ],
    "tests": [
      "crates/jj-acp-cli/src/tools/tests.rs#trusted_tools_refuse_duplicate_names_relative_commands_and_reserved_identity",
      "crates/jj-acp-cli/tests/fleet.rs#warm_worker_opens_distinct_sessions_without_reinitializing"
    ],
    "runs": [
      {
        "command": "cargo test -p jj-acp-cli --test fleet --locked (before incorporation)",
        "result": "pass",
        "failures": 0,
        "detail": "1 test passed; main still omitted trusted tool support."
      },
      {
        "command": "target/debug/jj-acp agents --tools /dev/null (before incorporation)",
        "result": "fail",
        "failures": 1,
        "detail": "Exit 2: unknown option --tools."
      },
      {
        "command": "cargo test -p jj-acp-cli --test fleet --test interactive --lib --locked",
        "result": "pass",
        "failures": 0,
        "detail": "2 tools tests, 1 fleet test and 7 interactive tests passed after combining both lineages."
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "note": "Rebased local-compute change mkopntkz onto mtvyswyw; preserved prepared context, comments-only prompts and request-scoped RPC rejection. Historical evidence retained. Live v3 qualification remains separate."
  },
  {
    "id": "E-RELEASE-001",
    "tasks": ["M13"],
    "date": "2026-10-14",
    "kind": "release preparation",
    "versions": "Rust 1.98.1; Node 26.9.0 locally / Node 22 in CI; pnpm 10.34.5; mdBook 0.5.4; macOS ARM64",
    "formats": "coordinated crate 0.1.0; task/record v1; controls v1/v2/v3 unchanged",
    "files": [
      "Cargo.toml",
      "Cargo.lock",
      "README.md",
      "LICENSE.md",
      "scripts/progress-vcs.mjs",
      "scripts/progress-vcs.test.mjs",
      "scripts/release.test.mjs",
      "scripts/package-check.mjs",
      ".github/workflows/ci.yml",
      ".github/workflows/release.yml",
      ".github/workflows/publish-spec-pages.yml",
      "spec/src/release.md",
      "spec/src/verification.md",
      "crates/jj-acp-cli/tests/common/mod.rs",
      "crates/jj-acp-cli/tests/scratch.rs",
      "scripts/release-assets.mjs"
    ],
    "removed_files": [
      "tests/fixtures/fixture_agent.py",
      "tests/fixtures/fleet_agent.py",
      "tests/fixtures/interactive_agent.py"
    ],
    "tests": [
      "scripts/progress-vcs.test.mjs#explicit_git_range_wins_over_local_jj",
      "scripts/progress-vcs.test.mjs#incomplete_range_fails_closed",
      "scripts/progress-vcs.test.mjs#initial_git_commit_uses_empty_tree",
      "scripts/release.test.mjs#release_refuses_mismatched_or_nonversion_tags",
      "crates/jj-acp-testkit/tests/fixture.rs#the_fixture_completes_a_turn_without_a_model_provider",
      "crates/jj-acp-cli/tests/scratch.rs#concurrent_scratch_directories_survive_peer_teardown",
      "crates/jj-acp-cli/tests/signals.rs#sigterm_produces_bounded_cleanup_and_exit_143"
    ],
    "runs": [
      {
        "command": "node scripts/check-progress.mjs (Git-only checkout before fix)",
        "result": "fail",
        "failures": 1,
        "detail": "Required jj metadata absent from ordinary Git checkout."
      },
      {
        "command": "node --test scripts/progress-vcs.test.mjs scripts/release.test.mjs",
        "result": "pass",
        "failures": 0,
        "detail": "6 tests passed."
      },
      {
        "command": "pnpm spec:validate (initial release chapter)",
        "result": "fail",
        "failures": 5,
        "detail": "Verification rows were accidentally separated from their table; fixed the canonical map."
      },
      {
        "command": "pnpm package:check (uncommitted preparation)",
        "result": "fail",
        "failures": 1,
        "detail": "Cargo correctly required a clean source or explicit validation-only --allow-dirty."
      },
      {
        "command": "pnpm package:check --allow-dirty",
        "result": "pass",
        "failures": 0,
        "detail": "All six portable manifests/archive file lists verified."
      },
      {
        "command": "cargo publish --workspace --locked --dry-run --allow-dirty",
        "result": "pass",
        "failures": 0,
        "detail": "All six archives compiled from packaged sources; no upload."
      },
      {
        "command": "pnpm spec:check; pnpm check (release preparation)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test (release preparation)",
        "result": "fail",
        "failures": 1,
        "detail": "SIGTERM fixture log removed by parallel timestamp collision in shared Scratch allocator."
      },
      {
        "command": "cargo test -p jj-acp --test scratch --locked -- --exact concurrent_scratch_directories_survive_peer_teardown (before allocator fix)",
        "result": "fail",
        "failures": 1,
        "detail": "32 callers obtained 31 unique paths."
      },
      {
        "command": "cargo test -p jj-acp --test scratch --test signals --locked",
        "result": "pass",
        "failures": 0,
        "detail": "Collision regression and both unchanged signal contracts pass."
      },
      {
        "command": "node scripts/check-progress.mjs (real Git-only checkout after fix)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check:workflow; pnpm spec:build",
        "result": "pass",
        "failures": 0,
        "detail": "Actionlint passes; mdBook builds without warnings."
      },
      {
        "command": "retained-history credential-pattern audit",
        "result": "pass",
        "failures": 0,
        "detail": "21 commits and 455 distinct blobs; no known credential/private-key patterns."
      },
      {
        "command": "pnpm spec:check (after parallel scratch fix)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check (after parallel scratch fix)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test (after parallel scratch fix)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build (after parallel scratch fix)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo build -p jj-acp --release --locked --target aarch64-apple-darwin; node scripts/release-archive.mjs aarch64-apple-darwin",
        "result": "pass",
        "failures": 0,
        "detail": "Native release binary and licensed checksum archive verified."
      },
      {
        "command": "cold jj-store developmentAgentEnvironment with candidate ACP executable",
        "result": "pass",
        "failures": 0,
        "detail": "Control v3 and text/context-only preflight; no provider prompt, service restart or consumer edits."
      },
      {
        "command": "README task-file/stdin/catalog/trusted-tools fixture acceptance",
        "result": "pass",
        "failures": 0,
        "detail": "Both task inputs complete with exactly one matching sanitized terminal record."
      }
    ],
    "result": "pass",
    "remaining": [],
    "note": "M12 remains In progress. Package metadata and bundled fixtures change; runtime launch authority and protocol contracts do not. Initial Cargo authentication supplied locally by the user. Full source rerun: 180 Rust tests/doctests and 59 governance tests passed; zero failures. All source lanes passed after repairing the parallel directory collision."
  },
  {
    "id": "E-GITHUB-001",
    "tasks": ["M14"],
    "date": "2026-10-14",
    "kind": "hosted publication",
    "versions": "GitHub source 273b6651; Rust 1.98.1; actionlint 1.7.12; mdBook 0.5.4",
    "formats": "public main; Actions Pages deployment; coordinated crate 0.1.0 pending",
    "files": [
      ".github/workflows/ci.yml",
      "spec/src/spec-governance.md",
      "spec/src/release.md",
      "scripts/release-registry.mjs",
      "scripts/registry-check.mjs",
      "scripts/registry-check.test.mjs",
      "package.json",
      "spec-validator.config.mjs"
    ],
    "tests": [
      "scripts/progress-vcs.test.mjs#explicit_git_range_wins_over_local_jj",
      "scripts/registry-check.test.mjs#missing_version_checks_metadata_before_cdn_download",
      "scripts/registry-check.test.mjs#existing_version_must_match_the_verified_package"
    ],
    "runs": [
      {
        "command": "jj git push --remote origin --bookmark main",
        "result": "pass",
        "failures": 0,
        "detail": "Published signed source history; automatic push signatures updated commit IDs without source changes."
      },
      {
        "command": "GitHub Pages run 38601008185",
        "result": "pass",
        "failures": 0,
        "detail": "Build/deploy succeeded; index, control, release and verification pages return HTTP 200."
      },
      {
        "command": "GitHub CI run 38601008085 canonical setup",
        "result": "fail",
        "failures": 1,
        "detail": "taiki-e/install-action does not support actionlint; native ARM64 Linux/macOS jobs passed."
      },
      {
        "command": "workspace incorporation and cleanup audit",
        "result": "pass",
        "failures": 0,
        "detail": "No substantive changes outside main in either workspace, clean working copies, no extra files to preserve. Forgot and removed both leftover workspaces; default remains."
      },
      {
        "command": "GitHub CI run 38601625239",
        "result": "pass",
        "failures": 0,
        "detail": "Canonical checks and all three additional native targets pass after tool setup correction."
      },
      {
        "command": "node scripts/release-registry.mjs --publish (initial registry existence check)",
        "result": "fail",
        "failures": 1,
        "detail": "No upload attempted: nonexistent CDN archive returned HTTP 403. Existence now checked through version metadata."
      },
      {
        "command": "node --test scripts/registry-check.test.mjs; node scripts/release-registry.mjs --status",
        "result": "pass",
        "failures": 0,
        "detail": "3 retry/existence regressions pass; live metadata confirms all six versions absent."
      },
      {
        "command": "pnpm spec:check; pnpm check; pnpm test; pnpm build (registry preflight correction)",
        "result": "pass",
        "failures": 0,
        "detail": "All source gates including 62 governance tests and 180 Rust tests/doctests pass."
      }
    ],
    "result": "pending",
    "remaining": [
      "Corrected hosted CI, registry bootstrap and native release artifacts pending."
    ],
    "note": "No source qualification was waived. M12 retains independent live-adapter gaps."
  },
  {
    "id": "E-PUBLISH-001",
    "tasks": ["M14"],
    "date": "2026-10-14",
    "kind": "public release acceptance",
    "versions": "All six crates 0.1.0; release tag v0.1.0 at 07a53851d857e16f426ac0645c56904e8850de17; Rust 1.98.1; Node 22 hosted; mdBook 0.5.4",
    "formats": "task/record v1; control v1/v2/v3; GNU/Linux and Darwin ARM64/x86-64 tar.gz; SHA256SUMS",
    "files": [
      "spec/src/milestones.md",
      "spec/src/release.md",
      "README.md",
      ".github/workflows/release.yml"
    ],
    "tests": [
      "scripts/registry-check.test.mjs#existing_version_must_match_the_verified_package",
      "crates/jj-acp-testkit/tests/fixture.rs#the_fixture_completes_a_turn_without_a_model_provider"
    ],
    "runs": [
      {
        "command": "GitHub CI run 38602685961 and release qualification run 38603867724",
        "result": "pass",
        "failures": 0,
        "detail": "Canonical checks and all native targets passed; Git-only checks, metadata, package compile and dry run passed."
      },
      {
        "command": "node scripts/release-registry.mjs --publish (first new-crate batch)",
        "result": "fail",
        "failures": 1,
        "detail": "Five libraries published; crates.io returned 429 for sixth new crate until 2026-10-14 17:12:50 UTC. No version overwritten."
      },
      {
        "command": "node scripts/release-registry.mjs --publish (after specified quota reset)",
        "result": "pass",
        "failures": 0,
        "detail": "Verified existing five package checksums and published only the missing CLI from the same source revision."
      },
      {
        "command": "node scripts/release-registry.mjs; trusted-publisher configuration and public metadata verification",
        "result": "pass",
        "failures": 0,
        "detail": "6/6 archive checksums match; all six enforce stevejuma/jj-acp release.yml with release environment and trusted-publishing-only."
      },
      {
        "command": "cargo install jj-acp --version 0.1.0 --locked --root .local/publication/registry-install",
        "result": "pass",
        "failures": 0,
        "detail": "Fresh registry installation reports jj-acp 0.1.0."
      },
      {
        "command": "published testkit archive standalone cargo +1.98.1 test --locked",
        "result": "pass",
        "failures": 0,
        "detail": "Seven tests and one doctest passed outside the checkout using published protocol/agent dependencies and packaged Python fixtures."
      },
      {
        "command": "GitHub release run 38603867724",
        "result": "pass",
        "failures": 0,
        "detail": "Four native archives tested and published; registry install and package identity gates passed; public v0.1.0 release finalized."
      },
      {
        "command": "downloaded public native archives and SHA256SUMS acceptance",
        "result": "pass",
        "failures": 0,
        "detail": "All four checksums match; downloaded macOS ARM64 binary passes --version and --help."
      },
      {
        "command": "docs.rs API documentation and hosted specification acceptance",
        "result": "pass",
        "failures": 0,
        "detail": "All six Rust API documents and canonical specification pages return HTTP 200."
      }
    ],
    "result": "pass",
    "remaining": [],
    "note": "Publication gate complete. Source remains at tagged immutable version; main may advance for evidence. M12 remains In progress: no new live-adapter qualification claim. Scoped bootstrap-token API revocation returned 403; local Cargo credentials were removed. Server-side token cleanup remains an account operation, separate from the completed publication gate."
  },
  {
    "id": "E-HANDOFF-001",
    "tasks": ["M14"],
    "date": "2026-10-14",
    "kind": "maintainer handoff",
    "versions": "Version 0.1.0 unchanged; published tag 07a53851; main acceptance fec38de7",
    "formats": "GitHub required-check protection and immutable v* tag rules; trusted publishing only",
    "files": ["spec/src/milestones.md", "spec/src/release.md"],
    "tests": [
      "scripts/release.test.mjs#release_refuses_mismatched_or_nonversion_tags"
    ],
    "runs": [
      {
        "command": "GitHub CI run 38606084277 and Pages run 38606084548",
        "result": "pass",
        "failures": 0,
        "detail": "Final publication-evidence main commit passes all checks and deploys the updated canonical book."
      },
      {
        "command": "public release download, four SHA256SUMS checks and native executable smoke",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "note": "User confirmed bootstrap token revoked on 2026-10-14. Local Cargo credentials were already removed. All six packages enforce OIDC trusted publishing. Immutable release-tag rules and four required CI checks protect future publication; source and tag are retained, and only the default Jujutsu workspace remains."
  },
  {
    "id": "E-CI-CLEANUP-001",
    "tasks": ["M13"],
    "date": "2026-10-14",
    "kind": "hosted containment fixture timing",
    "versions": "Rust 1.98.1; Ubuntu 22.04 x86-64/ARM64 hosted; macOS ARM64 local",
    "formats": "task/record v1; published 0.1.0 source and archives unchanged",
    "files": [
      "crates/jj-acp-cli/tests/cleanup.rs",
      "spec/src/cli.md",
      "spec/src/verification.md",
      "spec/src/milestones.md"
    ],
    "tests": [
      "crates/jj-acp-cli/tests/cleanup.rs#a_forced_termination_takes_the_agents_descendant_with_it"
    ],
    "runs": [
      {
        "command": "GitHub CI run 38606801599 canonical and native ARM64 Linux cleanup suites (before)",
        "result": "fail",
        "failures": 2,
        "detail": "Each job failed one containment test: the fixture never recorded a descendant before the 1.5-second absolute deadline."
      },
      {
        "command": "cargo test -p jj-acp --test cleanup a_forced_termination_takes_the_agents_descendant_with_it --locked -- --exact (before)",
        "result": "pass",
        "failures": 0,
        "detail": "Idle local interpreter passed; hosted failure required startup contention."
      },
      {
        "command": "JJ_ACP_FIXTURE_PYTHON=.local/publication/slow-python cargo test -p jj-acp --test cleanup a_forced_termination_takes_the_agents_descendant_with_it --locked -- --exact (before)",
        "result": "fail",
        "failures": 1,
        "detail": "Two-second interpreter delay reproduced the exact missing-descendant failure."
      },
      {
        "command": "JJ_ACP_FIXTURE_PYTHON=.local/publication/slow-python cargo test -p jj-acp --test cleanup a_forced_termination_takes_the_agents_descendant_with_it --locked -- --exact",
        "result": "pass",
        "failures": 0,
        "detail": "Two-second delayed interpreter now executes one prompt, times out with exit 124 and leaves no descendant."
      },
      {
        "command": "cargo test -p jj-acp --test cleanup --locked",
        "result": "pass",
        "failures": 0,
        "detail": "All six cleanup and crash classification tests passed."
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "note": "Test task allows ten seconds of startup/execution and PID readiness allows fifteen seconds. Timeout exit and descendant disappearance remain mandatory; added deadline-record, one-dispatch and bounded-completion assertions. No production or released package source changes. All local canonical lanes passed. Hosted merge requires the four protected CI checks."
  },
  {
    "id": "E-CHANGESETS-001",
    "tasks": ["M15"],
    "date": "2026-10-14",
    "kind": "changesets release orchestration",
    "versions": "Changesets CLI 2.31.1; Node 22 hosted; pnpm 10.34.5; Cargo crates remain 0.1.0",
    "formats": "Private npm coordinator; synchronized Cargo workspace; Version Packages PR; immutable v* tags",
    "files": [
      ".changeset/config.json",
      ".changeset/README.md",
      "docs/releases/0.1.0.md",
      ".github/workflows/changesets.yml",
      "scripts/changeset-version.mjs",
      "scripts/release-evidence.mjs",
      "scripts/release-state.mjs",
      "scripts/release-orchestration.mjs",
      "scripts/changesets.test.mjs",
      "scripts/release-orchestration.test.mjs",
      "package.json",
      "pnpm-lock.yaml",
      "spec-validator.config.mjs",
      "scripts/check-progress.mjs",
      "spec/src/release.md",
      "spec/src/milestones.md",
      "spec/src/verification.md",
      "README.md",
      "CHANGELOG.md",
      "CONTRIBUTING.md",
      "crates/jj-acp-cli/README.md",
      "docs/releasing.md",
      ".github/workflows/release.yml",
      "scripts/release-version.mjs",
      "spec/src/architecture.md",
      "spec/src/cli.md",
      "spec/src/compatibility.md",
      "spec/src/spec-governance.md"
    ],
    "tests": [
      "scripts/changesets.test.mjs#changesets_prepare_coordinated_cargo_version_and_consume_notes",
      "scripts/changesets.test.mjs#released_history_is_not_a_pending_changeset",
      "scripts/release-orchestration.test.mjs#already_released_history_never_retags_newer_main",
      "scripts/release-orchestration.test.mjs#bot_version_pr_explicitly_dispatches_required_ci_and_pages_build"
    ],
    "runs": [
      {
        "command": "pnpm changeset status (before)",
        "result": "fail",
        "failures": 1,
        "detail": "No Changesets command or configuration existed."
      },
      {
        "command": "node --test scripts/changesets.test.mjs scripts/release-orchestration.test.mjs (first)",
        "result": "fail",
        "failures": 1,
        "detail": "Configuration audit regex used an unescaped slash; corrected before continuing."
      },
      {
        "command": "node --test scripts/changesets.test.mjs scripts/release-orchestration.test.mjs (archive prototype)",
        "result": "fail",
        "failures": 4,
        "detail": "Changesets interprets subdirectories as legacy pending changesets. Moved published archive into docs/releases so it cannot enter the release queue."
      },
      {
        "command": "node --test scripts/changesets.test.mjs scripts/release-orchestration.test.mjs (post-format)",
        "result": "fail",
        "failures": 1,
        "detail": "New milestone row was separated from its table and formatted as prose. Restored the governed seven-column table layout."
      },
      {
        "command": "pnpm release:status (raw changeset status prototype)",
        "result": "fail",
        "failures": 1,
        "detail": "Raw Changesets status requires a note for local private tooling manifest changes. The project status command now reports the active queue directly without requiring a release bump for orchestration setup."
      },
      {
        "command": "node --test scripts/changesets.test.mjs scripts/release-orchestration.test.mjs",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm install --frozen-lockfile --ignore-pnpmfile",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm release:status",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm release:plan",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm release:version",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:first",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check:workflow",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "GitHub repository Actions workflow permissions update and readback",
        "result": "pass",
        "failures": 0,
        "detail": "Global token remains read-only; repository allows Actions to create release PRs. Workflow jobs explicitly scope write permissions."
      }
    ],
    "result": "pass",
    "remaining": [],
    "note": "Backfill initial 0.1.0 as historical archive, with no active release note or version bump. npm coordinator stays private. Actual isolated Changesets patch preparation passed, including offline Cargo/pnpm lock refresh and same-change specification/evidence. Empty-queue preparation is a no-op. All 180 Rust tests/doctests and 75 governance tests pass; no new version is published by this setup. Tagged source refuses active notes and existing-tag orchestration refuses replacement. Future new-version publication is performed by release.yml after maintainer merge and qualification."
  },
  {
    "id": "E-CHANGESETS-COLD-001",
    "tasks": ["M15"],
    "date": "2026-10-14",
    "kind": "cold Changesets Cargo dependency bootstrap",
    "versions": "Node 22 hosted; Node 26.9.0 locally; Rust 1.98.1; CLI 2.31.1",
    "formats": "Cargo.lock dependencies; candidate metadata unchanged on missing offline cache",
    "files": [
      ".github/workflows/ci.yml",
      ".github/workflows/changesets.yml",
      "scripts/changeset-version.mjs",
      "scripts/changesets.test.mjs",
      "docs/releasing.md",
      "spec/src/release.md",
      "spec/src/spec-governance.md",
      "spec/src/compatibility.md",
      "spec/src/milestones.md",
      "spec/src/verification.md"
    ],
    "tests": [
      "scripts/changesets.test.mjs#changesets_prepare_coordinated_cargo_version_and_consume_notes",
      "scripts/changesets.test.mjs#missing_cargo_cache_preserves_pending_notes_and_version_metadata"
    ],
    "runs": [
      {
        "command": "GitHub canonical checks run 38612667838",
        "result": "fail",
        "failures": 1,
        "detail": "Offline candidate lock refresh could not resolve serde on an empty restored Cargo registry cache."
      },
      {
        "command": "CARGO_HOME=<new isolated empty cache> node --test --test-name-pattern=^changesets_prepare_coordinated_cargo_version_and_consume_notes$ scripts/changesets.test.mjs (before)",
        "result": "fail",
        "failures": 1,
        "detail": "Reproduced the exact missing-serde failure with no inherited Cargo cache."
      },
      {
        "command": "CARGO_HOME=<isolated empty cache> cargo fetch --locked",
        "result": "pass",
        "failures": 0,
        "detail": "Committed dependencies downloaded into a fresh cache before offline acceptance."
      },
      {
        "command": "CARGO_HOME=<same bootstrapped cache> node --test --test-name-pattern=^changesets_prepare_coordinated_cargo_version_and_consume_notes$ scripts/changesets.test.mjs",
        "result": "pass",
        "failures": 0,
        "detail": "Exact original cold failure target now passes."
      },
      {
        "command": "node --test scripts/changesets.test.mjs scripts/release-orchestration.test.mjs",
        "result": "pass",
        "failures": 0,
        "detail": "14 release regressions pass, including preservation of pending notes when the dependency cache is missing."
      },
      {
        "command": "node scripts/package-check.mjs --allow-dirty",
        "result": "pass",
        "failures": 0,
        "detail": "All six crate metadata/content checks passed for the prepared local slice."
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check:workflow",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "note": "Bootstrap Cargo dependencies explicitly in workflow setup. The local wrapper checks cache completeness before changing versions or consuming notes; offline preparation remains deterministic. All canonical lanes pass after the fix: 180 Rust tests/doctests and 76 governance tests. Locked fetch remains a workflow dependency-setup step; version preparation and its regressions use offline Cargo operations."
  },
  {
    "id": "E-CI-AUTOMATION-001",
    "tasks": ["M15"],
    "date": "2026-10-14",
    "kind": "CI automation bookkeeping exception",
    "versions": "Rust 1.98.1; pnpm 10.34.5; local Node 26.9.0; hosted Node 22; crates remain 0.1.0",
    "formats": "GitHub event and associated PR metadata; existing runtime contracts unchanged",
    "files": [
      ".changeset/README.md",
      ".github/workflows/changesets.yml",
      ".github/workflows/ci.yml",
      ".github/workflows/release.yml",
      "AGENTS.md",
      "docs/releasing.md",
      "package.json",
      "scripts/changeset-version.mjs",
      "scripts/changesets.test.mjs",
      "scripts/check-progress.mjs",
      "scripts/ci-automation.mjs",
      "scripts/ci-automation.test.mjs",
      "scripts/ci-range.mjs",
      "scripts/governance.test.mjs",
      "spec/src/architecture.md",
      "spec/src/compatibility.md",
      "spec/src/release.md",
      "spec/src/spec-governance.md",
      "spec/src/verification.md",
      "spec-validator.config.mjs",
      "spec/src/milestones.md"
    ],
    "removed_files": ["scripts/release-evidence.mjs"],
    "tests": [
      "scripts/ci-automation.test.mjs#identifies_both_bots_without_exempting_ordinary_or_fork_prs",
      "scripts/ci-automation.test.mjs#dispatch_and_merged_qualification_recognize_associated_bot_prs",
      "scripts/ci-automation.test.mjs#automation_skips_only_spec_first_and_preserves_canonical_lanes",
      "scripts/governance.test.mjs#automation_exempts_same_change_evidence_but_preserves_ledger_integrity",
      "scripts/changesets.test.mjs#changesets_prepare_coordinated_cargo_version_and_consume_notes"
    ],
    "runs": [
      {
        "command": "GitHub Dependabot PR 1 canonical check job 116026582882 (before)",
        "result": "fail",
        "failures": 1,
        "detail": "Progress lane refused missing same-change ledger and evidence updates. The other three Dependabot PRs failed at the same boundary."
      },
      {
        "command": "node --test scripts/ci-automation.test.mjs (before implementation)",
        "result": "fail",
        "failures": 1,
        "detail": "New automation classification module did not exist."
      },
      {
        "command": "node --test scripts/ci-automation.test.mjs scripts/governance.test.mjs scripts/changesets.test.mjs",
        "result": "pass",
        "failures": 0,
        "detail": "38 focused tests passed before adding HTTP error regression."
      },
      {
        "command": "pnpm check:workflow",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node --test scripts/ci-automation.test.mjs scripts/governance.test.mjs scripts/changesets.test.mjs (final: 39 tests)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm spec:check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm check",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm test (84 governance tests; 180 Rust tests/doctests)",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm build",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "pnpm package:check --allow-dirty",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "cargo publish --workspace --locked --dry-run --allow-dirty",
        "result": "pass",
        "failures": 0
      },
      {
        "command": "node scripts/ci-range.mjs with live GitHub PR 1-4 payloads and exact head revisions",
        "result": "pass",
        "failures": 0
      }
    ],
    "result": "pass",
    "remaining": [],
    "note": "Lapis Notes precedent skips generated Changesets spec-first/release-intent gates. Here both bots skip only spec-first and same-change ledger requirements; ledger integrity and all actual qualification remain. No release/version change. All local required lanes passed. Exact live bot heads select the exemption. Hosted required checks and refreshed bot PRs will verify deployment of this policy; dependency PRs remain open for review."
  }
]

Recorded gaps

Both entry points are delivered and the persistent one is concurrent: submissions carry a caller-named scope, one scope has at most one turn in flight, distinct scopes overlap up to the worker limit, and a bounded queue refuses admission rather than growing. A crashed agent releases its slot and is never re-dispatched.

Version 1 does not reuse agents across scoped submissions. Explicit version 2 retains its agent and session across follow-ups and supports capability-gated loading without prompting. Installed interactive consumer qualification is recorded separately in M10; the fixture result does not establish shipping-adapter behavior.

Inbound batching is also absent. Upstream drains all queued events for a scope into one prompt; here each submission is one turn, so a controlling application that wants batching must do it before submitting.

ControlTransport was replaced rather than wired in. Its old shape modelled the one-shot path and nothing used it; the loop now depends on a FrameSource and a shared FrameSink, and the scheduler is tested in process over an in-memory pair as well as through the spawned binary.

Descendant containment is proven on macOS ARM64 for both the forced and the graceful path, but only on Unix. On other platforms cleanup signals the direct child and reports the reduced guarantee rather than approximating containment, and no non-Unix platform has been qualified at all.

One residual case is documented rather than closed: a descendant that ignores SIGTERM, whose leader then exits voluntarily inside the grace window, is not reached, because the leader is reaped by then and the group id is no longer safely ours. Containment is guaranteed for the fatal path and best-effort for the cooperative one.

Model, reasoning effort and execution mode are all applied, but only against what the current session advertises. Two gaps remain in that area. The thought_level configuration path is proven against a committed capture and not against a live adapter, because the adapter stopped advertising that option between captures at the same version. And a bare positional bracket such as gpt-6.1-sol[medium] is deliberately not read as an effort, so an adapter that encodes effort that way reports no effort control rather than being guessed at.

Advertised configuration surfaces are not stable across sessions. A committed capture is evidence about one session rather than a property of an adapter version, so no test asserts that a live adapter will offer a given option.

Two real adapters have been qualified: cursor-agent acp 2026.09.26-dd393fe and @agentclientprotocol/codex-acp 1.13.1, each driven through completed read-only turns. claude-agent-acp, opencode and goose have never been launched, and for opencode and goose a reported availability means only that an executable with that name exists, not that its ACP entrypoint works.

Neither qualified run raised a permission request, so the default deny policy has not been exercised against a real adapter. An adapter that does ask will be refused and the turn will end cancelled, which is the intended safe default but is not yet proven outside the fixture peer.

Codex was reached through an adapter already in the local npx cache rather than a fresh download. The codex CLI itself is not an ACP agent and has no acp subcommand, so a machine without a cached or globally installed codex-acp needs one before the compiled-in codex runner resolves.

Authentication is deliberately unimplemented, so an adapter that cannot reuse an existing operator login cannot be driven at all. Cursor worked because cursor-agent login had already been run outside this harness. That is a consequence of the trust boundary rather than a defect, and it bounds which adapters are reachable without a controlling application supplying credentials its own way.

The jj-store controlling-application consumer is qualified in M10 through the real host evidence E-M10-001 and jj-store E-EXEC-001/002. The qualified profile is macOS ARM64 LocalTrusted execution with installed Codex 1.13.1 and Cursor 2026.10.01-e373342, ordinary and native isolated workspace edits, sanitized publication, cold Chromium/restart and managed owner lifetime. Fixture-only permission/cancellation/deadline evidence remains distinct; these real turns raised zero permission requests. Signed desktop/WKWebView distribution and other platforms/adapters remain separate qualification.

E-M12-002 reviews separate prepared context blocks without expanding the real adapter or lifecycle qualification. M12 stays In progress.

Verification

Planned rows name a gate, not completed evidence. Mark Implemented only after passing concrete tests linked from the progress ledger.

E-M12-004 strengthens the JAC-CTRL-009/JAC-RUN-009/JAC-CLI-008 fleet binary regression: a rejected load preserves the other configured scopes, while a foreign-session protocol failure still invalidates them.

IDStatusEvidence
JAC-ARCH-001Implementedscripts/check-boundaries.mjs; scripts/governance.test.mjs; bootstrap evidence
JAC-ARCH-002Implementedscripts/check-boundaries.mjs; scripts/governance.test.mjs; bootstrap evidence
JAC-ARCH-003Implementedscripts/rust-architecture.mjs; scripts/rust-architecture.test.mjs; bootstrap evidence
JAC-ARCH-004Implementedscripts/rust-architecture.mjs; scripts/rust-architecture.test.mjs; bootstrap evidence
JAC-ARCH-005Implementedcrates/jj-acp-control/src/invocation.rs; crates/jj-acp-cli/tests/run_task.rs; M4 and real jj-store M10 evidence
JAC-ARCH-006Implementedcrates/jj-acp-cli/src/lib.rs; scripts/check-boundaries.mjs; M4 evidence
JAC-PROTO-001Implementedcrates/jj-acp-protocol/src/frame.rs; crates/jj-acp-protocol/tests/bounds.rs; M2 evidence
JAC-PROTO-002Implementedcrates/jj-acp-protocol/tests/wire.rs; M2 evidence
JAC-PROTO-003Implementedcrates/jj-acp-protocol/tests/wire.rs; crates/jj-acp-protocol/tests/inbound.rs; M2 evidence
JAC-PROTO-004Implementedcrates/jj-acp-protocol/tests/payload.rs; crates/jj-acp-protocol/tests/wire.rs; M2 evidence
JAC-PROTO-005Implementedcrates/jj-acp-protocol/tests/payload.rs; crates/jj-acp-protocol/tests/wire.rs; M2 evidence
JAC-PROTO-006Implementedscripts/governance.test.mjs; scripts/check-boundaries.mjs; crates/jj-acp-protocol/Cargo.toml; M2 evidence
JAC-AGENT-001Implementedcrates/jj-acp-agent/src/definition.rs; crates/jj-acp-agent/tests/catalog.rs; M3 evidence
JAC-AGENT-002Implementedcrates/jj-acp-agent/src/config.rs; crates/jj-acp-agent/tests/catalog.rs; M3 evidence
JAC-AGENT-003Implementedcrates/jj-acp-agent/src/environment.rs; crates/jj-acp-agent/tests/catalog.rs; M3 evidence
JAC-AGENT-004Implementedcrates/jj-acp-agent/src/catalog.rs; crates/jj-acp-agent/tests/catalog.rs; M3 evidence
JAC-AGENT-005Implementedcrates/jj-acp-agent/src/definition.rs; crates/jj-acp-agent/tests/catalog.rs; M3 evidence
JAC-AGENT-006Implementedcrates/jj-acp-agent/src/environment.rs; crates/jj-acp-agent/tests/catalog.rs; M3 evidence
JAC-RUN-001Implementedcrates/jj-acp-runner/src/turn.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence; crates/jj-acp-control/tests/serve.rs; crates/jj-acp-cli/tests/serve.rs; M9 evidence
JAC-RUN-002Implementedcrates/jj-acp-runner/src/process.rs; crates/jj-acp-cli/tests/signals.rs; M4 evidence
JAC-RUN-003Implementedcrates/jj-acp-protocol/tests/bounds.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-RUN-004Implementedcrates/jj-acp-runner/src/outcome.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-RUN-005Implementedcrates/jj-acp-cli/tests/cleanup.rs; crates/jj-acp-runner/src/outcome.rs; M5 evidence; crates/jj-acp-control/tests/serve.rs; crates/jj-acp-cli/tests/cleanup.rs; M9 evidence
JAC-RUN-006Implementedcrates/jj-acp-runner/src/configuration.rs; crates/jj-acp-protocol/tests/session_config.rs; crates/jj-acp-cli/tests/configuration.rs; M7 evidence
JAC-RUN-007Implementedcrates/jj-acp-runner/src/process.rs; crates/jj-acp-cli/tests/cleanup.rs; M5 evidence
JAC-CTRL-001Implementedcrates/jj-acp-control/src/protocol.rs; crates/jj-acp-control/src/serve.rs; crates/jj-acp-cli/tests/serve.rs; M8 evidence
JAC-CTRL-002Implementedcrates/jj-acp-control/src/task.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-CTRL-003Implementedcrates/jj-acp-control/src/record.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-CTRL-004Implementedcrates/jj-acp-cli/tests/stdio_contract.rs; M4 evidence
JAC-CTRL-005Implementedcrates/jj-acp-control/src/invocation.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-CTRL-006Implementedcrates/jj-acp-control/src/task.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-CTRL-007Implementedcrates/jj-acp-control/src/input.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-CLI-001Implementedcrates/jj-acp-cli/src/run_task.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-CLI-002Implementedcrates/jj-acp-cli/tests/run_task.rs; crates/jj-acp-cli/tests/signals.rs; M4 evidence
JAC-CLI-003Implementedcrates/jj-acp-cli/src/signals.rs; crates/jj-acp-cli/tests/signals.rs; M4 evidence
JAC-CLI-004Implementedcrates/jj-acp-cli/src/args.rs; crates/jj-acp-cli/tests/args.rs; M4 evidence
JAC-CLI-005Implementedcrates/jj-acp-cli/tests/stdio_contract.rs; crates/jj-acp-cli/tests/signals.rs; M4 evidence
JAC-CLI-006Implementedcrates/jj-acp-cli/src/lib.rs; crates/jj-acp-cli/tests/stdio_contract.rs; M4 evidence
JAC-SEC-001Implementedscripts/check-boundaries.mjs; scripts/governance.test.mjs; docs/upstream/disposition.json; M1 evidence
JAC-SEC-002Implementedcrates/jj-acp-runner/src/launch.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-SEC-003Implementedcrates/jj-acp-control/src/invocation.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-SEC-004Implementedscripts/check-boundaries.mjs; crates/jj-acp-runner/src/launch.rs; M4 evidence
JAC-SEC-005Implementedcrates/jj-acp-runner/src/turn.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-SEC-006Implementedcrates/jj-acp-cli/tests/run_task.rs; crates/jj-acp-protocol/tests/inbound.rs; M4 evidence
JAC-SEC-007Implementedcrates/jj-acp-control/src/input.rs; crates/jj-acp-cli/tests/run_task.rs; M4 evidence
JAC-COMPAT-001Implementeddocs/upstream-lock.md; docs/upstream/disposition.json; docs/upstream/lock.json; scripts/upstream-lock.test.mjs; M1 evidence
JAC-COMPAT-002Implementeddocs/licenses/NOTICE.md; docs/upstream-lock.md; scripts/upstream-lock.test.mjs; M1 evidence
JAC-COMPAT-003Implementedscripts/upstream-sync.mjs; scripts/upstream-lock.test.mjs; M1 evidence
JAC-COMPAT-004Implementedrust-toolchain.toml; rustfmt.toml; spec-validator.config.mjs; bootstrap evidence
JAC-COMPAT-005Implementedtests/fixtures/fixture_agent.py; crates/jj-acp-testkit/tests/fixture.rs; M3 evidence
JAC-COMPAT-006Implementedcrates/jj-acp-protocol/tests/real_adapter.rs; crates/jj-acp-protocol/tests/fixtures/cursor-acp-session.json; crates/jj-acp-protocol/tests/fixtures/codex-acp-session.json; M6 evidence
JAC-COMPAT-007Implementeddocs/upstream/disposition.json; scripts/upstream-lock.mjs; scripts/upstream-lock.test.mjs; M1 evidence
JAC-GOV-001Implementedscripts/governance.test.mjs; scripts/check-progress.mjs; spec-validator.config.mjs; bootstrap evidence
JAC-GOV-002Implementedscripts/governance.test.mjs; scripts/check-progress.mjs; spec-validator.config.mjs; bootstrap evidence
JAC-GOV-003Implementedscripts/governance.test.mjs; scripts/check-progress.mjs; spec-validator.config.mjs; bootstrap evidence
JAC-GOV-004Implementedscripts/governance.test.mjs; scripts/check-progress.mjs; spec-validator.config.mjs; bootstrap evidence
JAC-GOV-005Implementedscripts/governance.test.mjs; scripts/check-progress.mjs; spec-validator.config.mjs; bootstrap evidence
JAC-GOV-006Implementedrustfmt.toml; package.json; spec-validator.config.mjs; bootstrap evidence
JAC-GOV-007Implementedscripts/rust-architecture.mjs; scripts/rust-architecture.test.mjs; bootstrap evidence
JAC-CTRL-008Implementedcrates/jj-acp-cli/tests/interactive.rs; crates/jj-acp-control/tests/interactive_overflow.rs; interactive and queued-cancellation evidence
JAC-RUN-008Implementedcrates/jj-acp-cli/tests/interactive.rs; crates/jj-acp-control/tests/interactive_overflow.rs; interactive and queued-cancellation evidence
JAC-PROTO-007Implementedcrates/jj-acp-cli/tests/interactive.rs; crates/jj-acp-control/tests/interactive_overflow.rs; interactive and queued-cancellation evidence
JAC-CLI-007Implementedcrates/jj-acp-cli/tests/interactive.rs; crates/jj-acp-control/tests/interactive_overflow.rs; interactive and queued-cancellation evidence
JAC-CTRL-009Implementedcrates/jj-acp-cli/tests/fleet.rs; warm worker evidence
JAC-RUN-009Implementedcrates/jj-acp-cli/tests/fleet.rs; warm worker evidence
JAC-PROTO-008Implementedcrates/jj-acp-cli/tests/fleet.rs; warm worker evidence
JAC-CLI-008Implementedcrates/jj-acp-cli/tests/fleet.rs; warm worker evidence
JAC-REL-001Implementedscripts/release.test.mjs; scripts/progress-vcs.test.mjs; scripts/package-check.mjs; release evidence
JAC-REL-002Implementedscripts/release.test.mjs; scripts/progress-vcs.test.mjs; scripts/package-check.mjs; release evidence
JAC-REL-003Implementedscripts/release.test.mjs; scripts/progress-vcs.test.mjs; scripts/package-check.mjs; release evidence
JAC-REL-004Implementedscripts/release.test.mjs; scripts/progress-vcs.test.mjs; scripts/package-check.mjs; release evidence
JAC-REL-005Implementedscripts/registry-check.test.mjs; scripts/release-registry.mjs; release evidence
JAC-REL-006ImplementedChangesets version-preparation and orchestration regressions; automation recognition and selective bookkeeping exemptions in scripts/ci-automation.test.mjs; evidence in milestones.md.

Optional v2/v3 prompt context is verified by the protocol payload and control wire tests, with separate fleet CLI block evidence in E-M12-002.

Context-only prompts are covered by interactive_wire rejection/acceptance cases and the version-3 fleet test (E-M12-003).

Trusted MCP launch declarations are covered by crates/jj-acp-cli/tests/fleet.rs: warmup emits no tool session and both independent session/new calls receive the same trusted declarations. Installed local-agent tool acceptance remains consumer evidence.

E-CI-CLEANUP-001 strengthens the JAC-RUN-007/JAC-CLI-002 containment regression with an explicit deadline record, one prompt dispatch and a completion ceiling. Delayed-interpreter acceptance separates fixture startup from cleanup evidence.

Publication and release

The public repository is stevejuma/jj-acp. The initial version is 0.1.0. The CLI package is jj-acp and installs the jj-acp executable; the existing CLI source directory and jj_acp_cli library name remain. The layered libraries and dev-only testkit are released at the same version. The npm package remains a private development entry point.

Requirements

IDRequirement
JAC-REL-001Published crates MUST declare versioned registry dependencies, portable metadata, packaged fixtures where applicable, and Apache-2.0 license and upstream notices.
JAC-REL-002CI MUST check committed base/head ranges in Git checkouts, preserve local Jujutsu checks, and run canonical and native platform qualification without model credentials.
JAC-REL-003GitHub Pages MUST publish the canonical mdBook from main with the repository site prefix; pull requests MUST build without deployment authority.
JAC-REL-004Releases MUST match workspace versions and main ancestry, validate package contents and native executable archives, and include SHA-256 checksums.
JAC-REL-005Initial crate publication MUST use explicitly configured token authentication; subsequent automation MUST use scoped trusted publishing and refuse mismatched existing package contents.
JAC-REL-006Changesets MUST prepare one coordinated private coordinator/Cargo version and changelog; release PRs MUST pass canonical/native checks before a new immutable tag dispatches trusted publishing; released history MUST NOT remain pending.

Supported initial profile

Native CI covers Ubuntu 22.04 and macOS 15 on ARM64 and x86-64. GNU/Linux archives require an Ubuntu 22.04-compatible userspace. macOS distributions do not include Developer ID notarization. Windows has no initial support claim. Control v3 remains experimental; release tooling cannot close M12’s live-adapter gates. Deterministic fixtures remain separate from installed-runner evidence.

Release gates and recovery

Release tags are v<workspace-version> on main history. The release workflow qualifies the source, builds native archives, verifies registry package identity, publishes missing versions and tests registry installation before publishing GitHub assets. All published packages retain the same source revision. Existing versions are accepted only when their archive checksum matches the intended package; a different checksum blocks publication. Partial publication may resume only for missing packages. Published versions and release tags are never replaced.

The initial bootstrap uses local Cargo authentication. Each crate then configures stevejuma/jj-acp, release.yml and the release environment as its trusted publisher. No provider or registry credentials belong in tracked files, logs or artifacts. GitHub workflow tokens have minimum job permissions; private reports use GitHub’s private vulnerability reporting feature. Operational steps are in docs/releasing.md.

The canonical CI job installs mdBook through the pinned Rust tooling Action and actionlint 1.7.12 from its official release archive with an explicit SHA-256 check. actionlint is not a supported taiki-e/install-action tool. The initial hosted job failed at tool setup before source gates; corrected setup must pass those gates before release.

Registry existence is established by the version metadata endpoint before downloading an archive. An unpublished CDN object can return HTTP 403; that is not a credential failure and must not be used to decide existence. Metadata 404 admits publication, while any error downloading a known version blocks publication and any checksum mismatch still refuses replacement.

Initial publication evidence

Version 0.1.0 is published for all six crates at tag v0.1.0. The four native archives and SHA256SUMS are public, registry installation and standalone testkit fixtures passed, and all six Rust API documentation builds are available. M14 records the hosted source, Pages and release run IDs. A crates.io new-crate quota interrupted the sixth upload; checksum verification admitted only the missing CLI after the published retry time. All six crates enforce the configured trusted publisher. M12 remains experimental with its existing live-adapter gaps.

The user confirmed bootstrap-token revocation after local credentials were removed. Future publication uses the six configured trusted publishers. GitHub protects main with the canonical and three native CI checks, forbids force-push/deletion, and prevents updates or deletion of v* release tags. Contributors submit future source updates through a passing pull request; release tags continue to identify the exact published source.

Changesets orchestration

The private @lapismd/jj-acp development package coordinates all six Cargo crates. Pending notes target that package with patch, minor or major intent. Changesets generates the changelog and coordinator version; release:version synchronizes Cargo workspace/internal versions, both lockfiles, installation examples and the packaged CLI README. Recognized Dependabot and Changesets PRs skip same-change specification/evidence bookkeeping under JAC-GOV-002’s automation exception; all other qualification remains required. Version preparation does not modify canonical chapters or the ledger and never claims product qualification or publication.

The main workflow creates or updates a Version Packages pull request. Explicit CI and Pages dispatches qualify bot-created PRs, because events produced with GITHUB_TOKEN do not otherwise start those workflows. Maintainers merge a passing version PR. With no pending notes, a missing version tag is admitted only after the canonical/native lanes pass; the workflow creates that tag once and dispatches release.yml. An existing tag is retained without repackaging newer main. Failed releases resume through the existing tagged release workflow.

Version 0.1.0 is backfilled in CHANGELOG.md and docs/releases/0.1.0.md as already published history. The archive is outside the active note directory. Installing orchestration creates no pending bump, new tag or registry upload.

Cold workflows fetch locked Cargo dependencies during setup. Version preparation requires an offline locked fetch before Changesets consumes notes, so missing registry metadata aborts without changing the coordinator or release queue. The bootstrap is dependency installation, separate from offline source gates.