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

Current specification

The contracts, verification table and milestone ledger are authoritative. Imported ACP requirements preserve source JAC identifiers through the mechanical JAC to LAI prefix map. Import evidence is not native or platform acceptance.

Governance

Requirements

IDRequirement
LAI-GOV-100Protected implementation, interfaces, tooling and guidance MUST update mapped owning contracts or record a fresh justified unchanged-contract confirmation, validation and milestone review in the same Jujutsu change.
LAI-GOV-101Authored specification Markdown MUST remain within 400 physical lines and 32768 UTF-8 bytes. Each requirement MUST have exactly one verification row.
LAI-GOV-102New production Rust and scripts MUST stay within 300 counted lines and tests within 500. Exact imported exceptions MUST have pinned provenance and MUST NOT grow.
LAI-GOV-103Evidence MUST record commands, results, failures, affected formats, gaps and next action. Fixture, native inference, interoperability and platform evidence MUST remain distinct.
LAI-GOV-104Verification MUST start at the exact affected boundary and widen only for shared infrastructure or multiple affected packages. Native and platform gates MUST NOT pass from compilation alone.

The logical ledger lives under current/progress. Evidence lives under history. Committed records are append-only. Sources and existing user data remain untouched.

Completion checks require linked command/result/failure/gap evidence and completed dependencies. Source pins, composed imports and non-growing original file counts are validated separately from platform qualification. Hosted CI and broader release/native profiles retain their own explicit gates.

Recognized same-repository Dependabot/Changesets automation may omit only same-change spec-first bookkeeping. Recognition validates bot identity, reserved branch, base main, repository and the exact checked/merged revision. Lookup errors fail closed. Canonical integrity, evidence integrity, tests, native acceptance, package checks and builds remain required; version preparation invents no evidence.

Version preparation formats its regenerated pnpm lockfile before opening the version PR. A real preparation regression checks that output against the ordinary formatting gate; bot exceptions never suppress formatting (LAI-REL-003–004).

The canonical release identity is lapismd/lapis-ai; automation refuses other owners. Historical hosting evidence retains original URLs and is supplemented with transfer evidence rather than rewritten.

Configuration and supervised clients

Requirements

IDRequirement
LAI-CONFIG-001Explicitly selected version-1 TOML MUST configure runtime paths, ACP launch/catalog/tools, agent provider/environment references and compute limits/peers. Unknown fields and unsupported versions MUST fail before affected startup.
LAI-CONFIG-002Explicit API or CLI values MUST override TOML, which overrides retained legacy environment inputs and defaults. Relative paths MUST resolve against the selected file. No working-directory configuration discovery is permitted.
LAI-CONFIG-003Reading configuration MUST NOT spawn, download, authenticate or infer. Persisted configuration MUST contain credential references rather than secret values; secret resolution occurs only at launch and diagnostics MUST omit values.
LAI-RUNTIME-001Native clients MUST own isolated child environments, working directories, bounded frame transport, readiness and supervised shutdown. Compute caches MUST remain beneath its selected managed directory. They MUST NOT mutate parent environment or current directory.
LAI-RUNTIME-002ACP clients MUST preserve request/session identities and exact permission option IDs. Timeouts and lost acknowledgements MUST remain unknown outcomes without automatic prompt replay.
LAI-RUNTIME-003The agent client MUST support bounded model discovery and metadata requests. Compute clients MUST expose configuration, explicit model lifecycle, events and pairing without conversation policy or durable orchestration.
LAI-RUNTIME-004Host credential callbacks and private token-source IPC MUST remain host-owned. Application secrets, credentials and token sources MUST NOT appear in serialized configuration, debug output or logs.

Library publication and resource manifests use the coordinated Cargo package version under the release contract. Configuration and wire version numbers remain independent. The bundle producer must read the coordinator version rather than retain a literal source-baseline version.

Crate documentation and publication metadata follow lapismd/lapis-ai after the ownership transfer. Runtime, configuration and bundle semantics are unchanged.

Native services and packaging

Requirements

IDRequirement
LAI-LOCAL-001The maintained Buzz-derived agent MUST retain provider adapters, permission gates, model discovery, metadata summaries and token-source support with complete Apache attribution and source pins.
LAI-COMPUTE-001Compute MUST retain authenticated loopback inference, explicit model preparation/download/import/start/stop, bounded limits and mutually admitted inference-only peers. Sharing defaults off and startup never downloads.
LAI-COMPUTE-002Lapis compute MUST preserve the jj-store/compute/1 ALPN, invitation representation and request/response semantics. Mixed-version acceptance MUST use isolated pinned legacy helpers.
LAI-PACKAGE-001Staged local bundles MUST include production executables, licenses and a TOML manifest with target, versions, hashes and native ABI pins. Test executables, secrets and model weights MUST be excluded.
LAI-PACKAGE-002macOS ARM64 and Linux CPU acceptance MUST separately prove cold native inference and an approved ACP tool operation. Unavailable prerequisites MUST leave their gates pending.

Initial inference remains non-streaming. No cross-application singleton, public discovery or automatic cloud fallback is introduced. Consumer migration remains separate. Hosting and release automation follow the release contract. The Cargo and native-runtime pins remain exact.

Cargo packages retain the agent capability manifest and normative corpus inside crates/lapis-agent/data/. Relocation changes no bytes, capability rules, provider behavior or wire contract (LAI-LOCAL-001); required package inputs travel together.

The agent declares its Reqwest query feature explicitly so provider pagination compiles outside the workspace. This preserves the already qualified request behavior and removes an accidental dependency on compute feature unification.

The canonical GitHub owner is now lapismd. Service documentation and Cargo metadata follow this identity; providers, native pins and wire contracts remain unchanged (LAI-LOCAL-001, LAI-COMPUTE-001–002).

CI, versioning and release

The canonical repository is lapismd/lapis-ai. Repository transfers MUST update owner-bound automation, package metadata, remote URLs and trusted-publisher instructions while preserving public visibility, history and open version PRs. Version PRs require human review and are never merged by automation. No package or tag is published during this setup.

Requirements

IDRequirement
LAI-REL-001Cargo publication MUST include portable metadata, registry-versioned internal dependencies, notices, licenses and required fixture sources. Test helper binaries MUST require an explicit development feature and MUST NOT install by default.
LAI-REL-002CI MUST qualify exact committed revisions with explicit Git base/head ranges, retain local Jujutsu checks and run formatting, specifications, boundaries, release/package checks, Clippy, tests, builds and supported native acceptance.
LAI-REL-003Changesets MUST coordinate the private npm development package, all eleven Cargo versions, internal version ranges, lockfiles and changelog. Version preparation MUST NOT fabricate specification or milestone evidence.
LAI-REL-004A same-repository Changesets or Dependabot PR MAY omit same-change spec/evidence bookkeeping only after exact bot, branch, base, repository and revision checks. Every product, packaging and canonical integrity gate MUST remain required.
LAI-REL-005Cargo release MUST qualify an immutable version tag on main history, reject pending Changesets and mismatched registry contents, and publish only explicitly admitted crates. Credentials MUST remain in protected release jobs; normal automation MUST use scoped trusted publishing.
LAI-REL-006GitHub Pages MUST build the canonical mdBook for pull requests and deploy only main through the GitHub Pages environment with the /lapis-ai/ site prefix.
LAI-REL-007Native releases MUST include supported target inventories, SHA-256 checksums and licenses. The pinned compute executable MUST remain native-only while its exact Mesh dependency revision has no matching registry release.

