Cursor Agent Skills.
Cursor Agent Skills はエンジニアリング規約を Markdown にまとめ、安定したエージェント出力と Git で共有するスキル配置、再利用しやすいワークフローを実現します。

AI を使用する際の問題
- 一般的なユーザーは、質問に答えてもらうために直接エージェントへプロンプトを送ります。しかし、同じプロンプトでも文脈によってエージェントの解釈が変わり、異なる結果が返されることがあります。
例:
- context が
遊ぶについて話している場合、上手と prompt すると、エージェントは通常遊ぶのが上手と解釈します。 - context が
勉強について話している場合、上手と prompt すると、エージェントは通常勉強ができると解釈します。
=> 多くの場面では、遊ぶ と 勉強 は正反対の概念になります。
このような問題を減らすために skills が生まれました。
紹介
- Skill とは、
知識・ルール・ワークフローをファイルとしてパッケージ化したもので、Cursor Agent の動作をガイドします。 そのため、これをCursor Agent Skillsと呼びます。 - Claude や Codex など他のエージェントにも skills は存在しますが、私たちの会社では Cursor を使用しているため、ここでは Cursor Agent Skills に集中します。
- Skills は
.mdファイルとして管理され、状況に応じてエージェントが利用できます(ユーザーが明示的に skill を指定して使用することも可能です)。
Skills を使うタイミング
- 私たちの目的は、どのような文脈でもエージェントの出力を標準化することです。 そのため、繰り返し行う作業や、一定のルールや規則性がある作業では、skill を作成することを検討します。
例:
- Convention に従ったコード生成
- テスト作成
- Database Seeder 作成
良い Cursor Agent Skills の作り方
構成
# Skill 名
## 目的
この skill が何のために使われるかを説明する。
## 使用するタイミング
エージェントがこの skill を使うべき状況を説明する。
## ガイドライン
手順をステップごとに説明する。
## 出力形式
最終結果がどのようになるべきかを定義する。
## 例
良い出力例を提供する。
Skill の配置場所
Note: 通常、私は Cursor の設定ディレクトリに配置していますが、プロジェクト内に配置して Git でチーム共有することもできます。
.cursor/
skills/
technical-design-document.md
task-breakdown.md
graphql-resolver.md
vue-context-pattern.md
良い Skill とは
- 具体的であること
「クリーンなコードを書く」のような曖昧な指示は避ける。
代わりに、そのプロジェクトにおける「クリーン」の定義を明確にする。
- 実行可能であること
「Factory Method を作成する」
「test.each() を使う」など、直接的な指示を使う。
- 構造化されていること
ガイドラインをセクションやステップごとに整理する。
- 例があること
エージェントは例を非常によく学習する。
可能であれば、良い例と悪い例の両方を提供する。
- 現在のコードベースに沿っていること
実際にプロジェクトで使われているパターンを反映するべきであり、
存在しない理想論だけを書くべきではない。
- 再利用可能であること
頻繁に繰り返される作業でない限り、
ごく小さな単一用途だけの skill は避ける。
ORT への適用
理論
Note: skill は 知識・ルール・ワークフロー をパッケージ化したものです。
そのため、各 skill は ORT の以下のルールや規約を反映している必要があります。
明確な命名を行う
不要な略語を使わない
1 ファイル 1 クラス
Factory Method を使用する
意味のある Object Parameters を使用する
Vue ロジックには Context class を使用する
JSDoc を完全に記述する
「なぞなぞコード」を避ける
賢いが読みにくいコードより、読みやすいコードを優先する
実践
SKILLS
- 私は複数の skill を作成し、日々の業務で頻繁に利用しています。 その中には期待値の 90% 程度を達成できるものもあり、生成された output をほとんど修正する必要がありません。
- 将来的にルールや規約が変更された場合でも、rule を一箇所修正するだけで済みます。 (Chiho の AI Workflow template のようなものだと考えてください)
- 以下は私の skills と、その期待精度です (期待値が高いほど、修正量が少ないという意味です)
ort-graphql=> GraphQL を生成 (90%)ort-fetcher=> fetcher を生成 (90%)ort-submitter=> submitter を生成 (90%)ort-inject- この skill を呼び出すと、どこへ inject するか、どの fetcher / submitter を使うかなどを質問してきます。
- index.vue に fetcher や submitter を追加する作業に近いですが、 context に応じて reactive status や error message なども自動生成します。
- (約 85%)
ort-design- UI 作成を支援する skill です。
- どのような design にするか、どこへ実装するかを質問します。
- 私のおすすめは、
ort-injectの直後に実行することです。 fetcher, submitter, status, reactive などの context を利用できるため、 より良い結果になります。 - (約 60〜65%)
ort-worker- 1〜5 の step を自動化する workflow を作りました。
- ただし、AI に渡す質問をしっかり読み、 十分な context を与える必要があります。
- (私の体感では成功率 50% 未満なのでおすすめしません)
- 1〜5 を一つずつ順番に実行した方が、 はるかに良い結果になります。
以下は ort-fetcher skill の例です
/**
* Fetcher class for {queryName} GraphQL query.
*/
export default class {Entity}Fetcher {
/**
* Constructor.
*
* @param {{Entity}FetcherParams} params
*/
constructor ({
route, // omit if no route needed
statusReactive,
graphqlClientHash,
}) {
this.route = route
this.statusReactive = statusReactive
this.graphqlClientHash = graphqlClientHash
}
/**
* Factory method.
*
* @template {X extends typeof {Entity}Fetcher ? X : never} T, X
* @param {{Entity}FetcherFactoryParams} params
* @returns {InstanceType<T>}
* @this {T}
*/
static create ({
route,
statusReactive,
graphqlClientHash,
}) {
return /** @type {InstanceType<T>} */ (
new this({
route,
statusReactive,
graphqlClientHash,
})
)
}
/**
* get: {queryName} capsule ref
*
* @returns {import('vue').Ref<import('~/.../...GraphqlCapsule.js').default>}
*/
get {queryName}CapsuleRef () {
return this.graphqlClientHash
.{queryName}
.capsuleRef
}
/**
* get: {queryName} launcher hooks
*
* @returns {furo.GraphqlLauncherHooks}
*/
get {queryName}LauncherHooks () {
return {
beforeRequest: async payload => {
this.statusReactive.isFetching{Entity} = true
return false
},
afterRequest: async capsule => {
this.statusReactive.isFetching{Entity} = false
},
}
}
/**
* Generate {queryName} query params.
*
* @param {{QueryParams}} params
* @returns {{QueryInput} | null}
*/
generate{Entity}QueryParams ({
pagination,
}) {
const entityId = this.extract{RouteEntity}IdFromRoute()
if (!entityId) {
return null
}
return {
entityId,
pagination,
}
}
/**
* Fetch {queryName} on mounted.
*
* @param {{QueryParams}} params
* @returns {void}
*/
fetch{Entity}OnMounted ({
pagination,
}) {
const queryParams = this.generate{Entity}QueryParams({
pagination,
})
if (!queryParams) {
return
}
this.graphqlClientHash
.{queryName}
.invokeRequestOnMounted({
variables: {
input: queryParams,
},
hooks: this.{queryName}LauncherHooks,
})
}
/**
* Fetch {queryName} on event.
*
* @param {{QueryParams}} params
* @returns {Promise<void>}
*/
async fetch{Entity}OnEvent ({
pagination,
}) {
const queryParams = this.generate{Entity}QueryParams({
pagination,
})
if (!queryParams) {
return
}
await this.graphqlClientHash
.{queryName}
.invokeRequestOnEvent({
variables: {
input: queryParams,
},
hooks: this.{queryName}LauncherHooks,
})
}
/**
* Extract {routeEntity} id from route.
*
* @returns {number | null}
*/
extract{RouteEntity}IdFromRoute () {
const {
{routeParam},
} = this.route.params
const idFromRoute = Array.isArray({routeParam})
? {routeParam}.at(0)
: {routeParam}
const numericId = Number(idFromRoute)
if (isNaN(numericId)) {
return null
}
return numericId
}
}
/**
* @typedef {{
* route: ReturnType<typeof import('vue-router').useRoute>
* statusReactive: import('vue').Reactive<{
* isFetching{Entity}: boolean
* }>
* graphqlClientHash: {
* {queryName}: ReturnType<import('@openreachtech/furo-nuxt').useGraphqlClient>
* }
* }} {Entity}FetcherParams
*/
/**
* @typedef {{Entity}FetcherParams} {Entity}FetcherFactoryParams
*/
---
## Key rules
- `beforeRequest`: set `isFetching{Entity} = true`, return `false`
- `afterRequest`: set `isFetching{Entity} = false`
- `generate{X}QueryParams()`: extract route params + build input — return `null` if invalid
- `fetch{X}OnMounted()`: sync, calls `invokeRequestOnMounted`, guard null params with early return
- `fetch{X}OnEvent()`: async, calls `invokeRequestOnEvent`
- All JSDoc typedefs go at the bottom of the file
- `{ClassName}FactoryParams` = `{ClassName}Params` unless factory omits DI-created deps (use `Omit<>`)
(※ コード部分はそのまま保持してください)
ORT FE における Skill の使い方
Note: もちろん、エージェントが context に応じて適切な skill を自動判断することもできます。 しかし、私はそれをあまり推奨していません。
なぜなら、agent が誤った判断をすると token を大量に消費し、後から多くの修正が必要になるからです。
そのため、自分の feature と自分の skills を理解し、 各 context ごとに適切な skill を明示的に呼び出すべきです。 これにより token 使用量を大幅に削減できます。
私が 1 つの機能を作る際の Workflow
1. 機能を理解する
- これは全メンバーに必要です。
- skill を使う場合は特に、 どの skill を使うべきか判断できるレベルまで理解する必要があります。
2. Backend から schema を取得する
例:
type Query {
locales: LocalesResult!
translatedIssue(input: TranslatedIssueInput!): TranslatedIssueResult!
}
type Mutation {
translateIssue(input: TranslateIssueInput!): TranslateIssueResult!
}
type LocalesResult {
locales: [Locale!]!
}
type Locale {
localeId: Int!
name: String!
}
type TranslatedContent {
translatedName: String!
translatedDescription: String!
translatedAt: String!
}
type TranslatedIssueResult {
translation: TranslatedContent
}
type TranslateIssueResult {
translation: TranslatedContent!
}
input TranslatedIssueInput {
issueId: Int!
localeId: Int!
}
input TranslateIssueInput {
issueId: Int!
localeId: Int!
}
3. 以下の順番で skill を呼び出す
ort-graphql → ort-fetcher → ort-submitter
- 上記 schema を agent に渡してください。
4. 続いて
ort-inject → ort-design
5. 最後に修正する
- AI が生成したファイルは必ず確認してください。
- 「AI が正しく生成したはず」と思い込まないでください。
以下が私の Skill Pack です
(現在 document system に file attach 機能がないため、後ほど issue に添付します)
Download: click to download
インストール方法
- zip を解凍する
- Cursor の chat に drag & drop する
- Cursor に install させる
呼び出し方法
/skill-name
例:
/ort-fetcher