@gallopsystems/agent-skills 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gallopsystems/agent-skills",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Gallop Systems agent skills, symlinked into .claude/skills (Claude Code) and .agents/skills (Codex) on install.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: copier-template
3
- description: Maintain a Copier project template and propagate updates to generated ("descendant") repos. Covers template anatomy (copier.yml, jinja, tasks), testing template changes, tagging/releasing versions, the automated update-notification PR pattern, and applying copier update in descendants with conflict resolution.
3
+ description: Maintain a Copier project template and propagate updates to generated ("descendant") repos. Covers template anatomy (copier.yml, jinja, tasks), testing template changes, tagging/releasing versions, the automated update-notification PR pattern, and applying copier update in descendants — bringing a repo fully to the latest template version while preserving its app behavior, with conflict resolution an autonomous agent can run end to end.
4
4
  ---
5
5
 
6
6
  # Copier Template Maintenance & Propagation
@@ -19,7 +19,7 @@ Patterns for the full lifecycle of a [Copier](https://copier.readthedocs.io/) pr
19
19
 
20
20
  - The **template repo** holds `copier.yml` (questions + settings + tasks) and a `template/` subdirectory of scaffold files (some `.jinja`-suffixed for substitution). **Git tags (`v*`) are the version protocol**; GitHub Releases are the changelog protocol.
21
21
  - Each **descendant** carries `.copier-answers.yml` recording its answers and `_commit: vX.Y.Z` — the template version it's on. Never hand-edit this file; `copier update` maintains it.
22
- - `copier update` re-renders from old-tag → newest tag and three-way merges against local changes. **It always jumps to the latest tag** (unless `--vcs-ref` pins one) — a notification PR advertising v1.5.0 may actually land v1.8.0 if the template moved on.
22
+ - `copier update` re-renders from old-tag → newest tag and three-way merges against local changes. **It always jumps to the latest tag** (unless `--vcs-ref` pins one) — a notification PR advertising v1.5.0 may actually land v1.8.0 if the template moved on. The job is **never to bump a version string** — it is to standardize whatever the template now sells while keeping this app's behavior intact. Hand-editing `_commit` or any version number to shrink a diff is always wrong; let `copier update` move it, and absorb the real changes.
23
23
  - Run copier via `uvx copier ...` (no global install needed). Templates with `_tasks` require `--trust` — without it copier refuses to render at all. Non-interactive contexts also need `--defaults` (and `--data key=value` for required questions without defaults).
24
24
 
25
25
  ## Direction of Change
@@ -62,6 +62,15 @@ git ls-remote --tags --refs --sort=-v:refname <template-url> 'v*' | head -1
62
62
 
63
63
  If newer, it pushes a **static branch name** (e.g. `chore/template-update`) with an `--allow-empty` commit and opens a PR whose body contains the version delta, release-notes/compare links, and step-by-step instructions an agent can execute. Hard-won details to keep if reimplementing: an explicit `permissions: contents: write, pull-requests: write` block (default token can't open PRs), a static branch name (dated branches caused duplicate PRs), and comparing **tag versions, not commit SHAs**.
64
64
 
65
+ ## Dependency Updates: Who Owns What
66
+
67
+ Two Renovate instances run, with a deliberate boundary so they never fight:
68
+
69
+ - **The template's Renovate** (in the template repo) keeps the pins in `template/package.json.jinja` fresh via a custom regex manager, and **auto-merges `@gallopsystems/agent-skills`** — which release-please then cuts as a template release. Those bumps reach descendants through `copier update`.
70
+ - **Each descendant's Renovate** (shipped as `renovate.json`, gated on the `include_renovate` question) owns that repo's **own** app dependencies — the only place an upgrade can be tested against the real app's code and CI.
71
+
72
+ The one overlap is resolved by ownership: the descendant's `renovate.json` **disables `@gallopsystems/agent-skills`**, leaving it solely template-owned. Every other pin is the descendant's. Because the template keeps bumping all pins, a descendant's `package.json` arrives with version conflicts on `copier update` — resolve them by keeping the descendant's versions (see [applying-updates.md](applying-updates.md) → *`package.json` dependency pins*). Newly-scaffolded repos start on the template's pins and are freshened by their own Renovate within a day.
73
+
65
74
  ## Branch Protection in Descendants
66
75
 
67
76
  A template **cannot** enable branch protection for the repos it generates — GitHub reads required status checks from repo config, never from committed workflow files. So every descendant starts with nothing gating merges until someone sets it once (after the first CI run, so the check is known). This template's CI exposes a **`ci-success`** summary job to be exactly that gate — require it on `main`:
@@ -79,7 +88,7 @@ echo '{"required_status_checks":{"strict":false,"contexts":["ci-success"]},"enfo
79
88
  ## Further Reading
80
89
 
81
90
  - **Template anatomy & testing changes**: [template-authoring.md](template-authoring.md)
82
- - **Applying an update in a descendant** (the conflict-resolution procedure): [applying-updates.md](applying-updates.md)
91
+ - **Applying an update in a descendant** (the full procedure — read-the-changes-first, multi-version deltas, `.rej` triage, silent-overwrite review, and autonomous merge-readiness): [applying-updates.md](applying-updates.md)
83
92
 
84
93
  ## Contributing Back
85
94
 
@@ -2,24 +2,68 @@
2
2
 
3
3
  Usually triggered by the automated "template update available" PR — its body contains the runbook; this file is the full procedure with the judgment calls spelled out.
4
4
 
5
- ## The sequence
5
+ **The point is not to bump the version number.** It is to standardize whatever the template now sells *while preserving this app's behavior*. For every release in range: understand what it changed, decide how it fits THIS app, and merge it in. A PR that only advances `_commit` / a version string without absorbing the template's actual changes is a failure, not a success — escalate instead of shipping one.
6
+
7
+ This procedure runs unattended end to end. There is no "ask the user" step mid-run — finish in one of the honest end states at the bottom.
8
+
9
+ ## Before you start: establish a green baseline
10
+
11
+ Post-update failures are only attributable if main was clean first.
12
+
13
+ ```bash
14
+ gh run list --branch main --limit 1 --json conclusion,headSha # latest main CI green?
15
+ git checkout main && git pull --ff-only && git status --short # clean, up to date
16
+ ```
17
+
18
+ If main's CI is red *before* the update, note it — that failure is pre-existing and must not be attributed to (or silently fixed under cover of) the template bump. If you can't confirm a baseline, say so in the PR body.
19
+
20
+ **Diff the bot's branch against current main first.** The notification branch (`chore/template-update`) is cut far behind main — often tens of commits — and main has frequently *already* absorbed equivalent changes through normal feature work, so the real conflict set is much smaller than the version delta implies. Start the update from current main, not from the stale bot branch:
6
21
 
7
22
  ```bash
8
- gh pr view <n> --json title,body # read the bot PR's instructions
9
- git checkout chore/template-update && git pull # the bot's static branch
23
+ gh pr view <n> --json title,body,headRefName
24
+ git log --oneline origin/main..origin/chore/template-update # how stale is the bot branch?
25
+ git checkout -B chore/template-update origin/main # rebuild the branch onto current main
26
+ ```
27
+
28
+ ## The sequence
10
29
 
30
+ ```bash
11
31
  # clean tree required — copier refuses otherwise
12
32
  git stash --include-untracked -m "wip before template update" # if dirty
13
33
 
14
- uvx copier update --trust --defaults
34
+ uvx copier update --trust --defaults --skip-tasks
15
35
 
16
36
  # triage
17
- git status --short # UU = unmerged (inline conflict markers)
18
- find . -name '*.rej' # hunks copier couldn't apply
19
- grep _commit .copier-answers.yml # confirm the new version
37
+ git status --short # UU = unmerged (inline conflict markers)
38
+ find . -name '*.rej' # hunks copier couldn't apply
39
+ grep _commit .copier-answers.yml # confirm the new (latest) version
20
40
  ```
21
41
 
22
- Note the version it reports: `copier update` goes to the **latest** tag, which may be newer than the one the bot PR advertised. A multi-version jump means several releases' worth of changes land at once — budget for more conflicts.
42
+ - **`--skip-tasks`**: `_tasks` (migrations, `npx volt-vue add`, seeds) only run on initial `copy`, but skip them explicitly so an update never fires surprise scaffold work (a stray migration, a regenerated component). Run any genuinely-needed task by hand afterward.
43
+ - **Dirty-tree refuses on untracked dirs/files too**, not just staged changes — `git stash` alone misses them. Use `--include-untracked`, or move local-tooling dirs (`.agents/`, `.claude/`, `.do/`, editor swap files) aside and restore them after.
44
+ - **The landed version is almost always higher than the bot PR advertised** — `copier update` targets the latest tag. Read the real version from `_commit` *after* the run and title the commit/PR from that. A multi-version jump means several releases land at once — budget for more conflicts.
45
+
46
+ ## Understand what changed before you resolve anything
47
+
48
+ Resolving conflicts blind produces a literal bump. Read the delta first.
49
+
50
+ **Enumerate every release in range**, not just the endpoints — copier jumped straight to the latest tag, so several releases may have landed at once:
51
+
52
+ ```bash
53
+ TPL=<template-url>
54
+ git ls-remote --tags --refs --sort=v:refname "$TPL" 'v*' # full tag list; pick OLD..NEW
55
+ gh release list --repo <template-owner/template-repo> --limit 20
56
+ gh release view v<X> --repo <template-owner/template-repo> # read each release's notes, oldest→newest
57
+ ```
58
+
59
+ Read the **cumulative compare** the bot links (or build it): `<template-url>/compare/v<OLD>...v<NEW>`. Scan specifically for:
60
+
61
+ - **Breaking changes** — renamed/removed questions, restructured `template/` layout, moved files (a factory file relocating, an auth file added).
62
+ - **CI / workflow** changes — new jobs, a changed gate (`ci-success`), runner/tool-version bumps.
63
+ - **Scaffold-owned files** the template overwrites silently (configs, lint/tsconfig, `main.css`, seed/factory/preview-login files).
64
+ - **New `_tasks` or dependencies** the descendant must now satisfy.
65
+
66
+ Write down, per release, what it sells. That list is the checklist the final PR body is graded against, and it tells you which conflicts are load-bearing vs cosmetic.
23
67
 
24
68
  ## Resolving conflicts
25
69
 
@@ -55,33 +99,136 @@ perl -0pi -e 's/^<<<<<<< before updating\n(.*?)^\|\|\|\|\|\|\| last update\n.*?^
55
99
  perl -0pi -e 's/^<<<<<<< before updating\n(.*?)^\|\|\|\|\|\|\| last update\n.*?^=======\n(.*?)^>>>>>>> after updating\n/$1$2/gms' <file>
56
100
  ```
57
101
 
58
- **`.rej` files**: copier couldn't apply a hunk (the local file diverged too far). Read the `.rej`, re-apply its *intent* manually — and check whether copier dropped project-specific content nearby (e.g. local vars in `.env.example`) — then `rm` the `.rej`.
102
+ After editing markers out, **`git add` each resolved file** — it stays `UU` until staged, and an unstaged `UU` later blocks `git stash pop` and the push.
103
+
104
+ **`package.json` dependency pins — keep *this repo's* versions, not the template's.** The template's Renovate bumps every pin in `package.json.jinja`, so each release moves those lines and they arrive here as conflicts. But a descendant runs its **own** Renovate, which keeps its deps ahead of — and CI-tested against — the template's pins, so the template side is almost always *behind*. Resolve each dependency-version conflict by **keeping ours**, with two exceptions:
105
+
106
+ - **`@gallopsystems/agent-skills`** is template-owned (the descendant's `renovate.json` is configured to ignore it, so the template is its only updater). Always **take theirs** for that line.
107
+ - If the template's pin is genuinely *higher* than ours (this repo lagged — Renovate paused, or a dep Renovate doesn't manage), taking theirs is fine **only when it's an obviously-safe move** — a patch within the same minor. For a minor/major where ours is behind, keep ours and let this repo's Renovate make the jump afterward rather than adopting the template's pin blind.
108
+
109
+ A *new* dependency the template adds is not a conflict (the descendant doesn't have it yet) — copier just adds it; keep it. This rule is only about shared pins. The split is deliberate: the template owns `agent-skills`, each descendant owns its own app dependencies.
110
+
111
+ **Scaffold files arrive written against the TEMPLATE's schema — adopt the feature, adapt it to yours; never a naive side-pick.** Files like preview-login, factories, seeds, and `auth.d.ts` ship assuming the template's columns (`first_name`/`last_name`, `deactivated_at`, numeric `id`). A descendant that diverged (a single `name` column, camelCase, string IDs) won't compile against them. Take the template's *feature* but rewrite it to the real schema: revert `Number(id)` coercions, fix the anchor-user/seed columns, drop selects on columns that don't exist, repair the matching test. After adopting any such file, grep it against the real `db.d.ts`:
112
+
113
+ ```bash
114
+ grep -nE 'first_name|last_name|deactivated_at|Number\(' <adopted-file>
115
+ ```
116
+
117
+ **Convention migrations require porting, not side-picking.** When a release *moves* a convention (e.g. factories relocating from `server/test-utils/` into a shared `server/db/factories.ts` with a new `DbLike` type), a naive ours/theirs pick loses every project-specific factory. Port them into the new shape. Clone the template at the exact target tag to get the authoritative "after":
118
+
119
+ ```bash
120
+ git clone --depth 1 --branch v<NEW> <template-url> /tmp/tpl-<NEW>
121
+ ```
122
+
123
+ When a conflict reflects a deliberate template convention change (snake_case session fields, a renamed env var), migrating *toward* the template and updating the few consumers kills future churn — unless it's an intentional permanent fork (styled-PrimeVue vs Volt, a deliberately different env-var name), which you keep.
124
+
125
+ **`.rej` files** — copier couldn't apply a hunk because the local file diverged too far. A `.rej` is a unified diff of *just the rejected hunk*; the target file was left untouched there. Triage each:
126
+
127
+ ```bash
128
+ find . -name '*.rej'
129
+ cat path/to/file.ext.rej # ` ` context, `-` old template line, `+` new template line
130
+ ```
131
+
132
+ 1. **Read the hunk's intent** — the `+` lines are the new template content; `-` lines are what the old template had (which this project already diverged from).
133
+ 2. **Classify**: template intent (a feature/fix every descendant should get) vs a deliberate project customization at that spot. Template intent → apply it by hand into the live file, adapting to local naming. Project customization → keep the project's version; note the intentional skip.
134
+ 3. **Check adjacent content** — copier may have dropped project-specific lines near the hunk (local vars in `.env.example`, an extra `package.json` script). Diff the region against `origin/main` before trusting the live file.
135
+ 4. **`rm` the `.rej`** only after the live file reflects your decision — a committed `.rej` is a defect.
136
+
137
+ `.env.example` recurs as a separate `.rej` even when everything else merged inline — always check for both.
138
+
139
+ **Review every silently-overwritten file — a clean, conflict-free merge can still be harmful.** Copier overwrites scaffold-owned files with **no markers**, and its text merge is structure-blind: it has duplicated `public:` inside `runtimeConfig` and the entire `@theme`/CSS-token block in `main.css`, both of which would silently break the app. `git diff` everything the update touched, not just the `UU` files:
140
+
141
+ ```bash
142
+ git diff origin/main --stat # everything the update touched
143
+ git diff origin/main -- <file> # per-file: keep / revert / merge — watch for duplicated keys/blocks
144
+ ```
145
+
146
+ - A new template assumption can be wrong for this project → revert the bad part, note it, and consider whether the *template* needs the fix (open a template issue/PR if other descendants are affected).
147
+ - An overwritten config the project had customized → re-apply the project's customization on top of the template's new baseline.
148
+
149
+ **CI workflows get explicit, line-by-line attention — never blind-accept them:**
150
+
151
+ 1. Diff the new workflow: `git diff origin/main -- .github/workflows/`.
152
+ 2. Understand **what the new template CI does** — new jobs, changed triggers, the gate job (`ci-success`), runner/tool versions.
153
+ 3. Decide, per change, **whether it's an improvement this app wants**, and keep it.
154
+ 4. **Preserve the app's own CI** — project-specific jobs, secrets, deploy steps, matrix tweaks the template doesn't know about. Merge them on top; don't let the overwrite drop them.
155
+ 5. **Validate the adopted commands actually fit**: does `test:run` accept `--shard`? do the new `ci-success` `needs:` reference the repo's real job names? does a new job hard-code an env name the harness doesn't read (e.g. `TEST_POSTGRESQL_CONNECTION_STRING` when the harness reads `NUXT_DATABASE_URL_TEST`)? does the Postgres service version match? Conflict-resolved YAML can be valid yet reference scripts/jobs that don't exist.
156
+ 6. If the gate job name or required contexts changed, reconcile with branch protection (see SKILL.md → *Branch Protection in Descendants*).
59
157
 
60
- **Review the non-conflicted changes too.** Copier silently overwrites scaffold-owned files, and a new template assumption can be wrong for this project (e.g. a type coercion that assumes numeric IDs in a project using string IDs). `git diff` every copier-touched app-code file; revert what doesn't fit, and consider whether the template itself needs a fix.
158
+ A chore that recurs every single update (e.g. `fmt:check` always failing on arrival) is a signal to fix the **template**, not the descendant — flag it upstream.
61
159
 
62
- Final sweep before staging:
160
+ Final sweep before staging — both marker kinds and leftover `.rej`, excluding vendored noise:
63
161
 
64
162
  ```bash
65
- grep -rn '<<<<<<<\|>>>>>>>' . --exclude-dir=node_modules
163
+ grep -rnE '^<<<<<<< |^>>>>>>> ' . --exclude-dir=node_modules --exclude-dir=.yarn # .yarn release contains literal >>>>>>>
164
+ find . -name '*.rej' # must be empty
66
165
  ```
67
166
 
68
- ## Validate, commit, hand back
167
+ ## Validate, commit, and decide merge-readiness
69
168
 
70
169
  ```bash
71
170
  git add -A # includes .copier-answers.yml — it must be committed with the update
72
171
  yarn install && yarn typecheck && yarn lint && yarn fmt:check && yarn test:run
172
+ git status --short # re-check for newly-generated install artifacts (a new symlink dir, lockfile churn)
73
173
  ```
74
174
 
75
- Commit as `chore: update to template vX.Y.Z` with a body listing the notable upstream changes **and each conflict resolution with its rationale** — that's the audit trail for the squash-merge. Then retitle the bot PR (`gh pr edit <n> --title "chore: update to template vX.Y.Z"`), push, watch CI, and ask the user before merging. The bot's empty placeholder commit is fine — it disappears in the squash.
175
+ - **Brand-new scaffold files can fail `fmt:check` on arrival** (a new `.mcp.json` with a leading blank line) — even a conflict-free update needs the full suite, because new files aren't pre-formatted to your formatter.
176
+ - **A template/Nuxt bump drags dependency-alignment failures that look like update breakage** — e.g. a `vue-router@^4` pin throwing `ERR_PACKAGE_PATH_NOT_EXPORTED` after the bump wanted `^5`, or a newly-strict `@vue/language-core` flagging valid Vue. Fix the dependency/toolchain drift; don't revert the template change.
177
+ - **A bumped post-install dep can generate new artifacts the old `.gitignore` misses** (a `.agents/` symlink dir). Add the ignore + `git rm -r --cached <dir>`, but still stage `.copier-answers.yml`.
178
+ - **Recover non-destructively.** `git reset --hard` / `git stash drop` risk discarding the update work (and are blocked under agent auto-mode). Prefer `git checkout --ours/--theirs`, `git rm --cached`, `git merge --abort`. If a `reset --hard origin/<branch>` is genuinely warranted, first prove the commits being discarded are already in main.
179
+
180
+ **If a check fails, prove whether it's pre-existing** before blaming the update: `git worktree add /tmp/<proj>-main origin/main`, symlink `node_modules`, rerun the failing check there. A byte-identical failure on main means fix-forward in this PR, not a regression. A local lefthook pre-push hook may block on such a pre-existing failure — only bypass (`--no-verify`) once you've proven it reproduces on pristine main and CI is the real gate; push with `--force-with-lease` since the branch was rebuilt.
181
+
182
+ ### Commit / PR body template
183
+
184
+ Commit as `chore: update to template vX.Y.Z`. The body is the audit trail for the squash-merge — it must show the update was *absorbed*, not just stamped:
185
+
186
+ ```
187
+ chore: update to template vX.Y.Z
188
+
189
+ Version: vOLD → vNEW (copier jumped to latest)
190
+ Releases included: vA, vB, vNEW
191
+
192
+ Notable upstream changes (what the template now sells):
193
+ - vA: <feature/fix> — applied / adapted as <how>
194
+ - vB: <CI change> — kept <X>, preserved app's <Y>
195
+ - vNEW: <breaking change> — handled by <how>
196
+
197
+ Conflicts & resolutions:
198
+ - path/to/file: kept ours (hand-customized) — <why>
199
+ - path/to/file: took theirs (untouched scaffold)
200
+ - path/to/file: merged — project <X> + template <Y>
201
+ - path/to/file.rej: applied template intent by hand / intentionally skipped — <why>
202
+
203
+ Reverted template changes (wrong for this app):
204
+ - <file>: <what + why>; template issue: <link or "n/a">
205
+
206
+ Validation: typecheck / lint / fmt / tests — <pass | pre-existing failure on main: link>
207
+ Baseline: main CI was <green | red (link)> before this update.
208
+ ```
209
+
210
+ Retitle the bot PR to match: `gh pr edit <n> --title "chore: update to template vX.Y.Z"`, push, watch CI. The bot's empty placeholder commit disappears in the squash.
211
+
212
+ ### What "done" means
213
+
214
+ Land in one of three honest end states:
215
+
216
+ - **`pr_open`, ready for human review** — every release in range absorbed, all conflicts resolved with rationale, `.rej`/markers gone, CI green (or failure proven pre-existing), PR body complete. **Do not self-merge a template update** — stop here for a human to merge. This is the normal success state.
217
+ - **`pr_open`, escalated** — you hit a conflict you cannot resolve safely (a breaking change whose correct adaptation is ambiguous, a template assumption that contradicts core app behavior, CI you can't get green without guessing). **Escalating is correct.** Push what you have, mark the PR a **draft**, and write the blocker explicitly in the body: which release, which file, the two options, why you stopped. **Never paper over it by reverting the hard part and shipping a literal version bump** — a bump that drops the template's real change is worse than an open question.
218
+ - **No change needed** — `_commit` already equals the latest tag (main absorbed it). Close the bot PR with a note.
76
219
 
77
- **If something fails after the update**, prove whether it's pre-existing before blaming the update: `git worktree add /tmp/<proj>-main origin/main`, reuse node_modules (symlink), rerun the failing check there. A byte-identical failure on main means fix-forward in this PR, not a regression.
220
+ The bar: would a reviewer reading the PR body see *what each version changed and how it was fit into this app*? If not, it is not done.
78
221
 
79
222
  ## Troubleshooting
80
223
 
81
224
  | Symptom | Fix |
82
225
  |---|---|
83
- | `Destination repository is dirty; cannot continue` | `git stash --include-untracked` — plain `git stash` misses untracked files (including editor swap files), which still count as dirty |
226
+ | `Destination repository is dirty; cannot continue` | `git stash --include-untracked` — plain `git stash` misses untracked files/dirs (editor swap files, local-tooling dirs), which still count as dirty |
84
227
  | Copier refuses to render at all | Template has `_tasks` — add `--trust` |
85
228
  | Hangs or fails in non-interactive shells | Add `--defaults` (and `--data key=value` for questions without defaults) |
229
+ | Update wants to run scaffold tasks (migrations, component installs) | Add `--skip-tasks`; run any genuinely-needed task by hand |
86
230
  | Update landed a version you didn't expect | `copier update` always targets the latest tag; pin with `--vcs-ref v<X.Y.Z>` if you need a specific one |
231
+ | Conflict marker grep returns hits in `.yarn/` | False positives from the vendored yarn release — `--exclude-dir=.yarn` |
232
+ | An auto-merged file looks fine but the app breaks | Structure-blind text merge duplicated a key/block (`runtimeConfig.public`, `main.css` `@theme`) — `git diff origin/main` the non-conflicted files too |
233
+ | Tempted to just edit `_commit` / a version string to make the diff small | Stop — that's the literal-bump failure. The template's changes must be merged, not stamped. Re-run `copier update` clean; if conflicts are unresolvable, escalate (see *What "done" means*) |
87
234
  | Template change is wrong for this project | Revert locally, note it in the commit body, open a template issue/PR if other descendants are affected |
@@ -236,7 +236,16 @@ The setup file stubs Nuxt/Nitro auto-imports:
236
236
 
237
237
  3. **Nested transactions work** - Code that calls `db.transaction()` works because we patch the prototype
238
238
 
239
- 4. **Test file location** - Co-locate with handlers: `handler.ts` → `handler.test.ts`
239
+ 4. **Test file location** - Co-locate with handlers: `handler.ts` → `handler.test.ts`.
240
+ **Exception — module-scanned directories:** never co-locate a test inside a
241
+ directory a Nuxt/Nitro module auto-imports *wholesale* (it globs every file in
242
+ the dir and bundles it into the server build — e.g. a tool/plugin registry
243
+ like an MCP toolkit's `server/mcp/tools/`, where dropping a `*.test.ts` next to
244
+ the tool means the test is pulled into the build). `yarn build` then fails when
245
+ that test imports build-absent test utilities (e.g. `~/server/test-utils` →
246
+ `ENOENT`). Vitest **and** `typecheck` stay green — only `yarn build` (or the
247
+ build CI job) catches it. Keep such tests outside the scanned dir (e.g. under
248
+ `server/utils/`) and import the unit under test by alias.
240
249
 
241
250
  5. **Separate test database** - Always use a dedicated test DB (`myapp-test`, not `myapp`)
242
251