Skip to content

Fail the lint check on the warnings it reports - #120

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
NathanTarbert:fix/issue-75-lint-gate
Aug 21, 2026
Merged

Fail the lint check on the warnings it reports#120
davidmckayv merged 2 commits into
CopilotKit:mainfrom
NathanTarbert:fix/issue-75-lint-gate

Conversation

@NathanTarbert

@NathanTarbert NathanTarbert commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

What this changes

bun run lint now fails when biome has something to report, instead of printing it and exiting successfully.

Fixes #75. Thanks @Hotragn — the exit-code table and, more than that, tying it back to #63 is what makes this worth doing rather than a tidy-up. That was a function recursing into its own default parameter, biome saw it as an unused import, and the check went green over it.

Measured on main at f1701d8, unchanged from what the issue reported:

$ bunx biome lint .
Found 25 warnings.
$ echo $?
0

$ bunx biome lint --error-on-warnings . ; echo $?
1

ci.yml:37 runs bun run lint, so the flag lives in the script rather than in the workflow.

Clearing the 25 first

The flag is one word. The work is that turning it on with warnings standing would fail main immediately, so all 25 had to go somewhere honest first. Three groups, handled differently on purpose:

Two are real, and are fixed. server/tests/skill-ownership.integration.test.ts had an unused eq import and a let that is never reassigned. Neither is suppressed.

Eight are false positives, and are suppressed individually. noTemplateCurlyInString fires on tests asserting literal ${...} text — tests/compose.test.ts:13 checks that docker-compose.yml contains ${POSTGRES_PORT:-5432}, and server/tests/tenant-package.test.ts checks placeholder expansion by feeding in unexpanded placeholders. A template literal there would break the test, which is the opposite of what the rule is warning about. Each carries that reason on the line above, as the issue suggested.

Fifteen are noNonNullAssertion, all in tests, on fixtures the assertion directly above has just proved exist (expect(atlassian).not.toBeNull() then hostAdmissible(atlassian!, …)). Rather than fifteen identical suppressions, biome.json scopes that one rule off for test files. It still applies everywhere else.

That last one is the judgement call in this PR, and it is the one to push back on if you disagree. The alternative is rewriting all fifteen call sites to narrow properly, which is churn in tests that are currently clear, and one of them (plugin-catalogue.test.ts:94) is at module scope and would need restructuring. Happy to do it that way instead if you would rather the rule applied everywhere with no exceptions.

Where it runs

  • New state that outlives a request? None. This is a lint script and a lint config.
  • What happens on the second replica? Nothing differs. No runtime code is touched.
  • Anything serialised? N/A, no writes.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

  • N/A. No gateway, policy, or audit path touched.

Changelog

No line. A deployment behaves identically afterwards — this changes what fails for contributors, not what runs for operators.

Proof

The point of this PR is that a check starts failing, so the evidence is that it does. Three mutations, each applied to a clean branch, bun run lint run, then reverted:

mutation before this PR after
unused import in server/src/audit.ts exit 0 exit 1, lint/correctness/noUnusedImports
unused import in server/tests/audit.test.ts exit 0 exit 1 — the override is scoped to one rule and does not hide correctness in tests
non-null assertion in server/src/audit.ts exit 0 exit 1, lint/style/noNonNullAssertion — the override does not leak out of test files

The first is the #63 shape exactly, and it is the one that matters.

Rest of the suite:

  • bun run lint0 warnings, exit 0, where main reports 25 warnings and also exit 0.
  • bun run format:check — clean, 385 files.
  • bun run typecheck — clean across app, server and worker.
  • bun run build — clean.
  • bun test916 pass, 8 skip, 114 fail, 1038 tests across 105 files, identical to unmodified main at f1701d8 on this machine. The 114 are the database integration tests; there is no local Postgres here and they fail the same way on a clean main, which is what the CI tests job runs pgvector for.

This branch carries a merge of main up to f1701d8, so the numbers above are measured against #117 and #118 rather than the base it was first cut from.

Not in this PR

The two agent-computer and supervisor packages that typecheck never reaches, and the six lockfiles installed without --frozen-lockfile, are #112, which @Hotragn has said they are already working on.

biome exits 0 when it has only warnings to report, so ci.yml's static job ran
the linter, printed its findings and passed. CopilotKit#63 is what that costs: an unused
import biome flagged as a warning was a function recursing into its own default
parameter, and the check went green over it.

The script now passes --error-on-warnings. Clearing the 25 warnings standing on
main to get there:

- server/tests/skill-ownership.integration.test.ts had an unused eq import and a
  let that is never reassigned. Both fixed rather than suppressed.
- Eight noTemplateCurlyInString are false positives in tests that assert on
  literal ${...} text, where an expanded template would break the test.
  Suppressed individually with that reason.
- Fifteen noNonNullAssertion are all in tests, on fixtures the assertion above
  them has just proved exist. Scoped off for test files in biome.json rather
  than suppressed fifteen times; the rule still applies to everything else.

Verified by mutation: an unused import in server/src, an unused import in a
test, and a non-null assertion in server/src each fail the check now, so the
override is narrower than the rule it scopes.
@davidmckayv
davidmckayv merged commit e9ac71f into CopilotKit:main Aug 21, 2026
6 checks passed
davidmckayv added a commit that referenced this pull request Aug 21, 2026
The lint gate now fails on warnings (#120), and roster[0]! tripped
noNonNullAssertion. Guard on the resolved value instead: find the first public
coworker or fall back to the first, and 409 when there is none, which also reads
straighter than asserting a length check the linter cannot see.
davidmckayv added a commit that referenced this pull request Aug 21, 2026
* Send an untagged message to the coworker it is for

Typing without naming anyone reached the default coworker; a specialist needed
an @. That makes the person the router, which is the friction that reads as
dated next to assistants that just take what you say and act. Now an untagged
message is routed to the coworker whose purpose matches it.

A channel is pinned to one coworker before its first turn, so the choice happens
at the one seam where an untagged message picks a coworker, before the channel is
created. A new POST /api/route reads the roster for the person asking (so it can
only ever pick a coworker they may already reach), asks the deployment's own
model to choose against each coworker's own description, validates the answer is
a coworker on that roster, records a channel.routed row, and returns it. The
model call is the deployment's existing model and key, not a second thing to
configure.

Named, not silent, which the composer already believed: the channel header is
the coworker it went to, and the audit row carries the reason and the candidates
but never the message, which the payload redaction would drop anyway.

@ is unchanged and wins: an addressed message skips routing entirely, no model
call, no inference recorded. And every uncertain path lands on the same default
the composer always used and says so rather than misroute or drop: no roster,
one coworker, model unreachable, unparseable answer, an id not on the roster, or
low confidence. The classifier is a pure function with the model call injected,
so those failure paths are a plain test rather than a mock of a network.

* Pick the default coworker without a non-null assertion

The lint gate now fails on warnings (#120), and roster[0]! tripped
noNonNullAssertion. Guard on the resolved value instead: find the first public
coworker or fall back to the first, and 409 when there is none, which also reads
straighter than asserting a length check the linter cannot see.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The lint check reports its findings and exits successfully, which is how #63 got through

2 participants