作ったもの
チームミーティングで「賛成/反対/保留」をリアルタイム集計できる匿名投票ツール。
Claude Codeと一緒に、設計から実装まで約40分で作った。
vote-live というプロジェクト名で作った。
- URLを共有するだけで参加できる
- 投票結果がリアルタイムで全員の画面に反映される
-
?host=trueをつけるとリセットボタンが出るホストモードになる - 投票後に取り消しもできる
ハーネスエンジニアリングとは
今回、コードを書く前にハーネスエンジニアリングという考え方を取り入れた。
AIエージェントが動作する「環境そのもの」を設計する行為。モデルを改善するのではなく、モデルの外側(ツール・ルール・権限・フィードバック)を整備して、AIが安定して成果を出せる実行環境を作ること。
4つの設計領域がある:
| 設計 | 内容 |
|---|---|
| コンテキスト設計 | AIに渡す情報を必要なものだけに絞る |
| 行動設計 | 何をしていいか・ダメかを仕組みとして定義する |
| フィードバック設計 | 完了条件・テスト・検証を自動化する |
| 運用設計 | 進捗を外部ファイルに記録し、小さく分割して進める |
要するに「Claude Codeに頼む前に環境を整える」という話だ。
※ 本記事では以下の記事を参考にこの言葉を使っています。
先に設計書とAGENTS.mdを書いた
コードより先にこの2ファイルを書いた。
DESIGN.md(設計書)
機能要件・技術スタック・メッセージ仕様・ディレクトリ構成を書いた。
技術選定の決定事項も記録:
| 項目 | 決定 | 理由 |
|---|---|---|
| ホスティング | Railway | WebSocket対応・無料枠あり |
| 永続化 | メモリのみ | シンプルに始める |
| ホスト判定 | URLパラメータ |
?host=true でシンプルに |
| WebSocketライブラリ |
ws(素のライブラリ) |
Socket.ioは使わない |
AGENTS.md(ハーネス設計)
Claude Codeへの指示書として書いた。
## 行動設計(何をしていいか・ダメか)
### ✅ 許可
- server/ 以下のファイルの作成・編集
- client/ 以下のファイルの作成・編集
- npm install の実行
### ❌ 禁止
- `DESIGN.md` の無断変更(設計変更は人間が判断する)
- DBの追加(メモリのみと決定済み)
- Socket.io の使用(素の ws ライブラリを使う)
- フレームワーク(React/Vue等)の導入
完了チェックリストも定義した:
## フィードバック設計(完了の定義)
### サーバー
- [ ] node server/index.js でエラーなく起動する
- [ ] 投票メッセージを受け取り、全クライアントにbroadcastできる
### 結合テスト
- [ ] ブラウザを2タブ開いて投票すると両方の画面に反映される
WebSocketのコード
技術スタックは Node.js + ws ライブラリ のみ。フレームワークなし。
サーバー(server/index.js)
const http = require('http');
const WebSocket = require('ws');
// HTTPサーバーとWebSocketサーバーを同じポートで動かす
const server = http.createServer(/* 静的ファイル配信 */);
const wss = new WebSocket.Server({ server });
let votes = { agree: 0, disagree: 0, neutral: 0 };
function broadcast(data) {
const message = JSON.stringify(data);
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
wss.on('connection', (ws) => {
// 接続直後に現在の投票状況を返す
ws.send(JSON.stringify({ type: 'update', result: votes }));
ws.on('message', (data) => {
const message = JSON.parse(data);
if (message.type === 'vote') {
votes[message.choice]++;
broadcast({ type: 'update', result: votes });
} else if (message.type === 'cancel') {
if (votes[message.choice] > 0) votes[message.choice]--;
broadcast({ type: 'update', result: votes });
} else if (message.type === 'reset') {
votes = { agree: 0, disagree: 0, neutral: 0 };
broadcast({ type: 'update', result: votes });
}
});
});
ポイントは2つ。
① HTTPサーバーとWebSocketを同じポートに相乗りさせる
Railwayは1プロセス1ポートなので、HTTPとWebSocketを分けられない。new WebSocket.Server({ server }) で同じポートに相乗りさせることで解決した。
② broadcastで全クライアントに配信
誰かが投票したら wss.clients をループして全員に送る。これがWebSocketのリアルタイム性の核心。
クライアント(app.js)
// URLからWebSocketのURLを自動生成
// ローカル → ws://localhost:8080
// 本番 → wss://ドメイン名
const wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${wsProtocol}://${location.host}`);
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'update') {
updateResult(message.result); // バーグラフを更新
}
};
// 投票ボタン
btn.addEventListener('click', () => {
ws.send(JSON.stringify({ type: 'vote', choice: 'agree' }));
});
ローカルと本番でURLが変わる問題は location.host を使うことで吸収した。
メッセージ設計
クライアントとサーバー間のやりとりはシンプルなJSONにした。
// 投票
{ "type": "vote", "choice": "agree" | "disagree" | "neutral" }
// 取り消し
{ "type": "cancel", "choice": "agree" }
// リセット(ホストのみ)
{ "type": "reset" }
// サーバー → 全員:結果更新
{ "type": "update", "result": { "agree": 3, "disagree": 1, "neutral": 2 } }
やってみた感想
設計書とAGENTS.mdを先に書いて正解だった
AGENTS.mdで「Socket.ioは使わない」「DBは使わない」と書いておいたことで、Claude Codeが勝手に余計なものを入れることがなかった。制約を事前に言語化しておくことの効果をはっきり感じた。
WebSocketはシンプルだった
ws.send / ws.onmessage / broadcast の3つを理解すれば、リアルタイム通信の本質はほぼ掴める。フレームワークを使わず素の ws ライブラリで作ったことで、中身が見えてよかった。
完了チェックリストが効いた
AGENTS.mdに「2タブで同期確認」という結合テストを書いておいたおかげで、単体テストだけで終わらなかった。完了条件を事前に定義することの重要さを改めて感じた。
まとめ
- コードより先に設計書・
AGENTS.mdを書いた - 制約を言語化しておくことでClaude Codeが迷わない
- WebSocketはHTTPサーバーと同じポートに相乗りさせると便利
-
wsライブラリ1本でリアルタイム通信は十分作れる
