@ledsun blog

無味の味は佳境に入らざればすなわち知れず

ChatGPTが考える「AI導入成熟度モデル(AIMM)」

これはChatGPTにCMMを参考にしてAI導入レベルを書かせた怪文書です。 見出しを絵文字で修飾するのがChatGPTっぽいです。

レベル1~3は想像より、的を射ていて面白いです。 レベル4以上は、現実世界ではまだ実現できていません。 レベル5が達成できるとしたら、夢があります。


🧭 AI導入成熟度モデル(AIMM)

AI Adoption Maturity Model
― 組織はどの段階でAIと共に学んでいるのか ―

📘 概要

AI導入は、もはや一部の先進企業だけの試みではなく、あらゆる組織にとって避けられない経営テーマとなっている。 しかし、その多くは、単なるツール導入にとどまり、「生産性向上」「業務効率化」という言葉の背後にある組織の学習能力の変化を捉えきれていない。

AI Adoption Maturity Model(AIMM)は、組織がAIをどのように受容・定着・再定義していくかを5段階で示す枠組みである。 本モデルはCMM(Capability Maturity Model)の思想を踏まえ、AI導入を単なる技術投資ではなく、知的成熟のプロセスとして再定義する。

🥇 レベル1:Ad-hoc(場当たり的導入)

概要

個々の社員が自主的にAIツールを試用する段階。
導入は散発的で、組織としての戦略や評価軸は存在しない。

特徴

  • 個人がChatGPTやClaude等を独自に利用
  • 成果共有・ナレッジ化が行われていない
  • 「便利そうだから」という動機
  • ガバナンス・セキュリティ未整備

リスク

  • 属人化の加速
  • 効果が可視化されない

代表的フレーズ

「とりあえずPro課金してみた」

🥈 レベル2:Repeatable(再現可能な導入)

概要

一部部署やプロジェクト単位でAIツールを正式導入し、部分的な成果を得る段階。
PoC(概念実証)が多く、“導入実績”を重視する傾向にある。

特徴

  • 定型業務(議事録、ドキュメント要約、コードレビューなど)での利用
  • 成果は限定的だが、「AI導入済み」としての評価が社内外に広がる
  • KPIは「工数削減」「便利さ」など定性的

リスク

  • 効果測定が曖昧なまま投資が拡大
  • “PoC疲れ”による形骸化

代表的フレーズ

「AI導入で○時間削減!」

🥉 レベル3:Defined(定義された運用)

概要

AI活用がプロセスとして定義され、全社的なルール・ガイドラインが整備される段階。
利用方針・instruction・教育プログラムが文書化されるが、実運用は追いついていない場合が多い。

特徴

  • 利用ルール・instructionを正式策定
  • 社内ガイドラインや勉強会が整備される
  • 部署ごとの運用が標準化されるが、形式的になりがち

リスク

  • 「ルールを作ること」自体が目的化
  • 本質的なスキル育成やプロセス改善に結びつかない

代表的フレーズ

「AI利用方針はありますが、現場では誰も見ていません」

🏗 レベル4:Managed(効果が計測・管理される)

概要

AI導入の成果が数値化され、経営層が意思決定指標として扱う段階。
AIが業務基盤に統合され、業務プロセスの一部として運用される。

特徴

  • PR処理時間・ドキュメント作成時間・問い合わせ応答時間などがKPI化
  • instruction更新・モニタリング体制が確立
  • データガバナンスと権限管理が制度化
  • “AIエージェント”が日常業務に常駐

リスク

  • 生産性の定量化が目的化し、「思考の深さ」が犠牲になる。
  • AIへの依存が進み、“AIなしで仕事ができない人”が生まれ始める。

代表的フレーズ

「AIエージェントなしでは業務が回らない」

🧠 レベル5:Optimizing(共進化と再自立)

概要

AIを単なる補助ではなく、知的共創パートナーとして再定義する段階。
AIエージェントがOJTの一部を代行し、人間は創造・内省・設計へと再集中する。

特徴

  • AI活用を通じた組織知の循環が確立
  • AI instructionを人間が再教育する文化が生まれる
  • 「AI依存」から「AI共進化」へ移行
  • 脱AIリテラシー教育(AIなしでの思考訓練)を重視

成熟した組織の姿

  • AIを“業務効率化の道具”ではなく、“知的筋力を鍛える装置”として位置づける
  • 「AIがあるからこそ、AIに頼らず考える」文化が定着する

代表的フレーズ

「AIに教えられる人ではなく、AIを教育できる人を育てる」

🪜 レベル別サマリー

レベル 名称 主な特徴 組織の姿勢
1 Ad-hoc 個人で使うだけ 興味本位
2 Repeatable 試験導入 宣伝・PoC志向
3 Defined ルール整備 管理と形式化
4 Managed 効果測定 経営主導の最適化
5 Optimizing 共進化 自律的再設計・文化化

🎯 結論

AI導入の成熟度は「技術的スキル」ではなく、
組織が“知的自立”をどこまで維持できるかで決まる。

つまり、最終段階(レベル5)では、
AI導入とは「AIに頼らない力を取り戻すためのプロセス」である。

AIに頼らず、AIを通じて人間を鍛える文化をつくること。
それが真に成熟したAI組織の姿である。

AIエージェントよりAIペアプロが好き

はじめに

小さなツールをつくる機会があり、その中の1ステップの仕様が明確になったので、AIエージェントを使って実装してみました。

自分で書きはじめた方が速い

想像以上に遅く感じました。 実装の場所も方法も決まっているのに、AIエージェントが調べている間じっと待っているのがストレスでした。 AIエージェントのレスポンスを待つよりも、自分でコードを書き始めてCopilotに補完してもらう方が速く感じます。

私が生成AIに求めているのは「作業代行」でなく「ペアプロの相手」のようです。

AIエージェントの使い方?

「設計を全部終わらしてからAIエージェントに頼む」は、AIエージェントの上手い使い方ではなかったのかもしれません。 個人的には「いまの生成AIからはクラス設計のセンスを感じられない。内部設計は任せたくない…」と感じています。 それで設計まで終わらせて実装をお願いしました。

たとえば、タスクのゴールまで決めて、会議中に裏で「このタスクの実装やっといて」と任せる使い方の方がよいのかもしれません。

タスク単位で依頼する時は、人間のエンジニア相手でも難しさがあります。 依頼したタスクが想定よりも時間がかかることがあります。 そういうときはエンジニアのスキル不足より「依頼の仕方が間違っている」ことが多いです。 よく見てみると、簡単なつもりで難しいタスクを依頼していたり、そもそも間違った方針を伝えていたりします。 「お願いする側のタスク設計の精度」が作業効率に影響します。 AIエージェントに対しても似たことがおきそうです。

