送信確認のメールが届きます。
お問い合わせ内容に応じて、24〜72時間以内に担当者よりご連絡いたします。
送信することで、当社の【プライバシーポリシー】および、Open Reach Techからのメール受信に同意したものとみなします。
プライバシー同意チェックボックスを選択してください。

components..title

components..description

components..title

components..description

送信確認のメールが届きます。
お問い合わせ内容に応じて、24〜72時間以内に担当者よりご連絡いたします。
送信することで、当社の【プライバシーポリシー】および、Open Reach Techからのメール受信に同意したものとみなします。
プライバシー同意チェックボックスを選択してください。

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