Troubleshooting
This page starts from a symptom, not a message: something looks wrong and you need to know why. For the exact text of every error Lattice can print, see Errors — this page links to it wherever a fix needs you to have seen the specific message.
Detection and configuration
A workspace halts with “ambiguous or undeclared task driver”
Lattice climbs an evidence ladder to pick the tool that runs a workspace’s
tasks — your engines declaration, then a native file like
packageManager or .nvmrc, then a tool-unique lockfile. If none of that is
present, or two tools of the same role are present at once (say,
bun.lockb and pnpm-lock.yaml), it halts rather than guess. See Driver
detection for the model and Errors
for the exact message shapes.
Fix: paste the engines line the error suggests into that workspace’s entry
in lattice.json, naming whichever tool you actually use. If you’d rather
not have anything inferred for that workspace, set "auto": false and
declare scripts yourself — the halt only ever fires for auto: true
workspaces.
A task’s resolved command isn’t what you expected
Command resolution has a fixed precedence: an explicit entry in a
workspace’s scripts map always wins; only a task with no entry there
falls back to the driver’s inferred invocation
(crates/lattice-workspace/src/lib.rs:770-789). For JavaScript-family
drivers, inference requires the task name to exist in the manifest’s own
scripts/tasks map — Lattice never invents a script your package.json
doesn’t have. For direct-invoke drivers (cargo, go, and similar),
inference never fabricates a command for a persistent task — there’s no
universal cargo dev — so a persistent task on one of those drivers needs
an explicit scripts entry.
Fix: run lattice run <task> --dry-run first. It prints a dry run · build
banner, then every workspace:task that would run, in order, next to its
fully resolved command — including cross-workspace dependencies — without
executing anything:
lattice run build --dry-run
lattice dry run · build
→ web:build pnpm run build
→ cli:build cargo build --release
If the command shown isn’t the one you meant, add or edit that workspace’s
scripts entry for the task; it takes precedence over anything inferred.
--dry-run returns before toolchains are provisioned or PATH is adjusted,
so it shows the command as written, not necessarily as it would resolve if a
provisioned tool changes what’s first on PATH.
A config validation error on lattice.json
Duplicate workspace names, an empty path, a bad maxCacheSize string, an
engines entry using the version-only string form for a tool Lattice
doesn’t know how to version-check — all of these are caught before any task
runs, with a message naming the exact field and value. See
Errors for every shape
and how to fix each. For the field reference itself, see
Configuration.
A cycle in the task graph
cycle detected in task dependency graph
Two or more tasks depend on each other, directly or transitively, through
dependsOn (same-workspace or ^-prefixed cross-workspace edges). There’s
no valid run order, so nothing runs. Fix by tracing the dependsOn chain
back to where it loops — lattice run <task> --dry-run will not print an
order for a graph that can’t be scheduled, but the error names the graph,
not a specific edge, so the fastest way to find the loop is to read
each involved task’s dependsOn in lattice.json directly. See Task
graph for how ^task versus task builds edges.
Caching
A task never hits the cache
The cache key is a hash over the task name, its resolved command, every
file matched by that task’s inputs globs, tool-unique lockfiles in the
workspace, the current values of any env vars listed in the task’s env,
the resolved toolchain identity, and the Lattice version
(crates/lattice-cache/src/lib.rs:349-352). If the key changes on every run
even though nothing you’d call “the code” changed, the usual cause is
something feeding the hash that shouldn’t:
- an
inputsglob that matches a file which legitimately changes every run (a timestamp, a generated lockfile-adjacent file,.DS_Store) — narrowinputsor add it toignore - an
enventry whose value differs across your shells or machines (an absolute path, a session id) — drop it fromenvunless the task’s command output genuinely depends on it
Run with -l/--loquacious to see the truncated key and hit/miss status
per task as it runs:
web:build: hash a1b2c3d4e5f6a7b8
web:build: cache miss
A different hash prefix on back-to-back runs with no source change points
at one of the inputs above. Loquacious mode shows the outcome, not which
field changed the hash — narrow it by elimination, or compare inputs/env
against what the build script actually reads.
A task hits the cache when it shouldn’t
The inverse problem: the build reads a file, or an env var, that isn’t
declared. inputs only matches what its globs say — a file the build
reads outside of that set is invisible to compute_key, so editing it
changes nothing about the key, and the stale result gets replayed. Same
for env: only the names you list are read and hashed
(crates/lattice-runner/src/lib.rs:509-516); a var your command depends on
but that isn’t listed can change value with no effect on the key. Neither
case raises a warning — a wider inputs glob or a fuller env list is the
only fix. See Caching for what belongs in each,
and force a fresh run while you investigate with lattice run <task> --force or --no-cache.
--force/--no-cache doesn’t help, or the output looks wrong after a hit
A cache hit only counts if the metadata parses, the tarball opens, and its
digest matches what’s recorded — anything else is treated as a miss and the
task reruns, never a false hit
(crates/lattice-cache/src/lib.rs). If restoring the outputs into the
workspace itself fails partway (permissions, disk space), that’s a
non-fatal warning and the task still ran fresh, so the code is correct even
though the warning printed. See
Errors for the exact warning text.
Toolchains
An engine version check fails on a teammate’s machine but not yours
This means the constraint is in validate-only mode — a version range
with no installCmd — so Lattice trusts whatever’s on that machine’s
PATH and only checks its version. Different machines with different
tools installed will disagree. See
Errors for the exact
messages (command not found, version doesn’t parse, doesn’t satisfy the
range).
Fix: either get every machine’s host tool onto the same version, or move
the constraint from validate-only to provision by adding an
installCmd — Lattice then installs its own copy into
.lattice/toolchains/ per machine, so nobody’s host PATH matters. See
Engines and provisioning for the three-mode
gradient.
installCmd provisioning fails partway
Provisioning stages the install into a temporary directory, runs
installCmd, version-checks the result, then renames the staged directory
into its final content-addressed path
(crates/lattice-workspace/src/toolchain.rs). A failure at any of those
steps — the install command itself exiting non-zero, the freshly installed
tool failing its version check, or the final rename failing — is fatal and
names the exact stage; see
Errors for each message.
Nothing is left pinned on partial failure, so rerunning the same command
retries provisioning from scratch; there’s no partial state to clean up by
hand.
Toolchain resolution isn’t shown by --dry-run
--dry-run prints resolved commands only — it returns before any
toolchain is provisioned or validated
(crates/lattice/src/commands/run.rs). An engine failure only surfaces
when a task actually runs (or via lattice setup, which provisions root
engines up front). If you need to confirm a teammate’s toolchain resolves
before handing them a task to run, have them run lattice setup first.
Output
The interactive TUI doesn’t show up
detect_mode picks Raw output whenever stdout isn’t a real terminal, a
CI environment variable is set, or -l/--loquacious was passed —
otherwise it’s Interactive
(crates/lattice-output/src/lib.rs:22-35). On top of that, lattice run
forces Raw whenever the tasks being run pull a persistent task into
their closure, regardless of the three checks above — a dev server’s
streaming output is the point of the run, and a live TUI can’t render it
underneath. Redirecting output to a file or a pipe also disables the TTY
check, which is the most common surprise cause. See Output and
logging for the full model.
You wanted plain lines but got the interactive view
Pass -l/--loquacious, set settings.loquacious: true in
lattice.json, or set CI in the environment — any one forces Raw. This
is also the fastest way to get a readable log when piping lattice run
output into another tool or a CI log viewer.
Colored output shows up somewhere it shouldn’t
Color is only ever emitted in Interactive mode, and only when NO_COLOR
is unset. Set NO_COLOR=1 (any value) to suppress it without changing the
output mode itself.
Running
A persistent task never lets lattice run finish
This is expected, not a bug. Once a run starts a persistent: true task
(a dev server, a watcher), that task is detached and the run waits on a
shutdown signal before it will exit — non-persistent prerequisites still
run to completion and their results are visible first, but the process as
a whole blocks until you send Ctrl-C
(crates/lattice-runner/src/lib.rs:441-461). There’s no flag that makes a
persistent task’s own exit end the run, because the task is defined by
never exiting on its own. See Persistent tasks
and Dev servers and watchers for the intended
day-to-day shape of this — typically running the persistent task on its
own in one terminal, with everything else run separately.
If you want the run to fail fast instead of waiting on a task that will never finish, don’t include the persistent task in the requested task list — run its non-persistent prerequisites on their own.
A --filter runs fewer workspaces than you expected
--filter <pattern> keeps only workspaces whose name contains
pattern — a substring match, not a glob, and not a path match
(crates/lattice/src/commands/run.rs). It’s applied before the graph is
built, so a filtered-out workspace has no node in the graph at all: if
another workspace depends on it, that edge is simply never created — it is
not run first, and it is not an error either. A filter matching nothing
prints lattice: no workspaces matched filter '<pattern>'. and exits 0,
not an error.
Fix: check the filter is matching on workspace name as declared in
lattice.json, not a directory name or a glob pattern. If you need a
dependency’s task to run too, include enough of its name to match, or drop
the filter and use --continue/task selection instead. See Selecting
what runs for the full model, including why
dependencies are never pulled in implicitly.
.lattice/schema.json is missing or your editor shows a stale schema
lattice run, lattice setup, and lattice prune each self-heal a
missing .lattice/schema.json by writing the bundled schema — but only
when the file is absent. An existing file, even an old one, is left alone
on purpose, to avoid churning a file you may have customized or committed
(crates/lattice/src/schema.rs).
Fix: if the file is present but your editor’s JSON Schema validation looks
out of date, delete it and rerun any command that loads the config (lattice run ... --dry-run is enough to trigger the self-heal), or force a full
rewrite with:
lattice init --force
schema.json is meant to be committed — it’s the one thing under
.lattice/ that lattice init does not add to .gitignore.
Clean slate and gathering information
Everything under .lattice/ is derived state:
.lattice/cache/— cached task outputs, keyed by content. Deleting it costs nothing but time: every task re-runs cold on the nextlattice run..lattice/toolchains/— provisioned engines. Deleting it means any engine with aninstallCmdreprovisions from scratch next time it’s needed — a network fetch, not a config change..lattice/bin/— Lattice’s own self-managed versions, if this repo pinslatticeVersionto something other than whatever binary is on yourPATH. Deleting it means the next command re-downloads and re-verifies the pinned release..lattice/schema.json— the only one of these meant to be committed. If it’s gone from disk but still tracked in git, agit statuswill show it as deleted; if it’s genuinely gone, the nextrun/setup/prunerewrites it (see above).
rm -rf .lattice is a complete, safe reset: nothing under it is required
for correctness, only for speed and for not re-provisioning tools you
already have. It never touches lattice.json itself, which lives at the
repo root, not inside .lattice/. You do not need to run lattice init
again afterward — the schema self-heals and the cache and toolchains
rebuild as needed.
Before filing an issue or asking a teammate for help, gather:
lattice version— confirms the exact binary version, useful when a repo pinslatticeVersionand you’re not sure which one actually ran.lattice run <tasks> --dry-run— confirms the graph and every resolved command without running or caching anything.lattice run <tasks> -l— reruns with raw, unbuffered output and cache hash/hit-miss trace lines, useful for both caching questions and for capturing full output to paste into a report.
For the underlying models these symptoms come from, see Driver detection, Engines and provisioning, Caching, Task graph, and Output and logging. For the full message-by-message reference, see Errors.