diff --git a/.agents/skills/verify-agent-tty/SKILL.md b/.agents/skills/verify-agent-tty/SKILL.md index 385afcef..d315bf8c 100644 --- a/.agents/skills/verify-agent-tty/SKILL.md +++ b/.agents/skills/verify-agent-tty/SKILL.md @@ -75,15 +75,16 @@ It then copies each session's `session.json`, `events.jsonl` and `artifacts/` in ## Helpers -All scripts take the run's `env.sh` path, except `start-run.sh` and `stale-dist.sh`: - -| Script | Use | -| ------------------------------ | --------------------------------------------------------------------------------- | -| `scripts/start-run.sh [name]` | Sync deps with the lockfile, build, make the run folder, print the `env.sh` path. | -| `scripts/stale-dist.sh` | Print why `dist/` is not a complete, current build of `src/`, or nothing. | -| `scripts/doctor.sh ` | Check that the run is worth driving. Exit 0 or 1. | -| `scripts/run-pids.sh ` | Print the PIDs this run started. Exit 2: no `/proc`. Exit 3: ledger error. | -| `scripts/cleanup.sh ` | Tear the run down and keep the evidence. | +All scripts take the run's `env.sh` path, except `start-run.sh`, `stale-dist.sh` and `dependabot-resolve.sh`: + +| Script | Use | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `scripts/start-run.sh [name]` | Sync deps with the lockfile, build, make the run folder, print the `env.sh` path. | +| `scripts/stale-dist.sh` | Print why `dist/` is not a complete, current build of `src/`, or nothing. | +| `scripts/doctor.sh ` | Check that the run is worth driving. Exit 0 or 1. | +| `scripts/run-pids.sh ` | Print the PIDs this run started. Exit 2: no `/proc`. Exit 3: ledger error. | +| `scripts/cleanup.sh ` | Tear the run down and keep the evidence. | +| `scripts/dependabot-resolve.sh [dep@version ...]` | Replay Dependabot's npm resolve on an empty pnpm store. Run it after a change to `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml` or `.github/dependabot.yml`. Exit 1: a dep fails. Exit 2: setup error. | ## Keep the map honest diff --git a/.agents/skills/verify-agent-tty/scripts/dependabot-resolve.sh b/.agents/skills/verify-agent-tty/scripts/dependabot-resolve.sh new file mode 100755 index 00000000..331c4da9 --- /dev/null +++ b/.agents/skills/verify-agent-tty/scripts/dependabot-resolve.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Replays the resolve that a Dependabot npm version update runs on this +# checkout: `pnpm update --lockfile-only` with Dependabot's 7-day +# cooldown as the minimum release age, on copies of package.json, +# pnpm-lock.yaml and pnpm-workspace.yaml, against an empty pnpm store. A +# store that already holds the locked packages skips pnpm's trust check, so +# only an empty store shows what Dependabot sees. Needs the npm registry and +# the pnpm version that package.json's packageManager field pins. +# +# .agents/skills/verify-agent-tty/scripts/dependabot-resolve.sh [dep@version ...] +# +# Prints one line per dep and a count line. Exit 0 when every dep resolves, +# 1 when one fails, 2 on a setup error. +set -uo pipefail + +repo=$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel) || exit 2 +pnpm=$(cd "$repo" && { mise which pnpm 2>/dev/null || command -v pnpm; }) || { + echo "dependabot-resolve.sh: pnpm is not on PATH; run 'mise install' first" >&2 + exit 2 +} +want=$(sed -n 's/^ *"packageManager": "pnpm@\([^"]*\)".*/\1/p' "$repo/package.json") +have=$(cd "$repo" && "$pnpm" --version 2>/dev/null) +if [ -z "$want" ] || [ "$have" != "$want" ]; then + echo "dependabot-resolve.sh: $pnpm is pnpm ${have:-unknown}, package.json pins ${want:-nothing}" >&2 + exit 2 +fi +[ "$#" -gt 0 ] || set -- commander@latest zod@latest vitest@latest +work=$(mktemp -d "${TMPDIR:-/tmp}/dependabot-resolve-XXXXXX") || exit 2 +trap 'rm -rf "$work"' EXIT + +failed=0 +for dep in "$@"; do + rm -rf "$work/repo" "$work/store" "$work/cache" + mkdir "$work/repo" + cp "$repo/package.json" "$repo/pnpm-lock.yaml" "$repo/pnpm-workspace.yaml" "$work/repo/" || exit 2 + if (cd "$work/repo" && "$pnpm" update "$dep" --lockfile-only \ + --config.minimum-release-age=10080 \ + --store-dir="$work/store" --cache-dir="$work/cache") >"$work/log" 2>&1; then + echo "ok $dep" + else + failed=$((failed + 1)) + reason=$(grep -m1 -oE 'ERR_PNPM_[A-Z_]+.{0,160}' "$work/log" || tail -n 1 "$work/log") + echo "FAIL $dep: $reason" + fi +done +echo "dependabot-resolve: $(($# - failed)) of $# resolved" +[ "$failed" -eq 0 ] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b1bf00a2..6e1e6632 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,14 @@ updates: # pnpm project (Dependabot detects pnpm from pnpm-lock.yaml and the # packageManager field). The 7-day cooldown (zizmor's minimum) also exceeds # pnpm-workspace.yaml's 1-day minimumReleaseAge, so version updates never - # propose a release pnpm would refuse to resolve. + # propose a release pnpm would refuse to resolve. Dependabot passes the + # cooldown to pnpm as its minimum release age, which pnpm checks against + # every locked package: a locked release younger than 7 days needs an + # exact-version minimumReleaseAgeExclude entry in pnpm-workspace.yaml. + # Dependabot also resolves on an empty store, where pnpm's trust check sees + # every locked package. + # .agents/skills/verify-agent-tty/scripts/dependabot-resolve.sh replays that + # resolve. - package-ecosystem: npm directory: / schedule: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f4b5e94e..d6c4d8a2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,6 +6,13 @@ # resolver falls back to an older match), and reject versions whose publish # trust evidence weakens between releases. minimumReleaseAge: 1440 +# Dependabot runs pnpm with its 7-day cooldown as the minimum release age, and +# pnpm applies that age to every locked package: while one locked release is +# younger than 7 days, every npm version update fails. Exclude only such +# exact versions, so newer releases of the same package stay behind both +# age checks. +minimumReleaseAgeExclude: + - '@coder/libghostty-vt-node@0.1.0-beta.7' trustPolicy: no-downgrade # Resolve optional platform dependencies for every platform devs and CI use, @@ -27,3 +34,10 @@ trustPolicyExclude: # CJS-only); octokit only publishes provenance attestations for the newer # pure-ESM majors, so this exact version has no trust evidence to verify. - '@octokit/endpoint@9.0.6' + # semver 5.7.2 (2023) is a backport release without provenance, published + # after 7.5.1 had it. release-please reaches it through + # conventional-changelog-writer > meow > read-pkg-up > read-pkg > + # normalize-package-data. + # A fresh resolve (Dependabot's `pnpm update` on an empty store) refuses it + # as a trust downgrade, and then every npm update fails. + - 'semver@5.7.2'