Skip to content
HeapyPublic

About

Source-distributed Kotlin/Native build tool with explicit foreground sessions and artifact caching

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Kvik

An experimental Kotlin Multiplatform build tool, distributed with its build plugins as sources and compiled into a cached Kotlin/Native executable. Invocations own their workers; repeated development work uses an explicit foreground session.

The launcher compiles Kvik and its local source plugin into one Kotlin/Native executable. Its current commands include help, runtime-info, doctor, inspect, inspect-graph, resolve, check-lock, build, test, verify, run-script, inspect-script, resolve-script, check-script-lock, prepare-application, run-application, run-prepared, dev, verification, test-report, test-aggregate, coverage-report, coverage-check, coverage-aggregate, coverage-export, coverage-publish, explain, run-stats, packages versions, packages dependencies and dependencies. Consumer commands connect the locked project graph to the JVM/Native adapters, local compilation cache, remote JVM compilation cache and retained run results. Foreground sessions retain project models and action graphs across reloads. See PLAN.md, current status, build command contract and limits, launcher evidence and v1 contracts.

With tools matching the explicitly provisioned host lock:

./kvik --java-home "$JDK" --konan-home "$KONAN" -- runtime-info
./kvik --offline --no-build -- help
./kvik --offline --no-build -- inspect --project /absolute/project/kvik.json
./kvik --no-build -- resolve --project /absolute/project/kvik.json
./kvik --offline --no-build -- check-lock --project /absolute/project/kvik.json
./kvik --offline --no-build -- test --project /absolute/project/kvik.json \
  --tools /absolute/project/tools.json --module library --target macosArm64
./kvik --offline --no-build -- verification --run /absolute/project/.kvik/runs/RUN_ID

For agent-driven one-shot builds and tests, add --output summary:

./kvik --offline --no-build -- test --project /absolute/project/kvik.json \
  --tools /absolute/project/tools.json --module library --target jvm --output summary

This prints diagnostics and a compact build-summary after the run is committed. It includes the run directory, exact input identity, status, freshness at completion, requested/completed checks, test counts and action outcomes. Full events, test details and verification remain in that directory. Use verification --run PATH to check current freshness, run-stats --run PATH for timings and cache counts, explain --run PATH --action ID for an action, or test-report --run PATH --output NEW_DIRECTORY for test details. A summary describes the completed run; later source edits can make it stale.

--output events is the default full JSONL stream. The output selector applies to direct build and test commands; foreground, MCP and BSP keep their existing protocols. Output selection does not affect action keys or which tests execute.

On the same warm JVM fixture, stdout shrank from 7,140 to 1,569 bytes for build and from 12,508 to 1,693 bytes for test. Both formats reused the same compilation keys and ran fresh tests; these are output-size observations for one fixture, not measurements of agent productivity.

resolve explicitly publishes kvik.lock.json and fills .kvik/cache relative to the project. --store selects a shared content store, --lock selects a lockfile, and repeated --pin group:artifact:version options declare resolver policy. Supply the same pins to inspection and lock checks. check-lock verifies the lock and cached artifacts without downloading or repairing anything. resolve --offline also performs only that check; the launcher's --offline propagates to dependency commands. Publication uses a persistent .kvik-dependency-publication.lock in the destination directory and rejects configuration/lock changes observed during resolution. See CLI dependency evidence.

inspect exports the versioned project model for tools and editor integrations: configuration, file ownership by module/source set/target, tags, configuration origins and capabilities. Kotlin consumers can parse it with ProjectInspection.fromJson(parseJson(output)) from the core inspection model. Provider-backed inspection also carries the source revision, project root and manifest identity as one source context.

inspect also returns effectiveConfiguration, the canonical declaration and resolver-policy document identified by its configuration digest, and a settings map. Keys such as target:library:jvm:test:compilerOptions expose applied options, selected compiler plugins, dependency scope/export defaults, test budgets and configured application/framework/benchmark settings. Each value identifies whether it is configured, inherited, defaulted, derived or supplied through invocation policy; authored values carry configuration-file JSON pointers. invocation identifies resolver-policy values, including unchanged policy defaults. Build-plugin settings are available under buildPlugin:PLUGIN_ID:settings, and resolver controls under resolver:NAME. These are model settings: installed tools, resolved artifact versions, adapter-generated command lines and invocation-specific test filters are queried separately through graph/dependency/run inspection. Script declarations and resolution domains remain available in effectiveConfiguration.

Use inspect --file library/src/Example.kt --project /absolute/project/kvik.json for a focused file-inspection result. Paths are relative to the configuration directory, including for Kode revisions. The result lists source-set ownership, direct script ownership, generator inputs, conservative downstream build/test/ABI checks and target settings with configuration origins. Proposed or deleted files are matched against declarations with observed: false; unknown paths widen to all declared checks. Inspection does not execute those checks or validate tools and locks. The same query is available as kvik_inspect with {"file": "..."} through MCP. File inspection reads project declarations without expanding script annotations or imports, so a missing script cannot block it. Its configuration digest covers those declarations and resolver policy, as identified by configurationScope: "declaration-and-resolver-policy".

Use inspect-graph --module library --target jvm --tools /absolute/tools.json to inspect the planned build before execution. Select --mode test, compile-tests, classpath-main, classpath-test, benchmark-smoke, benchmark-measurement, application, package, abi-dump, abi-check or documentation for other graphs. Module and target selectors are repeatable; test mode also accepts the usual JVM coverage options.

The versioned graph-inspection result lists action prerequisites, reverse output.resultConsumers links, terminal actions for requested checks, mandatory plugin contributions and declared script/SQLDelight/KSP generated-source roots. Each generated root is relative to its producer's output tree and carries its module, source set and configuration JSON pointer. A result consumer receives the producer's action result; it need not consume every file in that output tree. output.declaredArtifacts lists the output-relative path, semantic kind and shape (file, tree, or file-or-tree) of each declared root. Built-in project and script graph actions describe their outputs, including compiler-plugin roots, coverage files and Native debug-symbol trees. An empty list means no declared outputs; null means a source plugin has not supplied a contract. Actual file inventories, digests and cache decisions remain unresolved until execution. A declared root may produce no files. The scheduler rejects a recipe whose roots disagree with its contract and rejects successful results with incompatible file/tree paths, including cache hits.

Test actions also expose testLaunch: runner implementation, target, timeout, resource reservations and the adapter's typed parameters. JVM parameters contain the ordered runtime classpath, selected classes/engines and optional JaCoCo instrumentation settings; Native and JS/Wasm describe executable or entry-point paths and supported filters. Paths are relative to staged action inputs. null means no launch contract was supplied. Inspection does not discover test cases or confirm that runtime artifacts are available.

Graph inspection checks the existing dependency lock against locally available metadata/artifacts and captures current inputs. It uses desired tool identities without checking compiler installations, creating a run, compiling or running generators. It does not write the local project or artifact cache. Kode selectors are supported; online source queries may cache provider metadata, while offline queries require previously cached source content. The JSON contract is graphInspection in the v1 schemas.

Agents can query the project model, package/dependency versions, retained run results and foreground-session state through kvik mcp --project PATH, a foreground MCP stdio server with cancellable queries and structured CLI results. Add --tools PATH to enable build, test, change-aware kvik_verify, script inspection and script execution using the same executor, cache and retained run results. Add --reports for local test/coverage reports, aggregation, thresholds and exports. Add --config-edits for guarded local configuration/lock edits or Kode transaction plans through MCP. Add --session-controls to control an existing foreground dev session. MCP also accepts Kode source selectors for inspection, dependency/package queries, validation, builds, tests and scripts without a checkout; see MCP usage.

BSP project import and builds

Register a project for a BSP client with:

./kvik --no-build -- bsp-install --project /absolute/project/kvik.json

This writes complete connection details atomically under the project's .bsp/ directory. A launch through ./kvik records that source launcher's explicit tool and cache settings; invoking the native executable directly records that exact cached executable. The file contains machine-specific paths. Repeating the same installation is harmless; different settings require removing the old connection file before installing again.

bsp --project PATH serves BSP 2.2 on foreground stdio. A client can initialize the connection, list module/target/ compilation targets, query source directories and resources, map an existing source file back to its targets, and request workspace/reload. A failed reload keeps the previous model available. The kvik/projectModel extension returns the same typed model as inspect; target data with kind kvik-kotlin includes source sets, compiler options and configuration identities. Closing input, sending build/exit, or losing the parent process ends the connection.

buildTarget/dependencyModules lists the selected external library versions from an existing validated lock and local content store. It does not resolve or download artifacts. Pass --lock and --store when using nondefault locations.

Enable IDE compile/test operations by registering verified tools:

./kvik --no-build -- bsp-install --project /absolute/project/kvik.json --tools /absolute/tools.json

The connection accepts buildTarget/compile and buildTarget/test for supported host targets. Compiling a test target compiles its sources without executing tests (the equivalent CLI operation is compile-tests). Requests use the ordinary build graph, lock, cache and resource budgets; optional --jobs, --memory-mib and --remote-cache settings are retained in the connection. Each connection executes one build request at a time while continuing to answer project queries. Reload is rejected until the build finishes. $/cancelRequest, input closure, exit, signals and parent loss cancel invocation-owned children.

Results include status, task notifications, file diagnostics and a kvik-build-result extension containing paths to the ordinary per-target run results. originId is preserved when supplied. Each target gets an exact independent build scope. Compiler diagnostics are cleared when that compilation succeeds. Test filtering and custom BSP arguments are not supported; configure test selection in the project.

With tools configured, buildTarget/run launches an explicitly configured main application target on JVM or the current Native host. The server advertises runProvider and marks those targets canRun. A request containing only target uses the configured entry point, arguments and environment, with the project root as its working directory. Optional arguments replace the declared arguments (an empty list clears them); environmentVariables overlay the declared environment, and workingDirectory accepts a local file URI. These overrides apply only to that execution. Preparation can reuse cached outputs; each application invocation executes anew.

Run requests remain active until the application exits. Project queries stay responsive; cancellation, connection closure and server shutdown stop the owned application and its descendants. A successful preparation followed by a failing application returns an error status. When originId is supplied, output streams arrive through run/printStdout and run/printStderr; otherwise output uses build/logMessage. Forwarding is bounded and the complete raw logs remain in the retained application run. Application stdin is closed. Alternate entry-point data extensions and interactive stdin forwarding are not supported.

With tools configured, buildTarget/jvmCompileClasspath returns ordered absolute paths to dependency JARs and workspace class directories. It uses the same compile closure as normal JVM compilation, excluding runtime-only dependencies and compiler plugins. Only prerequisite modules are compiled: main queries work with broken or empty main sources in the requested module; test queries require its main output but do not compile or run its tests. The connection stays responsive during preparation, and cancellation follows the same foreground ownership rules as builds.

The matching CLI operation is:

./kvik --no-build -- jvm-classpath --project /absolute/project/kvik.json --tools /absolute/tools.json --module app --target jvm --compilation test

Each successful run exposes classpath/<module>/<main|test>/manifest.json with ordered paths relative to its input/ directory. Materialized files belong to that run; later builds do not overwrite them. Failed or stale prerequisites produce an error instead of a partial classpath. Preparation records use classpath-main or classpath-test checks and do not claim that the requested sources compiled.

After a build runs a Native Kotest/KSP, SQLDelight, script or source-plugin generator, buildTarget/sources also returns its generated Kotlin files. buildTarget/inverseSources maps these files to the owning main or test targets. The kvik-generated-source metadata identifies each file's producer, action key, logical path and content digest. Files are restored from verified cache content into a retained directory for that build, including when generation succeeds but downstream compilation fails. Later builds use separate directories.

The connection sends buildTarget/didChange when generated sources change or the project is reloaded. A new build clears previous generated mappings for its generator owners, including prerequisite modules and a test target's main module; failed or cancelled generation does not restore them. Deleting the last SQL input removes that module's generated mapping. Reload starts with declared sources only. Source queries do not run generators: compile the owning target or a target that needs it to populate them. Source plugins declare ownership through ProjectGenerationContributor; BSP reconstructs those declarations and action recipes from the completed build's captured inputs and verifies output digests. Generated resources appear in buildTarget/resources with their declared source-set ownership. Kotlin source paths match the compiler inputs, allowing compiler diagnostics to link to the retained files. Each plugin producer's imported outputs are limited to 64 MiB, with at most 8 MiB per file. A new BSP build conservatively clears generated mappings on all targets served by source-plugin generators because their declarations may depend on any captured input; successful producers repopulate them. Reload clears all mappings. SQLDelight errors are published at their .sq source locations on the owning JVM main target, including when generation is a prerequisite of another target. Generator and compiler diagnostics are tracked separately: a failed generator retains diagnostics from the blocked compiler. Successful generation, including a cache hit or deletion of the last SQL input, clears previous SQL errors. Reload clears published diagnostics before importing the new model. BSP keeps a bounded connection-local history to replay observed warnings for exact cached action keys, including when returning to an earlier successful build variant. The exposed launcher is the original KSP output. Native adapter diagnostics link to separately retained copies of the exact prepared/kotest.kt and prepared/KvikKotestRuntime.kt compiler inputs. Their URIs, logical paths and digests appear in the launcher's data.diagnosticSources metadata; they are excluded from source lists and roots to avoid duplicate declarations. Rebuilds and reload clear old diagnostic mappings.

Debugging and unsaved-buffer support remain pending. Compatibility is exercised by the framed protocol clients in tools/test_bsp*_cli.py; individual IDE clients still need integration testing.

IDEA project export

Export a JVM, JS, Wasm or Native project into a new directory and open that directory in IDEA:

./kvik --no-build -- idea-export --project /project/kvik.json \
  --tools /project/tools.json --module app --target jvm \
  --sdk my-jdk-25 --output /project/.kvik/idea/first

--sdk is the name of an IDEA JDK configured to match the project's build tools. Repeat --target to import multiple platforms together. The export prepares locked JVM JARs, JS/Wasm KLIBs, shared-source metadata KLIBs and source/resource generators, including their prerequisites. Ordinary module dependencies become source-module edges, so errors in the edited modules do not prevent import. Main and test compilations retain their own platform settings, private dependency visibility and test friend access. Generated files and libraries are copied into the export; authored sources and resources remain in their original directories. The included Kvik build and Kvik test run configurations invoke the native build tool.

Native preparation captures stdlib and platform KLIB directories from the configured, verified Native toolchain in a separate local cache action. The export contains its own copies of those libraries; it does not invoke the Native compiler to prepare them.

prepare-ide accepts the same JVM/JS/Wasm/Native build selectors and retains the editor input model under the run's ide/<module>/model.json for JVM-only selections, or ide/<module>/<target>/model.json when selecting any non-JVM target, with ordinary files under each model's input/. Web preparation does not require Node. The ide-inputs-ready event identifies that directory. Preparation is a separate check from compilation or tests. Its model has a versioned ideInputs JSON Schema.

