| name | update-deps |
| description | Autonomous Dependabot — auto-discover outdated packages, audit overrides, apply migrations for major bumps, resolve conflicts, run quality gate. Trigger when the user clicks the statusline `Run /update-deps` indicator or asks "update dependencies", "bump deps", "run dependabot". |
Autonomous superpowered Dependabot. Auto-discover all outdated packages, audit overrides, apply codebase migrations for major bumps, resolve dependency conflicts, and run the quality gate. No user prompts — just execute.
Pre-flight: Worktree check
This wrapper writes a new pnpm-lock.yaml and opens a PR — both belong on the main checkout, not a per-SPEC worktree branch. If invoked from a linked worktree, reject hard with a message that surfaces the cached state from main so the user knows whether action is even pending.
Detection (run this first, before anything else):
common_dir="$(git rev-parse --git-common-dir 2>/dev/null)"
if [ -n "$common_dir" ]; then
case "$common_dir" in
/*) absolute_common_dir="$common_dir" ;;
*) absolute_common_dir="$(pwd)/$common_dir" ;;
esac
main_root="$(cd "$(dirname "$absolute_common_dir")" 2>/dev/null && pwd)"
current_root="$(git rev-parse --show-toplevel 2>/dev/null)"
if [ -n "$main_root" ] && [ -n "$current_root" ] && [ "$main_root" != "$current_root" ]; then
cached_line="Cached state unavailable on main; symlinks may be broken — run \`.gaia/cli/gaia setup link-worktree\` to repair."
cache_file="$main_root/.gaia/cache/update-check.json"
if [ -f "$cache_file" ] && command -v jq >/dev/null 2>&1; then
outdated_count="$(jq -r '.outdatedCount // 0' "$cache_file" 2>/dev/null)"
checked_at="$(jq -r '.checkedAt // 0' "$cache_file" 2>/dev/null)"
if [ -n "$outdated_count" ] && [ -n "$checked_at" ] && [ "$checked_at" != "0" ]; then
now=$(date +%s)
age=$((now - checked_at))
ago_unit="s"; ago_value="$age"
if [ "$age" -ge 86400 ]; then ago_unit="d"; ago_value=$((age / 86400));
elif [ "$age" -ge 3600 ]; then ago_unit="h"; ago_value=$((age / 3600));
elif [ "$age" -ge 60 ]; then ago_unit="m"; ago_value=$((age / 60));
fi
cached_line="Cached on main: $outdated_count packages outdated (last checked ${ago_value}${ago_unit} ago)."
fi
fi
cat <<EOF
/update-deps must run from the main checkout, not a worktree.
Worktree: $current_root
Main checkout: $main_root
$cached_line
Run \`cd $main_root\` then re-invoke /update-deps.
EOF
exit 1
fi
fi
If the detection does not fire, fall through to the existing ## Pre-flight: Branch check section.
Pre-flight: Branch check
git branch --show-current
If the current branch is main or master and not running in CI, set a flag (SHOULD_CREATE_BRANCH=true) but do not create the branch yet — branch creation is deferred until after Phase 1 confirms there are packages to update. Creating a branch when there is nothing to update pollutes the branch list.
In CI (CI=true, set by GitHub Actions, GitLab CI, CircleCI, and most CI providers), skip branch creation — the workflow owns branch management and pre-creates the appropriate branch before this skill runs.
Otherwise set SHOULD_CREATE_BRANCH=false and proceed on the current branch.
Composition: --scope <group-name>
When invoked with --scope <group-name> (e.g. /update-deps --scope react-router):
- Skip Phase 0 (override audit) — out of scope for a single-group run.
- Skip Phase 1 (discovery) — the group's members are known from the
companion-group table.
- Skip Phase 3 (wave classification) — the run is implicitly a single
group; treat it as Wave A if all members are minor/patch, else Wave B.
- Phase 4 / Phase 5 still apply, scoped to the named group's members
in root
package.json.
- Quality gate, return value, and final report still run.
Used by the GAIA CI update-deps workflow's wave-B matrix shards to fan
out one PR per major-bump group.
Phase 0–4: Haiku agent
Spawn a Haiku agent (model: "haiku") to run Phases 0–4. Pass it these instructions verbatim:
Phase 0: Override audit
For each key in pnpm.overrides (in package.json):
- Temporarily remove that single key from
pnpm.overrides.
- Run
pnpm install.
- Run
pnpm ls 2>&1 and scan for peer-dep errors.
- If no errors → override is obsolete. Leave it removed. Note as removed in final report.
- If errors → restore that key. Note as retained in final report.
Operate on one key at a time. Always pnpm install after each toggle.
Phase 1: Discover outdated packages
pnpm outdated --json
Parse the JSON. For each entry record:
name
current version
latest version
is_major_bump (compare leading integers)
is_pinned (no ^ or ~ prefix in the spec found in package.json)
ESLint cap: if eslint or @eslint/js show a latest whose major is >= 10, find the highest available 9.x (pnpm view eslint versions --json and pick the highest 9.x.y) and treat that as the target. If already on the latest 9.x, drop the entry.
Apply this silently. Capped packages MUST NOT appear anywhere in the final report — not in Updated, not in Skipped, not in Breaking changes. Adopters know about the cap; surfacing it on every run is noise.
Release-age cooldown: pnpm-workspace.yaml may set minimumReleaseAge (the minutes a version must be published before pnpm installs it). pnpm 11 enforces this against the whole lockfile on every install, so a target newer than the window makes the install fail. For each candidate target, cap it to the newest version that has already cleared the window: read publish times from pnpm view <name> time --json and pick the newest stable version that is an upgrade, at or below latest, and old enough. If no upgrade has cleared the window yet, skip the package this run — it bumps automatically once the version ages out. When minimumReleaseAge is unset, the cooldown is a no-op. Apply this silently too, like the ESLint cap. (The update-deps run --emit-updates primitive already does this — it records cooldown skips under skipped with reason release-age-cooldown, or release-age-unresolved if the publish-time lookup fails.)
If nothing is outdated after this filtering, print All packages are up to date. and exit.
Phase 2: Resolve companion groups
Map each outdated package into its group. When any member of a group is outdated, include all members present in package.json in the update — even ones not flagged outdated — so the group moves together.
| Group | Members |
|---|
react-router | react-router, react-router-dom, @react-router/dev, @react-router/node, @react-router/serve, @react-router/fs-routes, @react-router/remix-routes-option-adapter |
react | react, react-dom, @types/react, @types/react-dom |
tailwindcss | tailwindcss, @tailwindcss/vite, @tailwindcss/forms, @tailwindcss/typography, prettier-plugin-tailwindcss |
storybook | storybook, @storybook/*, eslint-plugin-storybook, msw-storybook-addon, storybook-react-i18next, @vueless/storybook-dark-mode |
vitest | vitest, @vitest/coverage-v8, @vitest/ui, @vitest/eslint-plugin |
playwright | @playwright/test, @playwright-testing-library/test |
eslint | eslint, @eslint/js, @eslint/compat, eslint-config-*, eslint-plugin-* (9.x cap applies) |
testing-library | @testing-library/dom, @testing-library/react, @testing-library/jest-dom, @testing-library/user-event |
typescript | typescript, @types/node |
i18next | i18next, react-i18next, remix-i18next, i18next-browser-languagedetector |
msw | msw, msw-storybook-addon |
vite | vite, @vitejs/plugin-react |
zod-conform | zod, @conform-to/react, @conform-to/zod |
fontawesome | @fortawesome/* |
stylelint | stylelint, stylelint-config-*, stylelint-order |
prettier | prettier, eslint-config-prettier, eslint-plugin-prettier |
husky | husky, lint-staged |
Packages not matched form singleton groups.
Phase 3: Classify into waves
- Wave A — groups whose members all have minor or patch bumps only. Batched into one install.
- Wave B — groups containing at least one major bump. Processed individually, ordered:
react-router, react, tailwindcss, storybook, vitest, playwright, eslint, then remaining alphabetically.
Phase 4: Wave A (batch minor/patch)
- Build install args. For each package: if
is_pinned use exact target, else use ^<latest>. Example: pnpm add foo@1.2.3 bar@^4.5.0 ....
- Run the single
pnpm add command.
- Run
pnpm ls 2>&1. Scan for peer-dep errors.
- On error: try one targeted
pnpm.overrides fix (e.g. add a parent>child pin), then pnpm install again.
- If still failing: revert the offending packages (
pnpm add <pkg>@<previous>) and log them as skipped with the reason.
- Run the quality gate (below). If it fails, revert the entire Wave A batch.
Quality gate
pnpm typecheck
pnpm lint
pnpm test --run
pnpm pw
pnpm build
Return value
Report back to the orchestrator with:
- Override audit results (removed / retained)
- Wave A results (updated packages, any skipped)
- Wave B groups — list each group name and its major bump (e.g.
react-router: 6 → 7)
- Quality gate results
Branch creation (after discovery)
After the Haiku agent returns:
-
If Phase 1 reported All packages are up to date., skip the rest of this skill entirely. Do not create a branch.
-
Otherwise, updates were confirmed. Immediately bust the update-check cache so the statusline reflects the post-update state on the next session regardless of whether this run completes. Use the Write tool to overwrite .gaia/cache/update-check.json, preserving gaiaCurrent, gaiaLatest, and gaiaHasUpdate from the existing cache (read it first), but setting outdatedCount to 0 and checkedAt to the current Unix timestamp. If the cache file does not exist, skip this step.
-
If SHOULD_CREATE_BRANCH=true, create the branch now and remember that you created it (this determines publish behavior in Phase 8):
git checkout -b chore/update-deps-$(date +%Y-%m-%d-%H-%M)
Otherwise (SHOULD_CREATE_BRANCH=false), proceed on the current branch and remember that you did NOT create a new branch.
Phase 5: Wave B (per-group major bumps)
After the Haiku agent returns, if there are no Wave B groups, skip to Phase 6.
For each Wave B group, classify complexity and assign a model:
Opus (model: "opus"): react-router, react, typescript, storybook
Sonnet (model: "sonnet"): eslint, all other groups
Spawn one agent per group (or sequentially if resource-constrained), passing it these instructions:
Wave B group instructions
You are upgrading the {GROUP} dependency group from {FROM} to {TO}.
Scope. Operate on the root pnpm project only.
- Run every
pnpm command from the project root. Never cd into subdirectories or use -C <path>.
- For code edits and grep-style searches, scan only
app/, test/, and root config files (*.config.*, tsconfig*.json at root). Do not scan the entire repository — sibling directories may be independent pnpm projects with their own package.json/pnpm-lock.yaml, and they are out of scope for this skill.
- A directory is "out of scope" if it contains its own
package.json or pnpm-lock.yaml. Skip those subtrees entirely.
- Fetch migration guide via WebFetch using the table below. If no URL applies, scan the GitHub release notes.
- Install the group, from project root only:
storybook group: run pnpm dlx storybook@latest upgrade (Storybook's own upgrade tool migrates config alongside the version bump).
- All others:
pnpm add <pkg1>@<latest> <pkg2>@<latest> ... for every group member present in root package.json.
- Conflict check:
pnpm ls 2>&1. On peer-dep error, attempt one pnpm.overrides fix. If still failing, revert the group and skip with reason.
- Apply breaking changes within root scope: from the migration guide, identify code-affecting changes (renamed APIs, removed exports, config schema changes). Grep
app/, test/, and root config files for affected patterns. Edit only files inside root scope.
- Verify root
package.json moved: read root package.json and confirm every group member you bumped now shows the new version. If pnpm add did not change root's spec (e.g. the dep is declared in a sibling project's package.json and not actually consumed by root code), revert the install and report the package as skipped — not a root dep. The skill does not resolve cross-project declarations; the maintainer must clean up manually. If the dep is in root package.json but has zero call sites in root scope, that's a phantom declaration: bump it anyway so the version stays current, and add a one-line note phantom: no call sites in root to the breaking-changes report so the maintainer can investigate.
- Quality gate:
pnpm typecheck
pnpm lint
pnpm test --run
pnpm pw
pnpm build
On failure, attempt fixes inferred from the migration guide. If unfixable after a reasonable attempt, revert the entire group and log as skipped.
Migration guide URLs:
| Group | URL |
|---|
| react-router | https://reactrouter.com/upgrading/v7 |
| react | https://react.dev/blog (find the major-version post) |
| tailwindcss | https://tailwindcss.com/docs/upgrade-guide |
| storybook | https://storybook.js.org/docs/migration-guide |
| vitest | https://vitest.dev/guide/migration |
| playwright | https://playwright.dev/docs/release-notes |
| eslint | https://eslint.org/docs/latest/use/migrate-to-9 (or relevant X) |
| typescript | https://www.typescriptlang.org/docs/handbook/release-notes/overview.html |
| msw | https://mswjs.io/docs/migrations |
| vite | https://vite.dev/guide/migration |
Report back: updated packages, breaking changes applied, any skipped reason, quality gate results.
Phase 6: Post-update override audit
For every override that was retained in Phase 0, repeat the Phase 0 toggle test now that surrounding packages have moved. New versions may have resolved the original conflict. Run this as a Haiku agent.
Phase 7: Final report
Build the report only from the agent reports returned to you. Do not add rows from your own memory of the run.
What goes in each section:
- Updated packages — every package the Haiku agent or a Wave B agent reports as
updated. Nothing else.
- Breaking changes applied — only what Wave B agents report editing in the codebase. Empty if no Wave B group ran.
- Overrides audited — only what the Phase 0 / Phase 6 audit reports. If
pnpm.overrides was empty, write "None" and move on.
- Skipped packages — only packages that were attempted and reverted mid-run (peer-dep conflict, quality-gate failure, manual revert by an agent). Never include packages filtered out before installation by a policy rule (e.g. the Phase 1 ESLint 9.x cap or the release-age cooldown). Those are silent by design — surfacing them is noise that adopters see every run. If nothing was actually skipped during the run, write "None" or omit the table.
- Quality gate — the gate result reported by the agents, verbatim.
If a section would be empty, write "None" rather than leaving it blank or fabricating filler.
Print the report. Do not commit.
## Migration Report
### Updated packages
| Group | Package | From | To | Type |
| --- | --- | --- | --- | --- |
### Breaking changes applied
- [group] description
### Overrides audited
- Removed: <key> — <reason>
- Retained: <key> — <reason>
### Skipped packages
| Package | Reason |
| --- | --- |
### Quality gate
| Step | Result |
| --- | --- |
Phase 8: Publish
If nothing was updated (all packages were already up to date or all were skipped), skip this phase entirely.
If a new branch was created (you were on main/master at pre-flight and branched off):
- Push the branch:
git push -u origin <branch-name>
- Open a PR against
main. Title: the commit message from the update commit. Body: the migration report rendered as markdown. Use --body-file with a temp file to avoid shell-hook false positives on package manager keywords in the body.
If you were already on a non-main branch at pre-flight (no new branch was created):
- Push the branch:
git push
- Do not open a PR — the user owns the branch context.
In both cases, print the resulting PR URL (if created) or confirm the push completed.