Skip to content

feat(fork): fail CI on a lint warning in fork-owned code - #19

Merged
NoahHendrickson merged 2 commits into
customfrom
fork/lint-owned-guard
Jul 27, 2026
Merged

feat(fork): fail CI on a lint warning in fork-owned code#19
NoahHendrickson merged 2 commits into
customfrom
fork/lint-owned-guard

Conversation

@NoahHendrickson

Copy link
Copy Markdown
Owner

Implements the guard specified in #17. Closes the gap that review identified: three PRs touched the same nine dead imports because nothing fails on a lint warningvp check lints everything and exits 0, and no --max-warnings was set anywhere.

What it does

.fork/lint-owned.mjs lints every fork-owned file and fails on the first warning. Wired as a fenced step in CI's check job, registered as fork-lint-cleanliness in the manifest, with a guard test.

Scoped to fork-owned paths, not repo-wide — the decision §3 of the handoff argues for. A repo-wide --max-warnings 12 ratchets against upstream: the first sync landing an upstream warning goes red for code the fork can't fix, and the only response is raising the number. The 12 upstream warnings today stay untouched and can grow freely.

§8 — I took a fourth option

The handoff offered three (guard test shells out / CI-only / text-assert the wiring) and recommended the third. I split the concern instead:

  • The guard test asserts path selection and CI wiring — selection is the half that fails quietly, because a gate aimed at the wrong paths still exits 0, so "green" would mean "inspected nothing"
  • CI runs the actual lint — 30 files is a multi-second subprocess; this suite does 1652 tests in ~10s

The recommended option pins wiring but never tests selection, which is the part most likely to silently rot. This tests the risky logic cheaply and leaves the expensive check where expensive checks already live.

§7 unknowns — resolved

§7.3 (lint.ignorePatterns interaction) was the important one, and the hazard is real. Measured:

$ vp lint apps/web/src/routeTree.gen.ts --format json     # matches ignorePatterns
No files found to lint. Please check your paths and ignore patterns.
{ "diagnostics": [], "number_of_files": 0, ... }

An explicitly-passed ignored path is silently skipped. Skip all paths and oxlint exits non-zero on its own — but skip some and the rest lint clean and the gate passes while covering less than it claims.

So the gate compares oxlint's number_of_files against the count it handed in. That check is what makes this trustworthy rather than decorative.

§7.2 (dedup / overlap): measured — dropping the dedup takes 29 paths to 39, and oxlint counts each occurrence rather than collapsing. So duplicates don't produce a false green, they just duplicate work and overstate coverage. I'd written the opposite in a code comment before measuring; corrected.

Proof it fails — caused, not asserted

Per §5 step 6.

Clean:

fork-lint: 30 fork-owned files, no warnings.        exit=0

1. Unused import planted in a fork-owned file:

fork-lint: 1 warning(s) in fork-owned code. The fork owns these files, so there is no upstream to wait for — fix them.

  apps/web/src/custom/forkMarker.ts:1:10  eslint(no-unused-vars)  Identifier 'CopyIcon' is imported but never used.
exit=1

2. Fork path added to lint.ignorePatterns:

fork-lint: expected to lint 30 fork-owned files, oxlint reported 20. Some path was skipped — most likely it now matches lint.ignorePatterns in vite.config.ts. Skipped paths:

  - apps/web/src/custom/SidebarHeaderBackdrop.tsx
  - apps/web/src/custom/SidebarStageDitherArt.tsx
  … (10 total)
exit=1

3. CI step deletedkeeps the gate wired into CI fails.
4. Dedup removednever hands the same path over twice fails.

All four reverted; final state clean.

One thing the gate caught in itself

The first version filtered to .ts/.tsx, so .fork/lint-owned.mjs wasn't in its own scope — and the repo-wide lint then flagged a t3code(namespace-node-imports) violation in it that the gate had passed over. Added .mjs so the fork's own tooling is held to the standard it enforces, with a test pinning it.

Verification