Publication scope

release.toml declares ten crates.io packages and one native-only package. The initial registry API check found all Lapis names unallocated. The exact Mesh source version 0.76.0-rc9 is absent from crates.io, so lapis-compute retains publish = false; the SDK pin is unchanged. GitHub native bundles include compute alongside ACP, agent and tools. They do not include model weights. The npm coordinator stays private and has no npm publication lane.

Changesets generates a Version Packages PR. A passing PR is merged by a human. The reviewed version on main then becomes a candidate for exact-source CI and one immutable v<version> tag. Existing tags are never moved. Release resumes against the existing tag and accepts existing crates only after checksum verification; retries publish only missing packages.

Initial crates.io namespace creation needs explicit bootstrap credentials before per-crate trusted publishers can be configured. The optional bootstrap mode is manual, protected and limited to the initial release version. It is never an automatic fallback when OIDC fails. Later releases use the repository, release.yml and release environment as trusted-publisher scope.

This setup includes an initial patch note and deliberately leaves the generated version PR open. Native profile claims remain macOS ARM64 Metal and Linux ARM64 CPU with glibc 2.38 or later. Public release claims require actual hosted gates; local historical qualification does not substitute for hosted results.

Coordinated preparation includes formatting regenerated lockfiles. A disposable real version-preparation test checks the resulting pnpm lock against the normal formatter, in addition to synchronized versions and preserved canonical evidence.

Future Tauri adapter contract

Requirements

IDRequirement
LAI-TAURI-001Future adapters MUST wrap the Rust runtime clients with host-selected executables and admitted workspace roots, scoped opaque session handles, ordered events and explicit cancellation/teardown. Secrets and arbitrary process launch MUST remain on the Rust side.

This milestone defines the boundary only; no Tauri implementation is qualified. The adapter maps ACP warm/open/configure/prompt/permission/cancel/close and compute configuration/model/pairing operations to clients. Each WebView subscription is owned by its originating window and application instance; teardown releases it. The host validates root admission before opening a provider session. No request may override executable selection, credential resolution or trusted MCP launch. Services remain lazy and independently available. Application conversation state, context preparation, tool authorization and memory stay in the consuming app.

Requirement verification

IDStatusEvidence
LAI-AGENT-001ImplementedFoundation
LAI-AGENT-002ImplementedFoundation
LAI-AGENT-003ImplementedFoundation
LAI-AGENT-004ImplementedFoundation
LAI-AGENT-005ImplementedFoundation
LAI-AGENT-006ImplementedFoundation
LAI-ARCH-001ImplementedFoundation
LAI-ARCH-002ImplementedFoundation
LAI-ARCH-003ImplementedFoundation
LAI-ARCH-004ImplementedFoundation
LAI-ARCH-005ImplementedFoundation
LAI-ARCH-006ImplementedFoundation
LAI-CLI-001ImplementedFoundation
LAI-CLI-002ImplementedFoundation
LAI-CLI-003ImplementedFoundation
LAI-CLI-004ImplementedFoundation
LAI-CLI-005ImplementedFoundation
LAI-CLI-006ImplementedFoundation
LAI-CLI-007ImplementedFoundation
LAI-CLI-008ImplementedFoundation
LAI-COMPAT-001ImplementedFoundation
LAI-COMPAT-002ImplementedFoundation
LAI-COMPAT-003ImplementedFoundation
LAI-COMPAT-004ImplementedFoundation
LAI-COMPAT-005ImplementedFoundation
LAI-COMPAT-006ImplementedFoundation
LAI-COMPAT-007ImplementedFoundation
LAI-COMPUTE-001ImplementedFoundation
LAI-COMPUTE-002ImplementedQualification
LAI-CONFIG-001ImplementedFoundation
LAI-CONFIG-002ImplementedFoundation
LAI-CONFIG-003ImplementedFoundation
LAI-CTRL-001ImplementedFoundation
LAI-CTRL-002ImplementedFoundation
LAI-CTRL-003ImplementedFoundation
LAI-CTRL-004ImplementedFoundation
LAI-CTRL-005ImplementedFoundation
LAI-CTRL-006ImplementedFoundation
LAI-CTRL-007ImplementedFoundation
LAI-CTRL-008ImplementedFoundation
LAI-CTRL-009ImplementedFoundation
LAI-GOV-100ImplementedFoundation
LAI-GOV-101ImplementedFoundation
LAI-GOV-102ImplementedFoundation
LAI-GOV-103ImplementedFoundation
LAI-GOV-104ImplementedFoundation
LAI-LOCAL-001ImplementedFoundation
LAI-PACKAGE-001ImplementedQualification
LAI-PACKAGE-002ImplementedQualification
LAI-PROTO-001ImplementedFoundation
LAI-PROTO-002ImplementedFoundation
LAI-PROTO-003ImplementedFoundation
LAI-PROTO-004ImplementedFoundation
LAI-PROTO-005ImplementedFoundation
LAI-PROTO-006ImplementedFoundation
LAI-PROTO-007ImplementedFoundation
LAI-PROTO-008ImplementedFoundation
LAI-RUN-001ImplementedFoundation
LAI-RUN-002ImplementedFoundation
LAI-RUN-003ImplementedFoundation
LAI-RUN-004ImplementedFoundation
LAI-RUN-005ImplementedFoundation
LAI-RUN-006ImplementedFoundation
LAI-RUN-007ImplementedFoundation
LAI-RUN-008ImplementedFoundation
LAI-RUN-009ImplementedFoundation
LAI-RUNTIME-001ImplementedFoundation
LAI-RUNTIME-002ImplementedFoundation
LAI-RUNTIME-003ImplementedFoundation
LAI-RUNTIME-004ImplementedFoundation
LAI-SEC-001ImplementedFoundation
LAI-SEC-002ImplementedFoundation
LAI-SEC-003ImplementedFoundation
LAI-SEC-004ImplementedFoundation
LAI-SEC-005ImplementedFoundation
LAI-SEC-006ImplementedFoundation
LAI-SEC-007ImplementedFoundation
LAI-TAURI-001ImplementedFoundation
LAI-REL-001ImplementedRelease setup
LAI-REL-002PlannedRelease setup
LAI-REL-003ImplementedRelease setup
LAI-REL-004ImplementedRelease setup
LAI-REL-005PlannedRelease setup
LAI-REL-006ImplementedRelease setup
LAI-REL-007PlannedRelease setup

Milestones

