@gallopsystems/agent-skills 1.10.0 → 1.12.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/README.md +15 -7
- package/package.json +2 -2
- package/plugins/contribute-skill/.claude-plugin/plugin.json +8 -0
- package/{commands/contribute-skill.md → plugins/contribute-skill/skills/contribute-skill/SKILL.md} +2 -1
- package/plugins/git-github/skills/git-github/SKILL.md +1 -0
- package/plugins/nitro-testing/skills/nitro-testing/frontend-testing.md +40 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/SKILL.md +5 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/vueuse.md +13 -5
- package/plugins/vue-nuxt/skills/vue-nuxt/watch.md +6 -6
- package/scripts/link-skills.mjs +27 -11
package/README.md
CHANGED
|
@@ -44,10 +44,17 @@ yarn add -D @gallopsystems/agent-skills
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
On install, a `postinstall` script symlinks the package's content into the
|
|
47
|
-
project
|
|
47
|
+
project, for both Claude Code and Codex:
|
|
48
48
|
|
|
49
49
|
- **skills** — each directory containing a `SKILL.md` → `.claude/skills/<name>`
|
|
50
|
-
|
|
50
|
+
(Claude Code) **and** `.agents/skills/<name>` (Codex). The `SKILL.md` format is
|
|
51
|
+
identical for both agents, so each skill dir is linked verbatim. Everything ships
|
|
52
|
+
as a skill: in current Claude Code a skill is both slash-invocable (`/<name>`) and
|
|
53
|
+
model-invocable, so `contribute-skill`, for example, gives you `/contribute-skill`
|
|
54
|
+
*and* gets auto-offered when relevant — no separate command needed.
|
|
55
|
+
- **commands** — each `.md` file under any `commands/` directory →
|
|
56
|
+
`.claude/commands/<name>.md` (Claude only; Codex has no project command dir). Kept
|
|
57
|
+
for legacy/Claude-only commands; this package ships none today.
|
|
51
58
|
|
|
52
59
|
Updating is just a version bump:
|
|
53
60
|
|
|
@@ -61,12 +68,13 @@ recreated on every install), so ignore them in `.gitignore`:
|
|
|
61
68
|
```
|
|
62
69
|
.claude/skills
|
|
63
70
|
.claude/commands
|
|
71
|
+
.agents/skills
|
|
64
72
|
```
|
|
65
73
|
|
|
66
74
|
Notes:
|
|
67
|
-
- The script never clobbers a real `.claude/skills/<name
|
|
68
|
-
you authored, and only removes symlinks it created.
|
|
69
|
-
remove all managed links.
|
|
75
|
+
- The script never clobbers a real `.claude/skills/<name>`, `.agents/skills/<name>`,
|
|
76
|
+
or `.claude/commands/<name>` you authored, and only removes symlinks it created.
|
|
77
|
+
Run `yarn unlink-skills` to remove all managed links.
|
|
70
78
|
- Works out of the box with Yarn (Classic, or Berry with `nodeLinker: node-modules`)
|
|
71
79
|
and npm. **pnpm** (v10+) blocks dependency build scripts by default — add the
|
|
72
80
|
package to `pnpm.onlyBuiltDependencies` for the `postinstall` to run.
|
|
@@ -163,8 +171,8 @@ Covers:
|
|
|
163
171
|
|
|
164
172
|
Every skill ends with a **Contributing Back** section: when Claude works through
|
|
165
173
|
something the skill didn't cover, it offers to contribute the lesson upstream. The
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
`contribute-skill` skill (run `/contribute-skill` on Claude Code, or let either agent
|
|
175
|
+
invoke it when relevant) automates the flow: distill the generic lesson,
|
|
168
176
|
privacy-sweep it, clone or fork this repo, and open a PR against the right skill
|
|
169
177
|
file. PRs from forks are welcome — content must be generic (placeholders only, no
|
|
170
178
|
project-specific names, IDs, or domains).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gallopsystems/agent-skills",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Gallop Systems
|
|
3
|
+
"version": "1.12.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "contribute-skill",
|
|
3
|
+
"description": "Contribute a lesson learned this session back to the gallop-systems/agent-skills repo as a PR: distill the generic rule, privacy-sweep it, and open a PR against the right skill.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "yeedle"
|
|
7
|
+
}
|
|
8
|
+
}
|
package/{commands/contribute-skill.md → plugins/contribute-skill/skills/contribute-skill/SKILL.md}
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
name: contribute-skill
|
|
3
|
+
description: Contribute a lesson learned this session back to the gallop-systems/agent-skills repo as a PR. Use when capturing an error→fix, a behavior that contradicted a skill, or a workflow gap the skill didn't cover, and turning it into a generic, public-safe addition to an installed skill.
|
|
3
4
|
argument-hint: [which skill and/or what lesson]
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -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
|
|
|
@@ -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*;
|
|
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).
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
73
|
-
the right tool only when the URL can't model the
|
|
74
|
-
"empty" states** (e.g. a filter that defaults to
|
|
75
|
-
`null` — an absent param can map to only one of
|
|
76
|
-
fanning out to many params** (a PrimeVue filter
|
|
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 —
|
package/scripts/link-skills.mjs
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Postinstall: symlink this package's skills and commands into the consuming
|
|
3
|
-
// project
|
|
4
|
-
//
|
|
2
|
+
// Postinstall: symlink this package's skills (and any commands) into the consuming
|
|
3
|
+
// project, so a `yarn add` / `yarn install` keeps the project in sync with this
|
|
4
|
+
// package's version:
|
|
5
5
|
//
|
|
6
|
-
// skills -> .claude/skills/<name> (a dir
|
|
7
|
-
//
|
|
6
|
+
// skills -> .claude/skills/<name> (Claude Code; a dir with SKILL.md)
|
|
7
|
+
// skills -> .agents/skills/<name> (Codex; same SKILL.md format)
|
|
8
|
+
// commands -> .claude/commands/<rel>.md (Claude Code only; a .md under commands/)
|
|
9
|
+
//
|
|
10
|
+
// The SKILL.md format (name + description frontmatter, no nesting) is identical
|
|
11
|
+
// across Claude and Codex, so skill dirs are linked verbatim into both roots. In
|
|
12
|
+
// current Claude Code, custom commands were merged into skills: a skill is itself
|
|
13
|
+
// slash-invocable (`/<name>`) AND model-invocable, so it fully covers what a command
|
|
14
|
+
// did. We therefore ship workflows as skills, not commands. The commands/ path is
|
|
15
|
+
// still honored for any legacy Claude-only command, but Codex has no project command
|
|
16
|
+
// dir, so commands are never linked there.
|
|
8
17
|
//
|
|
9
18
|
// This runs during `yarn install`, so it MUST NOT throw — a thrown error aborts
|
|
10
19
|
// the whole install. Every failure path here degrades to a warning + exit 0.
|
|
@@ -229,20 +238,27 @@ function main() {
|
|
|
229
238
|
return
|
|
230
239
|
}
|
|
231
240
|
|
|
232
|
-
const
|
|
233
|
-
const
|
|
241
|
+
const claudeSkillsRoot = path.join(projectRoot, '.claude', 'skills') // Claude Code
|
|
242
|
+
const codexSkillsRoot = path.join(projectRoot, '.agents', 'skills') // Codex
|
|
243
|
+
const commandsRoot = path.join(projectRoot, '.claude', 'commands') // Claude Code only
|
|
234
244
|
|
|
235
245
|
if (UNLINK) {
|
|
236
|
-
for (const root of [
|
|
246
|
+
for (const root of [claudeSkillsRoot, codexSkillsRoot, commandsRoot]) {
|
|
237
247
|
if (fs.existsSync(root)) removeStaleLinks(realOrSelf(root), new Set())
|
|
238
248
|
}
|
|
239
|
-
log('removed managed symlinks from .claude/
|
|
249
|
+
log('removed managed symlinks from .claude/ and .agents/.')
|
|
240
250
|
return
|
|
241
251
|
}
|
|
242
252
|
|
|
243
|
-
const
|
|
253
|
+
const skillSources = collectSkills(PKG_DIR)
|
|
254
|
+
const skills = linkInto(claudeSkillsRoot, skillSources, 'skills')
|
|
255
|
+
linkInto(codexSkillsRoot, skillSources, 'skills')
|
|
244
256
|
const commands = linkInto(commandsRoot, collectCommands(PKG_DIR), 'commands')
|
|
245
|
-
log(
|
|
257
|
+
log(
|
|
258
|
+
`linked ${skills} skill${skills === 1 ? '' : 's'} (.claude + .agents)` +
|
|
259
|
+
(commands ? ` and ${commands} command${commands === 1 ? '' : 's'} (.claude)` : '') +
|
|
260
|
+
'.',
|
|
261
|
+
)
|
|
246
262
|
}
|
|
247
263
|
|
|
248
264
|
try {
|