{"owner":"dcastil","repo":"tailwind-merge","hasSkills":true,"totalSkillsCount":4,"totalTokensCount":14863,"categories":["root-instruction","subagent-persona"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","agents/tailwind-css-version-update.md","agents/tailwind-merge-internals.md","agents/release-workflow.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nThis repository is a pnpm monorepo hosting `tailwind-merge` — a TypeScript library that merges Tailwind class strings by removing style conflicts while preserving non-conflicting and non-Tailwind classes — and its sibling packages. Packages are independent: each owns its lint, test, and type-check setup and releases on its own schedule.\n\n## Primary Goals\n\n- Keep merge behavior correct for the supported Tailwind versions listed in the library documentation.\n- Keep runtime fast (browser-oriented hot path, lazy init, lightweight cache).\n- Keep API and type contracts stable.\n\n## Repo Map\n\n- The library package: `packages/tailwind-merge/` — source in `src/`, tests in `tests/`, human docs in `docs/`, build/release scripts in `scripts/`, published overview `README.md` (autogenerated from `docs/README.md`).\n- Unreleased workspace packages: `packages/configurator/` generates tailwind-merge configs from Tailwind v4 CSS; `packages/plugin-core/` adds what every bundler plugin needs on top (CSS root discovery, runtime-module generation, the generation session); `packages/vite/` and `packages/next/` wrap both for Vite and Next.js with automatic generation and usage pruning. The configurator and the plugins have a maintained `README.md` and user documentation in `docs/`; the plugin core is internal and inlined into both plugins.\n- Repo root: workspace orchestration only — `pnpm-workspace.yaml` (workspace list + shared tool-version catalog), `eslint.config.base.mjs` (shared lint base every scope spreads), root `eslint.config.mjs` (lints root files and `.github/`), root `vitest.config.mts` (aggregates the packages' Vitest projects plus the local GitHub actions' tests), repo-wide release tooling in `scripts/` (`update-pinned-links.mjs`, `pin-readme-links.mjs`, `stamp-dev-version.mjs`, `check-library-release.mjs`), and the repo-level `README.md`.\n- CI workflows: `.github/workflows/`\n- Local GitHub actions: `.github/actions/`\n\n## Agent Docs Map\n\n- `agents/tailwind-merge-internals.md`: library architecture, file ownership, shared testing/build behavior, and CI/security guardrails.\n- `agents/configurator.md`: configurator goals, strategy, generation/classification architecture, correctness gates, and development follow-ups.\n- `agents/plugin-core.md`: the shared plugin core — discovery, generation, and session invariants, testing, and its inlined packaging.\n- `agents/vite-plugin.md`: Vite integration goals, runtime resolution, watching/failure contracts, packaging, and test coverage.\n- `agents/next-plugin.md`: Next.js integration goals, the loader-rule mechanism and its Turbopack/webpack facts, testing harness, packaging, and test coverage.\n- `agents/configurator-performance.md`: dated measurements, optimization rationale, and reproduction methodology.\n- `agents/tailwind-css-version-update.md`: Tailwind CSS version support workflow.\n- `agents/release-workflow.md`: changelog authoring, GitHub release text, sponsor handling, and release comments on issues/PRs.\n\n## Environment and Commands\n\n- Package manager: `pnpm` with a workspace rooted at `pnpm-workspace.yaml`\n- Local development runtime: Node `22.18.0` or newer on a supported LTS release, satisfying the requirements of pnpm 11 and the Babel 8 build toolchain\n- CI runtime: Node `24.18.1`, pinned via `node-version` in the workflows under `.github/workflows/` (Renovate bumps it there; treat the workflows as the source of truth if this number looks stale)\n- Consumer TypeScript support: TypeScript `3.8` and newer; `pnpm test:exports` verifies the generated declarations with the pinned minimum compiler\n- Dependency supply-chain guardrail: `minimumReleaseAge: 4320` in `pnpm-workspace.yaml`, so newly published package versions must be at least three days old before pnpm installs them. Renovate has a matching cooldown in `.github/renovate.json` (see `agents/tailwind-merge-internals.md` for how the two must stay in sync).\n- Dependency build scripts are denied by default unless explicitly allowed in `pnpm-workspace.yaml`; `esbuild` is currently allowed because `.github/actions/metrics-report` needs it.\n- The root manifest's `devEngines.runtime` must encode the real documented Node floor (currently `>=22.18.0`), not a rounded-down one: pnpm validates dependency `engines` against the range's minimum version, and a floor looser than a dependency's own requirement makes pnpm silently skip optional dependencies during install — this is how rolldown's platform binary went missing under an earlier `>=22`, with only a debug-level log mentioning it.\n\nCore commands (repo root):\n\n- `pnpm install --frozen-lockfile`\n- `pnpm lint` — lints root files, then every package's own lint script (`pnpm --recursive lint`)\n- `pnpm test` — runs every package's Vitest project plus the local GitHub actions' tests through the root projects config, with coverage\n- `pnpm test:types` — every package's own tsc project (`pnpm --recursive test:types`)\n- `pnpm test:watch` — cross-package watch mode\n\nLibrary-specific commands (run with `pnpm --filter tailwind-merge <script>` from anywhere, or plain `pnpm <script>` inside `packages/tailwind-merge/`):\n\n- `build`\n- `test:exports`\n- `bench`\n\nVite-plugin-specific commands (run with `pnpm --filter @tailwind-merge/vite <script>`, or plain `pnpm <script>` inside `packages/vite/`):\n\n- `build` — tsdown, ESM bundles plus declarations for all three subpaths into `dist/`\n- `test:exports` — packs the tarball and verifies the published shape; requires both this package's and the library's `build` to have run first (see [Vite build and packaging](./agents/vite-plugin.md#build-and-packaging) for what it checks)\n\nNext.js-plugin-specific commands (run with `pnpm --filter @tailwind-merge/next <script>`, or plain `pnpm <script>` inside `packages/next/`):\n\n- `build` — tsdown, ESM bundles plus declarations for the plugin entry, the loader, and the runtime into `dist/`; the package's `exports` point at `dist/` even in the workspace, and its test global setup runs this build\n- `test` — spawns real `next dev`/`next build` processes for both bundlers (about two minutes); needs the library built, which the global setup does when `packages/tailwind-merge/dist` is missing\n- `test:exports` — packs the tarball and verifies the published shape including a real `next build` of a consumer app; requires both this package's and the library's `build` to have run first (see [Next.js build and packaging](./agents/next-plugin.md#build-and-packaging))\n\nMonorepo conventions:\n\n- Shared tool versions (eslint, typescript, vitest, @types/node) live in the `catalog:` section of `pnpm-workspace.yaml`; package manifests reference them as `\"catalog:\"`. Single-consumer dependencies stay pinned in their package manifest.\n- Every package lints with its own `eslint.config.mjs` spreading `eslint.config.base.mjs` from the root, and tests with its own `vitest.config.mts`; the root configs only orchestrate.\n- No task runner (turborepo or similar) and no affected-only CI on purpose: at the current package count the full suite is fast, and running everything keeps cross-package effects (a tailwind-merge or plugin-core change must exercise both plugins' tests) trivially correct. Revisit when the package count grows or build times make caching pay off.\n- Releases are per package with `<package-name>@<version>` git tags (for example `tailwind-merge@3.7.0`); see `agents/release-workflow.md`.\n\n## Working Rules for Changes\n\n1. Treat `packages/tailwind-merge/src/lib/default-config.ts` as the behavioral source of truth for default class groups and conflicts.\n2. If behavior changes, update tests in `packages/tailwind-merge/tests/` in the same change.\n3. If public behavior/docs change, update docs in `packages/tailwind-merge/docs/` (not directly in a `README.md`).\n4. The library's `README.md` and the generated section of the repo-level `README.md` are version-generated from the library's `docs/README.md`; do not regenerate them during normal development changes. The configurator, Vite, and Next.js READMEs are maintained landing pages, not generated files; edit them alongside their own `docs/` pages.\n5. Avoid manual edits to `dist/` and `coverage/` directories; they are generated artifacts.\n6. Validate packaging paths with `pnpm --filter tailwind-merge build && pnpm --filter tailwind-merge test:exports` when touching exports/build tooling.\n\n## Documentation Sync Policy\n\nTreat documentation updates as part of the same change, not as follow-up work.\n\nSelf-improvement is required while working in this repo: whenever you discover useful information about architecture, workflows, commands, constraints, debugging findings, or recurring pitfalls, add it to the appropriate file in `agents/` or to `AGENTS.md` in the same change. When existing agent guidance becomes stale, misleading, or irrelevant, remove or revise it immediately.\n\nAvoid repeating the same guidance in both `AGENTS.md` and specialized `agents/*` docs unless the rule is critically important. Prefer keeping `AGENTS.md` as the high-level entry point and putting detailed workflow, architecture, release, or CI notes in the relevant specialized agent document.\n\nCross-package user-docs convention: every package's docs recommend setting `twMerge` up in one project-owned file (shown as `tw-merge.ts`) and importing it from there, even without configuration, so that configuring, wrapping, or replacing `twMerge` later is a one-file change. The canonical explanation lives in the library's `docs/configuration.md` (\"Import `twMerge` from one place\"); the plugin and configurator getting-started pages carry their own short version. Keep new setup instructions and usage examples consistent with it.\n\nIt is acceptable to periodically restructure agent documentation when files become too large, when topics are hard to find, or when the current organization no longer matches how the repo is maintained. Split files, rename sections, or move information between `AGENTS.md` and `agents/*` as needed, while preserving useful guidance and removing stale duplication.\n\nRequired when relevant:\n\n1. Update `packages/tailwind-merge/docs/*.md` (or the owning package's docs) for any user-visible behavior, API, version support, or limitation changes.\n2. Update `AGENTS.md` whenever repository-wide agent workflow, repo conventions, required commands, or high-level guardrails change.\n3. Update the relevant file in `agents/` whenever specialized architecture, workflow, CI, release, or testing guidance changes.\n4. Do not regenerate the READMEs during normal development. The library `README.md` and the marked section of the repo-level `README.md` are intentionally generated as part of the version/release flow so visitors are routed to docs for the latest release.\n5. Never reference repo files through `blob/main`/`raw/main`/`tree/main` URLs — they break whenever a file moves. Within the repo use relative links; where a link must be absolute (published docs, JSDoc, release notes), pin it to the latest release tag whose tree contains the file (e.g. `blob/v3.6.0/docs/...`, `blob/tailwind-merge@3.7.0/packages/tailwind-merge/docs/...`). Links pinned to a package's latest tag are re-pinned automatically at that package's next release by `scripts/update-pinned-links.mjs` (run in each publishable package's `version` lifecycle); links pinned to older tags are treated as deliberately historical and never touched. Two escape hatches for historical links that sit at the latest tag: changelog directories (`docs/changelog/`) are never scanned, and a link carrying the `twm-historical` query parameter (e.g. `blob/v3.6.0/docs/foo.md?twm-historical`, before any `#fragment`) stays untouched — GitHub ignores the parameter when serving the page.\n\nDefinition of done for every PR/change:\n\n1. Code and tests are updated.\n2. Relevant docs are updated in the same change set.\n3. `AGENTS.md` and `agents/*` guidance is reviewed, with useful new context added and stale guidance removed whenever the change affects how agents should work in this repo.\n\n## Quick Validation Matrix\n\nAll test paths below are relative to `packages/tailwind-merge/`; run single files with `pnpm --filter tailwind-merge test:watch tests/<file>`.\n\n- Parser or modifier semantics: run `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts`.\n- Class groups/conflicts/default config: run `tests/default-config.test.ts`, `tests/class-group-conflicts.test.ts`, `tests/tailwind-css-versions.test.ts`.\n- Public API/types: run `tests/public-api.test.ts`, `tests/type-generics.test.ts`, and `pnpm --filter tailwind-merge test:types`.\n- Release/package surface: run `pnpm --filter tailwind-merge build` and `pnpm --filter tailwind-merge test:exports`.\n- Configurator, plugin core, or a plugin: run `pnpm --filter @tailwind-merge/configurator test` / `pnpm --filter @tailwind-merge/plugin-core test` / `pnpm --filter @tailwind-merge/vite test` / `pnpm --filter @tailwind-merge/next test` and the matching `test:types` script. A core or configurator change must run both plugin suites.\n- Plugin build/packaging surface: run `pnpm --filter tailwind-merge build`, then the plugin's `build` and `test:exports` (`pnpm --filter @tailwind-merge/vite …` or `pnpm --filter @tailwind-merge/next …`).\n- Repo-wide before finalizing: `pnpm lint`, `pnpm test:types`, `pnpm test` from the root.\n","agents/tailwind-css-version-update.md":"# Tailwind CSS version update workflow\n\nUse this guide when updating tailwind-merge for a new Tailwind CSS version.\n\nAll `src/`, `tests/`, and `docs/` paths in this file are relative to the library package `packages/tailwind-merge/`; run the quoted commands from that directory (or through `pnpm --filter tailwind-merge`).\n\nThe main risk in this work is false confidence: Tailwind release notes, deployed docs, source code, and website docs can land at different times. Treat every feature as something to verify from official sources before changing `src/lib/default-config.ts` or parser behavior.\n\n## Official sources\n\n- Tailwind CSS releases: https://github.com/tailwindlabs/tailwindcss/releases\n- Tailwind CSS source and PRs: https://github.com/tailwindlabs/tailwindcss\n- Tailwind CSS release tag pattern: `https://github.com/tailwindlabs/tailwindcss/releases/tag/vX.Y.Z`\n- Tailwind CSS deployed docs: https://tailwindcss.com/docs\n- Tailwind CSS website source and docs PRs: https://github.com/tailwindlabs/tailwindcss.com\n- Prior tailwind-merge support PRs for comparison, for example v4.2 support: https://github.com/dcastil/tailwind-merge/pull/651\n\nWhen docs for a just-released Tailwind version are not deployed yet, use the `tailwindcss.com` repository and its PRs to find future docs pages, sidebar order, and link targets.\n\n## Investigation checklist\n\n1. Read the Tailwind CSS release notes for the target version.\n2. Open every linked Tailwind CSS PR that mentions utilities, variants, parser syntax, arbitrary values, or behavior changes.\n3. Inspect Tailwind CSS source and tests when release notes or PR descriptions are ambiguous. Do not infer support for arbitrary values or variables from naming patterns alone.\n4. Check the `tailwindcss.com` repository for docs PRs when deployed docs lag behind the release.\n5. Compare against the previous tailwind-merge Tailwind-support PR to reuse the same structure and testing style.\n6. Make a feature list before coding. Split work into simple utility additions, ordering/docs updates, conflict changes, and parser/config API changes.\n7. Record no-op decisions in the PR or issue summary, especially when a Tailwind PR looked relevant but does not require a tailwind-merge change.\n\n## Triage questions for each Tailwind change\n\n- Does it emit new class names? If yes, it likely needs `classGroups` entries.\n- Does it change which utilities conflict? If yes, update `conflictingClassGroups` or `conflictingClassGroupModifiers`.\n- Does it add a new arbitrary value or arbitrary variable form? Verify the exact accepted syntax in Tailwind source before adding validators.\n- Does it add a slash form that is part of the base class rather than a postfix modifier? Prefer config-driven lookup such as `postfixLookupClassGroups` before changing parser logic.\n- Does it change modifier, variant, prefix, important, or arbitrary-variant syntax? Parser changes are higher risk and need focused parser/modifier tests.\n- Does it only change generated CSS for an existing class without changing class syntax or conflicts? It may not require a tailwind-merge change.\n- Does it remove or rename classes? Check whether keeping old classes would create incorrect merges for the supported Tailwind range.\n\n## Default config ordering and links\n\nKeep `src/lib/default-config.ts` ordered according to the Tailwind CSS docs sidebar, not alphabetically and not by implementation convenience.\n\nFor new utility groups:\n\n- Use `tailwindcss.com` or the website repository to find the final docs URL for each `@see` comment.\n- If the public docs are not deployed, inspect docs PRs in `tailwindlabs/tailwindcss.com`.\n- Replace temporary `TBD` links before finishing the change.\n- Keep related utility groups in sidebar order even when they share a prefix.\n\nThere is an important exception to sidebar order: class-group order affects validator precedence for groups that share the same prefix path. The class map tries exact child paths first, then runs validators in the order they were registered. If a broad validator appears before a more specific validator for the same prefix, the broad group can claim the class before the specific group sees it.\n\nWhen groups with the same prefix use validators:\n\n- Put the most specific validators first.\n- Put broad or catch-all validators last for that prefix.\n- Treat color validators as especially sensitive because they often accept arbitrary values or otherwise broad value sets.\n- Prefer correctness over strict sidebar order inside that shared-prefix cluster.\n\nFor example, if a `text-*` color group uses a broad color validator, keep it after more specific `text-*` groups such as font-size-related groups so ambiguous classes resolve to the intended class group.\n\nExamples learned during the Tailwind CSS v4.3 update:\n\n- `tab-size` belongs after `text-indent` and before `vertical-align`.\n- `zoom` belongs after `translate`.\n- Scrollbar groups should follow the sidebar order for scrollbar color, scrollbar gutter, and scrollbar width.\n- PRs that look relevant to existing utilities, such as changes around `start` and `end`, still need source review before changing tailwind-merge.\n\n## Implementation guidance\n\nUse `src/lib/default-config.ts` as the behavioral source of truth for utilities and conflicts.\n\nPrefer config changes over parser changes. Parser and merge hot paths run frequently in UI renders, so keep extra work small and opt-in:\n\n- Normalize config arrays into lookup objects in `createConfigUtils` when they need fast querying.\n- Avoid scanning or balancing brackets in validators when a cheap prefix or character check can reject most inputs.\n- Put specialized syntax checks in `src/lib/validators.ts` when they are reusable or need direct tests.\n- Add comments only for non-obvious behavior, especially when a config entry compensates for parser ambiguity.\n\nFor slash syntax, remember that tailwind-merge initially treats the part after `/` as a possible postfix modifier. If the full class can belong to a different group, use a generic config mechanism rather than a syntax-specific parser branch.\n\nThe Tailwind CSS v4.3 named container query support is the reference pattern:\n\n- `@container-size` is a container type class.\n- `@container-size/sidebar` combines container type and container name.\n- The parser already exposes a possible postfix modifier, so no bracket-counting parser change is needed.\n- `container-named` creates a conflict with `container-type`, but not the reverse, because a later type-only class should preserve the earlier container name.\n- `postfixLookupClassGroups: ['container-type']` tells merge logic to try resolving the full slash class only after the pre-slash class resolves to `container-type`.\n\n## Testing checklist\n\nAdd or update tests based on the kind of change:\n\n- Tailwind version compatibility: `tests/tailwind-css-versions.test.ts`\n- Default config class map coverage: `tests/class-map.test.ts`\n- Class group conflicts: `tests/class-group-conflicts.test.ts` and `tests/conflicts-across-class-groups.test.ts`\n- Parser or modifier semantics: `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts`\n- Validators: `tests/validators.test.ts`\n- Public exports and config API: `tests/public-api.test.ts`, `tests/type-generics.test.ts`, `tests/merge-configs.test.ts`\n- Docs examples: `tests/docs-examples.test.ts`\n\nFor new utility support, include tests for:\n\n- Last class wins within the new class group.\n- Cross-group conflicts in both orders when conflicts are asymmetric.\n- Arbitrary values and arbitrary variables only when Tailwind source confirms support.\n- Important modifier and variant combinations when parser or conflict behavior is involved.\n- Unknown or invalid classes remaining untouched.\n\nFor TypeScript config changes, remember that `pnpm tsc --noEmit` only checks `src`. Use the test tsconfig as an additional check when touching public types:\n\n```sh\npnpm tsc --noEmit -p tests/tsconfig.json --moduleResolution bundler\n```\n\nThe plain `tests/tsconfig.json` currently inherits `moduleResolution: Node`, which can surface Vitest package export resolution errors unrelated to tailwind-merge types.\n\n## Documentation checklist\n\nUpdate docs in the same change when behavior or public API changes:\n\n- `docs/configuration.md` for config behavior.\n- `docs/api-reference.md` for exported validators, config fields, or public types.\n- `docs/changelog/v3-changelog.md` when preparing a release note.\n- `AGENTS.md` and `agents/*` when the update process or repo guidance changes.\n\nDo not regenerate `README.md` during normal development; it is generated from `docs/README.md` during the release flow.\n\n## Validation checklist\n\nRun targeted tests while iterating, then run the full validation set before finalizing:\n\n```sh\npnpm lint\npnpm tsc --noEmit\npnpm test\ngit diff --check\n```\n\nRun `pnpm build && pnpm test:exports` when touching package exports, build tooling, or generated declaration behavior.\n","agents/tailwind-merge-internals.md":"# tailwind-merge internals guide\n\nThis file is a practical map for agent-driven changes in this repo.\n\nSince the 2026-08-10 monorepo migration the library lives in `packages/tailwind-merge/`; all `src/`, `tests/`, `docs/`, and `scripts/` paths in this file are relative to that package directory unless a path starts with `packages/` or `.github/`.\n\nLinks from the library's `docs/` to repository-wide files must traverse three parents; for example, `../../../.github/CONTRIBUTING.md`. Resolve relative links from the owning document after moving files, not from the repository root.\n\n## How merge execution works\n\n1. `twMerge` in `src/lib/tw-merge.ts` is created via `createTailwindMerge(getDefaultConfig)`.\n2. `createTailwindMerge` in `src/lib/create-tailwind-merge.ts`:\n    - lazily builds config utils on first call,\n    - joins inputs via `twJoin`,\n    - caches final joined-input results,\n    - calls `mergeClassList` on cache miss.\n3. `mergeClassList` in `src/lib/merge-classlist.ts`:\n    - parses classes,\n    - walks from right to left so \"last class wins\",\n    - resolves class group IDs and asymmetric conflicts,\n    - keeps non-Tailwind/external classes untouched.\n4. `createClassGroupUtils` in `src/lib/class-group-utils.ts`:\n    - compiles class definitions into a recursive map + validator list,\n    - maps class names to class group IDs,\n    - assigns arbitrary properties (`[key:value]`) synthetic group IDs prefixed with `ARBITRARY_PROPERTY_PREFIX` (e.g. `arbitrary..key`) so distinct properties conflict independently,\n    - returns conflicting groups, including postfix-modifier conflicts.\n5. `createParseClassName` in `src/lib/parse-class-name.ts`:\n    - parses stacked modifiers with bracket/paren depth tracking,\n    - supports important modifier (`!`) and legacy v3 position,\n    - supports optional prefix and `experimentalParseClassName` override.\n6. `createSortModifiers` in `src/lib/sort-modifiers.ts`:\n    - alphabetically sorts non-sensitive modifiers,\n    - preserves placement around arbitrary/order-sensitive modifiers.\n\nThe unstable tooling entry point also exports `createClassGroupLookup` from `src/lib/class-group-lookup.ts`. It runs one candidate through `mergeClassList` with an observing class-map function and returns every successful lookup, including a slash class's intermediate base. The configurator uses this to preserve lookup dependencies during pruning. Keep this instrumentation outside the normal merge path; it is an adapter for build-time inspection, not a second implementation of modifier or lookup semantics. Both built ESM/CJS export checks exercise it.\n\n## Performance constraints in hot-path code\n\nThe hot paths are `src/lib/merge-classlist.ts` and `src/lib/class-group-utils.ts`; `src/lib/parse-class-name.ts` runs per class on cache misses. When editing these files, preserve the existing V8-oriented patterns:\n\n- Keep call sites monomorphic: functions are deliberately split per input type (see the `create*` factories and the \"monomorphic\" comments in `class-group-utils.ts` and `parse-class-name.ts`); do not merge them back into one polymorphic function.\n- Keep object shapes consistent: always define optional properties explicitly (as `undefined`/`null`) so objects created in different branches share a hidden class; result objects are built through factory functions for this reason.\n- Pre-allocate arrays where the final length is known (see `concatArrays` in `src/lib/utils.ts`) instead of spreading or pushing, and return shared constants like `EMPTY_CONFLICTS` instead of fresh empty arrays.\n- Avoid adding function calls or allocations in hot loops when an inline check works.\n\n## Files to edit by intent\n\n- Add or change Tailwind utility support:\n    - `src/lib/default-config.ts`\n    - Usually paired with tests in `tests/tailwind-css-versions.test.ts` and focused utility tests.\n    - For a full Tailwind CSS version update, follow `agents/tailwind-css-version-update.md`.\n\n- Change conflict behavior:\n    - `src/lib/default-config.ts` (`conflictingClassGroups`, `conflictingClassGroupModifiers`)\n    - `tests/class-group-conflicts.test.ts`\n    - `tests/conflicts-across-class-groups.test.ts`\n\n- Change parser/modifier behavior:\n    - `src/lib/parse-class-name.ts`\n    - `src/lib/sort-modifiers.ts`\n    - `tests/modifiers.test.ts`\n    - `tests/arbitrary-variants.test.ts`\n    - `tests/experimental-parse-class-name.test.ts`\n\n- Change config extension APIs:\n    - `src/lib/extend-tailwind-merge.ts`\n    - `src/lib/create-tailwind-merge.ts`\n    - `src/lib/merge-configs.ts`\n    - `tests/create-tailwind-merge.test.ts`\n    - `tests/extend-tailwind-merge.test.ts`\n    - `tests/merge-configs.test.ts`\n    - `tests/type-generics.test.ts`\n\n- Change validators:\n    - `src/lib/validators.ts`\n    - `tests/validators.test.ts`\n    - Any impacted arbitrary-value tests\n\n## Testing strategy in this repo\n\n- Unit/behavior tests are granular by feature area in `tests/*.test.ts`.\n- Color-function regression tests should verify preservation of length utilities and replacement of other colors in `tests/colors.test.ts`.\n- `tests/tailwind-css-versions.test.ts` is the compatibility anchor for Tailwind feature coverage by version.\n- `tests/docs-examples.test.ts` enforces that documented `twMerge(...) // -> ...` examples stay correct. Its extraction regex supports a limited set of characters (for example, `/` in arguments is excluded); verify new examples are collected and update the assertion count when adding supported examples.\n- `tests/public-api.test.ts` guards runtime exports and broad type usage.\n- `tests/tw-merge.benchmark.ts` is for performance benchmarking (`pnpm bench`), not correctness gating in CI.\n\nRun a single test file or test by name with Vitest's filter syntax: `pnpm --filter tailwind-merge test:watch tests/modifiers.test.ts` or `pnpm --filter tailwind-merge test:watch -t \"test name pattern\"`.\n\nPackage-specific generation, scanning, and Vite test guidance lives in [configurator development](./configurator.md#debugging-and-validation) and [Vite plugin development](./vite-plugin.md#testing-and-remaining-coverage).\n\nVitest is set up as one project per package: each package's `vitest.config.mts` carries its aliases and plugins, and the root `vitest.config.mts` aggregates them (plus an inline project for `.github/actions/**/*.test.mjs`, since the local actions aren't workspace packages) so root `pnpm test` runs everything with coverage. Because collection is scoped to those projects, stray test files elsewhere in the repo — notably full repo copies in `.claude/` worktrees, which the pre-monorepo root config silently collected and ran as duplicates — never join the suite.\n\nThe root coverage configuration measures `packages/*/src/**/*.ts`, including the configurator and Vite plugin. In projects mode, package-local coverage settings do not control the aggregate report. Coverage is diagnostic, with no percentage threshold; use uncovered branches to guide behavioral tests rather than weakening assertions or padding the percentage. The Vitest report does not capture code executed by spawned CLI/build processes, so interpret unmeasured entrypoints alongside their integration tests.\n\nRecommended local sequence for non-trivial changes:\n\n1. `pnpm lint`\n2. `pnpm test:types`\n3. `pnpm test`\n4. `pnpm --filter tailwind-merge build`\n5. `pnpm --filter tailwind-merge test:exports`\n\n## Build and packaging\n\n- Build uses Rollup (`scripts/rollup.config.mjs`) to produce:\n    - ESM and CJS bundles,\n    - ES5 variants,\n    - unified type declarations.\n- The `build` script passes Rollup `--forceExit` because the TypeScript Rollup plugin can leave referenced file-watch handles open after a non-watch build has already written all outputs.\n- Babel 8 performs the final target-specific transforms for all four JavaScript bundles. Its `@babel/preset-env` `bugfixes` behavior is unconditional, and the removed `loose: true` option is preserved through top-level assumptions plus the `transform-typeof-symbol` exclusion in `scripts/rollup.config.mjs`. The config deliberately replaces the migration guide's `arrayLikeIsIterable` with `iterableIsArray` because Babel disallows enabling both and every transformed spread and destructuring operand is an array; this suppresses generic iterable helpers and keeps the generated JavaScript bundles equivalent in size and behavior to the Babel 7 bundles.\n- TypeScript 6 requires the source `rootDir` to be explicit when declaration output is redirected by Rollup, so `tsconfig.json` fixes it to `src`. Module resolution uses `Bundler`, matching the Rollup build and avoiding the deprecated `Node`/`node10` resolver.\n- Export smoke tests:\n    - `scripts/test-built-package-exports.cjs`\n    - `scripts/test-built-package-exports.mjs`\n    - Both scripts execute the default and ES5 bundles so build-tool upgrades cannot break only the legacy entry point unnoticed.\n- `pnpm test:types` type-checks both the source and the test suite with the development compiler. `scripts/typescript-compatibility/consumer.ts` separately compiles the built declaration bundle with the `typescript-compat` compiler alias pinned to TypeScript 3.8.3. `pnpm test:exports` includes the compatibility check because TypeScript 3.8 is the documented minimum consumer version; keep the alias excluded from Renovate updates and treat any increase to this floor as a major-version change.\n- `scripts/update-readme.mjs` (run in the `version` script via `zx`) generates two targets from `docs/README.md`: the package `README.md` shipped to npm, and the section between the `tailwind-merge-docs` markers in the repo-level `README.md`; it fails the version step loudly when the repo README's markers are missing.\n- The `version` script also runs the repo-wide `scripts/update-pinned-links.mjs` (repo root, dependency-free Node; the vite package additionally runs `scripts/pin-readme-links.mjs`, see the release guide), which re-pins every link pinned to the package's newest existing tag onto the tag being released, verifying targets against the working tree and failing loudly — without touching anything — when a linked path no longer resolves. Older-tag links are deliberately historical and stay untouched, changelog directories are excluded from scanning entirely, and the `twm-historical` query parameter on a link shields it individually for the rare historical link elsewhere.\n\nFor the Vite package's tsdown build, workspace/published export split, and packed-consumer gate, read [Vite build and packaging](./vite-plugin.md#build-and-packaging).\n\n## CI Behavior And Security\n\nTreat this section as the source of truth for CI and publish security guardrails. Keep detailed CI guidance here instead of duplicating it in `AGENTS.md` unless a rule is critical enough to be visible before opening specialized docs.\n\n- The repo uses a pnpm workspace rooted at `pnpm-workspace.yaml`; the root manifest is a private orchestrator (`tailwind-merge-monorepo`), the library is the `packages/tailwind-merge` workspace package, and `.github/actions/metrics-report` is a workspace package so action dependencies share the root lockfile.\n- CI steps that target the library use `pnpm --filter tailwind-merge <script>`; build artifacts live at `packages/tailwind-merge/dist`, and `npm publish`/version-stamping steps run with `working-directory: packages/tailwind-merge` because npm operates on the manifest in its working directory.\n- The metrics-report action measures the PR base branch by checking it out in place, so `.github/actions/metrics-report/src/utils/path.mjs` resolves the library root per call, falling back to the repo root for pre-monorepo base branches where the library manifest still sat there. `pnpm --filter tailwind-merge build` works on both layouts because the pre-monorepo root package carried the same name.\n- pnpm 11 requires Node 22 or newer and no longer reads pnpm-specific settings from `.npmrc` or the `pnpm` field in `package.json`; keep repository settings in `pnpm-workspace.yaml`.\n- CI enables Corepack so the root `packageManager` pin selects the pnpm version before every frozen install. For pnpm major updates, run both `pnpm install` and `pnpm install --frozen-lockfile` with the new pinned version, and commit any lockfile migration produced by the non-frozen install.\n- `pnpm-workspace.yaml` sets `minimumReleaseAge: 4320` (minutes, i.e. three days) with `minimumReleaseAgeStrict: true`, so dependency versions must be at least three days old before pnpm installs them. It also allows the `esbuild` dependency build script because the metrics action imports esbuild directly.\n- `.github/renovate.json` sets `minimumReleaseAge: \"4 days\"` (all datasources) so Renovate never proposes versions that pnpm strict mode would reject during lockfile updates; the extra day over pnpm's three days is a deliberate buffer against boundary races. If the pnpm cooldown changes, keep the Renovate value at least as large. Renovate exempts vulnerability alerts from its cooldown, but pnpm strict mode still blocks immature versions at install time; use pnpm's `minimumReleaseAgeExclude` if a security fix must land early.\n- `.github/workflows/test.yml` runs `pnpm lint`, `pnpm test:types`, `pnpm test`, `pnpm --filter tailwind-merge build`, `pnpm --filter tailwind-merge test:exports`, then each plugin's `build` and `test:exports` (`@tailwind-merge/vite`, then `@tailwind-merge/next`) — the plugin steps after the library build because the packed-tarball gates resolve tailwind-merge through its published dist and types. The Next.js package's Vitest project spawns real Next.js processes and builds the library itself when its dist is missing (its global setup), so `pnpm test` stays independent of the build steps' order.\n- `.github/workflows/benchmark.yml` runs `pnpm --filter tailwind-merge bench` with CodSpeed tokenless uploads for this public repository; do not pass static CodSpeed upload tokens to jobs that execute PR benchmark code.\n- `.github/workflows/codeql-analysis.yml` runs CodeQL for both JavaScript/TypeScript and GitHub Actions workflows; the JavaScript job builds the library and both plugins with explicit package filters, since the workspace root has no build script. The Actions analysis uses the `security-extended` query suite to catch workflow-specific security issues. It also runs on PRs that touch `.github/actions/**` or `.github/workflows/**` so CI-control-plane changes are scanned before merge.\n- Every workflow should declare explicit least-privilege `permissions`; read-only build/test jobs use `contents: read`, and write scopes should appear only on the jobs that need them.\n- Pin every `actions/checkout` use to an immutable commit and set `persist-credentials: false` unless the job must push commits or tags through git; checkout receives the job token even in read-only jobs.\n- `actions/checkout` v7+ blocks fork pull request code under `pull_request_target` and PR-triggered `workflow_run`; never set `allow-unsafe-pr-checkout: true`. Keep `label.yml` checkout-free, keep the metrics comment job on the base SHA, and keep release-comment workflow runs restricted to successful `push` events on `main`.\n- Third-party GitHub Actions that receive secrets or write-capable tokens are pinned to full commit SHAs with comments showing the source tag or branch.\n- Artifact handoffs intentionally pair `actions/upload-artifact@v7` with `actions/download-artifact@v8` because the two actions have different latest major versions; do not try to align their version numbers unless upstream releases change.\n- Local JavaScript GitHub Actions use `runs.using: node24`; keep action scripts compatible with the declared runner and avoid runtime-fragile ESM features, such as JSON module import assertion syntax, when a simple filesystem read works across supported Node versions.\n- `.github/workflows/metrics-report.yml` keeps PR code execution in a read-only `generate-report` job and posts comments from a separate `post-comment` job that checks out trusted base-repo code. The metrics report action itself only writes the generated comment body to the artifact path and must not post PR comments directly. If a transition PR introduces or moves the trusted posting script, the workflow should skip the `post-comment` job at the job level until that script exists in the base checkout rather than running PR-provided posting code with write permissions.\n- `.github/workflows/npm-publish.yml`:\n    - publishes `dev` tag builds of tailwind-merge and both plugins on `main` pushes: one build job builds all three (library first, topologically) and runs every `test:exports` gate, one OIDC job stamps the manifests with `scripts/stamp-dev-version.mjs` (dev version plus the plugins' exact pins on the library's same-commit dev version) and publishes the library before the plugins. The plugin core and the configurator have no dev release; they ship inlined in the plugin bundles. A new package joins these jobs only after its npm trusted publisher exists, since a publish step without one fails on every push (bootstrap in the release guide),\n    - publishes production on release events, routing by the namespaced tag prefix (`tailwind-merge@*` and legacy `v*` map to `packages/tailwind-merge`, `@tailwind-merge/vite@*` to `packages/vite`, `@tailwind-merge/next@*` to `packages/next`, anything else fails),\n    - the release build job builds the library alongside the released package (`pnpm --filter tailwind-merge --filter \"$PACKAGE_NAME\" build` — a plugin's test:exports needs the library's published shape; duplicate filters collapse for tailwind-merge releases, and pnpm's topological order builds the library before a plugin) and runs the released package's `test:exports` unconditionally, plus `test:library-release` for plugin releases (registry check that the packed tailwind-merge range's lowest version ships the unstable-entry-point imports; see the Vite and Next.js guides),\n    - publishes with `pnpm publish --access public --provenance --ignore-scripts --no-git-checks` (plus `--tag dev` in the dev flow): pnpm applies `publishConfig` field overrides at pack time (the vite exports swap) and implements npm trusted publishing natively — it fetches the GitHub OIDC id-token, exchanges it per package at the registry, and publishes through npm's own libnpmpublish vendored inside pnpm. `--provenance` must be explicit because pnpm reads it from flags/config, not from `publishConfig.provenance`; `--no-git-checks` because CI checkouts are detached HEADs (and the dev flow's version stamp dirties the tree),\n    - the publish jobs enable corepack (for pnpm) and deliberately set no `registry-url` in setup-node: that input writes an `.npmrc` auth line referencing the unset `NODE_AUTH_TOKEN` env var, and pnpm fails config loading on unresolvable env references in `.npmrc`,\n    - keeps linting, tests, and builds in non-OIDC jobs,\n    - avoids dependency caches in the publish workflow,\n    - grants `id-token: write` only to minimal publish jobs that download the verified `dist` artifact and run `pnpm publish --ignore-scripts`. The release publish job first runs `pnpm install --frozen-lockfile --ignore-scripts --filter \"$PACKAGE_NAME\"` because pnpm rewrites `workspace:` specifiers at publish time by reading the linked package's manifest from `node_modules` — without it, publishing a plugin fails with `ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL`. The filter installs only the released package's dependencies, and `--ignore-scripts` keeps dependency code from running in the job. The dev publish job runs the same filtered install for both plugins because their devDependencies keep the inlined plugin core's and configurator's `workspace:*` specifiers; tailwind-merge alone would need no install, since its specifiers are all `catalog:` and resolve from `pnpm-workspace.yaml`,\n    - pins `actions/download-artifact` to an immutable commit in the OIDC jobs and keeps digest mismatches fatal so corrupted or substituted artifacts cannot reach npm publishing.\n- `.github/workflows/label.yml` uses `pull_request_target` only for labeling metadata; do not add repository checkout or PR-code execution to that workflow. `gh` commands in that workflow must pass `--repo` explicitly because there is intentionally no `.git` checkout for repository inference.\n- `.github/workflows/draft-release.yml` runs the release-drafter autolabeler under `pull_request_target` for the same reason: the plain `pull_request` token is read-only on fork PRs, so labeling fails with \"Resource not accessible by integration\" (seen on PR #689). Like `label.yml`, the `auto_label` job must stay checkout-free and never execute PR code.\n- `.github/workflows/comment-released-prs-and-issues.yml`:\n    - runs local action `.github/actions/release-commenter`,\n    - runs on `release.published`, on manual `workflow_dispatch`, and after successful `npm Publish` workflow completion for `push` events on `main`,\n    - keeps the `workflow_run` checkout on trusted default-branch code and reads the triggering main commit's package metadata through the GitHub API instead of checking out the triggering SHA,\n    - supports manual `workflow_dispatch` with optional `head_tag`, `base_tag`, `dry_run`, and `npm_package_name`.\n\n## Practical guardrails\n\n- Keep `README.md` edits synchronized with `docs/README.md`; source-of-truth is docs.\n- Do not rely on `dist/` as edit target; regenerate instead.\n- Favor targeted tests while iterating, then run full suite before finalizing.\n- Any behavior change should include at least one regression test.\n","agents/release-workflow.md":"# Release workflow for agents\n\nUse this guide when preparing release changelog entries and GitHub release text.\n\n## Release model\n\nReleases are per package. Each release belongs to exactly one workspace package and its git tag is namespaced with the package name:\n\n- `tailwind-merge@3.7.0`\n- `@tailwind-merge/vite@0.1.0`\n- `@tailwind-merge/next@0.1.0`\n\nTags without a package prefix (`v3.6.0` and earlier) are the pre-monorepo history and belong to `tailwind-merge`. The publish workflow's tag routing and `scripts/update-pinned-links.mjs` treat them that way via a fallback; release-drafter does not (see below), so the first namespaced tailwind-merge release needs its draft assembled by hand.\n\nThe tag and version-commit format is set by each package's `release` script, which wraps `pnpm version` with `--tag-version-prefix=<package-name>@` and `--message=<package-name>@%s`. pnpm 11 implements `pnpm version` natively instead of proxying to `npm version`, so it never reads npm's `tag-version-prefix`/`message` from `.npmrc`; running bare `pnpm version` yields a plain `v<version>` tag with a bare-version commit message, which the publish workflow's tag routing would treat as a legacy tailwind-merge tag. Both settings are pnpm rc options, but pnpm config is repo-wide (`pnpm-workspace.yaml`) while the prefix differs per package, hence the per-package script.\n\n## Scope\n\n- Applies to release preparation tasks like `tailwind-merge@3.4.1`.\n- Covers:\n    - Draft release ingestion from GitHub.\n    - Changelog updates in `packages/tailwind-merge/docs/changelog/*-changelog.md` (each plugin gets its own changelog location with its first release).\n    - Sponsor section generation and ordering.\n    - GitHub Releases UI text formatting.\n    - Release comments on PRs/issues via `.github/actions/release-commenter`.\n\n## Release drafting\n\n`.github/workflows/draft-release.yml` maintains one draft per package via release-drafter with per-package configs (`config-name`): `.github/release-drafter-tailwind-merge.yml`, `.github/release-drafter-vite.yml`, and `.github/release-drafter-next.yml`. Each config scopes itself with `tag-prefix` (previous-release lookup and version resolution consider only that package's tags) and `include-paths` (only PRs touching the package or its bundled source appear in its draft). Consequences to know:\n\n- A PR touching several packages appears in each of their drafts — correct, since each release ships it.\n- Both plugin scopes include `packages/plugin-core` and `packages/configurator` as well as their own directory: fixes in the shared code ship in both plugins' bundles and belong in both changelogs. The library remains a separate dependency with its own release scope.\n- A PR touching none of those directories (repo infrastructure, CI, root docs) appears in no draft; mention it by hand in a changelog if it matters to users.\n- First namespaced release per package: `tag-prefix` matches no existing release, and the pinned release-drafter (v7) then drafts from nothing rather than from full history — its previous-release lookup keeps only releases whose tag starts with the prefix, and without one it collects no pull requests at all. The draft therefore proposes version `0.1.0` and its body is the `no-changes-template` (\"No changes\") plus a warning banner, while the pre-existing unprefixed draft (`v3.6.1` for tailwind-merge) keeps the real, categorized pull request list and is never updated or removed by the drafter again. When publishing: copy the categorized list from that orphaned draft into the new one, then delete the orphaned draft; set the tag name and title by hand (for example `tailwind-merge@3.7.0` following `v3.6.0`); and write the Full Changelog compare line manually (`v3.6.0...tailwind-merge@3.7.0`). For the plugins the proposed `0.1.0` happens to be right, but their pull request lists are equally empty and must be written from the PRs touching the package, `packages/plugin-core`, and `packages/configurator`.\n- Tag-pinned file links repo-wide (the AGENTS.md link policy) are maintained automatically: each publishable package's `version` lifecycle runs `scripts/update-pinned-links.mjs`, which rewrites every link pinned to that package's newest existing tag to the tag being released (logging each rewrite for release-time review), verifies each target path against the working tree (failing the version step loudly on unresolvable paths, touching nothing), remaps pre-monorepo root paths into the package directory, and leaves links pinned to older tags alone as deliberately historical. Historical links that sit at the newest tag anyway are covered by two escape hatches: changelog directories are never scanned (their entries — including the one for the latest release — always describe a specific past release), and a link carrying the `twm-historical` query parameter stays untouched and exempt from path validation. First release of a package is a no-op for it. Concretely: the tailwind-merge 3.7.0 release re-pinned the vite docs' `v3.6.0` links to `tailwind-merge@3.7.0` with `packages/tailwind-merge/` paths on its own. Relative README links are the complementary case, handled by `scripts/pin-readme-links.mjs`: the plugin's `version` lifecycle runs it after the re-pin to turn the README's `./docs/...` and `../../agents/...` links into absolute links pinned to the tag being released (staging the README for the version commit), because npm cannot resolve relative links, while the repo keeps them relative between releases; the dev-release stamp imports the same helper and pins to the commit instead. Links it has pinned to a tag are absolute from then on and fall under `update-pinned-links.mjs` at later releases. The library needs neither: its README is generated from `docs/README.md` with absolute links by `scripts/update-readme.mjs`.\n- The autolabeler section lives only in the tailwind-merge config; labels are repo-wide.\n\n## Publishing\n\n`.github/workflows/npm-publish.yml` routes `release.published` events by tag prefix: `tailwind-merge@*` and legacy `v*` build and publish `packages/tailwind-merge`, `@tailwind-merge/vite@*` builds and publishes `packages/vite`, `@tailwind-merge/next@*` builds and publishes `packages/next`, and unknown prefixes fail the run. The build job runs repo-wide lint and tests, builds the library alongside the released package (a plugin's `test:exports` verifies its packed tarball against the library's published shape), runs the released package's `test:exports`, and for plugin releases `test:library-release`; publishing happens from the package directory in an isolated OIDC job via `pnpm publish` — after a scripts-disabled install of the released package's dependencies, which pnpm needs to rewrite `workspace:` specifiers (the plugins' tailwind-merge dependency), and with pnpm applying `publishConfig` overrides at pack time (the vite package's dist-exports swap) and handling npm trusted publishing natively, `--provenance` passed explicitly because pnpm does not read `publishConfig.provenance` (mechanics in `agents/tailwind-merge-internals.md`). Dev releases on `main` pushes cover the library and both plugins (see [Dev releases](#dev-releases)).\n\n### First Vite release\n\nRelease the next tailwind-merge update before the Vite plugin. Dev builds of the plugin are a separate track from its first stable release: see [Dev releases](#dev-releases) below for the stamping script, the one-time bootstrap publish, and the trusted-publisher setup that automatic dev publishing depends on.\n\nThe selected initial plugin release is `0.1.0`, with no prerelease suffix or alternate dist-tag. It is the only new package intended for initial publication: the configurator is inlined and keeps an unstable direct API until a separately planned release. This is a sequencing decision, not a promise that the configurator stays unpublished forever.\n\nBefore publishing:\n\n1. Done with `tailwind-merge@3.7.0`, which ships the generator's required `themeKey`, classification/parser exports, and matching configuration semantics; the plugin's `workspace:^` dependency now packs as `tailwind-merge: ^3.7.0` (the caret range keeps later library patches installable without a plugin release). The release build job guards this for every plugin release: `pnpm --filter @tailwind-merge/vite test:library-release` (the shared `scripts/check-library-release.mjs`, given the source trees to scan with `--sources`) resolves the dependency range's lowest version, downloads that release from npm, and fails unless it ships every value and type the bundled plugin core and configurator import from the unstable entry point — a packed check against the workspace library cannot establish that. When it fails, release the library first and bump the plugin's dependency to it; `--library-version` checks an arbitrary release by hand.\n2. The trusted publisher for `@tailwind-merge/vite` is set up as part of the dev-release bootstrap (see [Dev releases](#dev-releases)); the stable release reuses it. The library's configuration does not cover a second package.\n3. Set the plugin's release version when preparing that release. The plugin manifest carries no `private` flag since dev builds made it a published package; the configurator keeps `private: true` because it is never published. Add the plugin's initial changelog/release text and update the pre-release notices in its README/getting-started/versioning docs. The README's relative links need no manual pinning: the plugin's `version` lifecycle rewrites them to the release tag.\n4. Run the library and plugin builds, packed-package gates, and repo-wide checks. The [Vite packaging guide](./vite-plugin.md#build-and-packaging) explains the pnpm export rewrite and which consumer behavior the gate verifies.\n\nField trials use dev builds (`@tailwind-merge/vite@dev`), which pin the same-commit library dev build. Trials against local checkouts must likewise resolve both packages to matching builds; installing only a plugin tarball while letting its library dependency resolve to a registry release does not meet that requirement.\n\n### First Next.js release\n\nThe Next.js plugin follows the Vite plugin's model exactly: initial release `0.1.0`, the plugin core and the configurator inlined, the library as a regular dependency packed from `workspace:^`, `test:library-release` in the release build job (its `--sources` list covers the configurator, the plugin core, and the plugin), and a trusted publisher set up through the dev-release bootstrap below. The Next.js-specific facts a release must respect are in the [Next.js guide](./next-plugin.md#release-boundaries). Release the library update the plugin's imports need before the plugin, as for the Vite plugin.\n\n### Dev releases\n\n`scripts/stamp-dev-version.mjs` prepares a package for a dev publish; run it from the package directory. It stamps `<version>-dev.<sha>` (`GITHUB_SHA` in CI, `HEAD` locally), pins `workspace:` runtime dependencies to the exact same-SHA dev version of that workspace package (the plugins' bundled configurator imports the library's unstable entry point, so a registry release could lack the internals the same commit changed), and rewrites relative README links to commit-pinned absolute links so the npm page resolves them (through the shared `scripts/pin-readme-links.mjs`, see Release drafting above). `--check-registry` verifies that every pinned dependency version is already on npm; a local publish needs that, CI does not because it publishes the library first in the same job. The script dirties the working tree by design; restore the manifest and README from git after publishing locally.\n\nnpm cannot attach a trusted publisher to a package name that has never been published, so a plugin's first publish is a one-time local dev publish with a token — the only step of the whole flow that uses one — after which the trusted publisher covers all later dev and stable publishes. A new plugin stays out of the dev jobs until it is bootstrapped, because a publish step without a trusted publisher fails on every `main` push; both current plugins went through this. The bootstrap, on a clean checkout of the pushed `main` commit whose library dev release has finished publishing — shown for `@tailwind-merge/next`, the Vite plugin's was the same with `packages/vite`:\n\n```bash\npnpm install --frozen-lockfile\npnpm --filter tailwind-merge --filter @tailwind-merge/next build\npnpm --filter @tailwind-merge/next test:exports\ncd packages/next\nnode ../../scripts/stamp-dev-version.mjs --check-registry\npnpm publish --tag dev --access public --no-git-checks --ignore-scripts\ngit checkout -- package.json README.md\n```\n\nPublish without `--provenance`: provenance needs a CI OIDC token and fails locally. Then configure the trusted publisher (repository `dcastil/tailwind-merge`, workflow `npm-publish.yml`, no environment) on the package's npm access page or with `npx npm@latest trust github @tailwind-merge/next --file npm-publish.yml --repo dcastil/tailwind-merge --allow-publish` (`npm trust` needs npm 11.15 or newer). Then add the package to both dev jobs the way the plugins are wired: the build and `test:exports` filters, an artifact upload and download for its `dist`, the filtered install, a stamp line, and a publish step after the library's. With the trusted publishers in place, the publish workflow's dev jobs publish all three packages on every `main` push: the build job builds them and runs every `test:exports` gate, the OIDC job stamps all manifests with the script and publishes the library before the plugins, so the plugins' exact pins always resolve. A bootstrap publish sets `latest` as well as `dev` on the plugin, which npm does for a package's first version regardless of `--tag`; the first stable release moves `latest` to a real version.\n\nThe release commenter's dev pass stays library-only (it reads the library manifest for the head tag, and the action fails when npm has no prior dev version of a package); plugin dev builds do not comment on issues until that pass is extended.\n\n## Release commenter behavior\n\nThe workflow `.github/workflows/comment-released-prs-and-issues.yml` uses the local action `.github/actions/release-commenter`.\n\n- It runs for:\n    - published GitHub releases,\n    - manual workflow dispatch,\n    - completed successful `npm Publish` runs triggered by `push` on `main` (dev-release comment pass, tailwind-merge only even though the same run also publishes the vite package's dev build; the synthetic head tag is `tailwind-merge@<version>-dev.<sha>`).\n- Base-tag selection is scoped to the released tag's package. Legacy un-prefixed tags belong to the `fallback-package-name` input (default `tailwind-merge`).\n- Target collection separately checks each commit's changed files, then the changed files of its associated PRs, before reading their closing references or connected-issue timelines. An unrelated package's commit or PR must not announce its issues as released. PR scope results are cached within the run. Each plugin's scope includes the bundled plugin core and configurator; the library scope includes its package directory plus historical root `src/`, `docs/`, and `tests/` paths for comparisons spanning the monorepo move. Root infrastructure remains outside automatic notification scope.\n- Changed-file checks paginate both REST endpoints and include a rename's previous path, so changes moved out of a package still count. Missing file data or reaching GitHub's file-list cap without establishing scope fails before posting. Keep `releasePackagePaths()` in the action aligned with the release-drafter configs when adding packages or changing bundle ownership. Mocked tests cover mixed ranges, linked issues, pagination, renames, and dry runs without contacting GitHub.\n- Automatic base-tag selection is semver-aware within the package:\n    - Stable release tags compare to the package's previous stable tag (legacy `v*` tags included for tailwind-merge).\n    - Prerelease tags with a SHA suffix (for example `tailwind-merge@3.4.1-dev.<sha>`) resolve base from npm-published versions of the package — the npm package name is derived from the tag prefix — with the same prerelease prefix, picking the nearest ancestor commit.\n    - For SHA-suffixed prereleases, if no prior dev release exists for the same core version, resolution falls back to all dev releases from the highest lower core version (same prerelease prefix) and picks the nearest ancestor commit.\n    - SHA-suffixed prerelease comments link to the npm published version page instead of GitHub release tags.\n    - Other prerelease tags compare to the previous semver tag of the same package (including prereleases).\n- A `0.1.0` release with no same-package history skips commenting gracefully (green run, logged): `0.1.0` is always a package's first version here, so a missing base is the expected state. Trigger manually with an explicit `base_tag` if comments are wanted for a first release. Any other missing-base case still fails loudly, because above `0.1.0` it means release tooling is broken.\n- The action fails before posting if any target issue/PR already has a previous stable release-comment for the same package. Comments from other packages' releases never block — a PR touching two packages legitimately receives one comment per package release. These arrive from separate workflow runs at each package's own release time, and staying separate is deliberate: each comment notifies subscribers about that release, while editing an earlier comment into a combined one would notify nobody.\n- For prereleases, targets that already have a release-comment for the same package are skipped so repeated `-dev.*` ranges do not post duplicate dev comments. Later stable releases can still comment after a prerelease.\n- Posted comment URLs are logged and added to the workflow run summary so all comments from one run can be inspected together.\n- Manual trigger supports optional overrides:\n    - `head_tag`\n    - `base_tag`\n    - `dry_run`\n    - `npm_package_name`\n\nManual dry run example:\n\n```bash\ngh workflow run comment-released-prs-and-issues.yml \\\n  -f head_tag=tailwind-merge@3.4.1 \\\n  -f dry_run=true\n```\n\n## Inputs to collect first\n\n1. Release tag and base compare range (for example `tailwind-merge@3.4.0...tailwind-merge@3.4.1`).\n2. Draft release body from GitHub.\n3. Current active GitHub sponsors.\n4. Sponsor payout input from thanks.dev for the relevant time window.\n\n`thanks.dev` input must come from the user. Do not infer or invent this data.\n\n## Commands\n\nBump a package's version with `pnpm release <version|patch|minor|major>` run inside the package directory (for the library: `packages/tailwind-merge/`); see Release model above for why it is not bare `pnpm version`. pnpm requires a clean working tree, so commit the changelog first. pnpm runs the existing `preversion`, `version`, and `postversion` lifecycle scripts — each stages the files it rewrites itself, because pnpm only stages the manifest — and creates the version commit and an annotated namespaced tag. pnpm also normalizes the manifest when writing the new version (an empty `dependencies` object gets dropped), so keep the manifest free of such fields. To create the commit and tag locally without pushing, run with the `DANYS_MACHINE` variable unset (`env -u DANYS_MACHINE pnpm release minor`): the `preversion` pull and the `postversion` push are gated on it. The `version` step first re-pins tag-pinned links repo-wide via the shared `scripts/update-pinned-links.mjs` (see Release drafting notes above); for the library it then regenerates both the package README and the generated section of the repo-level README via its own `scripts/update-readme.mjs`, with links pinned to the new release tag.\n\nFetch draft release:\n\n```bash\ngh release view <tag> --json tagName,name,isDraft,body,url,createdAt,targetCommitish\n```\n\nFetch active GitHub sponsors in start-date order:\n\n```bash\ngh api graphql -f query='query($login:String!){ user(login:$login){ sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false, orderBy:{field:CREATED_AT, direction:ASC}){ nodes{ sponsorEntity{ ... on User{ login } ... on Organization{ login } } } } } }' -F login=dcastil\n```\n\nCheck whether private active sponsors exist:\n\n```bash\ngh api graphql -f query='query($login:String!){ user(login:$login){ public: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false){ totalCount } all: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:true){ totalCount } } }' -F login=dcastil\n```\n\n## Changelog authoring rules\n\n1. Update the matching major-version changelog file:\n    - v3 releases: `packages/tailwind-merge/docs/changelog/v3-changelog.md`\n    - v2 releases: `packages/tailwind-merge/docs/changelog/v2-changelog.md`\n2. Add new version section at the top:\n    - `## vX.Y.Z`\n3. Keep category headings from draft release:\n    - `### Bug Fixes`, `### New Features`, `### Documentation`, `### Other`, etc.\n4. Keep full compare link in docs style, using the real tag names:\n    - `**Full Changelog**: [\\`tailwind-merge@A.B.C...tailwind-merge@X.Y.Z\\`](https://github.com/dcastil/tailwind-merge/compare/tailwind-merge@A.B.C...tailwind-merge@X.Y.Z)` (the base is `vA.B.C` when it predates the monorepo)\n\n## Sponsor rules\n\n1. Preserve ordering convention from the most recent changelog sponsor sentence unless user explicitly asks otherwise.\n2. Merge sponsor sources:\n    - GitHub Sponsors (active sponsors, ordered by start date).\n    - User-provided thanks.dev payouts for the release window.\n3. Include thanks.dev sponsors only above the user-defined threshold.\n    - Current default: include only amounts greater or equal to 1 USD.\n4. Do not mention sub-threshold sponsors individually unless requested.\n5. Keep summary phrases aligned with user preference:\n    - Examples: `a private sponsor`, `and more via @thnxdev`.\n\n## GitHub release body formatting\n\nWhen producing text for GitHub Releases UI, transform docs formatting:\n\n1. Keep headings and bullet content.\n2. Convert markdown profile links to plain handles:\n    - `[@name](https://github.com/name)` -> `@name`\n3. Convert markdown PR links to plain URLs.\n4. Use plain compare URL:\n    - `**Full Changelog**: https://github.com/dcastil/tailwind-merge/compare/tailwind-merge@A.B.C...tailwind-merge@X.Y.Z`\n\n## Final output contract\n\n1. Provide the release body in one copy-paste-ready markdown block.\n2. If sponsor payout input is missing, stop and ask the user for thanks.dev data before finalizing sponsor lines.\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nThis repository is a pnpm monorepo hosting `tailwind-merge` — a TypeScript library that merges Tailwind class strings by removing style conflicts while preserving non-conflicting and non-Tailwind classes — and its sibling packages. Packages are independent: each owns its lint, test, and type-check setup and releases on its own schedule.\n\n## Primary Goals\n\n- Keep merge behavior correct for the supported Tailwind versions listed in the library documentation.\n- Keep runtime fast (browser-oriented hot path, lazy init, lightweight cache).\n- Keep API and type contracts stable.\n\n## Repo Map\n\n- The library package: `packages/tailwind-merge/` — source in `src/`, tests in `tests/`, human docs in `docs/`, build/release scripts in `scripts/`, published overview `README.md` (autogenerated from `docs/README.md`).\n- Unreleased workspace packages: `packages/configurator/` generates tailwind-merge configs from Tailwind v4 CSS; `packages/plugin-core/` adds what every bundler plugin needs on top (CSS root discovery, runtime-module generation, the generation session); `packages/vite/` and `packages/next/` wrap both for Vite and Next.js with automatic generation and usage pruning. The configurator and the plugins have a maintained `README.md` and user documentation in `docs/`; the plugin core is internal and inlined into both plugins.\n- Repo root: workspace orchestration only — `pnpm-workspace.yaml` (workspace list + shared tool-version catalog), `eslint.config.base.mjs` (shared lint base every scope spreads), root `eslint.config.mjs` (lints root files and `.github/`), root `vitest.config.mts` (aggregates the packages' Vitest projects plus the local GitHub actions' tests), repo-wide release tooling in `scripts/` (`update-pinned-links.mjs`, `pin-readme-links.mjs`, `stamp-dev-version.mjs`, `check-library-release.mjs`), and the repo-level `README.md`.\n- CI workflows: `.github/workflows/`\n- Local GitHub actions: `.github/actions/`\n\n## Agent Docs Map\n\n- `agents/tailwind-merge-internals.md`: library architecture, file ownership, shared testing/build behavior, and CI/security guardrails.\n- `agents/configurator.md`: configurator goals, strategy, generation/classification architecture, correctness gates, and development follow-ups.\n- `agents/plugin-core.md`: the shared plugin core — discovery, generation, and session invariants, testing, and its inlined packaging.\n- `agents/vite-plugin.md`: Vite integration goals, runtime resolution, watching/failure contracts, packaging, and test coverage.\n- `agents/next-plugin.md`: Next.js integration goals, the loader-rule mechanism and its Turbopack/webpack facts, testing harness, packaging, and test coverage.\n- `agents/configurator-performance.md`: dated measurements, optimization rationale, and reproduction methodology.\n- `agents/tailwind-css-version-update.md`: Tailwind CSS version support workflow.\n- `agents/release-workflow.md`: changelog authoring, GitHub release text, sponsor handling, and release comments on issues/PRs.\n\n## Environment and Commands\n\n- Package manager: `pnpm` with a workspace rooted at `pnpm-workspace.yaml`\n- Local development runtime: Node `22.18.0` or newer on a supported LTS release, satisfying the requirements of pnpm 11 and the Babel 8 build toolchain\n- CI runtime: Node `24.18.1`, pinned via `node-version` in the workflows under `.github/workflows/` (Renovate bumps it there; treat the workflows as the source of truth if this number looks stale)\n- Consumer TypeScript support: TypeScript `3.8` and newer; `pnpm test:exports` verifies the generated declarations with the pinned minimum compiler\n- Dependency supply-chain guardrail: `minimumReleaseAge: 4320` in `pnpm-workspace.yaml`, so newly published package versions must be at least three days old before pnpm installs them. Renovate has a matching cooldown in `.github/renovate.json` (see `agents/tailwind-merge-internals.md` for how the two must stay in sync).\n- Dependency build scripts are denied by default unless explicitly allowed in `pnpm-workspace.yaml`; `esbuild` is currently allowed because `.github/actions/metrics-report` needs it.\n- The root manifest's `devEngines.runtime` must encode the real documented Node floor (currently `>=22.18.0`), not a rounded-down one: pnpm validates dependency `engines` against the range's minimum version, and a floor looser than a dependency's own requirement makes pnpm silently skip optional dependencies during install — this is how rolldown's platform binary went missing under an earlier `>=22`, with only a debug-level log mentioning it.\n\nCore commands (repo root):\n\n- `pnpm install --frozen-lockfile`\n- `pnpm lint` — lints root files, then every package's own lint script (`pnpm --recursive lint`)\n- `pnpm test` — runs every package's Vitest project plus the local GitHub actions' tests through the root projects config, with coverage\n- `pnpm test:types` — every package's own tsc project (`pnpm --recursive test:types`)\n- `pnpm test:watch` — cross-package watch mode\n\nLibrary-specific commands (run with `pnpm --filter tailwind-merge <script>` from anywhere, or plain `pnpm <script>` inside `packages/tailwind-merge/`):\n\n- `build`\n- `test:exports`\n- `bench`\n\nVite-plugin-specific commands (run with `pnpm --filter @tailwind-merge/vite <script>`, or plain `pnpm <script>` inside `packages/vite/`):\n\n- `build` — tsdown, ESM bundles plus declarations for all three subpaths into `dist/`\n- `test:exports` — packs the tarball and verifies the published shape; requires both this package's and the library's `build` to have run first (see [Vite build and packaging](./agents/vite-plugin.md#build-and-packaging) for what it checks)\n\nNext.js-plugin-specific commands (run with `pnpm --filter @tailwind-merge/next <script>`, or plain `pnpm <script>` inside `packages/next/`):\n\n- `build` — tsdown, ESM bundles plus declarations for the plugin entry, the loader, and the runtime into `dist/`; the package's `exports` point at `dist/` even in the workspace, and its test global setup runs this build\n- `test` — spawns real `next dev`/`next build` processes for both bundlers (about two minutes); needs the library built, which the global setup does when `packages/tailwind-merge/dist` is missing\n- `test:exports` — packs the tarball and verifies the published shape including a real `next build` of a consumer app; requires both this package's and the library's `build` to have run first (see [Next.js build and packaging](./agents/next-plugin.md#build-and-packaging))\n\nMonorepo conventions:\n\n- Shared tool versions (eslint, typescript, vitest, @types/node) live in the `catalog:` section of `pnpm-workspace.yaml`; package manifests reference them as `\"catalog:\"`. Single-consumer dependencies stay pinned in their package manifest.\n- Every package lints with its own `eslint.config.mjs` spreading `eslint.config.base.mjs` from the root, and tests with its own `vitest.config.mts`; the root configs only orchestrate.\n- No task runner (turborepo or similar) and no affected-only CI on purpose: at the current package count the full suite is fast, and running everything keeps cross-package effects (a tailwind-merge or plugin-core change must exercise both plugins' tests) trivially correct. Revisit when the package count grows or build times make caching pay off.\n- Releases are per package with `<package-name>@<version>` git tags (for example `tailwind-merge@3.7.0`); see `agents/release-workflow.md`.\n\n## Working Rules for Changes\n\n1. Treat `packages/tailwind-merge/src/lib/default-config.ts` as the behavioral source of truth for default class groups and conflicts.\n2. If behavior changes, update tests in `packages/tailwind-merge/tests/` in the same change.\n3. If public behavior/docs change, update docs in `packages/tailwind-merge/docs/` (not directly in a `README.md`).\n4. The library's `README.md` and the generated section of the repo-level `README.md` are version-generated from the library's `docs/README.md`; do not regenerate them during normal development changes. The configurator, Vite, and Next.js READMEs are maintained landing pages, not generated files; edit them alongside their own `docs/` pages.\n5. Avoid manual edits to `dist/` and `coverage/` directories; they are generated artifacts.\n6. Validate packaging paths with `pnpm --filter tailwind-merge build && pnpm --filter tailwind-merge test:exports` when touching exports/build tooling.\n\n## Documentation Sync Policy\n\nTreat documentation updates as part of the same change, not as follow-up work.\n\nSelf-improvement is required while working in this repo: whenever you discover useful information about architecture, workflows, commands, constraints, debugging findings, or recurring pitfalls, add it to the appropriate file in `agents/` or to `AGENTS.md` in the same change. When existing agent guidance becomes stale, misleading, or irrelevant, remove or revise it immediately.\n\nAvoid repeating the same guidance in both `AGENTS.md` and specialized `agents/*` docs unless the rule is critically important. Prefer keeping `AGENTS.md` as the high-level entry point and putting detailed workflow, architecture, release, or CI notes in the relevant specialized agent document.\n\nCross-package user-docs convention: every package's docs recommend setting `twMerge` up in one project-owned file (shown as `tw-merge.ts`) and importing it from there, even without configuration, so that configuring, wrapping, or replacing `twMerge` later is a one-file change. The canonical explanation lives in the library's `docs/configuration.md` (\"Import `twMerge` from one place\"); the plugin and configurator getting-started pages carry their own short version. Keep new setup instructions and usage examples consistent with it.\n\nIt is acceptable to periodically restructure agent documentation when files become too large, when topics are hard to find, or when the current organization no longer matches how the repo is maintained. Split files, rename sections, or move information between `AGENTS.md` and `agents/*` as needed, while preserving useful guidance and removing stale duplication.\n\nRequired when relevant:\n\n1. Update `packages/tailwind-merge/docs/*.md` (or the owning package's docs) for any user-visible behavior, API, version support, or limitation changes.\n2. Update `AGENTS.md` whenever repository-wide agent workflow, repo conventions, required commands, or high-level guardrails change.\n3. Update the relevant file in `agents/` whenever specialized architecture, workflow, CI, release, or testing guidance changes.\n4. Do not regenerate the READMEs during normal development. The library `README.md` and the marked section of the repo-level `README.md` are intentionally generated as part of the version/release flow so visitors are routed to docs for the latest release.\n5. Never reference repo files through `blob/main`/`raw/main`/`tree/main` URLs — they break whenever a file moves. Within the repo use relative links; where a link must be absolute (published docs, JSDoc, release notes), pin it to the latest release tag whose tree contains the file (e.g. `blob/v3.6.0/docs/...`, `blob/tailwind-merge@3.7.0/packages/tailwind-merge/docs/...`). Links pinned to a package's latest tag are re-pinned automatically at that package's next release by `scripts/update-pinned-links.mjs` (run in each publishable package's `version` lifecycle); links pinned to older tags are treated as deliberately historical and never touched. Two escape hatches for historical links that sit at the latest tag: changelog directories (`docs/changelog/`) are never scanned, and a link carrying the `twm-historical` query parameter (e.g. `blob/v3.6.0/docs/foo.md?twm-historical`, before any `#fragment`) stays untouched — GitHub ignores the parameter when serving the page.\n\nDefinition of done for every PR/change:\n\n1. Code and tests are updated.\n2. Relevant docs are updated in the same change set.\n3. `AGENTS.md` and `agents/*` guidance is reviewed, with useful new context added and stale guidance removed whenever the change affects how agents should work in this repo.\n\n## Quick Validation Matrix\n\nAll test paths below are relative to `packages/tailwind-merge/`; run single files with `pnpm --filter tailwind-merge test:watch tests/<file>`.\n\n- Parser or modifier semantics: run `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts`.\n- Class groups/conflicts/default config: run `tests/default-config.test.ts`, `tests/class-group-conflicts.test.ts`, `tests/tailwind-css-versions.test.ts`.\n- Public API/types: run `tests/public-api.test.ts`, `tests/type-generics.test.ts`, and `pnpm --filter tailwind-merge test:types`.\n- Release/package surface: run `pnpm --filter tailwind-merge build` and `pnpm --filter tailwind-merge test:exports`.\n- Configurator, plugin core, or a plugin: run `pnpm --filter @tailwind-merge/configurator test` / `pnpm --filter @tailwind-merge/plugin-core test` / `pnpm --filter @tailwind-merge/vite test` / `pnpm --filter @tailwind-merge/next test` and the matching `test:types` script. A core or configurator change must run both plugin suites.\n- Plugin build/packaging surface: run `pnpm --filter tailwind-merge build`, then the plugin's `build` and `test:exports` (`pnpm --filter @tailwind-merge/vite …` or `pnpm --filter @tailwind-merge/next …`).\n- Repo-wide before finalizing: `pnpm lint`, `pnpm test:types`, `pnpm test` from the root.\n","agents/tailwind-css-version-update.md":"# Tailwind CSS version update workflow\n\nUse this guide when updating tailwind-merge for a new Tailwind CSS version.\n\nAll `src/`, `tests/`, and `docs/` paths in this file are relative to the library package `packages/tailwind-merge/`; run the quoted commands from that directory (or through `pnpm --filter tailwind-merge`).\n\nThe main risk in this work is false confidence: Tailwind release notes, deployed docs, source code, and website docs can land at different times. Treat every feature as something to verify from official sources before changing `src/lib/default-config.ts` or parser behavior.\n\n## Official sources\n\n- Tailwind CSS releases: https://github.com/tailwindlabs/tailwindcss/releases\n- Tailwind CSS source and PRs: https://github.com/tailwindlabs/tailwindcss\n- Tailwind CSS release tag pattern: `https://github.com/tailwindlabs/tailwindcss/releases/tag/vX.Y.Z`\n- Tailwind CSS deployed docs: https://tailwindcss.com/docs\n- Tailwind CSS website source and docs PRs: https://github.com/tailwindlabs/tailwindcss.com\n- Prior tailwind-merge support PRs for comparison, for example v4.2 support: https://github.com/dcastil/tailwind-merge/pull/651\n\nWhen docs for a just-released Tailwind version are not deployed yet, use the `tailwindcss.com` repository and its PRs to find future docs pages, sidebar order, and link targets.\n\n## Investigation checklist\n\n1. Read the Tailwind CSS release notes for the target version.\n2. Open every linked Tailwind CSS PR that mentions utilities, variants, parser syntax, arbitrary values, or behavior changes.\n3. Inspect Tailwind CSS source and tests when release notes or PR descriptions are ambiguous. Do not infer support for arbitrary values or variables from naming patterns alone.\n4. Check the `tailwindcss.com` repository for docs PRs when deployed docs lag behind the release.\n5. Compare against the previous tailwind-merge Tailwind-support PR to reuse the same structure and testing style.\n6. Make a feature list before coding. Split work into simple utility additions, ordering/docs updates, conflict changes, and parser/config API changes.\n7. Record no-op decisions in the PR or issue summary, especially when a Tailwind PR looked relevant but does not require a tailwind-merge change.\n\n## Triage questions for each Tailwind change\n\n- Does it emit new class names? If yes, it likely needs `classGroups` entries.\n- Does it change which utilities conflict? If yes, update `conflictingClassGroups` or `conflictingClassGroupModifiers`.\n- Does it add a new arbitrary value or arbitrary variable form? Verify the exact accepted syntax in Tailwind source before adding validators.\n- Does it add a slash form that is part of the base class rather than a postfix modifier? Prefer config-driven lookup such as `postfixLookupClassGroups` before changing parser logic.\n- Does it change modifier, variant, prefix, important, or arbitrary-variant syntax? Parser changes are higher risk and need focused parser/modifier tests.\n- Does it only change generated CSS for an existing class without changing class syntax or conflicts? It may not require a tailwind-merge change.\n- Does it remove or rename classes? Check whether keeping old classes would create incorrect merges for the supported Tailwind range.\n\n## Default config ordering and links\n\nKeep `src/lib/default-config.ts` ordered according to the Tailwind CSS docs sidebar, not alphabetically and not by implementation convenience.\n\nFor new utility groups:\n\n- Use `tailwindcss.com` or the website repository to find the final docs URL for each `@see` comment.\n- If the public docs are not deployed, inspect docs PRs in `tailwindlabs/tailwindcss.com`.\n- Replace temporary `TBD` links before finishing the change.\n- Keep related utility groups in sidebar order even when they share a prefix.\n\nThere is an important exception to sidebar order: class-group order affects validator precedence for groups that share the same prefix path. The class map tries exact child paths first, then runs validators in the order they were registered. If a broad validator appears before a more specific validator for the same prefix, the broad group can claim the class before the specific group sees it.\n\nWhen groups with the same prefix use validators:\n\n- Put the most specific validators first.\n- Put broad or catch-all validators last for that prefix.\n- Treat color validators as especially sensitive because they often accept arbitrary values or otherwise broad value sets.\n- Prefer correctness over strict sidebar order inside that shared-prefix cluster.\n\nFor example, if a `text-*` color group uses a broad color validator, keep it after more specific `text-*` groups such as font-size-related groups so ambiguous classes resolve to the intended class group.\n\nExamples learned during the Tailwind CSS v4.3 update:\n\n- `tab-size` belongs after `text-indent` and before `vertical-align`.\n- `zoom` belongs after `translate`.\n- Scrollbar groups should follow the sidebar order for scrollbar color, scrollbar gutter, and scrollbar width.\n- PRs that look relevant to existing utilities, such as changes around `start` and `end`, still need source review before changing tailwind-merge.\n\n## Implementation guidance\n\nUse `src/lib/default-config.ts` as the behavioral source of truth for utilities and conflicts.\n\nPrefer config changes over parser changes. Parser and merge hot paths run frequently in UI renders, so keep extra work small and opt-in:\n\n- Normalize config arrays into lookup objects in `createConfigUtils` when they need fast querying.\n- Avoid scanning or balancing brackets in validators when a cheap prefix or character check can reject most inputs.\n- Put specialized syntax checks in `src/lib/validators.ts` when they are reusable or need direct tests.\n- Add comments only for non-obvious behavior, especially when a config entry compensates for parser ambiguity.\n\nFor slash syntax, remember that tailwind-merge initially treats the part after `/` as a possible postfix modifier. If the full class can belong to a different group, use a generic config mechanism rather than a syntax-specific parser branch.\n\nThe Tailwind CSS v4.3 named container query support is the reference pattern:\n\n- `@container-size` is a container type class.\n- `@container-size/sidebar` combines container type and container name.\n- The parser already exposes a possible postfix modifier, so no bracket-counting parser change is needed.\n- `container-named` creates a conflict with `container-type`, but not the reverse, because a later type-only class should preserve the earlier container name.\n- `postfixLookupClassGroups: ['container-type']` tells merge logic to try resolving the full slash class only after the pre-slash class resolves to `container-type`.\n\n## Testing checklist\n\nAdd or update tests based on the kind of change:\n\n- Tailwind version compatibility: `tests/tailwind-css-versions.test.ts`\n- Default config class map coverage: `tests/class-map.test.ts`\n- Class group conflicts: `tests/class-group-conflicts.test.ts` and `tests/conflicts-across-class-groups.test.ts`\n- Parser or modifier semantics: `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts`\n- Validators: `tests/validators.test.ts`\n- Public exports and config API: `tests/public-api.test.ts`, `tests/type-generics.test.ts`, `tests/merge-configs.test.ts`\n- Docs examples: `tests/docs-examples.test.ts`\n\nFor new utility support, include tests for:\n\n- Last class wins within the new class group.\n- Cross-group conflicts in both orders when conflicts are asymmetric.\n- Arbitrary values and arbitrary variables only when Tailwind source confirms support.\n- Important modifier and variant combinations when parser or conflict behavior is involved.\n- Unknown or invalid classes remaining untouched.\n\nFor TypeScript config changes, remember that `pnpm tsc --noEmit` only checks `src`. Use the test tsconfig as an additional check when touching public types:\n\n```sh\npnpm tsc --noEmit -p tests/tsconfig.json --moduleResolution bundler\n```\n\nThe plain `tests/tsconfig.json` currently inherits `moduleResolution: Node`, which can surface Vitest package export resolution errors unrelated to tailwind-merge types.\n\n## Documentation checklist\n\nUpdate docs in the same change when behavior or public API changes:\n\n- `docs/configuration.md` for config behavior.\n- `docs/api-reference.md` for exported validators, config fields, or public types.\n- `docs/changelog/v3-changelog.md` when preparing a release note.\n- `AGENTS.md` and `agents/*` when the update process or repo guidance changes.\n\nDo not regenerate `README.md` during normal development; it is generated from `docs/README.md` during the release flow.\n\n## Validation checklist\n\nRun targeted tests while iterating, then run the full validation set before finalizing:\n\n```sh\npnpm lint\npnpm tsc --noEmit\npnpm test\ngit diff --check\n```\n\nRun `pnpm build && pnpm test:exports` when touching package exports, build tooling, or generated declaration behavior.\n","agents/tailwind-merge-internals.md":"# tailwind-merge internals guide\n\nThis file is a practical map for agent-driven changes in this repo.\n\nSince the 2026-08-10 monorepo migration the library lives in `packages/tailwind-merge/`; all `src/`, `tests/`, `docs/`, and `scripts/` paths in this file are relative to that package directory unless a path starts with `packages/` or `.github/`.\n\nLinks from the library's `docs/` to repository-wide files must traverse three parents; for example, `../../../.github/CONTRIBUTING.md`. Resolve relative links from the owning document after moving files, not from the repository root.\n\n## How merge execution works\n\n1. `twMerge` in `src/lib/tw-merge.ts` is created via `createTailwindMerge(getDefaultConfig)`.\n2. `createTailwindMerge` in `src/lib/create-tailwind-merge.ts`:\n    - lazily builds config utils on first call,\n    - joins inputs via `twJoin`,\n    - caches final joined-input results,\n    - calls `mergeClassList` on cache miss.\n3. `mergeClassList` in `src/lib/merge-classlist.ts`:\n    - parses classes,\n    - walks from right to left so \"last class wins\",\n    - resolves class group IDs and asymmetric conflicts,\n    - keeps non-Tailwind/external classes untouched.\n4. `createClassGroupUtils` in `src/lib/class-group-utils.ts`:\n    - compiles class definitions into a recursive map + validator list,\n    - maps class names to class group IDs,\n    - assigns arbitrary properties (`[key:value]`) synthetic group IDs prefixed with `ARBITRARY_PROPERTY_PREFIX` (e.g. `arbitrary..key`) so distinct properties conflict independently,\n    - returns conflicting groups, including postfix-modifier conflicts.\n5. `createParseClassName` in `src/lib/parse-class-name.ts`:\n    - parses stacked modifiers with bracket/paren depth tracking,\n    - supports important modifier (`!`) and legacy v3 position,\n    - supports optional prefix and `experimentalParseClassName` override.\n6. `createSortModifiers` in `src/lib/sort-modifiers.ts`:\n    - alphabetically sorts non-sensitive modifiers,\n    - preserves placement around arbitrary/order-sensitive modifiers.\n\nThe unstable tooling entry point also exports `createClassGroupLookup` from `src/lib/class-group-lookup.ts`. It runs one candidate through `mergeClassList` with an observing class-map function and returns every successful lookup, including a slash class's intermediate base. The configurator uses this to preserve lookup dependencies during pruning. Keep this instrumentation outside the normal merge path; it is an adapter for build-time inspection, not a second implementation of modifier or lookup semantics. Both built ESM/CJS export checks exercise it.\n\n## Performance constraints in hot-path code\n\nThe hot paths are `src/lib/merge-classlist.ts` and `src/lib/class-group-utils.ts`; `src/lib/parse-class-name.ts` runs per class on cache misses. When editing these files, preserve the existing V8-oriented patterns:\n\n- Keep call sites monomorphic: functions are deliberately split per input type (see the `create*` factories and the \"monomorphic\" comments in `class-group-utils.ts` and `parse-class-name.ts`); do not merge them back into one polymorphic function.\n- Keep object shapes consistent: always define optional properties explicitly (as `undefined`/`null`) so objects created in different branches share a hidden class; result objects are built through factory functions for this reason.\n- Pre-allocate arrays where the final length is known (see `concatArrays` in `src/lib/utils.ts`) instead of spreading or pushing, and return shared constants like `EMPTY_CONFLICTS` instead of fresh empty arrays.\n- Avoid adding function calls or allocations in hot loops when an inline check works.\n\n## Files to edit by intent\n\n- Add or change Tailwind utility support:\n    - `src/lib/default-config.ts`\n    - Usually paired with tests in `tests/tailwind-css-versions.test.ts` and focused utility tests.\n    - For a full Tailwind CSS version update, follow `agents/tailwind-css-version-update.md`.\n\n- Change conflict behavior:\n    - `src/lib/default-config.ts` (`conflictingClassGroups`, `conflictingClassGroupModifiers`)\n    - `tests/class-group-conflicts.test.ts`\n    - `tests/conflicts-across-class-groups.test.ts`\n\n- Change parser/modifier behavior:\n    - `src/lib/parse-class-name.ts`\n    - `src/lib/sort-modifiers.ts`\n    - `tests/modifiers.test.ts`\n    - `tests/arbitrary-variants.test.ts`\n    - `tests/experimental-parse-class-name.test.ts`\n\n- Change config extension APIs:\n    - `src/lib/extend-tailwind-merge.ts`\n    - `src/lib/create-tailwind-merge.ts`\n    - `src/lib/merge-configs.ts`\n    - `tests/create-tailwind-merge.test.ts`\n    - `tests/extend-tailwind-merge.test.ts`\n    - `tests/merge-configs.test.ts`\n    - `tests/type-generics.test.ts`\n\n- Change validators:\n    - `src/lib/validators.ts`\n    - `tests/validators.test.ts`\n    - Any impacted arbitrary-value tests\n\n## Testing strategy in this repo\n\n- Unit/behavior tests are granular by feature area in `tests/*.test.ts`.\n- Color-function regression tests should verify preservation of length utilities and replacement of other colors in `tests/colors.test.ts`.\n- `tests/tailwind-css-versions.test.ts` is the compatibility anchor for Tailwind feature coverage by version.\n- `tests/docs-examples.test.ts` enforces that documented `twMerge(...) // -> ...` examples stay correct. Its extraction regex supports a limited set of characters (for example, `/` in arguments is excluded); verify new examples are collected and update the assertion count when adding supported examples.\n- `tests/public-api.test.ts` guards runtime exports and broad type usage.\n- `tests/tw-merge.benchmark.ts` is for performance benchmarking (`pnpm bench`), not correctness gating in CI.\n\nRun a single test file or test by name with Vitest's filter syntax: `pnpm --filter tailwind-merge test:watch tests/modifiers.test.ts` or `pnpm --filter tailwind-merge test:watch -t \"test name pattern\"`.\n\nPackage-specific generation, scanning, and Vite test guidance lives in [configurator development](./configurator.md#debugging-and-validation) and [Vite plugin development](./vite-plugin.md#testing-and-remaining-coverage).\n\nVitest is set up as one project per package: each package's `vitest.config.mts` carries its aliases and plugins, and the root `vitest.config.mts` aggregates them (plus an inline project for `.github/actions/**/*.test.mjs`, since the local actions aren't workspace packages) so root `pnpm test` runs everything with coverage. Because collection is scoped to those projects, stray test files elsewhere in the repo — notably full repo copies in `.claude/` worktrees, which the pre-monorepo root config silently collected and ran as duplicates — never join the suite.\n\nThe root coverage configuration measures `packages/*/src/**/*.ts`, including the configurator and Vite plugin. In projects mode, package-local coverage settings do not control the aggregate report. Coverage is diagnostic, with no percentage threshold; use uncovered branches to guide behavioral tests rather than weakening assertions or padding the percentage. The Vitest report does not capture code executed by spawned CLI/build processes, so interpret unmeasured entrypoints alongside their integration tests.\n\nRecommended local sequence for non-trivial changes:\n\n1. `pnpm lint`\n2. `pnpm test:types`\n3. `pnpm test`\n4. `pnpm --filter tailwind-merge build`\n5. `pnpm --filter tailwind-merge test:exports`\n\n## Build and packaging\n\n- Build uses Rollup (`scripts/rollup.config.mjs`) to produce:\n    - ESM and CJS bundles,\n    - ES5 variants,\n    - unified type declarations.\n- The `build` script passes Rollup `--forceExit` because the TypeScript Rollup plugin can leave referenced file-watch handles open after a non-watch build has already written all outputs.\n- Babel 8 performs the final target-specific transforms for all four JavaScript bundles. Its `@babel/preset-env` `bugfixes` behavior is unconditional, and the removed `loose: true` option is preserved through top-level assumptions plus the `transform-typeof-symbol` exclusion in `scripts/rollup.config.mjs`. The config deliberately replaces the migration guide's `arrayLikeIsIterable` with `iterableIsArray` because Babel disallows enabling both and every transformed spread and destructuring operand is an array; this suppresses generic iterable helpers and keeps the generated JavaScript bundles equivalent in size and behavior to the Babel 7 bundles.\n- TypeScript 6 requires the source `rootDir` to be explicit when declaration output is redirected by Rollup, so `tsconfig.json` fixes it to `src`. Module resolution uses `Bundler`, matching the Rollup build and avoiding the deprecated `Node`/`node10` resolver.\n- Export smoke tests:\n    - `scripts/test-built-package-exports.cjs`\n    - `scripts/test-built-package-exports.mjs`\n    - Both scripts execute the default and ES5 bundles so build-tool upgrades cannot break only the legacy entry point unnoticed.\n- `pnpm test:types` type-checks both the source and the test suite with the development compiler. `scripts/typescript-compatibility/consumer.ts` separately compiles the built declaration bundle with the `typescript-compat` compiler alias pinned to TypeScript 3.8.3. `pnpm test:exports` includes the compatibility check because TypeScript 3.8 is the documented minimum consumer version; keep the alias excluded from Renovate updates and treat any increase to this floor as a major-version change.\n- `scripts/update-readme.mjs` (run in the `version` script via `zx`) generates two targets from `docs/README.md`: the package `README.md` shipped to npm, and the section between the `tailwind-merge-docs` markers in the repo-level `README.md`; it fails the version step loudly when the repo README's markers are missing.\n- The `version` script also runs the repo-wide `scripts/update-pinned-links.mjs` (repo root, dependency-free Node; the vite package additionally runs `scripts/pin-readme-links.mjs`, see the release guide), which re-pins every link pinned to the package's newest existing tag onto the tag being released, verifying targets against the working tree and failing loudly — without touching anything — when a linked path no longer resolves. Older-tag links are deliberately historical and stay untouched, changelog directories are excluded from scanning entirely, and the `twm-historical` query parameter on a link shields it individually for the rare historical link elsewhere.\n\nFor the Vite package's tsdown build, workspace/published export split, and packed-consumer gate, read [Vite build and packaging](./vite-plugin.md#build-and-packaging).\n\n## CI Behavior And Security\n\nTreat this section as the source of truth for CI and publish security guardrails. Keep detailed CI guidance here instead of duplicating it in `AGENTS.md` unless a rule is critical enough to be visible before opening specialized docs.\n\n- The repo uses a pnpm workspace rooted at `pnpm-workspace.yaml`; the root manifest is a private orchestrator (`tailwind-merge-monorepo`), the library is the `packages/tailwind-merge` workspace package, and `.github/actions/metrics-report` is a workspace package so action dependencies share the root lockfile.\n- CI steps that target the library use `pnpm --filter tailwind-merge <script>`; build artifacts live at `packages/tailwind-merge/dist`, and `npm publish`/version-stamping steps run with `working-directory: packages/tailwind-merge` because npm operates on the manifest in its working directory.\n- The metrics-report action measures the PR base branch by checking it out in place, so `.github/actions/metrics-report/src/utils/path.mjs` resolves the library root per call, falling back to the repo root for pre-monorepo base branches where the library manifest still sat there. `pnpm --filter tailwind-merge build` works on both layouts because the pre-monorepo root package carried the same name.\n- pnpm 11 requires Node 22 or newer and no longer reads pnpm-specific settings from `.npmrc` or the `pnpm` field in `package.json`; keep repository settings in `pnpm-workspace.yaml`.\n- CI enables Corepack so the root `packageManager` pin selects the pnpm version before every frozen install. For pnpm major updates, run both `pnpm install` and `pnpm install --frozen-lockfile` with the new pinned version, and commit any lockfile migration produced by the non-frozen install.\n- `pnpm-workspace.yaml` sets `minimumReleaseAge: 4320` (minutes, i.e. three days) with `minimumReleaseAgeStrict: true`, so dependency versions must be at least three days old before pnpm installs them. It also allows the `esbuild` dependency build script because the metrics action imports esbuild directly.\n- `.github/renovate.json` sets `minimumReleaseAge: \"4 days\"` (all datasources) so Renovate never proposes versions that pnpm strict mode would reject during lockfile updates; the extra day over pnpm's three days is a deliberate buffer against boundary races. If the pnpm cooldown changes, keep the Renovate value at least as large. Renovate exempts vulnerability alerts from its cooldown, but pnpm strict mode still blocks immature versions at install time; use pnpm's `minimumReleaseAgeExclude` if a security fix must land early.\n- `.github/workflows/test.yml` runs `pnpm lint`, `pnpm test:types`, `pnpm test`, `pnpm --filter tailwind-merge build`, `pnpm --filter tailwind-merge test:exports`, then each plugin's `build` and `test:exports` (`@tailwind-merge/vite`, then `@tailwind-merge/next`) — the plugin steps after the library build because the packed-tarball gates resolve tailwind-merge through its published dist and types. The Next.js package's Vitest project spawns real Next.js processes and builds the library itself when its dist is missing (its global setup), so `pnpm test` stays independent of the build steps' order.\n- `.github/workflows/benchmark.yml` runs `pnpm --filter tailwind-merge bench` with CodSpeed tokenless uploads for this public repository; do not pass static CodSpeed upload tokens to jobs that execute PR benchmark code.\n- `.github/workflows/codeql-analysis.yml` runs CodeQL for both JavaScript/TypeScript and GitHub Actions workflows; the JavaScript job builds the library and both plugins with explicit package filters, since the workspace root has no build script. The Actions analysis uses the `security-extended` query suite to catch workflow-specific security issues. It also runs on PRs that touch `.github/actions/**` or `.github/workflows/**` so CI-control-plane changes are scanned before merge.\n- Every workflow should declare explicit least-privilege `permissions`; read-only build/test jobs use `contents: read`, and write scopes should appear only on the jobs that need them.\n- Pin every `actions/checkout` use to an immutable commit and set `persist-credentials: false` unless the job must push commits or tags through git; checkout receives the job token even in read-only jobs.\n- `actions/checkout` v7+ blocks fork pull request code under `pull_request_target` and PR-triggered `workflow_run`; never set `allow-unsafe-pr-checkout: true`. Keep `label.yml` checkout-free, keep the metrics comment job on the base SHA, and keep release-comment workflow runs restricted to successful `push` events on `main`.\n- Third-party GitHub Actions that receive secrets or write-capable tokens are pinned to full commit SHAs with comments showing the source tag or branch.\n- Artifact handoffs intentionally pair `actions/upload-artifact@v7` with `actions/download-artifact@v8` because the two actions have different latest major versions; do not try to align their version numbers unless upstream releases change.\n- Local JavaScript GitHub Actions use `runs.using: node24`; keep action scripts compatible with the declared runner and avoid runtime-fragile ESM features, such as JSON module import assertion syntax, when a simple filesystem read works across supported Node versions.\n- `.github/workflows/metrics-report.yml` keeps PR code execution in a read-only `generate-report` job and posts comments from a separate `post-comment` job that checks out trusted base-repo code. The metrics report action itself only writes the generated comment body to the artifact path and must not post PR comments directly. If a transition PR introduces or moves the trusted posting script, the workflow should skip the `post-comment` job at the job level until that script exists in the base checkout rather than running PR-provided posting code with write permissions.\n- `.github/workflows/npm-publish.yml`:\n    - publishes `dev` tag builds of tailwind-merge and both plugins on `main` pushes: one build job builds all three (library first, topologically) and runs every `test:exports` gate, one OIDC job stamps the manifests with `scripts/stamp-dev-version.mjs` (dev version plus the plugins' exact pins on the library's same-commit dev version) and publishes the library before the plugins. The plugin core and the configurator have no dev release; they ship inlined in the plugin bundles. A new package joins these jobs only after its npm trusted publisher exists, since a publish step without one fails on every push (bootstrap in the release guide),\n    - publishes production on release events, routing by the namespaced tag prefix (`tailwind-merge@*` and legacy `v*` map to `packages/tailwind-merge`, `@tailwind-merge/vite@*` to `packages/vite`, `@tailwind-merge/next@*` to `packages/next`, anything else fails),\n    - the release build job builds the library alongside the released package (`pnpm --filter tailwind-merge --filter \"$PACKAGE_NAME\" build` — a plugin's test:exports needs the library's published shape; duplicate filters collapse for tailwind-merge releases, and pnpm's topological order builds the library before a plugin) and runs the released package's `test:exports` unconditionally, plus `test:library-release` for plugin releases (registry check that the packed tailwind-merge range's lowest version ships the unstable-entry-point imports; see the Vite and Next.js guides),\n    - publishes with `pnpm publish --access public --provenance --ignore-scripts --no-git-checks` (plus `--tag dev` in the dev flow): pnpm applies `publishConfig` field overrides at pack time (the vite exports swap) and implements npm trusted publishing natively — it fetches the GitHub OIDC id-token, exchanges it per package at the registry, and publishes through npm's own libnpmpublish vendored inside pnpm. `--provenance` must be explicit because pnpm reads it from flags/config, not from `publishConfig.provenance`; `--no-git-checks` because CI checkouts are detached HEADs (and the dev flow's version stamp dirties the tree),\n    - the publish jobs enable corepack (for pnpm) and deliberately set no `registry-url` in setup-node: that input writes an `.npmrc` auth line referencing the unset `NODE_AUTH_TOKEN` env var, and pnpm fails config loading on unresolvable env references in `.npmrc`,\n    - keeps linting, tests, and builds in non-OIDC jobs,\n    - avoids dependency caches in the publish workflow,\n    - grants `id-token: write` only to minimal publish jobs that download the verified `dist` artifact and run `pnpm publish --ignore-scripts`. The release publish job first runs `pnpm install --frozen-lockfile --ignore-scripts --filter \"$PACKAGE_NAME\"` because pnpm rewrites `workspace:` specifiers at publish time by reading the linked package's manifest from `node_modules` — without it, publishing a plugin fails with `ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL`. The filter installs only the released package's dependencies, and `--ignore-scripts` keeps dependency code from running in the job. The dev publish job runs the same filtered install for both plugins because their devDependencies keep the inlined plugin core's and configurator's `workspace:*` specifiers; tailwind-merge alone would need no install, since its specifiers are all `catalog:` and resolve from `pnpm-workspace.yaml`,\n    - pins `actions/download-artifact` to an immutable commit in the OIDC jobs and keeps digest mismatches fatal so corrupted or substituted artifacts cannot reach npm publishing.\n- `.github/workflows/label.yml` uses `pull_request_target` only for labeling metadata; do not add repository checkout or PR-code execution to that workflow. `gh` commands in that workflow must pass `--repo` explicitly because there is intentionally no `.git` checkout for repository inference.\n- `.github/workflows/draft-release.yml` runs the release-drafter autolabeler under `pull_request_target` for the same reason: the plain `pull_request` token is read-only on fork PRs, so labeling fails with \"Resource not accessible by integration\" (seen on PR #689). Like `label.yml`, the `auto_label` job must stay checkout-free and never execute PR code.\n- `.github/workflows/comment-released-prs-and-issues.yml`:\n    - runs local action `.github/actions/release-commenter`,\n    - runs on `release.published`, on manual `workflow_dispatch`, and after successful `npm Publish` workflow completion for `push` events on `main`,\n    - keeps the `workflow_run` checkout on trusted default-branch code and reads the triggering main commit's package metadata through the GitHub API instead of checking out the triggering SHA,\n    - supports manual `workflow_dispatch` with optional `head_tag`, `base_tag`, `dry_run`, and `npm_package_name`.\n\n## Practical guardrails\n\n- Keep `README.md` edits synchronized with `docs/README.md`; source-of-truth is docs.\n- Do not rely on `dist/` as edit target; regenerate instead.\n- Favor targeted tests while iterating, then run full suite before finalizing.\n- Any behavior change should include at least one regression test.\n","agents/release-workflow.md":"# Release workflow for agents\n\nUse this guide when preparing release changelog entries and GitHub release text.\n\n## Release model\n\nReleases are per package. Each release belongs to exactly one workspace package and its git tag is namespaced with the package name:\n\n- `tailwind-merge@3.7.0`\n- `@tailwind-merge/vite@0.1.0`\n- `@tailwind-merge/next@0.1.0`\n\nTags without a package prefix (`v3.6.0` and earlier) are the pre-monorepo history and belong to `tailwind-merge`. The publish workflow's tag routing and `scripts/update-pinned-links.mjs` treat them that way via a fallback; release-drafter does not (see below), so the first namespaced tailwind-merge release needs its draft assembled by hand.\n\nThe tag and version-commit format is set by each package's `release` script, which wraps `pnpm version` with `--tag-version-prefix=<package-name>@` and `--message=<package-name>@%s`. pnpm 11 implements `pnpm version` natively instead of proxying to `npm version`, so it never reads npm's `tag-version-prefix`/`message` from `.npmrc`; running bare `pnpm version` yields a plain `v<version>` tag with a bare-version commit message, which the publish workflow's tag routing would treat as a legacy tailwind-merge tag. Both settings are pnpm rc options, but pnpm config is repo-wide (`pnpm-workspace.yaml`) while the prefix differs per package, hence the per-package script.\n\n## Scope\n\n- Applies to release preparation tasks like `tailwind-merge@3.4.1`.\n- Covers:\n    - Draft release ingestion from GitHub.\n    - Changelog updates in `packages/tailwind-merge/docs/changelog/*-changelog.md` (each plugin gets its own changelog location with its first release).\n    - Sponsor section generation and ordering.\n    - GitHub Releases UI text formatting.\n    - Release comments on PRs/issues via `.github/actions/release-commenter`.\n\n## Release drafting\n\n`.github/workflows/draft-release.yml` maintains one draft per package via release-drafter with per-package configs (`config-name`): `.github/release-drafter-tailwind-merge.yml`, `.github/release-drafter-vite.yml`, and `.github/release-drafter-next.yml`. Each config scopes itself with `tag-prefix` (previous-release lookup and version resolution consider only that package's tags) and `include-paths` (only PRs touching the package or its bundled source appear in its draft). Consequences to know:\n\n- A PR touching several packages appears in each of their drafts — correct, since each release ships it.\n- Both plugin scopes include `packages/plugin-core` and `packages/configurator` as well as their own directory: fixes in the shared code ship in both plugins' bundles and belong in both changelogs. The library remains a separate dependency with its own release scope.\n- A PR touching none of those directories (repo infrastructure, CI, root docs) appears in no draft; mention it by hand in a changelog if it matters to users.\n- First namespaced release per package: `tag-prefix` matches no existing release, and the pinned release-drafter (v7) then drafts from nothing rather than from full history — its previous-release lookup keeps only releases whose tag starts with the prefix, and without one it collects no pull requests at all. The draft therefore proposes version `0.1.0` and its body is the `no-changes-template` (\"No changes\") plus a warning banner, while the pre-existing unprefixed draft (`v3.6.1` for tailwind-merge) keeps the real, categorized pull request list and is never updated or removed by the drafter again. When publishing: copy the categorized list from that orphaned draft into the new one, then delete the orphaned draft; set the tag name and title by hand (for example `tailwind-merge@3.7.0` following `v3.6.0`); and write the Full Changelog compare line manually (`v3.6.0...tailwind-merge@3.7.0`). For the plugins the proposed `0.1.0` happens to be right, but their pull request lists are equally empty and must be written from the PRs touching the package, `packages/plugin-core`, and `packages/configurator`.\n- Tag-pinned file links repo-wide (the AGENTS.md link policy) are maintained automatically: each publishable package's `version` lifecycle runs `scripts/update-pinned-links.mjs`, which rewrites every link pinned to that package's newest existing tag to the tag being released (logging each rewrite for release-time review), verifies each target path against the working tree (failing the version step loudly on unresolvable paths, touching nothing), remaps pre-monorepo root paths into the package directory, and leaves links pinned to older tags alone as deliberately historical. Historical links that sit at the newest tag anyway are covered by two escape hatches: changelog directories are never scanned (their entries — including the one for the latest release — always describe a specific past release), and a link carrying the `twm-historical` query parameter stays untouched and exempt from path validation. First release of a package is a no-op for it. Concretely: the tailwind-merge 3.7.0 release re-pinned the vite docs' `v3.6.0` links to `tailwind-merge@3.7.0` with `packages/tailwind-merge/` paths on its own. Relative README links are the complementary case, handled by `scripts/pin-readme-links.mjs`: the plugin's `version` lifecycle runs it after the re-pin to turn the README's `./docs/...` and `../../agents/...` links into absolute links pinned to the tag being released (staging the README for the version commit), because npm cannot resolve relative links, while the repo keeps them relative between releases; the dev-release stamp imports the same helper and pins to the commit instead. Links it has pinned to a tag are absolute from then on and fall under `update-pinned-links.mjs` at later releases. The library needs neither: its README is generated from `docs/README.md` with absolute links by `scripts/update-readme.mjs`.\n- The autolabeler section lives only in the tailwind-merge config; labels are repo-wide.\n\n## Publishing\n\n`.github/workflows/npm-publish.yml` routes `release.published` events by tag prefix: `tailwind-merge@*` and legacy `v*` build and publish `packages/tailwind-merge`, `@tailwind-merge/vite@*` builds and publishes `packages/vite`, `@tailwind-merge/next@*` builds and publishes `packages/next`, and unknown prefixes fail the run. The build job runs repo-wide lint and tests, builds the library alongside the released package (a plugin's `test:exports` verifies its packed tarball against the library's published shape), runs the released package's `test:exports`, and for plugin releases `test:library-release`; publishing happens from the package directory in an isolated OIDC job via `pnpm publish` — after a scripts-disabled install of the released package's dependencies, which pnpm needs to rewrite `workspace:` specifiers (the plugins' tailwind-merge dependency), and with pnpm applying `publishConfig` overrides at pack time (the vite package's dist-exports swap) and handling npm trusted publishing natively, `--provenance` passed explicitly because pnpm does not read `publishConfig.provenance` (mechanics in `agents/tailwind-merge-internals.md`). Dev releases on `main` pushes cover the library and both plugins (see [Dev releases](#dev-releases)).\n\n### First Vite release\n\nRelease the next tailwind-merge update before the Vite plugin. Dev builds of the plugin are a separate track from its first stable release: see [Dev releases](#dev-releases) below for the stamping script, the one-time bootstrap publish, and the trusted-publisher setup that automatic dev publishing depends on.\n\nThe selected initial plugin release is `0.1.0`, with no prerelease suffix or alternate dist-tag. It is the only new package intended for initial publication: the configurator is inlined and keeps an unstable direct API until a separately planned release. This is a sequencing decision, not a promise that the configurator stays unpublished forever.\n\nBefore publishing:\n\n1. Done with `tailwind-merge@3.7.0`, which ships the generator's required `themeKey`, classification/parser exports, and matching configuration semantics; the plugin's `workspace:^` dependency now packs as `tailwind-merge: ^3.7.0` (the caret range keeps later library patches installable without a plugin release). The release build job guards this for every plugin release: `pnpm --filter @tailwind-merge/vite test:library-release` (the shared `scripts/check-library-release.mjs`, given the source trees to scan with `--sources`) resolves the dependency range's lowest version, downloads that release from npm, and fails unless it ships every value and type the bundled plugin core and configurator import from the unstable entry point — a packed check against the workspace library cannot establish that. When it fails, release the library first and bump the plugin's dependency to it; `--library-version` checks an arbitrary release by hand.\n2. The trusted publisher for `@tailwind-merge/vite` is set up as part of the dev-release bootstrap (see [Dev releases](#dev-releases)); the stable release reuses it. The library's configuration does not cover a second package.\n3. Set the plugin's release version when preparing that release. The plugin manifest carries no `private` flag since dev builds made it a published package; the configurator keeps `private: true` because it is never published. Add the plugin's initial changelog/release text and update the pre-release notices in its README/getting-started/versioning docs. The README's relative links need no manual pinning: the plugin's `version` lifecycle rewrites them to the release tag.\n4. Run the library and plugin builds, packed-package gates, and repo-wide checks. The [Vite packaging guide](./vite-plugin.md#build-and-packaging) explains the pnpm export rewrite and which consumer behavior the gate verifies.\n\nField trials use dev builds (`@tailwind-merge/vite@dev`), which pin the same-commit library dev build. Trials against local checkouts must likewise resolve both packages to matching builds; installing only a plugin tarball while letting its library dependency resolve to a registry release does not meet that requirement.\n\n### First Next.js release\n\nThe Next.js plugin follows the Vite plugin's model exactly: initial release `0.1.0`, the plugin core and the configurator inlined, the library as a regular dependency packed from `workspace:^`, `test:library-release` in the release build job (its `--sources` list covers the configurator, the plugin core, and the plugin), and a trusted publisher set up through the dev-release bootstrap below. The Next.js-specific facts a release must respect are in the [Next.js guide](./next-plugin.md#release-boundaries). Release the library update the plugin's imports need before the plugin, as for the Vite plugin.\n\n### Dev releases\n\n`scripts/stamp-dev-version.mjs` prepares a package for a dev publish; run it from the package directory. It stamps `<version>-dev.<sha>` (`GITHUB_SHA` in CI, `HEAD` locally), pins `workspace:` runtime dependencies to the exact same-SHA dev version of that workspace package (the plugins' bundled configurator imports the library's unstable entry point, so a registry release could lack the internals the same commit changed), and rewrites relative README links to commit-pinned absolute links so the npm page resolves them (through the shared `scripts/pin-readme-links.mjs`, see Release drafting above). `--check-registry` verifies that every pinned dependency version is already on npm; a local publish needs that, CI does not because it publishes the library first in the same job. The script dirties the working tree by design; restore the manifest and README from git after publishing locally.\n\nnpm cannot attach a trusted publisher to a package name that has never been published, so a plugin's first publish is a one-time local dev publish with a token — the only step of the whole flow that uses one — after which the trusted publisher covers all later dev and stable publishes. A new plugin stays out of the dev jobs until it is bootstrapped, because a publish step without a trusted publisher fails on every `main` push; both current plugins went through this. The bootstrap, on a clean checkout of the pushed `main` commit whose library dev release has finished publishing — shown for `@tailwind-merge/next`, the Vite plugin's was the same with `packages/vite`:\n\n```bash\npnpm install --frozen-lockfile\npnpm --filter tailwind-merge --filter @tailwind-merge/next build\npnpm --filter @tailwind-merge/next test:exports\ncd packages/next\nnode ../../scripts/stamp-dev-version.mjs --check-registry\npnpm publish --tag dev --access public --no-git-checks --ignore-scripts\ngit checkout -- package.json README.md\n```\n\nPublish without `--provenance`: provenance needs a CI OIDC token and fails locally. Then configure the trusted publisher (repository `dcastil/tailwind-merge`, workflow `npm-publish.yml`, no environment) on the package's npm access page or with `npx npm@latest trust github @tailwind-merge/next --file npm-publish.yml --repo dcastil/tailwind-merge --allow-publish` (`npm trust` needs npm 11.15 or newer). Then add the package to both dev jobs the way the plugins are wired: the build and `test:exports` filters, an artifact upload and download for its `dist`, the filtered install, a stamp line, and a publish step after the library's. With the trusted publishers in place, the publish workflow's dev jobs publish all three packages on every `main` push: the build job builds them and runs every `test:exports` gate, the OIDC job stamps all manifests with the script and publishes the library before the plugins, so the plugins' exact pins always resolve. A bootstrap publish sets `latest` as well as `dev` on the plugin, which npm does for a package's first version regardless of `--tag`; the first stable release moves `latest` to a real version.\n\nThe release commenter's dev pass stays library-only (it reads the library manifest for the head tag, and the action fails when npm has no prior dev version of a package); plugin dev builds do not comment on issues until that pass is extended.\n\n## Release commenter behavior\n\nThe workflow `.github/workflows/comment-released-prs-and-issues.yml` uses the local action `.github/actions/release-commenter`.\n\n- It runs for:\n    - published GitHub releases,\n    - manual workflow dispatch,\n    - completed successful `npm Publish` runs triggered by `push` on `main` (dev-release comment pass, tailwind-merge only even though the same run also publishes the vite package's dev build; the synthetic head tag is `tailwind-merge@<version>-dev.<sha>`).\n- Base-tag selection is scoped to the released tag's package. Legacy un-prefixed tags belong to the `fallback-package-name` input (default `tailwind-merge`).\n- Target collection separately checks each commit's changed files, then the changed files of its associated PRs, before reading their closing references or connected-issue timelines. An unrelated package's commit or PR must not announce its issues as released. PR scope results are cached within the run. Each plugin's scope includes the bundled plugin core and configurator; the library scope includes its package directory plus historical root `src/`, `docs/`, and `tests/` paths for comparisons spanning the monorepo move. Root infrastructure remains outside automatic notification scope.\n- Changed-file checks paginate both REST endpoints and include a rename's previous path, so changes moved out of a package still count. Missing file data or reaching GitHub's file-list cap without establishing scope fails before posting. Keep `releasePackagePaths()` in the action aligned with the release-drafter configs when adding packages or changing bundle ownership. Mocked tests cover mixed ranges, linked issues, pagination, renames, and dry runs without contacting GitHub.\n- Automatic base-tag selection is semver-aware within the package:\n    - Stable release tags compare to the package's previous stable tag (legacy `v*` tags included for tailwind-merge).\n    - Prerelease tags with a SHA suffix (for example `tailwind-merge@3.4.1-dev.<sha>`) resolve base from npm-published versions of the package — the npm package name is derived from the tag prefix — with the same prerelease prefix, picking the nearest ancestor commit.\n    - For SHA-suffixed prereleases, if no prior dev release exists for the same core version, resolution falls back to all dev releases from the highest lower core version (same prerelease prefix) and picks the nearest ancestor commit.\n    - SHA-suffixed prerelease comments link to the npm published version page instead of GitHub release tags.\n    - Other prerelease tags compare to the previous semver tag of the same package (including prereleases).\n- A `0.1.0` release with no same-package history skips commenting gracefully (green run, logged): `0.1.0` is always a package's first version here, so a missing base is the expected state. Trigger manually with an explicit `base_tag` if comments are wanted for a first release. Any other missing-base case still fails loudly, because above `0.1.0` it means release tooling is broken.\n- The action fails before posting if any target issue/PR already has a previous stable release-comment for the same package. Comments from other packages' releases never block — a PR touching two packages legitimately receives one comment per package release. These arrive from separate workflow runs at each package's own release time, and staying separate is deliberate: each comment notifies subscribers about that release, while editing an earlier comment into a combined one would notify nobody.\n- For prereleases, targets that already have a release-comment for the same package are skipped so repeated `-dev.*` ranges do not post duplicate dev comments. Later stable releases can still comment after a prerelease.\n- Posted comment URLs are logged and added to the workflow run summary so all comments from one run can be inspected together.\n- Manual trigger supports optional overrides:\n    - `head_tag`\n    - `base_tag`\n    - `dry_run`\n    - `npm_package_name`\n\nManual dry run example:\n\n```bash\ngh workflow run comment-released-prs-and-issues.yml \\\n  -f head_tag=tailwind-merge@3.4.1 \\\n  -f dry_run=true\n```\n\n## Inputs to collect first\n\n1. Release tag and base compare range (for example `tailwind-merge@3.4.0...tailwind-merge@3.4.1`).\n2. Draft release body from GitHub.\n3. Current active GitHub sponsors.\n4. Sponsor payout input from thanks.dev for the relevant time window.\n\n`thanks.dev` input must come from the user. Do not infer or invent this data.\n\n## Commands\n\nBump a package's version with `pnpm release <version|patch|minor|major>` run inside the package directory (for the library: `packages/tailwind-merge/`); see Release model above for why it is not bare `pnpm version`. pnpm requires a clean working tree, so commit the changelog first. pnpm runs the existing `preversion`, `version`, and `postversion` lifecycle scripts — each stages the files it rewrites itself, because pnpm only stages the manifest — and creates the version commit and an annotated namespaced tag. pnpm also normalizes the manifest when writing the new version (an empty `dependencies` object gets dropped), so keep the manifest free of such fields. To create the commit and tag locally without pushing, run with the `DANYS_MACHINE` variable unset (`env -u DANYS_MACHINE pnpm release minor`): the `preversion` pull and the `postversion` push are gated on it. The `version` step first re-pins tag-pinned links repo-wide via the shared `scripts/update-pinned-links.mjs` (see Release drafting notes above); for the library it then regenerates both the package README and the generated section of the repo-level README via its own `scripts/update-readme.mjs`, with links pinned to the new release tag.\n\nFetch draft release:\n\n```bash\ngh release view <tag> --json tagName,name,isDraft,body,url,createdAt,targetCommitish\n```\n\nFetch active GitHub sponsors in start-date order:\n\n```bash\ngh api graphql -f query='query($login:String!){ user(login:$login){ sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false, orderBy:{field:CREATED_AT, direction:ASC}){ nodes{ sponsorEntity{ ... on User{ login } ... on Organization{ login } } } } } }' -F login=dcastil\n```\n\nCheck whether private active sponsors exist:\n\n```bash\ngh api graphql -f query='query($login:String!){ user(login:$login){ public: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false){ totalCount } all: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:true){ totalCount } } }' -F login=dcastil\n```\n\n## Changelog authoring rules\n\n1. Update the matching major-version changelog file:\n    - v3 releases: `packages/tailwind-merge/docs/changelog/v3-changelog.md`\n    - v2 releases: `packages/tailwind-merge/docs/changelog/v2-changelog.md`\n2. Add new version section at the top:\n    - `## vX.Y.Z`\n3. Keep category headings from draft release:\n    - `### Bug Fixes`, `### New Features`, `### Documentation`, `### Other`, etc.\n4. Keep full compare link in docs style, using the real tag names:\n    - `**Full Changelog**: [\\`tailwind-merge@A.B.C...tailwind-merge@X.Y.Z\\`](https://github.com/dcastil/tailwind-merge/compare/tailwind-merge@A.B.C...tailwind-merge@X.Y.Z)` (the base is `vA.B.C` when it predates the monorepo)\n\n## Sponsor rules\n\n1. Preserve ordering convention from the most recent changelog sponsor sentence unless user explicitly asks otherwise.\n2. Merge sponsor sources:\n    - GitHub Sponsors (active sponsors, ordered by start date).\n    - User-provided thanks.dev payouts for the release window.\n3. Include thanks.dev sponsors only above the user-defined threshold.\n    - Current default: include only amounts greater or equal to 1 USD.\n4. Do not mention sub-threshold sponsors individually unless requested.\n5. Keep summary phrases aligned with user preference:\n    - Examples: `a private sponsor`, `and more via @thnxdev`.\n\n## GitHub release body formatting\n\nWhen producing text for GitHub Releases UI, transform docs formatting:\n\n1. Keep headings and bullet content.\n2. Convert markdown profile links to plain handles:\n    - `[@name](https://github.com/name)` -> `@name`\n3. Convert markdown PR links to plain URLs.\n4. Use plain compare URL:\n    - `**Full Changelog**: https://github.com/dcastil/tailwind-merge/compare/tailwind-merge@A.B.C...tailwind-merge@X.Y.Z`\n\n## Final output contract\n\n1. Provide the release body in one copy-paste-ready markdown block.\n2. If sponsor payout input is missing, stop and ask the user for thanks.dev data before finalizing sponsor lines.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/dcastil/tailwind-merge/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\nThis repository is a pnpm monorepo hosting `tailwind-merge` — a TypeScript library that merges Tailwind class strings by removing style conflicts while preserving non-conflicting and non-Tailwind classes — and its sibling packages. Packages are independent: each owns its lint, test, and type-check setup and releases on its own schedule.\n\n## Primary Goals\n\n- Keep merge behavior correct for the supported Tailwind versions listed in the library documentation.\n- Keep runtime fast (browser-oriented hot path, lazy init, lightweight cache).\n- Keep API and type contracts stable.\n\n## Repo Map\n\n- The library package: `packages/tailwind-merge/` — source in `src/`, tests in `tests/`, human docs in `docs/`, build/release scripts in `scripts/`, published overview `README.md` (autogenerated from `docs/README.md`).\n- Unreleased workspace packages: `packages/configurator/` generates tailwind-merge configs from Tailwind v4 CSS; `packages/plugin-core/` adds what every bundler plugin needs on top (CSS root discovery, runtime-module generation, the generation session); `packages/vite/` and `packages/next/` wrap both for Vite and Next.js with automatic generation and usage pruning. The configurator and the plugins have a maintained `README.md` and user documentation in `docs/`; the plugin core is internal and inlined into both plugins.\n- Repo root: workspace orchestration only — `pnpm-workspace.yaml` (workspace list + shared tool-version catalog), `eslint.config.base.mjs` (shared lint base every scope spreads), root `eslint.config.mjs` (lints root files and `.github/`), root `vitest.config.mts` (aggregates the packages' Vitest projects plus the local GitHub actions' tests), repo-wide release tooling in `scripts/` (`update-pinned-links.mjs`, `pin-readme-links.mjs`, `stamp-dev-version.mjs`, `check-library-release.mjs`), and the repo-level `README.md`.\n- CI workflows: `.github/workflows/`\n- Local GitHub actions: `.github/actions/`\n\n## Agent Docs Map\n\n- `agents/tailwind-merge-internals.md`: library architecture, file ownership, shared testing/build behavior, and CI/security guardrails.\n- `agents/configurator.md`: configurator goals, strategy, generation/classification architecture, correctness gates, and development follow-ups.\n- `agents/plugin-core.md`: the shared plugin core — discovery, generation, and session invariants, testing, and its inlined packaging.\n- `agents/vite-plugin.md`: Vite integration goals, runtime resolution, watching/failure contracts, packaging, and test coverage.\n- `agents/next-plugin.md`: Next.js integration goals, the loader-rule mechanism and its Turbopack/webpack facts, testing harness, packaging, and test coverage.\n- `agents/configurator-performance.md`: dated measurements, optimization rationale, and reproduction methodology.\n- `agents/tailwind-css-version-update.md`: Tailwind CSS version support workflow.\n- `agents/release-workflow.md`: changelog authoring, GitHub release text, sponsor handling, and release comments on issues/PRs.\n\n## Environment and Commands\n\n- Package manager: `pnpm` with a workspace rooted at `pnpm-workspace.yaml`\n- Local development runtime: Node `22.18.0` or newer on a supported LTS release, satisfying the requirements of pnpm 11 and the Babel 8 build toolchain\n- CI runtime: Node `24.18.1`, pinned via `node-version` in the workflows under `.github/workflows/` (Renovate bumps it there; treat the workflows as the source of truth if this number looks stale)\n- Consumer TypeScript support: TypeScript `3.8` and newer; `pnpm test:exports` verifies the generated declarations with the pinned minimum compiler\n- Dependency supply-chain guardrail: `minimumReleaseAge: 4320` in `pnpm-workspace.yaml`, so newly published package versions must be at least three days old before pnpm installs them. Renovate has a matching cooldown in `.github/renovate.json` (see `agents/tailwind-merge-internals.md` for how the two must stay in sync).\n- Dependency build scripts are denied by default unless explicitly allowed in `pnpm-workspace.yaml`; `esbuild` is currently allowed because `.github/actions/metrics-report` needs it.\n- The root manifest's `devEngines.runtime` must encode the real documented Node floor (currently `>=22.18.0`), not a rounded-down one: pnpm validates dependency `engines` against the range's minimum version, and a floor looser than a dependency's own requirement makes pnpm silently skip optional dependencies during install — this is how rolldown's platform binary went missing under an earlier `>=22`, with only a debug-level log mentioning it.\n\nCore commands (repo root):\n\n- `pnpm install --frozen-lockfile`\n- `pnpm lint` — lints root files, then every package's own lint script (`pnpm --recursive lint`)\n- `pnpm test` — runs every package's Vitest project plus the local GitHub actions' tests through the root projects config, with coverage\n- `pnpm test:types` — every package's own tsc project (`pnpm --recursive test:types`)\n- `pnpm test:watch` — cross-package watch mode\n\nLibrary-specific commands (run with `pnpm --filter tailwind-merge <script>` from anywhere, or plain `pnpm <script>` inside `packages/tailwind-merge/`):\n\n- `build`\n- `test:exports`\n- `bench`\n\nVite-plugin-specific commands (run with `pnpm --filter @tailwind-merge/vite <script>`, or plain `pnpm <script>` inside `packages/vite/`):\n\n- `build` — tsdown, ESM bundles plus declarations for all three subpaths into `dist/`\n- `test:exports` — packs the tarball and verifies the published shape; requires both this package's and the library's `build` to have run first (see [Vite build and packaging](./agents/vite-plugin.md#build-and-packaging) for what it checks)\n\nNext.js-plugin-specific commands (run with `pnpm --filter @tailwind-merge/next <script>`, or plain `pnpm <script>` inside `packages/next/`):\n\n- `build` — tsdown, ESM bundles plus declarations for the plugin entry, the loader, and the runtime into `dist/`; the package's `exports` point at `dist/` even in the workspace, and its test global setup runs this build\n- `test` — spawns real `next dev`/`next build` processes for both bundlers (about two minutes); needs the library built, which the global setup does when `packages/tailwind-merge/dist` is missing\n- `test:exports` — packs the tarball and verifies the published shape including a real `next build` of a consumer app; requires both this package's and the library's `build` to have run first (see [Next.js build and packaging](./agents/next-plugin.md#build-and-packaging))\n\nMonorepo conventions:\n\n- Shared tool versions (eslint, typescript, vitest, @types/node) live in the `catalog:` section of `pnpm-workspace.yaml`; package manifests reference them as `\"catalog:\"`. Single-consumer dependencies stay pinned in their package manifest.\n- Every package lints with its own `eslint.config.mjs` spreading `eslint.config.base.mjs` from the root, and tests with its own `vitest.config.mts`; the root configs only orchestrate.\n- No task runner (turborepo or similar) and no affected-only CI on purpose: at the current package count the full suite is fast, and running everything keeps cross-package effects (a tailwind-merge or plugin-core change must exercise both plugins' tests) trivially correct. Revisit when the package count grows or build times make caching pay off.\n- Releases are per package with `<package-name>@<version>` git tags (for example `tailwind-merge@3.7.0`); see `agents/release-workflow.md`.\n\n## Working Rules for Changes\n\n1. Treat `packages/tailwind-merge/src/lib/default-config.ts` as the behavioral source of truth for default class groups and conflicts.\n2. If behavior changes, update tests in `packages/tailwind-merge/tests/` in the same change.\n3. If public behavior/docs change, update docs in `packages/tailwind-merge/docs/` (not directly in a `README.md`).\n4. The library's `README.md` and the generated section of the repo-level `README.md` are version-generated from the library's `docs/README.md`; do not regenerate them during normal development changes. The configurator, Vite, and Next.js READMEs are maintained landing pages, not generated files; edit them alongside their own `docs/` pages.\n5. Avoid manual edits to `dist/` and `coverage/` directories; they are generated artifacts.\n6. Validate packaging paths with `pnpm --filter tailwind-merge build && pnpm --filter tailwind-merge test:exports` when touching exports/build tooling.\n\n## Documentation Sync Policy\n\nTreat documentation updates as part of the same change, not as follow-up work.\n\nSelf-improvement is required while working in this repo: whenever you discover useful information about architecture, workflows, commands, constraints, debugging findings, or recurring pitfalls, add it to the appropriate file in `agents/` or to `AGENTS.md` in the same change. When existing agent guidance becomes stale, misleading, or irrelevant, remove or revise it immediately.\n\nAvoid repeating the same guidance in both `AGENTS.md` and specialized `agents/*` docs unless the rule is critically important. Prefer keeping `AGENTS.md` as the high-level entry point and putting detailed workflow, architecture, release, or CI notes in the relevant specialized agent document.\n\nCross-package user-docs convention: every package's docs recommend setting `twMerge` up in one project-owned file (shown as `tw-merge.ts`) and importing it from there, even without configuration, so that configuring, wrapping, or replacing `twMerge` later is a one-file change. The canonical explanation lives in the library's `docs/configuration.md` (\"Import `twMerge` from one place\"); the plugin and configurator getting-started pages carry their own short version. Keep new setup instructions and usage examples consistent with it.\n\nIt is acceptable to periodically restructure agent documentation when files become too large, when topics are hard to find, or when the current organization no longer matches how the repo is maintained. Split files, rename sections, or move information between `AGENTS.md` and `agents/*` as needed, while preserving useful guidance and removing stale duplication.\n\nRequired when relevant:\n\n1. Update `packages/tailwind-merge/docs/*.md` (or the owning package's docs) for any user-visible behavior, API, version support, or limitation changes.\n2. Update `AGENTS.md` whenever repository-wide agent workflow, repo conventions, required commands, or high-level guardrails change.\n3. Update the relevant file in `agents/` whenever specialized architecture, workflow, CI, release, or testing guidance changes.\n4. Do not regenerate the READMEs during normal development. The library `README.md` and the marked section of the repo-level `README.md` are intentionally generated as part of the version/release flow so visitors are routed to docs for the latest release.\n5. Never reference repo files through `blob/main`/`raw/main`/`tree/main` URLs — they break whenever a file moves. Within the repo use relative links; where a link must be absolute (published docs, JSDoc, release notes), pin it to the latest release tag whose tree contains the file (e.g. `blob/v3.6.0/docs/...`, `blob/tailwind-merge@3.7.0/packages/tailwind-merge/docs/...`). Links pinned to a package's latest tag are re-pinned automatically at that package's next release by `scripts/update-pinned-links.mjs` (run in each publishable package's `version` lifecycle); links pinned to older tags are treated as deliberately historical and never touched. Two escape hatches for historical links that sit at the latest tag: changelog directories (`docs/changelog/`) are never scanned, and a link carrying the `twm-historical` query parameter (e.g. `blob/v3.6.0/docs/foo.md?twm-historical`, before any `#fragment`) stays untouched — GitHub ignores the parameter when serving the page.\n\nDefinition of done for every PR/change:\n\n1. Code and tests are updated.\n2. Relevant docs are updated in the same change set.\n3. `AGENTS.md` and `agents/*` guidance is reviewed, with useful new context added and stale guidance removed whenever the change affects how agents should work in this repo.\n\n## Quick Validation Matrix\n\nAll test paths below are relative to `packages/tailwind-merge/`; run single files with `pnpm --filter tailwind-merge test:watch tests/<file>`.\n\n- Parser or modifier semantics: run `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts`.\n- Class groups/conflicts/default config: run `tests/default-config.test.ts`, `tests/class-group-conflicts.test.ts`, `tests/tailwind-css-versions.test.ts`.\n- Public API/types: run `tests/public-api.test.ts`, `tests/type-generics.test.ts`, and `pnpm --filter tailwind-merge test:types`.\n- Release/package surface: run `pnpm --filter tailwind-merge build` and `pnpm --filter tailwind-merge test:exports`.\n- Configurator, plugin core, or a plugin: run `pnpm --filter @tailwind-merge/configurator test` / `pnpm --filter @tailwind-merge/plugin-core test` / `pnpm --filter @tailwind-merge/vite test` / `pnpm --filter @tailwind-merge/next test` and the matching `test:types` script. A core or configurator change must run both plugin suites.\n- Plugin build/packaging surface: run `pnpm --filter tailwind-merge build`, then the plugin's `build` and `test:exports` (`pnpm --filter @tailwind-merge/vite …` or `pnpm --filter @tailwind-merge/next …`).\n- Repo-wide before finalizing: `pnpm lint`, `pnpm test:types`, `pnpm test` from the root.\n","isInternal":false,"tokens":3105,"sizeBytes":13425},{"name":"release-workflow.md","path":"agents/release-workflow.md","rawUrl":"https://raw.githubusercontent.com/dcastil/tailwind-merge/HEAD/agents/release-workflow.md","title":"Subagent: release-workflow","category":"subagent-persona","format":"markdown","content":"# Release workflow for agents\n\nUse this guide when preparing release changelog entries and GitHub release text.\n\n## Release model\n\nReleases are per package. Each release belongs to exactly one workspace package and its git tag is namespaced with the package name:\n\n- `tailwind-merge@3.7.0`\n- `@tailwind-merge/vite@0.1.0`\n- `@tailwind-merge/next@0.1.0`\n\nTags without a package prefix (`v3.6.0` and earlier) are the pre-monorepo history and belong to `tailwind-merge`. The publish workflow's tag routing and `scripts/update-pinned-links.mjs` treat them that way via a fallback; release-drafter does not (see below), so the first namespaced tailwind-merge release needs its draft assembled by hand.\n\nThe tag and version-commit format is set by each package's `release` script, which wraps `pnpm version` with `--tag-version-prefix=<package-name>@` and `--message=<package-name>@%s`. pnpm 11 implements `pnpm version` natively instead of proxying to `npm version`, so it never reads npm's `tag-version-prefix`/`message` from `.npmrc`; running bare `pnpm version` yields a plain `v<version>` tag with a bare-version commit message, which the publish workflow's tag routing would treat as a legacy tailwind-merge tag. Both settings are pnpm rc options, but pnpm config is repo-wide (`pnpm-workspace.yaml`) while the prefix differs per package, hence the per-package script.\n\n## Scope\n\n- Applies to release preparation tasks like `tailwind-merge@3.4.1`.\n- Covers:\n    - Draft release ingestion from GitHub.\n    - Changelog updates in `packages/tailwind-merge/docs/changelog/*-changelog.md` (each plugin gets its own changelog location with its first release).\n    - Sponsor section generation and ordering.\n    - GitHub Releases UI text formatting.\n    - Release comments on PRs/issues via `.github/actions/release-commenter`.\n\n## Release drafting\n\n`.github/workflows/draft-release.yml` maintains one draft per package via release-drafter with per-package configs (`config-name`): `.github/release-drafter-tailwind-merge.yml`, `.github/release-drafter-vite.yml`, and `.github/release-drafter-next.yml`. Each config scopes itself with `tag-prefix` (previous-release lookup and version resolution consider only that package's tags) and `include-paths` (only PRs touching the package or its bundled source appear in its draft). Consequences to know:\n\n- A PR touching several packages appears in each of their drafts — correct, since each release ships it.\n- Both plugin scopes include `packages/plugin-core` and `packages/configurator` as well as their own directory: fixes in the shared code ship in both plugins' bundles and belong in both changelogs. The library remains a separate dependency with its own release scope.\n- A PR touching none of those directories (repo infrastructure, CI, root docs) appears in no draft; mention it by hand in a changelog if it matters to users.\n- First namespaced release per package: `tag-prefix` matches no existing release, and the pinned release-drafter (v7) then drafts from nothing rather than from full history — its previous-release lookup keeps only releases whose tag starts with the prefix, and without one it collects no pull requests at all. The draft therefore proposes version `0.1.0` and its body is the `no-changes-template` (\"No changes\") plus a warning banner, while the pre-existing unprefixed draft (`v3.6.1` for tailwind-merge) keeps the real, categorized pull request list and is never updated or removed by the drafter again. When publishing: copy the categorized list from that orphaned draft into the new one, then delete the orphaned draft; set the tag name and title by hand (for example `tailwind-merge@3.7.0` following `v3.6.0`); and write the Full Changelog compare line manually (`v3.6.0...tailwind-merge@3.7.0`). For the plugins the proposed `0.1.0` happens to be right, but their pull request lists are equally empty and must be written from the PRs touching the package, `packages/plugin-core`, and `packages/configurator`.\n- Tag-pinned file links repo-wide (the AGENTS.md link policy) are maintained automatically: each publishable package's `version` lifecycle runs `scripts/update-pinned-links.mjs`, which rewrites every link pinned to that package's newest existing tag to the tag being released (logging each rewrite for release-time review), verifies each target path against the working tree (failing the version step loudly on unresolvable paths, touching nothing), remaps pre-monorepo root paths into the package directory, and leaves links pinned to older tags alone as deliberately historical. Historical links that sit at the newest tag anyway are covered by two escape hatches: changelog directories are never scanned (their entries — including the one for the latest release — always describe a specific past release), and a link carrying the `twm-historical` query parameter stays untouched and exempt from path validation. First release of a package is a no-op for it. Concretely: the tailwind-merge 3.7.0 release re-pinned the vite docs' `v3.6.0` links to `tailwind-merge@3.7.0` with `packages/tailwind-merge/` paths on its own. Relative README links are the complementary case, handled by `scripts/pin-readme-links.mjs`: the plugin's `version` lifecycle runs it after the re-pin to turn the README's `./docs/...` and `../../agents/...` links into absolute links pinned to the tag being released (staging the README for the version commit), because npm cannot resolve relative links, while the repo keeps them relative between releases; the dev-release stamp imports the same helper and pins to the commit instead. Links it has pinned to a tag are absolute from then on and fall under `update-pinned-links.mjs` at later releases. The library needs neither: its README is generated from `docs/README.md` with absolute links by `scripts/update-readme.mjs`.\n- The autolabeler section lives only in the tailwind-merge config; labels are repo-wide.\n\n## Publishing\n\n`.github/workflows/npm-publish.yml` routes `release.published` events by tag prefix: `tailwind-merge@*` and legacy `v*` build and publish `packages/tailwind-merge`, `@tailwind-merge/vite@*` builds and publishes `packages/vite`, `@tailwind-merge/next@*` builds and publishes `packages/next`, and unknown prefixes fail the run. The build job runs repo-wide lint and tests, builds the library alongside the released package (a plugin's `test:exports` verifies its packed tarball against the library's published shape), runs the released package's `test:exports`, and for plugin releases `test:library-release`; publishing happens from the package directory in an isolated OIDC job via `pnpm publish` — after a scripts-disabled install of the released package's dependencies, which pnpm needs to rewrite `workspace:` specifiers (the plugins' tailwind-merge dependency), and with pnpm applying `publishConfig` overrides at pack time (the vite package's dist-exports swap) and handling npm trusted publishing natively, `--provenance` passed explicitly because pnpm does not read `publishConfig.provenance` (mechanics in `agents/tailwind-merge-internals.md`). Dev releases on `main` pushes cover the library and both plugins (see [Dev releases](#dev-releases)).\n\n### First Vite release\n\nRelease the next tailwind-merge update before the Vite plugin. Dev builds of the plugin are a separate track from its first stable release: see [Dev releases](#dev-releases) below for the stamping script, the one-time bootstrap publish, and the trusted-publisher setup that automatic dev publishing depends on.\n\nThe selected initial plugin release is `0.1.0`, with no prerelease suffix or alternate dist-tag. It is the only new package intended for initial publication: the configurator is inlined and keeps an unstable direct API until a separately planned release. This is a sequencing decision, not a promise that the configurator stays unpublished forever.\n\nBefore publishing:\n\n1. Done with `tailwind-merge@3.7.0`, which ships the generator's required `themeKey`, classification/parser exports, and matching configuration semantics; the plugin's `workspace:^` dependency now packs as `tailwind-merge: ^3.7.0` (the caret range keeps later library patches installable without a plugin release). The release build job guards this for every plugin release: `pnpm --filter @tailwind-merge/vite test:library-release` (the shared `scripts/check-library-release.mjs`, given the source trees to scan with `--sources`) resolves the dependency range's lowest version, downloads that release from npm, and fails unless it ships every value and type the bundled plugin core and configurator import from the unstable entry point — a packed check against the workspace library cannot establish that. When it fails, release the library first and bump the plugin's dependency to it; `--library-version` checks an arbitrary release by hand.\n2. The trusted publisher for `@tailwind-merge/vite` is set up as part of the dev-release bootstrap (see [Dev releases](#dev-releases)); the stable release reuses it. The library's configuration does not cover a second package.\n3. Set the plugin's release version when preparing that release. The plugin manifest carries no `private` flag since dev builds made it a published package; the configurator keeps `private: true` because it is never published. Add the plugin's initial changelog/release text and update the pre-release notices in its README/getting-started/versioning docs. The README's relative links need no manual pinning: the plugin's `version` lifecycle rewrites them to the release tag.\n4. Run the library and plugin builds, packed-package gates, and repo-wide checks. The [Vite packaging guide](./vite-plugin.md#build-and-packaging) explains the pnpm export rewrite and which consumer behavior the gate verifies.\n\nField trials use dev builds (`@tailwind-merge/vite@dev`), which pin the same-commit library dev build. Trials against local checkouts must likewise resolve both packages to matching builds; installing only a plugin tarball while letting its library dependency resolve to a registry release does not meet that requirement.\n\n### First Next.js release\n\nThe Next.js plugin follows the Vite plugin's model exactly: initial release `0.1.0`, the plugin core and the configurator inlined, the library as a regular dependency packed from `workspace:^`, `test:library-release` in the release build job (its `--sources` list covers the configurator, the plugin core, and the plugin), and a trusted publisher set up through the dev-release bootstrap below. The Next.js-specific facts a release must respect are in the [Next.js guide](./next-plugin.md#release-boundaries). Release the library update the plugin's imports need before the plugin, as for the Vite plugin.\n\n### Dev releases\n\n`scripts/stamp-dev-version.mjs` prepares a package for a dev publish; run it from the package directory. It stamps `<version>-dev.<sha>` (`GITHUB_SHA` in CI, `HEAD` locally), pins `workspace:` runtime dependencies to the exact same-SHA dev version of that workspace package (the plugins' bundled configurator imports the library's unstable entry point, so a registry release could lack the internals the same commit changed), and rewrites relative README links to commit-pinned absolute links so the npm page resolves them (through the shared `scripts/pin-readme-links.mjs`, see Release drafting above). `--check-registry` verifies that every pinned dependency version is already on npm; a local publish needs that, CI does not because it publishes the library first in the same job. The script dirties the working tree by design; restore the manifest and README from git after publishing locally.\n\nnpm cannot attach a trusted publisher to a package name that has never been published, so a plugin's first publish is a one-time local dev publish with a token — the only step of the whole flow that uses one — after which the trusted publisher covers all later dev and stable publishes. A new plugin stays out of the dev jobs until it is bootstrapped, because a publish step without a trusted publisher fails on every `main` push; both current plugins went through this. The bootstrap, on a clean checkout of the pushed `main` commit whose library dev release has finished publishing — shown for `@tailwind-merge/next`, the Vite plugin's was the same with `packages/vite`:\n\n```bash\npnpm install --frozen-lockfile\npnpm --filter tailwind-merge --filter @tailwind-merge/next build\npnpm --filter @tailwind-merge/next test:exports\ncd packages/next\nnode ../../scripts/stamp-dev-version.mjs --check-registry\npnpm publish --tag dev --access public --no-git-checks --ignore-scripts\ngit checkout -- package.json README.md\n```\n\nPublish without `--provenance`: provenance needs a CI OIDC token and fails locally. Then configure the trusted publisher (repository `dcastil/tailwind-merge`, workflow `npm-publish.yml`, no environment) on the package's npm access page or with `npx npm@latest trust github @tailwind-merge/next --file npm-publish.yml --repo dcastil/tailwind-merge --allow-publish` (`npm trust` needs npm 11.15 or newer). Then add the package to both dev jobs the way the plugins are wired: the build and `test:exports` filters, an artifact upload and download for its `dist`, the filtered install, a stamp line, and a publish step after the library's. With the trusted publishers in place, the publish workflow's dev jobs publish all three packages on every `main` push: the build job builds them and runs every `test:exports` gate, the OIDC job stamps all manifests with the script and publishes the library before the plugins, so the plugins' exact pins always resolve. A bootstrap publish sets `latest` as well as `dev` on the plugin, which npm does for a package's first version regardless of `--tag`; the first stable release moves `latest` to a real version.\n\nThe release commenter's dev pass stays library-only (it reads the library manifest for the head tag, and the action fails when npm has no prior dev version of a package); plugin dev builds do not comment on issues until that pass is extended.\n\n## Release commenter behavior\n\nThe workflow `.github/workflows/comment-released-prs-and-issues.yml` uses the local action `.github/actions/release-commenter`.\n\n- It runs for:\n    - published GitHub releases,\n    - manual workflow dispatch,\n    - completed successful `npm Publish` runs triggered by `push` on `main` (dev-release comment pass, tailwind-merge only even though the same run also publishes the vite package's dev build; the synthetic head tag is `tailwind-merge@<version>-dev.<sha>`).\n- Base-tag selection is scoped to the released tag's package. Legacy un-prefixed tags belong to the `fallback-package-name` input (default `tailwind-merge`).\n- Target collection separately checks each commit's changed files, then the changed files of its associated PRs, before reading their closing references or connected-issue timelines. An unrelated package's commit or PR must not announce its issues as released. PR scope results are cached within the run. Each plugin's scope includes the bundled plugin core and configurator; the library scope includes its package directory plus historical root `src/`, `docs/`, and `tests/` paths for comparisons spanning the monorepo move. Root infrastructure remains outside automatic notification scope.\n- Changed-file checks paginate both REST endpoints and include a rename's previous path, so changes moved out of a package still count. Missing file data or reaching GitHub's file-list cap without establishing scope fails before posting. Keep `releasePackagePaths()` in the action aligned with the release-drafter configs when adding packages or changing bundle ownership. Mocked tests cover mixed ranges, linked issues, pagination, renames, and dry runs without contacting GitHub.\n- Automatic base-tag selection is semver-aware within the package:\n    - Stable release tags compare to the package's previous stable tag (legacy `v*` tags included for tailwind-merge).\n    - Prerelease tags with a SHA suffix (for example `tailwind-merge@3.4.1-dev.<sha>`) resolve base from npm-published versions of the package — the npm package name is derived from the tag prefix — with the same prerelease prefix, picking the nearest ancestor commit.\n    - For SHA-suffixed prereleases, if no prior dev release exists for the same core version, resolution falls back to all dev releases from the highest lower core version (same prerelease prefix) and picks the nearest ancestor commit.\n    - SHA-suffixed prerelease comments link to the npm published version page instead of GitHub release tags.\n    - Other prerelease tags compare to the previous semver tag of the same package (including prereleases).\n- A `0.1.0` release with no same-package history skips commenting gracefully (green run, logged): `0.1.0` is always a package's first version here, so a missing base is the expected state. Trigger manually with an explicit `base_tag` if comments are wanted for a first release. Any other missing-base case still fails loudly, because above `0.1.0` it means release tooling is broken.\n- The action fails before posting if any target issue/PR already has a previous stable release-comment for the same package. Comments from other packages' releases never block — a PR touching two packages legitimately receives one comment per package release. These arrive from separate workflow runs at each package's own release time, and staying separate is deliberate: each comment notifies subscribers about that release, while editing an earlier comment into a combined one would notify nobody.\n- For prereleases, targets that already have a release-comment for the same package are skipped so repeated `-dev.*` ranges do not post duplicate dev comments. Later stable releases can still comment after a prerelease.\n- Posted comment URLs are logged and added to the workflow run summary so all comments from one run can be inspected together.\n- Manual trigger supports optional overrides:\n    - `head_tag`\n    - `base_tag`\n    - `dry_run`\n    - `npm_package_name`\n\nManual dry run example:\n\n```bash\ngh workflow run comment-released-prs-and-issues.yml \\\n  -f head_tag=tailwind-merge@3.4.1 \\\n  -f dry_run=true\n```\n\n## Inputs to collect first\n\n1. Release tag and base compare range (for example `tailwind-merge@3.4.0...tailwind-merge@3.4.1`).\n2. Draft release body from GitHub.\n3. Current active GitHub sponsors.\n4. Sponsor payout input from thanks.dev for the relevant time window.\n\n`thanks.dev` input must come from the user. Do not infer or invent this data.\n\n## Commands\n\nBump a package's version with `pnpm release <version|patch|minor|major>` run inside the package directory (for the library: `packages/tailwind-merge/`); see Release model above for why it is not bare `pnpm version`. pnpm requires a clean working tree, so commit the changelog first. pnpm runs the existing `preversion`, `version`, and `postversion` lifecycle scripts — each stages the files it rewrites itself, because pnpm only stages the manifest — and creates the version commit and an annotated namespaced tag. pnpm also normalizes the manifest when writing the new version (an empty `dependencies` object gets dropped), so keep the manifest free of such fields. To create the commit and tag locally without pushing, run with the `DANYS_MACHINE` variable unset (`env -u DANYS_MACHINE pnpm release minor`): the `preversion` pull and the `postversion` push are gated on it. The `version` step first re-pins tag-pinned links repo-wide via the shared `scripts/update-pinned-links.mjs` (see Release drafting notes above); for the library it then regenerates both the package README and the generated section of the repo-level README via its own `scripts/update-readme.mjs`, with links pinned to the new release tag.\n\nFetch draft release:\n\n```bash\ngh release view <tag> --json tagName,name,isDraft,body,url,createdAt,targetCommitish\n```\n\nFetch active GitHub sponsors in start-date order:\n\n```bash\ngh api graphql -f query='query($login:String!){ user(login:$login){ sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false, orderBy:{field:CREATED_AT, direction:ASC}){ nodes{ sponsorEntity{ ... on User{ login } ... on Organization{ login } } } } } }' -F login=dcastil\n```\n\nCheck whether private active sponsors exist:\n\n```bash\ngh api graphql -f query='query($login:String!){ user(login:$login){ public: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false){ totalCount } all: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:true){ totalCount } } }' -F login=dcastil\n```\n\n## Changelog authoring rules\n\n1. Update the matching major-version changelog file:\n    - v3 releases: `packages/tailwind-merge/docs/changelog/v3-changelog.md`\n    - v2 releases: `packages/tailwind-merge/docs/changelog/v2-changelog.md`\n2. Add new version section at the top:\n    - `## vX.Y.Z`\n3. Keep category headings from draft release:\n    - `### Bug Fixes`, `### New Features`, `### Documentation`, `### Other`, etc.\n4. Keep full compare link in docs style, using the real tag names:\n    - `**Full Changelog**: [\\`tailwind-merge@A.B.C...tailwind-merge@X.Y.Z\\`](https://github.com/dcastil/tailwind-merge/compare/tailwind-merge@A.B.C...tailwind-merge@X.Y.Z)` (the base is `vA.B.C` when it predates the monorepo)\n\n## Sponsor rules\n\n1. Preserve ordering convention from the most recent changelog sponsor sentence unless user explicitly asks otherwise.\n2. Merge sponsor sources:\n    - GitHub Sponsors (active sponsors, ordered by start date).\n    - User-provided thanks.dev payouts for the release window.\n3. Include thanks.dev sponsors only above the user-defined threshold.\n    - Current default: include only amounts greater or equal to 1 USD.\n4. Do not mention sub-threshold sponsors individually unless requested.\n5. Keep summary phrases aligned with user preference:\n    - Examples: `a private sponsor`, `and more via @thnxdev`.\n\n## GitHub release body formatting\n\nWhen producing text for GitHub Releases UI, transform docs formatting:\n\n1. Keep headings and bullet content.\n2. Convert markdown profile links to plain handles:\n    - `[@name](https://github.com/name)` -> `@name`\n3. Convert markdown PR links to plain URLs.\n4. Use plain compare URL:\n    - `**Full Changelog**: https://github.com/dcastil/tailwind-merge/compare/tailwind-merge@A.B.C...tailwind-merge@X.Y.Z`\n\n## Final output contract\n\n1. Provide the release body in one copy-paste-ready markdown block.\n2. If sponsor payout input is missing, stop and ask the user for thanks.dev data before finalizing sponsor lines.\n","isInternal":false,"tokens":5081,"sizeBytes":22598},{"name":"tailwind-css-version-update.md","path":"agents/tailwind-css-version-update.md","rawUrl":"https://raw.githubusercontent.com/dcastil/tailwind-merge/HEAD/agents/tailwind-css-version-update.md","title":"Subagent: tailwind-css-version-update","category":"subagent-persona","format":"markdown","content":"# Tailwind CSS version update workflow\n\nUse this guide when updating tailwind-merge for a new Tailwind CSS version.\n\nAll `src/`, `tests/`, and `docs/` paths in this file are relative to the library package `packages/tailwind-merge/`; run the quoted commands from that directory (or through `pnpm --filter tailwind-merge`).\n\nThe main risk in this work is false confidence: Tailwind release notes, deployed docs, source code, and website docs can land at different times. Treat every feature as something to verify from official sources before changing `src/lib/default-config.ts` or parser behavior.\n\n## Official sources\n\n- Tailwind CSS releases: https://github.com/tailwindlabs/tailwindcss/releases\n- Tailwind CSS source and PRs: https://github.com/tailwindlabs/tailwindcss\n- Tailwind CSS release tag pattern: `https://github.com/tailwindlabs/tailwindcss/releases/tag/vX.Y.Z`\n- Tailwind CSS deployed docs: https://tailwindcss.com/docs\n- Tailwind CSS website source and docs PRs: https://github.com/tailwindlabs/tailwindcss.com\n- Prior tailwind-merge support PRs for comparison, for example v4.2 support: https://github.com/dcastil/tailwind-merge/pull/651\n\nWhen docs for a just-released Tailwind version are not deployed yet, use the `tailwindcss.com` repository and its PRs to find future docs pages, sidebar order, and link targets.\n\n## Investigation checklist\n\n1. Read the Tailwind CSS release notes for the target version.\n2. Open every linked Tailwind CSS PR that mentions utilities, variants, parser syntax, arbitrary values, or behavior changes.\n3. Inspect Tailwind CSS source and tests when release notes or PR descriptions are ambiguous. Do not infer support for arbitrary values or variables from naming patterns alone.\n4. Check the `tailwindcss.com` repository for docs PRs when deployed docs lag behind the release.\n5. Compare against the previous tailwind-merge Tailwind-support PR to reuse the same structure and testing style.\n6. Make a feature list before coding. Split work into simple utility additions, ordering/docs updates, conflict changes, and parser/config API changes.\n7. Record no-op decisions in the PR or issue summary, especially when a Tailwind PR looked relevant but does not require a tailwind-merge change.\n\n## Triage questions for each Tailwind change\n\n- Does it emit new class names? If yes, it likely needs `classGroups` entries.\n- Does it change which utilities conflict? If yes, update `conflictingClassGroups` or `conflictingClassGroupModifiers`.\n- Does it add a new arbitrary value or arbitrary variable form? Verify the exact accepted syntax in Tailwind source before adding validators.\n- Does it add a slash form that is part of the base class rather than a postfix modifier? Prefer config-driven lookup such as `postfixLookupClassGroups` before changing parser logic.\n- Does it change modifier, variant, prefix, important, or arbitrary-variant syntax? Parser changes are higher risk and need focused parser/modifier tests.\n- Does it only change generated CSS for an existing class without changing class syntax or conflicts? It may not require a tailwind-merge change.\n- Does it remove or rename classes? Check whether keeping old classes would create incorrect merges for the supported Tailwind range.\n\n## Default config ordering and links\n\nKeep `src/lib/default-config.ts` ordered according to the Tailwind CSS docs sidebar, not alphabetically and not by implementation convenience.\n\nFor new utility groups:\n\n- Use `tailwindcss.com` or the website repository to find the final docs URL for each `@see` comment.\n- If the public docs are not deployed, inspect docs PRs in `tailwindlabs/tailwindcss.com`.\n- Replace temporary `TBD` links before finishing the change.\n- Keep related utility groups in sidebar order even when they share a prefix.\n\nThere is an important exception to sidebar order: class-group order affects validator precedence for groups that share the same prefix path. The class map tries exact child paths first, then runs validators in the order they were registered. If a broad validator appears before a more specific validator for the same prefix, the broad group can claim the class before the specific group sees it.\n\nWhen groups with the same prefix use validators:\n\n- Put the most specific validators first.\n- Put broad or catch-all validators last for that prefix.\n- Treat color validators as especially sensitive because they often accept arbitrary values or otherwise broad value sets.\n- Prefer correctness over strict sidebar order inside that shared-prefix cluster.\n\nFor example, if a `text-*` color group uses a broad color validator, keep it after more specific `text-*` groups such as font-size-related groups so ambiguous classes resolve to the intended class group.\n\nExamples learned during the Tailwind CSS v4.3 update:\n\n- `tab-size` belongs after `text-indent` and before `vertical-align`.\n- `zoom` belongs after `translate`.\n- Scrollbar groups should follow the sidebar order for scrollbar color, scrollbar gutter, and scrollbar width.\n- PRs that look relevant to existing utilities, such as changes around `start` and `end`, still need source review before changing tailwind-merge.\n\n## Implementation guidance\n\nUse `src/lib/default-config.ts` as the behavioral source of truth for utilities and conflicts.\n\nPrefer config changes over parser changes. Parser and merge hot paths run frequently in UI renders, so keep extra work small and opt-in:\n\n- Normalize config arrays into lookup objects in `createConfigUtils` when they need fast querying.\n- Avoid scanning or balancing brackets in validators when a cheap prefix or character check can reject most inputs.\n- Put specialized syntax checks in `src/lib/validators.ts` when they are reusable or need direct tests.\n- Add comments only for non-obvious behavior, especially when a config entry compensates for parser ambiguity.\n\nFor slash syntax, remember that tailwind-merge initially treats the part after `/` as a possible postfix modifier. If the full class can belong to a different group, use a generic config mechanism rather than a syntax-specific parser branch.\n\nThe Tailwind CSS v4.3 named container query support is the reference pattern:\n\n- `@container-size` is a container type class.\n- `@container-size/sidebar` combines container type and container name.\n- The parser already exposes a possible postfix modifier, so no bracket-counting parser change is needed.\n- `container-named` creates a conflict with `container-type`, but not the reverse, because a later type-only class should preserve the earlier container name.\n- `postfixLookupClassGroups: ['container-type']` tells merge logic to try resolving the full slash class only after the pre-slash class resolves to `container-type`.\n\n## Testing checklist\n\nAdd or update tests based on the kind of change:\n\n- Tailwind version compatibility: `tests/tailwind-css-versions.test.ts`\n- Default config class map coverage: `tests/class-map.test.ts`\n- Class group conflicts: `tests/class-group-conflicts.test.ts` and `tests/conflicts-across-class-groups.test.ts`\n- Parser or modifier semantics: `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts`\n- Validators: `tests/validators.test.ts`\n- Public exports and config API: `tests/public-api.test.ts`, `tests/type-generics.test.ts`, `tests/merge-configs.test.ts`\n- Docs examples: `tests/docs-examples.test.ts`\n\nFor new utility support, include tests for:\n\n- Last class wins within the new class group.\n- Cross-group conflicts in both orders when conflicts are asymmetric.\n- Arbitrary values and arbitrary variables only when Tailwind source confirms support.\n- Important modifier and variant combinations when parser or conflict behavior is involved.\n- Unknown or invalid classes remaining untouched.\n\nFor TypeScript config changes, remember that `pnpm tsc --noEmit` only checks `src`. Use the test tsconfig as an additional check when touching public types:\n\n```sh\npnpm tsc --noEmit -p tests/tsconfig.json --moduleResolution bundler\n```\n\nThe plain `tests/tsconfig.json` currently inherits `moduleResolution: Node`, which can surface Vitest package export resolution errors unrelated to tailwind-merge types.\n\n## Documentation checklist\n\nUpdate docs in the same change when behavior or public API changes:\n\n- `docs/configuration.md` for config behavior.\n- `docs/api-reference.md` for exported validators, config fields, or public types.\n- `docs/changelog/v3-changelog.md` when preparing a release note.\n- `AGENTS.md` and `agents/*` when the update process or repo guidance changes.\n\nDo not regenerate `README.md` during normal development; it is generated from `docs/README.md` during the release flow.\n\n## Validation checklist\n\nRun targeted tests while iterating, then run the full validation set before finalizing:\n\n```sh\npnpm lint\npnpm tsc --noEmit\npnpm test\ngit diff --check\n```\n\nRun `pnpm build && pnpm test:exports` when touching package exports, build tooling, or generated declaration behavior.\n","isInternal":false,"tokens":1906,"sizeBytes":8994},{"name":"tailwind-merge-internals.md","path":"agents/tailwind-merge-internals.md","rawUrl":"https://raw.githubusercontent.com/dcastil/tailwind-merge/HEAD/agents/tailwind-merge-internals.md","title":"Subagent: tailwind-merge-internals","category":"subagent-persona","format":"markdown","content":"# tailwind-merge internals guide\n\nThis file is a practical map for agent-driven changes in this repo.\n\nSince the 2026-08-10 monorepo migration the library lives in `packages/tailwind-merge/`; all `src/`, `tests/`, `docs/`, and `scripts/` paths in this file are relative to that package directory unless a path starts with `packages/` or `.github/`.\n\nLinks from the library's `docs/` to repository-wide files must traverse three parents; for example, `../../../.github/CONTRIBUTING.md`. Resolve relative links from the owning document after moving files, not from the repository root.\n\n## How merge execution works\n\n1. `twMerge` in `src/lib/tw-merge.ts` is created via `createTailwindMerge(getDefaultConfig)`.\n2. `createTailwindMerge` in `src/lib/create-tailwind-merge.ts`:\n    - lazily builds config utils on first call,\n    - joins inputs via `twJoin`,\n    - caches final joined-input results,\n    - calls `mergeClassList` on cache miss.\n3. `mergeClassList` in `src/lib/merge-classlist.ts`:\n    - parses classes,\n    - walks from right to left so \"last class wins\",\n    - resolves class group IDs and asymmetric conflicts,\n    - keeps non-Tailwind/external classes untouched.\n4. `createClassGroupUtils` in `src/lib/class-group-utils.ts`:\n    - compiles class definitions into a recursive map + validator list,\n    - maps class names to class group IDs,\n    - assigns arbitrary properties (`[key:value]`) synthetic group IDs prefixed with `ARBITRARY_PROPERTY_PREFIX` (e.g. `arbitrary..key`) so distinct properties conflict independently,\n    - returns conflicting groups, including postfix-modifier conflicts.\n5. `createParseClassName` in `src/lib/parse-class-name.ts`:\n    - parses stacked modifiers with bracket/paren depth tracking,\n    - supports important modifier (`!`) and legacy v3 position,\n    - supports optional prefix and `experimentalParseClassName` override.\n6. `createSortModifiers` in `src/lib/sort-modifiers.ts`:\n    - alphabetically sorts non-sensitive modifiers,\n    - preserves placement around arbitrary/order-sensitive modifiers.\n\nThe unstable tooling entry point also exports `createClassGroupLookup` from `src/lib/class-group-lookup.ts`. It runs one candidate through `mergeClassList` with an observing class-map function and returns every successful lookup, including a slash class's intermediate base. The configurator uses this to preserve lookup dependencies during pruning. Keep this instrumentation outside the normal merge path; it is an adapter for build-time inspection, not a second implementation of modifier or lookup semantics. Both built ESM/CJS export checks exercise it.\n\n## Performance constraints in hot-path code\n\nThe hot paths are `src/lib/merge-classlist.ts` and `src/lib/class-group-utils.ts`; `src/lib/parse-class-name.ts` runs per class on cache misses. When editing these files, preserve the existing V8-oriented patterns:\n\n- Keep call sites monomorphic: functions are deliberately split per input type (see the `create*` factories and the \"monomorphic\" comments in `class-group-utils.ts` and `parse-class-name.ts`); do not merge them back into one polymorphic function.\n- Keep object shapes consistent: always define optional properties explicitly (as `undefined`/`null`) so objects created in different branches share a hidden class; result objects are built through factory functions for this reason.\n- Pre-allocate arrays where the final length is known (see `concatArrays` in `src/lib/utils.ts`) instead of spreading or pushing, and return shared constants like `EMPTY_CONFLICTS` instead of fresh empty arrays.\n- Avoid adding function calls or allocations in hot loops when an inline check works.\n\n## Files to edit by intent\n\n- Add or change Tailwind utility support:\n    - `src/lib/default-config.ts`\n    - Usually paired with tests in `tests/tailwind-css-versions.test.ts` and focused utility tests.\n    - For a full Tailwind CSS version update, follow `agents/tailwind-css-version-update.md`.\n\n- Change conflict behavior:\n    - `src/lib/default-config.ts` (`conflictingClassGroups`, `conflictingClassGroupModifiers`)\n    - `tests/class-group-conflicts.test.ts`\n    - `tests/conflicts-across-class-groups.test.ts`\n\n- Change parser/modifier behavior:\n    - `src/lib/parse-class-name.ts`\n    - `src/lib/sort-modifiers.ts`\n    - `tests/modifiers.test.ts`\n    - `tests/arbitrary-variants.test.ts`\n    - `tests/experimental-parse-class-name.test.ts`\n\n- Change config extension APIs:\n    - `src/lib/extend-tailwind-merge.ts`\n    - `src/lib/create-tailwind-merge.ts`\n    - `src/lib/merge-configs.ts`\n    - `tests/create-tailwind-merge.test.ts`\n    - `tests/extend-tailwind-merge.test.ts`\n    - `tests/merge-configs.test.ts`\n    - `tests/type-generics.test.ts`\n\n- Change validators:\n    - `src/lib/validators.ts`\n    - `tests/validators.test.ts`\n    - Any impacted arbitrary-value tests\n\n## Testing strategy in this repo\n\n- Unit/behavior tests are granular by feature area in `tests/*.test.ts`.\n- Color-function regression tests should verify preservation of length utilities and replacement of other colors in `tests/colors.test.ts`.\n- `tests/tailwind-css-versions.test.ts` is the compatibility anchor for Tailwind feature coverage by version.\n- `tests/docs-examples.test.ts` enforces that documented `twMerge(...) // -> ...` examples stay correct. Its extraction regex supports a limited set of characters (for example, `/` in arguments is excluded); verify new examples are collected and update the assertion count when adding supported examples.\n- `tests/public-api.test.ts` guards runtime exports and broad type usage.\n- `tests/tw-merge.benchmark.ts` is for performance benchmarking (`pnpm bench`), not correctness gating in CI.\n\nRun a single test file or test by name with Vitest's filter syntax: `pnpm --filter tailwind-merge test:watch tests/modifiers.test.ts` or `pnpm --filter tailwind-merge test:watch -t \"test name pattern\"`.\n\nPackage-specific generation, scanning, and Vite test guidance lives in [configurator development](./configurator.md#debugging-and-validation) and [Vite plugin development](./vite-plugin.md#testing-and-remaining-coverage).\n\nVitest is set up as one project per package: each package's `vitest.config.mts` carries its aliases and plugins, and the root `vitest.config.mts` aggregates them (plus an inline project for `.github/actions/**/*.test.mjs`, since the local actions aren't workspace packages) so root `pnpm test` runs everything with coverage. Because collection is scoped to those projects, stray test files elsewhere in the repo — notably full repo copies in `.claude/` worktrees, which the pre-monorepo root config silently collected and ran as duplicates — never join the suite.\n\nThe root coverage configuration measures `packages/*/src/**/*.ts`, including the configurator and Vite plugin. In projects mode, package-local coverage settings do not control the aggregate report. Coverage is diagnostic, with no percentage threshold; use uncovered branches to guide behavioral tests rather than weakening assertions or padding the percentage. The Vitest report does not capture code executed by spawned CLI/build processes, so interpret unmeasured entrypoints alongside their integration tests.\n\nRecommended local sequence for non-trivial changes:\n\n1. `pnpm lint`\n2. `pnpm test:types`\n3. `pnpm test`\n4. `pnpm --filter tailwind-merge build`\n5. `pnpm --filter tailwind-merge test:exports`\n\n## Build and packaging\n\n- Build uses Rollup (`scripts/rollup.config.mjs`) to produce:\n    - ESM and CJS bundles,\n    - ES5 variants,\n    - unified type declarations.\n- The `build` script passes Rollup `--forceExit` because the TypeScript Rollup plugin can leave referenced file-watch handles open after a non-watch build has already written all outputs.\n- Babel 8 performs the final target-specific transforms for all four JavaScript bundles. Its `@babel/preset-env` `bugfixes` behavior is unconditional, and the removed `loose: true` option is preserved through top-level assumptions plus the `transform-typeof-symbol` exclusion in `scripts/rollup.config.mjs`. The config deliberately replaces the migration guide's `arrayLikeIsIterable` with `iterableIsArray` because Babel disallows enabling both and every transformed spread and destructuring operand is an array; this suppresses generic iterable helpers and keeps the generated JavaScript bundles equivalent in size and behavior to the Babel 7 bundles.\n- TypeScript 6 requires the source `rootDir` to be explicit when declaration output is redirected by Rollup, so `tsconfig.json` fixes it to `src`. Module resolution uses `Bundler`, matching the Rollup build and avoiding the deprecated `Node`/`node10` resolver.\n- Export smoke tests:\n    - `scripts/test-built-package-exports.cjs`\n    - `scripts/test-built-package-exports.mjs`\n    - Both scripts execute the default and ES5 bundles so build-tool upgrades cannot break only the legacy entry point unnoticed.\n- `pnpm test:types` type-checks both the source and the test suite with the development compiler. `scripts/typescript-compatibility/consumer.ts` separately compiles the built declaration bundle with the `typescript-compat` compiler alias pinned to TypeScript 3.8.3. `pnpm test:exports` includes the compatibility check because TypeScript 3.8 is the documented minimum consumer version; keep the alias excluded from Renovate updates and treat any increase to this floor as a major-version change.\n- `scripts/update-readme.mjs` (run in the `version` script via `zx`) generates two targets from `docs/README.md`: the package `README.md` shipped to npm, and the section between the `tailwind-merge-docs` markers in the repo-level `README.md`; it fails the version step loudly when the repo README's markers are missing.\n- The `version` script also runs the repo-wide `scripts/update-pinned-links.mjs` (repo root, dependency-free Node; the vite package additionally runs `scripts/pin-readme-links.mjs`, see the release guide), which re-pins every link pinned to the package's newest existing tag onto the tag being released, verifying targets against the working tree and failing loudly — without touching anything — when a linked path no longer resolves. Older-tag links are deliberately historical and stay untouched, changelog directories are excluded from scanning entirely, and the `twm-historical` query parameter on a link shields it individually for the rare historical link elsewhere.\n\nFor the Vite package's tsdown build, workspace/published export split, and packed-consumer gate, read [Vite build and packaging](./vite-plugin.md#build-and-packaging).\n\n## CI Behavior And Security\n\nTreat this section as the source of truth for CI and publish security guardrails. Keep detailed CI guidance here instead of duplicating it in `AGENTS.md` unless a rule is critical enough to be visible before opening specialized docs.\n\n- The repo uses a pnpm workspace rooted at `pnpm-workspace.yaml`; the root manifest is a private orchestrator (`tailwind-merge-monorepo`), the library is the `packages/tailwind-merge` workspace package, and `.github/actions/metrics-report` is a workspace package so action dependencies share the root lockfile.\n- CI steps that target the library use `pnpm --filter tailwind-merge <script>`; build artifacts live at `packages/tailwind-merge/dist`, and `npm publish`/version-stamping steps run with `working-directory: packages/tailwind-merge` because npm operates on the manifest in its working directory.\n- The metrics-report action measures the PR base branch by checking it out in place, so `.github/actions/metrics-report/src/utils/path.mjs` resolves the library root per call, falling back to the repo root for pre-monorepo base branches where the library manifest still sat there. `pnpm --filter tailwind-merge build` works on both layouts because the pre-monorepo root package carried the same name.\n- pnpm 11 requires Node 22 or newer and no longer reads pnpm-specific settings from `.npmrc` or the `pnpm` field in `package.json`; keep repository settings in `pnpm-workspace.yaml`.\n- CI enables Corepack so the root `packageManager` pin selects the pnpm version before every frozen install. For pnpm major updates, run both `pnpm install` and `pnpm install --frozen-lockfile` with the new pinned version, and commit any lockfile migration produced by the non-frozen install.\n- `pnpm-workspace.yaml` sets `minimumReleaseAge: 4320` (minutes, i.e. three days) with `minimumReleaseAgeStrict: true`, so dependency versions must be at least three days old before pnpm installs them. It also allows the `esbuild` dependency build script because the metrics action imports esbuild directly.\n- `.github/renovate.json` sets `minimumReleaseAge: \"4 days\"` (all datasources) so Renovate never proposes versions that pnpm strict mode would reject during lockfile updates; the extra day over pnpm's three days is a deliberate buffer against boundary races. If the pnpm cooldown changes, keep the Renovate value at least as large. Renovate exempts vulnerability alerts from its cooldown, but pnpm strict mode still blocks immature versions at install time; use pnpm's `minimumReleaseAgeExclude` if a security fix must land early.\n- `.github/workflows/test.yml` runs `pnpm lint`, `pnpm test:types`, `pnpm test`, `pnpm --filter tailwind-merge build`, `pnpm --filter tailwind-merge test:exports`, then each plugin's `build` and `test:exports` (`@tailwind-merge/vite`, then `@tailwind-merge/next`) — the plugin steps after the library build because the packed-tarball gates resolve tailwind-merge through its published dist and types. The Next.js package's Vitest project spawns real Next.js processes and builds the library itself when its dist is missing (its global setup), so `pnpm test` stays independent of the build steps' order.\n- `.github/workflows/benchmark.yml` runs `pnpm --filter tailwind-merge bench` with CodSpeed tokenless uploads for this public repository; do not pass static CodSpeed upload tokens to jobs that execute PR benchmark code.\n- `.github/workflows/codeql-analysis.yml` runs CodeQL for both JavaScript/TypeScript and GitHub Actions workflows; the JavaScript job builds the library and both plugins with explicit package filters, since the workspace root has no build script. The Actions analysis uses the `security-extended` query suite to catch workflow-specific security issues. It also runs on PRs that touch `.github/actions/**` or `.github/workflows/**` so CI-control-plane changes are scanned before merge.\n- Every workflow should declare explicit least-privilege `permissions`; read-only build/test jobs use `contents: read`, and write scopes should appear only on the jobs that need them.\n- Pin every `actions/checkout` use to an immutable commit and set `persist-credentials: false` unless the job must push commits or tags through git; checkout receives the job token even in read-only jobs.\n- `actions/checkout` v7+ blocks fork pull request code under `pull_request_target` and PR-triggered `workflow_run`; never set `allow-unsafe-pr-checkout: true`. Keep `label.yml` checkout-free, keep the metrics comment job on the base SHA, and keep release-comment workflow runs restricted to successful `push` events on `main`.\n- Third-party GitHub Actions that receive secrets or write-capable tokens are pinned to full commit SHAs with comments showing the source tag or branch.\n- Artifact handoffs intentionally pair `actions/upload-artifact@v7` with `actions/download-artifact@v8` because the two actions have different latest major versions; do not try to align their version numbers unless upstream releases change.\n- Local JavaScript GitHub Actions use `runs.using: node24`; keep action scripts compatible with the declared runner and avoid runtime-fragile ESM features, such as JSON module import assertion syntax, when a simple filesystem read works across supported Node versions.\n- `.github/workflows/metrics-report.yml` keeps PR code execution in a read-only `generate-report` job and posts comments from a separate `post-comment` job that checks out trusted base-repo code. The metrics report action itself only writes the generated comment body to the artifact path and must not post PR comments directly. If a transition PR introduces or moves the trusted posting script, the workflow should skip the `post-comment` job at the job level until that script exists in the base checkout rather than running PR-provided posting code with write permissions.\n- `.github/workflows/npm-publish.yml`:\n    - publishes `dev` tag builds of tailwind-merge and both plugins on `main` pushes: one build job builds all three (library first, topologically) and runs every `test:exports` gate, one OIDC job stamps the manifests with `scripts/stamp-dev-version.mjs` (dev version plus the plugins' exact pins on the library's same-commit dev version) and publishes the library before the plugins. The plugin core and the configurator have no dev release; they ship inlined in the plugin bundles. A new package joins these jobs only after its npm trusted publisher exists, since a publish step without one fails on every push (bootstrap in the release guide),\n    - publishes production on release events, routing by the namespaced tag prefix (`tailwind-merge@*` and legacy `v*` map to `packages/tailwind-merge`, `@tailwind-merge/vite@*` to `packages/vite`, `@tailwind-merge/next@*` to `packages/next`, anything else fails),\n    - the release build job builds the library alongside the released package (`pnpm --filter tailwind-merge --filter \"$PACKAGE_NAME\" build` — a plugin's test:exports needs the library's published shape; duplicate filters collapse for tailwind-merge releases, and pnpm's topological order builds the library before a plugin) and runs the released package's `test:exports` unconditionally, plus `test:library-release` for plugin releases (registry check that the packed tailwind-merge range's lowest version ships the unstable-entry-point imports; see the Vite and Next.js guides),\n    - publishes with `pnpm publish --access public --provenance --ignore-scripts --no-git-checks` (plus `--tag dev` in the dev flow): pnpm applies `publishConfig` field overrides at pack time (the vite exports swap) and implements npm trusted publishing natively — it fetches the GitHub OIDC id-token, exchanges it per package at the registry, and publishes through npm's own libnpmpublish vendored inside pnpm. `--provenance` must be explicit because pnpm reads it from flags/config, not from `publishConfig.provenance`; `--no-git-checks` because CI checkouts are detached HEADs (and the dev flow's version stamp dirties the tree),\n    - the publish jobs enable corepack (for pnpm) and deliberately set no `registry-url` in setup-node: that input writes an `.npmrc` auth line referencing the unset `NODE_AUTH_TOKEN` env var, and pnpm fails config loading on unresolvable env references in `.npmrc`,\n    - keeps linting, tests, and builds in non-OIDC jobs,\n    - avoids dependency caches in the publish workflow,\n    - grants `id-token: write` only to minimal publish jobs that download the verified `dist` artifact and run `pnpm publish --ignore-scripts`. The release publish job first runs `pnpm install --frozen-lockfile --ignore-scripts --filter \"$PACKAGE_NAME\"` because pnpm rewrites `workspace:` specifiers at publish time by reading the linked package's manifest from `node_modules` — without it, publishing a plugin fails with `ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL`. The filter installs only the released package's dependencies, and `--ignore-scripts` keeps dependency code from running in the job. The dev publish job runs the same filtered install for both plugins because their devDependencies keep the inlined plugin core's and configurator's `workspace:*` specifiers; tailwind-merge alone would need no install, since its specifiers are all `catalog:` and resolve from `pnpm-workspace.yaml`,\n    - pins `actions/download-artifact` to an immutable commit in the OIDC jobs and keeps digest mismatches fatal so corrupted or substituted artifacts cannot reach npm publishing.\n- `.github/workflows/label.yml` uses `pull_request_target` only for labeling metadata; do not add repository checkout or PR-code execution to that workflow. `gh` commands in that workflow must pass `--repo` explicitly because there is intentionally no `.git` checkout for repository inference.\n- `.github/workflows/draft-release.yml` runs the release-drafter autolabeler under `pull_request_target` for the same reason: the plain `pull_request` token is read-only on fork PRs, so labeling fails with \"Resource not accessible by integration\" (seen on PR #689). Like `label.yml`, the `auto_label` job must stay checkout-free and never execute PR code.\n- `.github/workflows/comment-released-prs-and-issues.yml`:\n    - runs local action `.github/actions/release-commenter`,\n    - runs on `release.published`, on manual `workflow_dispatch`, and after successful `npm Publish` workflow completion for `push` events on `main`,\n    - keeps the `workflow_run` checkout on trusted default-branch code and reads the triggering main commit's package metadata through the GitHub API instead of checking out the triggering SHA,\n    - supports manual `workflow_dispatch` with optional `head_tag`, `base_tag`, `dry_run`, and `npm_package_name`.\n\n## Practical guardrails\n\n- Keep `README.md` edits synchronized with `docs/README.md`; source-of-truth is docs.\n- Do not rely on `dist/` as edit target; regenerate instead.\n- Favor targeted tests while iterating, then run full suite before finalizing.\n- Any behavior change should include at least one regression test.\n","isInternal":false,"tokens":4771,"sizeBytes":21591}],"systemPromptSnippet":"<yakaai_skills repo=\"dcastil/tailwind-merge\">\n<!-- File: AGENTS.md (Tokens: ~3105 | Category: root-instruction) -->\n# AGENTS.md\n\nThis repository is a pnpm monorepo hosting `tailwind-merge` — a TypeScript library that merges Tailwind class strings by removing style conflicts while preserving non-conflicting and non-Tailwind classes — and its sibling packages. Packages are independent: each owns its lint, test, and type-check setup and releases on its own schedule.\n\n## Primary Goals\n\n- Keep merge behavior correct for the supported Tailwind versions listed in the library documentation.\n- Keep runtime fast (browser-oriented hot path, lazy init, lightweight cache).\n- Keep API and type contracts stable.\n\n## Repo Map\n\n- The library package: `packages/tailwind-merge/` — source in `src/`, tests in `tests/`, human docs in `docs/`, build/release scripts in `scripts/`, published overview `README.md` (autogenerated from `docs/README.md`).\n- Unreleased workspace packages: `packages/configurator/` generates tailwind-merge configs from Tailwind v4 CSS; `packages/plugin-core/` adds what every bundler plugin needs on top (CSS root discovery, runtime-module generation, the generation session); `packages/vite/` and `packages/next/` wrap both for Vite and Next.js with automatic generation and usage pruning. The configurator and the plugins have a maintained `README.md` and user documentation in `docs/`; the plugin core is internal and inlined into both plugins.\n- Repo root: workspace orchestration only — `pnpm-workspace.yaml` (workspace list + shared tool-version catalog), `eslint.config.base.mjs` (shared lint base every scope spreads), root `eslint.config.mjs` (lints root files and `.github/`), root `vitest.config.mts` (aggregates the packages' Vitest projects plus the local GitHub actions' tests), repo-wide release tooling in `scripts/` (`update-pinned-links.mjs`, `pin-readme-links.mjs`, `stamp-dev-version.mjs`, `check-library-release.mjs`), and the repo-level `README.md`.\n- CI workflows: `.github/workflows/`\n- Local GitHub actions: `.github/actions/`\n\n## Agent Docs Map\n\n- `agents/tailwind-merge-internals.md`: library architecture, file ownership, shared testing/build behavior, and CI/security guardrails.\n- `agents/configurator.md`: configurator goals, strategy, generation/classification architecture, correctness gates, and development follow-ups.\n- `agents/plugin-core.md`: the shared plugin core — discovery, generation, and session invariants, testing, and its inlined packaging.\n- `agents/vite-plugin.md`: Vite integration goals, runtime resolution, watching/failure contracts, packaging, and test coverage.\n- `agents/next-plugin.md`: Next.js integration goals, the loader-rule mechanism and its Turbopack/webpack facts, testing harness, packaging, and test coverage.\n- `agents/configurator-performance.md`: dated measurements, optimization rationale, and reproduction methodology.\n- `agents/tailwind-css-version-update.md`: Tailwind CSS version support workflow.\n- `agents/release-workflow.md`: changelog authoring, GitHub release text, sponsor handling, and release comments on issues/PRs.\n\n## Environment and Commands\n\n- Package manager: `pnpm` with a workspace rooted at `pnpm-workspace.yaml`\n- Local development runtime: Node `22.18.0` or newer on a supported LTS release, satisfying the requirements of pnpm 11 and the Babel 8 build toolchain\n- CI runtime: Node `24.18.1`, pinned via `node-version` in the workflows under `.github/workflows/` (Renovate bumps it there; treat the workflows as the source of truth if this number looks stale)\n- Consumer TypeScript support: TypeScript `3.8` and newer; `pnpm test:exports` verifies the generated declarations with the pinned minimum compiler\n- Dependency supply-chain guardrail: `minimumReleaseAge: 4320` in `pnpm-workspace.yaml`, so newly published package versions must be at least three days old before pnpm installs them. Renovate has a matching cooldown in `.github/renovate.json` (see `agents/tailwind-merge-internals.md` for how the two must stay in sync).\n- Dependency build scripts are denied by default unless explicitly allowed in `pnpm-workspace.yaml`; `esbuild` is currently allowed because `.github/actions/metrics-report` needs it.\n- The root manifest's `devEngines.runtime` must encode the real documented Node floor (currently `>=22.18.0`), not a rounded-down one: pnpm validates dependency `engines` against the range's minimum version, and a floor looser than a dependency's own requirement makes pnpm silently skip optional dependencies during install — this is how rolldown's platform binary went missing under an earlier `>=22`, with only a debug-level log mentioning it.\n\nCore commands (repo root):\n\n- `pnpm install --frozen-lockfile`\n- `pnpm lint` — lints root files, then every package's own lint script (`pnpm --recursive lint`)\n- `pnpm test` — runs every package's Vitest project plus the local GitHub actions' tests through the root projects config, with coverage\n- `pnpm test:types` — every package's own tsc project (`pnpm --recursive test:types`)\n- `pnpm test:watch` — cross-package watch mode\n\nLibrary-specific commands (run with `pnpm --filter tailwind-merge <script>` from anywhere, or plain `pnpm <script>` inside `packages/tailwind-merge/`):\n\n- `build`\n- `test:exports`\n- `bench`\n\nVite-plugin-specific commands (run with `pnpm --filter @tailwind-merge/vite <script>`, or plain `pnpm <script>` inside `packages/vite/`):\n\n- `build` — tsdown, ESM bundles plus declarations for all three subpaths into `dist/`\n- `test:exports` — packs the tarball and verifies the published shape; requires both this package's and the library's `build` to have run first (see [Vite build and packaging](./agents/vite-plugin.md#build-and-packaging) for what it checks)\n\nNext.js-plugin-specific commands (run with `pnpm --filter @tailwind-merge/next <script>`, or plain `pnpm <script>` inside `packages/next/`):\n\n- `build` — tsdown, ESM bundles plus declarations for the plugin entry, the loader, and the runtime into `dist/`; the package's `exports` point at `dist/` even in the workspace, and its test global setup runs this build\n- `test` — spawns real `next dev`/`next build` processes for both bundlers (about two minutes); needs the library built, which the global setup does when `packages/tailwind-merge/dist` is missing\n- `test:exports` — packs the tarball and verifies the published shape including a real `next build` of a consumer app; requires both this package's and the library's `build` to have run first (see [Next.js build and packaging](./agents/next-plugin.md#build-and-packaging))\n\nMonorepo conventions:\n\n- Shared tool versions (eslint, typescript, vitest, @types/node) live in the `catalog:` section of `pnpm-workspace.yaml`; package manifests reference them as `\"catalog:\"`. Single-consumer dependencies stay pinned in their package manifest.\n- Every package lints with its own `eslint.config.mjs` spreading `eslint.config.base.mjs` from the root, and tests with its own `vitest.config.mts`; the root configs only orchestrate.\n- No task runner (turborepo or similar) and no affected-only CI on purpose: at the current package count the full suite is fast, and running everything keeps cross-package effects (a tailwind-merge or plugin-core change must exercise both plugins' tests) trivially correct. Revisit when the package count grows or build times make caching pay off.\n- Releases are per package with `<package-name>@<version>` git tags (for example `tailwind-merge@3.7.0`); see `agents/release-workflow.md`.\n\n## Working Rules for Changes\n\n1. Treat `packages/tailwind-merge/src/lib/default-config.ts` as the behavioral source of truth for default class groups and conflicts.\n2. If behavior changes, update tests in `packages/tailwind-merge/tests/` in the same change.\n3. If public behavior/docs change, update docs in `packages/tailwind-merge/docs/` (not directly in a `README.md`).\n4. The library's `README.md` and the generated section of the repo-level `README.md` are version-generated from the library's `docs/README.md`; do not regenerate them during normal development changes. The configurator, Vite, and Next.js READMEs are maintained landing pages, not generated files; edit them alongside their own `docs/` pages.\n5. Avoid manual edits to `dist/` and `coverage/` directories; they are generated artifacts.\n6. Validate packaging paths with `pnpm --filter tailwind-merge build && pnpm --filter tailwind-merge test:exports` when touching exports/build tooling.\n\n## Documentation Sync Policy\n\nTreat documentation updates as part of the same change, not as follow-up work.\n\nSelf-improvement is required while working in this repo: whenever you discover useful information about architecture, workflows, commands, constraints, debugging findings, or recurring pitfalls, add it to the appropriate file in `agents/` or to `AGENTS.md` in the same change. When existing agent guidance becomes stale, misleading, or irrelevant, remove or revise it immediately.\n\nAvoid repeating the same guidance in both `AGENTS.md` and specialized `agents/*` docs unless the rule is critically important. Prefer keeping `AGENTS.md` as the high-level entry point and putting detailed workflow, architecture, release, or CI notes in the relevant specialized agent document.\n\nCross-package user-docs convention: every package's docs recommend setting `twMerge` up in one project-owned file (shown as `tw-merge.ts`) and importing it from there, even without configuration, so that configuring, wrapping, or replacing `twMerge` later is a one-file change. The canonical explanation lives in the library's `docs/configuration.md` (\"Import `twMerge` from one place\"); the plugin and configurator getting-started pages carry their own short version. Keep new setup instructions and usage examples consistent with it.\n\nIt is acceptable to periodically restructure agent documentation when files become too large, when topics are hard to find, or when the current organization no longer matches how the repo is maintained. Split files, rename sections, or move information between `AGENTS.md` and `agents/*` as needed, while preserving useful guidance and removing stale duplication.\n\nRequired when relevant:\n\n1. Update `packages/tailwind-merge/docs/*.md` (or the owning package's docs) for any user-visible behavior, API, version support, or limitation changes.\n2. Update `AGENTS.md` whenever repository-wide agent workflow, repo conventions, required commands, or high-level guardrails change.\n3. Update the relevant file in `agents/` whenever specialized architecture, workflow, CI, release, or testing guidance changes.\n4. Do not regenerate the READMEs during normal development. The library `README.md` and the marked section of the repo-level `README.md` are intentionally generated as part of the version/release flow so visitors are routed to docs for the latest release.\n5. Never reference repo files through `blob/main`/`raw/main`/`tree/main` URLs — they break whenever a file moves. Within the repo use relative links; where a link must be absolute (published docs, JSDoc, release notes), pin it to the latest release tag whose tree contains the file (e.g. `blob/v3.6.0/docs/...`, `blob/tailwind-merge@3.7.0/packages/tailwind-merge/docs/...`). Links pinned to a package's latest tag are re-pinned automatically at that package's next release by `scripts/update-pinned-links.mjs` (run in each publishable package's `version` lifecycle); links pinned to older tags are treated as deliberately historical and never touched. Two escape hatches for historical links that sit at the latest tag: changelog directories (`docs/changelog/`) are never scanned, and a link carrying the `twm-historical` query parameter (e.g. `blob/v3.6.0/docs/foo.md?twm-historical`, before any `#fragment`) stays untouched — GitHub ignores the parameter when serving the page.\n\nDefinition of done for every PR/change:\n\n1. Code and tests are updated.\n2. Relevant docs are updated in the same change set.\n3. `AGENTS.md` and `agents/*` guidance is reviewed, with useful new context added and stale guidance removed whenever the change affects how agents should work in this repo.\n\n## Quick Validation Matrix\n\nAll test paths below are relative to `packages/tailwind-merge/`; run single files with `pnpm --filter tailwind-merge test:watch tests/<file>`.\n\n- Parser or modifier semantics: run `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts`.\n- Class groups/conflicts/default config: run `tests/default-config.test.ts`, `tests/class-group-conflicts.test.ts`, `tests/tailwind-css-versions.test.ts`.\n- Public API/types: run `tests/public-api.test.ts`, `tests/type-generics.test.ts`, and `pnpm --filter tailwind-merge test:types`.\n- Release/package surface: run `pnpm --filter tailwind-merge build` and `pnpm --filter tailwind-merge test:exports`.\n- Configurator, plugin core, or a plugin: run `pnpm --filter @tailwind-merge/configurator test` / `pnpm --filter @tailwind-merge/plugin-core test` / `pnpm --filter @tailwind-merge/vite test` / `pnpm --filter @tailwind-merge/next test` and the matching `test:types` script. A core or configurator change must run both plugin suites.\n- Plugin build/packaging surface: run `pnpm --filter tailwind-merge build`, then the plugin's `build` and `test:exports` (`pnpm --filter @tailwind-merge/vite …` or `pnpm --filter @tailwind-merge/next …`).\n- Repo-wide before finalizing: `pnpm lint`, `pnpm test:types`, `pnpm test` from the root.\n\n<!-- File: agents/release-workflow.md (Tokens: ~5081 | Category: subagent-persona) -->\n# Release workflow for agents\n\nUse this guide when preparing release changelog entries and GitHub release text.\n\n## Release model\n\nReleases are per package. Each release belongs to exactly one workspace package and its git tag is namespaced with the package name:\n\n- `tailwind-merge@3.7.0`\n- `@tailwind-merge/vite@0.1.0`\n- `@tailwind-merge/next@0.1.0`\n\nTags without a package prefix (`v3.6.0` and earlier) are the pre-monorepo history and belong to `tailwind-merge`. The publish workflow's tag routing and `scripts/update-pinned-links.mjs` treat them that way via a fallback; release-drafter does not (see below), so the first namespaced tailwind-merge release needs its draft assembled by hand.\n\nThe tag and version-commit format is set by each package's `release` script, which wraps `pnpm version` with `--tag-version-prefix=<package-name>@` and `--message=<package-name>@%s`. pnpm 11 implements `pnpm version` natively instead of proxying to `npm version`, so it never reads npm's `tag-version-prefix`/`message` from `.npmrc`; running bare `pnpm version` yields a plain `v<version>` tag with a bare-version commit message, which the publish workflow's tag routing would treat as a legacy tailwind-merge tag. Both settings are pnpm rc options, but pnpm config is repo-wide (`pnpm-workspace.yaml`) while the prefix differs per package, hence the per-package script.\n\n## Scope\n\n- Applies to release preparation tasks like `tailwind-merge@3.4.1`.\n- Covers:\n    - Draft release ingestion from GitHub.\n    - Changelog updates in `packages/tailwind-merge/docs/changelog/*-changelog.md` (each plugin gets its own changelog location with its first release).\n    - Sponsor section generation and ordering.\n    - GitHub Releases UI text formatting.\n    - Release comments on PRs/issues via `.github/actions/release-commenter`.\n\n## Release drafting\n\n`.github/workflows/draft-release.yml` maintains one draft per package via release-drafter with per-package configs (`config-name`): `.github/release-drafter-tailwind-merge.yml`, `.github/release-drafter-vite.yml`, and `.github/release-drafter-next.yml`. Each config scopes itself with `tag-prefix` (previous-release lookup and version resolution consider only that package's tags) and `include-paths` (only PRs touching the package or its bundled source appear in its draft). Consequences to know:\n\n- A PR touching several packages appears in each of their drafts — correct, since each release ships it.\n- Both plugin scopes include `packages/plugin-core` and `packages/configurator` as well as their own directory: fixes in the shared code ship in both plugins' bundles and belong in both changelogs. The library remains a separate dependency with its own release scope.\n- A PR touching none of those directories (repo infrastructure, CI, root docs) appears in no draft; mention it by hand in a changelog if it matters to users.\n- First namespaced release per package: `tag-prefix` matches no existing release, and the pinned release-drafter (v7) then drafts from nothing rather than from full history — its previous-release lookup keeps only releases whose tag starts with the prefix, and without one it collects no pull requests at all. The draft therefore proposes version `0.1.0` and its body is the `no-changes-template` (\"No changes\") plus a warning banner, while the pre-existing unprefixed draft (`v3.6.1` for tailwind-merge) keeps the real, categorized pull request list and is never updated or removed by the drafter again. When publishing: copy the categorized list from that orphaned draft into the new one, then delete the orphaned draft; set the tag name and title by hand (for example `tailwind-merge@3.7.0` following `v3.6.0`); and write the Full Changelog compare line manually (`v3.6.0...tailwind-merge@3.7.0`). For the plugins the proposed `0.1.0` happens to be right, but their pull request lists are equally empty and must be written from the PRs touching the package, `packages/plugin-core`, and `packages/configurator`.\n- Tag-pinned file links repo-wide (the AGENTS.md link policy) are maintained automatically: each publishable package's `version` lifecycle runs `scripts/update-pinned-links.mjs`, which rewrites every link pinned to that package's newest existing tag to the tag being released (logging each rewrite for release-time review), verifies each target path against the working tree (failing the version step loudly on unresolvable paths, touching nothing), remaps pre-monorepo root paths into the package directory, and leaves links pinned to older tags alone as deliberately historical. Historical links that sit at the newest tag anyway are covered by two escape hatches: changelog directories are never scanned (their entries — including the one for the latest release — always describe a specific past release), and a link carrying the `twm-historical` query parameter stays untouched and exempt from path validation. First release of a package is a no-op for it. Concretely: the tailwind-merge 3.7.0 release re-pinned the vite docs' `v3.6.0` links to `tailwind-merge@3.7.0` with `packages/tailwind-merge/` paths on its own. Relative README links are the complementary case, handled by `scripts/pin-readme-links.mjs`: the plugin's `version` lifecycle runs it after the re-pin to turn the README's `./docs/...` and `../../agents/...` links into absolute links pinned to the tag being released (staging the README for the version commit), because npm cannot resolve relative links, while the repo keeps them relative between releases; the dev-release stamp imports the same helper and pins to the commit instead. Links it has pinned to a tag are absolute from then on and fall under `update-pinned-links.mjs` at later releases. The library needs neither: its README is generated from `docs/README.md` with absolute links by `scripts/update-readme.mjs`.\n- The autolabeler section lives only in the tailwind-merge config; labels are repo-wide.\n\n## Publishing\n\n`.github/workflows/npm-publish.yml` routes `release.published` events by tag prefix: `tailwind-merge@*` and legacy `v*` build and publish `packages/tailwind-merge`, `@tailwind-merge/vite@*` builds and publishes `packages/vite`, `@tailwind-merge/next@*` builds and publishes `packages/next`, and unknown prefixes fail the run. The build job runs repo-wide lint and tests, builds the library alongside the released package (a plugin's `test:exports` verifies its packed tarball against the library's published shape), runs the released package's `test:exports`, and for plugin releases `test:library-release`; publishing happens from the package directory in an isolated OIDC job via `pnpm publish` — after a scripts-disabled install of the released package's dependencies, which pnpm needs to rewrite `workspace:` specifiers (the plugins' tailwind-merge dependency), and with pnpm applying `publishConfig` overrides at pack time (the vite package's dist-exports swap) and handling npm trusted publishing natively, `--provenance` passed explicitly because pnpm does not read `publishConfig.provenance` (mechanics in `agents/tailwind-merge-internals.md`). Dev releases on `main` pushes cover the library and both plugins (see [Dev releases](#dev-releases)).\n\n### First Vite release\n\nRelease the next tailwind-merge update before the Vite plugin. Dev builds of the plugin are a separate track from its first stable release: see [Dev releases](#dev-releases) below for the stamping script, the one-time bootstrap publish, and the trusted-publisher setup that automatic dev publishing depends on.\n\nThe selected initial plugin release is `0.1.0`, with no prerelease suffix or alternate dist-tag. It is the only new package intended for initial publication: the configurator is inlined and keeps an unstable direct API until a separately planned release. This is a sequencing decision, not a promise that the configurator stays unpublished forever.\n\nBefore publishing:\n\n1. Done with `tailwind-merge@3.7.0`, which ships the generator's required `themeKey`, classification/parser exports, and matching configuration semantics; the plugin's `workspace:^` dependency now packs as `tailwind-merge: ^3.7.0` (the caret range keeps later library patches installable without a plugin release). The release build job guards this for every plugin release: `pnpm --filter @tailwind-merge/vite test:library-release` (the shared `scripts/check-library-release.mjs`, given the source trees to scan with `--sources`) resolves the dependency range's lowest version, downloads that release from npm, and fails unless it ships every value and type the bundled plugin core and configurator import from the unstable entry point — a packed check against the workspace library cannot establish that. When it fails, release the library first and bump the plugin's dependency to it; `--library-version` checks an arbitrary release by hand.\n2. The trusted publisher for `@tailwind-merge/vite` is set up as part of the dev-release bootstrap (see [Dev releases](#dev-releases)); the stable release reuses it. The library's configuration does not cover a second package.\n3. Set the plugin's release version when preparing that release. The plugin manifest carries no `private` flag since dev builds made it a published package; the configurator keeps `private: true` because it is never published. Add the plugin's initial changelog/release text and update the pre-release notices in its README/getting-started/versioning docs. The README's relative links need no manual pinning: the plugin's `version` lifecycle rewrites them to the release tag.\n4. Run the library and plugin builds, packed-package gates, and repo-wide checks. The [Vite packaging guide](./vite-plugin.md#build-and-packaging) explains the pnpm export rewrite and which consumer behavior the gate verifies.\n\nField trials use dev builds (`@tailwind-merge/vite@dev`), which pin the same-commit library dev build. Trials against local checkouts must likewise resolve both packages to matching builds; installing only a plugin tarball while letting its library dependency resolve to a registry release does not meet that requirement.\n\n### First Next.js release\n\nThe Next.js plugin follows the Vite plugin's model exactly: initial release `0.1.0`, the plugin core and the configurator inlined, the library as a regular dependency packed from `workspace:^`, `test:library-release` in the release build job (its `--sources` list covers the configurator, the plugin core, and the plugin), and a trusted publisher set up through the dev-release bootstrap below. The Next.js-specific facts a release must respect are in the [Next.js guide](./next-plugin.md#release-boundaries). Release the library update the plugin's imports need before the plugin, as for the Vite plugin.\n\n### Dev releases\n\n`scripts/stamp-dev-version.mjs` prepares a package for a dev publish; run it from the package directory. It stamps `<version>-dev.<sha>` (`GITHUB_SHA` in CI, `HEAD` locally), pins `workspace:` runtime dependencies to the exact same-SHA dev version of that workspace package (the plugins' bundled configurator imports the library's unstable entry point, so a registry release could lack the internals the same commit changed), and rewrites relative README links to commit-pinned absolute links so the npm page resolves them (through the shared `scripts/pin-readme-links.mjs`, see Release drafting above). `--check-registry` verifies that every pinned dependency version is already on npm; a local publish needs that, CI does not because it publishes the library first in the same job. The script dirties the working tree by design; restore the manifest and README from git after publishing locally.\n\nnpm cannot attach a trusted publisher to a package name that has never been published, so a plugin's first publish is a one-time local dev publish with a token — the only step of the whole flow that uses one — after which the trusted publisher covers all later dev and stable publishes. A new plugin stays out of the dev jobs until it is bootstrapped, because a publish step without a trusted publisher fails on every `main` push; both current plugins went through this. The bootstrap, on a clean checkout of the pushed `main` commit whose library dev release has finished publishing — shown for `@tailwind-merge/next`, the Vite plugin's was the same with `packages/vite`:\n\n```bash\npnpm install --frozen-lockfile\npnpm --filter tailwind-merge --filter @tailwind-merge/next build\npnpm --filter @tailwind-merge/next test:exports\ncd packages/next\nnode ../../scripts/stamp-dev-version.mjs --check-registry\npnpm publish --tag dev --access public --no-git-checks --ignore-scripts\ngit checkout -- package.json README.md\n```\n\nPublish without `--provenance`: provenance needs a CI OIDC token and fails locally. Then configure the trusted publisher (repository `dcastil/tailwind-merge`, workflow `npm-publish.yml`, no environment) on the package's npm access page or with `npx npm@latest trust github @tailwind-merge/next --file npm-publish.yml --repo dcastil/tailwind-merge --allow-publish` (`npm trust` needs npm 11.15 or newer). Then add the package to both dev jobs the way the plugins are wired: the build and `test:exports` filters, an artifact upload and download for its `dist`, the filtered install, a stamp line, and a publish step after the library's. With the trusted publishers in place, the publish workflow's dev jobs publish all three packages on every `main` push: the build job builds them and runs every `test:exports` gate, the OIDC job stamps all manifests with the script and publishes the library before the plugins, so the plugins' exact pins always resolve. A bootstrap publish sets `latest` as well as `dev` on the plugin, which npm does for a package's first version regardless of `--tag`; the first stable release moves `latest` to a real version.\n\nThe release commenter's dev pass stays library-only (it reads the library manifest for the head tag, and the action fails when npm has no prior dev version of a package); plugin dev builds do not comment on issues until that pass is extended.\n\n## Release commenter behavior\n\nThe workflow `.github/workflows/comment-released-prs-and-issues.yml` uses the local action `.github/actions/release-commenter`.\n\n- It runs for:\n    - published GitHub releases,\n    - manual workflow dispatch,\n    - completed successful `npm Publish` runs triggered by `push` on `main` (dev-release comment pass, tailwind-merge only even though the same run also publishes the vite package's dev build; the synthetic head tag is `tailwind-merge@<version>-dev.<sha>`).\n- Base-tag selection is scoped to the released tag's package. Legacy un-prefixed tags belong to the `fallback-package-name` input (default `tailwind-merge`).\n- Target collection separately checks each commit's changed files, then the changed files of its associated PRs, before reading their closing references or connected-issue timelines. An unrelated package's commit or PR must not announce its issues as released. PR scope results are cached within the run. Each plugin's scope includes the bundled plugin core and configurator; the library scope includes its package directory plus historical root `src/`, `docs/`, and `tests/` paths for comparisons spanning the monorepo move. Root infrastructure remains outside automatic notification scope.\n- Changed-file checks paginate both REST endpoints and include a rename's previous path, so changes moved out of a package still count. Missing file data or reaching GitHub's file-list cap without establishing scope fails before posting. Keep `releasePackagePaths()` in the action aligned with the release-drafter configs when adding packages or changing bundle ownership. Mocked tests cover mixed ranges, linked issues, pagination, renames, and dry runs without contacting GitHub.\n- Automatic base-tag selection is semver-aware within the package:\n    - Stable release tags compare to the package's previous stable tag (legacy `v*` tags included for tailwind-merge).\n    - Prerelease tags with a SHA suffix (for example `tailwind-merge@3.4.1-dev.<sha>`) resolve base from npm-published versions of the package — the npm package name is derived from the tag prefix — with the same prerelease prefix, picking the nearest ancestor commit.\n    - For SHA-suffixed prereleases, if no prior dev release exists for the same core version, resolution falls back to all dev releases from the highest lower core version (same prerelease prefix) and picks the nearest ancestor commit.\n    - SHA-suffixed prerelease comments link to the npm published version page instead of GitHub release tags.\n    - Other prerelease tags compare to the previous semver tag of the same package (including prereleases).\n- A `0.1.0` release with no same-package history skips commenting gracefully (green run, logged): `0.1.0` is always a package's first version here, so a missing base is the expected state. Trigger manually with an explicit `base_tag` if comments are wanted for a first release. Any other missing-base case still fails loudly, because above `0.1.0` it means release tooling is broken.\n- The action fails before posting if any target issue/PR already has a previous stable release-comment for the same package. Comments from other packages' releases never block — a PR touching two packages legitimately receives one comment per package release. These arrive from separate workflow runs at each package's own release time, and staying separate is deliberate: each comment notifies subscribers about that release, while editing an earlier comment into a combined one would notify nobody.\n- For prereleases, targets that already have a release-comment for the same package are skipped so repeated `-dev.*` ranges do not post duplicate dev comments. Later stable releases can still comment after a prerelease.\n- Posted comment URLs are logged and added to the workflow run summary so all comments from one run can be inspected together.\n- Manual trigger supports optional overrides:\n    - `head_tag`\n    - `base_tag`\n    - `dry_run`\n    - `npm_package_name`\n\nManual dry run example:\n\n```bash\ngh workflow run comment-released-prs-and-issues.yml \\\n  -f head_tag=tailwind-merge@3.4.1 \\\n  -f dry_run=true\n```\n\n## Inputs to collect first\n\n1. Release tag and base compare range (for example `tailwind-merge@3.4.0...tailwind-merge@3.4.1`).\n2. Draft release body from GitHub.\n3. Current active GitHub sponsors.\n4. Sponsor payout input from thanks.dev for the relevant time window.\n\n`thanks.dev` input must come from the user. Do not infer or invent this data.\n\n## Commands\n\nBump a package's version with `pnpm release <version|patch|minor|major>` run inside the package directory (for the library: `packages/tailwind-merge/`); see Release model above for why it is not bare `pnpm version`. pnpm requires a clean working tree, so commit the changelog first. pnpm runs the existing `preversion`, `version`, and `postversion` lifecycle scripts — each stages the files it rewrites itself, because pnpm only stages the manifest — and creates the version commit and an annotated namespaced tag. pnpm also normalizes the manifest when writing the new version (an empty `dependencies` object gets dropped), so keep the manifest free of such fields. To create the commit and tag locally without pushing, run with the `DANYS_MACHINE` variable unset (`env -u DANYS_MACHINE pnpm release minor`): the `preversion` pull and the `postversion` push are gated on it. The `version` step first re-pins tag-pinned links repo-wide via the shared `scripts/update-pinned-links.mjs` (see Release drafting notes above); for the library it then regenerates both the package README and the generated section of the repo-level README via its own `scripts/update-readme.mjs`, with links pinned to the new release tag.\n\nFetch draft release:\n\n```bash\ngh release view <tag> --json tagName,name,isDraft,body,url,createdAt,targetCommitish\n```\n\nFetch active GitHub sponsors in start-date order:\n\n```bash\ngh api graphql -f query='query($login:String!){ user(login:$login){ sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false, orderBy:{field:CREATED_AT, direction:ASC}){ nodes{ sponsorEntity{ ... on User{ login } ... on Organization{ login } } } } } }' -F login=dcastil\n```\n\nCheck whether private active sponsors exist:\n\n```bash\ngh api graphql -f query='query($login:String!){ user(login:$login){ public: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false){ totalCount } all: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:true){ totalCount } } }' -F login=dcastil\n```\n\n## Changelog authoring rules\n\n1. Update the matching major-version changelog file:\n    - v3 releases: `packages/tailwind-merge/docs/changelog/v3-changelog.md`\n    - v2 releases: `packages/tailwind-merge/docs/changelog/v2-changelog.md`\n2. Add new version section at the top:\n    - `## vX.Y.Z`\n3. Keep category headings from draft release:\n    - `### Bug Fixes`, `### New Features`, `### Documentation`, `### Other`, etc.\n4. Keep full compare link in docs style, using the real tag names:\n    - `**Full Changelog**: [\\`tailwind-merge@A.B.C...tailwind-merge@X.Y.Z\\`](https://github.com/dcastil/tailwind-merge/compare/tailwind-merge@A.B.C...tailwind-merge@X.Y.Z)` (the base is `vA.B.C` when it predates the monorepo)\n\n## Sponsor rules\n\n1. Preserve ordering convention from the most recent changelog sponsor sentence unless user explicitly asks otherwise.\n2. Merge sponsor sources:\n    - GitHub Sponsors (active sponsors, ordered by start date).\n    - User-provided thanks.dev payouts for the release window.\n3. Include thanks.dev sponsors only above the user-defined threshold.\n    - Current default: include only amounts greater or equal to 1 USD.\n4. Do not mention sub-threshold sponsors individually unless requested.\n5. Keep summary phrases aligned with user preference:\n    - Examples: `a private sponsor`, `and more via @thnxdev`.\n\n## GitHub release body formatting\n\nWhen producing text for GitHub Releases UI, transform docs formatting:\n\n1. Keep headings and bullet content.\n2. Convert markdown profile links to plain handles:\n    - `[@name](https://github.com/name)` -> `@name`\n3. Convert markdown PR links to plain URLs.\n4. Use plain compare URL:\n    - `**Full Changelog**: https://github.com/dcastil/tailwind-merge/compare/tailwind-merge@A.B.C...tailwind-merge@X.Y.Z`\n\n## Final output contract\n\n1. Provide the release body in one copy-paste-ready markdown block.\n2. If sponsor payout input is missing, stop and ask the user for thanks.dev data before finalizing sponsor lines.\n\n<!-- File: agents/tailwind-css-version-update.md (Tokens: ~1906 | Category: subagent-persona) -->\n# Tailwind CSS version update workflow\n\nUse this guide when updating tailwind-merge for a new Tailwind CSS version.\n\nAll `src/`, `tests/`, and `docs/` paths in this file are relative to the library package `packages/tailwind-merge/`; run the quoted commands from that directory (or through `pnpm --filter tailwind-merge`).\n\nThe main risk in this work is false confidence: Tailwind release notes, deployed docs, source code, and website docs can land at different times. Treat every feature as something to verify from official sources before changing `src/lib/default-config.ts` or parser behavior.\n\n## Official sources\n\n- Tailwind CSS releases: https://github.com/tailwindlabs/tailwindcss/releases\n- Tailwind CSS source and PRs: https://github.com/tailwindlabs/tailwindcss\n- Tailwind CSS release tag pattern: `https://github.com/tailwindlabs/tailwindcss/releases/tag/vX.Y.Z`\n- Tailwind CSS deployed docs: https://tailwindcss.com/docs\n- Tailwind CSS website source and docs PRs: https://github.com/tailwindlabs/tailwindcss.com\n- Prior tailwind-merge support PRs for comparison, for example v4.2 support: https://github.com/dcastil/tailwind-merge/pull/651\n\nWhen docs for a just-released Tailwind version are not deployed yet, use the `tailwindcss.com` repository and its PRs to find future docs pages, sidebar order, and link targets.\n\n## Investigation checklist\n\n1. Read the Tailwind CSS release notes for the target version.\n2. Open every linked Tailwind CSS PR that mentions utilities, variants, parser syntax, arbitrary values, or behavior changes.\n3. Inspect Tailwind CSS source and tests when release notes or PR descriptions are ambiguous. Do not infer support for arbitrary values or variables from naming patterns alone.\n4. Check the `tailwindcss.com` repository for docs PRs when deployed docs lag behind the release.\n5. Compare against the previous tailwind-merge Tailwind-support PR to reuse the same structure and testing style.\n6. Make a feature list before coding. Split work into simple utility additions, ordering/docs updates, conflict changes, and parser/config API changes.\n7. Record no-op decisions in the PR or issue summary, especially when a Tailwind PR looked relevant but does not require a tailwind-merge change.\n\n## Triage questions for each Tailwind change\n\n- Does it emit new class names? If yes, it likely needs `classGroups` entries.\n- Does it change which utilities conflict? If yes, update `conflictingClassGroups` or `conflictingClassGroupModifiers`.\n- Does it add a new arbitrary value or arbitrary variable form? Verify the exact accepted syntax in Tailwind source before adding validators.\n- Does it add a slash form that is part of the base class rather than a postfix modifier? Prefer config-driven lookup such as `postfixLookupClassGroups` before changing parser logic.\n- Does it change modifier, variant, prefix, important, or arbitrary-variant syntax? Parser changes are higher risk and need focused parser/modifier tests.\n- Does it only change generated CSS for an existing class without changing class syntax or conflicts? It may not require a tailwind-merge change.\n- Does it remove or rename classes? Check whether keeping old classes would create incorrect merges for the supported Tailwind range.\n\n## Default config ordering and links\n\nKeep `src/lib/default-config.ts` ordered according to the Tailwind CSS docs sidebar, not alphabetically and not by implementation convenience.\n\nFor new utility groups:\n\n- Use `tailwindcss.com` or the website repository to find the final docs URL for each `@see` comment.\n- If the public docs are not deployed, inspect docs PRs in `tailwindlabs/tailwindcss.com`.\n- Replace temporary `TBD` links before finishing the change.\n- Keep related utility groups in sidebar order even when they share a prefix.\n\nThere is an important exception to sidebar order: class-group order affects validator precedence for groups that share the same prefix path. The class map tries exact child paths first, then runs validators in the order they were registered. If a broad validator appears before a more specific validator for the same prefix, the broad group can claim the class before the specific group sees it.\n\nWhen groups with the same prefix use validators:\n\n- Put the most specific validators first.\n- Put broad or catch-all validators last for that prefix.\n- Treat color validators as especially sensitive because they often accept arbitrary values or otherwise broad value sets.\n- Prefer correctness over strict sidebar order inside that shared-prefix cluster.\n\nFor example, if a `text-*` color group uses a broad color validator, keep it after more specific `text-*` groups such as font-size-related groups so ambiguous classes resolve to the intended class group.\n\nExamples learned during the Tailwind CSS v4.3 update:\n\n- `tab-size` belongs after `text-indent` and before `vertical-align`.\n- `zoom` belongs after `translate`.\n- Scrollbar groups should follow the sidebar order for scrollbar color, scrollbar gutter, and scrollbar width.\n- PRs that look relevant to existing utilities, such as changes around `start` and `end`, still need source review before changing tailwind-merge.\n\n## Implementation guidance\n\nUse `src/lib/default-config.ts` as the behavioral source of truth for utilities and conflicts.\n\nPrefer config changes over parser changes. Parser and merge hot paths run frequently in UI renders, so keep extra work small and opt-in:\n\n- Normalize config arrays into lookup objects in `createConfigUtils` when they need fast querying.\n- Avoid scanning or balancing brackets in validators when a cheap prefix or character check can reject most inputs.\n- Put specialized syntax checks in `src/lib/validators.ts` when they are reusable or need direct tests.\n- Add comments only for non-obvious behavior, especially when a config entry compensates for parser ambiguity.\n\nFor slash syntax, remember that tailwind-merge initially treats the part after `/` as a possible postfix modifier. If the full class can belong to a different group, use a generic config mechanism rather than a syntax-specific parser branch.\n\nThe Tailwind CSS v4.3 named container query support is the reference pattern:\n\n- `@container-size` is a container type class.\n- `@container-size/sidebar` combines container type and container name.\n- The parser already exposes a possible postfix modifier, so no bracket-counting parser change is needed.\n- `container-named` creates a conflict with `container-type`, but not the reverse, because a later type-only class should preserve the earlier container name.\n- `postfixLookupClassGroups: ['container-type']` tells merge logic to try resolving the full slash class only after the pre-slash class resolves to `container-type`.\n\n## Testing checklist\n\nAdd or update tests based on the kind of change:\n\n- Tailwind version compatibility: `tests/tailwind-css-versions.test.ts`\n- Default config class map coverage: `tests/class-map.test.ts`\n- Class group conflicts: `tests/class-group-conflicts.test.ts` and `tests/conflicts-across-class-groups.test.ts`\n- Parser or modifier semantics: `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts`\n- Validators: `tests/validators.test.ts`\n- Public exports and config API: `tests/public-api.test.ts`, `tests/type-generics.test.ts`, `tests/merge-configs.test.ts`\n- Docs examples: `tests/docs-examples.test.ts`\n\nFor new utility support, include tests for:\n\n- Last class wins within the new class group.\n- Cross-group conflicts in both orders when conflicts are asymmetric.\n- Arbitrary values and arbitrary variables only when Tailwind source confirms support.\n- Important modifier and variant combinations when parser or conflict behavior is involved.\n- Unknown or invalid classes remaining untouched.\n\nFor TypeScript config changes, remember that `pnpm tsc --noEmit` only checks `src`. Use the test tsconfig as an additional check when touching public types:\n\n```sh\npnpm tsc --noEmit -p tests/tsconfig.json --moduleResolution bundler\n```\n\nThe plain `tests/tsconfig.json` currently inherits `moduleResolution: Node`, which can surface Vitest package export resolution errors unrelated to tailwind-merge types.\n\n## Documentation checklist\n\nUpdate docs in the same change when behavior or public API changes:\n\n- `docs/configuration.md` for config behavior.\n- `docs/api-reference.md` for exported validators, config fields, or public types.\n- `docs/changelog/v3-changelog.md` when preparing a release note.\n- `AGENTS.md` and `agents/*` when the update process or repo guidance changes.\n\nDo not regenerate `README.md` during normal development; it is generated from `docs/README.md` during the release flow.\n\n## Validation checklist\n\nRun targeted tests while iterating, then run the full validation set before finalizing:\n\n```sh\npnpm lint\npnpm tsc --noEmit\npnpm test\ngit diff --check\n```\n\nRun `pnpm build && pnpm test:exports` when touching package exports, build tooling, or generated declaration behavior.\n\n<!-- File: agents/tailwind-merge-internals.md (Tokens: ~4771 | Category: subagent-persona) -->\n# tailwind-merge internals guide\n\nThis file is a practical map for agent-driven changes in this repo.\n\nSince the 2026-08-10 monorepo migration the library lives in `packages/tailwind-merge/`; all `src/`, `tests/`, `docs/`, and `scripts/` paths in this file are relative to that package directory unless a path starts with `packages/` or `.github/`.\n\nLinks from the library's `docs/` to repository-wide files must traverse three parents; for example, `../../../.github/CONTRIBUTING.md`. Resolve relative links from the owning document after moving files, not from the repository root.\n\n## How merge execution works\n\n1. `twMerge` in `src/lib/tw-merge.ts` is created via `createTailwindMerge(getDefaultConfig)`.\n2. `createTailwindMerge` in `src/lib/create-tailwind-merge.ts`:\n    - lazily builds config utils on first call,\n    - joins inputs via `twJoin`,\n    - caches final joined-input results,\n    - calls `mergeClassList` on cache miss.\n3. `mergeClassList` in `src/lib/merge-classlist.ts`:\n    - parses classes,\n    - walks from right to left so \"last class wins\",\n    - resolves class group IDs and asymmetric conflicts,\n    - keeps non-Tailwind/external classes untouched.\n4. `createClassGroupUtils` in `src/lib/class-group-utils.ts`:\n    - compiles class definitions into a recursive map + validator list,\n    - maps class names to class group IDs,\n    - assigns arbitrary properties (`[key:value]`) synthetic group IDs prefixed with `ARBITRARY_PROPERTY_PREFIX` (e.g. `arbitrary..key`) so distinct properties conflict independently,\n    - returns conflicting groups, including postfix-modifier conflicts.\n5. `createParseClassName` in `src/lib/parse-class-name.ts`:\n    - parses stacked modifiers with bracket/paren depth tracking,\n    - supports important modifier (`!`) and legacy v3 position,\n    - supports optional prefix and `experimentalParseClassName` override.\n6. `createSortModifiers` in `src/lib/sort-modifiers.ts`:\n    - alphabetically sorts non-sensitive modifiers,\n    - preserves placement around arbitrary/order-sensitive modifiers.\n\nThe unstable tooling entry point also exports `createClassGroupLookup` from `src/lib/class-group-lookup.ts`. It runs one candidate through `mergeClassList` with an observing class-map function and returns every successful lookup, including a slash class's intermediate base. The configurator uses this to preserve lookup dependencies during pruning. Keep this instrumentation outside the normal merge path; it is an adapter for build-time inspection, not a second implementation of modifier or lookup semantics. Both built ESM/CJS export checks exercise it.\n\n## Performance constraints in hot-path code\n\nThe hot paths are `src/lib/merge-classlist.ts` and `src/lib/class-group-utils.ts`; `src/lib/parse-class-name.ts` runs per class on cache misses. When editing these files, preserve the existing V8-oriented patterns:\n\n- Keep call sites monomorphic: functions are deliberately split per input type (see the `create*` factories and the \"monomorphic\" comments in `class-group-utils.ts` and `parse-class-name.ts`); do not merge them back into one polymorphic function.\n- Keep object shapes consistent: always define optional properties explicitly (as `undefined`/`null`) so objects created in different branches share a hidden class; result objects are built through factory functions for this reason.\n- Pre-allocate arrays where the final length is known (see `concatArrays` in `src/lib/utils.ts`) instead of spreading or pushing, and return shared constants like `EMPTY_CONFLICTS` instead of fresh empty arrays.\n- Avoid adding function calls or allocations in hot loops when an inline check works.\n\n## Files to edit by intent\n\n- Add or change Tailwind utility support:\n    - `src/lib/default-config.ts`\n    - Usually paired with tests in `tests/tailwind-css-versions.test.ts` and focused utility tests.\n    - For a full Tailwind CSS version update, follow `agents/tailwind-css-version-update.md`.\n\n- Change conflict behavior:\n    - `src/lib/default-config.ts` (`conflictingClassGroups`, `conflictingClassGroupModifiers`)\n    - `tests/class-group-conflicts.test.ts`\n    - `tests/conflicts-across-class-groups.test.ts`\n\n- Change parser/modifier behavior:\n    - `src/lib/parse-class-name.ts`\n    - `src/lib/sort-modifiers.ts`\n    - `tests/modifiers.test.ts`\n    - `tests/arbitrary-variants.test.ts`\n    - `tests/experimental-parse-class-name.test.ts`\n\n- Change config extension APIs:\n    - `src/lib/extend-tailwind-merge.ts`\n    - `src/lib/create-tailwind-merge.ts`\n    - `src/lib/merge-configs.ts`\n    - `tests/create-tailwind-merge.test.ts`\n    - `tests/extend-tailwind-merge.test.ts`\n    - `tests/merge-configs.test.ts`\n    - `tests/type-generics.test.ts`\n\n- Change validators:\n    - `src/lib/validators.ts`\n    - `tests/validators.test.ts`\n    - Any impacted arbitrary-value tests\n\n## Testing strategy in this repo\n\n- Unit/behavior tests are granular by feature area in `tests/*.test.ts`.\n- Color-function regression tests should verify preservation of length utilities and replacement of other colors in `tests/colors.test.ts`.\n- `tests/tailwind-css-versions.test.ts` is the compatibility anchor for Tailwind feature coverage by version.\n- `tests/docs-examples.test.ts` enforces that documented `twMerge(...) // -> ...` examples stay correct. Its extraction regex supports a limited set of characters (for example, `/` in arguments is excluded); verify new examples are collected and update the assertion count when adding supported examples.\n- `tests/public-api.test.ts` guards runtime exports and broad type usage.\n- `tests/tw-merge.benchmark.ts` is for performance benchmarking (`pnpm bench`), not correctness gating in CI.\n\nRun a single test file or test by name with Vitest's filter syntax: `pnpm --filter tailwind-merge test:watch tests/modifiers.test.ts` or `pnpm --filter tailwind-merge test:watch -t \"test name pattern\"`.\n\nPackage-specific generation, scanning, and Vite test guidance lives in [configurator development](./configurator.md#debugging-and-validation) and [Vite plugin development](./vite-plugin.md#testing-and-remaining-coverage).\n\nVitest is set up as one project per package: each package's `vitest.config.mts` carries its aliases and plugins, and the root `vitest.config.mts` aggregates them (plus an inline project for `.github/actions/**/*.test.mjs`, since the local actions aren't workspace packages) so root `pnpm test` runs everything with coverage. Because collection is scoped to those projects, stray test files elsewhere in the repo — notably full repo copies in `.claude/` worktrees, which the pre-monorepo root config silently collected and ran as duplicates — never join the suite.\n\nThe root coverage configuration measures `packages/*/src/**/*.ts`, including the configurator and Vite plugin. In projects mode, package-local coverage settings do not control the aggregate report. Coverage is diagnostic, with no percentage threshold; use uncovered branches to guide behavioral tests rather than weakening assertions or padding the percentage. The Vitest report does not capture code executed by spawned CLI/build processes, so interpret unmeasured entrypoints alongside their integration tests.\n\nRecommended local sequence for non-trivial changes:\n\n1. `pnpm lint`\n2. `pnpm test:types`\n3. `pnpm test`\n4. `pnpm --filter tailwind-merge build`\n5. `pnpm --filter tailwind-merge test:exports`\n\n## Build and packaging\n\n- Build uses Rollup (`scripts/rollup.config.mjs`) to produce:\n    - ESM and CJS bundles,\n    - ES5 variants,\n    - unified type declarations.\n- The `build` script passes Rollup `--forceExit` because the TypeScript Rollup plugin can leave referenced file-watch handles open after a non-watch build has already written all outputs.\n- Babel 8 performs the final target-specific transforms for all four JavaScript bundles. Its `@babel/preset-env` `bugfixes` behavior is unconditional, and the removed `loose: true` option is preserved through top-level assumptions plus the `transform-typeof-symbol` exclusion in `scripts/rollup.config.mjs`. The config deliberately replaces the migration guide's `arrayLikeIsIterable` with `iterableIsArray` because Babel disallows enabling both and every transformed spread and destructuring operand is an array; this suppresses generic iterable helpers and keeps the generated JavaScript bundles equivalent in size and behavior to the Babel 7 bundles.\n- TypeScript 6 requires the source `rootDir` to be explicit when declaration output is redirected by Rollup, so `tsconfig.json` fixes it to `src`. Module resolution uses `Bundler`, matching the Rollup build and avoiding the deprecated `Node`/`node10` resolver.\n- Export smoke tests:\n    - `scripts/test-built-package-exports.cjs`\n    - `scripts/test-built-package-exports.mjs`\n    - Both scripts execute the default and ES5 bundles so build-tool upgrades cannot break only the legacy entry point unnoticed.\n- `pnpm test:types` type-checks both the source and the test suite with the development compiler. `scripts/typescript-compatibility/consumer.ts` separately compiles the built declaration bundle with the `typescript-compat` compiler alias pinned to TypeScript 3.8.3. `pnpm test:exports` includes the compatibility check because TypeScript 3.8 is the documented minimum consumer version; keep the alias excluded from Renovate updates and treat any increase to this floor as a major-version change.\n- `scripts/update-readme.mjs` (run in the `version` script via `zx`) generates two targets from `docs/README.md`: the package `README.md` shipped to npm, and the section between the `tailwind-merge-docs` markers in the repo-level `README.md`; it fails the version step loudly when the repo README's markers are missing.\n- The `version` script also runs the repo-wide `scripts/update-pinned-links.mjs` (repo root, dependency-free Node; the vite package additionally runs `scripts/pin-readme-links.mjs`, see the release guide), which re-pins every link pinned to the package's newest existing tag onto the tag being released, verifying targets against the working tree and failing loudly — without touching anything — when a linked path no longer resolves. Older-tag links are deliberately historical and stay untouched, changelog directories are excluded from scanning entirely, and the `twm-historical` query parameter on a link shields it individually for the rare historical link elsewhere.\n\nFor the Vite package's tsdown build, workspace/published export split, and packed-consumer gate, read [Vite build and packaging](./vite-plugin.md#build-and-packaging).\n\n## CI Behavior And Security\n\nTreat this section as the source of truth for CI and publish security guardrails. Keep detailed CI guidance here instead of duplicating it in `AGENTS.md` unless a rule is critical enough to be visible before opening specialized docs.\n\n- The repo uses a pnpm workspace rooted at `pnpm-workspace.yaml`; the root manifest is a private orchestrator (`tailwind-merge-monorepo`), the library is the `packages/tailwind-merge` workspace package, and `.github/actions/metrics-report` is a workspace package so action dependencies share the root lockfile.\n- CI steps that target the library use `pnpm --filter tailwind-merge <script>`; build artifacts live at `packages/tailwind-merge/dist`, and `npm publish`/version-stamping steps run with `working-directory: packages/tailwind-merge` because npm operates on the manifest in its working directory.\n- The metrics-report action measures the PR base branch by checking it out in place, so `.github/actions/metrics-report/src/utils/path.mjs` resolves the library root per call, falling back to the repo root for pre-monorepo base branches where the library manifest still sat there. `pnpm --filter tailwind-merge build` works on both layouts because the pre-monorepo root package carried the same name.\n- pnpm 11 requires Node 22 or newer and no longer reads pnpm-specific settings from `.npmrc` or the `pnpm` field in `package.json`; keep repository settings in `pnpm-workspace.yaml`.\n- CI enables Corepack so the root `packageManager` pin selects the pnpm version before every frozen install. For pnpm major updates, run both `pnpm install` and `pnpm install --frozen-lockfile` with the new pinned version, and commit any lockfile migration produced by the non-frozen install.\n- `pnpm-workspace.yaml` sets `minimumReleaseAge: 4320` (minutes, i.e. three days) with `minimumReleaseAgeStrict: true`, so dependency versions must be at least three days old before pnpm installs them. It also allows the `esbuild` dependency build script because the metrics action imports esbuild directly.\n- `.github/renovate.json` sets `minimumReleaseAge: \"4 days\"` (all datasources) so Renovate never proposes versions that pnpm strict mode would reject during lockfile updates; the extra day over pnpm's three days is a deliberate buffer against boundary races. If the pnpm cooldown changes, keep the Renovate value at least as large. Renovate exempts vulnerability alerts from its cooldown, but pnpm strict mode still blocks immature versions at install time; use pnpm's `minimumReleaseAgeExclude` if a security fix must land early.\n- `.github/workflows/test.yml` runs `pnpm lint`, `pnpm test:types`, `pnpm test`, `pnpm --filter tailwind-merge build`, `pnpm --filter tailwind-merge test:exports`, then each plugin's `build` and `test:exports` (`@tailwind-merge/vite`, then `@tailwind-merge/next`) — the plugin steps after the library build because the packed-tarball gates resolve tailwind-merge through its published dist and types. The Next.js package's Vitest project spawns real Next.js processes and builds the library itself when its dist is missing (its global setup), so `pnpm test` stays independent of the build steps' order.\n- `.github/workflows/benchmark.yml` runs `pnpm --filter tailwind-merge bench` with CodSpeed tokenless uploads for this public repository; do not pass static CodSpeed upload tokens to jobs that execute PR benchmark code.\n- `.github/workflows/codeql-analysis.yml` runs CodeQL for both JavaScript/TypeScript and GitHub Actions workflows; the JavaScript job builds the library and both plugins with explicit package filters, since the workspace root has no build script. The Actions analysis uses the `security-extended` query suite to catch workflow-specific security issues. It also runs on PRs that touch `.github/actions/**` or `.github/workflows/**` so CI-control-plane changes are scanned before merge.\n- Every workflow should declare explicit least-privilege `permissions`; read-only build/test jobs use `contents: read`, and write scopes should appear only on the jobs that need them.\n- Pin every `actions/checkout` use to an immutable commit and set `persist-credentials: false` unless the job must push commits or tags through git; checkout receives the job token even in read-only jobs.\n- `actions/checkout` v7+ blocks fork pull request code under `pull_request_target` and PR-triggered `workflow_run`; never set `allow-unsafe-pr-checkout: true`. Keep `label.yml` checkout-free, keep the metrics comment job on the base SHA, and keep release-comment workflow runs restricted to successful `push` events on `main`.\n- Third-party GitHub Actions that receive secrets or write-capable tokens are pinned to full commit SHAs with comments showing the source tag or branch.\n- Artifact handoffs intentionally pair `actions/upload-artifact@v7` with `actions/download-artifact@v8` because the two actions have different latest major versions; do not try to align their version numbers unless upstream releases change.\n- Local JavaScript GitHub Actions use `runs.using: node24`; keep action scripts compatible with the declared runner and avoid runtime-fragile ESM features, such as JSON module import assertion syntax, when a simple filesystem read works across supported Node versions.\n- `.github/workflows/metrics-report.yml` keeps PR code execution in a read-only `generate-report` job and posts comments from a separate `post-comment` job that checks out trusted base-repo code. The metrics report action itself only writes the generated comment body to the artifact path and must not post PR comments directly. If a transition PR introduces or moves the trusted posting script, the workflow should skip the `post-comment` job at the job level until that script exists in the base checkout rather than running PR-provided posting code with write permissions.\n- `.github/workflows/npm-publish.yml`:\n    - publishes `dev` tag builds of tailwind-merge and both plugins on `main` pushes: one build job builds all three (library first, topologically) and runs every `test:exports` gate, one OIDC job stamps the manifests with `scripts/stamp-dev-version.mjs` (dev version plus the plugins' exact pins on the library's same-commit dev version) and publishes the library before the plugins. The plugin core and the configurator have no dev release; they ship inlined in the plugin bundles. A new package joins these jobs only after its npm trusted publisher exists, since a publish step without one fails on every push (bootstrap in the release guide),\n    - publishes production on release events, routing by the namespaced tag prefix (`tailwind-merge@*` and legacy `v*` map to `packages/tailwind-merge`, `@tailwind-merge/vite@*` to `packages/vite`, `@tailwind-merge/next@*` to `packages/next`, anything else fails),\n    - the release build job builds the library alongside the released package (`pnpm --filter tailwind-merge --filter \"$PACKAGE_NAME\" build` — a plugin's test:exports needs the library's published shape; duplicate filters collapse for tailwind-merge releases, and pnpm's topological order builds the library before a plugin) and runs the released package's `test:exports` unconditionally, plus `test:library-release` for plugin releases (registry check that the packed tailwind-merge range's lowest version ships the unstable-entry-point imports; see the Vite and Next.js guides),\n    - publishes with `pnpm publish --access public --provenance --ignore-scripts --no-git-checks` (plus `--tag dev` in the dev flow): pnpm applies `publishConfig` field overrides at pack time (the vite exports swap) and implements npm trusted publishing natively — it fetches the GitHub OIDC id-token, exchanges it per package at the registry, and publishes through npm's own libnpmpublish vendored inside pnpm. `--provenance` must be explicit because pnpm reads it from flags/config, not from `publishConfig.provenance`; `--no-git-checks` because CI checkouts are detached HEADs (and the dev flow's version stamp dirties the tree),\n    - the publish jobs enable corepack (for pnpm) and deliberately set no `registry-url` in setup-node: that input writes an `.npmrc` auth line referencing the unset `NODE_AUTH_TOKEN` env var, and pnpm fails config loading on unresolvable env references in `.npmrc`,\n    - keeps linting, tests, and builds in non-OIDC jobs,\n    - avoids dependency caches in the publish workflow,\n    - grants `id-token: write` only to minimal publish jobs that download the verified `dist` artifact and run `pnpm publish --ignore-scripts`. The release publish job first runs `pnpm install --frozen-lockfile --ignore-scripts --filter \"$PACKAGE_NAME\"` because pnpm rewrites `workspace:` specifiers at publish time by reading the linked package's manifest from `node_modules` — without it, publishing a plugin fails with `ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL`. The filter installs only the released package's dependencies, and `--ignore-scripts` keeps dependency code from running in the job. The dev publish job runs the same filtered install for both plugins because their devDependencies keep the inlined plugin core's and configurator's `workspace:*` specifiers; tailwind-merge alone would need no install, since its specifiers are all `catalog:` and resolve from `pnpm-workspace.yaml`,\n    - pins `actions/download-artifact` to an immutable commit in the OIDC jobs and keeps digest mismatches fatal so corrupted or substituted artifacts cannot reach npm publishing.\n- `.github/workflows/label.yml` uses `pull_request_target` only for labeling metadata; do not add repository checkout or PR-code execution to that workflow. `gh` commands in that workflow must pass `--repo` explicitly because there is intentionally no `.git` checkout for repository inference.\n- `.github/workflows/draft-release.yml` runs the release-drafter autolabeler under `pull_request_target` for the same reason: the plain `pull_request` token is read-only on fork PRs, so labeling fails with \"Resource not accessible by integration\" (seen on PR #689). Like `label.yml`, the `auto_label` job must stay checkout-free and never execute PR code.\n- `.github/workflows/comment-released-prs-and-issues.yml`:\n    - runs local action `.github/actions/release-commenter`,\n    - runs on `release.published`, on manual `workflow_dispatch`, and after successful `npm Publish` workflow completion for `push` events on `main`,\n    - keeps the `workflow_run` checkout on trusted default-branch code and reads the triggering main commit's package metadata through the GitHub API instead of checking out the triggering SHA,\n    - supports manual `workflow_dispatch` with optional `head_tag`, `base_tag`, `dry_run`, and `npm_package_name`.\n\n## Practical guardrails\n\n- Keep `README.md` edits synchronized with `docs/README.md`; source-of-truth is docs.\n- Do not rely on `dist/` as edit target; regenerate instead.\n- Favor targeted tests while iterating, then run full suite before finalizing.\n- Any behavior change should include at least one regression test.\n</yakaai_skills>"}