Source sets shared by at least two targets become separate common modules with their own roots and locked metadata. Refinement edges preserve expect/actual and internal visibility; shared tests receive explicit access to their main source sets, including when the test declaration omits a main refinement edge. Native-only shared source sets use Native stdlib for builtins such as kotlinx.cinterop. Their platform libraries are commonized for exactly the source set's declared Native targets in a separate, locally cached action. For example, a macOS+iOS source set can use shared platform.posix APIs, while macOS-only platform.AppKit remains unavailable. The pinned commonizer runs in an invocation-owned JVM with its own runtime classpath; it reads the verified Native distribution and does not use an ambient Gradle commonizer cache. Generated libraries are checked against the requested target intersection before being prepared for IDEA. Custom cinterop commonization remains pending. Compiler-plugin analysis, debugging and live project-model reload remain pending. idea-export rejects single-target common roots and compiler plugins; prepare-ide preserves their ownership/presence in the intermediate model. After configuration or generator changes, export to another new directory and open that generation. Existing exports are never overwritten.

tools/test_idea_export_cli.py exercises preparation, generator changes and the generated build/test commands. tools/test_idea_import.py --prepared-case PATH checks the resulting project with installed IDEA's Kotlin resolver and diagnostics in an isolated headless profile. The current inspector fixture is pinned to IDEA build 263.7307.44; it explicitly refreshes source roots and imports model changes as new projects, so it does not establish live GUI model reload.

The same import harness accepts --common-stdlib PATH_TO_STDLIB_ALL_JAR to test the common-source renderer with a transitive refinement chain and extracted KLIB metadata. It checks expect/actual, internal visibility, and rejection of JVM APIs from common code. tools/test_common_idea_export_cli.py exercises locked per-source-set preparation through the public export command, including shared test dependencies and generated roots. Its --web mode also exports JVM, JS and Wasm together, compiles web production/test sources and checks platform library kinds. Pass the resulting case to test_idea_import.py --prepared-case to verify platform-specific APIs and rejection of JVM APIs in web modules with installed IDEA. Add --native-target macosArm64 or --native-target linuxX64 to prepare the host Native SDK, execute a Native test and check Native platform APIs and test friends in the import harness, including Native builtins and commonized POSIX APIs in a source set shared with a second declared Native target. The import check rejects target-only APIs in that shared set. Linux CI combines --web with --native-target linuxX64.

Configuration changes

config validate parses the configuration and validates the project graph and resolver policy. It does not check a dependency lock or contact repositories. config plan applies a batch of structured edits in memory, validates the resulting project, and resolves its dependency lock. It returns the proposed configuration and lock contents with digests of the original files; the original files stay unchanged. Dependency resolution may populate the content store.

./kvik --no-build -- config validate --project /absolute/project/kvik.json
./kvik --no-build -- config plan --project /absolute/project/kvik.json \
  --edits edits.json > configuration-plan.json
# Inspect configuration.content and dependencyLock.content before applying.
./kvik --no-build -- config apply --plan configuration-plan.json

An edit batch has schemaVersion: 1 and one to 128 operations, for example:

{
  "schemaVersion": 1,
  "operations": [
    {"op": "set-dependency-version", "module": "app", "sourceSet": "main",
     "package": "org.jetbrains.kotlinx:kotlinx-coroutines-core", "version": "1.10.2"},
    {"op": "set-target", "module": "app",
     "target": {"name": "jvm", "main": "main", "test": "test", "compilerOptions": ["-Werror"]}}
  ]
}

The operations are:

  • set-dependency: supply module, sourceSet, and a complete dependency object from the project schema. Adds or replaces by package or workspace-module identity, rejecting ambiguous duplicates.
  • set-dependency-version: supply module, sourceSet, package (group:artifact), and version. Updates one existing external dependency, preserving scope and export settings.
  • remove-dependency: supply module, sourceSet, and selector, either {"package":"group:artifact"} or {"module":"library"}. Requires one match.
  • set-target: supply module and a complete target object. Adds or replaces by target name.
  • remove-target: supply module and target (the target name). Requires an existing target.
  • set-source-set: supply module and a complete sourceSet object. Adds or replaces by name, including source/resource directories, dependsOn edges and dependencies.
  • remove-source-set: supply module and sourceSet (the name). Requires an existing source set. Update any targets, source-set edges or generator settings that reference it.
  • set-catalog-entry: supply alias and a full Maven coordinate. Adds or replaces an entry in dependencyCatalog.
  • set-catalog-version: supply alias and version. Updates an existing catalog entry, preserving its group and artifact; all scripts using that alias see the update.
  • remove-catalog-entry: supply alias. Requires an existing entry.
  • set-dependency-profile: supply name and a complete profile object from the project schema. Adds or replaces its dependencies, compiler options and plugins.
  • remove-dependency-profile: supply name. Requires an existing profile.
  • set-script: supply name and a complete script object from the project schema. Adds or replaces the named script.
  • remove-script: supply name. Requires an existing script.
  • set-plugin-settings: supply plugin (composed build-plugin ID) and a complete settings object.
  • remove-plugin-settings: supply plugin. Requires existing settings.

The entire batch must produce a valid project; coordinated edits can temporarily invalidate the graph between operations. Unrelated authoring fields are preserved. Removing a catalog entry or profile still referenced by a script is rejected; update its consumers in the same batch. Set operations replace the named value, so include any existing settings you want to retain. For example, a shared script dependency update is {"op":"set-catalog-version","alias":"json","version":"2.0.0"}. To add a platform with dedicated sources, combine set-source-set and set-target in one batch. The final graph must contain all referenced source sets and have no dependsOn cycles. Removing a source set changes configuration only; source files remain on disk. Changed JSON is canonicalized, so formatting may change. The version in the example is illustrative; packages versions lists repository versions before choosing one. The edit and result schemas are in contracts.json, under configurationEdits, configurationValidation, configurationEditPlan, configurationEditResult, and configurationSourceEditPlan.

Planning accepts the usual --store, --lock, and resolver policy options. A local lock must be inside the configuration directory, separate from source inputs and the content store. --offline requires an existing lock compatible with the edited configuration; it cannot resolve new dependencies. Applying a plan uses its recorded paths and policy, verifies cached dependencies without network access, and rejects changed configuration or lock bytes with KVIK_EDIT_CONFLICT (exit 3).

Local application serializes cooperating Kvik publishers and retains an intent in .kvik/configuration-edits. The two files are replaced individually, so readers can briefly see a mismatched pair; normal lock validation rejects that pair. After interruption, repeat config apply with the same plan to finish publication. A repeated completed application returns unchanged. If another editor changes either file during recovery, application refuses to overwrite it; inspect the files and plan again. External editors do not take Kvik's locks, so they are checked before each replacement but are not excluded for the entire operation. There is no automatic rollback over another editor's changes.

config validate and config plan also accept the revision source flags --kode, --kode-home, --source-project, and --revision. A revision-backed plan returns a Kode transaction with head and blob preconditions, using the same explicit kode tx workflow described below for revision-backed resolve. It publishes configuration and lock together in one revision. Local config apply accepts local file plans only. An offline revision plan checks lock compatibility and does not propose a transaction.

Package queries return versioned JSON for agents:

./kvik --no-build -- packages versions org.jetbrains.kotlin:kotlin-stdlib \
  --project /absolute/project/kvik.json --json
./kvik --no-build -- packages dependencies org.junit.jupiter:junit-jupiter-api:5.13.4 \
  --project /absolute/project/kvik.json --target jvm --usage java-runtime --json
./kvik --offline --no-build -- dependencies --project /absolute/project/kvik.json --json

packages versions supports --channel all|stable|prerelease, bounded pages and cursors. Repository queries support --refresh and --offline, report their sources and freshness, and use a separate query cache. dependencies verifies the existing project lock and cached bytes without writes or network access; supply the same pins and store used for resolution. Queries keep available versions, original requirements and locked selections distinct. They do not execute package code or change configuration/locks, and availability does not establish compatibility. See the query contract and composed CLI evidence.

workspace dependencies connects locked dependencies to projects registered in Kode:

./kvik --no-build -- workspace dependencies \
  --kode /absolute/bin/kode --kode-home /absolute/kode-store \
  --store /absolute/dependency-cache --json

It discovers registered projects, pins their observed main revisions, and reads kvik.json and kvik.lock.json from each snapshot. Dependency metadata and artifacts must already exist in the selected cache; the query replays each lock without downloads, builds or writes. Registry discovery is live, so the collection of project revisions is not an atomic workspace snapshot. The output records every successfully read project's source revision and configuration/lock digests.

Repeat --source-project NAME to restrict the graph, or --at NAME=REVISION to pin a project explicitly. --revision SELECTOR applies a common selector to all selected projects. --project PATH and --lock PATH set common logical paths; --configuration NAME=PATH and --dependency-lock NAME=PATH override them per project. Configuration paths are relative to a source revision, and lock paths are relative to that configuration's directory. Supply resolution overrides as --project-pin NAME=GROUP:ARTIFACT:VERSION, --project-scope-pin NAME=MODULE:TARGET:COMPILATION:USAGE:PURPOSE=GROUP:ARTIFACT:VERSION or --project-exclude-metadata-edge NAME=JSON; ordinary pin/correction flags apply to every selected project.

Edges preserve module/script, target, compilation, usage and purpose from the consumer lock, including transitive and redirected packages. Matching uses the coordinates emitted by library packaging: target packages and multi-target roots. match distinguishes exact ownership, multiple publishers, another available version and packages outside the selected projects. Coordinate ownership leaves sourceArtifactEquivalence unchecked: current source may differ from published bytes. Missing inputs, invalid locks or ambiguous ownership return an incomplete graph and exit status 3. The command supports up to 256 projects, 16,384 publication coordinates, 65,536 edges and 16 MiB each of retained graph inputs and response JSON. Detailed per-project paths and artifact lists are discarded after lock replay. Discovery requires access to Kode; there is no offline registry fallback. The output follows the workspace dependency schema.

Named project scripts and standalone .kt or .kts files use explicit dependency locks:

./kvik --no-build -- resolve-script maintenance --project /absolute/project/kvik.json
./kvik --offline --no-build -- run-script maintenance --project /absolute/project/kvik.json \
  --tools /absolute/project/tools.json -- "space argument" @literal.args
./kvik --no-build -- resolve-script --file /absolute/scripts/update.kt
./kvik --offline --no-build -- inspect-script --file /absolute/scripts/update.kt \
  --tools /absolute/scripts/tools.json

Use inspect-script NAME --project PATH --configuration-only or inspect-script --file FILE --configuration-only before provisioning tools or resolving dependencies. This returns expanded dependencies, compiler options and plugins with profile/catalog origins, the effective configuration digest and script repository list. It reads reachable .kts imports and dependency/repository annotations without executing the body. It does not read a dependency lock, construct an action graph, provision tools or write project artifact blobs. Source queries accept the same Kode revision selectors and may retain provider metadata. The result has kind script-configuration-inspection and explicitly leaves dependency resolution and execution unrequested; it does not assert build readiness. MCP exposes the same query as kvik_inspect_script with configurationOnly: true, available without mcp --tools. Omit the flag for the existing locked inspection.

A standalone update.kt or update.kts requires an adjacent update.kvik.json companion and defaults to update.kvik.lock.json. Use check-script-lock with the same selector to verify an existing lock. Compilation supplies the pinned standard library and can be reused; script execution always runs afresh. Pass program arguments after --. Run stdout contains JSON events, while observations identify the script's separate UTF-8 stdout and stderr files. inspect-script reports inputs, expanded dependency/profile origins and action prerequisites without compiling or running. See the script contract and CLI validation status.

For top-level Kotlin scripts, set entryScript to a declared .kts source:

{
  "sources": ["scripts/update.main.kts", "scripts/helpers.kts"],
  "entryScript": "scripts/update.main.kts"
}

The primary script can use @file:Import("helpers.kts"); paths are relative to the importing file and must resolve to captured .kts sources. Imported code runs with the primary script, after compilation. Use args for program arguments and workspace module dependencies for helpers written as ordinary .kt files. entryScript and mainClass are mutually exclusive. A standalone update.main.kts uses update.main.kvik.json and update.main.kvik.lock.json.

Both .kt and .kts scripts support up to 16 workspace or explicitly described Maven compiler plugins, optionally combined with the pinned serialization shorthand. Explicit plugins run in declaration order, with distinct IDs, isolated classpaths and separate declared outputs. Changing any plugin's implementation, options or order invalidates script compilation.

The adapter supports the same locked dependencies and compiler options as ordinary scripts. resolve-script reads @file:DependsOn("group:artifact:version") and @file:Repository("https://host/path") from the entry script and imported helpers into the same dependency lock. There is no need to repeat those dependencies in JSON. Added repositories apply only to that script's dependencies, leaving module and compiler-plugin repositories unchanged. Compilation and execution use the existing lock without fetching.

Declarations accept literal strings (including escaped or triple-quoted strings), named arrays, and explicit imports/aliases of kvik.jvm annotations. Computed values, interpolation and spread arguments are rejected before resolution. Dependency, repository or import changes require resolve-script; body and comment edits retain the lock. A project lock covers all named scripts, so their entry files and reachable imports must be available even when running one script. Standalone companions retain independent locks. The same rules apply to immutable Kode revisions and offline checks. Compilation produces relocatable classes and a script descriptor; cache hits still evaluate the script in a fresh foreground-owned JVM.

All four script commands also accept --kode, --kode-home, --source-project and --revision to select an immutable Kode snapshot. For example:

./kvik --no-build -- run-script --file scripts/update.kt \
  --kode /absolute/kode --kode-home /absolute/workspace \
  --source-project repository --revision REVISION \
  --tools /absolute/tools.json --store /absolute/cache -- --revision literal-argument

With a source selector, --project and --file are logical revision paths; the lock path is relative to the configuration's directory. Tools, cache and run directories remain local, relative to the invoking directory. Script execution uses captured inputs in its action workspace. Inspection and run results retain the source revision, and reproduction commands pin that revision. Arguments after -- always belong to the script.

resolve-script returns a guarded Kode transaction for the lockfile. Apply it explicitly through kode tx, then use the returned revision for inspection or execution. It does not advance the workspace head or write a local source copy. --offline requires a previously captured immutable revision and cached inputs, dependencies and tools; missing source data never falls back to local files.

Named .kt and .kts scripts can generate Kotlin sources for a module source set. Add a generator attachment to the script declaration:

"scripts": {
  "generateModel": {
    "sources": ["scripts/GenerateModel.kt"],
    "generator": {
      "module": "library",
      "sourceSet": "commonMain",
      "inputs": ["model/schema.json"],
      "arguments": ["com.example.model"]
    }
  }
}

