Change-gated, branch-scoped lint preflight. It lints only the categories relevant
to origin/<base>...HEAD, on changed paths only — not a whole-repo pnpm lint —
then classifies each violation as introduced (on a line this branch added or
changed) or pre-existing (already there, in a file the branch happens to
touch).
This skill is the single source of truth for the preflight loop. It is invoked two ways:
- Standalone (
/preflight) — a quick "will my branch pass scoped lint?" check, leaving any fixes in the working tree. - Inside a ship flow (e.g.
/send-it) — the lint gate that runs after commits and before the changelog/push steps.
All bundled scripts use only Node built-ins — no npm install, no build step.
They operate on the consumer repo's root (run them from the repo root, where
git resolves the branch diff).
Running it
- Make sure the base branch is up to date:
git fetch origin <base>(the base is auto-detected — see Configuration). - Run the preflight:
node skills/preflight/scripts/preflight.mjs(append--dry-runto report categories and scoped file lists without classifying violations).--dry-runis a true preview — every linter reportswould-runand nothing is written, including.preflight-summary.json. Before anything else, the script checks that the active Node major matches the repo's pin — an exactengines.nodepin (24.x,24.18.0) or, failing that, the.nvmrcmajor — and exits 1 with a switch hint (nvm use/fnm use/mise use) if it does not, so ESLint never runs under the wrong Node. A>=/^/~engines range is not a pin; a repo with neither is unchecked. - Read
.preflight-summary.jsonfor the categories run, the violation counts (passed,deferred,blocking), andlintSurface(see Lint surfaces). Written only on a real run, not under--dry-run. It is a transient scratch artefact, never committed — consumer repos gitignore it (therheged-skills-setupskill adds the entry when it reconciles a repo).
The script's exit code drives the loop:
- Exit 0 — pass. No introduced violations and every linter ran cleanly. Continue.
- Exit 1 — introduced violations (blocking). Run
node skills/preflight/scripts/lint-fix.mjson the branch-scoped paths, then re-run preflight. Repeat until introduced violations clear or the user aborts. Fix the code: never clear a violation by editing lint config or adding an ignore / disable directive. When that is the only fix, stop the loop and report it as a developer decision (see Lint surfaces). (Inside a ship flow, commit the fixes; standalone, leave them in the working tree for the user to review and commit.) Only introduced errors block by default — introduced ESLint warnings are reported as a non-blocking notice and don't fail the gate, matchingpnpm lint/ CI (which exit 0 on warnings). SetblockOnWarnings: trueto gate on them too (see Configuration). - Exit 2 — pre-existing violations only. Show the list and ask the user to
choose:
- Fix now — apply the fixes, (commit if shipping), re-run preflight.
- Defer — open a debt issue in Linear (see Debt-issue create below). After creating (or refusing), decide whether to continue or abort.
Exit 1 can also signal a linter that failed to run (non-zero exit with no parseable violations) — inspect its stderr; this is blocking too — or a Node major mismatch (see step 2): switch Node and re-run, don't treat it as a lint failure.
Lint surfaces are a developer decision
Changing how a linter is configured, or telling it to look away, is the
developer's call, never the agent's. The full rule (the surfaces covered, the
preference order, what to report, and the carve-out for repairing a developer's
own change) is in references/lint-surfaces.md,
shared with triage-pr and commit. In preflight it means:
- In the exit-1 loop, fix the offending code. If the only way to clear an introduced violation is a lint-config edit or an ignore / disable directive, don't make it. Stop and report the four points from the reference (file and rule, the change you would have made, why no code fix was available, the preferred alternative) at the end of the run.
lintSurfacein the summary lists the lint config files the branch changed (configFiles) and the ignore / disable directives it adds (directives, each withfile,line,directiveandtext), with acount. Preflight can't tell who made them, so it is informational and never blocks: the exit code is unchanged. Only changed files that can carry a directive are diffed, and if that scan fails,lintSurface.errorsays so (config files are still listed) instead of the failure stopping preflight. Whencount > 0, list them in the report as developer decisions for the developer to confirm. Don't revert them, and don't make more.
The report is the natural stopping point. Never stop mid-loop to ask about a lint surface.
Categories
Each category is gated on what the branch changed (mirrors CI path triggers, narrower scope):
| Category | Runs when | Skipped when |
|---|---|---|
| ESLint | Branch diff includes lintable code or eslint/tsconfig config paths | Markdown-only or non-lintable changes |
| markdownlint | Branch diff includes .md / .mdx (respecting repo ignores) |
No markdown changes; warns and skips if the markdownlint-cli2 binary is missing |
| actionlint | Branch diff includes .github/workflows/*.yml or .github/actionlint.yaml |
No workflow changes; config-only changes lint all tracked workflows; warns and skips if actionlint binary missing |
ESLint runs per workspace (via pnpm --filter), plus a root/scripts bucket.
Typecheck, tests, and framework checks (e.g. astro check) are not part of
preflight — they stay in CI.
Standalone vs inside a ship flow
- Standalone (
/preflight) does the lint preflight and the exit-code loop, then reports. On introduced violations it may runnode skills/preflight/scripts/lint-fix.mjsand re-run, but it leaves fixes in the working tree — it never commits, writes a changelog, pushes, or opens a PR. - Inside
/send-itthe same loop runs as the lint gate (after commits, before changelog work); fixes are committed so the branch is clean before the changelog is written. The changelog and its validation are separate gates owned by the ship flow — they are not part of this skill.
Configuration
The two repo-specific inputs are auto-detected — a consuming repo edits nothing in the common case:
- Linted workspaces are derived from
pnpm-workspace.yamlplus each package'spackage.json: a workspace is included only if it declares alintscript. This auto-excludes intentionally-unlinted workspaces and non-package directories without a hand-maintained list. - Base branch is detected from
origin/HEAD(e.g.main,master,develop), falling back tomainwhen that symbolic ref is absent.
To override either, add a preflight.config.json at the consumer repo root
(a config.example.json ships beside this file as a
template):
{
"baseBranch": "main",
"blockOnWarnings": false,
"workspaces": {
"web": { "filter": "@acme/web", "prefix": "apps/web/" }
},
"linearTeamName": "",
"debtProject": ""
}
Any key may be supplied on its own; the others are still auto-detected/defaulted. Use the override for non-pnpm repos, deliberate exclusions, or nested workspace globs the detector does not expand.
blockOnWarnings(defaultfalse) — whether introduced ESLint warning-severity findings block the gate. Off by default, preflight blocks only on introduced errors (and linters that fail to run); introduced warnings are surfaced non-blockingly, matchingpnpm lint/ CI semantics. Settruefor repos that want warn-level findings the branch adds to gate as well. markdownlint/actionlint findings always block — the warn/error split is ESLint-only.linearTeamName/debtProject— agent-facing keys for the Exit 2 Defer path (the lint scripts ignore them). Both must be non-empty to mint a debt issue; see Debt-issue create. Empty disables debt-create (tell the user to set them, or choose Fix now) — never file with no project.
Debt-issue create
When the user chooses Defer on Exit 2, mint a Linear debt issue only when
both linearTeamName and debtProject are set in the repo-root
preflight.config.json (read that file; empty / missing keys mean debt-create
is disabled). Fail closed:
- If either key is empty, or the Linear MCP server is unavailable → do not
call
save_issue. Tell the user to setlinearTeamName+debtProject(or choose Fix now). Skip silently only when MCP is unavailable and they still want to continue without tracking. - Resolve the team by name (
linearTeamName) and the project vialist_projects(debtProjectas name, id, or slug). On a miss → do not callsave_issue; fail loudly with the unresolved value. - On a hit → call
save_issuewithteam,project(always), a title/description covering the pre-existing violations, assignee = the maintainer, and a link to the branch/PR context. Never omitproject.
Implementation
The bundled scripts live beside this file under
scripts/ and are invoked directly with node — no pnpm aliases,
no npm install:
scripts/preflight.mjs— the change-gated preflight and exit-code contract.scripts/lint-fix.mjs— scopedeslint --fix/markdownlint-cli2 --fixon the branch-changed paths.scripts/classify-lint.mjs— parse + classify violations as introduced vs pre-existing.scripts/lib/{scope,diff-lines,paths}.mjs— shared helpers (workspace/base-branch detection, diff-line mapping, path normalisation).scripts/lib/lint-surface.mjs— thelintSurfacedetector: changed lint config files and added ignore / disable directives (R-1203).references/lint-surfaces.md— the shared lint-surface rule, vendored from the repo'slib/lint-surfaces.md(ADR-0006). Edit the source, not the copy.
They have no external npm dependencies (Node built-ins only).
Arguments
$ARGUMENTS