Anvil
Architecture

Project tooling

Trace project declarations, prepared runtimes, and session ownership across build tools and IDE clients.

Project tooling connects a build's declared scenario runtime to clients such as IntelliJ IDEA. Importing a project and running its code are separate operations: sync supplies a declaration, while preparation compiles and resolves the inputs needed by a tooling JVM.

IDE sync → build integration → project declaration → imported module data
Definition loading → declared preparation action → prepared launch → tooling runner
Run scenario → RunnerSession → global engine → launcher ScenarioFactory.create → prepared topology
Run all → ScenarioContext.start() → ready processes → global extensions and scenario setup

Keep the contracts separate from their adapters

OwnerResponsibility
anvil-integration/integration-gradle/gradle-artifactsTracked artifact file inputs and JVM property binding
anvil-integration/integration-gradle/gradle-toolingNative Gradle task discovery, definition scanning, and deferred runtime export
anvil-tooling/tooling-apiPortable scenario descriptors, sessions, actions, observations, and logs
anvil-tooling/tooling-extension-apiHost-side contributions using core scenario/process/player views
anvil-tooling/tooling-builtinOptional actions and observations for built-in capabilities
anvil-tooling/tooling-runnerDefinition discovery, sessions supplied with an engine factory, terminal commands and protocol
anvil-tooling/tooling-launcherExecutable CLI/IDE entry points, engine property parsing and default engine assembly
anvil-integration/integration-gradle/gradle-baseShared source-set setup, engine inputs, and internal plugin state
anvil-integration/integration-gradle/gradle-dslPublic Gradle DSL facade composed from the base and artifact integrations
anvil-integration/integration-gradle/gradle-artifactsNamed artifact registration, resolution, and task input wiring
anvil-integration/integration-gradle/gradle-pluginAssemble foreground execution and project discovery under me.whereareiam.anvil
anvil-integration/integration-gradle/gradle-junitAssemble Gradle testing and the independent JUnit runtime
anvil-integration/integration-gradle/gradle-platformsGradle adapters for platform providers
anvil-integration/integration-gradle/gradle-capabilitiesGradle adapters for capability providers
anvil-integration/integration-intellijIntelliJ plugin assembly composing the feature modules
anvil-integration/integration-intellij/intellij-apiContracts the IntelliJ features and build integrations share
anvil-integration/integration-intellij/intellij-commonStandard implementations of the shared IntelliJ services
anvil-integration/integration-intellij/intellij-scenarioScenario catalog, tooling launches, environments, consoles, and run configurations
anvil-integration/integration-intellij/intellij-accountAccounts, pools, sign-in, and the accounts a run is given
anvil-integration/integration-intellij/intellij-gradleNative Gradle adapter

Bind the IntelliJ integration

The plugin's modules and how they reach each other are described in the module architecture. This section follows one run through the scenario module's services.

SourceDiscovery owns source selection, native sync, and reloading after successful imports. Initialization begins when Anvil is first opened in a project. Observation continues until the project closes, independently of open views. Reloads coalesce imported revisions and wait for preparation and environment cleanup. Views read SourceDiscovery.Snapshot and submit selection, reload, or sync commands.

ScenarioCatalog owns the selected source's discovered definitions, loading state, and preparation output in one ScenarioCatalog.Snapshot. EnvironmentLifecycle owns the active environment and the retained sessions. Catalog state remains available after an environment fails; each EnvironmentExecution owns its own authoritative snapshot.

Both services share one tooling launch through ToolingHost. Discovery can prepare a source before the user selects a scenario; starting an environment reserves that same launch, including while preparation is still in progress. Reloading can replace an idle launch. ToolingLaunch owns preparation, the runner, the manifest, and the staged accounts it obtains from the AccountStaging service. Its private resource owner registers the manifest and the staged accounts as they are acquired, including during partial startup. ToolingConnection reads process output and waits for diagnostic delivery after termination. The resource scope then releases the accounts and the manifest, preserving the first failure and suppressing additional cleanup failures. A replacement starts only after this cleanup has completed.

Launch scheduling, cancellation, execution, and cleanup settle through one terminal-completion path. A failing listener cannot prevent other listeners, pending futures, or owned resources from being settled. Readiness and discovery callbacks remain separate from resource acquisition and release.

An EnvironmentSession stays bound to its original execution. Stopped sessions retain their state and output, and their controls cannot reach a newer environment. Closing a session's tab removes it from the retained list, but the project remains busy until its execution finishes cleanup. Session controls report transport submission asynchronously; contributed actions return their remote results.

ToolingClient.request(operation, payload) serializes writes, correlates replies, and completes pending results when the connection closes or fails. Each pending request retains the operation's response type, including collection element types. ToolingMessageCodec maps typed envelopes and shared payload models through Jackson, and validates protocol compatibility before delivering events and results. ToolingOperationRegistry binds shared operation declarations to typed session handlers and their cancellation policy. ToolingRequestReader decodes each registered request type generically, while ToolingProtocolWriter serializes typed response and event envelopes. Wire operations and envelopes stay inside the protocol implementations; views call explicit session methods such as restartProcess and console.

BuildIntegration is the native build-system extension point, implemented by intellij-gradle. Gradle SDK classes stay within that adapter. Output remains a neutral retained SessionLog; every view creates its own ConsolePresentation over it.

Each module's tests load that module's real descriptor fragment through a test-only plugin.xml, with stand-ins for contracts other modules implement; the plugin's own tests check how the fragments compose.

Exchange protocol messages

The runner and IDE use protocol version 7. The ready message advertises the runner's version; the IDE requires an exact match before submitting requests. Update the project's Anvil artifacts and the IDE plugin together when this version changes. Version 6 peers use a different action-target shape and are rejected by the version check.

Portable payloads such as ScenarioDescriptor, SessionSnapshot, LogEvent, ActionRequest, and ActionResult live in tooling-api. Their immutable Lombok builders carry Jackson binding metadata, so both peers use the model's field definitions and defaults. Mapper configuration, transport headers, request correlation, and dispatch remain in the implementations. Missing required model values and invalid enums or identifiers fail decoding; protocol mappers reject scalar type coercion.

Predefined operation contracts live at the tooling-api root. Each ToolingOperation<Q, R> declares one stable name, a request class, and a response type reference. A Void request type uses null and emits only the transport header. Payload models contain operation arguments; request IDs and operation names belong to the transport envelope.

Contract groupOperations
ScenarioOperationsDiscover definitions and start a selected scenario.
EnvironmentOperationsRead snapshots, start remaining processes, stop the environment, and invoke contributed actions.
ProcessOperationsStart, stop, or restart a process, and submit console commands.

For a connected IntelliJ tooling client, discovery returns its declared collection type:

CompletableFuture<List<ScenarioDescriptor>> definitions =
        client.request(ScenarioOperations.DISCOVER, null).result();

The exchange also exposes submitted(), which reports transport acceptance independently of remote completion. Adding an operation requires a shared declaration and a typed runner handler; request and response mapping use the generic transport path. Runner registration determines whether an operation can be interrupted or cancels pending work. Protocol framing, resource ownership, and cancellation remain outside the shared API.

An action request contains its target as the shared ActionTarget object:

{
  "id": "request-1",
  "operation": "action",
  "sessionId": "session-1",
  "actionId": "example.inspect",
  "target": { "type": "PROCESS", "name": "lobby" },
  "arguments": {}
}

The response echoes the request ID. The pending request supplies the expected result type, so the client can decode the response payload into a shared model after correlation. Round-trip tests run the actual runner protocol against the IDE codec, covering nested targets, results, logs, and rejected malformed requests.

On this page