The script receives the output directory as args[0], the captured input root as args[1], then the declared arguments. For this declaration it reads File(args[1], "model/schema.json") and writes regular .kt files beneath File(args[0]). Each invocation starts in a private staged directory with fresh generated and inputs directories. Source paths and generator inputs are relative to the project configuration; generated files are held in the artifact store and do not appear in the project source tree.

The generator's compiled script, dependencies, resources and workspace module prerequisites use the existing script pipeline. Generation completes before compiling every target that includes the selected source set, and one generator execution is shared across those targets in the same build. Attach to a test source set to generate test sources. Generated common sources retain their source-set ownership for Native compilation, metadata and documentation. inspect --file reports generator input ownership and affected checks.

To generate resources, set generator.resources to a nonempty list of output file names, for example ["META-INF/version.json", "assets/logo.bin"]. The script must write exactly those files beneath args[0]. They become resources of the selected source set, available to tests, applications, scripts, workspace compiler plugins and JVM library packages. A resource generator does not add Kotlin sources. Generated resources use the same runtime/<module>/<main|test>/... layout as ordinary resources; inspect-graph exposes their producer and declared files in generatedResources. Conflicts with ordinary or other generated resources fail before execution. KLIB publication and Native frameworks retain their existing resource restrictions.

Plain JVM main and test compilation can reuse cached classes after edits to their own resources; fresh tests, application runs and packaging consume the updated resource bytes. Resource generators still execute and validate their outputs before reuse. Compilations using compiler plugins retain resource bytes in their inputs, as do other targets.

Both generator modes reject symlinks, executable files and input modification. Each file is limited to 8 MiB, with at most 10,000 files and 64 MiB total. Resource files may contain binary data. Missing or extra declared resource files fail the generator and block its consumers.

Generator execution always runs. Its compilation and downstream compilation can reuse cached outputs; identical generated bytes preserve the downstream compilation key. Because external effects remain possible, change-aware verification keeps generator consumers and downstream checks selected with generator-fresh:<name>, even for an empty diff. This staging is not a sandbox. Ordinary run-script retains its normal argument and execution behavior.

Successful generation requires unchanged staged inputs and regular, non-executable .kt outputs (up to 10,000 files, 8 MiB per file and 64 MiB total). Symlinks, invalid outputs, failure and cancellation cannot publish a successful generated output manifest. Module-level validation rejects generator dependency cycles, including runtime and compiler-plugin prerequisites; it is conservative across targets and compilation scopes. A standalone script cannot attach to a project source set.

build and test always consume the existing lock and cached dependencies without network resolution. Select modules and targets explicitly with repeatable flags. JVM tests are discovered from the selected module's compiled test output when testSettings.classes is omitted. Set testSettings.engines to select junit (default), kotest, or both. Production outputs and dependency JARs are available for loading but are not searched for tests. An explicit classes list restricts selection to those binary class names; an empty list selects nothing and produces incomplete verification. Discovery with no executable tests is also incomplete. Tests execute on every invocation; successful compilation can be reused locally. --tools names the explicit installed-tool configuration described in the build command contract. Run stdout contains versioned JSON events; compiler/test streams are retained in the run directory. verification checks whether a retained result still describes current inputs and tools. explain --run PATH --action ID describes a retained action’s inputs, settings and recorded execution or reuse reason.

For JUnit targets, filter discovery with literal tags:

"testSettings": {
  "includeTags": ["fast", "smoke"],
  "excludeTags": ["database"]
}

A test must match at least one included tag and none of the excluded tags. Omitting includeTags (or supplying an empty list) includes all tags and untagged tests before exclusions. Filters further narrow an explicit classes selection when supplied. Class-level and inherited tags follow JUnit tag filtering. Use literal names, not tag expressions; each list accepts up to 64 unique names, each 1–128 characters without whitespace, control characters or ,()&|!. Tag filtering currently requires only the junit engine; Kotest or mixed-engine filter requests are rejected.

Reports count the selected scope. Filtered-out tests are outside that scope and are not reported as skipped. An empty match returns incomplete, never a passing test check. Filters are retained in configuration and test action parameters; changing them preserves compilation cache keys. Run resolve after editing configuration, as for other project changes.

Independent module/target test actions run in parallel within --jobs and --memory-mib budgets (defaults: two CPU slots and 4096 MiB). Actions reserve one CPU slot by default. A target with tests can reserve more slots, reserve memory and declare shared resources:

"testResources": {
  "cpuSlots": 2,
  "memoryMiB": 768,
  "exclusiveResources": ["database:integration", "port:5432"]
}

Tests sharing any resource name run sequentially within the same Kvik run; unrelated tests remain eligible for parallel execution. Names are case-sensitive and shared across modules and targets. Give all tests the same name to serialize their execution, or use --jobs 1 for a build whose actions each need one slot. Resource ownership ends when the test action stops, including failure or cancellation. Separate Kvik invocations have separate resource pools.

tools/test_test_resources_cli.py exercises these policies with real JVM workers and records paired --jobs 1 / default-parallel measurements after warming compilation. --performance-trials defaults to four pairs with alternating order; test identities, outcomes and action keys must match. The fixture's parallel-comparison.json records invocation time, test-action slot occupancy, CPU time and largest individual test-child peak RSS, with tool and host identities. Slot occupancy is not CPU utilization, and individual RSS peaks do not represent combined process-tree memory. This small synthetic workload has no speedup threshold. Linux CI retains the JSON as the test-parallelism-linux artifact.

The public CLI suite also runs tools/check_worktree_isolation.py with two linked Git worktrees sharing one cache. It checks identical and divergent inputs, fresh test execution, separate mutable worker directories and cancellation of one run while the other continues. Barrier-controlled JVM tests verify actual overlap; recovery runs must reuse compilation and execute tests again. The gate can also run directly with --runtime PATH --fixture PATH, using a prepared fixture from tools/test_build_cli.py --prepare-only. Commands and results stay in the generated worktree-isolation-* directory beside that fixture.

These settings apply to JVM, Native (including Kotest), JS and WasmJS tests. cpuSlots defaults to 1 and accepts integers from 1 to 1024. The sum of active action reservations cannot exceed --jobs: with --jobs 4, two tests reserving two slots each may run together. A request larger than --jobs fails before test launch; increase that budget or lower the declared test concurrency. CPU slots reserve scheduler capacity; they do not limit arbitrary OS threads. For JVM JUnit tests, set testSettings.parallelism to configure a bounded worker pool:

"testSettings": {"classes": ["example.Tests"], "parallelism": 2},
"testResources": {"cpuSlots": 2}

parallelism defaults to 1, accepts integers from 1 to 1024 and must not exceed testResources.cpuSlots. Values above 1 currently require only the junit engine. JUnit methods and classes may then run concurrently, subject to JUnit's lifecycle, ordering, execution annotations and resource locks. Kvik configures both fixed parallelism and maximum pool size to the declared value; tests that block waiting for more peers than that budget can time out. Explicit test-created threads and other frameworks require their own concurrency configuration. See JUnit parallel execution.

Changing parallelism changes test action identity, while compilation remains reusable. inspect-graph exposes the effective value in the test launch contract (non-default values appear in parameters.parallelism). Tests still execute freshly. The scheduler accounts for the full cpuSlots reservation across all concurrent test processes. memoryMiB defaults to 512 and is a scheduler reservation, not a process heap limit; requests exceeding the run's memory budget fail before test launch. exclusiveResources defaults to an empty list. Names must be unique, start with an ASCII letter or digit, and contain only letters, digits, _, ., : or - (up to 64 names, each at most 128 characters). Changing test resources changes the test action identity while preserving compilation and linking cache keys. Tests still execute on every requested invocation. See the runnable scheduling fixture.

Set testSettings.maxRetries (0–9, default 0) to repeat the selected JVM scope with JUnit, Kotest or both engines after leaf test failures. Each retry discovers a new plan in the same owned JVM, reruns all selected classes and lifecycle callbacks, and preserves every attempt, including passing siblings and skips. Retries stop after a round without leaf failures or when the limit is reached. Container failures, incomplete discovery and infrastructure errors stop retries, as do changed test identities between attempts. The JVM and its static state remain shared across attempts. The action's timeout and resource budget cover all attempts; coverage includes execution from all attempts.

The verification policy remains strict: any failed attempt makes the run fail, even if a later attempt passes. Unique test counts use identities, while reports retain all attempts and initial failures. inspect-graph exposes the retry limit in test launch parameters; changing it does not invalidate compilation.

For host Native targets using the built-in kotlin.test runner, select test names with "nativeTestSettings": {"engine": "kotlin", "filter": "fixture.Checks.fast*"}. The pattern matches the qualified suite and test name, with * as a wildcard; ASCII letters, digits, underscores and dots are also accepted. Omit filter or set it to null to run the full declared test scope. Filtering is not supported for Kotest Native. The graph exposes the filter, and changing it reuses compilation and linking outputs while executing the selected tests afresh. An empty match returns incomplete. Counts describe the selected scope, excluding filtered-out tests. Run resolve after changing configuration.

Host Native tests support the same bounded retry policy through "nativeTestSettings": {"engine": "kotlin", "maxRetries": 2}. For Kotest, add "maxRetries": 2 to the existing pinned Kotest nativeTestSettings. Each Native attempt starts a fresh owned process and reruns the selected scope, including passing siblings and lifecycle callbacks. In-memory state starts fresh; the action's working directory and HOME remain available across attempts. Each attempt retains separate stdout/stderr logs. The action deadline and scheduler resource reservation cover discovery and all attempts. Cancellation, timeout, runner failures, incomplete reporting or changed test identities stop retries. Earlier failed attempts continue to make verification fail.

JS/Wasm Node tests accept "jsTestSettings": {"maxRetries": 2} with the same limit and strict failure policy. Every attempt starts a fresh Node process and reruns the selected scope; HOME and the working directory remain shared. Original reports, progress journals and stdout/stderr logs are retained separately for each attempt. The timeout covers all attempts, and changing the retry limit preserves compilation and linking cache entries.

JS and WasmJS

Targets js and wasmJs support library compilation and kotlin.test execution through Node. Declare org.jetbrains.kotlin:kotlin-stdlib:2.4.20 in main dependencies and org.jetbrains.kotlin:kotlin-test:2.4.20 in test dependencies. Resolution selects JS IR or WasmJS KLIBs from the published metadata; each target and usage has its own lock domain. Workspace library dependencies and common expect/target actual source sets use the ordinary project graph.

{"name":"wasmJs","main":"wasmMain","test":"wasmTest","compilerOptions":[],
 "jsTestSettings":{"filter":"example.Tests.*","timeoutMillis":60000}}

jsTestSettings is optional. Filters match compiler-discovered qualified test IDs, with * as the only wildcard. Zero matches or only skipped tests report incomplete verification; assertion failures report failure. Promise-returning tests are awaited. Application main() is not run by the test command.

Compilation, packaging, ABI checks and documentation use the JVM compiler/JDK tool configuration. Adding or changing Node does not invalidate these compilation results or their retained verification. To execute tests, add a node object to tools.json. Generate that object from an explicitly selected standalone Node installation with python3 tools/pin_node.py --root /absolute/node-installation. The command prints the physical root and content identity; Kvik verifies the entire installation before running tests. Node is never selected from PATH. The current JS/Wasm integration is tested with Node 24.21.0.

./kvik --no-build -- resolve --project /project/kvik.json
./kvik --no-build -- test --project /project/kvik.json --tools /project/tools.json \
  --module library --target js --target wasmJs

Compilation produces a packed KLIB; a separate link creates the test ES module and, for WasmJS, its Wasm files. Compilation and linking reuse the local cache; tests run fresh on every invocation. Test results use the same event and reporting commands as JVM/Native. Published serialization compiler plugins are supported; main-scoped plugins also apply when tests recompile production sources.

Workspace compiler plugins can select js and wasmJs targets. Kvik builds the plugin's JVM module and runtime dependencies before the consumer, then loads them in the owned compiler process. Declare id, compilerVersion, ordered options, inputs, pathOptions and outputs as for JVM/Native workspace plugins. Up to 16 workspace plugins can be combined with the pinned serialization plugin. Workspace plugins keep separate classpaths and are installed in declaration order. Main-scoped plugins also apply to tests because JS/Wasm tests recompile production sources. Plugin inputs and outputs affect compilation caching; source-free links consume the resulting KLIB without the plugin or its reports. The workspace plugin fixture exercises an IR transformation on both targets; other plugins must support the selected compiler backend.

JS/Wasm libraries can declare publication and use package-library --target js --target wasmJs to produce root KMP metadata and target KLIBs. Target coordinates use -js and -wasm-js suffixes. See library packaging for dependency semantics and scope selection.

JS/Wasm main KLIBs support abi-dump and abi-check with separate target baselines; these commands do not require a Node declaration or installation. See public API baselines.

docs --target js --target wasmJs generates API documentation with shared and platform source sets. See API documentation.

Targets js and wasmJs also support Node applications through prepare-application, run-application, run-prepared, BSP run requests and dev --mode application. Declare application.entryPoint as a public top-level Kotlin function returning Unit, with either no parameters or one Array<String> parameter. The entry does not need @JsExport and can have a name other than main. Overloaded and suspend entries are not supported. Kvik compiles a small entry bridge against the ordinary main KLIB, links it and packages an ES-module launcher; Wasm files stay beside it. Only the selected entry is invoked. Arguments are read at execution time, so changes to arguments or environment reuse compilation and linking. A pinned Node declaration is required for application preparation and its installation is verified before launch. Browser and WASI runtimes and npm dependencies are still pending. See the compiler adapter for its current boundaries and the pinned compiler workaround that suppresses Wasm application startup during tests.

Use verify to select affected module/target checks since retained Kvik runs. Checks default to test; repeat --check build|test|abi-check to choose the scope:

kvik verify --since /project/.kvik/runs/BASELINE_RUN \
  --project /project/kvik.json --module library --module app --target jvm \
  --tools /project/tools.json --plan-only
kvik verify --since /project/.kvik/runs/BASELINE_RUN \
  --project /project/kvik.json --module library --module app --target jvm \
  --tools /project/tools.json

--since takes a completed Kvik run directory. Changes are compared by content and executable bits across the declared input inventories, including new, deleted and renamed files. Git staging and commit state do not affect selection. Production changes propagate through module dependencies and workspace compiler plugins; test-only changes affect their own consumers. Configuration, dependency lock, runtime or desired tool changes widen to all requested checks. Unknown input ownership, damaged or older baselines without bound manifests also widen the selection. A check missing from the baseline's completed checks always runs.

verify also accepts --kode, --kode-home, --source-project and --revision. It resolves the revision once, then selects and executes checks against that immutable snapshot without a checkout. Source baselines can come from previous revisions of the same project and configuration root in the same provider store. Selection and result JSON include source; outputs are retained under the working directory's .kvik/verification. Exact cached revisions work with --offline. Omitted checks do not assess claims attached to the new revision.

