preflight
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-run to report categories and scoped file lists without classifying
violations). --dry-run is a true preview — every linter reports would-run
and nothing is written, including .preflight-summary.json.
- Read
.preflight-summary.json for the categories run and the violation counts
(passed, deferred, blocking). Written only on a real run, not under
--dry-run. It is a transient scratch artefact, never committed — consumer
repos gitignore it (the initialise-skills skill 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.mjs on the branch-scoped paths, then
re-run preflight. Repeat until introduced violations clear or the user aborts.
(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, matching pnpm lint / CI (which exit 0 on warnings).
Set blockOnWarnings: true to 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.
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 run
node skills/preflight/scripts/lint-fix.mjs and re-run, but it leaves fixes
in the working tree — it never commits, writes a changelog, pushes, or opens
a PR.
- Inside
/send-it the 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.yaml plus each package's
package.json: a workspace is included only if it declares a lint script. 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 to main when 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 (default false) — 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, matching pnpm lint / CI semantics. Set true for
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 set linearTeamName + 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 via
list_projects (debtProject as name, id, or slug). On a miss → do not
call save_issue; fail loudly with the unresolved value.
- On a hit → call
save_issue with team, project (always), a title/description
covering the pre-existing violations, assignee = the maintainer, and a link to
the branch/PR context. Never omit project.
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 — scoped eslint --fix / markdownlint-cli2 --fix on 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).
They have no external npm dependencies (Node built-ins only).
Arguments
$ARGUMENTS
1---2name: preflight3description: Run a change-gated, branch-scoped lint preflight (ESLint / markdownlint / actionlint) on the files a branch changes versus its base, classify each violation as introduced vs pre-existing, and drive the fix/defer loop via an exit-code contract (0 pass, 1 introduced/blocking, 2 pre-existing only). Use when asked to run preflight, check whether a branch will pass lint before pushing, or as the lint gate inside a ship/PR flow. Lints only the categories the branch touched — not a whole-repo lint — with linted workspaces and the base branch auto-detected, so a consuming repo configures nothing in the common case.4license: MIT5---67# preflight89Change-gated, branch-scoped lint preflight. It lints only the categories relevant10to `origin/<base>...HEAD`, on changed paths only — not a whole-repo `pnpm lint` —11then classifies each violation as **introduced** (on a line this branch added or12changed) or **pre-existing** (already there, in a file the branch happens to13touch).1415This skill is the single source of truth for the preflight loop. It is invoked16two ways:1718- **Standalone** (`/preflight`) — a quick "will my branch pass scoped lint?"19 check, leaving any fixes in the working tree.20- **Inside a ship flow** (e.g. `/send-it`) — the lint gate that runs after commits21 and before the changelog/push steps.2223All bundled scripts use only Node built-ins — no `npm install`, no build step.24They operate on the **consumer repo's root** (run them from the repo root, where25`git` resolves the branch diff).2627## Running it28291. Make sure the base branch is up to date: `git fetch origin <base>` (the base is30 auto-detected — see Configuration).312. Run the preflight: `node skills/preflight/scripts/preflight.mjs` (append32 `--dry-run` to report categories and scoped file lists without classifying33 violations). `--dry-run` is a true preview — every linter reports `would-run`34 and nothing is written, including `.preflight-summary.json`.353. Read `.preflight-summary.json` for the categories run and the violation counts36 (`passed`, `deferred`, `blocking`). Written only on a real run, not under37 `--dry-run`. It is a transient scratch artefact, never committed — consumer38 repos gitignore it (the `initialise-skills` skill adds the entry when it39 reconciles a repo).4041The script's exit code drives the loop:4243- **Exit 0 — pass.** No introduced violations and every linter ran cleanly.44 Continue.45- **Exit 1 — introduced violations (blocking).** Run46 `node skills/preflight/scripts/lint-fix.mjs` on the branch-scoped paths, then47 re-run preflight. Repeat until introduced violations clear or the user aborts.48 (Inside a ship flow, commit the fixes; standalone, leave them in the working49 tree for the user to review and commit.) **Only introduced _errors_ block by50 default** — introduced ESLint **warnings** are reported as a non-blocking notice51 and don't fail the gate, matching `pnpm lint` / CI (which exit 0 on warnings).52 Set `blockOnWarnings: true` to gate on them too (see Configuration).53- **Exit 2 — pre-existing violations only.** Show the list and ask the user to54 choose:55 - **Fix now** — apply the fixes, (commit if shipping), re-run preflight.56 - **Defer** — open a debt issue in Linear (see **Debt-issue create** below).57 After creating (or refusing), decide whether to continue or abort.5859Exit 1 can also signal a linter that failed to run (non-zero exit with no60parseable violations) — inspect its stderr; this is blocking too.6162## Categories6364Each category is gated on what the branch changed (mirrors CI path triggers,65narrower scope):6667| Category | Runs when | Skipped when |68| ------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |69| ESLint | Branch diff includes lintable code or eslint/tsconfig config paths | Markdown-only or non-lintable changes |70| markdownlint | Branch diff includes `.md` / `.mdx` (respecting repo ignores) | No markdown changes; warns and skips if the `markdownlint-cli2` binary is missing |71| 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 |7273ESLint runs per workspace (via `pnpm --filter`), plus a root/scripts bucket.74Typecheck, tests, and framework checks (e.g. `astro check`) are **not** part of75preflight — they stay in CI.7677## Standalone vs inside a ship flow7879- **Standalone (`/preflight`)** does the lint preflight and the exit-code loop,80 then **reports**. On introduced violations it may run81 `node skills/preflight/scripts/lint-fix.mjs` and re-run, but it leaves fixes82 **in the working tree** — it never commits, writes a changelog, pushes, or opens83 a PR.84- **Inside `/send-it`** the same loop runs as the lint gate (after commits, before85 changelog work); fixes are committed so the branch is clean before the changelog86 is written. The changelog and its validation are **separate gates owned by the87 ship flow** — they are not part of this skill.8889## Configuration9091The two repo-specific inputs are auto-detected — a consuming repo edits nothing in92the common case:9394- **Linted workspaces** are derived from `pnpm-workspace.yaml` plus each package's95 `package.json`: a workspace is included only if it declares a `lint` script. This96 auto-excludes intentionally-unlinted workspaces and non-package directories97 without a hand-maintained list.98- **Base branch** is detected from `origin/HEAD` (e.g. `main`, `master`,99 `develop`), falling back to `main` when that symbolic ref is absent.100101To override either, add a `preflight.config.json` at the **consumer repo root**102(a [`config.example.json`](config.example.json) ships beside this file as a103template):104105```json106{107 "baseBranch": "main",108 "blockOnWarnings": false,109 "workspaces": {110 "web": { "filter": "@acme/web", "prefix": "apps/web/" }111 },112 "linearTeamName": "",113 "debtProject": ""114}115```116117Any key may be supplied on its own; the others are still auto-detected/defaulted.118Use the override for non-pnpm repos, deliberate exclusions, or nested workspace119globs the detector does not expand.120121- **`blockOnWarnings`** (default `false`) — whether introduced ESLint122 warning-severity findings block the gate. Off by default, preflight blocks only123 on introduced **errors** (and linters that fail to run); introduced warnings are124 surfaced non-blockingly, matching `pnpm lint` / CI semantics. Set `true` for125 repos that want warn-level findings the branch adds to gate as well.126 markdownlint/actionlint findings always block — the warn/error split is127 ESLint-only.128- **`linearTeamName`** / **`debtProject`** — agent-facing keys for the Exit 2129 **Defer** path (the lint scripts ignore them). Both must be non-empty to mint a130 debt issue; see **Debt-issue create**. Empty disables debt-create (tell the131 user to set them, or choose Fix now) — never file with no project.132133### Debt-issue create134135When the user chooses **Defer** on Exit 2, mint a Linear debt issue only when136both `linearTeamName` and `debtProject` are set in the repo-root137`preflight.config.json` (read that file; empty / missing keys mean debt-create138is disabled). Fail closed:1391401. If either key is empty, or the Linear MCP server is unavailable → **do not**141 call `save_issue`. Tell the user to set `linearTeamName` + `debtProject` (or142 choose Fix now). Skip silently only when MCP is unavailable and they still143 want to continue without tracking.1442. Resolve the team by **name** (`linearTeamName`) and the project via145 `list_projects` (`debtProject` as name, id, or slug). On a miss → **do not**146 call `save_issue`; fail loudly with the unresolved value.1473. On a hit → call `save_issue` with `team`, `project` (always), a title/description148 covering the pre-existing violations, assignee = the maintainer, and a link to149 the branch/PR context. Never omit `project`.150151## Implementation152153The bundled scripts live beside this file under154[`scripts/`](scripts/) and are invoked directly with `node` — no `pnpm` aliases,155no `npm install`:156157- `scripts/preflight.mjs` — the change-gated preflight and exit-code contract.158- `scripts/lint-fix.mjs` — scoped `eslint --fix` / `markdownlint-cli2 --fix` on the159 branch-changed paths.160- `scripts/classify-lint.mjs` — parse + classify violations as introduced vs161 pre-existing.162- `scripts/lib/{scope,diff-lines,paths}.mjs` — shared helpers (workspace/base-branch163 detection, diff-line mapping, path normalisation).164165They have no external npm dependencies (Node built-ins only).166167## Arguments168169$ARGUMENTS