npm reads the project's own .npmrc, $HOME, and the global config. It
does not walk up parent directories (npm/npm#11437 is still open), so
the root .npmrc gates the root install and nothing else. Two nested
projects were resolving with no supply-chain quarantine at all:
scripts/whatsapp-bridge and plugins/platforms/photon/sidecar. Both now
carry their own gate, and `npm ci` in each returns its lockfile
byte-identical.
A project is a directory holding both package-lock.json and
package.json -- the pair npm resolves against. A lockfile with no
manifest beside it is a vendored artifact; nix/ keeps one for a
hash-pinned fetch that never consults npm config.
The required floor is read from the root .npmrc instead of hardcoded,
so the standard is raised in one place. A nested project may be
stricter, never weaker.
An override forces a version on every transitive consumer, so a ranged
override re-floats packages the manifest never names. That is a wider
hole than a ranged direct dependency, and the lint did not look at it.
Overrides nest, so the walk is recursive. `npm:<pkg>@<version>` aliases
are now recognised too, and held to the same rule on their version
tail; before this they were rejected outright even when exact.
Three ranges are pinned to close the lint on the live tree. yauzl and
the bridge's protobufjs take their locked versions; the root protobufjs
override is dormant (no protobufjs resolves anywhere in the root tree)
and takes its own declared floor. Re-resolving both lockfiles with
`npm install --package-lock-only` returns them byte-identical.
An unanchored key regex matched inside a longer key with the same
suffix: removing `h2` from `{ python-h2 = false, h2 = "..." }`
produced `{ python- }`. The structural verifier caught the unparseable
result and refused the write, so no file was corrupted -- but the fixer
raised, and `run.py --fix` is deliberately not continue-on-error, so
the autofix job would go red on main and stay red.
A word boundary is insufficient: \b does not fire between `-` and `h`.
Use a lookbehind that rejects word characters, dots, and hyphens.
Both edit paths and both TOML forms are covered; reverting the anchor
fails the new tests.
- no-hardcoded-hermes-home: the Known Pitfalls rule (source of 5 bugs
in PR #3575). flags Path.home()/'.hermes' + expanduser variants in
production code; a get_hermes_home()/HERMES_HOME mention within 8
lines counts as a guard hint (fallbacks and deliberate HOME anchors
like the desktop-ssh token root pass untouched), '# hermes-home: ok'
is the explicit override. found and fixed one real bug: the telegram
adapter's gmail-triage script path ignored the active profile.
- no-ansi-erase-eol: \033[K leaks as literal ?[K under prompt_toolkit
patch_stdout; comments documenting the pitfall are exempt.
- skill-frontmatter-standards: the HARDLINE authoring rules that were
review-only — description <=60 chars/ends with period/no marketing
words, name present, platforms: required when skill scripts import
POSIX-only primitives. live tree already clean on all three.
five new registered lints, all blocking, none autofixable:
- engines-satisfiable: from tests/test_engines_satisfiable.py, which
lived in the python pytest lane — a package.json-only PR never ran
it, the exact classifier gap that let the npm-12 floor outage ship.
- no-shadowed-test-definitions: from the pytest guard; same AST scan,
per-file annotations instead of one aggregated assert.
- no-committed-infographics: from infographic-check.yml; deletes the
workflow and its ci.yml wiring (one fewer runner per PR).
- workflow-sha-pins: the AGENTS.md action-pinning policy (commit SHA,
never tag/branch), previously enforced only by review. repo is
already clean — every uses: ref is pinned.
- pyproject-dep-bounds: the AGENTS.md upper-bound policy (>=floor,<
ceiling; git deps SHA-pinned) plus the pin-consistency invariant
pulled out of tests/test_packaging_metadata.py (the lazy_deps-coupled
tests stay put). self-referential hermes-agent[extra] includes exempt.
- packagejson-exact-versions: every tracked package.json dependency
must be an exact version or file:/link:/workspace: ref — ranges float
onto minutes-old releases via unreviewable lockfile churn. fixed the
one violator (whatsapp-bridge: express/pino/qrcode-terminal caret
ranges pinned to their locked versions, lockfile mirror regenerated).
autofix bot also gains ruff check --fix (continue-on-error like npm
run fix; ruff-blocking stays the PR gate), with .py added to the
patch-guard allowance.
reading was always plain tomllib — the inline-only restriction was a
write-side artifact. the [tool.uv.exclude-newer-package] section form
is the same one-key-per-line shape the fixer already edits in uv.lock,
so both files now share one section editor; plan() reads entries via
tomllib regardless of form. the loud 'unsupported form' failure is
gone — both TOML shapes are managed.
the python twin of npmrc-stale-exclude, for [tool.uv]
exclude-newer-package. two findings, both autofixable:
- 'pkg = false' disables the age gate for that package forever — an
unbounded hole. replaced with the uv-documented explicit timestamp:
the locked version's newest upload-time (from uv.lock) + 1 day, so
the exception admits exactly the release it was added for.
- entries whose locked versions have all outgrown the exclude-newer
span (+1d grace) are removed; so are entries matching no locked
package.
no network: publish dates come from uv.lock's upload-time fields;
versions without one fail open as young. names are PEP 503-normalized
to bridge pyproject's huggingface_hub and the lock's huggingface-hub.
the fixer edits BOTH files, mirroring the change into uv.lock's
[options.exclude-newer-package] so uv lock --check stays green
without re-resolution (admitted locked versions are unchanged by
construction; verified against the real lock). before each write a
tomllib-based verifier strips the exclude-newer-package data from old
and new and refuses unless everything else is structurally identical
— a lock diff that touches a dependency url/hash aborts the fix.
live run on the repo: vercel + huggingface_hub entries removed (both
locked >14d), nemo-relay + h2 converted from false to timestamps.
guard: package-lock.json slipped through the .json extension allowance
(a hole inherited from the old js-autofix guard). rather than a global
deny list, the allowance itself now excludes lockfile basenames — they
fall through to the default rejection like any other unallowed path. a
bot-authored, auto-merged lockfile rewrite is exactly the shape of a
supply-chain attack.
fixer: before writing, verify the new .npmrc is the original minus
deletions of exclude lines, comments, and blanks only — every surviving
line must appear in the original in order, and every dropped line must
be an exclude entry, comment, or blank. any other delta (touched
directives, edits, insertions) is a fixer bug and aborts the write.
a min-release-age-exclude[] exists to let resolution pick a release
younger than min-release-age. once every locked version of the
package is itself older than the gate (+1d boundary grace), the
exclude is dead weight and a standing hole in the age gate — this
lint automates the 'remove when > 2wks old' instruction those
entries' comments already carry.
matches exclude patterns (exact, @scope/*) against the sibling
lockfile's locked versions and checks registry publish dates. fails
open per pattern on any fetch error or unknown date — never removes
an exclude on partial data. network lint, so advisory on PRs by
policy; enforcement is the autofix pass on main. the fixer drops a
group's comment block only when no exclude in the group survives.
'# lint: keep' marks intentional standing excludes; applied to
@assistant-ui/* and the ink blocks in .npmrc. also removes the five
already-stale entries the first live run found (react-router,
eslint, @eslint/*, tar, concurrently — each locked >14d).
discovery fix: register lint modules in sys.modules before exec so
@dataclass can resolve annotations.
run_fixes now snapshots the working tree around every fixer and
validates each change against that lint's OWN fix_touches — a fixer
that writes a file only another lint's globs allow is an error, and
under-reported paths are attributed to the fixer that made them.
snapshot uses git status --porcelain so created files count too.
lints declare severity (blocking/advisory) and fixability (autofix)
explicitly. runner gates PRs only on blocking unfixable findings;
fixable ones are advisory because the autofix bot resolves them on
merge. first two registered lints wrap the existing checkers:
windows-footguns (was its own workflow job) and subprocess-stdin
(was wired to no workflow at all).