@gallopsystems/agent-skills 1.11.0 → 1.13.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.11.0",
3
+ "version": "1.13.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": {
@@ -10,7 +10,9 @@ doctl databases ca <db-id> # cluster CA certi
10
10
 
11
11
  - **The connection URI contains live credentials** — treat command output as a secret. Don't echo it into logs; pipe it directly to where it's needed.
12
12
  - **Check `Version`** and keep CI/local database versions in sync with production — a test suite running `postgres:15` against a pg-18 production cluster hides version-specific behavior.
13
- - Connections use port 25060 with `sslmode=require`. **TLS trap**: some clients (e.g. newer `pg-connection-string`) silently upgrade `require` to `verify-full`, which rejects DO's CA under the default trust store. Fix: supply the CA from `doctl databases ca <db-id>` explicitly, or configure ssl options in code rather than relying on the URI.
13
+ - Connections use port 25060 with `sslmode=require`. **TLS trap**: some clients (e.g. `pg-connection-string` as bundled with `pg` >= 8.16) silently treat `require` as `verify-full`, which rejects DO's self-signed CA under the default trust store (`SELF_SIGNED_CERT_IN_CHAIN`). Two non-obvious parts:
14
+ - **An ssl option set in code does NOT override an `sslmode` already in the URI.** Passing `ssl: { rejectUnauthorized: false }` while the connection string still ends in `?sslmode=require` keeps failing — the URI's `sslmode` wins. The fix has to land in the URI itself: drop/replace `sslmode`, append `uselibpqcompat=true` (restores libpq semantics: encrypt but don't verify), or supply the CA from `doctl databases ca <db-id>` explicitly.
15
+ - **When you can't edit the URI** — specifically, when you bind App Platform's generated `${db.DATABASE_URL}` straight into an env var in the app spec, it always carries `?sslmode=require` and you don't author the string — append the param to the bound value: `value: ${db.DATABASE_URL}&uselibpqcompat=true`. This only applies to that bound-pipethrough case; if you declare the connection-string env var yourself, just put the right params (or none) in from the start and an in-code ssl option is enough.
14
16
 
15
17
  ## Spaces
16
18
 
@@ -51,6 +51,7 @@ EOF
51
51
  - After merge: `git switch main && git pull --ff-only`, clean up `[gone]` branches, start the next branch from fresh main.
52
52
  - One concern per PR — hotfixes and review findings go in separate PRs unless told otherwise.
53
53
  - Stacked PRs: `gh pr create --base <parent-branch>`; after the parent merges, retarget with `gh pr edit <n> --base main` (and see [getting-unstuck.md](getting-unstuck.md) for rebasing onto main after the parent was squash-merged).
54
+ - If you discover uncommitted work on the wrong branch and the PR must be "off main", do not commit to the wrong branch. With a cleanly applicable worktree, `git fetch origin main && git switch -c feat/<short-description> origin/main` carries the unstaged changes onto a new branch from `origin/main`. Verify with `git status` and tests. If checkout would overwrite/conflict, stash with `-u`. Only resort to worktree if stash gets too complicated.
54
55
 
55
56
  ## Reading PR and CI State
56
57
 
@@ -964,6 +964,53 @@ await db.schema.createIndex("idx_order_user_id").on("order").column("user_id").e
964
964
  .addColumn("price", sql`numeric(10, 2)`)