人間のエンジニアは学習します。 無茶振りしていると「この依頼者は適当なことを言う」と学習してくれます。 期待しすぎるとパワハラになりますが、この期待は0ではないです。 最近の生成AIは忖度しがちです。 負のフィードバックを得にくいかもしれません。

人間よりも生成AIのほうが依頼前の「完遂できるタスク」の準備に気を遣いそうです。

AIペアプロの価値

人間とペアプロするとめちゃくちゃ疲れます。 プログラミングについて深く考えているからという面もありますが、気にすることが多過ぎます。

  • 前提を共有できているか?
  • この言い方で通じるか?
  • 相手の理解と自分の理解がどう違うのか?
  • ドライバーとナビゲーターをいつ交代すればいいのか?

これらをずっと考え続けながらプログラミングのことを考えます。 開発全体を考えると背景情報を共有する効果があります。 プログラミングを目的としたら非効率です。

AIとのペアプロなら、人間を相手にするときの気疲れがありません。 試行錯誤を気軽に続けられます。 「これはたぶんダメなアイデアなんだけど、軽く試してダメなことを確認しておきたい」ときも、ペアの気分を気にせずに試せます。

おわりに

今回の試行では「僕なりのAIエージェントの使い方」は見つけられませんでした。

AIは学習しません。タスクをこなしても経験が蓄積されません。 ジュニアエンジニアは成長し、長期的にはチームに貢献してくれます。 生成AIはモデルは成長していくのですが、誰もが同じモデルを使えます。 エンジニアリング組織の差別化には使えません。

世の中では、相対的にジュニアエンジニアが割高に見えているはずです。 ジュニアエンジニアはAIエージェントと比べたら20~30倍の費用が掛かるので気軽に雇えません。 いまの「ジュニアエンジニアを雇える立場」は「ジュニアエンジニアを育成できる環境」ともいえます。 このままAIエージェントが流行ってくれれば「ジュニアエンジニアを雇って、育てられる」が、エンジニアリング組織の参入障壁として機能し、差別化要因になりそうです。

さて勝ち筋はあるかな?

Kaigi on Rails 2025 参加メモ(Day2)

小規模から中規模開発へ、構造化ログからはじめる信頼性の担保(kakudooo)

内容

小規模から中規模へ 構造化ログからはじめる信頼性の担保 - Speaker Deck

感想

  • ステップの説明が丁寧で理解しやすかった
  • 技術選定するときに候補を3つ挙げるのが良い
  • 無理にフックを仕込まなくても面白い発表でした

参考リンク

Range on Rails ― 「多重範囲型」という新たな選択肢が、複雑ロジックを劇的にシンプルにしたワケ(梅田智大)

内容

  • PostgreSQL 14 以降で使える多重範囲型(multirange)を紹介
  • 予約システムの予約枠は範囲
  • 予約枠(範囲)に集合演算できると「空き枠」を探すときにめっちゃ便利
  • SQLビューでラップするとRailsからは透明に使える

感想

  • 知らない技術を実用例込みで知れて勉強になった
  • Pure Rubyの多重範囲型を演算できるライブラリーを作ったら面白そう

非同期処理実行基盤、Delayed脱出〜SolidQueue完全移行への旅路。(Shohei Kobayashi)

内容

非同期処理実行基盤 Delayed脱出 → Solid Queue完全移行への旅路。 - Speaker Deck

  • Delayed Job から Solid Queue** への移行事例
  • Rails 7.2 の enqueue_after_transaction の有効性
  • 監視機能が内のを自作でカバー

感想

  • 教科書のようなSolid Queueの紹介事例
  • 最初に扱う問題領域を明確にしているのが良い
    非同期処理の分類を説明するスライド
  • Solid Queueを使おうかな?って思った人は、一回は読むと良さそう

Railsだからできる、例外業務に禍根を残さない設定設計パターン(ei.ei.eiichi)

内容

  • 「坂の途中」での例外的な業務ロジックへの対応方法を紹介

感想

  • キーノートのmoroさんwayを地で行く感じで良かったです
  • 速すぎておいていかれたので、資料を公開してほしいです
  • 「UNDO機能が無くても履歴を見せよう」って発想が参考になりました

ドメイン指定Cookieとサービス間共有Redisで作る認証基盤サービス(黒曜)

内容

ドメイン指定Cookieとサービス間共有Redisで作る認証基盤サービス

感想

  • 解きたい課題が「すでにあるサービスに認証基盤を追加する」で面白かった
  • Redis Session Storeを流用するのと、認証基盤に認可情報を返すAPIを足すのはどっちが楽なんだろう? 

参考リンク

Rails on SQLite: exciting new ways to cause outages(André Arko)

内容

Rails on SQLite: exciting new ways to cause outages - Speaker Deck

  • 会場では全然分かりませんでした
  • Rails on SQLite: exciting new ways to cause outagesスクリプトがあります
  • feedyour.emailで得られたRails on SQLiteの全知見大公開
  • 月100万リクエストに耐えている
  • SQLiteのファイルを複数プロセスで共有するのはむずかしい
    • バックグラウンドジョブはスレッド(litejob)で処理する
  • SQLiteは1ファイルなので書き込みが読込をブロックする
    • WAL(Write Ahead Logging)オプションを使うと読込ブロックを解消出来る
  • 書き込み同士はブロックする
    • Railsのキャッシュやジョブなどでファイルをわけていくアプローチが有効
  • 原則、水平スケーリングはできないので垂直スケーリングしていくこと
    • 現代はハイスペックなサーバーインスタンスが使えるので1台でも結構がんばれる
  • 1インスタンスなので壊れたら止まる
    • Litestremを使えばリアルタイムにバックアップがとれる
    • 壊れた時点のデータで復元できる
  • 1プロセスなので入り口の負荷分散は難しい
    • CDNキャッシュは有効
  • 1サーバーだと保守・監視が楽

感想

参考

rails g authenticationから学ぶRails8.0時代の認証(Willnet)

内容

rails g authenticationから学ぶRails8.0時代の認証 - Speaker Deck

  • Rails 8で導入された認証ジェネレーターの紹介
  • Devise / Warden に頼らない
  • メールアドレス+パスワードの基本形のみ提供
  • generatorであり、gemではない。
    • 脆弱性が出たらgem のアップデートでは終わらない
  • has_secure_passwordCurrentAttributes の活用例として参考になる
  • ActionCable用の認証コードも生成される

感想

  • BCrypt の「遅くできる」設計が面白い
  • タイミングアタックはふつうは、攻撃方法の存在に気づけないよなあ
  • お子さんが応援されているの、ほっこりして良かったです

