CursorのProject Rules運用のベストプラクティスを探る
こんにちは、しば田です!
この記事では自分がProject Rulesをどのように運用しているかを書いていきます。
設定ではなく運用という表現が近いです。
注意事項
- 以下に語る運用が正解かは分からないです。1つの参考程度にお読みください。
- Cursorのアップデートでこの記事は一瞬で陳腐化する可能性があります。ご了承ください。
TL;DR
まずは結論から。現時点での僕の考えるベスプラは以下です。
- 日々アップデートして育てる
- 育てやすい構成にしておく
- 複数のmdファイルの中身を結合するスクリプトを作ってmdcファイルを生成する
- mdcファイルの参照ルール(Auto Attach、Description、alwaysApply)には落とし穴があるので気をつける
この運用のメリット
- エージェントのパフォーマンスが地道に改善されていく
- チームで共有できるようになる
自分の運用のサンプルを以下に置いておきました。(先に見ると記事がちょっと読みやすくなるかも)
Project Rulesとは
簡単に言うと、Cursor独自の 「AIにどう動いてほしいか」を伝える設定ファイル です。拡張子は.mdcです。mdファイルと同様マークダウンで書けます。rulesファイルなしではAIエディタの性能を引き出せないどころか、rulesなしで開発すると制御できずむしろストレスが溜まる可能性すらあるので面倒でもやりましょう。
.cursorrulesとの違い
Project Rulesはフォルダやファイル単位でruleの適用をコントロールできます。
グローバルで有効化する機能もあるので、.cursorrulesはProject Rulesによってリプレイスされると考えて大丈夫だと思います。現状は.cursorrulesも普通に機能しますが遅かれ早かれdeprecatedになると思うので早めの書き換えを推奨します。
Project Rulesの詳細や作り方はこちらの記事を読むと分かります。
さて、ここからが本題です!
1. Project Rulesは日々アップデートする
Project Rulesは一回作ったら終わりではないです。
なぜなら、AIに対しての要望は開発していれば無限に生まれてくるからです。
実装していると「AIここ毎回間違えるな」と「この指示は毎度したくないから覚えておいて欲しいな」という場面に結構遭遇します。なので都度蓋をしてあげる必要があります。
また、モデルの進化やツール側の内部プロンプトの変更により常にエージェントの得意不得意は変わってくるのでそれに合わせたアップデートも必要になります。
2. 育てやすい構成にする
毎度の更新はしっかりやるとかなり大変です。
なので最低限育てやすい構成にしておく必要があります。
僕が実際のプロジェクトで使用している構成は以下です。
mdcファイルは基本この4つのファイルです。(もっと細かく分けてもいいと思います。)
.cursor/rules
├── 000_general.mdc # 基本的な指示
├── 001_bestPractices_common.mdc # 共通のコーディングルール
├── 002_bestPractices_frontend.mdc # フロントエンドのコーディングルール
└── 003_bestPractices_backend.mdc # バックエンドのコーディングルール
後述しますが、mdcファイルは編集しません。
実際に編集するのは以下のように分けられたmdファイルです。
rules
├── backend
│ ├── 000_init.md
│ └── 001_nestjs.md
├── common
│ ├── 000_init.md
│ ├── 001_basic.md
│ ├── 002_structure.md
│ ├── 003_typescript.md
│ ├── 004_techStack.md
│ ├── 005_db.md
│ ├── 006_monorepo.md
│ └── 007_deployment.md
├── frontend
│ ├── 000_init.md
│ ├── 001_nextjs.md
│ └── 002_tailwind.md
└── general
├── 000_init.md
├── ...etc
このように細かくしておくことで、
指示を追加したくなった時にどこに書けばいいのかがめちゃくちゃ分かりやすくなります。
また、チームで開発で真価を発揮できるようになります。
これはとてもメリットがありました。
チームで知の共有文化ができるので暗黙知が減ります。
また、小さく分割しておけばコンフリクトが起きにくく、起きたとしても簡単に解決できます。
3. 複数のmdファイルの中身を結合するスクリプトを作ってmdcファイルを組み立てる
MDCファイルからMDファイルを読み込む方法は安定しない。
まずは、スクリプトを作らなきゃいけなくなった背景から。
元々以下のようにMDCファイルからMDファイルを読み込ませるように設定していました。
しかし、読み込みが安定しませんでした。
## Next.js
- このプロジェクトではNext.jsを採用しています。
- こちらのファイルを参照してください: @nextjs.md
## tailwind
- このプロジェクトではtailwindを採用しています。
- こちらのファイルを参照してください: @tailwind.md
@で読み込ませた先のファイルには「このファイルを確認したらYAAAA!と言って下さい」みたいな指示を書いていましたが、ほとんどの場合で言わないし、その中に書いてあった指示も明らかに抜け落ちていました。
どうにかこうにか読み込まれるようにあれこれ工夫しましたが無理そうでした。
スクリプトを作る
上記理由で、細かく分けたmdファイルの中身を結合してmdcファイルに書き込むという方法にしようと決めました。なのでmdcファイルをいじることはありません。
この辺の着想は、以下あたりを参考にしました。
やっていることはとても単純で、番号順に上からMDファイルの中身の文字列を抽出して、それぞれを結合してmcdファイル内に出力するだけです。
以下のような単純なスクリプトです。(レポジトリにも入れてあります)
import * as fs from 'node:fs';
import * as path from 'node:path';
import { glob } from 'glob';
// mdcファイルとmdディレクトリの対応関係の定義
const mdcConfigurations = [
{
output: ".cursor/rules/000_general.mdc",
sourceDir: "rules/general",
header: "", // もしコメントとか入れたければ
filePattern: "*.md",
sortBy: "name"
},
{
output: ".cursor/rules/001_bestPractices_common.mdc",
sourceDir: "rules/common",
header: "", // もしコメントとか入れたければ
filePattern: "*.md",
sortBy: "name"
}
];
// ファイル名から数字プレフィックスを抽出してソートするための関数
function extractNumberPrefix(filename: string): number {
const match = filename.match(/^(\d+)_/);
return match ? parseInt(match[1], 10) : Infinity;
}
// mdファイルを検索して結合する関数
async function buildMdcFile(config: typeof mdcConfigurations[0]) {
// ルートディレクトリの取得(スクリプトの実行場所から相対パスで計算)
const rootDir = path.resolve(process.cwd());
// mdファイルのパターンを作成
const pattern = path.join(rootDir, config.sourceDir, config.filePattern);
// mdファイルを検索
const files = await glob(pattern);
// ファイル名でソート
files.sort((a: string, b: string) => {
const numA = extractNumberPrefix(path.basename(a));
const numB = extractNumberPrefix(path.basename(b));
return numA - numB;
});
// コンテンツの初期化
let content = '';
// ヘッダー情報を追加
content += config.header;
// 各mdファイルの内容を結合
for (const file of files) {
// console.log(`Processing file: ${file}`);
const fileContent = await fs.promises.readFile(file, 'utf8');
content += fileContent + '\n\n';
}
// mdcファイルを出力
const outputPath = path.join(rootDir, config.output);
// 出力ディレクトリが存在することを確認
const outputDir = path.dirname(outputPath);
try {
await fs.promises.mkdir(outputDir, { recursive: true });
} catch (error) {
// ディレクトリが既に存在する場合は無視
}
// ファイルに書き込み
await fs.promises.writeFile(outputPath, content);
console.log(`Generated ${config.output} from ${files.length} files in ${config.sourceDir}`);
}
// 既存のMDCファイルの中身を空にする関数
async function cleanMdcFiles() {
const rootDir = path.resolve(process.cwd());
// .cursor/rules ディレクトリの存在確認
const rulesDir = path.join(rootDir, '.cursor/rules');
try {
await fs.promises.access(rulesDir);
} catch (error) {
// ディレクトリが存在しない場合は何もしない
return;
}
// .mdc ファイルを検索して中身を空にする
const mdcFiles = await glob(path.join(rulesDir, '*.mdc'));
for (const file of mdcFiles) {
console.log(`Clearing content of MDC file: ${file}`);
await fs.promises.writeFile(file, ''); // ファイルの中身を空にする
}
}
// メイン処理
async function main() {
try {
// 既存のMDCファイルを削除
await cleanMdcFiles();
// 各設定に対してmdcファイルを生成
for (const config of mdcConfigurations) {
await buildMdcFile(config);
}
console.log('All mdc files have been successfully generated!');
} catch (error) {
console.error('Error generating mdc files:', error);
process.exit(1);
}
}
// スクリプトの実行
main();
新しいルールを追加した時は、
yarn build:mdc
でmdcファイルを都度更新しています。
時間があればこの辺ももっと自動化/簡略化していきたいと思っています。
今は自分でコマンド打っていますが、エージェントにやらせるようにすると思います。
4. Auto AttachとDescription、alwaysApplyの設定はガチでやる。
この部分の話です。
ちなみに、Cursor以外でmdcファイルを開くと、こんな感じになります。
---
description: this file describes how you need to carry out the tasks. always refer to this file first.
globs: *
alwaysApply: true
---
エージェントがrules適用するか否かを判断する思考の流れ
僕は以下だと考えています。
- alwaysApplyを見る
- Auto Attach(globs)を見る(alwaysApplyがfalseなら)
- Descriptionを見る
ここからが重要なのですが、1を設定したから3を書かなくていいみたいなことにはならないので注意して下さい。
エージェントにきちんと拾ってもらえるように1,2,3すべてしっかり書いて下さい。
以下は、色々サボった僕がハマった落とし穴です。
落とし穴1. alwaysApplyを設定していたのでDescriptionを書かなかった
alwaysApplyは強力なので9割くらいはalwaysApplyで拾ってもらえますが、たまに拾ってもらえない時があってDescriptionに「常に参照して」と書いたら常に拾ってもらえるようになった。
落とし穴2. Auto Attach(globs)を発火させるにはプロンプトでの@でのファイル参照が必要
明らかにAuto Attachに引っかかるファイルをエージェントが編集しているのに、ruleが読み込まれない時がありました。
検証して分かったのが、Auto Attachは、現在プロンプト窓で@で参照しているファイルがAuto Attachのパスに引っかかっているか否かで判断しています。
最近のアップデートでプロンプト送信前にどのProject Rulesが適用されるかをUIから確認できるようになって仮説が確証に変わりました。
落とし穴3. Auto Attach(globs)の書き方
NG:globs: ["apps/frontend//", ".tsx"]
OK:globs: apps/frontend//*, *.tsx
配列に入れるとうまく読み込まれませんでした。
僕はClineに書いてもらった結果なかなかこの点に気づけなかったです。。。
落とし穴4. Descriptionだけでは基本拾われない
alwaysApplyはfalseで、Auto Attachでも表現するのがムズイ時に、Descriptionだけで拾ってくれるかなと思っていましたが、これは一生拾われませんでした。
その他
これをやったからといって全部楽にはならない
Project RulesはCursorでAI開発を楽にする1つの手段ですが、それ以外にも死ぬほど大事な要素がたくさんあります。
例えば単純ですが、「プロンプトで@でコンテキストを与える」、「Webで調べてもらう」など普通に超大事なので、rules設定したからといって気を抜かず丁寧にやった方がいいです。
rulesの編集もAIに任せる
「コードベースを見てファイルツリーを把握し、@002_structure.mdに記載して下さい。」とかやると、いい感じにしてくれます。
最強のオンボーディング資料になる
rulesを体系化しておくと、人間にとっても最強のオンボーディング資料になります。
.rulesのサイズが大きすぎるとダメなのでは?
モデルのコンテキストウィンドウはこの先どんどん増えていくことは間違いないのでそれを見越してしっかり溜めていく方が利口な選択だと思ってます。僕の肌感ですが、今のところある程度増えても問題なさそう。
とはいえ、実際にはコンテキストウィンドウの制限の課題は現状あるので、AIが既に得意で問題なくできていることをわざわざ追加することは避けています。
ファイル名に番号をつける
プロンプト内で、@で参照したい時にとても参照しやすくなるのでおすすめです。
WindsurfやClineではどうしてるの?
WindsurfやClineでは、別のスクリプトで全てを結合して1ファイルに突っ込んでいます。要望あれば公開します。
Xやってます
Xやってます!この記事について何か(質問/賛同/異論/等なんでも大歓迎)あればDM欲しいです。いくらでも教えますし、いくらでも教わりたいです。
参考
- https://github.com/mizchi/ailab
- https://github.com/kinopeee/cursorrules
- https://dotcursorrules.com/rules
- https://github.com/PatrickJS/awesome-cursorrules/blob/main/rules/angular-novo-elements-cursorrules-prompt-file/.cursorrules
- https://github.com/CodeGuide-dev/codeguide-starter-lite
- https://x.com/rileybrown_ai/status/1891632470207979646
- https://x.com/jelanifuel/status/1897036963162808617
Discussion