965
965
  ```
966
966
 
967
+ ### Migration Ordering Is Append-Only — Out-of-Order Timestamps Break Prod Deploys
968
+
969
+ Kysely's migrator enforces a **strict append-only ledger**: it refuses to run any
970
+ unexecuted migration whose timestamp sorts *before* the last-executed one
971
+ (throwing `Corrupted migrations: previously executed migration ... is missing`).
972
+ This bites when two branches each add a migration, and the one that merges *second*
973
+ carries the *earlier* timestamp:
974
+
975
+ ```
976
+ branch A merges first → 1700000200000_drop_thing (runs in prod)
977
+ branch B merges second → 1700000100000_add_table (timestamp is EARLIER)
978
+ → 1700000100001_add_column
979
+ ```
980
+
981
+ Prod has already recorded `...200000_drop_thing` as executed. On the next deploy
982
+ the `migrate` job sees two pending migrations that sort *before* it, throws
983
+ "Corrupted migrations", and **exits non-zero**. If migrations run as a pre-deploy
984
+ job (common on PaaS like DigitalOcean App Platform), the failed job fails the whole
985
+ deploy and the platform **auto-rolls-back to the previous image** — so prod silently
986
+ stays on stale code and every subsequent deploy fails the same way. A self-reinforcing
987
+ loop that looks like a deploy/token problem but is really a migration-ledger problem.
988
+
989
+ **Prevent it:** before merging a long-lived branch, check whether `main` has merged
990
+ any migration with a *later* timestamp than yours. If so, regenerate your migration's
991
+ timestamp so it sorts last (`migrate:make` again, or rename the file) **before it
992
+ merges** — only safe while the migration has not yet run in any shared DB. Never
993
+ re-stamp a migration that prod has already executed; that forces it to re-run.
994
+
995
+ **Fix it once prod is wedged:** reconcile the ledger so the executed set is a clean
996
+ *prefix* again, then let the normal strict migrate job run. Do **not** reach for
997
+ `allowUnorderedMigrations: true` — it works (kysely-ctl spreads the `migrations`
998
+ config into the `Migrator`), but it permanently weakens the ordering guard to paper
999
+ over one bad state. Instead, surgically remove the prematurely-recorded row from the
1000
+ migration ledger table (default `kysely_migration`):
1001
+
1002
+ ```sql
1003
+ -- prod ledger has the later-timestamp migration recorded, blocking the two earlier ones
1004
+ DELETE FROM kysely_migration WHERE name = '1700000200000_drop_thing';
1005
+ ```
1006
+
1007
+ Now `add_table → add_column → drop_thing` are all pending in true timestamp order, and
1008
+ the next deploy's strict migrate job applies them cleanly. This only works when the
1009
+ removed migration is **idempotent to re-run** (e.g. a `dropTable`/`dropColumn` written
1010
+ with `ifExists`, so re-applying it after the others is a safe no-op). Verify the
1011
+ ledger is a contiguous prefix after the DELETE, and prefer letting the deploy's own
1012
+ migrate job re-apply rather than running migrations from a laptop against prod.
1013
+
967
1014
  ## Type Generation
968
1015
 
969
1016
  Use `kysely-codegen` to generate types from your database:
@@ -526,6 +526,19 @@ await flushPromises();
526
526
  expect(replace.mock.calls.some((c) => (c[0] as any)?.query?.q === "foo")).toBe(true);
527
527
  ```
528
528
 
529
+ If the component's source of truth is `useRoute()`/`useRouter()`, `route:` is not
530
+ always the most direct test seam: app middleware, route rules, and redirects still
531
+ run. For component-level behavior, mock the Nuxt imports before mounting:
532
+
533
+ ```typescript
534
+ mockNuxtImport("useRoute", () => () => ({ query: { tab: "activity" }, params: {} }));
535
+ mockNuxtImport("useRouter", () => () => ({ replace: vi.fn(), push: vi.fn() }));
536
+ ```
537
+
538
+ If the component imports router helpers directly from `vue-router`, mock that
539
+ module too. Keep the test focused on what the component reads or writes; use an
540
+ end-to-end/page test when the middleware behavior itself is under test.
541
+
529
542
  ### 9. Testing a composable that needs a component scope
530
543
 
531
544
  For a composable using lifecycle hooks or `provide`/`inject`, mount a throwaway
@@ -541,6 +554,12 @@ Control a VueUse dependency with `vi.mock("@vueuse/core", …)` to return refs y
541
554
  drive. And make sure the vitest `include` glob covers `app/composables/**` — a
542
555
  composables dir is easy to leave out of the frontend config.
543
556
 
557
+ Do this for lifecycle composables even when the return value looks plain:
558
+ `onMounted`, `onUnmounted`, `useEventListener`, `useScrollLock`, and similar
559
+ helpers need a component effect scope to attach and clean up correctly. Calling
560
+ the composable directly in a Vitest test can produce Vue warnings and, worse,
561
+ skip the listener or cleanup path you meant to verify.
562
+
544
563
  ### 10. Reset the shared `useFetch` cache between mounts
545
564
 
546
565
  `useFetch`/`useAsyncData` cache by key, and the cache is **shared across
@@ -553,6 +572,27 @@ import { clearNuxtData } from "#imports"; // not exported from @nuxt/test-utils/
553
572
  beforeEach(() => clearNuxtData());