TaskStatusDependenciesGateEvidenceNext action
M0DonenoneStandalone workspace, governance, imports and provenanceFoundationMaintain source pins and frozen budgets
M1DoneM0Renamed ACP and retained deterministic suitesFoundationConsumer adoption is separate
M2DoneM1Agent, tools and compute imports with fixture acceptanceFoundationPreserve provider and wire boundaries
M3DoneM2TOML, clients, isolation and Rust examplesFoundationMaintain direct client acceptance
M4DoneM3macOS ARM64 Metal, Linux aarch64 CPU, mixed-version and staged bundlesQualificationQualify broader native and release profiles separately
M5In progressM4Hosted CI, Changesets, Cargo release automation and spec PagesOrganization transferVerify hosted product CI and retain version PR #1 unmerged

Runner catalog

A runner is an external ACP-speaking agent that lapis-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
LAI-AGENT-001A runner MUST be declared as data carrying identifier, label, command, arguments, environment floor, MCP transport and install hint.
LAI-AGENT-002Adding a runner MUST NOT require a code change, an install shell command or a network fetch.
LAI-AGENT-003Reserved harness environment keys MUST be stripped before a definition or caller override is merged, and MUST NOT be settable by any tier.
LAI-AGENT-004A disabled or unknown runner identifier MUST fail resolution and MUST NOT fall back to a default runner.
LAI-AGENT-005Runner identifiers MUST be validated against a fixed pattern and MUST be unique within a resolved catalog.
LAI-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 lapis-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 inside the ACP crates. The controlling application decided to run this runner with this environment; lapis-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 and TOML

Current qualification is recorded in the Lapis AI verification table. Adapter presence in a source checkout is not live acceptance here. Trusted TOML catalogs are described in docs/usage.md; JSON remains a compatibility input. Authored TOML rejects unknown fields and duplicate alias spellings, resolves executable paths against its file and requires environment references for credentials.

Cargo publication metadata and coordinated versions follow the release contract. The ACP wire, catalog, session, task and permission contracts remain unchanged by release preparation.

The GitHub ownership transfer updates package documentation links to lapismd/lapis-ai; the owning ACP requirements and runtime behavior are unchanged.

ACP architecture

These contracts govern the six ACP crates within the larger Lapis AI workspace. The native agent and compute services have separate provider/network authority under services. Their dependencies do not belong in ACP.

Requirements

IDRequirement
LAI-ARCH-001The repository MUST be a Cargo workspace of layered crates whose internal dependencies match the matrix enforced by scripts/boundaries.mjs.
LAI-ARCH-002lapis-acp MUST NOT depend on Nostr, transport, HTTP, TLS or database crates, and the boundary checker MUST fail closed when one appears.
LAI-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.
LAI-ARCH-004ACP production 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.
LAI-ARCH-005The controlling application MUST own runner selection, working directory, credentials and prompt content; lapis-acp MUST NOT derive them on its own.
LAI-ARCH-006A crate MUST own its behaviour; the CLI crate MUST compose and MUST NOT reimplement protocol, scheduling or transport logic.

Ownership and dependency matrix

The protocol crate owns ACP frames on supplied streams. The catalog crate owns runner data and executable resolution. The runner owns subprocess/session/turn lifecycle. Control owns task records, queues and the V1–V3 host protocols. The CLI composes them and the testkit owns deterministic peer fixtures.

scripts/boundaries.mjs reads locked Cargo metadata, rejects unknown packages, unauthorized internal edges, production testkit imports, sibling paths and forbidden dependencies in ACP or the inert configuration crate. Runtime clients may use authenticated loopback HTTP for compute; ACP itself remains stdio-only.

scripts/architecture.mjs checks new production Rust and scripts at 300 counted lines and tests at 500. The maintained agent import has exact frozen exceptions in scripts/architecture-baseline.json; their original measured sizes and source hashes are in docs/imports/source-manifest.json. Exceptions may shrink, never grow. New tests live in separate files. ACP test functions remain outside production modules. Existing agent tests retain their pinned source placement.

The workspace coordinates versions and commits one lockfile. Every package is unpublished. lapis-acp is the binary/Cargo package; its source directory and library are lapis-acp-cli and lapis_acp_cli. The legacy JJ_ACP_* launch and JJ_ACP_TASK_* reserved namespaces are retained for compatibility. Requirements map from the pinned source’s JAC identifiers to LAI identifiers without changing their numeric suffixes. This repository is named lapis-ai.

Qualification

The verification table and milestone ledger own current acceptance. Source repository live-runner, host, publication and platform results are historical references. They are not acceptance for renamed helpers. No consumer or public release is included in this foundation.

CLI and exit codes

The lapis-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
LAI-CLI-001The binary MUST expose run --task <path|-> executing exactly one prepared task in one fresh agent process and session, then exit.
LAI-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.
LAI-CLI-003SIGINT and SIGTERM MUST trigger bounded cleanup before exit and MUST NOT leave an unreaped child when cleanup succeeds.
LAI-CLI-004Every launch option MUST be settable by a command-line flag and by a matching environment variable, with the flag taking precedence.
LAI-CLI-005The binary MUST NOT write diagnostics, prompts or agent output to stdout on any path, including startup failure and signal handling.
LAI-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.
LAI-CLI-007serve –control-version 2 MUST select the interactive protocol explicitly while the default and one-shot commands retain version-1 behavior.
LAI-CLI-008serve –control-version 3 MUST explicitly select warm worker control; versions 1 and 2 MUST retain their existing contracts.

Commands

CommandRoleStatus
lapis-acp run --task -Execute one prepared task document read from stdinPlanned
lapis-acp run --task PATHExecute one prepared task document read from a filePlanned
lapis-acp agentsList resolved runners with availability and install hintsPlanned
lapis-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

lapis-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.

Cargo publication metadata and coordinated versions follow the release contract. The ACP wire, catalog, session, task and permission contracts remain unchanged by release preparation.

The GitHub ownership transfer updates package documentation links to lapismd/lapis-ai; the owning ACP requirements and runtime behavior are unchanged.

ACP compatibility and upstream port

The ACP-only contracts below preserve the decomposed jj-acp port. The maintained Buzz agent is a separate copied implementation, attributed in docs/agent-upstream.md; it is outside the no-verbatim-ACP-source boundary.

Requirements

IDRequirement
LAI-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.
LAI-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.
LAI-COMPAT-003Upstream drift MUST be detectable by a script that materializes the pinned source into ignored scratch storage and verifies recorded digests.
LAI-COMPAT-004The Rust toolchain MUST be pinned and formatting MUST be checked by both the normal and the specification check lanes.
LAI-COMPAT-005Integration tests MUST reach a terminal outcome without a model provider, a network endpoint or a credential.
LAI-COMPAT-006Qualification against a real installed runner MUST be recorded separately and MUST NOT be inferred from fixture-peer tests.
LAI-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.

Pins and drift