Repeat --since to combine completed checks from runs bound to the same input snapshot, input selection, configuration, lock, runtime and compatible tools. Runs from different snapshots force all requested checks. Mixed verification runs capture ABI baselines for every group, so their execution runs can be combined on the next invocation:

kvik verify --full --check build --check test --check abi-check \
  --project /project/kvik.json --module library --target jvm --tools /project/tools.json
kvik verify --since BUILD_RUN --since TEST_RUN --since ABI_RUN \
  --check build --check test --check abi-check \
  --project /project/kvik.json --module library --target jvm --tools /project/tools.json

An ABI baseline edit selects its own ABI check. Test source edits do not select build or ABI checks unless that source is also a declared production input. Build and ABI scopes can select modules without tests; ABI checks require configured baseline files.

--full runs every requested check and can be used without a baseline. --plan-only captures inputs and prints the selection without starting compilers or tests. Ordinary test still runs every requested test; verify executes every selected test freshly, using normal compilation cache behavior. Its scope includes compilation and generator prerequisites. Build and ABI checks may reuse verified cache entries. Documentation and script checks retain their separate commands.

The JSON stream starts with kind: "verification-selection", follows with normal run events when executing, and ends with kind: "change-verification". Planning alone prints only the selection. Results distinguish selectedChecks, omittedChecks and successfully completed freshChecks, with references to the execution runs. For build and ABI, freshChecks means newly completed checks, including valid cache hits. baselineRuns lists all supplied baselines; baselineRun retains the first one for compatibility. unchanged means no checks were selected; it does not describe a fresh test run. Sources and desired tools are rechecked before and after execution; changes during verification produce an incomplete result.

Selection and result records are retained under .kvik/verification/ in the project. Execution records stay in the usual run directory. These records describe the selected scope and observed inputs; they do not establish that undeclared external services or test fixtures are unchanged.

Use the coverage commands to capture JVM line coverage, generate retained reports, check thresholds, aggregate compatible test checks and export SonarQube coverage XML or unchanged JaCoCo reports.

inspect, build and test can read an immutable project revision from Kode:

./kvik --no-build -- test --kode /absolute/bin/kode --kode-home /absolute/kode-store \
  --source-project library --revision main --project sub/kvik.json \
  --lock locks/dependencies.json --store /absolute/cache --runs /absolute/runs \
  --tools /absolute/tools.json --module library --target jvm

The reference resolves once; later edits to main do not change the run's inputs. --project is relative to the revision root, and --lock is relative to the configuration directory. Relative tool, cache and run paths use the invocation directory. Build/test do not write source revisions. Equivalent local and stored inputs share eligible compilation results; tests still run on each request.

Results and diagnostics include the immutable revision and project root. Use a full revision ID with --offline to rebuild from cached source content, locked dependencies and installed tools. Missing or corrupt source content causes a failure. verification --run PATH checks the recorded revision using cached source content without starting Kode; unavailable content yields indeterminate.

Online builds and tests acquire a Kode retention pin before fetching source inputs. The pin protects the selected revision and its reachable source objects through collection, and remains after completion so the recorded result can refer to those sources. run.json and the run-started event contain its sourceRetention identity. These operations require Kode retention support and permission to write retention metadata. Offline builds use only cached inputs; inspection, views and verification queries do not acquire pins.

Pins have no automatic expiry. A failed or interrupted build can leave a pin, including failures before a run directory exists; find it with retention list. Release a pin explicitly when its inputs no longer need provider-side retention:

/absolute/bin/kode --home /absolute/kode-store retention list library
/absolute/bin/kode --home /absolute/kode-store retention release library PIN_NAME \
  --expect-record IMMUTABLE_RETENTION_RECORD --intent 'Release obsolete build inputs'
/absolute/bin/kode --home /absolute/kode-store gc
/absolute/bin/kode --home /absolute/kode-store gc --prune --intent 'Collect abandoned source attempts'

gc previews storage and reference-root counts. gc --prune expires reflogs and collects objects unreachable from all remaining refs; it does not delete source heads, task records, request receipts or retention pins. Detached attempts without one of those roots can be collected. Maintenance takes an exclusive workspace lock while ordinary Kode operations take shared locks. Use Kode maintenance for this coordination; direct Git commands do not participate.

A recorded pin describes what was retained when the run started. It does not promise that somebody has kept the pin, local cache, dependency artifacts or compiler installations. Removing a pin does not remove an existing local run record, and cached input verification remains independent of provider retention.

Kode revision claims appear in the result's sourceClaims with their original JSON declarations and confirmed, refuted or unchecked assessments. Kvik understands {"kind":"check-passes","root":"sub","check":"library:jvm:test"} and {"kind":"test-passes","root":"sub","module":"library","target":"jvm","test":"EXACT_TEST_ID"}. root selects the configuration directory inside the revision; omitting it means the revision root. Claims for another project directory remain unchecked. Use a test ID from the structured test results. A failed attempt refutes the test claim, including a failed attempt followed by a successful retry. Skipped, missing, unrequested or incomplete checks remain unchecked. Unknown formats, including ABI and unaffected-module claims, are retained without interpretation. Assessments describe the recorded run and do not change its success or failure. This requires Kode's log --claim-values-as-text support; cached declarations are required for offline builds and queries of runs that include assessments.

resolve and check-lock accept the same source flags. Revision-backed resolve returns a proposal with changed and transaction; it populates the local dependency cache without publishing a revision. Pass --replace-project FILE to resolve an edited configuration and include both configuration and lock changes in one proposal. The file is read once and its exact bytes are embedded in the proposal. Existing executable modes are preserved. An unchanged result has changed: false and transaction: null. A valid replacement can also repair a malformed or invalid stored configuration.

Apply a changed proposal explicitly, choosing the destination reference:

./kvik --no-build -- resolve --kode /absolute/bin/kode --kode-home /absolute/kode-store \
  --source-project library --revision main --project sub/kvik.json \
  --lock locks/dependencies.json --store /absolute/cache \
  --replace-project /absolute/edited-kvik.json > proposal.json
jq '.transaction.operations' proposal.json > operations.json
/absolute/bin/kode --home /absolute/kode-store tx library \
  --base "$(jq -r '.transaction.base' proposal.json)" --ref main \
  --expect-head "$(jq -r '.transaction.expectHead' proposal.json)" \
  --intent "$(jq -r '.transaction.intent' proposal.json)" --ops operations.json

The proposal pins the base and expected head to the selected immutable revision, with native blob guards for each edited file. A concurrent head change causes a conflict; resolve again against the new head or apply to an explicit branch. check-lock validates the selected revision's lock and cached dependencies. As with local resolution, resolve --offline performs this check without proposing an update. Offline checks need a full revision ID and previously cached configuration, lock and dependencies; --replace-project requires online resolve.

inspect includes derived tags on source and resource files. Use view to export a revision-pinned projection for Kode:

./kvik --no-build -- view --kode /absolute/bin/kode --kode-home /absolute/kode-store \
  --source-project library --revision main --project sub/kvik.json \
  --lock locks/dependencies.json --role production > production-view.json
/absolute/bin/kode --home /absolute/kode-store tree library \
  --rev IMMUTABLE_REVISION --view production-view.json
/absolute/bin/kode --home /absolute/kode-store search library 'TODO' \
  --rev IMMUTABLE_REVISION --view production-view.json --max 20

Use the revision in source.provenance.revision from the exported view. Kode rejects a view from another project or revision. read also accepts --view and refuses files outside the projection. Search limits count visible matches; hidden matches do not consume the limit. An empty view stays empty.

Roles production and tests select the corresponding source/resource scope, plus configuration and the selected lock; production also includes ABI baselines. Role all includes the whole configuration directory. Repeat --tag module:NAME, --tag target:jvm or another tag to require all selected tags; --exclude-tag TAG removes matching files. Source-set inheritance determines scopes: main sources inherited by test source sets remain production files. Script, plugin-input, documentation and ABI-baseline tags follow explicit declarations. Tags do not infer generated status or declaration visibility from filenames; generated build outputs remain artifacts outside the source revision.

Add exact configuration-relative file paths to fileTags in kvik.json to layer manual classifications on top of derived tags:

"fileTags": {
  "docs/architecture.md": ["manual:architecture"],
  "schemas/v1/contracts.json": ["manual:contract"]
}

Use view --role all --tag manual:architecture with the source options above to select those files. inspect includes existing manually tagged files even outside source/resource directories, with empty ownership lists when no source set owns them. Paths are literal files, not globs or recursive directory declarations; absent files and directory entries are omitted. The metadata remains part of the selected revision and works with offline views. Manual tags must use the manual: namespace (up to 128 characters), with 1–64 distinct tags per path and at most 4096 paths. They cannot replace derived tags or add compiler inputs.

A view is a display projection. tx --view validates the base revision and still applies to the full tree. merge --view validates the target revision, merges full trees and reports every conflict, with hiddenConflicts identifying paths outside the projection. View filters are not edit permissions. Offline view export needs a cached immutable inventory and configuration; it does not need source-file bytes.

To restrict a CLI invocation's mutations, export a role policy using the same source options and pass it to Kode with --policy:

./kvik --no-build -- view --kode /absolute/bin/kode --kode-home /absolute/kode-store \
  --source-project library --revision main --project sub/kvik.json \
  --role tests --policy-name test-author --edit-tag scope:test \
  --allow task-create > test-author.json
/absolute/bin/kode --home /absolute/kode-store write library sub/test/NewTest.kt \
  --base IMMUTABLE_REVISION --content '// new test' --intent 'Add test' \
  --policy test-author.json

The policy combines a read view with independent edit rules and capabilities. --edit-tag requires every specified tag; --edit-any-tag requires at least one specified tag; --exclude-edit-tag rejects matching files. No edit selection means read-only. --allow-all-edits explicitly permits any file under the source root, subject to exclusions. New files use classification rules compiled from the pinned project configuration; renames must permit both paths. Changes to project configuration do not widen the current policy. Export a new policy for a new base.

Capabilities are explicit: task-create, task-update, ref-create, ref-update, ref-delete, project-add, and merge. A merge also needs ref-update, and all resulting changed paths must pass the edit rules before reference publication. Transactions still apply to the full revision, including files outside the read view. Denials identify the policy and rejected rule. Unsupported commands with --policy fail explicitly; --policy and --view cannot be combined.

The caller chooses and supplies this policy for each invocation. It is a guardrail for a trusted agent launcher, not an authentication mechanism or a filesystem sandbox. The file and CLI invocation must be controlled by that launcher for the restrictions to remain in force. Policies work offline from the same pinned inventory and configuration used by views.

For several source windows in one call, pass a request file to Kode:

{"schemaVersion":1,"reads":[
  {"path":"sub/src/Main.kt","startLine":40,"endLine":60,"before":3,"after":3},
  {"path":"sub/test/MainTest.kt","startLine":1,"endLine":80}
]}
/absolute/bin/kode --home /absolute/kode-store read-batch library \
  --rev IMMUTABLE_REVISION --reads reads.json
/absolute/bin/kode --home /absolute/kode-store tree-page library \
  --rev IMMUTABLE_REVISION --limit 100

Both commands accept --view FILE. Batch reads preserve request order and return each file's blob identity, requested range, actual context range and total lines. read and read-batch also return absolute startByte/endByte offsets for the returned content, so a later window can be edited without reading preceding lines. Add the UTF-8 byte offset within that window to startByte when preparing a patch. Ranged responses exclude the terminal LF and preserve CR bytes; a whole-file read preserves all bytes. Missing, hidden, binary or oversized files fail the whole request. Each file's next value is a ready-to-use request document for the next window; it includes the immutable revision. Defaults are 200 lines and no context. Limits are 128 windows, 1,000 requested lines per window, 100 context lines on either side, 8 MiB per file, 32 MiB of unique file bytes and 16 MiB of response JSON.

For an exact edit without sending the whole file, use a patch operation in kode tx --ops. For example, if the base file contains val x = 1\n, this operation changes only the digit:

{
  "schemaVersion": 1,
  "operations": [{
    "op": "patch",
    "path": "src/Value.kt",
    "expectBlob": "BLOB_ID_FROM_THE_BASE_REVISION",
    "hunks": [{"startByte": 8, "endByte": 9, "expected": "1", "replacement": "2"}]
  }]
}

Offsets are zero-based UTF-8 byte positions in the immutable base file, with an exclusive end. They must fall on character boundaries. expectBlob is required, and each hunk must match its exact expected bytes. No fuzzy matching or newline normalization occurs; untouched CRLF bytes and executable mode are preserved. Hunks must be ordered and disjoint, with unique starting positions. An empty range inserts text, and an empty replacement deletes it. A patched path cannot overlap another operation's path, including rename endpoints and parent/child paths. Source and resulting files are limited to 8 MiB; each patch accepts up to 256 hunks. A transaction allows at most 4,096 hunks and 8 MiB of combined expected/replacement text, within a 16 MiB operations document.

Submit the operations with the usual --base, --intent and optional guarded --ref main --expect-head BASE_REVISION. All file edits publish atomically. --request ID retains the existing retry contract: retry the same payload to recover its result; changing a hunk under that request ID conflicts.

tree-page returns up to --limit entries (default 200, maximum 1,000), total, complete and nextCursor. Pass the returned cursor string as --cursor for the next page. Cursors bind the project, revision, path prefix and view; a moving reference or changed filter is rejected. Use the returned immutable revision to continue the original snapshot. Tree pages reduce returned data; the current implementation still scans the full tree internally.

Search results and revision history also support bounded pages:

/absolute/bin/kode --home /absolute/kode-store search-page library needle \
  --rev IMMUTABLE_REVISION --fixed --view production-view.json --limit 100
/absolute/bin/kode --home /absolute/kode-store log-page library \
  --rev IMMUTABLE_REVISION --claim-values-as-text --limit 20

Pass nextCursor back as --cursor with the same revision and query options. Search cursors bind the pattern, matching flags, literal --path prefixes and view; history cursors bind the claims encoding. Page size may change between requests. complete indicates the last page, whose nextCursor is null.

search-page searches regular text blobs, with one hit per matching line. Hits include the path, blob identity, mode, line number and one-based UTF-8 byte column. Hidden matches do not consume a view's page limit. The default limit is 200 and the maximum is 1,000. Git output is streamed and stopped after detecting the next page; matching records above 8 MiB or responses above 16 MiB fail explicitly. The default --index off scans file contents directly. The tree inventory is still scanned, and resuming within a file rescans that blob.

Build a reusable text index explicitly, then use it to skip blobs that cannot contain a fixed, case-sensitive pattern:

/absolute/bin/kode --home /absolute/kode-store index library --rev main
/absolute/bin/kode --home /absolute/kode-store search-page library needle \
  --rev IMMUTABLE_REVISION --fixed --index prefer --view production-view.json

