@gallopsystems/agent-skills 1.9.0 → 1.11.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/nitro-testing/skills/nitro-testing/frontend-testing.md +71 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/vueuse.md +26 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/watch.md +27 -2
- 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.11.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
|
|
|
@@ -482,6 +482,77 @@ for (const tz of timezones) {
|
|
|
482
482
|
}
|
|
483
483
|
```
|
|
484
484
|
|
|
485
|
+
### 7. Reading or driving a stubbed child needs an explicit stub
|
|
486
|
+
|
|
487
|
+
`Stub: true` (auto-stub) renders the child but **does not expose its props** to
|
|
488
|
+
`findComponent(Stub).props("x")`. To read or drive a child's `modelValue`, give it
|
|
489
|
+
an explicit stub that declares the prop:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const SelectStub = {
|
|
493
|
+
name: "Select",
|
|
494
|
+
props: ["modelValue"],
|
|
495
|
+
emits: ["update:modelValue"],
|
|
496
|
+
template: "<div />",
|
|
497
|
+
};
|
|
498
|
+
// findComponent(SelectStub).props("modelValue") — now readable
|
|
499
|
+
// findComponent(SelectStub).vm.$emit("update:modelValue", x) — drives the v-model
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
For a deep tree (a dialog full of fields), `shallow: true` auto-stubs everything;
|
|
503
|
+
add explicit stubs only for the parts you assert on — plus a dialog stub that
|
|
504
|
+
renders its slot, since UI-lib dialogs gate content behind a `visible` prop:
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
const DialogStub = { props: ["visible"], template: "<div v-if='visible'><slot /><slot name='footer' /></div>" };
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### 8. Driving and asserting route/query state
|
|
511
|
+
|
|
512
|
+
A global route middleware that redirects unauthenticated navigations (to a public
|
|
513
|
+
`/login`) means `mountSuspended(Comp, { route: "/private?foo=1" })` lands on the
|
|
514
|
+
public path and **drops the query** — so a `useRouteQuery`-bound ref reads empty.
|
|
515
|
+
Two ways around it:
|
|
516
|
+
|
|
517
|
+
- **Carry the params on the public route:** `route: "/login?foo=1"`. `useRouteQuery`
|
|
518
|
+
binds to `route.query` regardless of the path.
|
|
519
|
+
- **Assert URL *writes* by spying on `router.replace`**, not by reading
|
|
520
|
+
`currentRoute` (the redirect strips what you'd read back):
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
const replace = vi.spyOn((wrapper.vm as any).$router, "replace");
|
|
524
|
+
input.vm.$emit("update:modelValue", "foo");
|
|
525
|
+
await flushPromises();
|
|
526
|
+
expect(replace.mock.calls.some((c) => (c[0] as any)?.query?.q === "foo")).toBe(true);
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### 9. Testing a composable that needs a component scope
|
|
530
|
+
|
|
531
|
+
For a composable using lifecycle hooks or `provide`/`inject`, mount a throwaway
|
|
532
|
+
component whose `setup()` calls it and capture the return:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
let api: ReturnType<typeof useThing>;
|
|
536
|
+
const Comp = defineComponent({ setup() { api = useThing(); return () => h("div"); } });
|
|
537
|
+
await mountSuspended(Comp);
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Control a VueUse dependency with `vi.mock("@vueuse/core", …)` to return refs you
|
|
541
|
+
drive. And make sure the vitest `include` glob covers `app/composables/**` — a
|
|
542
|
+
composables dir is easy to leave out of the frontend config.
|
|
543
|
+
|
|
544
|
+
### 10. Reset the shared `useFetch` cache between mounts
|
|
545
|
+
|
|
546
|
+
`useFetch`/`useAsyncData` cache by key, and the cache is **shared across
|
|
547
|
+
`mountSuspended` calls in a file**. When several tests mount the same component
|
|
548
|
+
(fixed URL) but `registerEndpoint` returns different data per test, the second
|
|
549
|
+
test sees the first's response unless you clear it:
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
import { clearNuxtData } from "#imports"; // not exported from @nuxt/test-utils/runtime
|
|
553
|
+
beforeEach(() => clearNuxtData());
|
|
554
|
+
```
|
|
555
|
+
|
|
485
556
|
## File Organization
|
|
486
557
|
|
|
487
558
|
Co-locate tests with source files:
|
|
@@ -45,6 +45,32 @@ URL sync (`router.replace({ query: { ...route.query, tab } })`) →
|
|
|
45
45
|
param. Nuxt's own `useRoute()` is already reactive for *reads*; reach for
|
|
46
46
|
`useRouteQuery` when you want a **writable** ref bound to a single param.
|
|
47
47
|
|
|
48
|
+
### `useLocalStorage` reads at setup — guard SSR hydration
|
|
49
|
+
|
|
50
|
+
`useLocalStorage`/`useStorage` is SSR-safe: on the server there's no `window`, so
|
|
51
|
+
it returns the default and never touches storage (no `import.meta.client` guard
|
|
52
|
+
needed — that guard was only for raw `localStorage.*` calls, which throw on the
|
|
53
|
+
server). But on the client it reads storage **synchronously at setup**, so a value
|
|
54
|
+
rendered *without a mount gate* differs between the server render (the default) and
|
|
55
|
+
the hydrating client render (the stored value) → a hydration mismatch. Pass
|
|
56
|
+
`{ initOnMounted: true }` to defer the read to `onMounted` so the first client
|
|
57
|
+
render matches the server, or gate the rendering until mounted.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// shown straight away (no v-if gate) → defer the storage read to onMounted
|
|
61
|
+
const view = useLocalStorage('view', 'list', { initOnMounted: true })
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `useRouteQuery` makes the URL the single source of truth
|
|
65
|
+
|
|
66
|
+
Because the bound ref reads from and writes to the query param, the URL *is* the
|
|
67
|
+
state — so it **can't represent a ref with two distinct "empty" states** (e.g. a
|
|
68
|
+
filter that defaults to `"Open"` on load but clears to `null`; both would be "param
|
|
69
|
+
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.
|
|
73
|
+
|
|
48
74
|
## VueUse's `watch` sugar — when a `watch` IS warranted
|
|
49
75
|
|
|
50
76
|
When the effect genuinely belongs in a watcher, VueUse's Watch category removes the
|
|
@@ -68,9 +68,18 @@ Only when the effect crosses **out of** the reactive graph:
|
|
|
68
68
|
polling `setInterval` (clear it in `onUnmounted`).
|
|
69
69
|
- **Persist** — a `localStorage`/`useCookie` write, debounced auto-save of a
|
|
70
70
|
deep-watched form.
|
|
71
|
-
- **URL sync** — `router.replace({ query: { ...route.query, tab } })`.
|
|
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
78
|
- **Re-seed local state on dialog open** — `watch(visible, (v) => { if (v) initForm() })`.
|
|
73
|
-
The single most common legit pattern.
|
|
79
|
+
The single most common legit pattern. Its one-liner is `whenever(visible, initForm)`
|
|
80
|
+
(see `vueuse.md`); a compound guard keeps its inner half —
|
|
81
|
+
`watch(visible, (v) => { if (v && !props.x) reset() })` →
|
|
82
|
+
`whenever(visible, () => { if (!props.x) reset() })`.
|
|
74
83
|
- **Clone a server prop into a locally-editable draft** —
|
|
75
84
|
`watch(() => props.record, (r) => { if (r) form.value = structuredClone(toRaw(r)) }, { immediate: true })`.
|
|
76
85
|
|
|
@@ -130,6 +139,22 @@ watch(activeId, () => scrollActiveIntoView(), { flush: 'post' }) // DOM already
|
|
|
130
139
|
| **side-effect-in-handler** | watching a value that only changes via one control, to clear a dependent field | that control's `@update:model-value` handler |
|
|
131
140
|
| **manual-refetch** | watching filter refs to call a function that calls `useFetch` | a `computed` `query` passed to `useFetch` |
|
|
132
141
|
|
|
142
|
+
**An external source doesn't legitimize a deriving watch.** The
|
|
143
|
+
watch-vs-`computed` decision turns on whether the *body* leaves the reactive
|
|
144
|
+
graph — **not** on whether the *source* came from outside it. A watcher whose
|
|
145
|
+
source is a composable/library ref (`useEventSource`'s `status`, a store getter,
|
|
146
|
+
a `useWindowSize`) but whose body only assigns a derived value is still a
|
|
147
|
+
`computed`. "The source is external state" is the most common excuse for keeping
|
|
148
|
+
such a watch, and it's wrong — it's still derivation.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// ❌ a library ref as source tempts a watch, but the body only derives
|
|
152
|
+
const isConnected = ref(false)
|
|
153
|
+
watch(status, (s) => { isConnected.value = s === 'OPEN' }) // status from useEventSource
|
|
154
|
+
// ✅ derivation is a computed, regardless of where status came from
|
|
155
|
+
const isConnected = computed(() => status.value === 'OPEN')
|
|
156
|
+
```
|
|
157
|
+
|
|
133
158
|
The biggest real cluster was **side-effect-in-handler** — resetting dependent
|
|
134
159
|
fields when a Select changed. In a watcher it hides cause/effect and re-fires on
|
|
135
160
|
programmatic form reseeds; the colocated handler is direct and only fires on the
|
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 {
|