moon v2.3 - Task tags, dep cache strategies, native file hashing, CAS cache, and more
This release focuses on giving you more control over how tasks are organized, referenced, and cached. We're introducing first-class tags for tasks, a new cache strategy for task dependencies, and a pair of experimental layers — native file hashing and a local CAS — that lay the groundwork for further performance improvements.
Task tags
Projects have supported tags since v1, but tasks themselves have never had first-class support for
tags, until now. In v2.3, we're introducing tags for tasks,
allowing you to label and organize tasks in a more flexible way. This opens up new possibilities for
filtering, grouping, and managing tasks based on their tags.
tasks:
lint:
# ...
tags: ['quality', 'ci']
To start, targets have been updated to support the # tag syntax in the task scope, allowing you to
do crazy things, such as:
:#tag- Reference all tasks with thetagtag.^:#tag- Reference all tasks with thetagtag in upstream projects.project:#tag- Reference all tasks with thetagtag in a specific project.#tag1:#tag2- Reference all tasks with thetag2tag, in all projects with thetag1tag.
These targets can be utilized on the command line, enabling you to curate exactly which tasks to run based on their tags.
$ moon run ':#quality'
Or they can be used in task dependencies, which will be expanded at runtime to a list of fully-qualified targets.
tasks:
build:
# ...
deps: ['shared:#prereqs']
In addition to the new tags setting, we've also added an
options.mergeTags setting to control how tags are merged during
task inheritance, a taskTag field to MQL for querying tasks
by their tags, and a --tags filter for moon query tasks.
Heads up: the existing MQL tag field has been renamed to
projectTag to disambiguate from the new
taskTag field.
Task dependency cache strategies
When a task depends on another task, that dependency's changes need to invalidate the current task's
cache — but how aggressively? Until now, moon always mixed the dependency's hash into the current
task's hash. This meant that a build task depending on an upstream build would re-run any time
the upstream task's inputs changed, even if the upstream task's outputs ended up identical. Even
worse, depending on output-less tasks like lint or test invalidated builds for no real reason.
In v2.3, task dependencies now support a cacheStrategy
field that controls exactly how a dependency contributes to cache invalidation:
hash- Use the dependency task's hash for cache invalidation. The current task is invalidated whenever the dependency changes (inputs, command, args, env, etc.). This is the historical behavior.ignored- Ignore the dependency task's hash entirely. The current task is never invalidated by this dependency's changes — the dependency is purely a sequencing edge.outputs- Use the dependency task's outputs instead of its hash. The current task is only invalidated when the dependency's output files change, not when its inputs change.
tasks:
build:
command: 'webpack'
deps:
- target: '^:build'
cacheStrategy: 'outputs'
For build dependencies in particular, outputs is the strategy you almost certainly want. A change
to an upstream project's source files that doesn't affect its compiled output should not force a
downstream rebuild — and now it won't.
Behavior change
To make this work for everyone out of the box, the default cacheStrategy is now chosen for you
based on whether the dependency declares outputs:
- A dependency with outputs (e.g. a
buildtask) defaults tohash. - A dependency without outputs (e.g. a
lintortesttask) defaults toignored.
This is a behavior change from v2.2 and earlier, where the strategy was always effectively hash.
In practice, this means tasks depending on output-less tasks will see fewer (correct!) cache
invalidations going forward. If you want to restore the old behavior for a specific dependency, set
cacheStrategy: 'hash' explicitly.
Experimental native file hashing
For as long as moon has existed, file hashing has been delegated to the VCS (typically Git). This was a pragmatic choice — Git already has a hash for every file it tracks, so why hash them again? But shelling out to Git for every hashing operation has overhead, and Git's hashing doesn't run on moon's task pool, which means it can't take advantage of the parallelism we've spent the last few releases building out.
In v2.3, we're introducing an experimental native file hashing implementation that runs directly within moon's task pool, sidestepping the VCS entirely. In our benchmarks, this delivers a 10-50% improvement depending on workspace size and file count.
Enable it in your workspace configuration:
experiments:
nativeFileHashing: true
The CAS hasher can also be tuned via the new top-level cache
setting — see the workspace config docs for the available knobs.
Experimental local CAS for task outputs
Speaking of caching — moon has shipped a remote content-addressable storage (CAS) backend since v1.30 for remote caching, but locally, task outputs were still archived as tarballs. In v2.3, we're introducing an experimental local CAS layer that stores task outputs in the same content-addressed format used by the remote cache. This means deduplicated storage across tasks, and a unified cache shape locally and remotely.
Enable it in your workspace configuration:
experiments:
casOutputsCache: true
This is a foundational change — most of the work in this release went into reshaping the cache, remote, and hashing internals around a shared CAS abstraction. Expect this to become the default in a future release once it's been battle-tested.
At this time, the local CAS may be slower than the tarball approach, as we have more guardrails to prevent data loss and cache corruption. Additionally, archiving and hydration currently happen in the main thread, and not through the daemon. In subsequent releases, we plan to optimize the local CAS for performance and make it the default.
MCP tools for template discovery
For AI agents working in moon repositories, code generation is one of the highest-leverage operations they can perform — but only if they know which templates exist and what variables those templates accept. v2.3 adds two new tools to the MCP server that close this gap:
get_templates- Lists available templates with id, title, and description. Supports an optional case-insensitive filter regex (mirroringmoon templates --filter).get_template- Returns the merged variable schema for a given template id, resolving theextendschain. Variables markedinternal: trueare omitted.
Together, these let an agent discover what's available, inspect a template's contract, and then call
generate with the right inputs — no more guessing at template ids or required variables.
Thanks to ateirney-nz for this contribution!
Unstable uv pip support for Python
In the Python ecosystem, pip is the default package manager, but tools like uv are gaining
traction for their speed and modern features. To better support Python developers, we've added
experimental support for uv pip as a package manager in the Python toolchain, alongside pip and
uv.
This new option can be enabled via the unstable_python.packageManager setting in your project
configuration:
unstable_python:
packageManager: 'uv-pip'
Because uv pip is a combination of uv and pip, that offers the speed and efficiency of uv
while maintaining compatibility with pip, the way it is configured is quite unique. For install
related args, the unstable_pip toolchain is used (because pip compatibility), while sync and
venv related args, the unstable_uv toolchain is used (because uv is used directly).
unstable_python:
packageManager: 'uv-pip'
unstable_pip:
installArgs: ['--group', 'dev']
unstable_uv:
syncArgs: ['--check']
venvArgs: ['--clear']
Other changes
View the official release for a full list of changes.
- Added Deno v2.8 support to the JavaScript toolchain.
- Added Git SHA256 support for commit hashes. This is in preparation for Git's transition to SHA256 as the default hash algorithm.
- Reduced task target memory footprint by 50-100%, further trimming the in-memory size of the project and task graphs.
- Fixed a glob regression where unbounded walks could be up to 10x slower.
