Imported from tomada1114/quick-reply-drill (
.agents/skills/managing-dependencies/SKILL.md). Install upstream withnpx skills add tomada1114/quick-reply-drill --skill managing-dependencies. Copyright stays with the author.
Managing Dependencies
Owns: whether a package may exist in this repository at all, and what happens at
install time. Does not own: landing an existing bot PR (merge-dependabot).
This repository is a private application. Nothing here is packed, published, or consumed as a tarball, so a dependency is judged by what it costs to install and to run — never by what it would do to a published surface.
The review record a new runtime dependency needs
Adding a runtime dependency is a permanent supply-chain commitment, so before adding one, record all of the following in the PR that adds it. Missing one item is not a detail to fill in later — it means the review has not actually happened yet.
- Why a small hand-written helper or a Node builtin cannot replace it.
node:util'sparseArgscovers subcommands (allowPositionals) and rejects unknown flags (strict), so an argument-parser dependency needs a reason beyond convenience. - Maintainer and release continuity — actively maintained, not abandoned.
- License compatibility.
.github/workflows/dependency-review.ymldenies a copyleft license outright, so a package under one does not merge; find another. - Direct and transitive package count it pulls in — a large transitive surface is a real cost even behind a small direct API.
- Whether it runs an install script, ships a native binary, or does network access —
each needs its own justification, and an install script also needs a ruling in
allowBuilds(see below). - Supported Node versions and module format (ESM/CJS) against this repository's own
devEngines.runtimeand"type": "module"inpackage.json. - Open security advisories and npm provenance.
- Which of
dependencies,devDependencies, oroptionalDependenciesit belongs in, and why. There is no fourth option: this repository publishes nothing, so it declares nopeerDependenciesof its own. Every peer range you will meet was declared by something you installed.
A runtime entry is the expensive one: dependencies is what ships to the running
application and what the weekly security-audit.yml job audits with
pnpm audit --prod. A build- or test-only package belongs in devDependencies, where
an advisory is handled by a bot PR instead of paging whoever reads that schedule.
Verifying a dependency change before it lands
A manifest and lockfile change is verified by a real install and a real run, never by a test that mocks a package manager at its subprocess boundary. Run, in order:
pnpm install # regenerates pnpm-lock.yaml; must succeed under the policy below
pnpm check:quick # format, lint, typecheck, tests
pnpm build # next build — the App Router entry points and the framework's
# own build pipeline, which no unit test exercises
pnpm test:coverage # the coverage floors, which a swapped dependency can move
The last three are together what pnpm check:source runs, so one green run of that
covers them all. One narrower run is worth naming, because the gate reports its failure
only as a wall of output: the suite for the module that actually consumes the bumped
package, run on its own.
pnpm exec vitest run tests/<module>.test.ts
A zod bump surfaces first in whichever contract suite parses with it; a react bump
in the component tests. No suite reaches a live service either way:
tests/ai-port.test.ts's contract suite runs describeLlmPortContract against both
adapters, exercising the port's own schema handling — a zod bump that changes what
safeParseAsync accepts or reports shows up there first, entirely offline. zod is
also a peer of ai and @ai-sdk/openai, so a major bump of it is checked against
tests/ai-openai.test.ts too, which drives the real SDK over a substituted fetch and
still reaches no service; integrating-llm owns that seam.
Two checks run only on the PR. The Dependency review workflow fails on a new advisory
or a denied license, and the weekly production audit above is now a gate that can
actually fail: with runtime dependencies no longer empty, pnpm audit --prod is no
longer the no-op it was, so a red one is a finding about a package this application
ships, not noise.
Range vs. pin, and how a change lands
- A runtime dependency declares a SemVer range, never an exact pin — a range plus
pnpm-lock.yamlreproduces the install, and a pin only removes the fallback that makes the cooldown below survivable. - Add or bump through
pnpm add, never by hand-writing a version intopackage.json. A hand-typed recent version can be younger than the cooldown below, and a pin has no older version to fall back to, so the install fails outright rather than resolving to something older. - A dependency change and its
pnpm-lock.yamlupdate belong in the same commit. The lockfile is generated bypnpm install, never hand-edited: a hand-written entry states an integrity hash and a resolved graph nobody verified against the registry, and nothing downstream can tell that apart from a real resolution. Regenerate instead, withpnpm install --lockfile-onlywhen you want the lockfile without the install. - Dev dependencies are kept current by bot PRs plus the lockfile, not by hand.
REQUIRED:
merge-dependabotto land one.
The release-age cooldown
pnpm-workspace.yaml sets a minimumReleaseAge cooldown: a version published too
recently will not resolve, for any install. Treat this as behavior to design around, not
an obstacle to route past.
- A
^/~range silently resolves to an older, already-cooled version instead of the newest matching one — this is expected, not a bug. - An exact pin on a version younger than the cooldown fails the install outright, since there is no older matching version for a pin to fall back to.
- Before adding or bumping a dependency, check how long its target version has actually
been published; do not assume
latestwill resolve.
Exception process
For an urgent security fix younger than the cooldown, a human may approve an exact
package@version entry in minimumReleaseAgeExclude. The same PR must:
- cite the advisory,
- explain why waiting is riskier than skipping the cooldown,
- include the regenerated lockfile, and
- state when the exception will be removed.
Never add a wildcard entry or a package-only (unversioned) exclusion — the exception is scoped to one exact version, not to the package forever.
Supply-chain settings, as consequences
pnpm-workspace.yaml holds the values; this is what each one means when it fires. Read
the file for the current values rather than trusting a number copied here.
strictDepBuildsplusallowBuilds: an install-time lifecycle script from a dependency nobody has ruled on fails the install on purpose — that is the intended outcome, not a bug to route around. A Next.js dependency tree reaches several packages that want to run one, so this is a case-by-case review now rather than a single standing exception. Three questions settle an entry: what the script actually does (download a binary, compile native code, probe for a prebuilt one); whether anything this repository runs needs its result, or it belongs to a feature never turned on; and whether the package already ships a prebuilt platform binary as an optional dependency, making the script a fallback rather than the only path.falseis as much a decision astrue— it records that the script was reviewed and refused, so the next install failure is not answered with a reflexivetrue. Read the file's comments for the ruling each entry carries; adding atrueone carries the same review weight as adding a new dependency.strictPeerDependencies: a peer range declared by an installed dependency and left unmet or conflicting is a hard install failure, not a warning. This is what makes the TypeScript ceiling below an enforced constraint instead of an advisory one. The only sanctioned way past it is apeerDependencyRules.allowedVersionsentry naming oneparent>childedge, and adding one asserts the package really does work against the version it did not declare — it is not a way to quiet an inconvenient failure.overridesis the same shape of exception for a resolved version, with the same burden: prefer naming the singleparent>childedge, say why, and say what would let it be dropped. A package-wide override is the exception to that, and needs its own reason in the comment — that several independent edges reach the bad version, so an edge list would be incomplete the moment a new transitive dependency reopens it.minimumReleaseAgeStrictandminimumReleaseAgeIgnoreMissingTimeclose two specific bypasses of the cooldown above: an already-lockfiled version skipping the check, and registry metadata with no publish time being treated as old enough, respectively.trustPolicy,trustLockfile, andblockExoticSubdepsare independent supply-chain protections, not part of the cooldown: they reject a provenance/trusted-publisher regression, refuse to trust the trust metadata recorded in a contributor's lockfile, and refuse transitive dependencies fetched from git or arbitrary tarball URLs, respectively.verifyDepsBeforeRun: error: apnpm runwhosenode_modulesno longer matches the lockfile fails instead of letting a gate pass against stale dependencies. The fix ispnpm install, never a weaker value.pmOnFail: download: a local pnpm that does not satisfydevEngines.packageManagermakes the install fetch the pinned one rather than fail. It is the only convenience here rather than a gate — it changes which pnpm resolves the lockfile, never what the policy above admits.
When one of these fires, find out why the install is actually failing; AGENTS.md holds the prohibition on relaxing it.
The one dependency outside pnpm's graph
.mcp.json runs next-devtools-mcp through pnpm dlx, straight from the npm registry,
with no package.json entry and no pnpm-lock.yaml line. Every protection above is a
pnpm-install-time mechanism, so none of it reaches this file: no lockfile pins the
resolved version, minimumReleaseAge never sees a dlx fetch, and pnpm audit never
walks a graph this file is not part of. Left unpinned (next-devtools-mcp@latest), the
same commit runs a different tool depending on when it happens to be fetched — no
lockfile, no cooldown, no review record, for a server that runs inside real development
sessions.
The command is pnpm dlx, never npx — that choice is load-bearing, not stylistic.
npx is npm's own runner, so it evaluates this repository's package.json
devEngines.packageManager (pnpm@…) before doing anything else; npm sees itself as
the running manager and aborts with EBADDEVENGINES. An MCP client always launches the
server with the project root as its cwd, so npx fails from every real launch, not just
occasionally. pnpm dlx never evaluates that check. Do not "simplify" this back to
npx — the failure it reintroduces reports devEngines, not .mcp.json, so it reads
as unrelated to this line and is easy to chase in the wrong file.
The fix is a manual substitute for what the lockfile does automatically elsewhere:
- Pin an exact version in the
argsarray (next-devtools-mcp@<version>) — never@latestand never a^/~range. A range here has no lockfile to freeze its resolution, so it would still float to whatever is newest each timepnpm dlxruns; only an exact string is reproducible outside pnpm's graph. - Resolve the version by hand with
npm view next-devtools-mcp version, and check how recently it was published withnpm view next-devtools-mcp time --jsonbefore adopting it — the cooldown above exists for exactly this reason (a freshly published version installed unreviewed), and nothing enforces it here, so apply it by eye: prefer a version that has been out for at least theminimumReleaseAgewindow over the newest one. - Verify the pinned version actually starts from the repository root, not from a
temp directory or any other cwd: run it there and send it an
initializerequest over stdio (or otherwise confirm the MCP client connects to it) — a crash or a malformed response is the whole of what "starts" means for a version with no test suite of its own here. Verifying from anywhere else can pass while the pin is broken for every real MCP client, which always launches from the project root — that gap is exactly how thenpxform above shipped broken and unnoticed. - Bumping is a manual PR by whoever notices the pin is stale or hits a bug fixed
upstream — there is no bot PR for this one, unlike every dependency
package.jsondeclares. The PR that bumps it repeats the two steps above: resolve and age-check the new version, then verify it starts, from the repository root.
TypeScript version ceiling
typescript is held below the version typescript-eslint caps its peer support at.
That package is now the only thing setting this ceiling, and it caps at a minor, not
just below the next major, so read the real range instead of assuming:
node -p "require('typescript-eslint/package.json').peerDependencies.typescript"
Compare it to package.json's devDependencies range. With strictPeerDependencies
on, a bump past the ceiling fails the install rather than merely warning, so
.github/dependabot.yml ignores typescript minors as well as majors — an ignore
scoped to majors alone would still let the next minor through, and that is the one the
range actually caps at. Patches inside the current minor arrive as PRs as usual.
Do not "upgrade typescript to latest." Raising this ceiling is a coordinated upgrade —
typescript-eslint has to raise its own peer range first — not a routine bump.
Handoff
REQUIRED: merge-dependabot for landing an already-open bot PR against these rules.