はじめに
AI コーディングエージェントを勇者にして、ドット絵の町で眺めるアプリ Yushario を作りました。使い方は紹介記事に書いたので、こちらは作った側の記録です。
リポジトリの最初のコミットは 2026-10-09 で、この記事を書いている時点で約140コミット、約2週間です。
この記事で書くことは次の通りです。
- 方針が変わった経緯(Live2D → ドットの部屋 → RPG の町)
- 「エージェントを作らない」という設計の線引き
- フックのイベントを町の動きに変える core の実装
- ドット絵をどう作って揃えたか
- 公開前にやったこと
構成の全体像
Claude Code・Codex・pi は、いつものターミナルのまま動かします。町は、そこから届く知らせだけで描いています。
図の流れを、順に説明します。
-
CLI エージェント: Claude Code・Codex・pi を、そのまま
node-ptyの PTY で動かします。入出力は server が中継します -
フック / 拡張: 各 CLI のフックや拡張が
POST /eventで状態を知らせます。server が検証して、履歴に保存します -
server: Node 標準の
httpとwsだけで書いた1プロセスです。127.0.0.1で待ち受けます -
ブラウザ: server からの WebSocket(
/wsがイベント、/ptyが端末)で、町と埋め込みターミナルを更新します。web は Vite でフレームワーク無し、町は Canvas、端末は xterm.js です - core: 活動の分類や、戦う相手の決定は、DOM にも Node にも依存しない純粋関数です。第3章で中身を見ます
1. 方針はどう変わったか
最初から RPG の町を作ったわけではありません。docs/OBSERVATION.md に当時の選択肢が残っています。
| 案 | 内容 | 結果 |
|---|---|---|
| A. Live2D を続ける | 1体に集中して深める | 途中で削除 |
| B. ドットの部屋 | 複数エージェントをドットキャラで1部屋に表示 | 採用 |
| C. 両方 | 全体はドット、1体だけ Live2D | 廃止 |
最初は Live2D のキャラ1体を動かす構成で、表情を決める Decider という部品もありました。それをやめて、複数のエージェントを1つの部屋に並べるドットの表示に切り替えています。その部屋が、さらに「作業は戦闘、休憩は宿屋」という RPG の町になりました。リポジトリの docs/PLATFORM.md には、Live2D を削除した段階の記録が残っています。
いま私が開発をするとき、複数のエージェントに並行して作業させるのは欠かせません。その様子を Live2D の1体で表現するのが難しく、複数のエージェントが同時にいる場面を見せられる形を探しました。
2. 設計の線引き:エージェントを作らない
Yushario は、エージェント本体(ハーネス)ではありません。docs/ARCHITECTURE.md に、機能を足す時のルールを6つ書いています。
- 自分で LLM を呼ばない
- ツールの実行や許可を、エージェントの代わりに行わない
- エージェントに送る文は、人が書いたか、人が選んだ定型文だけ
- 仕事の分け方は、人か、人が指揮役に任せた勇者が決める
- CLI とは公式の差し込み口(フック・拡張・起動引数)だけでつなぐ
- 機能を特定の CLI に縛らない
これを先に決めたのは、町の見た目が賑やかになるほど「もっと便利にしたい」という誘惑が出るからです。許可要求を自動で通せば確かに楽ですが、それはもう別のアプリだと割り切りました。
許可要求は、人の判断をフックへ返すだけにしています。画面での回答の待ち時間は120秒で、過ぎたらサーバーが「ターミナルで答える」を返します。自動で許可はしません。
3. フックを町の動きに変える
Claude Code のフックは、PreToolUse などのイベント名と、ツール名・入力を JSON で渡してくれます。これを共通の AgentEvent に直し、そこから「いま何をしているか」を求めます。
// src/core/activity.ts(抜粋)
export type Activity =
| "think" | "read" | "search" | "edit" | "test" | "git" | "run" | "web"
| "plan" | "delegate" | "ask" | "tool" | "rest" | "idle";
const TOOLS: Record<string, Activity> = {
Read: "read", Grep: "search", Glob: "search",
Edit: "edit", MultiEdit: "edit", Write: "edit",
WebSearch: "web", WebFetch: "web",
Agent: "delegate", Task: "delegate",
AskUserQuestion: "ask",
};
export function toolActivity(tool: string, target?: string): Activity {
if (tool === "Bash") return commandActivity(target);
return TOOLS[tool] ?? "tool";
}
Bash だけは、コマンドの先頭を見て分類します。git や gh は「git」、vitest や tsc などは「テスト」、それ以外は「run」です。
const TEST_COMMAND = /^(?:(?:npm|pnpm|yarn|bun) (?:test|t|run (?:test|typecheck|lint|check)\S*)|npx (?:vitest|jest|tsc|eslint)|vitest|jest|pytest|tsc|eslint|go test|cargo test)(?: |$)/;
export function commandActivity(head: string | undefined): Activity {
if (!head) return "run";
if (/^(?:git|gh)(?: |$)/.test(head)) return "git";
return TEST_COMMAND.test(head) ? "test" : "run";
}
活動が決まると、あとは対応表です。編集はスライム、コマンド実行はゴースト、テストはゴーレム、委任はドラゴンと戦います。
// src/core/story.ts(抜粋)
const BATTLES: Partial<Record<Activity, MonsterKind>> = {
edit: "slime", run: "ghost", test: "golem", delegate: "dragon",
};
この辺りは全部、DOM にも Node API にも依存しない純粋関数にして src/core に置き、vitest でテストしています。描画や WebSocket をモックだらけにして試すのではなく、「このイベント列ならこの活動になる」を関数で確かめる形です。
「分からないことは推測しない」
Codex のフックには、道具の終了コードに当たる公式の定義がありません。PostToolUse の tool_response は、生の標準出力がそのまま入ることもある任意の JSON です。
そこで Yushario は、これをパースして成功・失敗を推測するのをやめました。
// src/core/codex.ts(抜粋)
// tool_response is arbitrary JSON (often raw stdout), NOT a documented exit status.
// Never parse its text, forward output, or send a completed command as check evidence.
if (name === "PostToolUse") event.detail = "Tool finished; result unknown";
テストが通ったかを推測して「合格」と表示すると、それは嘘になります。Codex の勇者では合格条件の自動証拠と MP が取れず、画面にも「未実行のまま」と出します。分からない部分を分からないと言える方が、後から困りません。
4. ドット絵の作り方
絵とロゴは OpenAI の画像生成(ChatGPT・Codex)で作りました。生成した絵は、次の流れで整えています。
-
tools/art/components.jsonにコンポーネント(大きさ・コマ・指示文)を一覧で書く - Codex に1コンポーネント1枚ずつ生成させる
- unfake でドットの大きさとグリッドを検出し、縮小とジャギー掃除をする
- 色を全部、共通のパレット Resurrect 64 に合わせる
- 緑の背景を透明にして、コマを同じ大きさのセルに並べ直す(足元をそろえる)
効いたのは次の2点です。
- パレットを1つに固定する: 生成された絵は色がバラバラですが、全コンポーネントを同じ 64 色に寄せると、部品を並べても色がなじみます
-
勇者の設定画を先に作る:
hero-modelを先に作り、各ポーズや仲間はそれを参照画像にして生成します。見た目のぶれが減りました
戦闘背景の例(森)。
絵は組み合わせ用の単品部品なので、建物や小物の位置は JSON のデータで持っています。絵を差し替えてもコードは変わりません。ただしプラグインのようなコードを読み込む仕組みは、わざと作っていません。
勇者のデザイン遍歴
勇者のデザインは、一度作り直しています。最初の勇者が公開前の権利の監査で引っかかり、そこから描き直しが始まりました。
描き直しの途中で並べた10案(正面・背中・横)。赤いマントに青い服、黒髪に赤い帯、銀髪、金髪のツンツン頭など、髪型と配色を変えて比べました。
最後の No.10(金髪のツンツン頭、青緑のマント)に決めました。決め手は、ほかのキャラクターと並べたときに違和感がないことと、ぱっと見たときのかっこよさです。
5. 作りすぎない
このリポジトリには AGENTS.md があり、実装の範囲を絞るルールを書いています。
- 指示された範囲だけを実装する。「将来のため」の拡張点を足さない
- ファクトリ、DI コンテナ、プラグイン機構、イベントバスを作らない
- 状態管理ライブラリや UI フレームワーク(React など)を入れない
- テストは
src/coreの振る舞いに絞る
依存も最小限で、実行時の依存は ws・node-pty・xterm.js(と fit アドオン)・フォント1つだけです。クエストの保存は Node 22.13 から組み込みの node:sqlite を使いました。
実装の範囲はこのルールで絞りましたが、勇者の動きの動線、細かい UI、足りない機能、大まかな動作フローは、実際に動かしながら自分でかなり見直しました。
機能を広げる時に参考になったのが、Orca でした。Yushario は、エージェントに仕事を任せ、確かめ、成果を採るための作業の場(ADE)としての機能を広げたい一方で、ドット絵 RPG の世界観も崩したくありません。この2つが両立するように、機能を RPG の言葉に置き換えました。
- エージェントチームを作る機能 → パーティー編成。町にいる勇者を仲間に足し、書きかえる担当と作戦を決める、ドラクエ風の編成窓にしました
- タスク管理 → クエスト。書く、勇者に任せる、指示を送る、報告と変更を見る、採用する、という流れの札にしました
6. 公開前にやったこと
- 作業報告・レビュー・没案は、非公開の別リポジトリに分けて残しました(本体の履歴は squash していません)
- 公開前に、文言や権利まわりを監査して直しました。たとえば README が動機を「効率化」寄りに書いていたので、「眺めて楽しい」に直しています
- 絵とロゴは ChatGPT で作ったものとして、権利を主張しない旨を書きました。パレットとフォントのクレジットは NOTICE にあります
- npm に公開し、
npx yushario@latestで動くようにしました。CI でタグと GitHub Release も作ります - 紹介用の PV を作り、BGM はベートーヴェンの旋律(パブリックドメイン)を使った自作の 8bit 曲にしました
7. 紹介サイトを作る
アプリとは別に、yushario-website という紹介サイトのリポジトリを作りました(https://yushario.morimizu.dev/)。
- 構成: Vite で静的ビルドし、Cloudflare Workers Static Assets で配信しています。アプリ本体のサーバーや CLI は要りません
- 体験版: アプリの UI と描画処理をコピーして、API と WebSocket の応答をブラウザ内の架空データに差し替えました。CLI 起動、実コマンド、Git 操作は一切しません
-
検査:
npm run checkが、ビルドのあとにリンク先、画像、ライセンスの同梱を検査します。デプロイもこのコマンドを通ります - メニュー: ホーム・冒険の様子・体験版・はじめ方・ヘルプなどの切り替えは、ARIA の tablist に対応し、URL のハッシュで直接リンクできます
世界観には時間をかけました。
- ずかん: モンスター4種、仲間4職業、町の人3人のカードを作り、横・後ろ・攻撃・被弾のモーションを見られるようにしました。文章は読み物ですが、「何をするとそのモンスターが出るか」は、アプリの実際の動作(第3章の分類)に基づいて書いています
- ギャラリー: 町5種、戦場3種、戦闘3種、屋内4種、部品を、アプリで実際に使っている絵のまま並べました。解像度も載せています
- ガイド: 出陣から採用までを、実際の画面を撮った画像に番号を重ねて説明しています。撮影は、空の撮影専用環境で行い、名前やコードは架空です
サイトで一番こだわったのは世界観です。アプリの機能とは全く関係のない設定を、キャラクターやモンスターに足していきました。たとえばゴーレムは「バグに親を殺された魔法使いが作った、しれんの番人」で、図書館の司書は「しずかに」が口ぐせです。愛着が湧いて、作業がすごく楽しくなりました。
おわりに
「エージェントを作らず、公式の差し込み口だけで外から眺める」という線引きは、結果的に実装を小さく保ってくれました。LLM を呼ばないので、動かすのに追加の課金も要りません。
次にやりたいこと:
- スマホアプリから、クエストの発行や鑑定ができる仕組みを作る予定です
紹介サイトとリポジトリはこちらです。