Check Result
node .fork/lint-owned.mjs 30 files, no warnings, exit 0
apps/web tests 189 files, 1652 tests, pass
Guard tests (18, incl. 6 new) pass
Manifest parses 15 entries
Drift detector on vite.config.ts + ci.yml flags fork-lint-cleanliness
typecheck exit 0
lint exit 0 — 0 errors, 12 warnings (unchanged, all upstream's)
fmt --check clean

Not verified

This has never run in an Actions runner — §7.1, still open until this PR's own CI proves it. That's the one remaining unknown, and this PR is the experiment.


🤖 Generated with Claude Code

Nothing in this repository gated on a lint warning. `vp check` lints
everything and exits 0, and no --max-warnings was set anywhere, so warnings in
fork-authored files accumulated in silence — nine dead imports survived three
pull requests that way. Removing them was never the difficulty; noticing was.
The guard apparatus exists to catch "the fork drifted and nothing noticed",
and this was that failure one level up, in the one place the apparatus did not
look.

Scoped to fork-owned paths deliberately, not a repo-wide --max-warnings count.
A repo-wide count ratchets against upstream: the first sync landing an
upstream warning turns the build red for code the fork cannot fix, and the
only response is to raise the number, which is how a ratchet stops meaning
anything. Scoped, the twelve upstream warnings present today stay untouched
and can grow without ever turning this red.

Scope is the three fork-owned directories plus every lintable files: entry in
the manifest, deduplicated and filtered by extension. .mjs is included so the
fork's own tooling is held to the standard it enforces: a .ts-only filter let
the repo-wide lint catch a namespace-node-imports violation in lint-owned.mjs
that the gate itself had passed over.

The gate compares oxlint's number_of_files against the count handed in, which
is what keeps it honest. An explicitly-passed path matching lint.ignorePatterns
is skipped silently — measured, not assumed: oxlint reports number_of_files 0
for one. Skipping every path exits non-zero on its own, but skipping some would
leave the rest linting clean and the run passing while covering less than it
claimed.

The guard test does not run the lint. It asserts path selection and the CI
wiring, because selection is the half that fails quietly — a gate aimed at the
wrong paths still exits 0 — while linting 30 files is a multi-second
subprocess that does not belong in a suite finishing 1650 tests in ten seconds.

Every failure mode was proven by causing it rather than asserted: a planted
unused import fails with its location, a fork path added to lint.ignorePatterns
fails and names the ten files it would have skipped, deleting the CI step fails
the wiring assertion, and removing the dedup fails its own. Transcripts in the
pull request.
@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Jul 27, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thermo-nuclear review: request changes.

The fork placement, scoped gate, and number_of_files honesty check are the right shape — walker + collector earn their keep for out-of-tree manifest files and silent ignorePatterns skips. What does not is a second lint-result interpreter on top of --max-warnings 0, an N+1 skip probe on the failure path, and a cast-only test wrapper. Collapse to one control plane and delete the rest.

Also: manifest intent still says filtered to .ts/.tsx while the gate includes .mjs, and the script header points at .fork/notes/FORK-LINT-GUARD-HANDOFF.md, which is not in the tree.

Open in Web View Automation 

Sent by Cursor Automation: Thermo-nuclear PR review

Comment thread .fork/lint-owned.mjs
Comment on lines +117 to +176
const result = NodeChildProcess.spawnSync(
"vp",
["lint", ...files, "--format", "json", "--max-warnings", "0"],
{
cwd: REPO_ROOT,
encoding: "utf8",
},
);

if (result.error) {
console.error(`fork-lint: could not run vp lint — ${result.error.message}`);
process.exit(2);
}

const report = parseReport(result.stdout);
if (report === undefined) {
console.error("fork-lint: could not parse oxlint JSON. Raw output follows.\n");
console.error(result.stdout || result.stderr);
process.exit(2);
}

// The check that keeps this honest. An explicitly-passed path that matches
// `lint.ignorePatterns` is silently skipped — verified: oxlint reports
// number_of_files 0 for one. Skip every path and it exits non-zero on its
// own, but skip *some* and the rest lint clean and this would pass while
// looking at less than it claims. Compare counts so that cannot happen.
if (report.number_of_files !== files.length) {
console.error(
`fork-lint: expected to lint ${files.length} fork-owned files, oxlint reported ` +
`${report.number_of_files}. Some path was skipped — most likely it now matches ` +
`lint.ignorePatterns in vite.config.ts. Skipped paths:\n`,
);
for (const file of files) {
const single = NodeChildProcess.spawnSync("vp", ["lint", file, "--format", "json"], {
cwd: REPO_ROOT,
encoding: "utf8",
});
const singleReport = parseReport(single.stdout);
if (singleReport === undefined || singleReport.number_of_files === 0) {
console.error(` - ${file}`);
}
}
process.exit(1);
}

const diagnostics = report.diagnostics ?? [];
if (diagnostics.length > 0) {
console.error(
`fork-lint: ${diagnostics.length} warning(s) in fork-owned code. The fork owns these ` +
`files, so there is no upstream to wait for — fix them.\n`,
);
for (const diagnostic of diagnostics) {
const span = diagnostic.labels?.[0]?.span;
const where = span
? `${diagnostic.filename}:${span.line}:${span.column}`
: diagnostic.filename;
console.error(` ${where} ${diagnostic.code} ${diagnostic.message}`);
}
process.exit(1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think there's a code-judo move here that makes this much simpler. can we reframe this so these branches disappear?

you pass --max-warnings 0, then never read result.status, and re-derive failure from diagnostics.length plus a hand-rolled location formatter. that is two control planes for one contract — the flag is dead for control flow, and a non-zero exit with empty/missing diagnostics (config blow-up, odd oxlint failure mode) greens while claiming clean.

pick one plane:

  1. trust the flag — after the number_of_files honesty check, process.exit(result.status ?? 1) (print stdout/stderr or re-run without --format json for humans). delete the diagnostics loop.
  2. trust the JSON — drop --max-warnings 0 and keep the diagnostics path, but then also fail on result.status !== 0 so spawn/tool failures cannot slip past an empty report.

same cut for the skip diagnosis above: N sequential spawnSync calls to rediscover what the count mismatch already proved. print the two counts, point at lint.ignorePatterns, exit. the walker + collector + honesty compare stay — those are load-bearing. the second interpreter and the N+1 probe are not.

Comment on lines +25 to +26
const selectForkOwnedFiles = (manifest: string, root: string): readonly string[] =>
collectForkOwnedFiles(manifest, root) as readonly string[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this abstraction seems unnecessary. can we just keep the direct flow?

selectForkOwnedFiles exists only to cast the untyped .mjs return. sibling customizationsManifest.test.ts casts once at the import/use site (as ManifestEntry[]) and calls through — no identity wrapper. either cast at each call, or once into a local (const collect = collectForkOwnedFiles as ...), and delete the pass-through.

Comment thread .fork/customizations.yaml Outdated
Comment on lines +586 to +589
Scope is the three fork-owned directories plus every lintable files: entry
in this manifest, deduplicated — most of those entries already sit under
one of the directories — and filtered to .ts/.tsx, because files: also
lists images, CSS, YAML and shell. The gate compares oxlint's

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intent says "filtered to .ts/.tsx" but lint-owned.mjs includes .mjs (and the guard asserts that). the script header also points at .fork/notes/FORK-LINT-GUARD-HANDOFF.md, which is not in the tree. fix the intent to match the gate, and either land the note or point at #fork-lint-cleanliness like every other guard.

Copy link
Copy Markdown
Owner Author

Review

The engineering discipline here is real — the §7.3 investigation into lint.ignorePatterns is the best thing in the PR, and the number_of_files comparison is the right instinct acted on properly. The self-catch on .mjs is a good result honestly reported.

But I think the central claim doesn't hold, and it's worth being blunt about it before this lands.


1. The gate would not have caught the bug it was built for

The PR opens with: "Closes the gap that review identified: three PRs touched the same nine dead imports."

All nine of those imports were in apps/web/src/components/SidebarV2.tsx (a517955):

--- a/apps/web/src/components/SidebarV2.tsx
-  EllipsisIcon,
-import { CommandDialogTrigger } from "./ui/command";
-import { Kbd } from "./ui/kbd";
-import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";
-import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar";

That path is not in the gate's scope. It isn't under any of the three FORK_OWNED_DIRECTORIES, and it appears in the manifest only under watch: (lines 295 and 415) — never under files:, which is the only key collectForkOwnedFiles reads. Verified against the manifest as it stood at a517955^, i.e. while the nine imports were live:

under files: -> NONE
under watch: -> [ 'sidebar-v2-card-rows', 'fork-sidebar-chrome' ]

And against the tree today:

SidebarV2.tsx selected: false      (30 files selected, none of them it)

So run this gate on the pre-#16 tree and it prints 30 fork-owned files, no warnings and exits 0, with 21 warnings in the repo and nine of them in fork-authored lines. The gate is real, it works, and it is aimed away from where the bug was.

I want to be clear that the §3 scoping decision is correct — I'm not arguing for a repo-wide --max-warnings, and the ratchet argument against it is right. The problem is the consequence §3 doesn't state: the fork's largest authored surface is fenced hunks inside upstream-path files — 24 fork-fenced .ts/.tsx files sit outside the three directories — and a file-level scope has no way to express "the fork owns these lines but not this file." That's where the motivating bug lived, and it's where the next one will.

That doesn't sink the PR, but it does mean the summary line is wrong. Either extend scope to an explicit list of fenced files whose warnings the fork accepts ownership of, or say plainly in the manifest intent that fenced hunks in upstream files remain uncovered and the original failure mode is still open. What shouldn't ship is a manifest entry asserting the gap is closed when the specific nine imports that motivated it would still slip through.

2. apps/web/fork/ is fork-authored and entirely uncovered

apps/web/fork/overrideResolver.ts
apps/web/fork/overrideResolver.test.ts
apps/web/fork/vitePluginForkOverrides.ts

The fork's own override machinery. In neither the directory list nor any manifest files: entry — grep "apps/web/fork" .fork/customizations.yaml returns nothing, and the selector picks up 0 files from it. If anything in this repo is unambiguously fork-owned, it's the code implementing the fork's override system.

3. The comment about detect-drift.mjs is false

lint-owned.mjs:39-44:

.mjs is included so the fork's own tooling — this file and detect-drift.mjs — is held to the standard it enforces on everything else

detect-drift.mjs selected: false

It's under no fork-owned directory, and it appears in customizations.yaml only inside a header comment on line 4 — which parseCustomizations skips by design. The guard test named covers the fork's own tooling asserts files contains .fork/lint-owned.mjs and stops there, so it certifies half the claim as if it were the whole one.

The thread running through 1–3: the gate's scope is a hand-maintained list with nothing reconciling it against the tree. Three fork-owned surfaces are missing from it on day one. That is the same failure this PR exists to fix — "the fork drifted and nothing noticed" — relocated one level up into the gate's own configuration. Worth a reconciliation assertion: every git-tracked .ts/.tsx/.mjs under .fork/ and apps/web/fork/ must appear in the selection, so adding fork tooling without adding it to scope fails loudly.

4. The CI-wiring assertion is weaker than the guard it cites as its model

expect(ci).toContain("node .fork/lint-owned.mjs");

This passes if the step is commented out, given if: false, moved into a job that doesn't run on pull_request, or relocated below a step that always fails. It asserts a string exists in a file, not that a step runs.

The header says it uses "the same shape ciOnCustom uses." It doesn't — ciOnCustom anchors structurally:

expect(ci).toMatch(/push:\s*\n\s+branches:\s*\n(?:.*\n)*?\s+- custom/u);

That one ties the value to its position in the tree. This one is unanchored. Given #18 — same author, same day — is specifically about a guard whose .trim() made it unable to fail, this is that class of assertion again. Anchor it to the check: job's steps: and assert the step carries no if:.

5. result.status is never read

The verdict comes entirely from the parsed JSON; the child process's exit code is discarded. --max-warnings 0 is passed on line 119 but changes nothing in the script's logic, which reads as though the flag is load-bearing when it isn't.

I have not reproduced a case where oxlint exits non-zero while emitting parseable JSON with an empty diagnostics array, so I'm flagging this as hardening rather than a demonstrated bug — but it's the shape that produces a false green, and the fix is one line:

if (result.status !== 0 && diagnostics.length === 0) { /* fail loudly */ }

6. walk() swallows a missing directory

existsSync → return []. The three FORK_OWNED_DIRECTORIES are not manifest paths, so customizationsManifest.test.ts's "references only paths that exist in the tree" doesn't cover them. custom/ and __fork_guards__/ are protected by the files.some(startsWith(...)) assertions. overrides/ is not — the test only asserts the constant contains the string, which stays true after the directory is deleted. Cheap fix: assert all three resolve on disk.

7. The manifest entry contradicts the code it documents

Scope is ... filtered to .ts/.tsx, because files: also lists images, CSS, YAML and shell.

LINTABLE is .ts, .tsx, .mjs. The .mjs inclusion is the PR's own highlighted late fix, and the manifest text wasn't updated for it. The guard test header likewise says "linting 28 files" where the real count is 30, as the PR body states.

Small in isolation. Less small in a repo where the manifest is the register of record that drift detection reads: the register shipped already drifted from the code it registers.

8. Minor — failure-path cost

The mismatch branch re-runs vp lint once per file to name skipped paths — 30 subprocesses. Failure-path only, so acceptable, but at ~1s of vp startup each that's a 30-second step at the exact moment someone is waiting to find out what broke.


§7.1 is resolved — by this PR's own run

The one unknown you left open ("this has never run in an Actions runner") now has an answer. On 3db1bc2, the Check job completed successfully (run 30233152385, job 89875685152), and the log tail shows Build desktop pipeline and Verify preload bundle output both completing — both sequenced after Lint fork-owned code in the same job, so the gate executed in the runner and exited 0.

I confirmed this via step ordering rather than by reading the gate's own stdout line, so it's worth pasting the actual fork-lint: 30 fork-owned files, no warnings. line from the runner into the PR body to close §7.1 with direct evidence.


Summary

Findings 5–8 are cleanup. Finding 4 is a guard that can't fail and should be fixed on principle in a PR whose sibling is about exactly that. Findings 1–3 are the ones I'd hold this on: the gate does not cover the file that produced the nine imports, does not cover apps/web/fork/, and does not cover the detect-drift.mjs its comment claims it covers.

The mechanism is well built. The scope is wrong, and the scope is the whole product.


Generated by Claude Code

Review of #19 established that the gate would not have caught the bug it was
built for. All nine dead imports were in apps/web/src/components/SidebarV2.tsx,
which sits under no fork-owned directory and appears in the manifest only under
watch: — a key the selector never reads. Run the first version against the
pre-#16 tree and it prints "no warnings" while all nine are live. Verified, and
now verified in the other direction too: reinstating one of those imports fails
the gate at SidebarV2.tsx:154.

The fork's largest authored surface is hunks inside files at upstream paths,
and a file-level scope cannot say "the fork owns these lines but not this
file". Add an explicit adopted-files list for upstream paths the fork has
edited enough to own their lint: SidebarV2.tsx, SidebarChrome.tsx,
AppSidebarLayout.tsx. Adoption is not free — an upstream warning in one turns
the build red — so ThreadTerminalDrawer.tsx stays out despite carrying fences,
because its one warning is upstream's line under upstream's rule config
surfaced by upstream's own flag, and no fork change can clear it. Fenced hunks
elsewhere remain uncovered; the manifest now says so instead of claiming the
gap is closed.

Two more fork-owned surfaces were missing on day one: apps/web/fork (the
override machinery — the fork's own code by any reading) and .fork itself,
which meant the comment claiming detect-drift.mjs was covered was false. Both
added, both pinned by tests rather than by comments.

The scope is hand-maintained and nothing reconciled it against the tree, which
is the same "drifted and nothing noticed" failure this gate exists to prevent,
one level up in its own configuration. The guard now walks the tree with a
second implementation and demands the selection match, so dropping a directory
fails in something that did not read the list.

Also from review: the CI assertion was toContain, which stayed true with the
step commented out, given if: false, or moved to another job — the same
unfalsifiable shape as the CLAUDE.md guard in #18. Anchored to the check job
and asserted unconditional; both evasions now fail it. result.status was
discarded, so a non-zero exit with no diagnostics would have read as clean;
it is a backstop now, no such case reproduced. walk() returned [] for a
missing directory and now throws. --report-unused-disable-directives added to
match the repo's own lint script, so the gate is never weaker than the lint it
enforces. The manifest said .ts/.tsx after .mjs had been added — the register
of record had already drifted from the code it registers.

The gate caught one warning in this very commit: prefer-set-has in the new
guard test, in code written to enforce exactly that standard.
@NoahHendrickson

Copy link
Copy Markdown
Owner Author

Addressed — findings 1-8

You were right on the central claim, and it was the one that mattered. Verified every finding before acting.

1. The gate would not have caught the nine imports — confirmed, fixed

SidebarV2.tsx selected: false      (30 files selected, none of them it)

Only under watch:, never files:. The gate was aimed away from the bug it was built for.

Added an adopted-files list for upstream paths the fork has edited enough to own their lint: SidebarV2.tsx, SidebarChrome.tsx, AppSidebarLayout.tsx. Proof it now works — reinstating one of the nine:

fork-lint: 1 warning(s) in fork-owned code.
  apps/web/src/components/SidebarV2.tsx:154:10  eslint(no-unused-vars)  Identifier 'Kbd' is imported but never used.
EXIT=1

ThreadTerminalDrawer.tsx stays out despite carrying fences, and I pinned that with a test. Its one warning is upstream's line, upstream's rule config, upstream's own lint flag — adopting it means going red for something no fork change clears. That's your ratchet argument applied at file granularity, and it's why adoption is a named list rather than "all watch: files".

The manifest now states plainly that fenced hunks in non-adopted upstream files remain uncovered, rather than asserting the gap is closed.

2 & 3. apps/web/fork/ and detect-drift.mjs — confirmed, fixed

Both were zero-coverage. Added .fork and apps/web/fork to the directories; 37 files now, up from 30. The detect-drift.mjs comment was false as written and the test certified half the claim — both are now pinned individually.

And your structural point is the fix that matters most. The scope was hand-maintained with nothing reconciling it against the tree — the same "drifted and nothing noticed" failure this gate exists to prevent, relocated into its own config. The guard now walks the tree with a second implementation and demands the selection match, so a dropped directory fails in something that never read the list.

4. Unfalsifiable CI assertion — confirmed, fixed

Fair hit, especially given #18. Anchored to the check: job and asserted unconditional. Both evasions you named now fail:

A. step given 'if: false'         → × runs the gate as an unconditional step
B. step moved out of check job    → × runs the gate as an unconditional step

5-8. Fixed

  • result.status discarded — now a backstop that refuses to call a non-zero exit with no diagnostics clean. No such case reproduced; hardening, as you framed it.
  • walk() swallowed a missing directory — throws now. Plus a test asserting all five resolve on disk, which is what actually covered overrides/.
  • Manifest drifted from code (.ts/.tsx vs .mjs, "28 files") — corrected. You're right that this stings more in the register drift detection reads.
  • 30 subprocesses on the failure path — left as-is, failure-path only, but now bounded by being reached less often.

One you didn't catch

The gate omitted --report-unused-disable-directives, which the repo's own lint script passes — so it was strictly weaker than the lint it claims to enforce. Added.

And the gate caught itself

Adding .mjs/.fork to scope meant the gate linted the new guard test, and it failed on unicorn(prefer-set-has) — in code written to enforce exactly that standard. Fixed, but worth reporting as evidence the thing works on its author.

§7.1 — closed with direct evidence

From the runner on 3db1bc2, as you asked:

Check  Lint fork-owned code  2026-07-27T02:52:06Z  fork-lint: 30 fork-owned files, no warnings.

Verification

Check Result
Gate 37 files, no warnings, exit 0
apps/web tests 189 files, 1655 tests, pass
Guard tests (9, was 6) pass
typecheck exit 0
lint exit 0 — 0 errors, 12 warnings (unchanged)
fmt --check clean

Pushed f1a0d36d3.


Generated by Claude Code

@NoahHendrickson
NoahHendrickson merged commit bfe9fc0 into custom Jul 27, 2026
10 checks passed
@NoahHendrickson
NoahHendrickson deleted the fork/lint-owned-guard branch July 27, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant