handatのdatファイル

日頃の出来事を格納するdatファイル

インセプションデッキのファシリテーターをGeminiに任せてみた

この記事ははてなエンジニア Advent Calendar 2025 25日目の記事です。
昨日は id:SlashNephy さんの Ktor Client の Pipeline を活用したクリーンなエラーハンドリング でした。


インセプションデッキの作成において参加者全員から意見を聞き出しながら認識を揃えるには熟練したファシリテーションスキルが必要不可欠です。進行がうまくいかないとインセプションデッキがその後の開発に活用しにくいものになってしまいます。

そこで、インセプションデッキのファシリテーターをGeminiにお願いしてみたのでやり方を紹介します。

モチベーション

私たちのチームではディレクターがプロダクトオーナー業だけでなく、クライアントとの対応やレポート作成なども担当し、多忙になりやすいという課題があります。 そんな中で要件が曖昧な新しいプロジェクトが始まろうとしていたため、関係者間の認識を揃えることを目指してインセプションデッキを作成することにしました。

リモートワーク環境かつインセプションデッキの経験者の少ない中、うまくファシリテーションできるか不安だったため、AIにファシリテーターを任せられないかと考えました。

※下記の例に出てくるプロジェクト・メンバーはすべて架空のものです。また、一部のプロンプト・Geminiの回答は加工しています。

事前準備

まず、事前に最終的な成果物とするインセプションデッキのフォーマットをGoogle Docs(以下、Docs)に用意し、参加者が編集できるようにしておきます。

インセプションデッキのフォーマット例

インセプションデッキ作成開始

参加者がGoogle Meetに集まったら、一人が画面を共有しGeminiに次のようなプロンプトを入力してインセプションデッキの作成を始めます。
事前に準備しておいたDocsを読み込ませることでGeminiがフォーマットに沿って進行してくれます。

プロンプト

Webでマンガを快適に読める漫画ビューワーを開発するプロジェクトを開始するため、インセプションデッキを作成しようと思っています。インセプションデッキを作るのが初めてなので、ファシリテーターをお願いします。   参加者は handat / sato / suzuki の3名で、1時間で作成できるものを作りたいです。   POの takahashi は不在なので、今回はエンジニア3名の中でインセプションデッキを作成し、後日すり合わせを行う予定です。

最初のプロンプト

Geminiの回答

Webでマンガを快適に読める「漫画ビューワー」の開発プロジェクト、非常にワクワクする取り組みですね!handatさん、satoさん、suzukiさんの3名で、1時間という限られた時間の中で効率よく認識を合わせられるよう、私がファシリテーターを務めさせていただきます。
takahashiさん(PO) が不在とのことですので、本日のゴールを以下のように設定しましょう。
本日のゴール: エンジニア視点での「仮説・たたき台」を作り、後日takahashiさんが「Yes/No」や「修正」を入れられる状態にする。
1時間はあっという間ですので、すべての項目は埋めず、 「エンジニアとして認識を合わせないと手戻りが大きくなる項目」 に絞って進行します。

⏱️ 本日のタイムボックス(60分)

今回は特に重要な以下の4つに絞りましょう。

  1. Why1:なぜやるのか(仮説) 【10分】
    • なぜ今これを作るのか?
  2. Why4:やらないことリスト 【15分】
    • ここが最重要です。風呂敷を広げすぎないよう、エンジニア視点で線引きします。
  3. How1:技術的な解決策 【15分】
    • 既存の資産をどう使うかなど、大まかな構成案。
  4. How5:トレードオフ・スライダー(諦める順番) 【10分】
    • 品質・スピード・スコープのどれを優先するつもりか、スタンスを決めます。

各設問の進め方

1. Geminiが設問の問いかけをする

ファシリテーターのGeminiによる設問の問いかけから始まります。
設問によっては、これまでの設問の内容から回答例や考えるべき観点を明らかにしてくれることもあるため、インセプションデッキ初心者でも何を書くべきかが明確になり、スムーズに進行できました。

最初のステップとして、「Why1: なぜやるのか(プロジェクトの目的)」について、皆さんが現時点で考えていることを教えていただけますか?
例えば、「既存のビューワーは読み込みが遅くてストレスなので、それを解消したい」のか、あるいは「全く新しい読書体験を提供したい」のか、などです。どなたからでも構いません!

2. 参加者がDocsに考えていることを書く

各参加者がDocsに書いた状態

この取り組みの鍵:GeminiとDocsの連携

Geminiのチャットは複数人で共有できないため、Geminiと会話できるのは画面共有をしている一人だけになってしまいます。
そのため、参加者にはDocsに自分の意見を書いてもらいます。その際に誰の意見か分かるようにしておくと良いでしょう。

Geminiは会話ごとに最新のDocsの状態を参照できるので、Docsがホワイトボードのような役割をしてくれるのです。

3. Geminiに要約させる

全員が意見を書き終わったら、Geminiに「書き終わりました」と送信します。すると、それぞれの意見を確認してまとめてくれます。

ドキュメントの更新、ありがとうございます! handatさん、satoさん、suzukiさんの視点が出揃いましたね。

「Why1: なぜやるのか」について、皆さんの意見をまとめると以下のようになるかと思います。

プロジェクトの目的(Why1)のまとめ案: 「人気アニメや映画の影響でマンガへの注目が高まる中、Web環境でもロード時間が短く、誰でも気軽に・快適に楽しめる漫画ビューワーを提供するため」 まずはこの方向性で認識にズレがないか確認をお願いします。


では、次のステップに進みましょう!

4. 参加者で合意した内容をDocsに書く

Geminiのまとめをそのまま採用してもいいですし、さらに議論してより良いものを考えてもいいでしょう。
最終的に決まったものは後から見返せるようにDocsに清書してから次の設問に進みます。

1つの設問が終わった状態

感想

自分がファシリテーターをしていたら、上から順番に進めて途中で時間が足りなくなったり、時間内ですべて終わらせようとしてインセプションデッキが浅い内容になっていたでしょう。
Geminiは進行はできてもタイムキープはしてくれないので注意は必要ですが、最初にタイムボックスが示されていたのでほぼ時間通りに進行できました。実際は10分ほどオーバーしてしまったのですが、それだけ議論は盛り上がったということにしたいと思います。参加者からもポジティブな感想がありました。

まだプロジェクトは進行中ですが、やらないことリストを参考に技術選定を考えたり、トレードオフスライダーを使って具体的な要件を詰めていくという活用ができていると思います。

まとめ

インセプションデッキはプロジェクト初期にメンバーの前提知識が少ない状態で取り組むものです。だからこそ多くのコンテキストを与える必要がないため、AIにファシリテーションを任せやすいと感じました。

エンジニアとして生成AIはコーディングエージェントとしての活用に着目しがちですが、苦手意識を感じやすいファシリテーションやコミュニケーションをカバーするためにも活用してみてください。


はてなエンジニア Advent Calendar 2025 はまだまだ続きます!明日は id:rokoucha さんです。

Claude Code以外のコーディングエージェントでもGitHubに共同作業者としてアイコンを表示したい

やりたいこと

Claude Codeがコミットした変更をGitHubにPushすると、次のように共同作業者としてコミットにClaude Codeのアイコンも表示されるようになっている。

Claude Codeがコミットした変更の表示例

他のAIコーディングエージェント(Gemini CLI, Codex CLI など)で作業したときも、そのエージェントのアイコンが表示されるようになってほしい。

こうする

Gemini CLI

AGENTS.md(インストラクションファイル)に次のような記述を追加する

AIエージェントがコミットするときは、コミットメッセージの末尾に次の`Co-authored-byトレーラー`を加えてどのAIエージェントが作業したか分かるようにしてください。

- Gemini CLI: `Co-Authored-By: gemini-cli <218195315+gemini-cli@users.noreply.github.com>`
<!-- 他のコーディングエージェントも追記できる -->

Gemini CLIがコミットした変更の表示例

Codex CLI

(少なくともGemini CLIと同じやり方では) アイコンを表示することはできなさそうだった。
※原因は後述

解説

GitHubではコミットメッセージのCo-authored-byトレーラーがあると、複数の作者を持つコミットとして認識します

docs.github.com

そこで、Gemini CLIgemini-cliのnoreply メール アドレスを使って、GitHubが共同作業者として認識できるようにしています。GitHub Actionsでコミットするときにメールアドレスを41898282+github-actions[bot]@users.noreply.github.comにするのと同じ考え方。

docs.github.com

2017 年 7 月 18 日 "以降" にアカウントを作成した場合、noreply のメール アドレスは ID+USERNAME@users.noreply.github.com 形式 (ID 番号とユーザー名)

Codex CLIも同様に Co-Authored-By: codex <223734131+codex@users.noreply.github.com> のようなトレーラーを入れてみましたが、共同作業者として認識されませんでした。
恐らくcodexはアカウントではなくOrganizationだからなのではないかと考えています。

Restyのテストを書いたらJSONが構造体にアンマーシャルされず困った

AIにコーディングさせていたらテストが通らなくて、結局自力で解決した話です。

困っていたこと

  • Goで外部APIを叩く実装があり、HTTPクライアントにRestyを使っていた
  • 実際のAPIを叩くと正しい挙動が得られるが、モックサーバーを使ったテストが期待通りに動作してくれなかった
  • テストコードをデバッグしたところ、JSON形式のレスポンスを返しているがresty.R().SetResult()でRestyがJSONを構造体にアンマーシャルする部分が動いていなかった

原因

モックサーバーでContent-Typeのレスポンスヘッダーを設定していなかった

問題の状況

実装コード

Restyを使ってメールアドレスとパスワードから認証トークンを取得する実装です。

type AuthRequest struct {
    MailAddress string `json:"mailaddress"`
    Password    string `json:"password"`
}

type AuthResponse struct {
    AuthToken string `json:"authToken"`
}

func (c *Client) getAuthToken(ctx context.Context) (string, error) {
    reqBody := &AuthRequest{
        MailAddress: c.config.MailAddress, // test@example.com
        Password:    c.config.Password,    // password
    }
    var resBody AuthResponse
    var errRes map[string]string

    resp, err := c.restyClient.R().
        SetContext(ctx).
        SetBody(reqBody).
        SetResult(&resBody).  // ここで構造体にパースされる
        SetError(&errRes).
        Post("/v1/auth/token")

    if err != nil {
        return "", fmt.Errorf("HTTPリクエストに失敗しました: %w", err)
    }
    if resp.IsError() {
        return "", fmt.Errorf("APIエラー: status=%s, body=%v", resp.Status(), errRes)
    }

    return resBody.AuthToken, nil
}

失敗したテスト

最初のテストコードでは、以下のようにモックサーバーを作成していました

// モックサーバーのセットアップ
mux := http.NewServeMux()
mux.HandleFunc("/v1/auth/token", func(w http.ResponseWriter, r *http.Request) {
    assert.Equal(t, http.MethodPost, r.Method)
    // リクエストボディのテスト

    // レスポンス生成
    w.WriteHeader(http.StatusOK)
    response := map[string]string{"authToken": "test-auth-token"}
    _ = json.NewEncoder(w).Encode(response)
})

token, err := client.getAuthToken()
require.NoError(t, err)
assert.Equal(t, "test-auth-token", token)

テスト結果

Error:        Not equal: 
              expected: "test-auth-token"
              actual  : ""

成功したテスト

テストのモックサーバーでContent-Typeヘッダーを明示的に設定することで解決しました

// モックサーバーのセットアップ
mux := http.NewServeMux()
mux.HandleFunc("/v1/auth/token", func(w http.ResponseWriter, r *http.Request) {
    assert.Equal(t, http.MethodPost, r.Method)
    // リクエストボディのテスト
    
    // レスポンス生成
    w.Header().Set("Content-Type", "application/json") // 【追加】この行を追加することでレスポンスをJSONとして扱うようになる
    w.WriteHeader(http.StatusOK)
    response := map[string]string{"authToken": "test-auth-token"}
    _ = json.NewEncoder(w).Encode(response)
})

token, err := client.getAuthToken()
require.NoError(t, err)
assert.Equal(t, "test-auth-token", token)

なぜContent-Typeが必要なのか

RestyのRequest.SetResultRequest.SetErrorはレスポンスヘッダーのContent-Typeに基づいて構造体へのアンマーシャルが行われるため

Out of the box, Resty does response automatic unmarshaling for JSON and XML based on the response header Content-Type with methods Request.SetResult or Request.SetError are used.

https://resty.dev/docs/response-auto-parse/

感想

AIに書かせると実装が早いが、詰まったときのリカバリは返って時間が掛かって結局自分で書いたのと同じくらいの時間になりがち。AIに実装を任せることで細部への理解が甘くなるからこそ、テストが疎かになりがちな個人開発でもテストを書いてもらうようにしたい。

でも、このブログも50%くらいはAIに書いてもらったので、AIくんとはうまく付き合っていきたい。

CloudFrontでLambdaのFunction URLをオリジンにしたときUseOriginCacheControlHeadersのキャッシュポリシーを使うとエラーになってしまう

困っていたこと

「CloudFrontでオリジンにAWS Lambdaの関数URLを指定している」 かつ「キャッシュポリシーに UseOriginCacheControlHeaders を指定している」ときにLambda関数が実行されずにCloudFrontからエラーレスポンスが返ってきてしまう

レスポンス内容

  • ステータスコード: 403 Forbidden
  • レスポンスボディー: {"Message":null}
  • X-Cacheヘッダー: Error from cloudfront

原因

UseOriginCacheControlHeadersを利用するとCloudFrontのオリジンに Originヘッダー が送信されてしまうため、Lambda関数にリクエストが到達できない。

  • キャッシュキーに含まれるヘッダー:
    • Host
    • Origin
    • X-HTTP-Method-Override
    • X-HTTP-Method
    • X-Method-Override

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-policy-origin-cache-headers

オリジンリクエストポリシーは気にしていたが、キャッシュポリシーで Originヘッダー が付いてしまうのは盲点だった。

ちなみに、マネージドキャッシュポリシーの中でOriginヘッダーがキャッシュキーに含まれるのは次の3つです。

  • Elemental-MediaPackage
  • UseOriginCacheControlHeaders
  • UseOriginCacheControlHeaders-QueryStrings

解決方法

CachingOptimized のキャッシュポリシーを使う

今回の用途では CachingOptimized でも必要十分なキャッシュは実現できそうだった。

参考URL

GoLand 2025.1 でもモーダル形式のコミットUIを使いたい

困っていたこと

JetBrains系のIDE GoLand 2025.1 をインストールしたら、GitのコミットUIが左ペインの非モーダル形式になってしまった。以前から使っているモーダル形式のUIを利用したい。

IntelliJ IDEA 2025.1 (日本語) では 設定 > 詳細設定 > バージョン管理.Gitモーダルのコミットインターフェースを使用するという項目が表示されるが、GoLand 2025.1 にはその項目もなく切り替えができない。

IntelliJ IDEA 2025.1 の設定画面
IntelliJ IDEA 2025.1 では「モーダルのコミットインターフェースを使用する」の項目がある
GoLand 2025.1の設定画面
GoLand 2025.1 だと「モーダルのコミットインターフェースを使用する」の項目がない

解決方法

Git Modal Commit Interfaceのプラグインをインストールすれば、「モーダルのコミットインターフェースを使用する」の項目が表示される。
しかし、今後は使えなくなるかもしれない(後述)。

plugins.jetbrains.com

解説

IntelliJ IDEA 2025.1の新機能に関連する変更が書かれている。

コミット動作に関する更新

早期アクセスプログラムで受け取った幅広いフィードバックに応えて、非モーダルのコミット(デフォルトで組み込まれているオプション)のワークフローを改良しています。 モーダルコミットインターフェースがプラグインとなり、Settings(設定)| Advanced Settings(高度な設定)| Version Control. Git(バージョン管理. Git)で有効化できるようになっています。 このプラグインはバージョン 2025.1 では IDE にバンドルされているため、手動でインストールしなくてもインターフェースを使用し続けることができます。

IntelliJだと2025.1にプラグインがバンドルされているようだが、GoLandにはバンドルされていないようだ。他言語のIDEはどうなんだろう?(調べてない)

さらに調べると、Modal commit moved to a separate plugin in 2025.1のページを見つけて、このページをGeminiに要約させると次のような結果になった。

IntelliJ IDEA 2025.1 におけるモーダルコミット機能の別プラグインへの移行に関する議論の要約
JetBrainsは、IntelliJ IDEAのバージョン2025.1において、従来のモーダル(ダイアログ形式)のコミット機能を標準から外し、別プラグインとして提供することを決定しました。

変更の背景:

  • パフォーマンスと体験の向上: 従来のモーダルコミットはパフォーマンス上の問題を引き起こすことがありました。
  • リモート開発への注力: モーダルコミットはリモート開発の妨げになることがありました。
  • 非モーダルコミットの改善: 数年前から導入されている非モーダル(ツールウィンドウ形式)コミットが改善され、現在では多くのユーザーにとってデフォルトとなっています。

ユーザーへの影響と対応:

  • 開発チームはこの変更が一部のユーザーに不便をもたらす可能性があることを認識しています。
  • 移行を支援するため、モーダルコミット機能は2025.1リリース時に別途インストール可能なプラグインとして提供されます。
  • ただし、このプラグインはリリース後のアップデートは予定されておらず、長期的なメンテナンスは行われません(コミュニティからの貢献は検討されます)。

ユーザーからの主なフィードバック:

  • 集中できるコミット体験が失われることへの懸念。
  • 非モーダルコミットツールウィンドウに関する問題点の指摘。
  • 既存のワークフローが中断されることへの不満。
  • プラグインの長期的なメンテナンスに対する不安。
  • モーダル/非モーダルのインターフェースを選択できるようにしてほしいという要望。

開発チームの対応:

  • ユーザーからのフィードバックを真摯に受け止め、非モーダルコミットの体験改善に取り組んでいます。

ということで、モーダル形式のコミットインターフェースはまだ使えるが、いずれ使えなくなるかもしれないということでした。

非モーダル形式のインターフェースに慣れようかと少し使ってみたけど、コミット後に左ペインにツールウインドウが残り続けてしまうのが今までと比べると慣れない感じ。表示モードを アンドック にすることで多少軽減はしそうなので、しばらく使ってみようと思います。

MySQLでJSONの配列から最大値や配列の要素数を取得したい

困っていたこと

例えば次のレコードのようにDBにJSONを文字列として入れるときがある。
このとき、JSONの配列から要素数や特定フィールドの最大値を取得したい。

sample

id json_data
1 [{"name": "sato", "age": 29}, {"name": "suzuki", "age": 23}]
2 [{"name": "sato", "age": 29}, {"name": "suzuki", "age": 23}]

解決方法

JSON_TABLE関数を使う

dev.mysql.com

例えば、次のようなクエリを書くと、

SELECT *
FROM sample,
JSON_TABLE(
  json_data,
  '$[*]' COLUMNS(
    name VARCHAR(255) PATH '$.name',
    age INT PATH '$.age'
  )
) AS json

JSONが展開されて別テーブルをJOINしたような結果が得られる。

id json_data name age
1 [{"name": "sato", "age": 29}, {"name": "suzuki", "age": 23}] sato 29
1 [{"name": "sato", "age": 29}, {"name": "suzuki", "age": 23}] suzuki 23
2 [{"name": "tanaka", "age": 26}, {"name": "sasaki", "age": 24}] tanaka 26
2 [{"name": "tanaka", "age": 26}, {"name": "sasaki", "age": 24}] sasaki 24

ここまで出せれば、あとはグルーピングすればいいだけなので

クエリ

SELECT
    id,
    MAX(age) AS max,
    COUNT(*) AS count
FROM sample,
JSON_TABLE(
  json_data,
  '$[*]' COLUMNS(
    age INT PATH '$.age'
  )
) AS json
GROUP BY id

結果

id max count
1 29 2
2 26 2

おまけ: 対応状況

Geminiに質問したところ、JSON_TABLEはMySQL 8.0以降で使えるが他の主要RDBMSでも同等の関数が存在しているとのこと。

  • Oracle Database: バージョン12.1以降で利用可能です。
  • PostgreSQL: バージョン12以降で利用可能です。
  • DB2: バージョン11.1以降で利用可能です。
  • SQL Server: バージョン2016以降で利用可能です。

ジョハリの窓を使ってコミュニケーションロスを考える

この記事は はてなエンジニア Advent Calendar 2024 12/39の記事 2025/1/8の記事です。
昨日は id:kk__777 さんの IntelliJ IDEA の progress bar を 犬にする pluginでした。
こんなかわいいプログレスバーなら何時まででも待てそうですね!

本当は年末年始でなんかやろうと思っていたけど、思った以上にゆっくりしてしまって書くことがないので、去年を振り返ってコミュニケーションが不十分なことが原因でミスに繋がったことがあったことを思い出し、どういうコミュニケーションが必要なのかを書きます。

関わるプロジェクトはに参加しているメンバーはエンジニアだけではなく、自分と異なる職種の人と一緒に1つのプロジェクトを進めている。 職種が違えば視点も違うので、同じ物事を見ていても自分とは異なる見方をしていて齟齬が生まれてしまうことがある。
それはエンジニア同士でも、サーバサイドエンジニアとアプリエンジニアでも言えることで、同じGraphQLのスキーマを見ていても、前提条件が異なると期待した動作にならないことがある。

例えば、「マンガが購入可能かどうかを示すフィールドが真だったので購入リクエストを送ったところ、ポイント不足でエラーになってしまった」というケース。 購入可能という状態の認識に齟齬が生まれていて「必要なポイント数を所持している」という条件を含めるかどうかがすり合わせできていなかったことで起こる問題だ。

ジョハリの窓

自分はこういうすれ違いが起きないようにするため、普段から ジョハリの窓 に置き換えて考えることがある。ジョハリの窓は自己分析で用いられるものだが、対象をプロジェクトやシステムにすることで認識を合わせるためにも応用できるツールだと思っている。

ジョハリの窓

プロジェクトで考えると、
開放の窓: 全員が知っている情報
秘密の窓: 自分だけが知っていて、他の人は知らない情報
盲点の窓: 他の人は知っているが、自分が知らない情報
未知の窓: 誰も知らない情報 と考えることができる。

コミュニケーションによって適切な情報共有ができると、矢印のように解放の窓を広げていくことができる。
しかし、ここで気をつけなければいけないのは自分から伝えるだけでは不十分ということだ。一方的な伝達だけだと秘密の窓から開放の窓を広げる下向きの矢印はできるが、盲点の窓から開放の窓を広げる右向きの矢印の効果は得られない。

どんなアクションができるか?

どんな情報のすり合わせが必要か分かったところで、それを適切に伝えたり、受け取ったりできないと意味がない。 普段自分がどんなことに気をつけているかというと、世間的にも良く言われていることをやっているだけで何か特殊な方法を用いているわけではない。

  • 具体例を織り交ぜて会話する
    • こういう状態でリクエストを送るとこういうレスポンスが返るというような例があった方が相手の理解度が高まる。実装する際のテストケースにもなるので一石二鳥!!
  • 聞いたことを自分の言葉で言い換えて確認する
    • 聞いた内容にちょっとでも不安があるときは、こういうことですか?と聞き返す。自分の言葉で説明できるくらいになっていればその情報は開放の窓になったと十分言えるはずだ。
    • ちゃんと聞いてなかったのかと怒られることを心配するひともイルかもしれないが、正しく理解しようとする姿勢が相手にも伝わることはむしろポジティブに捉えられることも多い。
  • 話した内容を文章に残しておく
    • 伝えるという面では会話の方が有利だが、時間が経つにつれて記憶は曖昧になって、一度開放の窓にあった内容が秘密の窓や盲点の窓になってしまうこともある。あとから見返せるようにテキストで残しておこう

まとめ

チームで進めて行く以上はコミュニケーションは必要不可欠なことだし、それが原因ですれ違いが起きると戻り工数も発生して勿体ないことになってしまう。 コミュニケーションにちょっと気をつけるだけで大きなリスクを回避できることも多いので話す側としても聞く側としても当事者意識を持って取り組みましょう。