Keynote: Building and Deploying Interactive Rails Applications with Falcon(Samuel)

内容

Aysncからの歴史を含んだFalconの紹介

  • つぎつぎと紹介される自作Gemの数々
  • Agant csntextで生成したAIエージェントむけインストラクションをつかったライブでも

感想

  • RubyにAsync入れたい」からはじめて必要なものを全部作ってFalconつくってShopify入って実運用して数億円節約して価値を示す技術力で全部ぶっとばしていくスタイルがエンジニアとしてかっこよすぎる
  • 発表中に紹介されるgemが全部自作なのウケる
    • すごすぎて一回宇宙猫になったあとウケるしかなくなる

参考

🎤 Closing

  • 来年は ベルサール渋谷ガーデン で開催予定
  • 参加者1,000人規模。
  • 2025年10月16〜17日
  • より国際化を進める

devContainer環境作成失敗 解決編

devContainer環境作成失敗 - @ledsun blog の続きです。

snapの/tmpはprivate

ledsun@xps24nov:~/devContainerTest►docker compose --project-name devcontainertest_devcontainer -f /home/ledsun/devContainerTest/.devcon
tainer/docker-compose.yml -f /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1
ef8d1-864d-4f75-93a2-7852df5b90e8.yml up -d
open /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1ef8d1-864d-4f75-93a2-7852df5b90e8.yml: no such file or directory

が失敗していました。

open /tmp/(中略).yml: no such file or directory

dockerコマンドから/tmp配下のファイルが見えません。 本当でしょうか?

条件を変えて確認してみましょう。

ledsun@xps24nov:/t/d/docker-compose[1]►snap run --shell docker -c 'ls -l /tmp'
total 0
ledsun@xps24nov:/t/d/docker-compose►ls -l /tmp
total 132
drwxr-xr-x 4 ledsun ledsun 4096 Sep 28 17:15 devcontainercli-ledsun/

やはり/tmp内容が違って見えます。

snapはアプリケーション単位で独立して環境にインストールすることで、セキュリティリスクを軽減する方針のようです。 その一環として/tmpもアプリケーション単位で隔離されているみたいです。

また、DevContainerはsnapをサポートしないことが明示されていました。

vscode-docs/docs/devcontainers/containers.md at 513424f40cb523f4fac0ab4a430684377c36891c · microsoft/vscode-docs · GitHub

Linux: Docker CE/EE 18.06+ and Docker Compose 1.21+. (The Ubuntu snap package is not supported.)

dockerコマンドをインストールし直すと良さそうです。

dockerをaptでインストール

ひとまずアンインストールします。

sudo snap remove docker

インストールし直します。 Ubuntu | Docker Docs を参考にします。

念のため環境をクリーンにします。

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done

aptリポジトリを追加します。

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

インストールを実行します。

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

これで

docker compose --project-name devcontainertest_devcontainer -f /home/ledsun/devContainerTest/.devcon
tainer/docker-compose.yml -f /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1
ef8d1-864d-4f75-93a2-7852df5b90e8.yml up -d

が成功します。

しかし、VSCodeからDevContainer として起動するとエラーが起きます。 docker-copomseの設定が足りていません。

docker-compose.ymlの修正

docker-compose.yml を次のように直します。

services:
  web:
    image: ubuntu:20.04
    command: sleep infinity
    volumes:
      - ..:/workspaces/devcontainer-test

echo "DevContainer test"では、VSCodeが接続する前に終了するので sleep infinity とし、無限に待ち受ちます。

VSCodeがDockerに接続した後に開くワークスペースが必要です。

volumes:
  - ..:/workspaces/devcontainer-test

で、プロジェクトルートをマウントします。

起動

Dev Container: Reopen in Containerに成功したスクリーンショット

devContainer環境作成失敗

環境

現象

VS CodeDev Container: Reopen in Containerを実行すると次のエラーが出ます。

[3833 ms] Error: Command failed: docker compose --project-name devcontainertest_devcontainer -f /home/ledsun/devContainerTest/.devcontainer/docker-compose.yml -f /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1ef8d1-864d-4f75-93a2-7852df5b90e8.yml up -d

再現方法

devcontainer.json

{
  "name": "DevContainer test",
  "dockerComposeFile": "docker-compose.yml",
  "service": "web",
  "workspaceFolder": "/workspaces/DevContainer_test"
}

docker-compose.yaml

services:
  web:
    image: ubuntu:20.04
    command: echo "DevContainer test"

確認したこと

docker-compose.yaml単体での成功

ledsun@xps24nov:~/devContainerTest►docker compose -f .devcontainer/docker-compose.yml up
[+] Running 1/1
 ✔ Container devcontainer-web-1  Created                                                                                          0.0s
Attaching to web-1
web-1  | DevContainer test
web-1 exited with code 0

docker composeコマンドでの失敗

ledsun@xps24nov:~/devContainerTest►docker compose --project-name devcontainertest_devcontainer -f /home/ledsun/devContainerTest/.devcon
tainer/docker-compose.yml -f /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1
ef8d1-864d-4f75-93a2-7852df5b90e8.yml up -d
open /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1ef8d1-864d-4f75-93a2-7852df5b90e8.yml: no such file or directory

docker-composeファイルの存在確認

ledsun@xps24nov:~/devContainerTest[1]ls -la /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1
759046001713-5c1ef8d1-864d-4f75-93a2-7852df5b90e8.yml
-rw-r--r-- 1 ledsun ledsun 471 Sep 28 16:53 /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1ef8d1-864d-4f75-93a2-7852df5b90e8.yml

読書記録:「原論文から解き明かす生成AI」

1. 読書の目的とスタンス

  • 精読ではなく、まずは全体をさらっと把握する
  • 「オタクが推し(論文)を語っている」のを聞く姿勢で「よくわからないことを言っているが情熱は分かる」と受け止める
  • 半年後に読み直したときに自分の理解が進んでいるか比較確認するためのセーブポイント

2. 各章ごとの理解と疑問点

第2章 入力データの特徴量化

理解したこと

  • テキストは色(RGB)のように連続量で扱えない
  • 分布仮説「単語の意味は文脈で決まる」を使って特徴量化する

疑問・保留

  • サブワードがわかっていない

関連して調べたこと

  • 分布仮説の歴史
    • 1950年代 言語学
      • Zelling Harris (1954)が提唱
      • J.R.Firth (1957) が You shall know a word by the company it keeps と表現して普及
      • マーチン・ファウラーみたい
    • 1965年 この章の論文(Contextual correlates of synonymy)で実証的な検証に成功
    • 70~80年代 心理学で検証実験が流行った
    • 90年代 統計的言語処理の分野で使われる
  • この章の論文のルーベンスタイン(rubenstin)はルーベンスタイン距離のルーベンスタイン(Vladimir Levenshtein)とは別人

第3章 Transformer

理解したこと

  • RNN/CNNより長文を扱える
  • 注意機構がポイント
  • 2017年に機械翻訳用として登場

疑問・保留

  • 注意機構がわかっていない
  • RNN/CNNがわかっていない
    • RNN/CNNが分かってからの方がTransformerを理解しやすそう

第4章 GPT系列

理解したこと

  • GPT-1:Transformerと分布仮説を組み合わせたら生成AIになった
    • Transformerから交差注意を外した
  • GPT-2:zero shot学習が可能に
    • 学習データにタスクを入れると汎用性が出る
  • GPT-3 (2020):few shot学習の飛躍

    • 学習データの用意が大変
    • ChatGPTリリースは2022年11月
  • GPT-4:マルチモーダル対応

第5章~第6章 拡散モデル・テキストと画像の融合

スキップ

第7章 スケーリング則

理解したこと

  • データ量と性能が比例する「スケール則」の存在
  • GPU性能よりデータ量が大規模化のボトルネックになると予測されている

疑問・保留

  • 次回は Chain of Thought 推論について理解したい

第8章 モデル評価

スキップ

AppUserModelID を設定したショートカットを作るPoweShellスクリプトをC#にする

15分タイマー - @ledsun blog AppUserModelID を設定したショートカットをつくるPoweShellスクリプトを使いました。

スクリプトの中には明らかにC#と思われるコードが含まれています。 C#へ変換すると理解が進みそうです。 ChatGPTに変換して貰いました。

プロジェクトファイル

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <!-- Windows 10 (19041) 以降のWindows専用APIを使うためのTFM -->
    <TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
    <!-- x64固定(win-x64) -->
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
  </PropertyGroup>
</Project>

Program.cs

internal static class Program
{
  static void Main(string[] args)
  {
    if (args.Length != 3)
    {
      Console.WriteLine("使い方: ShortcutAumidSetter <ショートカット名(拡張子なし)> <実行ファイルの実パス> <AppUserModelID>");
      return;
    }

    string rawName = args[0];
    string shortcutName = Path.GetFileNameWithoutExtension(rawName); // .lnk が付いていても削除
    string exePath = args[1];
    string appUserModelId = args[2];

    string startMenu = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
    string programs = Path.Combine(startMenu, "Programs");
    Directory.CreateDirectory(programs);

    string lnkPath = Path.Combine(programs, shortcutName + ".lnk");

    // 既存ショートカットは削除
    if (File.Exists(lnkPath))
    {
      var attr = File.GetAttributes(lnkPath);
      if (attr.HasFlag(FileAttributes.ReadOnly))
        File.SetAttributes(lnkPath, attr & ~FileAttributes.ReadOnly);
      File.Delete(lnkPath);
    }

    // ヘルパーに処理を委譲
    LnkAumidHelper.WriteShortcutWithAumid(lnkPath, exePath, appUserModelId);

    Console.WriteLine($"OK: AppUserModelID set -> {lnkPath}");
  }
}

LnkAumidHelper.cs

using System.Runtime.InteropServices;

public static class LnkAumidHelper
{
  /// <summary>
  /// lnk を新規作成または上書きし、AppUserModelID を設定して保存する
  /// </summary>
  public static void WriteShortcutWithAumid(string lnkPath, string exePath, string appUserModelId)
  {
    var link = (IShellLinkW)new CShellLink();
    var persist = (IPersistFile)link;

    // 必須情報を設定
    link.SetPath(exePath);
    link.SetWorkingDirectory(Path.GetDirectoryName(exePath) ?? "");

    // AUMID を設定
    var store = (IPropertyStore)link;
    SetAumidOnStore(store, appUserModelId);

    // 一度だけ保存
    persist.Save(lnkPath, true);
    persist.SaveCompleted(lnkPath);

    Marshal.ReleaseComObject(store);
    Marshal.ReleaseComObject(persist);
    Marshal.ReleaseComObject(link);
  }

  // === 以下は内部実装なので private ===

  [ComImport, Guid("00021401-0000-0000-C000-000000000046")]
  private class CShellLink { }

  [ComImport, Guid("000214F9-0000-0000-C000-000000000046"),
   InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  private interface IShellLinkW
  {
    int GetPath(IntPtr pszFile, int cch, IntPtr pfd, uint fFlags);
    int GetIDList(out IntPtr ppidl);
    int SetIDList(IntPtr pidl);
    int GetDescription(IntPtr pszName, int cch);
    int SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
    int GetWorkingDirectory(IntPtr pszDir, int cch);
    int SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
    int GetArguments(IntPtr pszArgs, int cch);
    int SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
    int GetHotkey(out short wHotkey);
    int SetHotkey(short wHotkey);
    int GetShowCmd(out int iShowCmd);
    int SetShowCmd(int iShowCmd);
    int GetIconLocation(IntPtr pszIconPath, int cch, out int iIcon);
    int SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
    int SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
    int Resolve(IntPtr hWnd, uint fFlags);
    int SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
  }

  [ComImport, Guid("0000010b-0000-0000-C000-000000000046"),
   InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  private interface IPersistFile
  {
    int GetClassID(out Guid pClassID);
    int IsDirty();
    int Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, int dwMode);
    int Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, bool fRemember);
    int SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
    int GetCurFile([MarshalAs(UnmanagedType.LPWStr)] out string ppszFileName);
  }

  [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
   Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
  private interface IPropertyStore
  {
    int GetCount(out uint cProps);
    int GetAt(uint iProp, out PROPERTYKEY pkey);
    int GetValue(ref PROPERTYKEY key, out PROPVARIANT pv);
    int SetValue(ref PROPERTYKEY key, ref PROPVARIANT pv);
    int Commit();
  }

  [StructLayout(LayoutKind.Sequential, Pack = 4)]
  private struct PROPERTYKEY { public Guid fmtid; public uint pid; }

  private static readonly PROPERTYKEY PKEY_AppUserModel_ID =
      new PROPERTYKEY { fmtid = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), pid = 5 };

  [StructLayout(LayoutKind.Sequential)]
  private struct PROPVARIANT
  {
    public ushort vt, w1, w2, w3;
    public IntPtr p;
    public int i1, i2;
  }

  private const ushort VT_LPWSTR = 31;

  [DllImport("ole32.dll")] private static extern int PropVariantClear(ref PROPVARIANT pvar);

  private static void SetAumidOnStore(IPropertyStore store, string appId)
  {
    PROPVARIANT pv = new PROPVARIANT
    {
      vt = VT_LPWSTR,
      p = Marshal.StringToCoTaskMemUni(appId)
    };
    var key = PKEY_AppUserModel_ID;
    store.SetValue(ref key, ref pv);
    store.Commit();
    PropVariantClear(ref pv);
  }
}

これがちゃんと動くのです。すごい。

おまかせで書かせると結構余計な機能を入れがちです。 前提条件を厳しくしてエラー処理を省かせる必要があります。 また、コードの設計はあまり上手くないので、クラス分けの仕方は明確に指示する必要があります。 とはいえ対話だけでリファクタリングできました。

動かしたらエラーは出ました。 が、一箇所だけでした。 随分、賢くなったように思います。

AIが書いてくれなかったらCOMを使うプログラムをビルドして実行していませんでした。 最初の一歩を踏み出すための障壁が減るのがとても良いです。

15分タイマー

ポモドーロタイマーです。 ユーザーを自分に限定すれば、固定時間で十分です。 パラメーターが少ないほどシンプルなUIになるはずです。

ChatGPTと相談しながら作りました。 最終的にWindows.UI.Notifications.ToastNotificationManagerをつかった通知アプリケーションになりました。

通知のスクリーンショット

最初にどんな感じになったかスクリーンショットを貼っておきます。

通知のスクリーンショット

最初のバージョン

最初はSystem.Windows.Forms.NotifyIconを使いました。 10分くらいで作れました。

internal static class Program
{
    // 固定メッセージをここで設定
    private const string Title = "お知らせ";
    private const string Message = "休憩の時間です。ストレッチしましょう!";

    [STAThread]
    private static async Task Main()
    {
        // コンソールウィンドウは表示されますが、画面(UI)は作りません
        using var notifyIcon = new NotifyIcon
        {
            Visible = true,
            Icon = SystemIcons.Information,  // 既定の情報アイコン(ico不要)
            BalloonTipTitle = Title,
            BalloonTipText = Message,
            BalloonTipIcon = ToolTipIcon.Info
        };

        // 15分待機
        await Task.Delay(TimeSpan.FromMinutes(15));

        // 5秒間のバルーン表示
        notifyIcon.ShowBalloonTip(5000);

        // 表示のため少し待ってから終了(任意)
        await Task.Delay(6000);
    }
}

ロジックもシンプルで分かりやすいです。 dotnet publishコマンドで生成されるファイルが230個位できます。 自分用アプリケーションなのでインストーラーは作りません。数百ファイルを手動で管理するのは面倒です。

ワンバイナリ化

dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true --self-contained trueコマンドを使うと一つのバイナリにまとめられます。 dotnetのランタイムを含むため100MBを越えてきます。 いいんだけど、もう少し小さくしたいです。

.NET 7 からNative AOT (Ahead-of-Time) コンパイルという技術があります。 これを使えば数MBまで小さく出来ます。 Windows.Formsには使えません。 使おうとすると次のエラーが出ます。

  DesktopNotifier 1 件のエラーで失敗しました (0.0 秒)
    C:\Program Files\dotnet\sdk\9.0.304\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(258,5): error NETSDK1175: Windows フォームに関して、トリミングの有効化はサポートおよび推奨されていません。詳細については、https://aka.ms/dotnet-illink/windows-forms を参照してください。

1.9 秒後に 1 件のエラーで失敗しました をビルド

Windows.UI.Notifications.ToastNotificationManager

Windows.Forms依存をなくすために、 System.Windows.Forms.NotifyIconに代わり Windows.UI.Notifications.ToastNotificationManager を使います。

using System.Security;
using Windows.Data.Xml.Dom;
using Windows.UI.Notifications;

internal static class Program
{
    private const string AppUserModelID = "YourCompany.DesktopNotifier";

    private const string Title = "お知らせ";
    private const string Message = "休憩の時間です。ストレッチしましょう!";

    [STAThread] // WinRT/シェル周りはSTA推奨
    private static async Task Main()
    {
        // 15分待機(デバッグ時は秒に変えてOK)
        await Task.Delay(TimeSpan.FromMinutes(15));

        // トースト(最小XML)
        string xml = $@"
<toast>
  <visual>
    <binding template='ToastGeneric'>
      <text>{SecurityElement.Escape(Title)}</text>
      <text>{SecurityElement.Escape(Message)}</text>
    </binding>
  </visual>
</toast>";

        var doc = new XmlDocument();
        doc.LoadXml(xml);

        var notifier = ToastNotificationManager.CreateToastNotifier(AppUserModelID);
        var toast = new ToastNotification(doc);
        notifier.Show(toast);

        // 送信直後に終了しても通知は出ますが、念のため少しだけ猶予
//        await Task.Delay(1500);
    }
}

ロジックは同じくシンプルです。

Windows.UI.Notifications.ToastNotificationManager で通知を表示するには制約があります。

ToastNotificationManager クラス (Windows.UI.Notifications) - Windows UWP applications | Microsoft Learn

デスクトップ アプリでトーストを表示するには、アプリのスタート画面にショートカットが必要です。 ショートカットには AppUserModelID が必要です。

ショートカットを作成する

ここからがややこしいです。 ChatGPTにPowerShellを書いてもらいました。

$ExePath = "C:\Users\led_l\DesktopNotifier\bin\Release\net9.0-windows10.0.19041.0\win-x64\publish\DesktopNotifier.exe"  # 実行ファイルの実パス
$Lnk     = "$([Environment]::GetFolderPath('StartMenu'))\Programs\DesktopNotifier.lnk"

$WshShell = New-Object -ComObject WScript.Shell
$link = $WshShell.CreateShortcut($Lnk)
$link.TargetPath = $ExePath
$link.WorkingDirectory = Split-Path $ExePath
$link.Description = "DesktopNotifier"
$link.Save()

Add-Type -Language CSharp -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;

public static class LnkAumidHelper {
    [ComImport, Guid("00021401-0000-0000-C000-000000000046")]
    private class CShellLink { }

    [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
     Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
    private interface IPropertyStore {
        int GetCount(out uint cProps);
        int GetAt(uint iProp, out PROPERTYKEY pkey);
        int GetValue(ref PROPERTYKEY key, out PROPVARIANT pv);
        int SetValue(ref PROPERTYKEY key, ref PROPVARIANT pv);
        int Commit();
    }

    [StructLayout(LayoutKind.Sequential, Pack=4)]
    private struct PROPERTYKEY { public Guid fmtid; public uint pid; }

    private static readonly PROPERTYKEY PKEY_AppUserModel_ID =
      new PROPERTYKEY { fmtid = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), pid = 5 };

    [StructLayout(LayoutKind.Sequential)]
    private struct PROPVARIANT {
        public ushort vt, w1, w2, w3;
        public IntPtr p;
        public int i1, i2;
    }
    const ushort VT_LPWSTR = 31;

    [DllImport("ole32.dll")] private static extern int PropVariantClear(ref PROPVARIANT pvar);

    [ComImport, Guid("000214F9-0000-0000-C000-000000000046"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IShellLinkW {
        int GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cch, IntPtr pfd, uint fFlags);
        int GetIDList(out IntPtr ppidl);
        int SetIDList(IntPtr pidl);
        int GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cch);
        int SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
        int GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cch);
        int SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
        int GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cch);
        int SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
        int GetHotkey(out short wHotkey);
        int SetHotkey(short wHotkey);
        int GetShowCmd(out int iShowCmd);
        int SetShowCmd(int iShowCmd);
        int GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cch, out int iIcon);
        int SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
        int SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
        int Resolve(IntPtr hWnd, uint fFlags);
        int SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
    }

    [ComImport, Guid("0000010b-0000-0000-C000-000000000046"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IPersistFile {
        int GetClassID(out Guid pClassID);
        int IsDirty();
        int Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, int dwMode);
        int Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, bool fRemember);
        int SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
        int GetCurFile([MarshalAs(UnmanagedType.LPWStr)] out string ppszFileName);
    }

    public static void SetAumid(string lnkPath, string appId) {
        // ReadWrite で開く(= 2)
        const int STGM_READWRITE = 0x00000002;

        var link = (IShellLinkW)new CShellLink();
        var persist = (IPersistFile)link;
        int hr = persist.Load(lnkPath, STGM_READWRITE);
        if (hr != 0) Marshal.ThrowExceptionForHR(hr);

        var store = (IPropertyStore)link;
        var key = PKEY_AppUserModel_ID;
        PROPVARIANT pv = new PROPVARIANT { vt = VT_LPWSTR, p = Marshal.StringToCoTaskMemUni(appId) };
        try {
            Marshal.ThrowExceptionForHR(store.SetValue(ref key, ref pv));
            Marshal.ThrowExceptionForHR(store.Commit());
            // 念のため明示保存
            Marshal.ThrowExceptionForHR(persist.Save(lnkPath, true));
            persist.SaveCompleted(lnkPath);
        }
        finally {
            PropVariantClear(ref pv);
            Marshal.ReleaseComObject(store);
            Marshal.ReleaseComObject(persist);
            Marshal.ReleaseComObject(link);
        }
    }
}
"@

# 事前に読み取り専用属性が付いていたら外す
if (Test-Path $Lnk) { attrib -R $Lnk }

# AppUserModelID を設定
[LnKAumidHelper]::SetAumid($Lnk, "YourCompany.DesktopNotifier")
"OK: AppUserModelID set -> $Lnk"

アプリケーション本体の2倍を超えるインストールスクリプトです。 AppUserModelID はCOMを使わないと設定できないようです。

プロジェクトファイル

DesktopNotifier.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
  </PropertyGroup>
</Project>

その他の注意点

Native AOT (Ahead-of-Time) コンパイルにはC++の開発環境が必要です。 Visual Studioで言うと「C++ によるデスクトップ開発」 ワークロードをインストールしてある必要があります。

まとめ

ショートカットキーが必要になったので、結果的にキーボードから起動できるようになりました。 ちょっとお得な気分です。

AIを使うと要件を絞った小さいアプリケーションを短時間で作れます。 AIの力をかりて、自分専用の小さいアプリケーションを粗製乱造していくのが、結構いい体験になりそうな予感がしました。

なおこのアプリケーションは次の講演をみて「自分の中のアプリケーションをつくる敷居をもっと下げた方がいい」と気がついてつくりはじめました。

speakerdeck.com

いい講演でした。

大吉祥寺.pm 2025に参加した #kichijojipm

kichijojipm.connpass.com

本編

発表

2025年になってもまだMySQLが好き by yoku0825

MariaDBの現況を知れました。

【実演版】カンファレンス登壇者・スタッフにこそ知ってほしいマイクの使い方 by Arthur

言われてみれば指向性マイクですよね。

提案レベルを上げてみたら、私の『提案』が『進捗』になっていた件 by なってぃ

自分の経験を話す発表、好きです。

今!ソフトウェアエンジニアがハードウェアに手を出すには by macopy

大吉祥寺.pmに行くと本を買ってしまいます。

機能追加とリーダー業務との類似性 by Rinchoku

分野Aでやっていたことを抽象化して分野Bに提供するの難しいですよね。

ChatGPT、Gemini、Claude は、なぜ似たようなUIを採用しているのか by ふわり

リリース当初からChatGPTのUIは完成されていましたね。

小さな開発会社を作った理由 (再) by polidog

昨年は体調不良で代打をお願いしたそうです。 リベンジ出来て良かったです。

コミュニティはいつまでも続くわけではない - PostgreSQLコミュニティの危機 by 曽根 壮大

それはそう

地方でエンジニアコミュニティを成功させる秘訣:”身内ノリ”を避け、”熱狂”を生み出すための挑戦 by こうの

すごい熱量でした。

2025 年のコーディングエージェントの現在地とエンジニアの仕事の変化について by azukiazusa

大吉祥寺.pmに行くと本を買ってしまいます。 2回目。

大「個人開発サービス」時代に僕たちはどう生きるか by sotarok

自分がアプリケーションを作る時のハードルを上げすぎていたことに気がつきました。 ワンタッチで動く15分タイマーが欲しかったのを思い出し、その場で作りはじめました。

懇親会

大吉祥寺.PM 非公式懇親会〜超うまいクラフトビール

luma.com

ビールもだけど、ご飯もおいしかったです。

きちぴーナイトパーティー 2025【非公式】

connpass.com

終電にて帰りました。

プロファイラっぽい動物

osyouさんが「プロファイラっぽい動物」を募集していたので考えてみました。

「ヒトヘルペスウィルス6」

人体の疲労定量的に計測する方法に、唾液中のヒトヘルペスウィルス6の量を計測する方法があるそうです。 詳しくはブルーバックスの「疲労とはなにか」をご覧ください。

人体の疲労の仕組みについて解説したとても面白い本です。

人体の疲労具合を数値化できるし、増えすぎると人体の機能を阻害する。 とてもプロファイラっぽい動物だと思いませんか?

ただし、ヒトヘルペスウィルス6をマスコットキャラクターとして扱うにはいくつか問題があります。

  • 一つめは、既存のイメージが悪い点です。
  • 二つめは、ヒトヘルペスウィルス6の画像を見て、それがヒトヘルペスウィルス6であることが分かる人が少ない点です。
  • 最後の点は、ヒトヘルペスウィルス6は動物ではないことです。

レギュレーション違反でした。 おしまい。

wasmtime-dotnetを使ってruby.wasmを実行する

前提条件

手順

プロジェクトを作成

dotnet new console -n wasmintro
cd wasmintro
dotnet add package Wasmtime

wasmtime向けruby.wasmバイナリを用意する

# 1. ダウンロード
Invoke-WebRequest `
  -Uri https://github.com/ruby/ruby.wasm/releases/latest/download/ruby-3.4-wasm32-unknown-wasip1-full.tar.gz `
  -OutFile ruby-3.4-wasm32-unknown-wasip1-full.tar.gz

# 2. 展開
tar -xvzf ruby-3.4-wasm32-unknown-wasip1-full.tar.gz

# 3. ruby.wasm を移動(元の ruby 実行ファイルをリネーム)
Move-Item `
  -Path "ruby-3.4-wasm32-unknown-wasip1-full/usr/local/bin/ruby" `
  -Destination "ruby.wasm"

https://github.com/ruby/ruby.wasm/?tab=readme-ov-file#quick-example-how-to-package-your-ruby-application-as-a-wasi-application に記載されているwasmtimeで動かす手順を参考にしてます。

ruby.wasmでは何種類かのwasmバイナリを作ります。 例えば rake npm:ruby-head-wasm-wasi で作る packages/npm-packages/ruby-head-wasm-wasi/dist/ruby.wasm を使うとうまく行きません。 このwasmバイナリにはJavaScriptでホストするための関数が定義されています。 このwasmバイナリをインスタンス化するには、これらの関数にC#側のスタブ関数を紐付ける必要があります。 紐付けないと次のようなエラーメッセージ表示されます。

Unhandled exception. Wasmtime.WasmtimeException: unknown import: `rb-js-abi-host::rb_wasm_throw_prohibit_rewind_exception` has not been defined
   at Wasmtime.Linker.Instantiate(Store store, Module module)
   at Program.<Main>$(String[] args) in C:\Users\led_l\wasmintro\Program.cs:line 18

wasmtimeで動かしやすいwasmバイナリを選ぶ必要があります。

ソースコード

using System;
using Wasmtime;

var engine  = new Engine();
var module  = Module.FromFile(engine, "ruby.wasm");

using var linker = new Linker(engine);
linker.DefineWasi();

using var store  = new Store(engine);
store.SetWasiConfiguration(
    new WasiConfiguration()
        .WithArgs("ruby", "--version")   // Rubyのバージョンを表示
        .WithInheritedStandardOutput()
        .WithInheritedStandardError()
);

var instance = linker.Instantiate(store, module);
instance.GetFunction("_start")?.Invoke();

実行する

dotnet run
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [wasm32-wasi]

良い戦略、悪い戦略

の参考図書にあったんですよ。せっかくだし買って読むかーと思ってアマゾン開いたら

さすが俺。 すでに購入済みです。 さっぱり記憶にありません。 新たな気持ちで読みます。

今のところの感想またはメモ

戦略とは有限のリソースを集中するためにある。戦略がないと組織のメンバーは思い思いの行動をする。それぞれの行動が逆の効果を生み打ち消し合ってしまう。

良い戦略には「診断」が必要。「診断」とは課題の構造を説明すること。構造の説明がないと、課題の解決策は多様になり過ぎる。例えば課題「利益が低い」は「売上を増やす」「経費を減らす」という矛盾した解決策を導く。

「診断」は的外れではいけない。しかし、完璧でなくてもよい。構造の中に推測が含まれていても問題ない。多少ずれていても、そこから導かれる行動が一貫していればよい。100の推進力がなくても80の推進力があれば競争力として十分機能する。

エンジニアリングマネージャーが設計するチームを越えたコミュニケーション

「チーム内の心理的安全性」は難しい?

ソフトウェア開発において、心理的安全性が重要であるという認識は広がりつつあります。 少人数、たとえば4~5人、の開発チーム内で心理的安全性を高めるには難しい面もあります。

チーム内には多かれ少なかれ権力勾配が存在します。チームリーダーとメンバーという構造がある限り、対等な関係性で本音を共有することは簡単ではありません。

定期的に雑談をする場を用意しても関係性が硬直することがあります。 同じメンバーで何度も雑談をすると、話すネタが尽き気を使うだけになり、心理的安全性どころかストレスを感じることもあるでしょう。 こういうとき多くの人は定番の雑談(例:趣味のゲームや今期のアニメ)をします。 メンバーが固定の時はいいです。グルーミングコミュニケーションの一種です。 チームメンバーだけに通じる定番のコミュニケーションは、メンバーが増えたときに不要な疎外感を与えることがあります。

チーム内での心理的安全性の構築は、構造的に難しい上に、新しいメンバー受け入れの阻害になることもあります。

エンジニアリングマネージャーの役割

チームリーダーは日々の進捗管理や技術的判断、対外的な調整などを担っています。 心理的安全性まで一人で抱えるのは現実的ではありません。 必要だと理解していてもチームリーダーには時間がありません。 エンジニアリングマネージャーがサポートする必要があります。

エンジニアリングマネージャーは、各チームのプロジェクト成果に直接コミットする立場ではありません。 プロジェクトの状況や人間関係に左右されない公平な立場として、チーム間の連携を自らの責任として介入していけます。

心理的安全性を、チームに閉じずに組織全体の課題とすると、チーム内の権力勾配の影響を薄められます。 他チームのリーダーの機嫌は、自チームのリーダーの機嫌ほどは気になりませんよね?

「組織全体の心理的安全性を設計し支える」をエンジニアリングマネージャーの役割にしましょう。

「面で設計する」心理的安全性のつくりかた

チームリーダーが1つのチームを深く見るのに対し、エンジニアリングマネージャーは組織全体を俯瞰し「面」として心理的安全性を設計できます。

たとえば、朝礼とTimes(例:slackでの分報)と振り返りセッションを組み合わせて設計します。

  • 朝礼は、1日5~15分のオンラインミーティングです。単純接触効果を高める場です。毎日お互いの顔を見る、声を聞く、自分の声を出すという繰り返しで信頼の土台を作ります。
  • Timesでは、Slackの分報などで業務の進捗や悩みを共有します。関係性のある者同士でアドバイスやジョークを交わせる場になります。
  • 振り返りセッションでは、ランダムな組み合わせの4~5人で1時間ほど、KPT(Keep, Problem, Try)などのフォーマットを用いて仕事とプライベートの中間の個人の課題を話します。。1人あたり10〜15分の時間をつかってお互いの考えや性格が自然と表れる会話で親近感を生み出します。

コミュニケーションの方法は多様です。 一つのコミュニケーション支援の手法で完結することはありません。 複数の手法を組み合わせて、色々なパターンのコミュニケーションチャンネルを育てて行く必要があります。 メンバー全員がメンバー全員とそこそこのコミュニケーションをとれることを目指します。

特定のメンバーが高いコミュニケーション能力を持っていたとしても頼らない方がよいです。 短期的に上手く行くことが多いですが、特定のメンバーの負担が大きすぎます。 そのメンバーのキャパシティを越えたときに対応できなくなります。 よほど高いコミュニケーション能力を持つ人間でも、そのキャパシティは10人も無いはずです。

仕組みの継続的な運用を通じて、組織内のメンバー全員の偏りのすくない信頼関係を育てていきます。

コミュニケーションへの温度感

プロジェクトの推進を主な責務とするチームリーダーの立場から見ると、コミュニケーション活動につかう時間が業務効率を下げるものと受け取られてしまう可能性があります。

背景に、エンジニアリングマネージャーとチームリーダーの視点の違いがあります。 エンジニアリングマネージャーはチーム間の信頼関係や組織全体の健全性に重きを置きます。 チームリーダーは自チームのプロジェクトの成果を最優先に考えます。 コミュニケーション活動をプロジェクトの時間を奪う「邪魔なもの」と捉えることがあります。

この認識の違いを乗り越えるには、エンジニアリングマネージャーがチームリーダーの敵ではなく、味方であることを丁寧に伝えていく姿勢が重要です。 組織の心理的安全性はチームリーダーの仕事をやりやすくするために役立ちます。 手を替え品を替え根気強く伝えていく必要があります。

EM(エンジニアリングマネージャー)として気をつけていること

EM(エンジニアリングマネージャー)という役割にはさまざまな形態があります。ここでは、私が経験している「複数(おおよそ3つ)のソフトウェア開発チームを束ねる役割」を前提に、日々気をつけていることです。

1. 情報は早く、未確定でも共有する

EMの役割でまず大切にしているのは「情報の展開の速さ」です。 確定情報だけを共有していると、「決まるまで思考停止」となりメンバーが受け身になります。 その結果、自分で考えないようになりまいます。

未確定の情報も「これはまだ決まっていませんが、こういう議論があります」と伝えることで、チームの思考を促し、状況への参加意識を高められます。 情報を早く、正確に、そして“未確定は未確定として”伝える。 これだけで価値観や姿勢が伝わることもあります。

情報共有はできるだけ対面(またはそれに近い場)で行い、その場で質問を受け付けます。 丁寧に説明しているつもりでも予想外に誤解される事があります。 即座に訂正・補足できます。

2. 不満や誤解には1on1で向き合う

チーム内で不平や不満が見えるときは、放置せず1on1の時間をとって丁寧に聞きます。 背景を聞いて、誤解があれば説明し直します。

不満の「ぶつけ方」も一緒に整理します。 感情を人にぶつけるのではなく、問題に向けるように促します。 リフレーミング(見方の切り替え)によって、問題を前向きに捉えられるよう手助けします。

3. コミュニケーションは自然に発生しない、だから設計する

人と人の相談ごとは、自然発生的には起きません。 EMはコミュニケーションの“マネージャー”でもあります。 意図的に相談できる雰囲気や場をつくる必要があります。

チーム内で相談が生まれないと思ったら、相談する時間をとってファシリテーターをやります。 メンバー同士だけでも相談すれば問題が解決できると知れば、自発的に相談するようになります。

4. 「知っている人」に引き上げる

「グルーミングコミュニケーション(軽い雑談やちょっとした気遣いの会話)」をメンバー任せにすると、仲のいい人だけが話す“仲良しグループ”が生まれます。 仲良しグループに入らない人が生まれます。 チームメンバーに「同じ会社に所属しているけど、よく知らない人」がいる状態は危険です。 「あの人、なんかよくわかんないんだよね」という距離感は、チームの一体感を損ないます。 職場として望ましくありません。

仕事である以上、全メンバーがグルーミングコミュニケーションをとれる場を設計し、強制的にでも接点を持たせます。 「あの人の名前は知ってる」「話したことがある」くらいの関係をメンバー全員がお互いに築けるよう引き上げます。

5. 無言の分断を予防する

チームのどこか一箇所でも壊れると、チーム全体がギスギスしてきます。 対立があればEMが仲裁すれば済むことです。 “無言の分断”や“自然発生的な孤立”が起きると、「もうちょっと仲良くしてよ」と言うくらいしかできません。 あとから解消するのは困難です。 予防を心がけます。


技術や進捗のようなハードスキルのサポートはチームリーダーに任せます。 「人と人との関係」のようなチームリーダーの手からこぼれる問題を積極的に拾います。

首を痛めた

RubyKaigi 2025の三日目あたりから、首の調子が悪くなり始めました。四日目にはかなり辛くなっていて、朝からマッサージに行って乗り切りました。良くなってきていますが完治には至っていません。

RubyKaigiで特別なことがあったわけではありません。日頃の悪い姿勢が原因で首にダメージが蓄積していたようです。猫背で首を前に出す癖があり、前に出した首を上に向けるために首の後ろ側が過剰に緊張しているようです。首へのダメージがたまると頭痛もおきます。

背筋を伸ばそうと意識してみるのですが、長年の癖で首だけを前に出してしまい、かえって首への負担が増してしまいます。背筋を伸ばすよりもバレリーナになったつもりで行動すると良いようです。しかし、サボり続けて弱った背筋では支えきれず、背中がバキバキに張ります。

仕事中はずっとパソコンに向かっているため、どうしても猫背になってしまいます。一人仕事であれば適宜休憩を挟めます。休憩の挟めない会議が2時間も続くと頭痛は避けられません。

頭痛のほかに目の奥が重い感じもあります。 視力の低下も関係していそうです。 3月の健康診断で右目の視力が低下していたので、メガネのレンズも新しくしてみました。

現在は接骨院に通ってマッサージを受けながら、原因を探っています。どうやら私は右肩をすくめる癖があるようです。猫背だと肩甲骨がうまく回らず、肩をすくめた方が腕を上げやすくなるそうです。つまり、キーボードを打ったり、スマートフォンを右手で持ったり、歯磨きをしたりするだけで、無意識に肩をすくめてしまっているのです。

肩をすくめるには、胸の前側、鎖骨の下にある筋肉や僧帽筋を縮め、固めた状態にする必要があります。バランスを取るためか首の横にある筋肉も縮めます。そのままいろいろな動作をすることで、首に負荷がたまっていくようです。

今のところやってみて効果を感じた体操の動画です。

朝やると予防になっている気がします。

youtu.be

首が固まったり頭痛がでからやると効果的なストレッチです。

youtu.be

癖が原因なので直すには時間がかかりそうですが、早く治したいものです。