Open Reach Tech Inc.

Cursor Agent Skills.

Hoang Tanのプロフィール写真
Hoang TanFrontend Developer

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

Banner of Cursor Agent Skills.

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 と、その期待精度です (期待値が高いほど、修正量が少ないという意味です)
  1. ort-graphql => GraphQL を生成 (90%)
  2. ort-fetcher => fetcher を生成 (90%)
  3. ort-submitter => submitter を生成 (90%)
  4. ort-inject
    • この skill を呼び出すと、どこへ inject するか、どの fetcher / submitter を使うかなどを質問してきます。
    • index.vue に fetcher や submitter を追加する作業に近いですが、 context に応じて reactive status や error message なども自動生成します。
    • (約 85%)
  5. ort-design
    • UI 作成を支援する skill です。
    • どのような design にするか、どこへ実装するかを質問します。
    • 私のおすすめは、ort-inject の直後に実行することです。 fetcher, submitter, status, reactive などの context を利用できるため、 より良い結果になります。
    • (約 60〜65%)
  6. 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

送信完了!

お問い合わせいただきありがとうございます。 担当者より24〜72時間以内にご連絡いたします。