index resolves the selector once and publishes an index for that immutable revision. Unchanged content reuses the same blob summaries across revisions, including renames and duplicate files; builtBlobs and reusedBlobs report the work performed. Summaries filter candidates; exact Git searches still establish the returned matches. Index construction runs in the foreground.

Search responses expose requestedRevision, indexedRevision, indexStatus and indexComplete, separately from result pagination's complete. indexComplete means that index coverage was verified for the query's selected file scope, including its path and view filters. --index prefer falls back to exact scanning when the requested revision's index is missing or invalid, or the query is unsupported. --index required fails explicitly in those cases. Index filtering supports fixed, case-sensitive single-line patterns of at least three UTF-8 bytes. Regular expressions, case-insensitive searches and shorter or multiline patterns use exact scanning. An old revision's index never stands in for a newly advanced reference.

searchMode is indexed-exact when filtering is active and exact-scan otherwise. scannedBlobs and skippedBlobs count selected file entries visited on that page; view and role-policy filters still govern search results. A complete, empty result means that the requested search found no matches, even when it used a fallback scan. Index construction covers the full project and rejects --view and --policy; indexed searches accept both in the same way as ordinary searches.

Index files under the Kode home's search-index/v1 directory are disposable derived data. Version, identity and checksum checks detect damaged or misplaced cache entries; they assume trusted local cache writers. These files do not retain source revisions or replace retention pins. They can be removed while Kode is stopped and rebuilt with index.

log-page traverses the pinned revision's complete ancestry in topological order, including merge parents. Its default limit is 20 and maximum is 1,000; each commit's captured log data is limited to 2 MiB and the response to 16 MiB. --claim-values-as-text preserves claim values as JSON text for consumers that cannot represent every JSON number exactly. History is not filtered by --view.

Kode tasks group source experiments into a reviewable change:

/absolute/bin/kode --home /absolute/kode-store tasks create library fix-parser \
  --base BASE_REVISION --rev FIRST_CANDIDATE --intent 'Handle empty input'
/absolute/bin/kode --home /absolute/kode-store tasks update library fix-parser \
  --expect-record PREVIOUS_TASK_RECORD --rev SELECTED_CANDIDATE \
  --intent 'Handle empty input and preserve valid parsing'
/absolute/bin/kode --home /absolute/kode-store tasks review library fix-parser \
  --record TASK_RECORD --patch

The response distinguishes record (immutable task metadata) from revision (selected source). Use revision with Kvik's --revision or Kode's guarded merge/export commands. Review returns the final base-to-candidate changes and, with --patch, their patch. It includes the full source tree, including files outside a role view. The base remains fixed; selecting another branch experiment or returning to the base creates a new task record without rewriting source history.

tasks show --record ID opens a historical selection. Follow previousRecord to inspect earlier attempts, or use log-page --rev SOURCE_REVISION for their individual edits and intents. Task refs retain these metadata records and source snapshots through Git reachability. --result REFERENCE may be repeated to attach up to 32 opaque run/result references to the selected source revision; Kode does not validate their contents or retain external artifacts. Each update supplies its own references, while previous records keep theirs.

Updates require --expect-record; conflicting selections fail. Retrying the same create/update returns the existing record while it remains the latest task selection. After a later update, a stale retry conflicts and can be reconciled through historical records. tasks list library --limit 100 --after TASK_ID enumerates live task heads in name order; nextAfter continues that listing, which is not an immutable snapshot. Task IDs are single lowercase name segments. The implementation scans the ref inventory with an 8 MiB bound. Review limits captured change data and optional patch data to 8 MiB each and response JSON to 16 MiB; oversized reviews fail explicitly. Task grouping preserves detailed history and does not squash it into a new source commit.

Kode can propose a text-first identifier rename in .kt and .kts sources:

/absolute/bin/kode --home /absolute/kode-store rename-identifier library \
  oldName newName --base IMMUTABLE_REVISION > rename.json

The response contains transaction.base, transaction.expectHead, an intent and transaction.operations in the same format accepted by kode tx --ops FILE. Save that operations object to a file and apply it with tx --base and --intent, omitting --ref to create a detached candidate. Run Kvik test with the candidate's immutable revision and merge it into the desired reference only after checking the result. The proposal itself never publishes changes; transaction is null when no identifiers match.

Repeat --path PREFIX to restrict the proposal to selected files or directories. Renames preserve comments, quoted text and executable bits. Affected backtick identifiers and string interpolation require an explicit edit. This operation rewrites identifier tokens without resolving symbols: a new name can collide with another declaration, and limited path selection can leave references behind. Compilation and tests on the complete candidate are required to check it.

To measure source-workflow overhead, prepare the collaboration fixture with python3 tools/test_kode_workflow_cli.py prepare --runtime /absolute/kvik.kexe --kode /absolute/kode --tools /absolute/tools.json, then pass its printed fixture path to:

python3 tools/compare_source_workflows.py --fixture /absolute/fixture.json --trials 4

The comparison replays the same two disjoint edits, independent tests, merge, integrated tests and repeated verification through Kode revisions and Git worktrees. Each trial gets fresh repositories and an independent Kvik cache with identical blob seeds and no action entries. It warms the baseline separately and alternates workflow order. The supplied workspace and cache remain unchanged. An optional --runtime PATH selects the same newer runtime for both workflows. The command rejects differing action keys, reuse decisions, search hits or test identities, and requires fresh tests even on the repeated build.

Results stay under .kvik/tests/source-comparison/: comparison.json records each trial, tool fingerprints and controls; individual result.json files retain operation times, process counts, stdin/file-write and output bytes, UTF-8 argument sizes and logical repository sizes. This is a scripted infrastructure comparison. It does not measure LLM tokens, agent tool calls, conflict-resolution effort or incorrect completion claims; those K17 stage 5 questions use the separate controlled agent trials below. Materialization and search timings are reported separately, without attributing query time to index construction. Filesystem allocation and OS page caches are not controlled.

To compare real agents using Kode revisions versus Git worktrees, reuse the same prepared collaboration fixture and explicitly enable model-provider calls:

python3 tools/compare_workspace_agents.py \
  --fixture /absolute/fixture.json --trials 2 --allow-model-calls \
  --model gpt-6-sol --effort high --timeout 900

Each paired scenario uses two authors starting from the same source tree and a separate integration agent. The three scenarios cover disjoint edits with a concurrent integration-branch update, conflicting name normalization changes, and individually passing batch/worker changes that can fail after integration. The harness observes the actual merge condition before integration; a task that never produces the intended conflict or failure is marked unexercised. Two repeats require up to 36 model sessions. Sessions and builds run serially; this experiment does not measure simultaneous-agent throughput.

Both backends use the same runtime, model, effort, deadline, dependency blobs, one build job and memory limit. Each gets a private action cache warmed once on the baseline. Independent grading uses another cache, freezes the submitted revision, preserves baseline test identities, checks the agent's own fresh test record, and executes additional behavior assertions. Failed attempts and false completion claims remain in the results. A pair is eligible for comparison only when its baseline and semantic tasks match and both scenarios were exercised.

Results under .kvik/tests/workspace-agent-comparison/ retain prompts, final answers, transcripts, tool fingerprints, verification and per-role measurements. They distinguish agent time from independent verification, returned command bytes from provider token usage, successful/failed compilation actions from process counts, and repository storage from materialized bytes. Integration-agent costs include merge and repair effort. Per-role command journals also record observed logical provider-store growth at each edit boundary. Git intervals include staging costs before a commit; Kode transactions and merges are recorded separately. Failed/no-op commands and trailing costs remain visible. Missing or malformed records, overlapping commands and inventory failures mark attribution incomplete. This is net logical file growth, not exact causal allocation or filesystem-wide storage; transient bypasses remain a limitation. Cumulative observation durations are reported but can overlap, so they cannot be subtracted from elapsed time. Instrumented agent timings are separate from the historical trial below. Explicit Kode index construction is measured after the trial, with first and second calls reported separately; the first call is not assumed cold. Model calls are never part of automatic CI; CI runs only the grading and accounting regression tests.

The 2026-10-26 macOS ARM64 run completed two repetitions of each scenario: six paired workflows and 36 agent roles using gpt-6-sol at high effort. All pairs met the recorded comparison controls, all submissions passed independent verification, and the grader found no false completion claims. Every conflict and integration-budget probe exercised its intended failure before repair.

Aggregate for 18 roles per backend Git worktrees Kode
Agent process time 913.854 s 1,150.148 s
Independent grading time 160.776 s 240.199 s
Tool calls 212 254
Returned command-output bytes 501,823 643,811
Output tokens 41,482 56,848
Successful compilation actions 40 40
Redundant / failed compilation actions 0 / 0 0 / 0
Summed role repository-storage increases 32,281 B 39,458 B
Snapshot materialization time 0.332 s 0.851 s

Kode took more agent time in all six pairs: 25.9% more in aggregate, with 19.8% more tool calls. Integration-agent time alone was 407.150 s for Git and 438.031 s for Kode. The six explicit Kode index measurements built 12 blobs on their first call and reused all 12 on their second; median latency was 125.51/31.10 ms, outside agent timing. This does not compare Git search latency. These historical storage deltas cover whole roles; the later per-edit instrumentation does not retroactively attribute their individual edits.

These results demonstrate the bounded revision/edit/test/merge workflow, but do not demonstrate better reliability or lower cost than Git. Keep Kode as an experiment and defer IDE overlays, composed projects and remote-execution expansion until a broader comparison justifies them. Prioritize command batching and smaller responses before another economy trial. The fixture is small, execution is serial, generated tests differ, and provisioned tools and OS caches are shared.

One integration agent used a temporary transaction file outside its prescribed scratch directory. Its committed project scope and verified result passed the rubric; the grader does not enforce every filesystem instruction, and repository storage omits that scratch file. Excluding that whole pair still leaves Kode with more agent time and tool calls. The retained run is .kvik/tests/workspace-agent-comparison/case-n9gmbbwq/comparison.json (SHA-256 af37fd7efa7e54402eec1343968fd58ef9550a0309e4c4b961c5320705fa2cbb).

A subsequent instrumented disjoint-change pair completed six independently verified agent roles with complete storage reconciliation. It measures two author edits and two integration merge transitions per backend:

Observed net logical store growth Git Kode
Author edit intervals 2,709 B 2,025 B
Integration merge intervals 1,673 B 930 B
Trailing provider operations 0 B 2,346 B
Total across the three roles 4,382 B 5,301 B

The Kode journal includes provider calls made by builds, including retention. All role totals equal their edit/merge intervals plus trailing costs. The run retains 38 Git and 82 Kode provider commands, including unsuccessful optional Git configuration probes. Cumulative observation durations are 3.52/7.48 s; these include concurrent observations and are not subtractable wall-time overhead. This single pair validates the accounting on assigned tasks, not a general storage advantage. Historical results remain unchanged. The preceding diagnostic trial also verified all six agent results, but correctly retained incomplete storage accounting; it is excluded from this table.

For real agent workflow trials against KTC 0.13.0, prepare the shared two-module JVM fixture and explicitly opt in to model calls:

python3 tools/compare_agent_workflows.py prepare \
  --ktc-wrapper .kvik/fixtures/kotaml/kotlin \
  --runtime /absolute/kvik.kexe --tools /absolute/tools.json
python3 tools/compare_agent_workflows.py run \
  --fixture /absolute/printed/fixture.json --trials 2 \
  --writable-cache "$HOME/Library/Caches/JetBrains/Kotlin" \
  --writable-cache "$HOME/.konan"

run invokes Codex with the same explicit model and effort for both backends (gpt-6-sol, high by default), an isolated workspace and a 15-minute deadline per task. It covers a compilation error, a failing boundary test, file ownership, stale verification and concurrent checks in independent Git worktrees. Repair tasks start from the same broken commit after warming an identical passing baseline, with Kotlin 2.4.20, JDK 25, JVM release 17, JUnit 5.13.4 and matched Kotlin test dependencies. The generated KTC wrapper fixes the JDK path across login shells; its distribution pin remains unchanged. Each trial uses fresh project caches and provisioned shared tool/download caches. Trial order alternates. Use --tasks file-ownership stale-verification to select just the query tasks.

Query tasks prohibit builds, test execution and source edits. Both receive the same changed source and passing baseline tied to a Git commit. The freshness case changes content while preserving file size and mtime. Ownership answers are checked against the prepared module/target declarations; stale answers are checked against changed inputs and a separate fresh test run that refutes the old passing result. Kvik's native freshness response is retained separately from that shared oracle. Executing either build system during a query invalidates the trial even if all compilation was cached.

Use --tasks independent-worktrees for the concurrent scenario. Two linked worktrees start with warm compilation and different source contents: the left suite should pass and the right should fail. The agent must preserve both and report both results. Test JVMs record their own execution windows and observed branch behavior; the grader requires overlapping windows, fresh results and unchanged sources and Git state. Separate clean checkouts independently rerun each branch. Kvik shares an action/blob cache between the worktrees; KTC shares provisioned tools/downloads and keeps project-local incremental state. This scenario uses a two-second test wait to make overlap observable. Identical-input worktrees and cancellation isolation are checked separately by the public CLI suite.

The harness retains prompts, JSONL transcripts, token usage, tool calls, returned command-output bytes, compiler invocation counts, diffs and independent test results under .kvik/tests/agent-comparison/. It validates all three test identities and tests candidate source in a separate clean checkout. Completion claims for repairs require observed passing agent tests as well as independent verification; staging, commits, branch changes and out-of-scope file edits are rejected. KTC compiler counts include BTA entries that can become incremental no-ops; Kvik counts observed compiler child processes. These measure invocation overhead, not equivalent amounts of compiler work. Returned log text can be truncated by Codex; full transcript bytes are a separate metric. macOS KTC process cleanup may need automatic sandbox escalation, whose time is included in the trial.

This small JVM experiment does not cover the Kode/Git agent comparison from K17. CI runs only the harness accounting/failure tests; model calls are manual and incur provider usage.

The completed comparison contains two repeats for each of the five tasks and both backends: 20 final agent runs, all independently verified, with no graded false completion claims. Mean time to the independently verified result is:

Task (two runs per backend) KTC 0.13.0 Kvik
Compilation-error repair 29.06 s 31.09 s
Failing-test repair 33.74 s 31.26 s
File ownership 25.52 s 15.24 s
Stale verification 26.53 s 23.08 s
Independent worktrees 59.07 s 54.34 s

Across ten runs per backend, verified-result time totals 347.844 s for KTC and 310.039 s for Kvik; tool calls total 110 and 83. Returned command-output bytes total 148,019 and 271,860 respectively. Observed compiler invocations total 11 and 1, with the different counting semantics described above. Both backends passed all final trials, so this sample does not establish a reliability advantage. The higher Kvik output volume is a concrete interface cost to reduce.

The retained final set under .kvik/tests/agent-comparison/ is case-nys6pb55/trials-lzx0alky (repairs), case-rx23w4z2/trials-pneety38 (stale-verification records only), case-rx23w4z2/trials-lhle_gmn (ownership) and case-rx23w4z2/trials-n1zns5mj (worktrees). Original ownership trials rejected by an overly strict source-set description grader are superseded by the replacement set; an earlier repair pair and a worktree pair interrupted by a harness setup error are excluded. Their raw results remain retained. Two repeats per task, a small JVM fixture, shared provisioned tools, and local approval overhead limit generalization; these totals combine distinct tasks rather than a throughput test.

The K11 decision is to continue the bounded internal prototype while keeping existing builds available. Source-built Kotlin/Native compositions, owned foreground lifecycle, fresh tests after compilation reuse and two-runner JVM remote caching have working acceptance paths. The scoped completion audit finds coverage for all eight runtime/lifecycle, five build/dependency, nine cache and six agent-interface functional gates. This covers the pinned fixtures and supported policies, including explicit Native local-only restrictions. The measured source bootstrap takes 591.215 s on the named Mac, including 445.447 s of archive provisioning; offline reconstruction and helper recomposition still take 97.352/107.663 s. This is a substantial plugin-development cost. After deterministic worker packaging and deferred bootstrap imports, separate 30-sample cached-launch checks report help/inspection p95 of 84.26/89.84 ms on the Mac and 79.75/87.94 ms in Linux CI, meeting the 150 ms budget on both measured hosts. These are not controlled speedup comparisons; the Mac sample retains one 263.70 ms inspection outlier. A later isolated plugin edit takes 142.24 s, of which 101.15 s is the final Native compiler command despite verified JVM worker reuse. A separate private compiler experiment extracts 194 stable source files into a core KLIB (14.25 s). Plain final compilation takes 86.92 s; first object-cache population plus final compilation takes 162.64 s, including standard/platform caches. A subsequent plugin-description edit links in 16.52 s with unchanged core/cache bytes and zero rebuilt cache archives. Runtime identity and project inspection pass for all three executables. The warm command excludes 30.14 s of installed-tool verification, 10.42 s of interop preparation and other launcher work; these single samples establish feasibility, not an end-to-end budget result. The experiment uses private outputs and does not publish an ordinary runtime cache entry. The integrated macOS ARM64 cache subsequently takes 223.94 s for initial population and 45.80 s after a plugin-description edit, measured around launcher.main with provisioned tools; Python process startup is excluded. The warm final compiler command takes 17.39 s, reusing the core, object seed and JVM worker with zero rebuilt cache archives. Both executables pass runtime identity and project inspection. A separate 30-sample cached-startup check of this composition reports launcher help/runtime-info p95 of 126.20/138.11 ms and direct Native peak RSS of 7.03/10.16 MiB. These observations cover one Mac composition and do not establish a general recomposition budget or Linux benefit. Cancellation during the live final compiler leaves no published runtime and preserves shared cache contents. An ordinary retry succeeds in 47.62 s including Python process startup, reusing the core and object seed. The composed executable also passes fresh JVM/Native consumer tests and foreground test/quit cleanup. The full HTTPS source composition now exercises the same cache through a thin launcher, two source plugins and a shared source helper. With provisioned tools and JVM artifact downloads, but fresh source/runtime/core/object/worker caches, one macOS ARM64 run takes 183.033 s initially, 40.124 s after a plugin edit and 37.997 s after a helper edit, including Python process startup. Both edits reuse identical core, object and worker caches with zero rebuilt archives; runtime construction takes 18.298/18.031 s and installed-tool verification takes 20.750/18.873 s. Both plugin outputs reflect the intended source changes. Cached original-composition launches take 0.107 s before the edits and 0.117 s afterward. These are single observations using loopback HTTPS, not a tool-download benchmark or a controlled comparison with the earlier compositions. Native outputs remain local-only where relocation is unsafe. The pinned Grogu fixture is now available through the separate kvik-fixture-4a10a089 tag; its Linux compiler-profile gate passes repository acquisition and JVM acceptance against a fresh KTC reference.

The next bounded work is to broaden Native core/object-cache measurements and evaluate compact output in larger agent workflows. K17 remains an experiment: its separate comparison did not demonstrate an economy or reliability advantage. These results do not justify replacing KTC generally or claiming broad KMP compatibility. Total maintenance cost versus extending KTC has not been measured; structured inspection and freshness contracts remain candidates for reuse there. The prototype assessment does not close the separate extension work, including broader K17 comparisons or the performance follow-ups tracked in implementation status.

The runtime/foreground functional audit now covers all eight mandatory cases. The final added check runs the real cached launcher offline with absent compiler paths, edits consumer source and configuration separately, and verifies the same runtime is reused while inspection observes the changed compiler setting and its origin. Run python3 tests/bootstrap/consumer_reuse.py --launcher-root CHECKOUT --cache-dir CACHE against a provisioned runtime; Linux CI includes this check.

Compiler plugins can be built from another module in the same project. Declare its module, plugin ID, compiler version and selected targets/scopes on the consumer; see workspace compiler plugins.

For a runnable target, declare application with an entryPoint, optional ordered arguments and an environment map. JVM entry points name the main class, such as example.MainKt; Native and Node-backed JS/Wasm entry points name the Kotlin function, such as example.main. Select exactly one module and target:

./kvik --offline --no-build -- run-application --project /absolute/project/kvik.json \
  --tools /absolute/project/tools.json --module app --target jvm

prepare-application accepts the same options and only prepares the bundle. run-application prepares it and starts a fresh owned process. Preparation may reuse cached compilation and packaging; application execution always runs. To replay a preparation, pass its application-prepared event's directory and descriptor to run-prepared --run PATH --descriptor DIGEST_JSON. Replay checks the bundle, completed evidence and current sources/tools before starting. The application uses the project directory, declared environment and EOF stdin; JSON events identify its retained stdout/stderr files. Its exit code propagates, and interruption stops its owned process group. See the application contract and JVM/Native acceptance evidence.

For continuous application work, select application mode explicitly:

./kvik --offline -- dev --mode application --project /absolute/project/kvik.json \
  --tools /absolute/project/tools.json --module app --target jvm

In a terminal, r requests reload, t runs fresh tests, q exits and ? shows help. File watching also requests reloads. A failed preparation keeps the previous application running; status identifies when it belongs to older inputs. A ready replacement starts only after the previous owned process tree has stopped. Test requests are retained independently of reload requests. --initial test requests tests before the first application preparation. Without --mode application, dev uses library mode.

Agents and other non-TTY callers must pass --control jsonl (versioned controls and events) or --control lines. Status includes the active application identity, candidate state and queued intents; closing input stops the session's children. Source changes to Kvik or its build plugins trigger a new native composition and an explicit session handoff. See the foreground controller and acceptance status; the full public JVM/Native replacement and compiled-source handoff gate is still being validated.

Dependencies default to private compile dependencies. Set exported: true when a dependency belongs on consuming modules' compile classpaths; use scope: "runtime" for runtime-only dependencies. Runtime closures include private dependencies. Target testCompilerOptions optionally replaces compilerOptions for test compilation, so production-only settings such as strict explicit API need not apply to tests. Omission preserves inheritance; an empty list explicitly selects default test options.

SQLDelight 2.3.2 can generate a synchronous SQLite 3.38 database for a main source set shared by JVM, Native, JS and Wasm targets. Add this optional module property:

"sqlDelight": {
  "sourceSet": "commonMain",
  "sourceDirectory": "src/commonMain/sqldelight",
  "packageName": "example.database",
  "className": "Database"
}

Place .sq files under package directories inside sourceDirectory, and declare app.cash.sqldelight:runtime:2.3.2 as a normal main dependency. For JVM SQLite execution, declare app.cash.sqldelight:sqlite-driver:2.3.2 in the consuming main or test source set. Run resolve after adding or changing the generator configuration. For the pinned Kotlin 2.4.20 toolchain, pass --pin org.jetbrains.kotlin:kotlin-stdlib:2.4.20 consistently to resolve and subsequent build/test commands to reconcile the compiler libraries’ stdlib requirements. The compiler tools get their own JVM-hosted sqldelight-compiler lock domain, even when the project has no JVM target; they do not enter the application's classpath.

Builds compile the embedded worker once into the shared cache, generate Kotlin in a separate owned JVM, and compile the generated sources alongside ordinary main sources. One generation action supplies every target consuming the configured source set. A module may contain only generated main Kotlin; generated common declarations also enter root publication metadata. SQL edits and database settings invalidate generation; unchanged worker compilation and unrelated modules retain their cached results. SQL inputs participate in snapshots, file ownership queries and affected-check selection. Documentation generation includes the generated API. The generator runs without a daemon and supports foreground cancellation. Both preparation actions currently use the local action cache.

This adapter supports one database per module, attached to a main source set. Tests inherit the generated production API. A test-only attachment is rejected. Migrations (.sqm), schema database inputs (.db), asynchronous generation, and the upstream optimistic-lock annotator are not supported. BSP source queries expose generated Kotlin after compilation, including generation restored from cache. See the runnable SQLite fixture, JVM CLI test, and multiplatform CLI test. Third-party license and attribution are in licenses/SQLDelight-NOTICE.txt.

Native targets can set nativeTimeouts, for example {"linkMillis": 600000} for a large test executable. Optional compileMillis and linkMillis values are integer milliseconds from 1 to 3,600,000; omitted phases retain their 180,000 ms limits. These settings affect compiler actions, while KSP and test execution keep their separate budgets. A compiler timeout reports KVIK_NATIVE_TIMEOUT with the phase and configured limit; partial outputs are not cached. See Native timeout settings and checks.

Native compilerOptions also accept -Xreturn-value-checker=check|full and -Xname-based-destructuring=only-syntax|name-mismatch|complete (choose one value per option). They apply to source compilation, participate in cache identity and follow the ordinary testCompilerOptions inheritance rules. JVM, shared metadata and KSP adapters currently reject these settings explicitly.

On macOS ARM64, iosArm64 and iosSimulatorArm64 library targets can compile KLIBs and link frameworks. Add "nativeFramework": {"baseName": "Sample", "isStatic": false} to a target with "test": null, then run build --module library --target iosSimulatorArm64 with the usual project and tools options. The framework-ready event returns the materialized framework path and, for a dynamic framework, its dSYM path. isStatic defaults to false. Framework linking includes runtime dependencies; changing the framework name or static setting reuses unchanged KLIBs. package-library continues to publish KLIBs.

Pin the consumer SDKs explicitly with python3 bootstrap/pin_tools.py --java-home "$JDK" --konan-home "$KONAN" --apple-target iosArm64 --apple-target iosSimulatorArm64 --output /absolute/project/ios-toolchains.lock.json. Put that lock in the consumer tools.json toolLock field and add matching iosSdk and iosSimulatorSdk roots from xcrun --sdk iphoneos --show-sdk-path and xcrun --sdk iphonesimulator --show-sdk-path. The SDKs must belong to the pinned Xcode installation. Fresh compilation verifies the selected SDK; cached frameworks can be restored without installed compiler tools. This support covers compilation and linking: framework resources, dependency API exports, XCFramework assembly, iOS test execution and app deployment are not yet supported.

The iOS workflow builds the native runtime on macOS ARM64 and checks device/simulator frameworks, Swift imports, static linking and cache reuse. Its local entry point is python3 tools/macos/verify.py --java-home "$JDK" --konan-home "$KONAN" --konan-data "$HOME/.konan". Use Python 3.13 or newer. In a disposable checkout, add --pin-tools to provision Native dependencies and replace that checkout's bootstrap tool lock for the selected Xcode installation. The consumer lock includes both iOS SDKs and stays separate from the host-only bootstrap lock. Provisioning only removes downloaded archives from a Native data directory created by that invocation.

Dependency locks use schema version 2 and resolve each module, target, compilation and usage separately. Existing unscoped locks require an explicit resolve. Repeatable --scope-pin MODULE:TARGET:COMPILATION:USAGE:PURPOSE[:PLUGIN_ID]=GROUP:ARTIFACT:VERSION overrides a global pin only in that domain; for example, --scope-pin library:jvm:test:java-api:dependencies=org.jetbrains:annotations:23.0.0. Explicit compiler plugins have independent domains: append the plugin ID when pinning one plugin's dependencies. Supply it consistently to resolve, check-lock, inspect, build and test; runtime usage is a separate domain. Unknown domains and duplicate component pins are errors. Native test source compilation and executable linking use separate API and runtime closures; private link dependencies remain unavailable to source compilation. Source-free links reject missing used symbols. See Native visibility evidence and portability limits.

The source bootstrap uses Python with os.waitid/WNOWAIT support (tested with Python 3.15). A cache hit verifies and executes the native binary without starting a JVM. A cache miss verifies the compiler, JDK, native dependencies and platform tools against bootstrap/toolchains.lock.json. That lock currently describes the development macOS ARM64 host, so another host must be explicitly provisioned and repinned with bootstrap/pin_tools.py before building.

Pass --tool-archives /absolute/tool-archives.json to acquire pinned JDK, Kotlin/Native, LLVM, libffi or Linux native-toolchain distributions during a rebuild. The optional --tool-cache PATH defaults to .kvik/tool-distributions; keep it separate from sources, installed tools and other bootstrap caches. Components listed in the archive lock replace their installed roots; omitted components still require installed tools. Xcode/SDKs and the Linux system libraries remain host prerequisites. --java-home and --konan-home can be omitted when their components are supplied by archives. Foreground recomposition preserves this acquisition policy.

The tool archive schema requires schemaVersion: 1, the exact host from bootstrap/toolchains.lock.json, and a nonempty components object. Each component (jdk, kotlinNative, llvm, libffi, or Linux nativeToolchain) specifies an HTTPS url, archive digest (hash and sizeBytes), format (tar.gz or zip), rootPrefix, directory, and tree. Copy tree from the matching toolchain-lock component: it identifies the extracted installation inventory, not the archive bytes. For a macOS JDK bundle, for example, use rootPrefix: "jdk-25.jdk" and directory: "Contents/Home". Use an empty prefix for an archive without a wrapper and directory: "." to select the whole extracted root. Archive member names must be normalized relative paths; internal symlinks and TAR hardlinks are supported. Archive URLs are explicit acquisition policy and do not alter the runtime composition identity.

Tool acquisition verifies both the archive digest and extracted installation before use, coordinates concurrent launchers, and repairs damaged extracted trees from cached archives. --offline permits this repair but forbids downloads; --no-build never acquires tools. --print-composition validates the archive lock without creating its cache, and a verified runtime cache hit needs no tool downloads or installed compilers. Native builds using archives get private mutable compiler state with links to the verified dependency trees.

Archive URLs reject redirects by default. For upstream release services that redirect to a CDN, declare redirectOrigins, for example ["https://release-assets.githubusercontent.com"]. Every hop must target an explicitly listed HTTPS origin, with at most three hops; credentials and ambient proxies are excluded. Keep the stable release URL in url, not its temporary signed destination. transferTimeoutSeconds optionally bounds the whole archive download (1–3600 seconds, default 900); source-package transfers keep their separate 120-second bound and reject redirects. Neither setting relaxes archive size or SHA-256 verification.

For a known incorrect published dependency edge, project commands accept repeatable --exclude-metadata-edge JSON. Each rule identifies the exact consumer domain, parent coordinate, metadata digest, selected variant, declared dependency and reason. Rules are part of the configuration and lock identity: pass them consistently to resolve, check-lock, inspect, build, test, dev and dependencies. The project dependency query shows corrected edges and their reasons. Raw packages dependencies queries inspect published metadata and do not apply project rules. See the metadata correction contract.

Bootstrap compiler commands have a 300-second deadline by default. On slower hosts, pass --bootstrap-command-timeout 1800 before -- (integer seconds, 1–3600). Foreground recomposition preserves this setting and scales its total rebuild budget proportionally above the default 15 minutes. The setting does not change artifact identities or consumer-action deadlines.

