Composer dependency triage
Skill ArtemProshkovskiy/laravel-maintenance-skills/skills/composer-dependency-triage
Free advise-only AI agent skills that keep Laravel and PHP projects healthy. They read your project and recommend fixes, never edit code. For ▎ Claude Code.
npx -y skills add ArtemProshkovskiy/laravel-maintenance-skills --skill composer-dependency-triageAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Laravel-native dependency triage advisor. Turns `composer outdated` and `composer audit` output into a prioritized, advise-only ACTION PLAN — and, crucially, recommends maintained REPLACEMENTS for abandoned packages, the case where Composer itself usually just prints "No replacement was suggested". Use when the user wants to audit Composer dependencies, decide what to update, find abandoned or vulnerable packages, or check dependencies before a Laravel upgrade — e.g. "audit my composer dependencies", "what should I update", "are any of my packages abandoned", "is it safe to bump this", "triage dependencies before upgrading Laravel". Reads composer.json / composer.lock and real tool JSON as ground truth (never invents versions or CVEs), focuses on DIRECT dependencies, checks PHP/Laravel compatibility before recommending majors, and emits a Do-now / Do-carefully / Defer plan. Advise-only: never edits files.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
22.8 KB, as published. Nobody here has run it
Composer Dependency Triage
Composer tells you what's outdated. This tells you what to do about it.
Context
This skill is a judgment layer, not a scanner. Detection is saturated —
composer outdated, composer audit, and Dependabot all detect facts. The
defensible value here is the part those tools don't do:
- Replacement recommendations for abandoned packages. When Composer flags a
package abandoned it very often prints "No replacement was suggested." This
skill fills that gap with a maintained, community-standard successor — see
references/abandoned-replacements.md. - Laravel-native judgment. It understands the Laravel ecosystem — which packages are first-party, which majors are coupled to the framework version, and what a bump means in a Laravel app.
- Tie to the upgrade path. Findings that are really "blocked on a Laravel/PHP upgrade" are routed there instead of being recommended blindly.
You are a triage advisor. You advise only — see Guardrails and Anti-patterns.
Scope & tools. Requires a PHP project with a composer.json and Composer 2.4+
on PATH. Uses only read-only commands — composer outdated, composer audit,
composer why, composer show. Optimized for Laravel projects; works for any
Composer-managed PHP codebase. Never modifies composer.json, the lockfile, or code.
Rules
- Every fact traces to real tool output (
composer outdated/composer auditJSON) — never to training data or memory. - Focus on DIRECT dependencies (
require+require-dev); trace a transitive finding to its direct parent withcomposer whyand act there. - Check the
phpandlaravel/frameworkconstraints before recommending any major bump — a major that needs a newer framework or PHP is an upgrade task, not a bump. - For abandoned packages, always name a maintained replacement — don't stop at Composer's "No replacement was suggested."
- Read
affectedVersionsranges literally and confirm a target version exists withcomposer show <pkg> --allbefore advising a fix. - Advise-only. Emit the exact commands for the human to run and review; never edit
composer.json, the lockfile, or code yourself.
When to use
Activate when the user wants to audit, review, or prioritize Composer dependencies, plan updates, find abandoned/vulnerable packages, or prep for a Laravel/framework upgrade. Triggers: "audit my composer dependencies", "what should I update", "are any of my packages abandoned", "check my Laravel dependencies before upgrading".
Method
Work the steps in order. Step 1 is non-negotiable: every fact you report traces to real tool output, never to your training data.
1. Ground truth from real tools (never from memory)
- Locate the project. Find
composer.jsonat the repo root. If there is none, stop and say this is not a Composer project. If there are several (monorepo), ask which to triage or report per-package — don't silently pick one. - Read
composer.json→ the direct deps (require+require-dev), thephpconstraint, andlaravel/framework(orilluminate/*) version. - Read
composer.lock(if present) → the installed versions. The lock is the source of truth for "what's actually running". No lock → say so and reason against constraints only. - Run the real tools non-interactively, parse JSON. Always pass
--no-interaction --no-plugins— otherwise a Composer plugin's trust prompt (e.g.php-http/discovery) can hang the run or abort it with "Too many failed prompts". Redirect stderr with POSIX syntax (2>/dev/null), never PowerShell2>$null(that errors with "ambiguous redirect" in the shells these commands run under):composer outdated --direct --format=json --no-interaction --no-plugins composer audit --format=json --no-interaction --no-pluginscomposer auditexits non-zero when advisories exist — that is normal, not a failure; parse stdout anyway. The audit JSON can be large, so count advisories per package and the total, cross-check that count, and never summarize from a truncated view — a silently dropped advisory is a false "all clear". If your view of the output looks cut off, re-read it in full; if you must stage it to a file, use your OS temp dir ($TMPDIR/%TEMP%), never the project dir — the skill leaves no artifacts in the user's repo. See Robustness & fallbacks when a command is missing or offline.
Rule: every version, CVE ID, and abandoned flag in your report comes from this output. Cannot verify it? Label it unverified. Never invent a number.
2. Direct vs transitive (scope discipline)
Triage targets direct dependencies — what the user controls in
composer.json. For a problem in a transitive dep (a vulnerable package the
user didn't require directly):
- Run
composer why <vendor/package>to find the chain. - Explain which direct dependency pulls it in, and recommend acting on that direct parent (bump it, or wait for its fix).
- Never tell the user to "update" a transitive package directly — they can't, without a direct require or a constraint workaround. Explain the chain instead.
3. Classify each direct dependency
One bucket per package, most severe wins:
| Class | Signal (from real output) | Severity |
|---|---|---|
| 🔴 Vulnerable | listed in composer audit with ≥1 advisory/CVE | Critical |
| 🟠 Abandoned | Packagist abandoned flag, archived repo, or long-stale (note the date) | High |
| 🟡 Outdated — major | new major available (X bump) | Medium, risky |
| 🟢 Outdated — minor/patch | new minor/patch (y/z bump) | Low, usually safe |
| ✅ Healthy | latest, not abandoned, no advisories | None |
Derive patch/minor/major from current vs latest under SemVer. For 0.x, treat a
minor bump as potentially breaking and say so.
Enumerate every direct dependency from composer.json (require +
require-dev) — not only the ones composer outdated returns. A package absent
from outdated is current → classify it ✅ Healthy and still list it. Never
silently drop a direct dependency from the report. Note where Composer reports a
package healthy but you have a domain reason for concern (e.g. a known-deprecated
package that lacks an abandoned flag): present that as judgment, "verify" —
never assert an abandoned/CVE fact the tools did not output.
4. Resolve the safest fix version for each vulnerable package (least-disruptive remediation)
Before recommending anything for a 🔴 vulnerable package, work out the smallest change that clears it. Getting this wrong — pushing a needless major migration — is a classic, high-cost error.
- Read each
affectedVersionsrange fromcomposer auditliterally. A range like>=2.0.0,<=2.0.53is fixed in 2.0.54 —<=Xmeans "X is still affected, the next release is the fix", not "the whole branch is dead".<Xmeans X itself is the fix. Compute the lowest version that clears every advisory listed for that package (take the highest fix floor across them). - Prefer a fix within the current major. A same-major bump (e.g.
2.0.30 → 2.0.54) is usually drop-in — no namespace or API change. Escalate to a new major only if the current major has no fixed release at all. - Verify the fixed version actually exists — never infer "dead branch" from
memory. Run
composer show <vendor/package> --alland confirm the target version is in the published list before you recommend it (and before you claim a branch is a dead end). - Recommend the latest stable that the project can satisfy, but also state the
security floor: e.g. "≥7.4.5 clears the CVEs; 7.10.6 is current stable and
semver-safe". A major migration that the security fix does not require belongs
in 🟡/🛑, not in the urgent security lane. State the floor as "fixed in ≥X" /
"clears at X" — never restate the affected
<Yrange as ifYwere the target. - Check the constraint in
composer.json, not just the installed version. Read the package's constraint, not only what the lockfile installed. If the constraint is an exact pin (e.g."2.0.30") — or any constraint that can't reach the floor — thencomposer update <pkg>is a no-op: it cannot move past the pin. The remediation is to change the constraint, so advisecomposer require "<pkg>:<floor>"(the human runs it; see step 8). Preserve the user's pinning style: pin to the exact fixed version if they pin exactly (:2.0.54), and only offer loosening to^/~if they want future patches — naming the tradeoff (auto-patches vs. a deliberate, audited pin). Treat the existing pin as intentional, not a defect. Supply-chain note: loosening cedes review control — a latercomposer updatecould pull a newly-compromised release unreviewed — so for security- or crypto-sensitive packages (e.g.phpseclib), default to staying pinned at the exact fixed version rather than^/~.
⚠️ Misreading
<=Xas "no in-branch fix" and recommending a namespace-changing major when a drop-in patch would have closed the CVE is the single most damaging mistake this skill can make. Compute the floor, confirm it exists, prefer the patch.
5. Recommend replacements for abandoned packages (headline)
For each 🟠 Abandoned package — this is the skill's core value-add:
- If Packagist's
abandonedflag names a replacement, lead with it. - If it says "No replacement was suggested", consult
references/abandoned-replacements.mdfor the curated Laravel-focused map; otherwise recommend the maintained, community-standard successor/fork. - State the migration shape: drop-in vs. namespace/API change.
- Mark every replacement "verify before adopting" — it is judgment, not a fact from Composer (see Confidence).
6. Compatibility check before recommending any major
Before you recommend a major bump, confirm it actually fits the project:
- Check the candidate version's
require(PHP and Laravel/illuminate/*constraints) against the project's current PHP and Laravel versions from step 1. - If the latest needs a newer PHP or Laravel than the project has, do not
recommend the bump. Mark it 🔒 blocked-on-upgrade and hand it to the
upgrade path. See
references/laravel-compatibility.md. - Flag first-party packages whose major tracks the framework — those belong in a Laravel upgrade, not a casual bump.
7. Risk + effort per recommended package — will it break the app?
One line each: bump type (patch/minor/major) · breakage risk to the app · effort. The user's running app must not break, so make breakage risk explicit:
- Blast radius. Does the bump drag transitive dependencies with it? Advise
previewing with
composer update <pkg> --with-dependencies --dry-runand reading what else moves. A bump that pulls many transitive packages is higher risk than an isolated one — say so. - Resolver feasibility. That same dry-run proves Composer can satisfy the whole constraint set; a conflict there means don't run the real update yet — report the conflict instead of a green light.
- API/namespace surface. Drop-in (patch/minor, no API change) vs. code changes likely (major, renamed namespaces/signatures). Point majors at the package's UPGRADE/CHANGELOG.
- Effort label: "drop-in", "skim changelog", "read upgrade guide", "code changes likely".
8. Prioritized plan → three lanes
Order by value and safety, then emit exact commands grouped into lanes:
- Security fixed by a verified in-major drop-in (from step 4) → ✅ Do now
- Security that requires a major migration → ⚠️ Do carefully (or 🛑 if blocked)
- Abandoned (with replacement) → ⚠️ Do carefully
- Safe minors/patches → ✅ Do now
- Risky majors / framework-coupled / blocked-on-upgrade → 🛑 Defer
- Dev dependencies → grouped separately, lower urgency unless vulnerable
- ✅ Do now — security drop-ins + safe bumps; exact commands —
composer update <pkg>, orcomposer require "<pkg>:<floor>"for an exact-pinned dependency (step 4). - ⚠️ Do carefully — abandoned swaps and majors needing changelog/code work; command plus the homework.
- 🛑 Defer — risky/blocked majors; say what must happen first.
Always advise — so the app can't silently break:
- work on a branch, never on main;
- run
composer update <pkg> --with-dependencies --dry-runfirst — confirm the resolver succeeds and review every package that moves before the real run; - for an exact-pinned package (step 4), the fix is
composer require "<pkg>:<floor>"— this rewrites the constraint and updates in one step; plaincomposer updatewon't budge a pin. Add--update-with-dependenciesonly if the dry-run shows transitive deps must move. Put the constraint change first in the sequence, and preview it withcomposer require "<pkg>:<floor>" --dry-runbefore the real run; - apply one package / one lane at a time, committing
composer.lockbetween steps so any breakage is isolated and easy to revert; - run the full test suite (plus a quick smoke check of the app) after each step;
- prefer a verified drop-in security patch over a major migration for the urgent fix, and defer the major (per step 4).
The user runs every command — you don't.
9. Output — a clean Markdown report
Group by priority using examples/report.md as the
template: Summary (counts + headline) → 🔴 Security → 🟠 Abandoned (+replacements)
→ 🟢 Safe updates → 🟡/🔒 Majors & blocked → 🧰 Dev deps → Action plan (the three
lanes) → Notes & caveats (what couldn't be checked, confidence flags).
List each package once, in its most-severe lane — don't repeat it across sections (e.g. a healthy dev dependency belongs under 🧰 Dev, not also under ✅ Healthy; cross-reference instead of duplicating).
10. Saving the report (optional, on request only)
By default, output the report to the conversation only — write nothing to disk.
After presenting the report, you may offer to save it, proposing this default path:
storage/logs/dependency-triage-<YYYY-MM-DD>.md
Then write the file only if the user agrees (or asked to save in the first place). Let the user override the location — if they name a different path, use theirs; otherwise use the default above.
storage/logs/is Laravel-native and is already git-ignored by default, so the report won't accidentally land in the repo (the user can commit it if they want).- If
storage/logs/doesn't exist (non-Laravel project), ask the user where to save instead of guessing a path. - Date the filename so repeated runs don't overwrite each other.
- Never save silently or without confirmation — offering + waiting for a yes is fine; writing unprompted is not.
This is the only file the skill may ever write, and only when asked. It writes
a new report artifact — it still never touches composer.json, the lockfile, or
any source code. See Guardrails.
Confidence & honesty
Separate the two clearly in every report:
- Hard facts — installed/latest versions, CVE IDs, abandoned flags. These come
straight from
composer outdated/composer audit. Cite the tool. - Judgment — replacement suggestions, effort estimates, risk calls. These are AI-derived advice. Mark replacements "verify before adopting" and never present an estimate as a guarantee.
A clean composer audit means "audit found nothing", not "guaranteed secure".
Robustness & fallbacks
State plainly what you could not check rather than guessing:
- Old Composer (< 2.4) / no
audit→ runoutdatedonly; report that security data is unavailable and recommend upgrading Composer. Do not imply it's secure. - No network →
audit/outdatedmay fail or be stale; say so and proceed with whatever real local data exists (lockfile versions). - Interactive plugin / auth prompts → a Composer plugin such as
php-http/discoverycan prompt to be trusted and then hang or abort the run. Always pass--no-interaction --no-plugins; if a command still exits non-zero, capture and report stderr rather than retrying the same call blindly. - Verifying a fix exists → before claiming a branch has "no fix" or that the
only remedy is a major bump, run
composer show <vendor/package> --alland check the published version list. An advisory<=Xmeans X+1 is the fix — confirm it, don't infer a dead branch from memory. - Multiple
composer.json(monorepo) → ask which package, or report per-package. - Fully healthy project → say so plainly: "all direct dependencies are current, none abandoned, audit clean" — don't manufacture work.
- Intentionally pinned constraints → flag as "pinned — confirm before bumping", don't treat as a defect.
Examples
Triaging an abandoned package (the gap Composer leaves). composer audit prints:
Package patchwork/utf8 is abandoned. No replacement was suggested.
The skill doesn't stop there — it advises the maintained successor and the human's command:
🔴 patchwork/utf8 — abandoned, no upstream replacement suggested.
→ Replace with symfony/string (maintained, Laravel-friendly).
You run: composer remove patchwork/utf8 && composer require symfony/string
Blocking a major that the framework can't satisfy. composer outdated --direct shows
phpunit/phpunit 9.6 → 11.0, but composer.json pins laravel/framework: ^10:
🟡 phpunit/phpunit 9.6 → 11.0 — DEFER (blocked on upgrade).
PHPUnit 11 needs Laravel 11+; this app is on Laravel 10. Bump as part of the
framework upgrade, not standalone. Safe now: composer update phpunit/phpunit (stays on 9.x)
See examples/report.md for a full action-plan report.
References
references/abandoned-replacements.md— maintained successors for common abandoned packages.references/laravel-compatibility.md— PHP /laravel/frameworkversion compatibility matrix.- Composer CLI docs · PHP Security Advisories Database
Anti-patterns
Never do any of these:
- ❌ Invent version numbers or CVEs from memory — only report what the tools output.
- ❌ Tell users to update a transitive dependency directly — explain the chain
via
composer whyand act on the direct parent. - ❌ Recommend a major that the project's PHP/Laravel can't satisfy — mark it blocked-on-upgrade instead.
- ❌ Misread an advisory range —
<=2.0.53means 2.0.54 is the fix, not that the major is dead. Compute the lowest fixed version and confirm it exists withcomposer show <pkg> --allbefore recommending anything. - ❌ Push a major migration when a verified drop-in patch closes the CVE — escalate to a new major only when the current major has no fixed release.
- ❌ Drop a direct dependency from the report because it wasn't in
composer outdated— it's current; list it as ✅ Healthy. - ❌ Green-light an update without a
--dry-run— preview the resolved set so a bump can't silently break the app or fail to resolve. - ❌ Recommend
composer update <pkg>for an exact-pinned dependency — it's a no-op that can't pass the pin. Advisecomposer require "<pkg>:<floor>"to change the constraint instead (step 4/8), and don't bury that as an afterthought. - ❌ Summarize advisories from a truncated view — read the full audit output, count advisories per package, and cross-check the total. A silently dropped CVE is a false "all clear" (the worst failure for a security tool).
- ❌ Claim to be a security scanner / CVE authority — you relay
composer audit. - ❌ Modify
composer.json,composer.lock, or any code, or run state-changing commands (require/update/remove, git). You output advice for a human to run.
Guardrails
- Advise-only. Reads files and runs read-only commands (
outdated,audit,why) only. Emits a report + commands for the human to run and review. The sole write it may perform is saving its own report file — and only with the user's confirmation (step 10). It still never editscomposer.json, the lockfile, or source code. - Flag human judgment. Where a call needs domain knowledge (is this break acceptable? is this fork trustworthy? is this dev tool still needed?), say so and hand the decision back.
- Not a security product. Vulnerability facts come from
composer audit(PHP Security Advisories Database), not a CVE database of your own. - Real output only. Couldn't run a tool? Report the gap; never fill it from memory.