Luchta is a Rust-based alternative to Microsoft's Lage build system, specifically designed for JavaScript/TypeScript (yarn) monorepos. The project is named after Luchta, the Irish god of woodwork, reflecting its role in crafting and assembling complex software projects.
Status: Early-stage / Work-in-Progress (WIP).
Luchta optimizes monorepo workflows by:
- Discovering yarn workspace packages.
- Building a Package Graph for dependency topology.
- Constructing a Task Graph (e.g.,
ui#build) for granular execution. - Executing tasks in topological order with weight-based concurrency to manage resources like RAM.
The project is organized into a multi-crate Cargo workspace under crates/:
luchta-types: Shared types such asPackageName,TaskId, andTaskDefinition.luchta-lockfiles:Lockfiletrait abstraction and Yarn v1 implementation.luchta-workspace: Workspace discovery and Package Graph construction.luchta-engine: Task Graph construction and the weighted task executor.luchta-cli: Entry point,clapCLI, and executable config script loading.
Project automation lives in the xtask/ crate (the standard Rust xtask
pattern), invoked via the cargo xtask alias.
To build the entire workspace:
cargo build --workspaceTests run via cargo-nextest. Install it once with
cargo install cargo-nextest --locked, then:
cargo nextest run --workspaceIt is recommended to run the suite 5 times to catch flaky tests before opening a PR:
cargo nextest run --workspace --stress-count=5To build and run the CLI:
cargo build -p luchta-cli
./target/debug/luchta --helpRepetitive project tasks live in the xtask crate, run through the
cargo xtask alias. To install all workspace binary crates in one step:
cargo xtask installThis discovers every workspace member with a binary target via cargo metadata and runs cargo install --path for each, so it stays correct as
crates are added.
Before committing, run the full pipeline (see AGENTS.md for details):
cargo build --workspace
cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
cargo nextest run --workspace --stress-count=5
cs delta origin/HEAD # CodeScene — must be all greenThe CodeScene cs delta check must be all green (no new code-health
problems) for a change to be considered done.
Releases are managed by knope and driven by changeset
files in .changeset/. Add a changeset for every user-visible change:
---
luchta: minor
---
Brief description of the change.The front-matter key is always luchta, and the bump level is one of patch,
minor, or major. To cut a release, run the Prepare Release GitHub
Action (or knope release locally); knope bumps the version, updates
CHANGELOG.md, and pushes a luchta/v<version> tag. The tag push triggers the
Release workflow, which cross-builds the luchta binary for Linux, macOS,
and Windows and attaches the archives to the GitHub Release. The Release
workflow can also be run on demand (workflow_dispatch) to build binaries
without cutting a version.
Luchta is configured via an executable script at the workspace root matching luchta-config.* (e.g., .ts, .js, .sh, .py).
The script must have a shebang line and print its configuration to stdout as a JSON object with camelCase fields. Luchta executes the script directly and parses this JSON to load the pipeline definition.
Example luchta-config.ts:
#!/usr/bin/env node
/**
* A dependency reference for a task. One of:
* - `"^task"` direct upstream packages' task
* - `"^^task"` transitive upstream packages' task
* - `"task"` same-package task
* - `"pkg#task"` a specific package's task
* - `"#task"` a specific top-level task
*/
type DependsOn = string;
interface EnvSpec {
/** Explicit value for the variable. Pins the value and is cache-relevant. */
value?: string;
/** Fallback value if the variable is unset in the ambient environment. Cache-relevant. */
default?: string;
/** Whether the variable should be included in the build cache hash. Defaults to true. */
input?: boolean;
}
interface TaskDefinition {
/** Tasks that must finish before this one runs. */
dependsOn?: DependsOn[];
/** Relative cost for the weighted scheduler. Defaults to 1. */
weight?: number;
/**
* Explicit command line. When omitted, the matching `scripts` entry from
* the package's `package.json` is used. For tasks routed to a `worker`,
* this is passed to the worker (e.g. the Yarn subcommand) and defaults to
* the task name.
*/
command?: string;
/** Name of a worker (from `workers`) that should execute this task. */
worker?: string;
/** Environment variables for this task. Overrides worker and global env. */
env?: Record<string, EnvSpec>;
}
interface WorkerDefinition {
/** Command that launches the long-lived worker process. */
command: string;
/** Environment variables for all tasks running on this worker. Overrides global env. */
env?: Record<string, EnvSpec>;
}
interface LuchtaConfig {
/** Global environment variables for all tasks. */
env?: Record<string, EnvSpec>;
/** Pipeline task definitions, keyed by task name (or pkg#task, #task). */
tasks?: Record<string, TaskDefinition>;
/** Stay-resident worker definitions, keyed by worker name (Unix only). */
workers?: Record<string, WorkerDefinition>;
/** Scheduler limits. */
concurrency?: {
/** Maximum cumulative task weight allowed to run at once. Overridden by --max-weight / LUCHTA_MAX_WEIGHT. */
maxWeight: number;
};
}
const config = {
env: {
NODE_ENV: { value: "production" }
},
tasks: {
build: {
dependsOn: ["^build"],
weight: 2,
env: {
BUILD_TYPE: { value: "full" }
}
},
"#prep": {
command: "echo 'Top-level prep'"
},
"web#test": {
dependsOn: ["build", "#prep"],
worker: "yarn",
env: {
CI: { input: false } // Passed to task but doesn't affect cache hash
}
},
test: {
dependsOn: ["build"],
worker: "yarn"
}
},
workers: {
yarn: {
command: "luchta-yarn-worker",
env: {
YARN_CACHE_FOLDER: { default: "./.yarn-cache" }
}
}
},
concurrency: {
maxWeight: 10
}
} satisfies LuchtaConfig;
console.log(JSON.stringify(config));The top-level tasks map defines the pipeline. Each task may set:
dependsOn: dependency list (see syntax below).weight: relative cost for the weighted scheduler (defaults to1).command: explicit command line. When omitted, the matchingscriptsentry from the package'spackage.jsonis used.worker: name of a long-lived worker (from theworkersmap) that should execute this task. The named worker must be defined or the run fails.cache: opt-in build cache. Provide an object (cache: {}) to enable change-detection skips for successful prior runs; omit the field to disable. (Reserved for future per-task cache options.)inputs: relative input paths/globs. Literal paths and glob matches are hashed from git-tracked files, so.gitignoreis respected. See Input Pattern Prefixes.outputs: relative output paths/globs. These are checked on disk, so missing/deleted outputs invalidate cache entries even if ignored by git.env: environment variables for the task. See Environment Variables for details on scopes and resolution modes.
inputs and worker-reported detected_inputs support package/root prefixes in addition to bare package-relative paths:
| Prefix | Resolves against | Semantics |
|---|---|---|
#path |
repo root | literal → absent if missing; glob → wildcard |
@scope/pkg#path / pkg#path |
named package | literal → absent if missing; glob → wildcard |
^path |
direct upstream packages | always wildcard; never errors on no match |
^^path |
transitive upstream packages | always wildcard; never errors on no match |
bare path |
own package | literal → absent if missing; glob → wildcard |
Notes:
^and^^are wildcard-only even when the suffix looks like a literal path.- Inter-package
outputsare not supported; prefixes apply to cache inputs only. - Cross-package inputs obey the target package's
.gitignore/ git-tracked file view because resolution happens relative to each target base directory. - Missing named packages or path escapes fail hard.
The tasks map defines how tasks are applied across the workspace:
task(e.g.,build): Default definition for all non-top-level packages. Does not apply to the workspace root.pkg#task(e.g.,web#build): Specific definition for packagepkg.#task(e.g.,#build): A top-level task that runs at the workspace root. Only#-prefixed keys run at the top level.
luchta run build: Runs packagebuildtasks. Top-level tasks are never included.luchta run -T build(or--top-level): Runs the top-level#buildtask.luchta run -p <PATTERN> build: Selects tasks by package name (not path). Supports glob wildcards (e.g.@repo/*,pkg-*). Repeatable.luchta run --since <GIT_REF> build: Restricts goal tasks to packages changed sinceGIT_REF, plus their transitive dependents.luchta run 'test*': Task arguments also support glob wildcards (e.g.test:*,build*).luchta run -T -p app build: Runs both@repo/app#buildand the top-level#buildtask (-Tis additive to-p).
Luchta uses a Goal-not-filter selection model. Filters select the entry-point goals you want to reach; transitive prerequisites of those goals always run, even if they live in packages or have task names that do not match the filter. Luchta ensures everything needed for your targets is built.
--since <GIT_REF> checks for package-folder changes from committed history (GIT_REF..HEAD), staged changes, unstaged changes, and untracked files that are not gitignored. The affected set is changed packages ∪ transitive dependents, then normal dependency expansion still runs prerequisites needed by those goals. If no packages are affected, luchta run exits 0 immediately and prints that nothing will run — unless top-level mode (-T) is requested. Top-level -T / #task goals bypass both the since filter and that early exit, so they still run regardless of whether the affected set is empty or non-empty.
Additional targeting rules:
- AND Logic: Filters across dimensions are combined, including
--since(e.g.-p pkg --since main buildmatches goals where package name matchespkg, task name matchesbuild, and package is in affected set). - Mandatory Tasks: At least one task argument is required;
luchta run -p pkgis an error. - Error Reporting: If no matches are found, Luchta provides a clear error distinguishing between "no packages matched the pattern" and "no tasks matched within the selected packages".
When a task fails during luchta run, its output is replayed to the console wrapped in a clear header and footer block.
To prevent extremely large logs from flooding the terminal, luchta run truncates output that exceeds 100 lines. It preserves the first 30 lines and the last 70 lines, inserting a placeholder that points to the exact luchta logs command needed to view the full output.
──▶ app#build
...
(first 30 lines)
...
… 150 lines hidden — run `luchta logs -p app build` for full output
...
(last 70 lines)
...
──◀ app#build (1200ms)
luchta run can pause dispatching new tasks when memory pressure is high. In-flight tasks keep running to completion.
--mem-usage-threshold <BYTES_OR_PERCENT>/LUCHTA_MEM_USAGE_THRESHOLD- Pauses new task dispatch while summed process-tree RSS is greater than threshold.
- Accepts percentages like
50%or absolute values like4GiB,512MiB,2GB, or bare bytes. - Default:
50%of total system memory.
--mem-free-threshold <BYTES_OR_PERCENT>/LUCHTA_MEM_FREE_THRESHOLD- Pauses new task dispatch while system available memory is less than threshold.
- Accepts percentages like
12.5%or absolute values like1GiB,512MiB,500MB, or bare bytes. - Default:
1/16of total system memory.
Precedence: flag > env var > default.
Behavior: luchta pauses dispatching NEW tasks while process-tree RSS exceeds --mem-usage-threshold or system available memory drops below --mem-free-threshold. In-flight tasks run to completion. There is no timeout or auto-abort while paused; use Ctrl-C to abort.
Status line: while paused, periodic progress output appends ⚠️ mem usage high and/or ⚠️ system free memory low.
--max-weight <WEIGHT>/LUCHTA_MAX_WEIGHT- Overrides the global maximum cumulative task weight allowed to run at once.
- Accepts a positive integer.
0or empty values are rejected. - Default:
concurrency.maxWeightfrom config, or available parallelism.
Precedence: flag > env var > config concurrency.maxWeight > default.
By default, luchta run suppresses the output of successful tasks to keep the console clean. You can view the full stdout, stderr, and execution metadata for any previously run task using the luchta logs command.
All executed tasks—even those that are not opt-in for caching—persist their run records and logs locally.
luchta logs: View logs for all tasks from the most recent runs.luchta logs build: View logs for all tasks namedbuild.luchta logs -p '@scope/*' build: View logs forbuildtasks in packages matching@scope/*.luchta logs --failed: View logs only for tasks that failed in their last run.luchta logs --show-outputs: Include metadata for all task outputs.
| Flag | Description |
|---|---|
tasks (positional) |
Task names to match; supports glob wildcards (e.g. b*). |
-p, --package <PKG> |
Match package name globs (not paths). Repeatable. |
-T, --top-level |
Match tasks defined at the workspace root instead of package tasks. |
--time-taken <MS> |
Filter to tasks that took at least this many milliseconds. |
--failed |
Filter to tasks that failed (succeeded == false). |
--show-inputs |
Show input file metadata (path, size, mtime, hash) for each task. |
--show-outputs |
Show output file metadata for each task. |
--file <NAME> |
Raw byte-exact passthrough of named report files (repeatable). |
luchta logs always displays the full, non-truncated output for every matching task.
By default, luchta logs surfaces all reports attached by workers after stdout/stderr. If a report's MIME type has a native renderer, it is pretty-printed; otherwise, it is dumped verbatim.
Native MIME renderers:
application/sarif+json: SARIF format. Prints IDE-clickable[LEVEL] message --> path:line:collines.application/vnd.ctrf+json: CTRF format. Prints a pass/fail/skip summary plus details for each failed test.
Dispatch is based on MIME type only, ignoring filename/extension. Pretty-printing automatically disables coloring when piped or when NO_COLOR is set.
To retrieve the raw, unformatted content of specific reports (e.g., for mechanical consumers like reviewdog), use the --file flag:
luchta logs build --file sarif.jsonThe --file flag uses union task selection: a task is included if it has at least one of the named files. If no tasks match any of the requested files, the command exits with a non-zero error code.
Luchta supports flexible dependency definitions:
^task: Direct upstream packages' task.^^task: Transitive upstream packages' task.task: Same-scope task. Inside a package task, targets the same package; inside a#task, targets the top-level.pkg#task: Specific package and task.#task: Specific top-level (workspace root) task.
Environment variables can be declared at three scopes, with the following precedence: Task > Worker > Global. A variable defined in a more specific scope overrides the same variable name from a broader scope.
Each variable in an env map follows one of four modes based on the fields provided:
| Mode | Configuration | Description | Cache-Relevant? |
|---|---|---|---|
| Set | value: "..." |
Use the exact provided value. | Yes |
| Inherit | (neither value nor default) |
Inherit from the ambient environment of the luchta process. |
Yes |
| Set Default | default: "..." |
Use ambient environment if present, otherwise fall back to the default. | Yes |
| Cache Ignore | input: false |
Inherit from ambient environment, but exclude from the build cache hash. | No |
Notes:
- An empty string (
value: "") counts as a present value and does not fall through to a default. luchta checkwill report an error if bothvalueanddefaultare set for the same variable in a single scope.- The build cache hash uses the effective resolved value (including the
defaultfallback).
Luchta executes task subprocesses in a strict environment. The ambient environment is cleared, and only the following are injected:
- Resolved variables declared in your
luchta-config. - A built-in passthrough whitelist of essential variables.
Variables in the passthrough whitelist are provided to the subprocess but do not affect the build cache hash, ensuring that caches remain portable across different machines.
Passthrough Whitelist:
PATH, PATHEXT, LD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH, HOME, USER, LOGNAME, SHELL, XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME, USERPROFILE, APPDATA, PROGRAMDATA, SystemRoot, SYSTEMDRIVE, WINDIR, ProgramFiles, ProgramFiles(x86), TMPDIR, TMP, TEMP, TERM, COLORTERM, FORCE_COLOR, NO_COLOR, LANG, LC_ALL, TZ, SSL_CERT_FILE, SSL_CERT_DIR, CI, HTTP_PROXY, HTTPS_PROXY, NO_PROXY, http_proxy, https_proxy, no_proxy.
Declared variables always override whitelist variables on name collision.
For tools with heavy startup costs (Yarn PnP, Babel, ESLint, Jest), Luchta can route tasks to stay-resident worker processes instead of spawning a fresh process per task. Workers are lazily spawned on first use and reused across jobs, then shut down cleanly when the run completes.
Workers are defined in the top-level workers map, keyed by name. They can be
defined as a bare string (command only) or an object (command + dependencies):
workers: {
// Bare string form: command only
bash: "luchta-bash-worker",
// Object form: command and optional dependencies
yarn: {
command: "luchta-yarn-worker",
dependsOn: ["#prep"]
}
}Then point a task at a worker with its worker field. Luchta ships several
standard worker binaries and a set of composable filters.
Workers can declare their own dependencies in the configuration.
workers.<name>.dependsOn uses the same syntax as task dependsOn (see below).
These dependencies are automatically appended (engine-side) to every task that
uses that worker.
Injected worker dependencies are:
- Deduped against existing task dependencies.
- Persistent even if the worker's
resolveprotocol message tries to modify task dependencies. - Tolerant of pointing at pruned or missing tasks.
Workers can attach report files (e.g., test results or linting findings) to a task using the report message in the JSONL protocol:
{"type":"report","id":"task-id","filename":"report.json","mimeType":"application/sarif+json","content":"..."}- content: Must be UTF-8 text. The engine writes this verbatim to the task's cache directory (
.luchta/cache/<hash>/<filename>) alongsidestdout.logandstderr.log. - filename: Must be a safe, plain basename. Filenames containing path separators (
/,\), reserved names (stdout.log,stderr.log,meta.bincode), or relative path segments (.,..) are rejected with a warning. - mimeType: Used by
luchta logsto determine how to display the report. Natively supported MIME types:application/sarif+json,application/vnd.ctrf+json. Unknown MIMEs are shown verbatim. Dispatch is by MIME, not filename. - Duplicate filenames: If multiple
reportmessages use the same filename within one task, the last message wins.
Reports are recorded in the task metadata and can be viewed via luchta logs.
- luchta-yarn-worker runs each task through Yarn so that Yarn-injected
environment variables (
PATH,NODE_OPTIONS, …) are available. For yarn-worker tasks, the task'scommandbecomes the Yarn subcommand (defaulting to the task name) and is invoked asyarn workspace <pkg> <command>for package tasks, oryarn <command>at the workspace root. Worker-reported detected inputs/outputs replace declared cache patterns for next run decisions; yarn worker always addspackage.jsonto detected inputs so script changes invalidate cache entries. - luchta-bash-worker runs arbitrary commands via
sh -c, useful for tasks that don't need Yarn workspace wrapping.
Luchta provides a set of composable wrapper workers that can be chained using
-- to add laziness or conditional pruning to any worker. Each wrapper spawns
the next stage in the chain as a child process and forwards the JSONL protocol.
Composition works from left to right; the rightmost stage is the real worker.
Pruning is silent.
- luchta-lazy-worker -- <delegate...>
Answers
resolvewithAcceptimmediately without starting the delegate. Spawns the delegate only on the firstRunrequest and reuses it thereafter. Useful for deferring expensive worker startup until a task actually runs. - luchta-file-exists-filter ... -- <delegate...>
During
resolve, prunes the task unless at least one of the provided file globs matches a file within the task's directory (OR semantics). - luchta-yarn-filter [--script NAME]... [--dependency NAME]... -- <delegate...>
Prunes tasks based on
package.jsoncontent. All conditions must be met (AND):- Default: Prune unless a script matching the task name exists.
--script NAME: Prune unless the specified script name(s) exist.--dependency NAME: Prune unless the specified package(s) are present independenciesordevDependencies. If only--dependencyis used, the default script check is skipped.
- luchta-command-filter -- <delegate...>
Runs the provided predicate command in the task's directory during
resolve. If the command exits with code 0, the task is kept; otherwise, it is pruned. Predicate output is kept off the protocol stdout.
Example: A complex worker chain
This example only runs the Babel worker if package.json has a babel
dependency, a babel.config.* file exists, and the worker startup is deferred
until needed:
workers: {
babel: {
command: "luchta-yarn-filter -- luchta-file-exists-filter 'babel.config.*' -- luchta-command-filter jq -e '.dependencies.babel' package.json -- luchta-lazy-worker -- yarn workspace luchta-workers luchta-babel-worker"
}
}Note: Stay-resident workers and filters are supported on Unix only.
Luchta build cache is opt-in per task via cache: {}. Cached task skips only when prior run succeeded and all cache inputs still match: task spec, significant env, package dependency versions from yarn.lock, dependency-task output hashes, declared or worker-detected inputs, and outputs.
- Default cache dir:
<workspace>/.luchta/cache - Override:
LUCHTA_CACHE_DIR=/abs/path - Inputs use git-tracked listing, so
.gitignoreis honored for globs and literals. - Input prefixes may target repo root (
#...), named packages (pkg#...,@scope/pkg#...), direct upstream packages (^...), or transitive upstream packages (^^...). ^/^^inputs are wildcard-only and never error on zero matches; missing literals becomeabsententries only for bare /#/pkg#forms.- Outputs are checked directly on disk, so missing output reruns task.
- Worker-detected inputs/outputs replace declared patterns for later cache checks.
- Inter-package outputs are not supported.
- Logs are stored in cache records; only FAILED-task logs are printed by default.
Example:
build: {
worker: "yarn",
cache: {},
inputs: ["src/**/*.ts", "package.json"],
outputs: ["dist/**"],
env: {
NODE_ENV: { value: "production" },
CI_JOB_ID: { input: false }
}
}The shared build cache is a cross-worktree, cross-clone cache that restores task outputs and logs from prior builds. While the standard Build Cache is local to a single workspace, the shared cache allows developers and CI to reuse results across different checkouts of the same repository.
- Commit-Keyed: Results are indexed by git commit hash.
- Content-Addressed Blobs: Build outputs are compressed and stored in a deduped blob store.
- Read Window: On cache lookup, Luchta consults the last 20 commits (configurable) to find a match.
- Remote Synchronization: Opt-in synchronization with S3 or other object stores via
rclone.
By default, the cache is stored at ~/.cache/luchta (on Linux/macOS):
blobs/<outputs_hash>.tar.zst— Content-addressed compressed output archives.snapshots/<commit>/<shard_id>.bincode— Metadata snapshots, stored as append-only content-addressed shards.
The shared cache is OPT-IN and is configured exclusively via environment variables:
LUCHTA_SHARED_CACHE— Configuration mode:off(default) — Disabled.local,1,true,on— Local-only shared cache.rclone:<spec>— Enable remote-sync via rclone, where<spec>is an rclone Fs base that points at a bucket and (recommended) a prefix, e.g.rclone:my-s3:my-bucket/luchta-cache.
LUCHTA_SHARED_CACHE_DIR— Override the cache root directory.LUCHTA_SHARED_CACHE_SYNC_TIMEOUT— Maximum seconds for the initial remote sync. Default:30.LUCHTA_SHARED_CACHE_GC_DAYS— Retention period for local cache entries. Default:14.LUCHTA_SHARED_CACHE_MAX_OUTPUT_MB— Maximum size for a single task's output to be cached. Default:250.LUCHTA_SHARED_CACHE_HISTORY— Number of recent commits to check for snapshots. Default:20.
Invalid numeric values will trigger a warning and fall back to their defaults.
Luchta can synchronize the shared cache with a remote object store (like S3, GCS, or Azure) using rclone.
- Setup: Run
rclone configto create and name a remote (e.g.,my-s3). - Enable: Set
LUCHTA_SHARED_CACHE=rclone:<remote-name>:<bucket>/<prefix>.- Example:
rclone:my-s3:my-bucket/luchta-cache. - Luchta appends
blobs/andsnapshots/beneath this base, so a dedicated bucket or prefix is recommended. - For S3 (and other bucket-based backends) you must include the bucket
name — pointing at the bare remote root (
rclone:my-s3) is not a valid write target.
- Example:
- Credentials: Luchta does not handle credentials directly. It uses the
rclonebinary on yourPATHand relies on yourrclone.conforRCLONE_*environment variables.
Resilience & Performance:
- Build Safety: Remote cache problems (timeouts or rclone errors) never fail a build. If an error occurs, Luchta issues a warning, disables the remote cache for the rest of the run, and continues using only the local cache.
- No CAS Required: Snapshots are stored as append-only content-addressed shards, eliminating the need for complex "Compare-and-Swap" operations on the remote store.
- Garbage Collection: Remote GC is not managed by Luchta. Use S3 bucket lifecycle rules or similar object store features to expire old objects.
A task is eligible for the shared cache if all the following are true:
- The task succeeded.
- It took at least 100ms to run.
- Its total output size is within the
LUCHTA_SHARED_CACHE_MAX_OUTPUT_MBlimit. - All its outputs are contained within its own package directory (outputs escaping the repository root are a hard error).
- The working tree is "clean" (bare
<commit>key) or "dirty" (staged or unstaged changes to tracked files; ignored files don't count). Both clean and dirty entries are reusable (content-validated on restore), though dirty entries are kept out of any future remote sync.
Luchta automatically performs throttled garbage collection of old local cache entries and blobs (those older than LUCHTA_SHARED_CACHE_GC_DAYS). The cache is read-tolerant; if a blob is missing due to GC or other reasons, it is treated as a cache miss.
Shared cache hits are shown in the build summary: 📥 <n>.
- Phase 1 (Current): Multi-crate workspace skeleton, CI, and release tooling (nextest, knope changesets, GitHub release workflows).
- Phase 2: Foundation libraries (workspace discovery, lockfile parsing, graph construction, weighted parallel execution).
- Phase 3 (Current): Opt-in build change-detection cache (blake3 hashing, local and shared) — see "Build cache" and "Shared Build Cache" above.
- Future: Cross-process build locking.