From 9820d77066e264eb5ccd5f1fd328b50a51ac092a Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Thu, 20 Aug 2026 12:23:20 -0700 Subject: [PATCH 1/2] Add pnpm clean:artifacts for branch-switch recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitignored build outputs survive git checkout across branches whose layouts disagree (main vs stable, or any two branches spanning a refactor), and the mismatched leftovers wedge the repo in ways that look like unrelated breakage: pnpm install hanging in workbench prepare scripts ('Could not resolve "workflow/internal/private"', SWC transform errors on valid workflow code), tsc failing on generated files the branch doesn't have, and — worst — Turbo capturing polluted dist as cached task outputs and faithfully restoring the pollution on every rebuild. scripts/clean-artifacts.sh removes exactly the files git ignores (candidates from git ls-files -o -i, filtered through a keep-list in shell). Untracked work-in-progress files are never touched — git clean -x removes those too, and git clean -X -e patterns modify the ignore rules under -X rather than protect, so both were deliberately avoided. Kept by default: node_modules, .env*/*.local, .vercel links, Cargo target/, and local agent state; --all also drops node_modules and target/, --dry-run previews. Wired as 'pnpm clean:artifacts' and documented in AGENTS.md under Branch-switch hygiene. --- .changeset/clean-artifacts-script.md | 2 + AGENTS.md | 29 ++++++ package.json | 1 + scripts/clean-artifacts.sh | 131 +++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 .changeset/clean-artifacts-script.md create mode 100755 scripts/clean-artifacts.sh diff --git a/.changeset/clean-artifacts-script.md b/.changeset/clean-artifacts-script.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/clean-artifacts-script.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/AGENTS.md b/AGENTS.md index 1119e1ed8e..b444aec841 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,8 +58,37 @@ pnpm typecheck # Clean build artifacts pnpm clean + +# Scrub ALL gitignored build artifacts (see "Branch-switch hygiene" below) +pnpm clean:artifacts ``` +### Branch-switch hygiene + +Build outputs are gitignored, so they survive `git checkout` across branches +whose layouts disagree (e.g. `main` vs `stable`). The mismatched leftovers — +stale `packages/*/dist`, generated sources from the other branch, stale +`.tsbuildinfo`, and a Turbo cache that may have captured polluted outputs — +wedge the repo in ways that look like unrelated breakage: `pnpm install` +hanging or erroring in workbench `prepare` scripts (`Could not resolve +"workflow/internal/private"`, SWC transform errors on valid workflow code), +`tsc` failing on generated files the branch doesn't have, or cleaned files +reappearing on every build (Turbo restoring a polluted cache entry). + +After switching between divergent branches — or whenever the workspace shows +any of those symptoms — run: + +```bash +pnpm clean:artifacts # add --dry-run to preview, --all to also drop node_modules + Cargo target/ +pnpm install +pnpm build +``` + +It is a thin wrapper around `git clean -xdf` with a small exclusion list +(`node_modules`, `.env*`/`*.local`, `.vercel`, Cargo `target/`, local agent +state), so it works even when the workspace is too broken to run turbo or +pnpm scripts. + ### Core Package Testing ```bash diff --git a/package.json b/package.json index 1347105d1e..a6fb68a986 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "build": "turbo build --filter='./packages/*'", "test": "turbo test", "clean": "turbo clean", + "clean:artifacts": "bash scripts/clean-artifacts.sh", "typecheck": "turbo typecheck", "test:e2e": "vitest run packages/core/e2e/e2e.test.ts packages/core/e2e/e2e-agent.test.ts", "test:e2e:event-log-race-repro": "vitest run packages/core/e2e/event-log-race-repro.test.ts", diff --git a/scripts/clean-artifacts.sh b/scripts/clean-artifacts.sh new file mode 100755 index 0000000000..553f1209b9 --- /dev/null +++ b/scripts/clean-artifacts.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# +# clean-artifacts.sh — scrub every gitignored build artifact so the workspace +# rebuilds from a pristine state. Run it after switching branches (or whenever +# the repo is behaving strangely); see "Why this exists" below. +# +# Usage: +# pnpm clean:artifacts # remove ignored artifacts, keep node_modules +# pnpm clean:artifacts --dry-run # print what would be removed +# pnpm clean:artifacts --all # also remove node_modules and the +# # Cargo target/ dir (slower reinstall + +# # Rust rebuild, but maximally pristine) +# +# Why this exists +# --------------- +# Build outputs (`packages/*/dist`, generated sources like +# `src/version.ts` / `quickjs-assets.generated.ts`, `tsc` incremental state, +# the Turbo cache, workbench state like `.next` / `.svelte-kit` / `.output`) +# are all gitignored, so they SURVIVE `git checkout` across branches whose +# layouts disagree — main vs stable, or any two branches spanning a +# refactor. The mismatched leftovers then wedge the repo in ways that look +# like unrelated breakage: +# +# * `pnpm install` hangs or errors: workbench `prepare` scripts (e.g. +# sveltekit's `svelte-kit sync`) load vite configs that resolve +# `workflow` to a stale `packages/workflow/dist`, whose imports no +# longer match the checked-out `package.json` exports map +# ("Could not resolve \"workflow/internal/private\""), or compile the +# checked-out workflows with a stale SWC plugin build that predates +# their syntax ("Functions marked with \"use step\" must be async"). +# * `tsc` builds fail on generated files from the other branch (e.g. +# `quickjs-assets.generated.ts` on a branch without QuickJS), or emit +# nothing because a stale `.tsbuildinfo` claims outputs are fresh. +# * Worst: a build that RUNS while dist is polluted lets Turbo capture +# the polluted dist as that task's cached outputs — after which the +# cache faithfully restores the pollution on every rebuild, and no +# amount of `rm -rf dist` alone fixes it. +# +# Design notes +# ------------ +# The candidate list comes from `git ls-files -o -i --exclude-standard +# --directory` — i.e. ONLY files git ignores. Untracked-but-not-ignored +# files (your work in progress) are never touched. `git clean -x` was +# deliberately avoided (it removes untracked files too), and so was +# `git clean -X -e ` (exclude patterns modify the ignore rules, +# which under -X can ADD to the removal set instead of protecting — +# negations leak into subdirectories). Filtering a listing in shell is +# boring and predictable. +# +# What is deliberately KEPT: +# * node_modules — reinstall is slow and pnpm's store makes staleness +# here rare (pass --all when you want it gone too); +# note the Turbo cache lives at .turbo/, which IS +# removed. +# * .env*, *.local — local secrets/config (e.g. VERCEL_OIDC_TOKEN pulled +# via `vercel env pull`). +# * .vercel — Vercel project links; losing them forces relinking. +# * target/ — Cargo's build cache; unlike the JS tooling above, +# Cargo fingerprints inputs correctly across branch +# switches, and a cold Rust rebuild of the SWC plugin +# costs minutes (pass --all to remove it anyway). +# * .opencode, .claude, .agents — local agent tooling state. +set -euo pipefail + +cd "$(dirname "$0")/.." + +DRY_RUN=0 +ALL=0 +for arg in "$@"; do + case "$arg" in + --dry-run | -n) DRY_RUN=1 ;; + --all) ALL=1 ;; + --help | -h) + sed -n '2,66p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "Unknown argument: $arg (try --help)" >&2 + exit 1 + ;; + esac +done + +# Whether an ignored path should be preserved. Paths are repo-relative, +# directories carry a trailing slash (--directory). +keep() { + local p="${1%/}" + case "$p" in + # Local secrets/config. + .env* | */.env* | *.local) return 0 ;; + # Vercel project links. + .vercel | */.vercel | .vercel/* | */.vercel/*) return 0 ;; + # Local agent tooling state. + .opencode | .opencode/* | .claude | .claude/* | .agents | .agents/*) return 0 ;; + esac + if [[ "$ALL" -ne 1 ]]; then + case "$p" in + node_modules | node_modules/* | */node_modules | */node_modules/*) return 0 ;; + target | target/* | */target | */target/*) return 0 ;; + esac + fi + return 1 +} + +removed=0 +kept=0 +while IFS= read -r path; do + [[ -z "$path" ]] && continue + if keep "$path"; then + kept=$((kept + 1)) + continue + fi + # A parent directory earlier in the listing may have removed it already. + [[ -e "$path" || -L "$path" ]] || continue + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "Would remove $path" + else + echo "Removing $path" + rm -rf -- "$path" + fi + removed=$((removed + 1)) +done < <(git ls-files -o -i --exclude-standard --directory) + +echo +if [[ "$DRY_RUN" -eq 1 ]]; then + echo "Dry run — nothing removed ($removed candidates, $kept kept). Re-run without --dry-run to clean." +else + echo "Cleaned $removed artifacts ($kept kept). Next steps:" + echo ' pnpm install' + echo ' pnpm build' +fi From c05fc54e4491f3ae423e83dad3f64cea00a66f70 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Thu, 20 Aug 2026 12:47:18 -0700 Subject: [PATCH 2/2] Remove em dashes from docs and script comments Per the Vercel technical writing guidelines: em dashes create ambiguity for agents parsing sentence boundaries, so replace them with periods, commas, parentheses, or colons. Also corrects the AGENTS.md description of the script's mechanics (it filters a git ls-files listing rather than wrapping git clean -xdf, which the earlier draft used) and makes --help print the header comment block dynamically instead of via a hardcoded line range that had drifted. --- AGENTS.md | 19 +++++++++-------- scripts/clean-artifacts.sh | 42 ++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b444aec841..2f6905d455 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,17 +66,17 @@ pnpm clean:artifacts ### Branch-switch hygiene Build outputs are gitignored, so they survive `git checkout` across branches -whose layouts disagree (e.g. `main` vs `stable`). The mismatched leftovers — -stale `packages/*/dist`, generated sources from the other branch, stale -`.tsbuildinfo`, and a Turbo cache that may have captured polluted outputs — +whose layouts disagree (e.g. `main` vs `stable`). The mismatched leftovers +(stale `packages/*/dist`, generated sources from the other branch, stale +`.tsbuildinfo`, and a Turbo cache that may have captured polluted outputs) wedge the repo in ways that look like unrelated breakage: `pnpm install` hanging or erroring in workbench `prepare` scripts (`Could not resolve "workflow/internal/private"`, SWC transform errors on valid workflow code), `tsc` failing on generated files the branch doesn't have, or cleaned files reappearing on every build (Turbo restoring a polluted cache entry). -After switching between divergent branches — or whenever the workspace shows -any of those symptoms — run: +After switching between divergent branches, or whenever the workspace shows +any of those symptoms, run: ```bash pnpm clean:artifacts # add --dry-run to preview, --all to also drop node_modules + Cargo target/ @@ -84,10 +84,11 @@ pnpm install pnpm build ``` -It is a thin wrapper around `git clean -xdf` with a small exclusion list -(`node_modules`, `.env*`/`*.local`, `.vercel`, Cargo `target/`, local agent -state), so it works even when the workspace is too broken to run turbo or -pnpm scripts. +The script removes only files git ignores (candidates come from `git +ls-files -o -i`, filtered through a keep-list that preserves `node_modules`, +`.env*`/`*.local`, `.vercel`, Cargo `target/`, and local agent state), so +untracked work-in-progress files are never touched, and it works even when +the workspace is too broken to run turbo or pnpm scripts. ### Core Package Testing diff --git a/scripts/clean-artifacts.sh b/scripts/clean-artifacts.sh index 553f1209b9..1aa288596f 100755 --- a/scripts/clean-artifacts.sh +++ b/scripts/clean-artifacts.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# clean-artifacts.sh — scrub every gitignored build artifact so the workspace +# clean-artifacts.sh: scrub every gitignored build artifact so the workspace # rebuilds from a pristine state. Run it after switching branches (or whenever # the repo is behaving strangely); see "Why this exists" below. # @@ -17,8 +17,8 @@ # `src/version.ts` / `quickjs-assets.generated.ts`, `tsc` incremental state, # the Turbo cache, workbench state like `.next` / `.svelte-kit` / `.output`) # are all gitignored, so they SURVIVE `git checkout` across branches whose -# layouts disagree — main vs stable, or any two branches spanning a -# refactor. The mismatched leftovers then wedge the repo in ways that look +# layouts disagree (main vs stable, or any two branches spanning a +# refactor). The mismatched leftovers then wedge the repo in ways that look # like unrelated breakage: # # * `pnpm install` hangs or errors: workbench `prepare` scripts (e.g. @@ -32,34 +32,34 @@ # `quickjs-assets.generated.ts` on a branch without QuickJS), or emit # nothing because a stale `.tsbuildinfo` claims outputs are fresh. # * Worst: a build that RUNS while dist is polluted lets Turbo capture -# the polluted dist as that task's cached outputs — after which the +# the polluted dist as that task's cached outputs, after which the # cache faithfully restores the pollution on every rebuild, and no # amount of `rm -rf dist` alone fixes it. # # Design notes # ------------ # The candidate list comes from `git ls-files -o -i --exclude-standard -# --directory` — i.e. ONLY files git ignores. Untracked-but-not-ignored +# --directory`, which lists ONLY files git ignores. Untracked-but-not-ignored # files (your work in progress) are never touched. `git clean -x` was # deliberately avoided (it removes untracked files too), and so was # `git clean -X -e ` (exclude patterns modify the ignore rules, -# which under -X can ADD to the removal set instead of protecting — +# which under -X can ADD to the removal set instead of protecting, and # negations leak into subdirectories). Filtering a listing in shell is # boring and predictable. # # What is deliberately KEPT: -# * node_modules — reinstall is slow and pnpm's store makes staleness -# here rare (pass --all when you want it gone too); -# note the Turbo cache lives at .turbo/, which IS -# removed. -# * .env*, *.local — local secrets/config (e.g. VERCEL_OIDC_TOKEN pulled -# via `vercel env pull`). -# * .vercel — Vercel project links; losing them forces relinking. -# * target/ — Cargo's build cache; unlike the JS tooling above, -# Cargo fingerprints inputs correctly across branch -# switches, and a cold Rust rebuild of the SWC plugin -# costs minutes (pass --all to remove it anyway). -# * .opencode, .claude, .agents — local agent tooling state. +# * node_modules: reinstall is slow and pnpm's store makes staleness +# here rare (pass --all when you want it gone too); +# note the Turbo cache lives at .turbo/, which IS +# removed. +# * .env*, *.local: local secrets/config (e.g. VERCEL_OIDC_TOKEN pulled +# via `vercel env pull`). +# * .vercel: Vercel project links; losing them forces relinking. +# * target/: Cargo's build cache; unlike the JS tooling above, +# Cargo fingerprints inputs correctly across branch +# switches, and a cold Rust rebuild of the SWC plugin +# costs minutes (pass --all to remove it anyway). +# * .opencode, .claude, .agents: local agent tooling state. set -euo pipefail cd "$(dirname "$0")/.." @@ -71,7 +71,9 @@ for arg in "$@"; do --dry-run | -n) DRY_RUN=1 ;; --all) ALL=1 ;; --help | -h) - sed -n '2,66p' "$0" | sed 's/^# \{0,1\}//' + # Print the header comment block (everything between the shebang and + # the first non-comment line), stripped of the leading "# ". + awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$0" exit 0 ;; *) @@ -123,7 +125,7 @@ done < <(git ls-files -o -i --exclude-standard --directory) echo if [[ "$DRY_RUN" -eq 1 ]]; then - echo "Dry run — nothing removed ($removed candidates, $kept kept). Re-run without --dry-run to clean." + echo "Dry run: nothing removed ($removed candidates, $kept kept). Re-run without --dry-run to clean." else echo "Cleaned $removed artifacts ($kept kept). Next steps:" echo ' pnpm install'