run-nx-checks
Run nx format, lint, test, and build on affected or specified projects, then fix unambiguous failures
Files4
View on GitHubSKILL.mdskills/run-nx-checks/SKILL.md
GitHubRun Nx Checks
Run format, lint, typecheck, test, build. Fix unambiguous failures. Report anything judgment-laden and every file you changed.
Arguments
$ARGUMENTS — optional, space-separated: [cpuCount] [projectName] [--remote-cache]. Number
token → cpuCount. Non-number, non-flag token → projectName. --remote-cache flag → opt back
into the remote cache by dropping the remote-cache-off prefix. Default cpuCount = cores - 4
(min 1); count cores with node -p 'os.cpus().length'.
Default project scope = affected. Default remote-cache state = off (see Steps).
$base, $cpuCount, $projectName and $snap in the commands below are placeholders:
substitute their values (a fresh shell per Bash tool call holds none of them).
Workspace
Run nx commands from wherever nx.json lives in this repo (commonly the root; in some repos a
subdirectory).
Setup (before step 1)
Keep the Nx daemon warm so the project graph is reused across targets:
NX_DAEMON=true npx nx daemon --start >/dev/null 2>&1 || true
Snapshot the dirty files, so the final report can name every file the run changes (hash-object
without -w writes nothing); substitute the printed path for $snap there:
snap=$(mktemp -d "${TMPDIR:-/tmp}/nx-checks.XXXXXX") && git ls-files -z -mo --exclude-standard | while IFS= read -r -d '' f; do echo "$(git hash-object "$f" 2>/dev/null) $f"; done > "$snap/before.txt" && echo "snap=$snap"
Affected base
nx affected and format:write diff against the local default branch, which is often stale or
missing. $projectName replaces only the affected commands, so resolve the base in both modes.
NX_BASE set: skip this section and omit --base from every command below; Nx reads it and a
deliberate env setting wins. Otherwise resolve the remote-tracking ref once and substitute the
printed ref for $base in every affected, format:write and show projects --affected command
below:
base=$(node -p 'const j=require("./nx.json"); j.defaultBase ?? j.affected?.defaultBase ?? "main"' 2>/dev/null || echo main)
base=${base#remotes/}; [[ $base == */* ]] || base="origin/$base"
GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=15' git fetch --quiet "${base%%/*}" "${base#*/}" || echo "fetch failed: base may be stale"
git rev-parse --verify --quiet "$base" >/dev/null && echo "base=$base" || echo "no such ref: omit --base"
- Omit
--basewhen the ref is missing (Nx falls back todefaultBase). - Never pass
--head. With both flags Nx diffs two commits and ignores uncommitted and untracked files, so an agent's unstaged edits vanish and every check passes vacuously. - A failed fetch is not a failure: continue on the current ref, flag "base may be stale" in the final report, and never retry outside the sandbox for it.
Why: affected-base-rationale.md.
Fix rule
- Apply only mechanical/unambiguous fixes: lint auto-fix output, missing imports/types, obvious type
errors, test expectations that mirror a clear code change. An assertion or snapshot update is
mechanical only when the failing expectation names a value the working-tree diff changed on
purpose; edit that expectation by hand. Never pass
-u/--updateSnapshotto a target. - Never guess on anything judgment-laden: test failure that could be a real bug vs. an outdated assertion, errors pointing at unrelated areas, pre-existing failures unrelated to recent work, anything where multiple plausible fixes exist. Running forked, you can't ask mid-run: leave such failures unfixed and carry them, with your analysis, into the final report.
- Keep changes minimal and scoped to the failure. No drive-by refactors.
- After fixing, re-run the same step command; the local cache replays the untouched projects. Repeat until clean or only judgment-laden failures remain.
Affected scope — sanity-check before step 1
Affected runs only (no $projectName); they can balloon. Before step 1, check scope with the
graph-only npx nx show projects --affected --base=$base (fast, no build):
- Sandbox
.envfalse positives. The sandbox denies reading**/.env;nx affectedhashes changed files, so an unreadable.envreads as changed and marks its project affected. Repos with.env-bearing apps (e.g.apps/*-api) thus drag those into every affected run and build them for nothing. Tell-tale: rungit status; it prints<path>/.env: Operation not permittedfor exactly those projects. Fix: add--exclude=<their project names>to everyaffectedcommand (format:writetakes none) and list them as excluded in the final report, naming any real edits they hold. Never re-run them outside the sandbox yourself; the user can, to include them. - Large fan-out. Large = more than half of
npx nx show projects | wc -lor more than 20 projects, whichever is lower, counting affected projects after the.envexclusions (e.g. a widely-shared lib change rippling across the workspace). Don't build the world unprompted: skip steps 3–5 and report the project count and list in the final report so the user can re-run with$projectName.
Steps
Scope is mandatory — never narrow it yourself. With no $projectName argument, every target
runs at full scope — nx affected for lint/typecheck/test/build, whole-changeset format:write.
The single-project forms in the steps below apply only when the user explicitly passed
$projectName. Never substitute nx run <proj>:<target>, nx <target> <changedLib>, or
file-scoped format --files for the affected sweep: that skips every other affected project —
exactly where a shared-lib change regresses (a dependent project whose tests import the changed
lib).
Always prefix each task-running nx command (steps 1–5 and the flaky retry; daemon and
show projects run no tasks) with the remote-cache-off env vars inline:
NX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache.
NX_SKIP_REMOTE_CACHE turns off every remote cache on Nx 20.4 and later; the other two cover
earlier versions and Powerpack. Unknown vars are no-ops, so the prefix is safe whatever
remote-cache backend (if any) the repo uses.
The invariant: remote cache always off (unless the user passed --remote-cache), local cache
always on — always disable, never autodetect (a passing shell-side auth check doesn't predict
the in-process credential chain). Why: remote-cache-rationale.md.
If the user explicitly passed --remote-cache, drop the prefix. Expect pipeline hits on build
(same --configuration) and typecheck; on test only in the single-project form and the flaky
retry (the affected run's --maxWorkers=1 is a hashed override); never on lint (--fix) — keep
the flags regardless.
Always keep the local cache on. The env vars above disable only the remote read-through
cache; the local Nx cache must stay enabled so unchanged targets are replayed instead of re-run.
Do not add --skip-nx-cache (nor NX_SKIP_NX_CACHE=true) to any command — it bypasses the
local cache too, making every run slower for no benefit. The only valid reason to pass
--skip-nx-cache is a specific, stated need to bypass the local cache — e.g. investigating a
failure you suspect is caused by a stale cache entry. In that case, scope it to the single command
under investigation and say why; never use it as the default.
Write the env vars literally at the start of each such command, exactly as in the steps below. Do
not rely on export — each Bash tool call is a fresh shell, so exports do not carry across
calls (true for any nx env var). Never stash the prefix in a shell variable
(NX_OFF="…"; $NX_OFF npx nx … fails with "command not found": expanded variables are not parsed
as assignments).
- Lint —
NX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache npx nx affected -t lint --base=$base --parallel=$cpuCount --fix(orNX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache npx nx lint $projectName --fix). - Format —
NX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache npx nx format:write --base=$base(both modes). - Typecheck —
NX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache npx nx affected -t typecheck --base=$base --parallel=$cpuCount(orNX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache npx nx typecheck $projectName --parallel=$cpuCount). Without the target,affectedruns nothing and exits 0 and the single-project form errors with "cannot find configuration": report typecheck as n/a, never as failing. - Test —
NX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache npx nx affected -t test --base=$base --parallel=$cpuCount --maxWorkers=1(orNX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache npx nx test $projectName). - Build —
NX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache npx nx affected -t build --base=$base --configuration=production --parallel=$cpuCount(orNX_SKIP_REMOTE_CACHE=true NX_NO_CLOUD=true NX_POWERPACK_CACHE_MODE=no-cache npx nx build $projectName --configuration=production --parallel=$cpuCount).
Apply the fix rule on any failure in steps 1–5 (a test failure: retry once first, see Flaky tests). A failure left unfixed never stops the run: carry on with the next step.
Flags per target
Why --configuration=production and --maxWorkers=1: flags-rationale.md.
--maxWorkers=1on the test step only. Forwarding it to lint (ESLint) or build (esbuild) fails as an unknown option.- Affected/run-many only, not single-project. With one project there's no project-level fan-out, so capping to one worker just serializes it — let a single project use all cores.
- Same reason,
--parallelis dropped from single-project lint and test (one task each — no fan-out). It stays on single-project typecheck and build, because both fan out across the whole dependency chain:buildhasdependsOn: ["^build"]and the inferredtypecheckdependsOn: ["^typecheck"].
Flaky tests — retry once to classify
A failed test target may be flaky, not a real break. On a test failure, re-run that one target
once in step 4's single-project form, prefixed like the steps (… npx nx test <project>). If it
then passes (or, with Nx Cloud on, Nx prints NX detected a flaky task), it's flaky — don't try
to "fix" it; record it as a flaky project:target for the report. If it fails the same way again,
treat it as real under the Fix rule.
Final report (mandatory)
This skill runs in a forked context, so its final message is the only channel back to the caller. End with:
- the affected base used: the resolved ref,
NX_BASEand its value, or omitted because the ref is missing; flag a failed fetch as possibly stale; - per target (lint / format / typecheck / test / build): clean, n/a, skipped (with the reason —
for large fan-out, the project count and list; for
.envfalse positives, the excluded projects), or each failingproject:targetwith its cause, plus any flakyproject:target; - every file the run changed: hand-fixed (from your own edits — file plus one line on what and
why) and tool-applied (lint
--fix,format:write— every other file that differs or is new when the Setup snapshot'sgit ls-filespipeline, not itsmktemp, is re-run into$snap/after.txtand the two files are diffed; thenrm -rf "$snap": a finished run leaves nothing in TMPDIR).
Never report a bare "checks passed": an unlisted failure, skipped target or unmentioned edit reads as green and the caller can't act on it.