Successful builds retain the same buildTimings in manifest.json and the runtime-built event. Ordered, nonoverlapping nanosecond phases cover tool root resolution, optional tool archive provisioning, installed tool inventory verification, optional source graph acquisition, JVM artifact acquisition, and runtime construction. Acquisition includes verified cache reuse. Construction includes source staging, C helpers, interop libraries, the JVM worker, the Native executable and staged output digests; compileMillis (and the event's milliseconds) rounds that phase to milliseconds. The total ends before cleanup, publication verification/sync and runtime invocation, and excludes earlier preflight and runtime cache inspection. Timings do not enter composition or action identities; cache-hit events do not replay build measurements.

tools/test_source_bootstrap.py --host-name "HOST MODEL / MEMORY" ... retains these phase boundaries alongside end-to-end invocation times, initial source/artifact cache state, compiler locks and exact recipe-selected sources. Its default uses installed JDK/Native/host tools, so it does not measure distribution downloads. An empty --artifact-cache exercises first-use JVM JAR acquisition. Add --tool-archives PATH to measure distribution acquisition from an empty tool cache; the gate supplies absent installed paths for those components and verifies every cached archive and extracted tree after the cold build and subsequent reuse. --tool-cache PATH must be absent or empty; by default it is created inside the fresh attempt directory. Cached and relocated invocations use absent tool caches, while offline reconstruction and helper recomposition reuse the original populated cache and JVM worker bundle. Use --invocation-timeout 1800 for slow downloads; this bounds the whole invocation without changing individual compiler deadlines. Partial results survive timeouts.

The macOS ARM64 archive fixture pins upstream Liberica JDK 25.0.4+9, Kotlin/Native 2.4.20, LLVM and libffi archives (684,845,327 bytes in total). It requires the recorded host identity and installed Xcode/SDK. Run it with a disposable --producer-root whose toolchain lock explicitly uses the fixture's component tree pins, and a clean --source-root. The checked-in installed-tool lock has different JDK and Native trees: SDKMAN flattened JDK legal symlinks, and the installed Native distribution contains generated caches. These are distinct tool identities; changing acquisition URLs does not reconcile them. The gate validates both locks before downloading and never repins them implicitly.

Archive provisioning includes download, extraction and verification; it does not isolate network time. Each mode is one observation of the full source composition, not the K01 small-spike budget. Every invocation retains raw wait4 CPU/RSS usage and explicit host units, plus JSONL samples of live descendants' RSS and process counts. wait4 describes the reaped invocation owner with OS-dependent child accounting, not exact whole-build CPU or aggregate tree peak RSS. Sampling can miss short-lived or detached/reparented processes and peaks; summed RSS double-counts shared pages and observation adds wall time. Cold, offline-rebuild and helper-mutation phases require valid OS usage and at least one live sample. Fast probes may explicitly report no live sample. Timeout and sampling failures retain partial measurements and command output before failing the gate; cleanup targets the owned process group.

Measure a provisioned runtime with python3 tests/bootstrap/test_launcher.py --quick --host-name "HOST MODEL / MEMORY". For a source composition, also supply --launcher-root, --cache-dir, --source-lock and --source-cache matching that runtime. The command runs offline with builds disabled and deliberately absent compiler/artifact locations. It retains 30 interleaved samples per help/runtime-info operation through the launcher and direct Native executable, plus 30 separate direct Native child RSS samples per operation. --samples accepts 20–300; p95 uses nearest rank and discards no warmup. RSS is the per-child wait4 high-water mark with explicit host units; resource polling is excluded from timing samples. The harness binds exact launcher, runtime, lock and Python identities and saves raw output and partial results in a fresh .kvik/tests/bootstrap/cached-startup-* directory.

The budgets are launcher p95 below 150 ms and direct Native peak RSS below 64 MiB. Functional success and budget results are separate; --strict-budgets exits with code 3 on a budget miss. Linux CI retains these measurements without making shared runner timing variance a functional failure. These cached help/inspection samples do not measure provisioning, builds, reloads or the absence of short-lived processes. OS page/file caches are uncontrolled. See foreground measurements for command acknowledgement, reload, cleanup and sampled process resources.

Add --profile-launcher to retain a separate Python cProfile of cached --print-runtime after all timing and RSS samples. launcher.prof and launcher-profile.json identify bootstrap imports and verification costs; this instrumented invocation excludes Native execution and never enters latency percentiles. Linux CI includes both files in its cached-startup artifact.

The bootstrap also compiles the JVM compiler/test workers from source and bundles their classes, three runtime libraries and the JaCoCo agent/reporter alongside the native executable. Compiler/library artifacts are checksum-pinned in bootstrap/jvm-artifacts.lock.json; missing jars are acquired from their locked URLs unless --offline is set. Use --artifact-cache PATH to select the artifact directory (default .kvik/tool-artifacts). Cached startup needs neither compiler installation nor the separate compiler-artifact directory. JVM workers are invoked only for tool operations; Kvik and its source build plugins run as Kotlin/Native.

Worker bundle v2 stores the implementation in a deterministic, uncompressed worker.jar. The manifest and worker identity bind the archive's complete bytes; both the Python launcher and Native runtime hash them before use. Cached checking does not parse or extract ZIP entries. Construction validates the class inventory, normalizes archive timestamps and modes, and orders entry names by UTF-16. Libraries and compiler manifests retain their separate integrity checks.

The complete archive must fit the existing 64 MiB CAS blob limit. Larger valid worker outputs use the legacy v1 class-directory representation at construction, preserving its existing capacity and complete tree verification. An invalid v2 bundle never falls back to a class directory. The format changes the worker and bootstrap cache identities; the first invocation rebuilds the affected composition.

Native recomposition reuses an unchanged JVM worker bundle from <cache-dir>/jvm-worker-cache. Its key includes ordered worker source paths and bytes, the complete artifact lock, verified JDK identity, compiler arguments, bootstrap worker implementation and effective JVM environment. Native-only plugin sources and SDK/Native environment settings do not enter that key. Changes to the working directory, Java executable location or retained environment values conservatively invalidate it; private source snapshots, output directories and artifact-cache locations use logical paths. Bundle reuse verifies every cached class and library and checks the compiler/documentation manifests against the lock. Concurrent builders coordinate publication through the local K04 store.

Each constructed runtime retains jvm-worker-cache.json, bound by its manifest, with the worker input key, inputs, bundle digest and compiled or cache-hit status. Source-composition command receipts record only commands actually run. The source bootstrap gate checks cold worker compilation, offline Native rebuilding with only a relocated worker cache, and worker reuse after a source-plugin helper changes. Tool verification and JVM artifact acquisition still run before worker lookup when a Native executable must be rebuilt.

Non-optimized macOS ARM64 bootstrap builds also reuse the stable runtime core, Native interop/helper outputs and compiler object caches from <cache-dir>/native-core-cache. CLI code, the generated registry and source plugins remain in the final compilation. The local source architecture requires the core to remain independent of CLI/plugin declarations; compatible core edits rebuild its cache. Source-graph runtimes use this split only when their core matches the invoking launcher's curated sources and has no separate runtime source dependencies. A thin launcher containing only kvik and bootstrap checks the acquired sources against bootstrap/native-core.lock.json; a full checkout compares its current source bytes. Missing, stale or malformed inventory, and partially present local source roots, select monolithic compilation. Other graphs, Linux and optimized builds also retain monolithic compilation.

After changing core sources or their declared roots, review the core/CLI boundary and run python3 tools/pin_native_core.py to update the packaged inventory. python3 tools/pin_native_core.py --check verifies it without editing files and runs in Linux CI. The inventory is part of the bootstrap recipe identity; plugin and helper edits outside the core leave it unchanged.

Core keys bind ordered source/support bytes, extra libraries, tool identities, flags, recipes, controlled Native dependencies and conservative environment/path assumptions. These caches are local and location-bound. Published objects stay immutable; each final compilation receives private writable object-cache copies. Object filenames are encoded in immutable storage and restored to their original compiler-visible paths, including external KLIB names containing colons. New cache entries created there are recorded without replacing the shared seed, and every seeded byte is checked again. A core-key lock protects stable dependency paths through final linking and validation. native-core-cache.json records reuse, private additions or fallback and is bound by the executable manifest. Oversized core outputs use monolithic compilation; oversized object seeds retain the valid private build without publishing an unrepresentable cache entry.

Select an explicit source composition with --source-lock /absolute/source.lock.json and optionally --source-cache /absolute/source-cache (default .kvik/source-cache). The source graph contract covers pinned Git/archive packages, local source packages, source plugin dependencies and exact Native library closures. --print-composition validates declarations and observes local inputs without downloading or creating caches. An allowed build acquires verified inputs and compiles the composition; --offline requires cached remote inputs, and --no-build requires an already verified executable. Without --source-lock, the existing local composition remains the default. The full HTTPS source composition fixture passes cold build, relocated/offline reuse and rebuild, and rebuilding after a shared helper changes; its runtime also passes the existing JVM consumer tests.

The source-composition development loop can be measured with python3 tools/test_source_bootstrap.py --producer-root . --source-root . --edit-loop; set --java-home, --konan-home, --konan-data and --artifact-cache for the host. The reference Native dependency closure defaults to the repository's source-graph example; --reference-lock accepts a separately retained baseline. This scenario acquires Kvik, two source plugins and their shared helper over owned local HTTPS, then independently edits one plugin and the shared helper. It checks changed plugin behavior, unchanged core/object/worker identities and offline return to the original executable with compiler locations unavailable.

One 2026-10-27 run on Apple M4 Max (36 GiB), using committed runtime/launcher sources from 963d149 and provisioned Kotlin/Native 2.4.20/JDK 25.0.4 tools, passed:

Source-composition invocation Wall time Installed-tool verification Runtime construction New object-cache archives
Cold runtime and source caches 179.679 s 22.998 s 154.637 s 676
One plugin edit 38.754 s 19.372 s 18.306 s 0
Shared helper edit 35.637 s 16.363 s 17.999 s 0

Both edits reused the byte-verified Native core, object seed and JVM worker. Cached offline startup and return to the original composition took 0.102/0.126 s. Sampled owner-plus-descendant RSS maxima were 1.753/1.043/0.971 GiB for cold/plugin/helper, with at most 8/7/6 observed live processes. Sampling may miss short-lived processes and peaks, and summed RSS may count shared pages repeatedly. These are individual instrumented observations with uncontrolled OS caches, not startup percentiles or a general speedup claim; tool installation was excluded. The retained receipt is .kvik/tests/source-bootstrap/case-p7e_lroh/attempt-ijhpg4m7/summary.json.

Use a completed source-bootstrap attempt directory as --reference for tools/test_source_graph_lifecycle.py --run to check concurrent construction, compiler interruption/retry, corrupt executable/receipt repair and incompatible tool refusal. Without --run it prepares a private case; resume that case with --case <directory> --run. The same attempt is accepted as --fixture by tools/test_shared_runtime_launcher.py for shared-store publication and compiler-free restoration, with explicit --java-home, --konan-home and --artifact-cache. Both scenarios use the verified helper revision and its recorded plugin schemas, even when the source-bootstrap run has subsequently restored its original active lock.

Use --runtime-store /absolute/shared-store to share complete verified native runtime bundles between checkouts or distinct --cache-dir directories on the same compatible host. The local K04 store includes the executable, guardian, JVM workers and runtime manifests. Concurrent launchers coordinate one build; --offline --no-build can restore an existing shared bundle into an empty local runtime cache without compiler or source/artifact caches. Local source declarations still need to be observable. The store must be separate from sources, other bootstrap caches and tool installations. See the shared runtime contract and actual build/restore evidence.

abi-dump and abi-check accept the same module, target, project and tools selectors as build. They read main compilation outputs on JVM, Native, JS and WasmJS targets; publication coordinates are optional. Add "abi": {"baseline": "api/library.jvm.api"} to each target you want to check, using a separate baseline per target. Tests are neither compiled nor run. See public API baselines for the JS/Wasm workflow.

Run abi-dump --module library --target jvm with your project and tools options to produce the first candidate. The abi-ready event provides paths to api.txt and its versioned abi.json metadata. Review api.txt and copy it to the configured baseline, then use abi-check in your normal verification workflow. Neither command updates baselines automatically. A failed comparison still exposes the current candidate for review.

Checks require an exact public API match, allowing CRLF/LF and an optional final newline in the baseline. Additions and removals both fail; this is not a binary compatibility classification. Successful extraction and comparisons are cached locally. Baseline edits invalidate comparison and retained verification freshness while preserving compilation and extraction caches. ABI readers use pinned BCV and Kotlin compiler versions in an owned, isolated JVM process.

Enable API documentation with "documentation": {} on a module, then run resolve to capture its documentation dependencies. docs --module library --target jvm --target macosArm64 accepts the usual project and tools options and creates one HTML site for the selected module and targets, including JS and WasmJS when selected. The documentation-ready event gives the site's directory and index.html. Documentation consumes source-set relationships and compiled dependency artifacts from the same graph as builds; publication coordinates are optional. See API documentation for the JS/Wasm workflow.

Module documentation settings support includes (Markdown files), documentedVisibilities, reportUndocumented, failOnWarning, skipDeprecated, suppressInheritedMembers, and timeoutMillis. For example, "documentation": {"includes": ["Module.md"], "reportUndocumented": true, "failOnWarning": true} enables strict documentation checks. Include edits invalidate the site and retained verification freshness while preserving compilation caches. Failed or cancelled generation never exposes a completed site.

Dokka 2.2.0 runs offline in an owned JVM, using a separate pinned tool distribution. Common metadata and Native stdlib are declared inputs. Successful sites are cached locally and can be restored without installed tools. This command generates per-module HTML for supported JVM, Native, JS and WasmJS source sets; tests are neither compiled nor run. Aggregation, custom Dokka plugins, external documentation links, SDK/cinterop documentation and deployment need separate integrations.

Source build plugins and action graphs

The composed registry participates in every project build and script invocation, including BSP requests and foreground sessions. Exactly one ProjectBuildPlugin provides the graph; the bundled KotlinBuildPlugin supplies Kotlin compilation, tests, packaging and script actions through the ordinary adapters.

A source build plugin can implement ConfigurableBuildPlugin to accept project-local settings, for example "pluginSettings": {"example.assets": {"format": "compact"}} in kvik.json. Its settingsSchema is discoverable through runtime-info, alongside its existing authoring schema. validateSettings(settings) must be pure and bounded; it receives an empty object when settings are omitted and should reject unknown fields and invalid values. Unknown plugin IDs and settings for plugins without this contract are rejected before resolution, inspection or configuration publication. The core schema bounds the settings map to 128 plugins, IDs to 128 characters and each settings object to 256 KiB of canonical UTF-8. Standalone script companions do not accept this project-level field; named project scripts share project settings.

Graph callbacks read context.settings(id). Lower every setting that affects task execution into the action's parameters, arguments, environment or declared inputs, so action caching reflects the behavior. Executors consume those captured action values. Settings already participate in project/lock identity and foreground graph invalidation; editing them does not rebuild the native runtime. The set-plugin-settings and remove-plugin-settings edit operations validate the final configuration with the composed plugins before publishing a configuration/lock plan.

Additional ProjectGraphContributor plugins receive the locked configuration, captured input manifest, tool identities and current graph. They can add actions that consume declared predecessor artifacts. graphDependencies declares required plugin IDs whose graph callbacks must finish first; ready plugins use plugin-ID order. Duplicate action IDs, missing dependencies, cycles and missing requested terminals are rejected. Existing BuildPlugin.actions(project) implementations remain supported as additive callbacks over the authoring document.

For example, an artifact consumer can declare override val graphDependencies = setOf("example.generator"). Its producer's actions, instrumentation and artifact publications are then visible regardless of their ID ordering. Missing plugins and dependency cycles fail before graph construction; Kvik does not fetch or enable plugins automatically. The sole graph provider runs first and cannot depend on graph contributors. These declarations order project and script graph callbacks; source package dependencies, configuration/input hooks and generator declarations keep their separate contracts. runtime-info includes each plugin's sorted graphDependencies list.

Plugins can select compiled outputs with graph.requireArtifact(module, target, compilation, kind) or list matches with findArtifacts(...). The returned ProjectArtifact provides the producer dependency, output contract and manifest(results, destination) for selecting and remapping files from successful predecessor results. This avoids depending on internal action IDs or output directory names. inspect-graph exposes the same references in artifacts; queries only cover actions already included in the requested graph. Test graphs also expose test-results, JVM coverage captures and declared instrumentation outputs. These can feed dependent plugin actions after successful tests; retained report commands handle failed and incomplete runs.

Plugins can obtain compiler inputs with graph.requireCompilation(module, target, compilation). Add its dependencies to the consumer action and call resolve(results) to obtain source files, common sources, the ordered classpath, friend paths and compiler-plugin inputs from the same recipe used by compilation. sourceFiles() and classpathFiles() select CAS manifests for analyzers. This waits for input producers, without requiring the module's compiled output. Authored source roots retain their source-set names and project/input paths; generated files appear after prerequisite resolution. JVM, Native, JS/Wasm and common metadata compilations are covered. Queries only cover module compilations in the selected graph, including script prerequisites. inspect-graph lists these declarations under compilations without resolving recipes or running generators.

Contributors can also override artifacts(context, graph) to publish semantic references to their own declared outputs. The callback runs after their actions and instrumentation are added, so later plugins can find these artifacts without knowing the producer's action IDs or paths. Selected generator outputs are supported; unused generators are not scheduled merely because a plugin publishes references.

Build plugins can implement ProjectInputContributor.projectInputs(configuration) to return BuildPluginInputs(files = ..., directories = ...) with project-relative schema, template or data paths. This pure hook runs before capture and may use authored configuration, including plugin settings, but must not read files or external state. Declared files are required; absent directories are tracked so creating them triggers a rebuild. Inputs participate in local and revision-backed snapshots and foreground freshness checks without becoming compiler source roots or packaged resources. inspect exposes buildPluginInputs and tags their files with kind:plugin-input and build-plugin:PLUGIN_ID. Generators select the captured entries they consume into their action inputs; only those declared action inputs participate in that action's cache key.

For code generation, implement ProjectGenerationContributor.generatedInputs(context). Its ProjectGeneratedInputs supplies generator/helper action nodes and typed source and resource roots before the Kotlin graph is built. The bundled provider wires those producers into the selected module/source-set compilations, runtime resources and packaging. Common sources can serve JVM, Native and JS; test-only generators run only when their source sets are needed. inspect-graph exposes their origins and prerequisite edges. Generator IDs use plugin:PLUGIN_ID:generate:NAME, output roots require declared tree contracts, and resource roots declare exact relative file names. See the generation API for dependency and output rules.

Actions must use the current runtime composition identity. Contributions can use built-in adapters or register native implementations through ProjectExecutorContributor.executors(context). The returned map binds names such as example.assets:manifest-v1 to the existing ActionExecutor contract. Names must belong to the registering plugin's namespace, so plugins cannot replace built-in executors or another plugin's implementations. Registration runs once per invocation, before scheduling, and must not execute tasks or launch processes. Plugin settings use the declarative pluginSettings field. Graph construction must be deterministic and perform no external effects.

ProjectExecutionContext supplies the run ID, composition identity, a private retained workspace, ContentStore, and the invocation-owned ProcessHost. Read declared input bytes by digest; publish output bytes to the content store and return their logical paths and digests in the action result. Use separate workspace paths for concurrent actions. Process-backed adapters must use the supplied host and reap their work in RunningAction.close(); ProcessActionExecutor provides a reusable process adapter. Plugin state belongs to this run, including when a foreground session reuses its graph.

The scheduler uses each executor's CPU, memory and exclusive-resource requests. Custom executors currently support GENERATE, COMPILE, LINK, PACKAGE and VERIFY, with LOCAL or NEVER cache policy. Remote caching is rejected even for custom graph providers. Cache hits bypass start; native source composition, inputs, tools, options and environment remain part of the normal action identity. Choose NEVER when execution depends on undeclared external state or performs external effects. Registering an executor does not establish hermetic execution.

Kvik checks the implementation/composition/run identity, applies the action deadline and cancellation, and closes each handle before exposing its result to the cache. Registration, resources, start and poll are cooperative native callbacks: keep them bounded and nonblocking, moving long tool work to owned processes. Cleanup must be idempotent. Source plugins are trusted native code; these interfaces are not an in-process sandbox. A failed, timed-out or cancelled contribution prevents the requested checks from completing successfully. See the native executor plugin for artifact consumption, content publication and process execution. Test and script execution belong to checks declared by the graph provider; additive callbacks cannot introduce extra test or script executions.

All contributed actions must succeed before requested checks count as completed. With additional plugins or a custom graph provider, verify defaults to the full requested scope (source-build-plugins): plugins can consume any captured input, so the core module dependency graph alone cannot safely narrow verification. Individual actions still reuse their valid local cache entries. A sole provider may opt into core change selection with usesCoreProjectImpact only when the core module/source-set impact model covers all of its graph inputs.

runtime-info exposes plugin identities, API versions and configuration schemas. The Kotlin schema references the project and standalone-script definitions in contracts.json; register that local schema by its $id when resolving these references. Source edits rebuild the composed native executable and invalidate its action identities. See the classpath contributor and its CLI test for a working example.

JVM benchmarks

Declare JMH benchmarks in a JVM module's main source set. Add org.openjdk.jmh:jmh-core:1.37 as a compile dependency and org.openjdk.jmh:jmh-generator-reflection:1.37 as a runtime dependency. Set benchmarkSettings on its JVM target:

"benchmarkSettings": {
  "classes": ["example.CollectionBenchmark"],
  "warmups": 3,
  "iterations": 5,
  "iterationMillis": 1000,
  "forks": 1,
  "threads": 1,
  "mode": "avgt",
  "outputTimeUnit": "ns",
  "timeoutMillis": 3600000
}

Use JMH annotations directly; Kotlin state classes must be open. Benchmark modules can depend on other workspace modules. Run resolve after adding dependencies or changing configuration, then:

./kvik --no-build -- benchmark --project /absolute/project/kvik.json --tools /absolute/tools.json --module benchmarks --target jvm --profile smoke
./kvik --no-build -- benchmark --project /absolute/project/kvik.json --tools /absolute/tools.json --module benchmarks --target jvm --profile measurement

The default smoke profile uses one warmup and one measurement iteration, each 50 ms. measurement uses the declared settings. Both reuse ordinary compilation and the same generated JMH harness. Measurements always execute in fresh forked JVMs; cancellation and timeout stop the owned process tree. benchmark-ready identifies the normalized summary and original JMH JSON in the run directory. Scores in the normalized summary are decimal strings; an unavailable score error is null. Empty selections, failed benchmarks and incomplete parameter combinations fail the invocation.

Current checks

On macOS ARM64, supply a Kotlin/Native 2.4.20 installation and Liberica JDK 25.0.4:

python3 experiments/k01/bootstrap.py --java-home "$JDK" --konan-home "$KONAN"
# Set BUILD to the directory printed above.
python3 experiments/k01/verify.py --build-dir "$BUILD" --java-home "$JDK" --konan-home "$KONAN"
python3 tools/test_contracts.py --java-home "$JDK" --konan-home "$KONAN"

The bounded spike downloads checksum-locked compiler artifacts and compiles sources directly. It does not call Gradle or Kotlin Toolchain to compile consumer code. The reference baseline separately uses the fixture's existing KTC build for comparison.

Schema validation uses development-only Python dependencies:

python3 -m venv .kvik/schema-tools
.kvik/schema-tools/bin/pip install -r tools/schema-requirements.txt
.kvik/schema-tools/bin/python tools/check_schemas.py

Upstream source submodules under references/ are optional design references, not runtime/bootstrap dependencies. See their provenance.

Inspect and control a foreground session

The session-started event from dev supplies its directory. Another terminal or agent can query it without taking over the owner's stdin:

kvik session status --session /project/.kvik/sessions/SESSION
kvik session reconnect --session /project/.kvik/sessions/SESSION
kvik session events --session /project/.kvik/sessions/SESSION --limit 100
kvik session events --follow --session /project/.kvik/sessions/SESSION --limit 100
kvik session control --session /project/.kvik/sessions/SESSION --request-id test-1 --command t
kvik session control --session /project/.kvik/sessions/SESSION --request-id cancel-1 --command cancel --operation OPERATION_ID

Pass nextCursor as JSON to session events --cursor for subsequent pages. Only complete journal records advance the cursor; reads and page sizes are bounded. Status and event queries remain available after exit. ownerAlive is a lock observation at query time; the saved state may describe the last observation before a crash.

session reconnect follows published runtime handoffs and returns the latest directory, state, ownerAlive and the visited chain. Its status is attached when an owner is observed, restarting when a successor has not published its state, or owner-exited when no owner or successor remains. A failed successor startup can leave restarting; the query never creates or recovers an owner. Handoffs must link back to their predecessor and stay in the same session root. Queries traverse at most 64 sessions; use a newer anchor for longer chains.

session events --follow returns one bounded session-follow-events page. Keep the original --session directory and pass each returned nextCursor unchanged. It drains each old journal before advancing the cursor to the successor, whose event sequence starts at one. The response's directory identifies the journal read by that page; ownerAlive describes that journal's owner, while reconnect reports the latest owner. nextCursor can identify the next session. An incomplete tail blocks advancement. hasMore=false means no further events were observed at that moment; poll again while the session is running or restarting. MCP provides kvik_session_reconnect and the equivalent follow option on kvik_session_events. Control requests are never forwarded automatically: use the directory returned by reconnect and reconcile earlier acknowledgements before issuing new commands.

Control commands are r, t, q, status and cancel. Each request requires an ID: repeating the same ID and command retrieves its original acknowledgement, while changing its arguments is rejected. Cancellation requires the immutable operationId exposed by operation events or application status. It cannot cancel a newer operation that happens to occupy the same slot. An acknowledgement confirms receipt, not successful compilation or completed cancellation. Its outcome.status is accepted, not-active or closing; the latter two reject the requested action. Follow the operation's events for its final outcome.

--wait-ms defaults to 1000 and accepts 0–10000. Exit status 3 with pending means the command was submitted but not acknowledged within the wait; retry with the same ID. busy means publication was blocked and this attempt did not submit a command. owner-exited means no acknowledgement was observed before owner exit; it does not prove the command never ran. A retained acknowledgement can still be retrieved after exit.

The transport lives in the session's private local directory and is processed by the foreground owner. Its lock is released on exit and runtime replacement. It starts no service, and clients do not extend the owner's lifetime: stdin EOF, terminal loss, output backpressure and normal shutdown retain their existing behavior. The directory has the same trusted-local-filesystem boundary as the artifact store; it is not a remote authorization protocol.

Within an explicit dev session, plugin-free JVM compilations can reuse up to two owned compiler JVMs. Each action gets a fresh BTA build session, input/output directories and logs; tests and compiler plugins still run in fresh JVMs. Idle compilers count against memory admission and are stopped when other work needs their capacity. Cancellation, session exit and runtime replacement drain them; there is no background daemon. Compiler distribution verification remains per action. See the compiler session contract.

Check installed tools before a build:

kvik doctor --tools /path/to/tools.json --target jvm

Repeat --target for additional platforms; add --check test to check test runtime availability, including pinned Node for JS/Wasm. The command checks the runtime guardian, tool declaration and the same installation digests/layouts used by compiler execution, reporting independent failures together as JSON (schema). It reads local files without starting tools, downloading dependencies or creating build outputs. Exit status is 0 when the selected checks pass, 3 for failed or skipped checks, and 2 for invalid command arguments. Passing covers the selected tool installations at query time; it does not compile a project, check its dependencies or establish compatibility of project-specific compiler plugins and test engines. The source launcher can still provision/build Kvik before this command starts.

The regression gate python3 tools/test_unavailable_target_cli.py --runtime /path/to/kvik.kexe --tools /path/to/tools.json checks foreign-host Native requests alone and alongside JVM. It requires an incomplete result with every requested check retained, no completed checks or compiler actions, and a toolchain diagnostic. Linux CI runs the gate and retains its results as unavailable-target-linux.

About

Source-distributed Kotlin/Native build tool with explicit foreground sessions and artifact caching

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages