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_IDFor 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 summaryThis 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.
Register a project for a BSP client with:
./kvik --no-build -- bsp-install --project /absolute/project/kvik.jsonThis 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.jsonThe 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 testEach 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.
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.
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.jsonAn 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: supplymodule,sourceSet, and a completedependencyobject from the project schema. Adds or replaces by package or workspace-module identity, rejecting ambiguous duplicates.set-dependency-version: supplymodule,sourceSet,package(group:artifact), andversion. Updates one existing external dependency, preserving scope and export settings.remove-dependency: supplymodule,sourceSet, andselector, either{"package":"group:artifact"}or{"module":"library"}. Requires one match.set-target: supplymoduleand a completetargetobject. Adds or replaces by target name.remove-target: supplymoduleandtarget(the target name). Requires an existing target.set-source-set: supplymoduleand a completesourceSetobject. Adds or replaces by name, including source/resource directories,dependsOnedges and dependencies.remove-source-set: supplymoduleandsourceSet(the name). Requires an existing source set. Update any targets, source-set edges or generator settings that reference it.set-catalog-entry: supplyaliasand a full Mavencoordinate. Adds or replaces an entry independencyCatalog.set-catalog-version: supplyaliasandversion. Updates an existing catalog entry, preserving its group and artifact; all scripts using that alias see the update.remove-catalog-entry: supplyalias. Requires an existing entry.set-dependency-profile: supplynameand a completeprofileobject from the project schema. Adds or replaces its dependencies, compiler options and plugins.remove-dependency-profile: supplyname. Requires an existing profile.set-script: supplynameand a completescriptobject from the project schema. Adds or replaces the named script.remove-script: supplyname. Requires an existing script.set-plugin-settings: supplyplugin(composed build-plugin ID) and a completesettingsobject.remove-plugin-settings: supplyplugin. 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 --jsonpackages 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 --jsonIt 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.jsonUse 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-argumentWith 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.
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 wasmJsCompilation 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.jsonAn 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 jvmThe 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.jsonThe 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 20Use 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.jsonThe 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 100Both 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 20Pass 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.jsonindex 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 --patchThe 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.jsonThe 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 4The 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 900Each 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 jvmprepare-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 jvmIn 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.
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.
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 measurementThe 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.
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.pyUpstream source submodules under references/ are optional design references, not runtime/bootstrap dependencies. See their provenance.
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_IDPass 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 jvmRepeat --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.