docs/upstream-lock.md, docs/upstream/disposition.json and docs/upstream/lock.json retain the original ACP design/port audit against block/buzz at 2664d14316790a57ea44b3f97c57440b70e43436. The Lapis AI import manifest separately pins jj-acp, JJ Store, maintained Buzz and Mesh sources.

node scripts/upstream-sync.mjs --self-test checks the original ACP pin and map offline. Explicit --fetch materializes the pinned source only under .local/upstream and compares its digests. --write changes the recorded lock and requires its own governed review. No normal build needs this source tree. The closed disposition vocabulary remains ported, ported-with-changes, dropped and not-ported. Repository-level upstream agent guidance is never adopted.

The deterministic ACP suite covers framing, configuration, bounded queues, permissions, deadlines, crashes and process cleanup without provider access. Installed cloud adapters remain independently unqualified here. The native Lapis agent and local-model acceptance are recorded separately; the original source’s live Codex/Cursor and JJ Store host evidence is not inherited acceptance.

docs/imports/acp-release-reference.md is historical source context. This new repository has no publication, signing or hosted release qualification.

Cargo publication metadata and coordinated versions follow the release contract. The ACP wire, catalog, session, task and permission contracts remain unchanged by release preparation.

The GitHub ownership transfer updates package documentation links to lapismd/lapis-ai; the owning ACP requirements and runtime behavior are unchanged.

Controlling application boundary

lapis-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
LAI-CTRL-001The controlling application MUST drive lapis-acp over a versioned newline-delimited JSON control protocol with no authentication and no network listener.
LAI-CTRL-002lapis-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.
LAI-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.
LAI-CTRL-004Diagnostics and child stderr MUST stay on stderr so that stdout remains unconditionally parseable by the controlling application.
LAI-CTRL-005The task document MUST NOT change the working directory, executable, tools or credentials, which come only from trusted launch configuration.
LAI-CTRL-006A control frame MUST carry an explicit integer version, and an unsupported version MUST be rejected before any agent subprocess is started.
LAI-CTRL-007Reading a task document from a file or stdin MUST have its own bounded interval, separate from the execution deadline.
LAI-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.
LAI-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

lapis-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.

Cargo publication metadata and coordinated versions follow the release contract. The ACP wire, catalog, session, task and permission contracts remain unchanged by release preparation.

The GitHub ownership transfer updates package documentation links to lapismd/lapis-ai; the owning ACP requirements and runtime behavior are unchanged.

ACP contracts

These chapters retain the imported ACP behaviour. See the milestones, governance, and verification.

ACP wire protocol

lapis-acp-protocol owns the Agent Client Protocol surface that lapis-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
LAI-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.
LAI-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.
LAI-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.
LAI-PROTO-004A turn MUST terminate in exactly one stop reason drawn from end_turn, max_tokens, max_turn_requests, refusal and cancelled.
LAI-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.
LAI-PROTO-006The protocol crate MUST remain free of process spawning, scheduling, credential handling and transport policy.
LAI-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.
LAI-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.

Cargo publication metadata and coordinated versions follow the release contract. The ACP wire, catalog, session, task and permission contracts remain unchanged by release preparation.

The GitHub ownership transfer updates package documentation links to lapismd/lapis-ai; the owning ACP requirements and runtime behavior are unchanged.

Session and turn lifecycle

lapis-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 lapis-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 lapis-acp treats it as an opaque key.

Requirements

IDRequirement
LAI-RUN-001The runner MUST execute one prepared turn per session scope with at most one turn in flight per scope.
LAI-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.
LAI-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.
LAI-RUN-004A timeout MUST be reported as an unknown outcome and MUST NOT be recorded as success or as a proven failure.
LAI-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.
LAI-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.
LAI-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.
LAI-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.
LAI-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

LAI-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.

Cargo publication metadata and coordinated versions follow the release contract. The ACP wire, catalog, session, task and permission contracts remain unchanged by release preparation.

The GitHub ownership transfer updates package documentation links to lapismd/lapis-ai; the owning ACP requirements and runtime behavior are unchanged.

Security and trust

lapis-acp runs an autonomous coding agent with tool access on a local machine. These requirements are scoped to the ACP crates. 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
LAI-SEC-001lapis-acp MUST NOT implement authentication, identity, key custody or author gating; the controlling application is the trust boundary.
LAI-SEC-002Credentials MUST NOT appear in process arguments and MUST NOT be written to a log, a diagnostic or a terminal record.
LAI-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.
LAI-SEC-004lapis-acp MUST NOT modify global Git configuration, install a credential helper or persist a signing key.
LAI-SEC-005A failed, cancelled or uncertain invocation MUST NOT be retried automatically.
LAI-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.
LAI-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: lapis-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 ACP cannot prevent, and LAI-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.

Pinned source import

Requirements: LAI-GOV-100, LAI-GOV-102, LAI-LOCAL-001, LAI-COMPUTE-001. Milestone review: M0 remains In progress; source import is not product acceptance.

Source bytes were read with jj file show -r REV PATH from the agreed committed revisions. docs/imports/source-manifest.json retains per-file SHA-256 and source paths. Package/import names were changed mechanically. Agent product entrypoint composes maintained model/metadata helpers. ACP versions and compute ALPN remain.

Commands: jj git init /Users/stevejuma/code/lapismd/lapis-ai, source file lists, source revision reads, and the ignored local import script. Import: 186 records. No source repository, running service, model cache or installed executable changed. Next: validate workspace manifests, then run deterministic tests and record results in a fresh evidence entry. Native, mixed-version and platform gates are pending.

Shared native AI foundation

Evidence: E-AI-FOUNDATION-001. Revision: enclosing Jujutsu change srssqlou. Requirements: LAI-GOV-100, LAI-GOV-101, LAI-GOV-102, LAI-GOV-103, LAI-GOV-104, LAI-CONFIG-001, LAI-CONFIG-002, LAI-CONFIG-003, LAI-RUNTIME-001, LAI-RUNTIME-002, LAI-RUNTIME-003, LAI-RUNTIME-004, LAI-LOCAL-001, LAI-COMPUTE-001, LAI-TAURI-001, and imported ACP requirements. Milestone review: M0, M1, M2 and M3 satisfy their foundation gates. M4 remains separate platform, native inference, interoperability and staging qualification. Gate result: passed for M0–M3.

Scope and contracts

The standalone workspace contains eleven unpublished packages and four production helpers. The six ACP packages were renamed without changing their Control V1–V3 wire formats or legacy environment namespaces. Agent composition retains the maintained Buzz fork, metadata/model discovery, provider adapters, permissions, private token-source IPC and Apache notices. Core-agent tests use a distinct fixture binary so product discovery does not change their tested contract.

Compute retains jj-store/compute/1, invitations, model controls, authenticated loopback HTTP and admitted inference-only peers. Root configuration is inert, strict version-1 TOML. JSON wire/state and catalog compatibility inputs remain. TOML catalogs and MCP declarations resolve file-relative executables and secret references; unknown runner fields and alias collisions fail closed. Application trust, context, conversation state and credentials remain consumer-owned. Tauri is a documented future adapter contract; no implementation or consumer migration is included. No shared AI controller is introduced.

Supervised clients own bounded subprocess transport, readiness, exact identities, permission choices, environments and teardown. Unknown outcomes never trigger prompt replay. Compute HOME, XDG, Hugging Face and Mesh caches follow the private managed compute directory. Generic subprocess output is bounded at 4 MiB/256 frames, ACP requests at 1 MiB and compute control at 65536 bytes including newline. Resource loading checks target/version, complete helper inventory, paths, hashes and optional native ABI. No credentials appear in Debug output or diagnostics.

Imported ACP contracts retain their requirement suffixes under the LAI prefix. Source publication and historical cloud-runner qualification are explicitly separated from this repository’s results. The frozen twenty-file exception set is backed by original file hashes/counts; reductions are retained and no baseline was raised. New Rust/script files are limited to 300 counted lines and tests 500. Specification Markdown uses 400 physical lines and 32768 bytes.

Commands

Initial source import commands and pins are recorded in import evidence. Source bytes and original lock hashes are recorded in docs/imports/source-manifest.json and lock-inputs.json.

The following commands were run from the new repository; Cargo always used pnpm dev:cargo -- and locked resolution where applicable:

  • pnpm install --frozen-lockfile.
  • pnpm dev:cargo -- check --workspace --locked.
  • pnpm dev:cargo -- test -p lapis-ai-config --locked and test -p lapis-ai-runtime --locked.
  • pnpm dev:cargo -- test -p lapis-acp-agent --test toml_catalog --locked.
  • pnpm dev:cargo -- test -p lapis-compute --test control --locked.
  • pnpm dev:cargo -- test -p lapis-ai-runtime --test lifecycle --locked.
  • pnpm dev:cargo -- test -p lapis-ai-config compute_launch_keeps_model_caches_inside_its_managed_working_directory --locked.
  • pnpm dev:cargo -- clippy --fix --allow-dirty --allow-no-vcs --workspace --all-targets --locked -- -D warnings, followed by ordinary non-fix clippy.
  • pnpm format, pnpm check, pnpm test, pnpm build, pnpm spec:validate, pnpm spec:first and pnpm spec:check.
  • python3 scripts/client-acceptance.py --acp target/debug/lapis-acp --agent target/debug/lapis-agent --tools target/debug/lapis-agent-tools --client target/debug/examples/native-session, once with --allow and once without.

The final full integration scope is required because this is a new shared workspace spanning the ACP, native services and client packages. Earlier exact regressions were run first. Development logs under ignored .local support the record; they are not the durable evidence authority.

Results

Workspace build, non-fix clippy, formatting, architecture, boundaries, progress, upstream pin self-test, governance tests and specification checks passed. The specification defines 78 requirements. The final integration suite passed 932 Rust tests and 18 governance tests, with two explicitly ignored Rust tests. The integration suite retains provider-free deterministic evidence; platform and native claims live in the separate qualification record.

Client acceptance proves TOML selection, native-agent launch, exact permission approval/denial and file-write gating through a separate Rust consumer. Runtime regressions prove environment/cwd isolation, malformed readiness rejection, process reaping, exact option IDs, frame limits, no replay on timeout, latched output coverage loss, compute command bounds and loopback readiness. Agent discovery/metadata acceptance uses the actual native helper and tests bounded single JSON documents at EOF separately from ACP newline framing. Resource regressions reject corrupt content, foreign platforms, wrong ABI/missing native manifest and symlink/path escape. Helper regressions prove startup configuration, HTTP authentication, no startup model downloads and immediate next-command use.

Failures and corrections

  • Fresh Mesh dependency resolution failed because all zip ^2 releases were yanked. The committed original compute lock seeded resolution; the final unified lock preserves that historical resolution and additional workspace packages. No dependency source, version pin or safety check was patched.
  • Combining source locks initially exposed different Tokio resolutions; that approach was discarded in favor of the pinned compute lock seed.
  • Original agent test assumptions conflicted with the new product wrapper. A separate core fixture preserves those assumptions. A copied Rust-2021 test used let-chains; equivalent conditional syntax preserved its assertions.
  • macOS temporary paths differed through /var and /private/var; the isolation regression now compares canonical paths.
  • Formatting exceeded one imported LLM file’s frozen count by four lines. Two unchanged classifiers were extracted; the exception was ratcheted down.
  • The TOML reader initially changed a JSON diagnostic; the original JSON parser and its regression were restored.
  • The Rust ACP example initially omitted prompt duration and then execution-mode configuration. It now follows the actual V3 warm/open/configure/prompt sequence; both allow and deny acceptance pass.
  • The output-overflow test initially expected a successful helper exit after its output pipe closed. It now checks latched coverage loss and accepts only the corresponding success or classified BrokenPipe helper exit during teardown.
  • The Rust compute example rejected a download because HOME/cache paths were outside its managed cwd. The exact cache-boundary regression failed before deriving private cache paths and passes afterward, as does native Rust-client inference. A trailing-separator mismatch was fixed without weakening the test.
  • The actual agent metadata helper emits one JSON document without a trailing newline. The initial newline reader reproduced helper_protocol_failed. Agent clients now use bounded single-document framing; the actual helper acceptance and the EOF/extra-document regression pass. ACP framing is unchanged.
  • Progress validation initially rejected two recorded inputs composed into one agent destination; it now rejects duplicate source identities while allowing that documented composition.
  • Spec-first initially excluded the exact governance.md name. Its canonical pattern and per-layer ownership mappings were corrected; the gate passes.

Gaps

Node and Python are development tools, not shipped runtimes. Cloud credentials, installed third-party ACP adapters, hosted CI, release builds, signing, publication and Tauri implementation remain outside this foundation. The two explicitly ignored tests require provider or prepared-native prerequisites and are not silently counted as deterministic passes.

Next action: qualify and record M4 independently, then adopt these libraries in a separately authorized application integration.

Native, platform, interoperability and staging qualification

Evidence: E-AI-QUALIFICATION-001. Revision: enclosing Jujutsu change srssqlou. Requirements: LAI-LOCAL-001, LAI-COMPUTE-001, LAI-COMPUTE-002, LAI-PACKAGE-001, LAI-PACKAGE-002, LAI-RUNTIME-001, LAI-RUNTIME-002, LAI-RUNTIME-003, LAI-RUNTIME-004, LAI-GOV-103, LAI-GOV-104. Milestone review: M4 qualifies the profiles below; M0–M3 retain their separately recorded foundation acceptance. Gate result: passed for M4.

Profiles and immutable inputs

  • macOS 27.0 ARM64, Rust 1.99.0 (b940084d7), Node 26.9.0, pnpm 10.34.5.
  • Linux aarch64 CPU in isolated Rust 1.99.0 Debian Trixie containers. Image: rust@sha256:81015672d6939b0265f09adf96d5de125482bb1330106076956b0518e74edc90. The container’s glibc is 2.41; this pinned native artifact requires at least 2.38.
  • Mesh source: 9f192c9d821991ba43d79bb15fd2f2f12e597571, Mesh version 0.76.0-rc9, Skippy ABI 0.1.44. Native backends: darwin-aarch64 Metal and linux-aarch64 CPU. Vendor manifests retain all native file hashes and patches.
  • Real inference model: unsloth/Qwen3-0.6B-GGUF:Q4_K_M, GGUF SHA-256 ac2d97712095a558e31573f62f466a3f9d93990898b0ec79d7c974c1780d524a.
  • Pristine legacy helper: JJ Store source revision 01ea0741ff608bbe14daa8d929448e3ec0790afd, original compute Cargo.toml, Cargo.lock and seven source files, materialized under .local/legacy-compute. Executable SHA-256: a8f2d0ff7399981802cdd54114ebe81d1fceee5a19c2b026d05c413a8426dd3d. The original helper source and lock were not changed for acceptance.

Commands

Commands ran from the new repository. Paths below identify ignored isolated test roots, never an installed service or user model cache.

python3 scripts/local-compute.acceptance.py --compute target/debug/lapis-compute --root .local/macos-native --acp target/debug/lapis-acp --agent target/debug/lapis-agent --tools target/debug/lapis-agent-tools
python3 scripts/local-compute.acceptance.py --compute dist/macos-arm64-debug/lapis-compute --root .local/macos-bundled-native --native-runtime dist/macos-arm64-debug/native-runtime --acp dist/macos-arm64-debug/lapis-acp --agent dist/macos-arm64-debug/lapis-agent --tools dist/macos-arm64-debug/lapis-agent-tools
pnpm dev:cargo -- build --manifest-path .local/legacy-compute/Cargo.toml --target-dir target --locked
python3 scripts/compute-interop.py --lapis target/debug/lapis-compute --legacy target/debug/jj-store-compute --legacy-sha256 a8f2d0ff7399981802cdd54114ebe81d1fceee5a19c2b026d05c413a8426dd3d --native-runtime .local/macos-native/native-runtime/0.76.0-rc9/meshllm-native-runtime-darwin-aarch64-metal --root .local/interop-hf
pnpm package:stage -- --profile debug --destination dist/macos-arm64-final --native-runtime .local/macos-native/native-runtime/0.76.0-rc9/meshllm-native-runtime-darwin-aarch64-metal
python3 scripts/client-acceptance.py --acp dist/macos-arm64-final/lapis-acp --agent dist/macos-arm64-final/lapis-agent --tools dist/macos-arm64-final/lapis-agent-tools --client target/debug/examples/native-session --catalog-client target/debug/examples/agent-catalog --bundle dist/macos-arm64-final --allow
python3 scripts/compute-client-acceptance.py --client target/debug/examples/native-compute --compute dist/macos-arm64-v1/lapis-compute --native-runtime dist/macos-arm64-v1/native-runtime --root .local/rust-bundle-final --bundle dist/macos-arm64-v1

Source materialization used jj file show -r PIN runtime/compute/PATH for every original file and the committed lock. scripts/prepare-legacy.py provides the same reproducible materialization with source and lock checksum verification. The legacy artifact hash was checked before interop startup.

Linux full qualification used docker run --rm --name lapis-ai-linux-final with CARGO_HOME=/cargo, mounts of this repository at /work and its ignored .local/linux-cargo at /cargo, workdir /work, image rust:1.99.0-trixie, and sh scripts/linux-qualification.sh. Environment LAPIS_AI_NATIVE_ACCEPTANCE_ROOT=/work/.local/linux-cold-final selected a new private root. This script built the workspace/examples, ran the workspace suite, ran client allow/deny and cold native inference plus approved ACP editing.

A subsequent isolated lapis-ai-linux-bundle container with the same mounts and image ran the following after installing Node, Python, pkg-config and libssl-dev. CARGO_TARGET_DIR=/work/.local/linux-target, CARGO_BUILD_JOBS=1:

node scripts/managed-cargo.mjs build --workspace --locked
node scripts/managed-cargo.mjs build -p lapis-ai-runtime --examples --locked
node scripts/managed-cargo.mjs test -p lapis-ai-runtime -p lapis-ai-config --locked
node scripts/stage.mjs --profile debug --destination dist/linux-aarch64-v1 --target-dir .local/linux-target --native-runtime .local/linux-cold-final/native-runtime/0.76.0-rc9/meshllm-native-runtime-linux-aarch64-cpu
python3 scripts/client-acceptance.py --acp dist/linux-aarch64-v1/lapis-acp --agent dist/linux-aarch64-v1/lapis-agent --tools dist/linux-aarch64-v1/lapis-agent-tools --client .local/linux-target/debug/examples/native-session --catalog-client .local/linux-target/debug/examples/agent-catalog --bundle dist/linux-aarch64-v1 --allow
python3 scripts/compute-client-acceptance.py --client .local/linux-target/debug/examples/native-compute --compute dist/linux-aarch64-v1/lapis-compute --native-runtime dist/linux-aarch64-v1/native-runtime --root .local/linux-rust-bundle --bundle dist/linux-aarch64-v1

Focused native diagnostics also ran pnpm dev:cargo -- test -p lapis-compute native_installed_model_starts_and_serves_without_public_discovery --locked -- --ignored --nocapture with explicit private test roots/bundles. Selected imported GGUF diagnostics used LAPIS_AI_NATIVE_TEST_MODEL; later direct test-binary runs used a private HOME and the same exact imported ID. These are separate from the cold helper and platform acceptance above.

Results

macOS cold preparation/download/start, real inference and the exact approved ACP file write passed. Bundled-runtime acceptance passed from a fresh root and left the native download cache empty. Model download remained an explicit operation. The independent Rust compute client passed actual native inference after the managed-cache fix. Agent discovery and single-document metadata passed through the reusable Rust client and actual native helper using a fixture provider.

Linux full deterministic qualification passed 931 tests with two explicit ignored tests. The subsequent affected-crate run passed 17 configuration/client tests, including the new EOF-framing regression added after that full run. The macOS final full run passed 932 Rust tests and 18 governance tests. Linux cold native CPU inference and approved ACP file editing passed with the unchanged terminal completion, permission and file-content assertions. No fixture inference was substituted for either native platform gate.

Both lapis-compute -> jj-store-compute and the reverse passed authenticated QUIC model discovery and real native inference. They refused missing mutual admission and sharing-off requests, refused a revoked peer, and preserved local inference after revocation. This qualifies the retained jj-store/compute/1 wire profile without editing or deploying JJ Store.

The macOS and Linux bundles contain exactly four production helpers, native resources, manifests and notices, including maintained Buzz, Mesh Apache and pinned llama.cpp MIT texts. Model weights and test executables are absent. Rust bundle loading and native client acceptance verified target, inventory, checksums, paths and native ABI. Final macOS notice-only restaging retained the exact previously qualified helper and native-file hashes.

Manifest SHA-256:

  • dist/macos-arm64-final/runtime-manifest.toml: b74d6e05beb622aa7427658b5d8aacf99228e725e82eaf46b65cd1404a7111ce.
  • dist/linux-aarch64-v1/runtime-manifest.toml: 3a29fa80584d2b1053761ac0cfbaf6fb15250b98ad88ed310af5af1d1cc9c735.

Failures and observations

  • The first Linux parallel test link was killed for memory pressure. Serial linking passed; the Linux launcher now uses one Cargo build job.
  • Debian Bookworm deterministic tests passed, but native loading failed because its glibc 2.36 lacks GLIBC_2.38. Trixie passed actual loading/inference. This is a pinned artifact host prerequisite; no dependency or loader was patched.
  • An initial warm Trixie native-agent turn reached permission but did not finish successfully. Its terminal classification was not captured. The subsequent fresh cold qualification passed without changing the completion assertion or the 180-second turn bound. Native model output remains nondeterministic.
  • Two initial interop attempts using an imported-only GGUF failed legacy helper startup. Explicit Lapis test diagnostics with the same imported bytes passed, including the 4 GiB limit. The qualified mixed-version profile uses an explicit named-model download; imported-only legacy startup remains unqualified.
  • macOS linking reports an inherited oversized __eh_frame warning. It did not prevent workspace checks, tests or real inference. It remains an observed native dependency/toolchain warning rather than a suppressed gate.
  • Initial resource staging lacked recursive directory-copy configuration; staging was corrected before successful artifact acceptance.
  • Initial spec evidence used a requirement-shaped evidence identifier and omitted its navigation entry. A distinct evidence prefix and SUMMARY entry corrected the document checks.

Gaps

Qualification is macOS ARM64 Metal and Linux aarch64 CPU with the pinned small model, debug helpers and native ABI. No x86 platform, other model/backend, release build, distribution signing or installer is qualified. Native output is nondeterministic; the earlier failed Linux turn and imported-only legacy gap are retained above. Hosted CI is configured but has not run. Tauri adapters remain a contract. Cloud providers, streaming inference, cross-application daemons, consumer migration and public release remain separate work.

Next action: integrate an explicitly admitted application adapter in a separate change, and qualify broader native/model/release profiles before distribution.

E-RELEASE-SETUP-001: public repository and release automation

Revision: enclosing Jujutsu change zvxxoukm.

Milestone review: M0–M4 retain their prior results. M5 is In progress until hosted CI, Changesets and Pages have been observed. Actual Cargo/native publication remains deliberately unexecuted; the initial version PR must stay open.

Scope and contracts

LAI-REL-001–007 add public hosting, exact-source CI, coordinated version PRs, protected Cargo/native release and canonical mdBook deployment. LAI-LOCAL-001, LAI-RUNTIME-004 and ACP requirements retain provider, permission, wire and unknown-outcome behavior. Cargo metadata, notices and development-only binary features change packaging, not production behavior. Embedded agent JSON inputs move into the Cargo package with identical source hashes. No frozen source-import budget grows, and all upstream/runtime pins remain unchanged.

Ten crates are admitted to crates.io; lapis-compute stays native-only because its pinned Mesh 0.76.0-rc9 crates are absent from crates.io. Native bundles retain macOS ARM64 Metal and Linux ARM64 CPU/glibc 2.38+ profiles. The npm coordinator stays private. No release tag or registry publication is performed.

Commands

  • Inspected Jujutsu status, parent/repository guides, canonical contracts and pinned jj-acp release workflows/scripts at 47946b870b18652964d94c0af110ea177c66a80c.
  • pnpm install --ignore-pnpmfile; pnpm format.
  • pnpm release:check; pnpm package:check --allow-dirty.
  • node --test scripts/changesets.test.mjs scripts/ci-automation.test.mjs scripts/registry-check.test.mjs scripts/release-orchestration.test.mjs.
  • pnpm dev:cargo -- fetch --locked; node --test scripts/changesets.test.mjs.
  • node --test scripts/release-identity.test.mjs.
  • pnpm check:workflow; pnpm check:architecture; pnpm check:progress.
  • pnpm spec:validate; pnpm spec:first; pnpm spec:build.
  • pnpm check; pnpm test; pnpm build (complete integration is required because release/test infrastructure and metadata span all packages).
  • pnpm dev:cargo -- test -p lapis-agent --lib model_capabilities --features test-support --locked.
  • pnpm dev:cargo -- publish --workspace --exclude lapis-compute --locked --dry-run --allow-dirty.
  • pnpm test:governance (final 41-test suite); bash scripts/native-qualification.sh debug (cold local qualification running during first push).
  • Compared SHA-256 of both relocated JSON inputs with immutable import records.
  • gh repo create stevejuma/lapis-ai --public with description and Pages homepage; jj git remote add origin git@github.com:stevejuma/lapis-ai.git.
  • GitHub API configured read-only default Actions tokens, bot PR creation, disabled auto-merge and a maintainer-reviewed, version-tag-only release environment. No release credential was configured.

Results

Release metadata, package inventory, workflow lint, architecture, import pins, canonical validation, spec-first mapping and local mdBook build passed. The actual Changesets fixture advances all eleven packages to the next patch, updates locks and internal ranges, consumes notes and preserves canonical evidence. Offline bootstrap failure preserves both notes and version metadata. Narrow bot exceptions retain canonical, packaging and product gates. Identity regressions reject differing tags/checkouts and non-manual/non-initial bootstrap attempts.

Complete integration passed: 932 Rust tests, zero failures, two explicitly ignored tests; 41 governance tests, zero failures. Full checks/Clippy, builds, examples and ten spec-check lanes passed. All ten admitted crates passed Cargo publication dry-run, including isolated package compilation. No upload occurred. The focused capability target passed 14 tests with one ignored generator. The new cold native workflow script is still running; its result is recorded separately after completion.

Failures

The initial package inventory failed on missing per-crate README/license/notice files; all admitted packages now carry them. The first Changesets fixture found an incomplete offline Cargo cache (wasm-streams); explicit locked fetch fixed it without consuming a note. The first Cargo dry-run found the agent’s embedded capability JSON outside its crate. Both embedded files were relocated unchanged, package inventory now checks them. A second dry-run exposed an undeclared Reqwest query feature that the workspace had supplied transitively through compute; the agent now declares it explicitly. The same publication target is rerun after both portability fixes.

Gaps

Hosted CI, generated version PR, Pages deployment and the new cold local native script remain pending. Protected bootstrap credentials and per-crate trusted publishers need setup when an initial release is authorized. No cloud provider credential, signing, notarization, new platform or consumer integration is qualified by this change.

Gate result: pending hosted qualification.

Next action: finish local integration, push main, inspect hosted workflows and retain the initial Version Packages PR open and unmerged.

E-RELEASE-FOLLOWUP-001: hosted setup and version-lock formatting

Revision: enclosing Jujutsu change mqkmvpos.

