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
| ID | Requirement |
|---|---|
| LAI-GOV-100 | Protected 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-101 | Authored specification Markdown MUST remain within 400 physical lines and 32768 UTF-8 bytes. Each requirement MUST have exactly one verification row. |
| LAI-GOV-102 | New 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-103 | Evidence MUST record commands, results, failures, affected formats, gaps and next action. Fixture, native inference, interoperability and platform evidence MUST remain distinct. |
| LAI-GOV-104 | Verification 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
| ID | Requirement |
|---|---|
| LAI-CONFIG-001 | Explicitly 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-002 | Explicit 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-003 | Reading 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-001 | Native 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-002 | ACP 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-003 | The 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-004 | Host 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
| ID | Requirement |
|---|---|
| LAI-LOCAL-001 | The 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-001 | Compute 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-002 | Lapis 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-001 | Staged 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-002 | macOS 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
| ID | Requirement |
|---|---|
| LAI-REL-001 | Cargo 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-002 | CI 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-003 | Changesets 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-004 | A 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-005 | Cargo 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-006 | GitHub 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-007 | Native 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
| ID | Requirement |
|---|---|
| LAI-TAURI-001 | Future 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
| ID | Status | Evidence |
|---|---|---|
| LAI-AGENT-001 | Implemented | Foundation |
| LAI-AGENT-002 | Implemented | Foundation |
| LAI-AGENT-003 | Implemented | Foundation |
| LAI-AGENT-004 | Implemented | Foundation |
| LAI-AGENT-005 | Implemented | Foundation |
| LAI-AGENT-006 | Implemented | Foundation |
| LAI-ARCH-001 | Implemented | Foundation |
| LAI-ARCH-002 | Implemented | Foundation |
| LAI-ARCH-003 | Implemented | Foundation |
| LAI-ARCH-004 | Implemented | Foundation |
| LAI-ARCH-005 | Implemented | Foundation |
| LAI-ARCH-006 | Implemented | Foundation |
| LAI-CLI-001 | Implemented | Foundation |
| LAI-CLI-002 | Implemented | Foundation |
| LAI-CLI-003 | Implemented | Foundation |
| LAI-CLI-004 | Implemented | Foundation |
| LAI-CLI-005 | Implemented | Foundation |
| LAI-CLI-006 | Implemented | Foundation |
| LAI-CLI-007 | Implemented | Foundation |
| LAI-CLI-008 | Implemented | Foundation |
| LAI-COMPAT-001 | Implemented | Foundation |
| LAI-COMPAT-002 | Implemented | Foundation |
| LAI-COMPAT-003 | Implemented | Foundation |
| LAI-COMPAT-004 | Implemented | Foundation |
| LAI-COMPAT-005 | Implemented | Foundation |
| LAI-COMPAT-006 | Implemented | Foundation |
| LAI-COMPAT-007 | Implemented | Foundation |
| LAI-COMPUTE-001 | Implemented | Foundation |
| LAI-COMPUTE-002 | Implemented | Qualification |
| LAI-CONFIG-001 | Implemented | Foundation |
| LAI-CONFIG-002 | Implemented | Foundation |
| LAI-CONFIG-003 | Implemented | Foundation |
| LAI-CTRL-001 | Implemented | Foundation |
| LAI-CTRL-002 | Implemented | Foundation |
| LAI-CTRL-003 | Implemented | Foundation |
| LAI-CTRL-004 | Implemented | Foundation |
| LAI-CTRL-005 | Implemented | Foundation |
| LAI-CTRL-006 | Implemented | Foundation |
| LAI-CTRL-007 | Implemented | Foundation |
| LAI-CTRL-008 | Implemented | Foundation |
| LAI-CTRL-009 | Implemented | Foundation |
| LAI-GOV-100 | Implemented | Foundation |
| LAI-GOV-101 | Implemented | Foundation |
| LAI-GOV-102 | Implemented | Foundation |
| LAI-GOV-103 | Implemented | Foundation |
| LAI-GOV-104 | Implemented | Foundation |
| LAI-LOCAL-001 | Implemented | Foundation |
| LAI-PACKAGE-001 | Implemented | Qualification |
| LAI-PACKAGE-002 | Implemented | Qualification |
| LAI-PROTO-001 | Implemented | Foundation |
| LAI-PROTO-002 | Implemented | Foundation |
| LAI-PROTO-003 | Implemented | Foundation |
| LAI-PROTO-004 | Implemented | Foundation |
| LAI-PROTO-005 | Implemented | Foundation |
| LAI-PROTO-006 | Implemented | Foundation |
| LAI-PROTO-007 | Implemented | Foundation |
| LAI-PROTO-008 | Implemented | Foundation |
| LAI-RUN-001 | Implemented | Foundation |
| LAI-RUN-002 | Implemented | Foundation |
| LAI-RUN-003 | Implemented | Foundation |
| LAI-RUN-004 | Implemented | Foundation |
| LAI-RUN-005 | Implemented | Foundation |
| LAI-RUN-006 | Implemented | Foundation |
| LAI-RUN-007 | Implemented | Foundation |
| LAI-RUN-008 | Implemented | Foundation |
| LAI-RUN-009 | Implemented | Foundation |
| LAI-RUNTIME-001 | Implemented | Foundation |
| LAI-RUNTIME-002 | Implemented | Foundation |
| LAI-RUNTIME-003 | Implemented | Foundation |
| LAI-RUNTIME-004 | Implemented | Foundation |
| LAI-SEC-001 | Implemented | Foundation |
| LAI-SEC-002 | Implemented | Foundation |
| LAI-SEC-003 | Implemented | Foundation |
| LAI-SEC-004 | Implemented | Foundation |
| LAI-SEC-005 | Implemented | Foundation |
| LAI-SEC-006 | Implemented | Foundation |
| LAI-SEC-007 | Implemented | Foundation |
| LAI-TAURI-001 | Implemented | Foundation |
| LAI-REL-001 | Implemented | Release setup |
| LAI-REL-002 | Planned | Release setup |
| LAI-REL-003 | Implemented | Release setup |
| LAI-REL-004 | Implemented | Release setup |
| LAI-REL-005 | Planned | Release setup |
| LAI-REL-006 | Implemented | Release setup |
| LAI-REL-007 | Planned | Release setup |
Milestones
| Task | Status | Dependencies | Gate | Evidence | Next action |
|---|---|---|---|---|---|
| M0 | Done | none | Standalone workspace, governance, imports and provenance | Foundation | Maintain source pins and frozen budgets |
| M1 | Done | M0 | Renamed ACP and retained deterministic suites | Foundation | Consumer adoption is separate |
| M2 | Done | M1 | Agent, tools and compute imports with fixture acceptance | Foundation | Preserve provider and wire boundaries |
| M3 | Done | M2 | TOML, clients, isolation and Rust examples | Foundation | Maintain direct client acceptance |
| M4 | Done | M3 | macOS ARM64 Metal, Linux aarch64 CPU, mixed-version and staged bundles | Qualification | Qualify broader native and release profiles separately |
| M5 | In progress | M4 | Hosted CI, Changesets, Cargo release automation and spec Pages | Organization transfer | Verify 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.
| Tier | Source | Editable | Auto-install |
|---|---|---|---|
| 1 | Compiled-in defaults | No | Never |
| 2 | Catalog file supplied by launch configuration | No | Never |
| 3 | Caller-supplied definition in a task or control frame | No | Never |
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
| ID | Requirement |
|---|---|
| LAI-AGENT-001 | A runner MUST be declared as data carrying identifier, label, command, arguments, environment floor, MCP transport and install hint. |
| LAI-AGENT-002 | Adding a runner MUST NOT require a code change, an install shell command or a network fetch. |
| LAI-AGENT-003 | Reserved harness environment keys MUST be stripped before a definition or caller override is merged, and MUST NOT be settable by any tier. |
| LAI-AGENT-004 | A disabled or unknown runner identifier MUST fail resolution and MUST NOT fall back to a default runner. |
| LAI-AGENT-005 | Runner identifiers MUST be validated against a fixed pattern and MUST be unique within a resolved catalog. |
| LAI-AGENT-006 | The 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.
| Identifier | Adapter | MCP transport |
|---|---|---|
codex | @agentclientprotocol/codex-acp | stdio |
claude | claude-agent-acp, legacy claude-code-acp | stdio |
opencode | opencode | stdio |
goose | goose acp | stdio |
cursor | cursor-agent acp | http |
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
| Module | Owns |
|---|---|
definition | RunnerDefinition, CommandSpec, McpTransport, identifier and label validation |
catalog | Tiers, collision rules, resolution and listing |
defaults | The compiled-in tier and its reserved identifiers |
config | Catalog document parsing |
environment | Reserved-key stripping and three-layer merge |
availability | The CommandProbe trait, a PATH probe, a fixed probe and availability reports |
error | Typed 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
| ID | Requirement |
|---|---|
| LAI-ARCH-001 | The repository MUST be a Cargo workspace of layered crates whose internal dependencies match the matrix enforced by scripts/boundaries.mjs. |
| LAI-ARCH-002 | lapis-acp MUST NOT depend on Nostr, transport, HTTP, TLS or database crates, and the boundary checker MUST fail closed when one appears. |
| LAI-ARCH-003 | Production 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-004 | ACP 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-005 | The controlling application MUST own runner selection, working directory, credentials and prompt content; lapis-acp MUST NOT derive them on its own. |
| LAI-ARCH-006 | A 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
| ID | Requirement |
|---|---|
| LAI-CLI-001 | The binary MUST expose run --task <path|-> executing exactly one prepared task in one fresh agent process and session, then exit. |
| LAI-CLI-002 | Exit 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-003 | SIGINT and SIGTERM MUST trigger bounded cleanup before exit and MUST NOT leave an unreaped child when cleanup succeeds. |
| LAI-CLI-004 | Every launch option MUST be settable by a command-line flag and by a matching environment variable, with the flag taking precedence. |
| LAI-CLI-005 | The binary MUST NOT write diagnostics, prompts or agent output to stdout on any path, including startup failure and signal handling. |
| LAI-CLI-006 | A 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-007 | serve –control-version 2 MUST select the interactive protocol explicitly while the default and one-shot commands retain version-1 behavior. |
| LAI-CLI-008 | serve –control-version 3 MUST explicitly select warm worker control; versions 1 and 2 MUST retain their existing contracts. |
Commands
| Command | Role | Status |
|---|---|---|
lapis-acp run --task - | Execute one prepared task document read from stdin | Planned |
lapis-acp run --task PATH | Execute one prepared task document read from a file | Planned |
lapis-acp agents | List resolved runners with availability and install hints | Planned |
lapis-acp serve | Persistent control transport with a standing session pool | Deferred |
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
| Module | Owns |
|---|---|
args | Flag and environment parsing, defaults, usage text, refusals |
run_task | Catalog assembly, prompt file read, launch configuration, one invocation |
agents | One JSON object per line describing each resolved runner and its availability |
signals | SIGINT and SIGTERM installation and the cancellation watch |
lib | Dispatch, 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
| ID | Requirement |
|---|---|
| LAI-COMPAT-001 | The upstream port source MUST be pinned by repository, path and commit in docs/upstream-lock.md together with a per-file disposition map. |
| LAI-COMPAT-002 | Adopted upstream attribution MUST be recorded in docs/licenses/NOTICE.md, and upstream repository-level agent instructions MUST NOT be copied into this repository. |
| LAI-COMPAT-003 | Upstream drift MUST be detectable by a script that materializes the pinned source into ignored scratch storage and verifies recorded digests. |
| LAI-COMPAT-004 | The Rust toolchain MUST be pinned and formatting MUST be checked by both the normal and the specification check lanes. |
| LAI-COMPAT-005 | Integration tests MUST reach a terminal outcome without a model provider, a network endpoint or a credential. |
| LAI-COMPAT-006 | Qualification against a real installed runner MUST be recorded separately and MUST NOT be inferred from fixture-peer tests. |
| LAI-COMPAT-007 | Every disposition in the port map MUST be one of ported, ported-with-changes, dropped or not-ported, and a dropped region MUST name the reason. |
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
| ID | Requirement |
|---|---|
| LAI-CTRL-001 | The controlling application MUST drive lapis-acp over a versioned newline-delimited JSON control protocol with no authentication and no network listener. |
| LAI-CTRL-002 | lapis-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-003 | Every 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-004 | Diagnostics and child stderr MUST stay on stderr so that stdout remains unconditionally parseable by the controlling application. |
| LAI-CTRL-005 | The task document MUST NOT change the working directory, executable, tools or credentials, which come only from trusted launch configuration. |
| LAI-CTRL-006 | A control frame MUST carry an explicit integer version, and an unsupported version MUST be rejected before any agent subprocess is started. |
| LAI-CTRL-007 | Reading a task document from a file or stdin MUST have its own bounded interval, separate from the execution deadline. |
| LAI-CTRL-008 | Interactive 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-009 | Explicit 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
}
| Field | Contract |
|---|---|
version | Integer 1. Any other version fails before agent launch. |
taskId | Nonblank string, 1 to 256 UTF-8 bytes, no control characters. Correlation only; not a deduplication key and not a claim. |
agent | Runner identifier that MUST resolve to an enabled catalog entry. This is a mis-execution guard, not an authorship check. |
prompt | Nonblank prepared task text. Context, evidence and reporting instructions belong here. |
maxDurationMs | Positive 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
| Module | Owns |
|---|---|
task | Document version 1, strict validation, the stable rejection code vocabulary |
input | Bounded reads from a path or stdin, capped while reading rather than after |
record | TerminalRecord, Status, ExitCode and the record-level outcome codes |
invocation | Resolving a runner, launching, executing one turn, stopping, and reducing to a record |
transport | The 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.
| Request | Fields | Answered by |
|---|---|---|
submit | requestId, task | one record frame, plus diagnostic frames |
cancel | requestId, target | ack, or error with not_in_flight |
runners | requestId | runners |
shutdown | requestId | ack, 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.
| Request | Fields | Answered by |
|---|---|---|
submit | requestId, scope?, task | queued, then one record, plus diagnostics |
cancel | requestId, target | ack, or error with not_in_flight |
runners | requestId | runners |
status | requestId | status |
shutdown | requestId | ack, 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
| ID | Requirement |
|---|---|
| LAI-PROTO-001 | The 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-002 | The 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-003 | The 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-004 | A turn MUST terminate in exactly one stop reason drawn from end_turn, max_tokens, max_turn_requests, refusal and cancelled. |
| LAI-PROTO-005 | An 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-006 | The protocol crate MUST remain free of process spawning, scheduling, credential handling and transport policy. |
| LAI-PROTO-007 | Session 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-008 | Inbound 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
| Method | Direction | Role |
|---|---|---|
initialize | client→agent | Negotiate protocol version and read agent capabilities |
session/new | client→agent | Open a session with working directory and MCP server list |
session/prompt | client→agent | Submit one prepared turn |
session/cancel | client→agent | Request cooperative cancellation of an in-flight turn |
session/set_model | client→agent | Select a model when the agent advertises the capability |
session/set_config_option | client→agent | Select a configuration option such as a reasoning effort |
session/update | agent→client | Stream turn progress, tool calls and content chunks |
session/request_permission | agent→client | Ask 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
| Module | Owns |
|---|---|
frame | Bounded newline-delimited reader and writer, skip counting |
envelope | Request, notification and response construction, inbound classification, id matching |
error | Typed wire failures and agent error extraction |
budget | Connection limits and the two-bound turn budget |
initialize | Client identity, advertised client capabilities, version negotiation |
content | Text content blocks and MCP server declarations |
session | session/new, session/prompt and session/cancel payloads and stop reason parsing |
config_option | Model and reasoning-effort discovery and selection |
update | session/update classification with raw pass-through |
permission | session/request_permission normalization and response construction |
handler | The inbound-traffic hook and a recording default |
client | The 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.
| Surface | cursor-agent acp | codex-acp 1.13.1 |
|---|---|---|
| Negotiated protocol version | 1 | 1 |
agentInfo | absent | name, title and version |
| Option identifier key | id | id |
| Model values | parameterized, bracketed | bare names |
thought_level option | absent | present, as reasoning_effort |
| Session modes | agent, plan, ask | read-only, agent and others |
| MCP transports | http and sse | http only |
| Extra capabilities | sessionCapabilities.list | auth.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
| ID | Requirement |
|---|---|
| LAI-RUN-001 | The runner MUST execute one prepared turn per session scope with at most one turn in flight per scope. |
| LAI-RUN-002 | Cancellation 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-003 | An 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-004 | A timeout MUST be reported as an unknown outcome and MUST NOT be recorded as success or as a proven failure. |
| LAI-RUN-005 | A 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-006 | Session 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-007 | On 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-008 | Interactive 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-009 | Warm 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
| Module | Owns |
|---|---|
launch | LaunchSpec, spawning with piped stdio, inherited stderr and a Unix process group |
process | ProcessGuard, bounded grace, process-group kill, bounded reap, Termination |
turn | Agent, TurnRequest, and run_turn for one negotiated, prompted, stopped turn |
outcome | TurnOutcome, 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.
| Module | Owns |
|---|---|
configuration | SessionConfiguration, 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:
- A separate configuration option in the
thought_levelcategory, with an adapter-defined identifier that must be discovered. - An attribute inside a parameterized model identifier, under any of
effort,reasoning_effortorvariant, where applying it means selecting a different model. - 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
| ID | Requirement |
|---|---|
| LAI-SEC-001 | lapis-acp MUST NOT implement authentication, identity, key custody or author gating; the controlling application is the trust boundary. |
| LAI-SEC-002 | Credentials MUST NOT appear in process arguments and MUST NOT be written to a log, a diagnostic or a terminal record. |
| LAI-SEC-003 | A 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-004 | lapis-acp MUST NOT modify global Git configuration, install a credential helper or persist a signing key. |
| LAI-SEC-005 | A failed, cancelled or uncertain invocation MUST NOT be retried automatically. |
| LAI-SEC-006 | Agent 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-007 | Resource 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 --lockedandtest -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:firstandpnpm 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--allowand 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 ^2releases 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
/varand/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.mdname. 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 version0.76.0-rc9, Skippy ABI0.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-256ac2d97712095a558e31573f62f466a3f9d93990898b0ec79d7c974c1780d524a. - 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_framewarning. 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-acprelease workflows/scripts at47946b870b18652964d94c0af110ea177c66a80c. 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 --publicwith 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
releaseenvironment. 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 withgh run view,gh run watch,gh run listand the job-log API.curl --fail --silent --show-erroragainst 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.mjsbefore 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 apiandgh 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.