@gallopsystems/agent-skills 1.25.1 → 1.27.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.25.1",
3
+ "version": "1.27.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": {
@@ -199,6 +199,16 @@ The first three are the foundational lenses — every other principle and rule s
199
199
  - **Why:** This is core to the tool-like feel. Persistent action buttons next to every value make lists feel cluttered and form-like; inline edit + hover actions keep the surface calm and let content dominate.
200
200
  - **How to apply:** For editable fields on detail pages and rows, prefer click-to-edit. For bulk row actions, use a hover-revealed action group or a per-row `…` (kebab) menu. Reserve always-visible buttons for the one or two primary actions of the surface.
201
201
 
202
+ #### Entering edit mode must not shift the layout
203
+
204
+ - **Rule:** Toggling a row or field between read and edit must not move anything around it. The read-out keeps its exact footprint, aligned columns (numeric value columns especially) stay put, and neighboring rows don't reflow. The classic offender is swapping a row's right-hand cells (the value/metric readout) for inputs — inputs have different widths, so the value column jumps and every other row's alignment breaks the instant one row opens.
205
+ - **Why:** A layout that lurches when you click "edit" reads as broken, and in a dense ledger/table it destroys the scannability that made the read view useful — the eye loses the column it was tracking. Stability is what makes inline editing feel native rather than like a bolted-on form.
206
+ - **How to apply:**
207
+ - Reveal the edit affordance (a pencil/kebab) with **absolute positioning** so it consumes no layout space — float it in the row's whitespace with `opacity-0 group-hover:opacity-100` (also reveal on `focus-visible`). Adding a real per-row action *column* pushes content and, unless you add the identical gutter to every row including headers/totals, misaligns the value column — absolute positioning sidesteps that entirely.
208
+ - For anything wider than the value it replaces (multiple inputs, unit×qty, a rate + suffix), **expand an editor strip below the row** rather than replacing the cells in place. The row above stays frozen showing the current value for context; the editor gets arbitrary width with zero horizontal shift. Animate the expand with a height/opacity transition (e.g. `v-auto-animate` / auto-height), never by animating siblings.
209
+ - Drop the row's bottom divider while it's being edited so the row and its editor strip read as one unit, not two stacked rows.
210
+ - One editor open at a time; autofocus the first input; **Enter** saves, **Esc** cancels. On save, patch → return to read-only → let the recomputed value settle back into the unchanged row.
211
+
202
212
  #### Every interaction gets immediate visible feedback
203
213
 
204
214
  - **Rule:** The moment a user acts, the UI must respond — within the same frame if possible. Pressed states on buttons (slight darken or scale, not just color), hover states on every clickable surface, optimistic UI updates for ordinary mutations (toggling a checkbox, renaming a field), an inline check or subtle highlight when a save completes. If a network call takes more than ~200ms, show a quiet inline spinner or skeleton in the affected region — not a global loading bar.
@@ -52,6 +52,7 @@ Runnable, copy-pasteable query examples live alongside as `.ts` files:
52
52
  1. **Always use Kysely's query builder — never reach for raw `sql`**: Almost anything expressible in SQL is expressible type-safely through Kysely's methods and the ExpressionBuilder (`eb`); raw `sql`` throws away the type safety this stack depends on. Treat it as a true last resort — only when Kysely genuinely cannot express the query (and type the template literal when you must). Being unsure how to do something is the cue to check the reference guides above, **not** to drop to raw SQL.
53
53
  2. **Use the ExpressionBuilder (eb)**: The `eb` parameter in callbacks is the foundation of type-safe query building
54
54
  3. **Let TypeScript guide you**: If it compiles, it's likely correct SQL
55
+ 4. **Document schema in the database, and read it before you touch a table**: Keep table/column/index/constraint notes as Postgres COMMENTs (not just migration source), surface them in the generated `db.d.ts`, and update them in the same migration that changes how something works. Consult that JSDoc to gather context before querying or altering a table — see [migrations-and-codegen.md](references/migrations-and-codegen.md#schema-comments--document-in-the-db-not-just-in-migration-source).
55
56
 
56
57
  ## Contributing Back
57
58
 
@@ -147,6 +147,46 @@ with `ifExists`, so re-applying it after the others is a safe no-op). Verify the
147
147
  ledger is a contiguous prefix after the DELETE, and prefer letting the deploy's own
148
148
  migrate job re-apply rather than running migrations from a laptop against prod.
149
149
 
150
+ ## Schema Comments — Document in the DB, Not Just in Migration Source
151
+
152
+ Design notes written as `//` comments in a migration file are invisible the moment
153
+ the migration runs: they don't reach a live database and they don't reach the
154
+ generated types. **Put the documentation in the database itself** as Postgres
155
+ COMMENTs so it survives, stays queryable (`\d+`, `obj_description`, any GUI), and —
156
+ via codegen below — lands in the generated `db.d.ts` as JSDoc.
157
+
158
+ Comment every object that carries intent, not just tables and columns:
159
+
160
+ ```typescript
161
+ await sql`COMMENT ON TABLE ${sql.ref("invoice")} IS ${sql.lit(
162
+ "A billed invoice. status is DERIVED from payments, never stored.",
163
+ )}`.execute(db);
164
+ await sql`COMMENT ON COLUMN ${sql.ref("invoice.void_reason")} IS ${sql.lit(
165
+ "Set only when status='void'; null otherwise.",
166
+ )}`.execute(db);
167
+ await sql`COMMENT ON INDEX ${sql.ref("uq_invoice_current_number")} IS ${sql.lit(
168
+ "One live number per org — partial unique WHERE void_at IS NULL.",
169
+ )}`.execute(db);
170
+ await sql`COMMENT ON CONSTRAINT ${sql.ref("invoice_status_check")} ON ${sql.ref("invoice")} IS ${sql.lit(
171
+ "status must be one of draft/sent/paid/void.",
172
+ )}`.execute(db);
173
+ ```
174
+
175
+ Use `sql.ref()` for identifiers and `sql.lit()` for the text (it escapes the
176
+ literal; `COMMENT` does not accept bound parameters). To reverse, set the comment
177
+ to `NULL`. Centralizing all comments in one data-driven migration (maps of
178
+ table/column/index/constraint → text, applied in `up`, nulled in `down`) keeps them
179
+ in one place and makes the `down` trivial.
180
+
181
+ **Keep comments in sync with behavior.** When a migration changes *how something
182
+ works* — a column's meaning, what a partial index enforces, a new CHECK — update
183
+ its comment in that same migration. A stale comment is worse than none.
184
+
185
+ **Gather context from these comments.** Before querying or changing a table, read
186
+ its JSDoc in the generated `db.d.ts` (see below) — the table/column/index/constraint
187
+ notes are the schema's own explanation of intent, invariants, and "derived, not
188
+ stored" rules. Prefer them over re-deriving intent from the raw column list.
189
+
150
190
  ## Type Generation
151
191
 
152
192
  Use `kysely-codegen` to generate types from your database:
@@ -160,3 +200,22 @@ Generated types use:
160
200
  - `ColumnType<Select, Insert, Update>` for different operation types
161
201
  - `Timestamp` for timestamptz columns
162
202
 
203
+ ### Surfacing schema comments in the generated types
204
+
205
+ `kysely-codegen` emits **column** comments as JSDoc automatically, but drops
206
+ **table**, **index**, and **constraint** comments (kysely #1368 / kysely-codegen
207
+ #316, unmerged as of 0.20.0 — indexes and constraints also have no symbol of their
208
+ own in the output). Recover them with a post-codegen step: a small script reads
209
+ `obj_description` for tables/indexes/constraints and injects each table's comment
210
+ plus `Indexes:` / `Constraints:` sections as JSDoc above its `export interface`.
211
+ Chain it into the codegen command:
212
+
213
+ ```jsonc
214
+ "db:codegen": "kysely-codegen --url $DATABASE_URL --out-file src/db/db.d.ts --dialect postgres --date-parser string && node scripts/inject-table-comments.mjs $DATABASE_URL src/db/db.d.ts"
215
+ ```
216
+
217
+ The result is a `db.d.ts` where each table interface is prefaced by its full design
218
+ note — the documentation surface an agent should consult first. (Ship the
219
+ `inject-table-comments.mjs` script and this wired-up `db:codegen` in your project
220
+ template so every scaffolded repo gets it out of the box.)
221
+
@@ -203,7 +203,7 @@ The CLI resolves friendly names against `workspace.json`, so you rarely need raw
203
203
 
204
204
  > **Important:** When assigning an issue to a cycle, always set `--state todo`. Issues default to Backlog, which doesn't work with cycles — they must be in Todo status.
205
205
  >
206
- > **Required placement rule:** Never create an issue without both `--project` and `--milestone`. If the right project does not exist, create it first. If the project exists but the right milestone does not, create the milestone first. Do not leave issues unscoped or unmilestoned.
206
+ > **Required placement rule:** Never create an issue without both `--project` and `--milestone`. **The project must already exist** — place the issue in the initiative's existing `M` project for the milestone it falls under, and never conjure a project to hold it (see "Never invent a project"). Creating a project is only correct for a confirmed out-of-scope revision. If the project exists but the right milestone does not, create the milestone first. Do not leave issues unscoped or unmilestoned.
207
207
  >
208
208
  > **Never target a completed milestone.** New work never belongs in a milestone that is already done — it distorts the completed phase and hides the issue from the team's current view. Only place an issue in an **open** milestone. If no open milestone matches the issue, create a new one and use that; do not reopen or reuse a completed milestone.
209
209
  >
@@ -241,8 +241,11 @@ node linear.mjs create-issue --title 'Investigate perf issue' --state todo \
241
241
  --description-file ./issue-body.md \
242
242
  --project 'project-uuid' --milestone 'milestone-uuid'
243
243
 
244
- # If the project or milestone does not exist yet, create it before the issue
245
- PROJECT_ID="$(node linear.mjs create-project --name '[CLIENT] Feature Area' --description 'Short description' | node -e "process.stdin.once('data',d=>console.log(JSON.parse(d).data.projectCreate.project.id))")"
244
+ # Find the existing M project for the milestone this work falls under — do not create one.
245
+ # (Prefer the MCP `get_initiative` with includeProjects; this lists them via the CLI.)
246
+ node linear.mjs list-projects # copy the [KEY] M<n> project's UUID
247
+ PROJECT_ID='project-uuid-here'
248
+ # Only the milestone may be created as part of intake
246
249
  MILESTONE_ID="$(node linear.mjs create-milestone "$PROJECT_ID" 'Phase 1' | node -e "process.stdin.once('data',d=>{const n=JSON.parse(d).data.projectMilestoneCreate.projectMilestone;console.log(n.id)})")"
247
250
  node linear.mjs create-issue --title 'Investigate performance issue' --state todo \
248
251
  --project "$PROJECT_ID" --milestone "$MILESTONE_ID" --cycle current
@@ -319,7 +322,9 @@ node linear.mjs search-issues "login bug"
319
322
  ### Projects & Milestones
320
323
  ```bash
321
324
  # Create a new project (linked to an initiative)
322
- node linear.mjs create-project --name "[KEY] Project Name" --initiative "$INITIATIVE_ID" --description "Short description"
325
+ # Projects mirror the signed proposal's milestones ([KEY] M<n>) or a confirmed revision ([KEY] R<n>).
326
+ # Never create one to hold work you couldn't place — see "Never invent a project".
327
+ node linear.mjs create-project --name "[KEY] M1 — Milestone Name" --initiative "$INITIATIVE_ID" --description "Short description"
323
328
 
324
329
  # List all projects (pretty table with initiative, state, progress)
325
330
  node linear.mjs list-projects
@@ -347,7 +352,7 @@ node linear.mjs create-issue \
347
352
 
348
353
  ### Initiatives
349
354
  ```bash
350
- # Create a new initiative (= new client)
355
+ # Create a new initiative (= a newly signed proposal; a repeat client gets another one)
351
356
  node linear.mjs create-initiative --name "ClientName" --description "Short description"
352
357
 
353
358
  # List all initiatives (pretty table with ID, status, description)
@@ -601,11 +606,11 @@ node linear.mjs update-initiative "$INITIATIVE_ID" --content-file ./initiative-n
601
606
 
602
607
  Linear organizes work in a top-down hierarchy: **Initiative → Project → Milestone → Issue**. Here's how the Gallop team uses each level.
603
608
 
604
- ### Initiative (= Client)
609
+ ### Initiative (= Signed Proposal)
605
610
 
606
- An **Initiative** represents a client engagement or internal program. Each client gets one initiative.
611
+ An **Initiative** represents **one signed proposal** for a client, not the client itself. A client who signs a second proposal (a later phase, a separate engagement) gets a **second initiative** — never a second set of projects bolted onto the first. The initiative's scope is fixed by what was signed.
607
612
 
608
- **The current client roster is not stored in this repo — fetch it live from Linear.** Initiatives are the source of truth for which clients exist, their descriptions, and their repo links:
613
+ **The current roster is not stored in this repo — fetch it live from Linear.** Initiatives are the source of truth for which engagements exist, their descriptions, and their repo links:
609
614
 
610
615
  - **List all clients:** `mcp__linear-server__list_initiatives`
611
616
  - **Read a client's full details (overview, repo structure, domain notes):** `mcp__linear-server__get_initiative` — these live in the initiative's `content` field
@@ -615,33 +620,69 @@ When you start any task that needs client context, query Linear instead of looki
615
620
 
616
621
  - One initiative can contain **multiple projects**
617
622
 
618
- ### Project (= Product / Workstream)
623
+ ### Project (= One Proposed Milestone, or a Revision)
619
624
 
620
- A **Project** is a distinct product, app, or major workstream within a client initiative. It groups related issues that ship together.
625
+ A **Project** is **one of the milestones the signed proposal committed to** — not a
626
+ product, not a workstream, not a theme you invented. The initiative's project list
627
+ *is* the proposal's milestone list: if the proposal promised three milestones, the
628
+ initiative has three projects, numbered and named after them.
621
629
 
622
- **Examples:**
623
- - `[ACME] Billing System` — one self-contained product
624
- - `[ACME] Analytics Demo` — separate product under the same client
625
- - `[CLIENT] Migration Workstream` — the single workstream for that client
626
- - `[CLIENT] Scheduling Platform` — the main product for that client
630
+ **Naming convention:** `[KEY] M<n> — <Milestone name>`, where `<n>` is the
631
+ milestone's number in the signed proposal.
627
632
 
628
- **When to create a new project:**
629
- - The work has its own deployment, repo, or codebase
630
- - It could be described independently to a stakeholder
631
- - It has a distinct "done" state separate from other work
633
+ - `[KEY] M1 — Data Model`
634
+ - `[KEY] M2 — Pricing Engine`
635
+ - `[KEY] M3 — Reporting`
632
636
 
633
- **Naming convention:** `[CLIENT_KEY] Project Name`
637
+ **Work outside the signed scope is a revision, and gets its own project** —
638
+ attached to the **same initiative**, named with an `R` prefix instead of `M`:
639
+
640
+ - `[KEY] R1 — <Name>`, `[KEY] R2 — <Name>`, …
641
+
642
+ `R` numbering is sequential across the whole proposal in the order revisions are
643
+ taken on, independent of which milestone the revision relates to. Never fold
644
+ out-of-scope work into an `M` project — that silently rewrites what was signed.
645
+
646
+ #### Never invent a project
647
+
648
+ **The default is that the right project already exists.** Creating one is a
649
+ structural change to a signed engagement, so it is never a side effect of intake.
650
+
651
+ Before placing any work, answer this question — and **ask the requester if they
652
+ are in the conversation, do not infer it**:
653
+
654
+ > Is this within the signed scope, or is it a revision?
655
+
656
+ - **Within scope** → **find the existing `M` project yourself.** List the
657
+ initiative's projects (`get_initiative` with `includeProjects: true`), read what
658
+ each milestone covers, and place the work in the one it falls under. Only ask
659
+ which project if two genuinely both fit. Do **not** create a project because none
660
+ of the names happens to match the request's wording — milestone names are broad
661
+ by design.
662
+ - **A revision** → determine the next `R<n>` (one past the highest existing `R`,
663
+ or `R1`), **confirm the name and the revision framing with the requester**, then
664
+ create it under the same initiative.
665
+
666
+ Never create a project to hold work you were unsure how to place, to mirror a repo
667
+ or deployment, to group work by theme or domain (that is what milestones and labels
668
+ are for), or because the initiative looked empty. If you cannot place the work and
669
+ the requester is unavailable, leave it unplaced and say so — an invented project is
670
+ harder to undo than an unplaced issue.
634
671
 
635
672
  ### Milestone (= Phase / Epic)
636
673
 
637
674
  A **Milestone** is a phase or epic within a project — a meaningful chunk of progress that can be demoed or shipped incrementally.
638
675
 
639
- **Examples within `[ACME] Billing System`:**
676
+ A milestone is the level where grouping decisions actually belong — **unlike
677
+ projects, milestones may be created freely as part of intake.** If a request needs
678
+ a new home inside its `M` project, that home is a milestone, never a new project.
679
+
680
+ **Examples within `[KEY] M2 — Billing`:**
640
681
  - `Core Billing` — create, edit, send invoices (done)
641
682
  - `Quotes` — quote workflow, create/edit/convert to invoice
642
683
  - `Payments` — payment methods, receipts, balance due display
643
684
 
644
- **Examples within `[NW] Appointment Scheduling`:**
685
+ **Examples within `[KEY] M1 — Scheduling`:**
645
686
  - `Providers Module` — list, create, edit, deactivate providers
646
687
  - `Booking Requests` — request creation, accept/reject workflow
647
688
  - `Scheduling & Calendar` — availability, scheduling UI
@@ -661,24 +702,30 @@ Individual work items live at the bottom of the hierarchy. Every issue belongs t
661
702
  ### Hierarchy in Practice
662
703
 
663
704
  ```
664
- Initiative: Northwind
665
- └── Project: [NW] Appointment Scheduling
666
- ├── Milestone: Providers Module
667
- │ ├── ACME-101: Create providers list page
668
- │ ├── ACME-102: Add provider create/edit form
669
- │ └── ACME-103: Provider deactivation support
670
- ├── Milestone: Booking Requests
671
- │ ├── ACME-110: Request creation form
672
- │ └── ACME-111: Accept/reject API endpoints
673
- └── Milestone: Notifications
674
- └── ACME-120: Set up email service
705
+ Initiative: <Client> — Phase 1 ← one signed proposal
706
+ ├── Project: [KEY] M1 — Scheduling ← proposal milestone 1
707
+ │ ├── Milestone: Providers Module
708
+ │ │ ├── KEY-101: Create providers list page
709
+ │ │ ├── KEY-102: Add provider create/edit form
710
+ │ │ └── KEY-103: Provider deactivation support
711
+ │ ├── Milestone: Booking Requests
712
+ │ │ ├── KEY-110: Request creation form
713
+ │ │ └── KEY-111: Accept/reject API endpoints
714
+ │ └── Milestone: Notifications
715
+ │ └── KEY-120: Set up email service
716
+ ├── Project: [KEY] M2 — Billing ← proposal milestone 2
717
+ └── Project: [KEY] R1 — SSO Integration ← out-of-scope revision
675
718
  ```
676
719
 
720
+ The project row is fixed by the proposal (`M1`, `M2`) plus whatever revisions have
721
+ been agreed (`R1`). New requests land as **issues in a milestone** inside an
722
+ existing project — the project row only grows when a revision is confirmed.
723
+
677
724
  ### Guidelines for the Team
678
725
 
679
726
  1. **Every issue must be placed into a cycle with Todo status.** **Do NOT default to the current/active cycle.** Follow this procedure: (a) Run `cycle-capacity` to see each cycle's capacity % (velocity-based, from last 3 completed cycles). (b) Starting from the earliest (current) cycle, find the first cycle that is **strictly under 100%** capacity. (c) If the current cycle is at or above 100%, **skip it** and use the next cycle with room. Assign the issue there via `--cycle`. **Always set `--state todo`** — issues in Backlog don't work with cycles. **Exception:** High priority or above (priority ≤ 2: Urgent, High) always go into the current active cycle regardless of capacity.
680
727
  2. **Every issue must belong to a project and a milestone.** Never create orphan issues and never leave an issue outside a milestone.
681
- 3. **If the correct project does not exist, create it before creating the issue.** Do not park work in a generic team backlog while waiting to organize it later.
728
+ 3. **Place the issue in an existing project — never invent one.** The initiative's `M` projects are the signed proposal's milestones; find the one the work falls under. A new project is correct *only* for work the requester confirmed is out of scope, and then only as the next `[KEY] R<n> — <Name>` revision project (see "Never invent a project"). Don't park work in a generic team backlog either — if you truly cannot place it, say so rather than manufacturing a home for it.
682
729
  4. **If the correct milestone does not exist, create it before creating the issue.** Milestone creation is part of issue intake, not optional cleanup. **Never add an issue to a completed milestone** — only open milestones may receive new issues. If no open milestone matches the issue, create a new one; do not reuse a completed one.
683
730
  5. **Use milestones for sequencing.** Milestones can have target dates, making them useful for communicating delivery phases to clients.
684
731
  6. **Track progress in Linear.** After creating/updating projects or milestones, update the initiative's content in Linear to reflect the current structure (see "Post-Organization: Update Initiative in Linear" below).
@@ -693,10 +740,12 @@ node linear.mjs list-projects
693
740
  # List milestones within a project
694
741
  node linear.mjs list-milestones "$PROJECT_ID"
695
742
 
696
- # If needed, create the missing project or milestone before creating the issue
697
- PROJECT_ID="$(node linear.mjs create-project --name "[CLIENT] Feature Area" --description "Short description" | node -e "process.stdin.once('data',d=>console.log(JSON.parse(d).data.projectCreate.project.id))")"
743
+ # If the milestone is missing, create it inside the EXISTING M project (never a new project)
698
744
  MILESTONE_ID="$(node linear.mjs create-milestone "$PROJECT_ID" "Phase 1" | node -e "process.stdin.once('data',d=>console.log(JSON.parse(d).data.projectMilestoneCreate.projectMilestone.id))")"
699
745
 
746
+ # Creating a project is only for a CONFIRMED out-of-scope revision — next R<n>, same initiative
747
+ node linear.mjs create-project --name "[KEY] R1 — Revision Name" --initiative "$INITIATIVE_ID" --description "Short description"
748
+
700
749
  # Create an issue within a project and milestone (with cycle)
701
750
  node linear.mjs create-issue \
702
751
  --title 'Add provider create form' \
@@ -146,3 +146,9 @@ The model's prior is mostly Zod 3 — these are the idioms that changed. Get the
146
146
  - **Format errors with the built-ins**, not `zod-validation-error`: `z.prettifyError()` (human string), `z.treeifyError()` (nested, replaces deprecated `.format()`), `z.flattenError()` (replaces deprecated `.flatten()`).
147
147
  - **`.default()` applies to the *output* type** and short-circuits parsing when input is `undefined`. For the old "run the default through the schema" behavior, use `.prefault()`.
148
148
  - **`z.coerce.*` input type is now `unknown`** (not the output type) — fine for h3 query/body parsing, but affects schemas you consume elsewhere.
149
+ - **`.default()` fires even under `.optional()` (and through `.partial()`)** — `someDefaulted.optional()` still emits the default for a missing key, so a defaulted field can never signal "not provided". The classic trap is deriving a PATCH body from a create schema: `CreateSchema.partial()` silently fills every defaulted field, so `body.someField !== undefined` is always true and "was this field sent?" logic (partial updates, tree-replace triggers) misfires on every request. For PATCH schemas, compose from bare **undefaulted** field schemas and add `.optional()` per field:
150
+ ```typescript
151
+ const itemsSchema = z.array(ItemSchema); // no .default([]) here
152
+ const createSchema = z.object({ items: itemsSchema.default([]) });
153
+ const patchSchema = z.object({ items: itemsSchema.optional() }); // undefined ⇢ "not sent"
154
+ ```
@@ -13,10 +13,18 @@ but your custom `@theme` tokens fail with *"Cannot apply unknown utility class."
13
13
 
14
14
  ```css
15
15
  /* ❌ @reference "tailwindcss"; → @apply text-fg fails */
16
- @reference "../assets/css/main.css"; /* ✅ exposes YOUR tokens; path is relative to the file */
16
+ @reference "~/assets/css/main.css"; /* ✅ exposes YOUR tokens */
17
17
  .prose :where(h2) { @apply text-fg; }
18
18
  ```
19
19
 
20
+ Prefer the bundler alias (`~/` in Nuxt, or your Vite alias) over a relative
21
+ path — `@tailwindcss/vite` resolves aliases in `@reference` fine, and the alias
22
+ survives file moves. A relative path (`"../assets/css/main.css"`) is depth-
23
+ sensitive: move the file a directory deeper (e.g. a route-namespace refactor)
24
+ and the production build fails with
25
+ `[@tailwindcss/vite:generate:build] Can't resolve '../assets/css/main.css'` —
26
+ typecheck, tests, and dev can all stay green; only the real build catches it.
27
+
20
28
  Better still in scoped styles: skip `@apply` and consume the generated CSS
21
29
  variables directly — `color: var(--color-fg)` always works with no `@reference`.
22
30