Milestone review: M0–M4 remain Done. M5 remains In progress while hosted product CI completes. Initial publication is intentionally deferred: Version Packages must remain open. This record supplements the immutable setup evidence.

Scope and unchanged contracts

LAI-REL-003–004 require the ordinary formatting gate on generated version PRs. Version preparation now formats pnpm-lock.yaml after regenerating it, and the real preparation regression checks that output. Bot recognition and all product, package, canonical and release-identity checks remain unchanged. ACP protocols, provider behavior, source pins and native ABI retain their qualified contracts. No Rust production source changes in this follow-up.

Commands

  • bash scripts/native-qualification.sh debug (started with the setup change, completed after its first push).
  • gh api repos/stevejuma/lapis-ai/pages --method POST -f build_type=workflow.
  • jj git push --bookmark main; inspected hosted jobs with gh run view, gh run watch, gh run list and the job-log API.
  • curl --fail --silent --show-error against the published root and /current/contracts/release.html.
  • gh pr view 1 --json title,state,isDraft,autoMergeRequest,files; read the version-branch package metadata and verified version 0.1.1/private coordinator.
  • gh api repos/stevejuma/lapis-ai/tags; gh release list.
  • node --test --test-name-pattern='real Changesets' scripts/changesets.test.mjs before and after the fix.
  • pnpm check; pnpm test; pnpm build; pnpm spec:check.

Results

The local cold macOS ARM64 native script passed every step: allow and deny clients, native preparation/download/start, real inference, approved ACP tool edit, four-helper staging, bundle acceptance and the Rust compute client. It used fresh private roots and retained Mesh 0.76.0-rc9 / ABI 0.1.44. The macOS compute linker retained the prior large-unwind-table warning; execution passed.

Changesets passed and created Version Packages #1. The PR is OPEN, at 0.1.1, with auto-merge disabled. It changes only the consumed note, version metadata, locks, changelog and installation examples. It contains no invented canonical specification or milestone evidence. CI and Pages were explicitly dispatched for its branch.

Main Pages passed and deployed https://stevejuma.github.io/lapis-ai/. Both HTTP document checks passed. The version-branch Pages build passed and skipped deployment. GitHub PR-event runs required approval; explicit dispatch supplied the actual version-branch checks.

The formatting regression changed from one failure to zero. The complete local follow-up check, build and test passed: 41 governance tests, 932 Rust tests, zero failures and two explicitly ignored Rust tests. All ten specification-check lanes passed. No package contents or Cargo graph changed after the successful ten-crate publication dry-run in the setup evidence.

Failures

Initial version CI failed its ordinary formatting gate on pnpm-lock.yaml. Offline pnpm lock regeneration changes its formatting; version preparation had formatted other outputs only. The new real preparation assertion reproduced the exact failure, then passed with the lock added to formatting. No check was disabled.

This installed JJ rejects the legacy --allow-new push option. Retrying with jj git push --bookmark main succeeded and preserved commit signing.

Gaps

Hosted main and updated version-branch product CI are pending. No release tag, GitHub release or crate was published. The protected release environment is maintainer-reviewed and restricted to version tags; no credential was added. Initial namespace bootstrap and ten per-crate trusted-publisher registrations remain prerequisites when the first release is authorized. Signing, notarization, other native platforms and consumer integration remain separate work.

Gate result: local fix, cold native acceptance, Changesets and Pages passed; hosted product CI pending.

Next action: push the formatting fix, verify updated hosted checks and retain Version Packages #1 open and unmerged.

E-ORG-TRANSFER-001: canonical organization repository

Revision: enclosing Jujutsu change kkwumuxz.

Milestone review: M0–M4 retain their qualified contracts. M5 remains In progress for hosted qualification and the separately authorized initial release. The Version Packages PR must remain open and unmerged.

Scope and unchanged contracts

At the user’s explicit request, the public repository moved from stevejuma/lapis-ai to lapismd/lapis-ai. Source history and PR #1 were preserved. Current package metadata, documentation links, release policy, API guards and workflow ownership checks now use lapismd. Local origin uses the organization SSH remote. Crate names, versions, source/native pins, wire protocols, provider behavior and client configuration remain unchanged. Prior historical records retain their original URLs and results.

LAI-REL-001–007, LAI-GOV-100 and the owning ACP/service/runtime requirements retain all product gates. Bootstrap/trusted-publisher instructions now use the organization as owner. No registry token, release tag, package publication or PR merge was performed.

Commands

  • Inspected Jujutsu status, organization membership/admin role and destination repository availability using gh api and gh repo view.
  • gh api repos/stevejuma/lapis-ai/transfer --method POST -f new_owner=lapismd.
  • jj git remote set-url origin git@github.com:lapismd/lapis-ai.git.
  • gh repo edit lapismd/lapis-ai --homepage https://lapismd.github.io/lapis-ai/.
  • Verified the transferred public repository, Pages configuration, release environment, Actions token defaults and open PR with GitHub API/read commands.
  • pnpm test:governance; pnpm check; pnpm test; pnpm build.
  • pnpm package:check --allow-dirty; pnpm dev:cargo -- publish --workspace --exclude lapis-compute --locked --dry-run --allow-dirty.
  • Formatting, specification, workflow lint and source searches verify the new canonical owner while preserving old historical evidence.

Results

The live repository is https://github.com/lapismd/lapis-ai, PUBLIC with admin access. Version Packages #1 remains OPEN with no auto-merge request. The protected release environment retained maintainer review and version-tag restrictions. Actions retained read-only default tokens and explicit bot PR creation. Pages retained workflow deployment and now reports https://lapismd.github.io/lapis-ai/.

The initial version-branch native jobs both passed: Linux ARM64 CPU and macOS ARM64 native inference, permissions, bundled resources and Rust clients. See the preserved run. Its canonical formatting failure is fixed by the preceding follow-up change. The transfer cancelled the still-running main jobs; completed Linux native acceptance was retained. Cancellation is not a product failure.

Organization-bound governance tests passed (41 tests). Full check/Clippy, builds, ten specification-check lanes and 932 Rust tests passed, with zero failures and two explicitly ignored tests. Package inventory and publication dry-run passed for all ten admitted crates with the new owner metadata. Workflow lint passed. Fresh owner-bound CI and Pages run after the source push.

Failures

No transfer or owner-bound regression failed. Package inventory initially refused the uncommitted metadata as designed; its explicit –allow-dirty local verification then passed. The preceding version-lock formatting failure and its fix remain recorded in follow-up evidence. The first Actions runs were interrupted by the repository transfer and require fresh organization runs.

Gaps

Fresh organization CI and Pages deployment are pending. Initial Cargo publication still requires protected namespace-bootstrap credentials and per-crate trusted-publisher setup for owner lapismd. Native-only compute, other platform/signing gaps and the unmerged version PR are unchanged.

Gate result: public transfer and owner-bound checks passed; final organization CI/Pages qualification pending.

Next action: push the validated owner updates, verify Pages and version-branch checks under lapismd, and retain PR #1 unmerged.