554
573
  ```
555
574
 
575
+ ### 11. Mock Nuxt fetch behavior at the right layer
576
+
577
+ `mockNuxtImport("$fetch", ...)` is not a reliable target: `$fetch` is not a normal
578
+ Nuxt auto-import in the same way `useRoute` or a composable is, so the transform
579
+ may fail before the test even runs. Prefer `registerEndpoint` when the component
580
+ calls `useFetch`, `useAsyncData`, or `$fetch` against an app route:
581
+
582
+ ```typescript
583
+ registerEndpoint("/api/search", {
584
+ method: "GET",
585
+ handler: (event) => {
586
+ const q = new URL(event.node.req.url!, "http://localhost").searchParams.get("q");
587
+ return [{ id: 1, name: q ?? "" }];
588
+ },
589
+ });
590
+ ```
591
+
592
+ If the component calls a local service/composable that wraps `$fetch`, mock that
593
+ service/composable instead. Mock the boundary you own; use `registerEndpoint` for
594
+ Nuxt's fetch path.
595
+
556
596
  ## File Organization
557
597
 
558
598
  Co-locate tests with source files:
@@ -39,6 +39,11 @@ cross-links to those rather than restating them.
39
39
  - [page-structure.md](./page-structure.md) — keep pages thin: route-param parsing + layout in the page, data/logic/forms in components
40
40
  - [formatters.md](./formatters.md) — never inline a currency/date/number formatter; centralize in `useFormatters`, prefer Intl/date-fns
41
41
 
42
+ Testing note: when a Vue/Nuxt refactor changes component behavior, route/query
43
+ state, or a composable with lifecycle hooks, use the Nuxt frontend testing
44
+ guidance in `nitro-testing`'s [frontend-testing.md](../../../../nitro-testing/skills/nitro-testing/frontend-testing.md)
45
+ instead of testing those pieces as plain Vue functions.
46
+
42
47
  ## Core Principles
43
48
 
44
49
  1. **Lean on auto-imports.** `app/components`, `app/composables`, `app/utils`, and the Vue/Nuxt APIs all auto-import. Add an explicit `import` only for third-party symbols and TS types. A nested component's tag carries its directory as a prefix (`components/customers/ProfileCard.vue` → `<CustomersProfileCard>`).
@@ -24,6 +24,9 @@ export default defineNuxtConfig({ modules: ['@vueuse/nuxt'] })
24
24
 
25
25
  The **`@vueuse/router`** and **`@vueuse/integrations`** add-ons are separate
26
26
  installs and are **not** auto-imported by the module — import them explicitly.
27
+ For URL query sync, install `@vueuse/router` and use its router composables before
28
+ hand-rolling `useRoute()`/`useRouter()` glue; the package exists exactly for that
29
+ boundary.
27
30
 
28
31
  ## The boundary cases (mapped from `watch.md`)
29
32
 
@@ -42,8 +45,9 @@ installs and are **not** auto-imported by the module — import them explicitly.
42
45
 
43
46
  URL sync (`router.replace({ query: { ...route.query, tab } })`) →
44
47
  `useRouteQuery('tab')` from `@vueuse/router` gives a ref bound two-way to the query
45
- param. Nuxt's own `useRoute()` is already reactive for *reads*; reach for
46
- `useRouteQuery` when you want a **writable** ref bound to a single param.
48
+ param. Nuxt's own `useRoute()` is already reactive for *reads*; install and import
49
+ `useRouteQuery` when you want a **writable** ref bound to a single param instead of
50
+ open-coding the same replace/query merge logic.
47
51
 
48
52
  ### `useLocalStorage` reads at setup — guard SSR hydration
49
53
 
@@ -67,9 +71,13 @@ Because the bound ref reads from and writes to the query param, the URL *is* the
67
71
  state — so it **can't represent a ref with two distinct "empty" states** (e.g. a
68
72
  filter that defaults to `"Open"` on load but clears to `null`; both would be "param
69
73
  absent"). When you need that distinction, keep a plain `ref` + a projecting `watch`
70
- (a sanctioned `watch.md` "URL sync" case). And **don't mix `useUrlSearchParams`
71
- (History API) with `useRouteQuery` (vue-router) in the same component** — they write
72
- the URL through different mechanisms and clobber each other's params; pick one.
74
+ (a sanctioned `watch.md` "URL sync" case). The same exception applies when one
75
+ source object fans out to several query params and the projection itself is the
76
+ behavior being tested. Otherwise, adding `@vueuse/router` is preferable to writing
77
+ your own route-query synchronization. And **don't mix `useUrlSearchParams`
78
+ (History API) with `useRouteQuery` (vue-router) in the same component** — they
79
+ write the URL through different mechanisms and clobber each other's params; pick
80
+ one.
73
81
 
74
82
  ## VueUse's `watch` sugar — when a `watch` IS warranted
75
83
 
@@ -69,12 +69,12 @@ Only when the effect crosses **out of** the reactive graph:
69
69
  - **Persist** — a `localStorage`/`useCookie` write, debounced auto-save of a
70
70
  deep-watched form.
71
71
  - **URL sync** — `router.replace({ query: { ...route.query, tab } })`. For a single
72
- ref ↔ one query param, prefer `useRouteQuery` (see below). A write-back watch is
73
- the right tool only when the URL can't model the state: a ref with **two distinct
74
- "empty" states** (e.g. a filter that defaults to `"Open"` on load but clears to
75
- `null` — an absent param can map to only one of them), or a **composite object
76
- fanning out to many params** (a PrimeVue filter object) that a one-ref-per-param
77
- `useRouteQuery` can't express.
72
+ ref ↔ one query param, install `@vueuse/router` and prefer `useRouteQuery` (see
73
+ below). A write-back watch is the right tool only when the URL can't model the
74
+ state: a ref with **two distinct "empty" states** (e.g. a filter that defaults to
75
+ `"Open"` on load but clears to `null` — an absent param can map to only one of
76
+ them), or a **composite object fanning out to many params** (a PrimeVue filter
77
+ object) that a one-ref-per-param `useRouteQuery` can't express.
78
78
  - **Re-seed local state on dialog open** — `watch(visible, (v) => { if (v) initForm() })`.
79
79
  The single most common legit pattern. Its one-liner is `whenever(visible, initForm)`
80
80
  (see `vueuse.md`); a compound guard keeps its inner half —