Spec-kitの活用

mcts-genとpy-chessboardjsにおけるSpec-kitの活用と開発ワークフロー改善の記録

この記事では、私が最近行った2つのプロジェクトmcts-genpy-chessboardjsでの開発記録を基に、AIを活用したspec-kitツールの使い方、GitHub Actionsを用いたCI/CDワークフローの構築、そしてPyPIのTrusted Publisher設定について解説します。


1. spec-kitのアップデートと古い設定ファイルの整理

プロジェクトmcts-genakuroiwa/mcts-gen)において、spec-kitのバージョン更新を行いました。この際、古いファイルの削除に手間取りました。また、仮想環境管理ツールuvxを用いた初期インストール手順が、恒久的なツールの利点を享受できない原因となりました。

🚨 古いバージョンからの移行時の注意点

以前のバージョンでspecify initを実行していたため、新しいバージョンのspec-kitを導入する際、古いコマンド定義ファイルが残ってしまいました。

現在のgemini-cliでspec-kitのコマンドは全て/speckit.から始まりますが、古いバージョンでは異なっていました。

【新しいspec-kitのインストール】

仮想環境をアクティベートした後、uv tool installで最新版を取得します。

source .venv/bin/activate
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git

【内部コマンドの更新】

プロジェクトのルートで以下のコマンドを実行し、gemini-cliの内部コマンドを更新します。

specify init --here

これにより、新しいコマンドファイルが.gemini/commands/以下に作成されます。

【新規ファイルの確認】

git statusで確認すると、以下のような新しいファイルが追加されていることが分かります。

ブランチ main
追跡されていないファイル:
  (use "git add <file>..." to include in what will be committed)
    .gemini/commands/speckit.analyze.toml
    .gemini/commands/speckit.checklist.toml
    ...(中略)...
    .gemini/commands/speckit.tasks.toml
    .specify/

.specify/ディレクトリにはmemory/, scripts/, templates/といったサブディレクトリが含まれます。

🧹 古いファイルの削除手順

新しいコマンドファイルと競合する古いファイルを特定し、手動で削除する必要がありました。

  1. 古いコマンド定義ファイルの削除

    cd .gemini/commands/
    rm plan.toml
    rm specify.toml
    rm tasks.toml
    cd -
  2. 古い.specify関連ファイルの削除

    rm -rf memory/
    rm -rf templates/
    rm -rf scripts/

💡 ヒント: コマンド定義ファイルが管理対象外になるように、.gitignore.geminiを追加することも検討できます。ただし、その場合はgit statusには表示されなくなります。


2. 既存プロジェクト(py-chessboardjs)でのspec-kit導入と運用

既存のプロジェクトpy-chessboardjsakuroiwa/py-chessboardjs)でもspec-kitを導入しました。

🛠️ 導入手順

プロジェクトのルートディレクトリで、以下の手順を実行します。

uv venv
source .venv/bin/activate
uv pip install -e . # プロジェクト依存パッケージのインストール
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git # spec-kitのインストール
specify init --here # プロジェクト設定ファイルの作成
specify --help # 動作確認

spec-kitをgemini-cliで使用し始めると、プロジェクトルートに**GEMINI.md**というファイルが作成され、これがAIとの対話の出発点となります。

🚀 spec-kitを利用した開発サイクル

spec-kitのコアな使い方は、事前にGeminiにチャットで相談し、戦略を固めてからスラッシュコマンドを実行していく流れが最も効果的です。

