LearnNewsExamplesServices
Frontmatter
id17171
titleNineteen lint workflows run on dev PRs; none is a required status context
stateOpen
labels
bugaiarchitecturebuildai-generated
assignees[]
createdAtAug 15, 2026, 2:02 PM
updatedAtAug 17, 2026, 1:05 PM
githubUrlhttps://github.com/neomjs/neo/issues/17171
authorneo-opus-vega
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Nineteen lint workflows run on dev PRs; none is a required status context

Open Backlog/active-chunk-16 bugaiarchitecturebuildai-generated
neo-opus-vega
neo-opus-vega commented on Aug 15, 2026, 2:02 PM

Context

Found by @neo-gpt during exact-head review of PR #17161, where I had claimed a new CI workflow closed a git push --no-verify hole. It does not, and the reason is not specific to that guard.

The Problem

Nineteen *-lint.yml workflows run on dev PRs. Zero of them are required status contexts.

ls .github/workflows/*-lint.yml | wc -l
→ 19

gh api repos/neomjs/neo/rules/branches/dev \
  --jq '[.[] | select(.type=="required_status_checks")] | .[].parameters.required_status_checks[]?.context'
→ integration-parity

integration-parity is the entire required set. The branch-protection endpoint 404s, so ruleset 19087298 is the whole story.

The consequence is mechanical: a failing lint moves a PR to mergeStateStatus: UNSTABLE, which means mergeable with failing non-required checks. It is a visible red mark, not a gate. Combined with git push --no-verify bypassing the local hooks, every lint in this repository is advisory at the moment of merge.

This is not a claim that lints are useless — they are loud, they are read, and authors fix them. It is a claim that the repo's own substrate describes them as gates when the ruleset does not back that. Sampled from the workflow headers currently on dev:

  • atomic-write-shape-lint.yml: "CI mirror of the .husky/pre-commit guard, so git commit --no-verify cannot bypass it."
  • The same sentence pattern recurs across the family.

Those comments are the enforcement claim, and the ruleset does not support any of them. An agent or human reading the header reasonably concludes the hole is closed.

Why this surfaced now

PR #17161 adds check-spec-retirement.mjs, whose entire purpose is catching a silent failure — a deleted unit spec, which no red build can report because the assertions leave with the file. I wired a CI mirror and wrote that it closed the --no-verify path. @neo-gpt checked the live ruleset instead of the workflow file and found the mirror is reachability, not enforcement.

That guard is a sharp instance because its defect class is silence: shipping it as advisory means a --no-verify push still deletes coverage silently, and the guard that exists to notice sits green-but-unconsulted beside it. But the gap is the family's, not that guard's.

The Architectural Reality

  • Ruleset 19087298 on dev; active rule types: code_scanning, copilot_code_review, deletion, non_fast_forward, required_status_checks. Only the last carries contexts, and it carries exactly one.
  • test/playwright/unit/ai/scripts/lint/lintWorkflowScanRootParity.spec.mjs already enforces scanned ⊆ watched — that a lint's workflow watches every path the lint scans. That is the reachability half, and it is well built. Nothing asserts the enforcement half.
  • ai/scripts/lint/guard-ci-parity-registry.json reasons about which lint-staged guards have CI mirrors — again mirror existence, never required-context membership.
  • So the repo has two mechanisms that both stop one step short of the gate, and no mechanism that reads the ruleset.

The Fix (shape — the decision is the deliverable, not the code)

Three candidate shapes; this ticket exists to pick one, and picking is operator-owned because every option mutates repo settings.

  1. One aggregate required context. A single lint-gate job that fans out to the lint family and is the required context. One ruleset entry, no per-lint churn. Cost: a red aggregate does not name which lint failed without opening it.
  2. Per-lint required contexts. Precise, and it makes the ruleset a readable inventory. Cost: 19 entries, and every new lint needs a ruleset edit — which is exactly the drift lintWorkflowScanRootParity exists to prevent, now moved to a surface a spec cannot see.
  3. Fold the lint family into integration-parity's job. No ruleset change at all. Cost: conflates two concerns in one context, and a lint failure reports as an integration-parity failure.

Whichever is chosen, the mechanical follow-on is the same: a spec that reads the live ruleset and asserts the family's membership, so this gap cannot silently reopen. That is the piece an agent can build and the piece that makes the decision durable.

Acceptance Criteria

  • A decision is recorded (aggregate context / per-lint contexts / fold into the existing job), with the rejected alternatives and why.
  • The chosen enforcement exists on the live dev ruleset, verified by gh api repos/neomjs/neo/rules/branches/dev output pasted into the PR — not by workflow-file inspection.
  • A falsifier: a PR that violates one lint is demonstrated to be merge-INELIGIBLE, not merely UNSTABLE.
  • A spec reads the live required-context set and asserts the lint family's membership, failing when a lint workflow exists outside it — the enforcement twin of lintWorkflowScanRootParity's reachability check.
  • Every workflow header claiming --no-verify closure either becomes true or is corrected. spec-retirement-lint.yml is already corrected in PR #17161 and is the model.
  • #17151's enforcement half is discharged by this ticket, and its PR's narrowed claim is reconciled.

Out of Scope

  • Changing what any individual lint checks.
  • The pre-commit / pre-push hook layer itself — hooks are bypassable by design; this is about the gate behind them.
  • integration-parity's own scope.

Avoided Traps

  • Fixing it for one guard. Adding only spec-retirement-lint as a required context would close my instance and leave eighteen open, while making the substrate more misleading — one honest header among nineteen false ones.
  • Treating the workflow file as the source of truth. That is the exact error this ticket documents. Every AC above demands live ruleset output, because a workflow header is a claim and the ruleset is the fact.
  • Assuming advisory means broken. Nineteen lints run and authors fix them; the loop works socially. The defect is the gap between that and what the substrate says, which is what a --no-verify push exploits without anyone lying.

Evidence class

L2 — live API reads against the deployed ruleset, reproducible by any seat with repo read access. The enforcement change itself needs repo-admin and is therefore operator-gated.

Related

#17151 (the guard whose PR surfaced this; its enforcement AC lands here) · PR #17161 · lintWorkflowScanRootParity.spec.mjs (the reachability half, already built) · guard-ci-parity-registry.json

Live latest-open sweep: checked the latest 20 open issues at 2026-08-15T12:01Z; no equivalent found — nearest is #17151 itself, which owns the guard rather than the gate. A2A in-flight claim sweep over the last 12 messages: no competing [lane-claim] on ruleset or CI-enforcement scope.

Origin Session ID: 5cd926fa-77e1-4309-8bbf-ca563ab07403

Retrieval Hint: query_raw_memories("lint workflows advisory not required status context ruleset dev") · falsification anchor: gh api repos/neomjs/neo/rules/branches/dev returns integration-parity as the sole required context.

tobiu referenced in commit acfb498 - "feat(build): a deleted unit spec must say where its coverage went (#17151) (#17161) on Aug 15, 2026, 2:57 PM
tobiu referenced in commit 319c477 - "feat(ai): derive an ai/scripts entrypoint's execution plane from what it reaches (#16929) (#17191) on Aug 16, 2026, 9:15 PM