コマンド 目的
/speckit.specify <文字列> 要件定義や仕様策定をAIに依頼し、**specs/**にMarkdownファイルを作成させる
/speckit.plan 実装計画を立てる
/speckit.tasks タスクリストを作成する
/speckit.implement 実際にコードの実装を依頼する

これらの作業を通して、specs/以下にMarkdownファイルが作成されます。原則として、specs/以下のサブディレクトリは作業ごとに作成されたブランチ名と同じになります。

🔄 作業終了後のブランチマージ

spec-kitがブランチを作成して作業を進めた後、作業をメインブランチに反映するには、以下の手順が必要です。

git commit -m "修正内容のメッセージ" # 作業ブランチで必ずコミットする
git checkout main
git merge 001-fix-settings-load-error # 作業ブランチをmainにマージ
git branch -d 001-fix-settings-load-error # 作業ブランチを削除

3. GitHub ActionsとPyPI Trusted Publisherの設定

パッケージの自動公開のために、GitHub Actionsのワークフローを.github/workflows/にYAMLファイルで記述します。このYAMLファイルはGeminiに依頼して作成してもらうことが可能ですが、動作確認が不可欠です。

🚢 パッケージ公開ワークフロー

py-chessboardjsのワークフロー例

このYAMLファイルを使用して、PyPIのTrusted Publisherを登録します。登録時には、上記のYAMLファイル名を指定する必要があります。これにより、GitHub ActionsとPyPIが連携し、OIDC(OpenID Connect)を使って安全にパッケージを公開できます。

🧪 テスト公開と本番公開

タグ名によって、TestPyPIと本番PyPIへの公開を分けます。

公開先 タグ名の条件 コマンド例
TestPyPI -testを含むタグ名 git tag v0.0.3-test1
本番PyPI -testを含まないタグ名 git tag v0.0.3

タグを作成したら、git push origin <タグ名>でプッシュすることで、GitHub Actionsが起動します。

📝 YAMLの構文チェック: Emacsではflycheck-yamllintをインストールすると、flycheck-modeでYAMLの構文エラーをリアルタイムに確認でき、大変便利です。


4. AIとの協調作業における重要な教訓

py-chessboardjsの開発中に遭遇した2つの不具合は、AIによる開発支援の限界と可能性を示す良い事例となりました。

不具合1: ファイルパスの取得問題(C:\fakepath\

現象: チェスエンジンのパス指定が機能しないエラーが発生し、出力にはFileNotFoundError: [Errno 2] そのようなファイルやディレクトリはありません: 'C:\\fakepath\\stockfish'とありました。

解決: Google検索の結果、「fakepath」はHTMLのファイル入力フィールドがセキュリティ上の理由から実際のパスを隠蔽するブラウザの挙動であることが判明しました。この情報をGeminiに提供したところ、**「input type="file"をボタンに置き換え、pywebview.apiを介してPythonのネイティブファイルダイアログを呼び出す」**という正確な解決策が提示され、実装に成功しました。

不具合2: キャスリング時のエラー(concurrent.futures.CancelledError

現象: キャスリングを行うとエラーが発生しました。

解決: ログからconcurrent.futures.CancelledErrorが見つかり、並列処理の不具合が疑われました。原因は、プロジェクトが使用しているchessboardjsの古いバージョンにあり、キャスリング時にFEN(盤面情報)が複数回更新され、チェスエンジンが処理しきれなかった可能性が高いと特定しました。

以下はGeminiによる解説です:

  1. python-chessエンジン呼び出しの問題について

    根本的な問題は、GUIフレームワークであるpywebviewと、asyncioベースのエンジン通信を行うpython-chessライブラリとの間の並行処理の競合でした。

    • pywebviewのAPI呼び出しは同期かつ別スレッドで実行される: JavaScriptからpywebview.api.some_function()を呼び出すと、pywebviewは新しいPythonスレッドでsome_functionを実行し、その関数が戻るまでスレッドをブロックします。

    • python-chessのエンジン通信は非同期 (asyncio):
      chess.engine.popen_uci()関数やengine.play()メソッドは、Pythonのasyncioライブラリを使用して外部のチェスエンジンプロセス(Stockfishなど)と通信します。asyncioはイベントループ上でタスクを実行し、通常は単一のスレッドで行われます。

    • 競合 (レースコンディション):

      1. 以前のApi.uci_engine_move()は、await self.engine.play()を直接呼び出していました。このawait呼び出しは、エンジンが指し手を返すまでpywebviewのAPIスレッドをブロックしていました。
      2. このpywebviewのAPIスレッドがブロックされている間に、ユーザーが別のUI操作(例: 「リセット」ボタンや「戻る」ボタンのクリック)を行うと、その操作もpywebviewのAPI呼び出し(例: Api.on_closed())をトリガーしていました。
      3. 古い設計のApi.on_closed()メソッドはself.engine.quit()を呼び出し、エンジンプロセスを終了させていました。
      4. エンジンプロセスが終了すると、self.engine.play()が待機していたasyncioのFutureがキャンセルされ、concurrent.futures.CancelledErrorが発生しました。エンジンプロセスが予期せず終了した場合はchess.engine.EngineTerminatedErrorが発生しました。
      5. pywebviewのAPIスレッドはself.engine.play()の結果を待ってブロックされていたため、Futureがキャンセルされると、CancelledError(またはEngineTerminatedError)が伝播し、アプリケーションがクラッシュしたり、一貫性のない状態になったりしていました。
    • 解決策 (専用ワーカースレッド):

      • 私たちは、専用のthreading.Threadで実行されるEngineWorkerクラスを導入しました。
      • このEngineWorkerスレッドは独自のasyncioイベントループを持ちます。
      • pywebviewからApi.uci_engine_move()が呼び出されても、もはやブロックされません。代わりに、EngineWorkerが監視するqueue.Queueに「プレイ」タスクを投入します。
      • EngineWorkerはタスクを受け取り、自身の asyncioイベントループ上でawait self.engine.play()を実行します。指し手が準備できると、スレッドセーフなコールバック(_engine_move_callback)を使用して結果をApiクラスに返します。
      • Apiクラスはself.window.evaluate_js()を使用して、指し手をJavaScriptのフロントエンドにプッシュします。
      • なぜこれで解決したのか: pywebviewのAPIスレッドはasyncio操作によってブロックされなくなりました。asyncioイベントループは独自の安定したスレッドで継続的に実行されます。エンジンを終了させる可能性のあるUI操作は、EngineWorkerのキューに「終了」タスクを送信するようになり、ワーカースレッドはエンジンとそのループを安全にシャットダウンできます。計算中に「終了」が受信されても、ワーカースレッド内でasyncioタスクがキャンセルされるだけで、pywebviewのAPIスレッドには伝播せず、クラッシュを防ぎます。Engine move discardedというメッセージは、この正常な処理を示しています。
  2. specs/002-fix-castling-errorの削除について

    削除したのは良い判断でした。002-fix-castling-errorブランチとその関連する仕様ファイルは、バグ修正の失敗した試みを表していました。それらを残しておくと、誤った情報や置き換えられた情報でリポジトリが煩雑になります。

    フィーチャーの試みが失敗したり、新しいアプローチに置き換えられたりした場合は、古いブランチとその成果物をクリーンアップするのがベストプラクティスです。これにより、プロジェクトの履歴がクリーンに保たれ、成功した作業や関連する作業のみが反映されます。

    新しい003-fix-engine-concurrencyブランチとその仕様は、根本的な並行処理の問題を解決するための現在の成功したアプローチを正確に反映しています。

💡 教訓: ユーザーの根気強さが成功の鍵

これらの事例から学んだのは、Geminiでも不具合の特定は難しく、ユーザーとの共同作業であるということです。特に、エラー出力や挙動を注意深く観察し、根気強くAIにフィードバックすることが、適切な解決策を導き出すために不可欠となります。AIは強力な助けとなりますが、最終的なデバッグと検証は人間の役割です。