# AI管制塔（運航状況 ／ FLIGHT STATUS）の作り方

この一枚は二人の読者に向けて書いてあります。最初の節は人間向けで、何ができて、安全で、どう頼めばよいかだけを書きました。そのあとは組み立てる AI 向けの仕様です。受け取った人は、このファイルを自分の Claude Code か Codex に読ませて「これを作って」と言うだけで、自分の Mac に同じものが組み上がります。

---

## 1. 人間向けの1画面

### AI管制塔とは
Claude Code と Codex の会話記録を読んで、「いまどの会話が走っていて、どれがあなたの返事を待っていて、どれが終わったか」を一枚の画面に並べる道具です。空港の運航状況板の見立てで、画面の呼び名は「運航状況 ／ FLIGHT STATUS」です。

### できること
画面は「🟢 実行中」「🟡 あなたの番」「✅ 終わった」の3列が基本で、ボタン一つで表示の件数を 4・8・16・全部 に切り替えられます（件数表示のときは画面にぴったり収まる大きさに文字が伸び縮みします）。向きは 自動・横・縦、対象は Claude・Codex・両方 を選べます。検索窓に言葉を入れると、昨日より前の分（7日分）も含めて当たる会話だけが並びます。

カード一枚には、会話の題名、頼んだこと、段取りの進み具合（点の列と 3/5 のような数）、そして列ごとに「❓ 聞かれていること」か「結果の一文」が載ります。Mac が2台以上あるなら、Box や iCloud のような共有フォルダを通して両方の分が一枚に合流します。自分の Mac の Claude の「あなたの番」カードを押すと、Claude アプリの返事待ち一覧が開きます。

### 安全について
会話記録は読むだけで、一文字も書きません。AI は使わないので、動かしてもトークンは 0 です。インターネットへは何も送りません。画面を出す係は 127.0.0.1（その Mac の中だけ）で待つので、外から覗かれることはありません。複数 Mac の合流に使う共有フォルダは、自分だけが見られる場所を使います（会話の題名や依頼の一部が入るので、人と共有するフォルダには置かないでください）。

### 頼み方（この一文を AI に言う）
> このファイルの仕様どおりに「AI管制塔」を作って。置き場は `<自分で決めたフォルダ>`、Mac は1台（または2台で共有フォルダは `<共有フォルダ>`）。できたら curl と画面で確かめて。

---

## 2. AI 向けの組み立て仕様

### 2.1 部品の構成
置き場は一つのフォルダ（以下 `<置き場>`）。中に `AI用/` を切り、スクリプトはそこに入れる。

| 部品 | 役目 | 置き場 |
|---|---|---|
| 本体 `生成.py` | 記録を読み、状態を判定し、画面の HTML を作る。単独で実行すると静的な `00_AI管制塔.html` を `<置き場>` 直下に書く | `AI用/` |
| 係 `係.py` | 127.0.0.1:8770 で待ち、本体を読み込んで `/` に画面、`/api` に最新の一覧を返す | `AI用/` |
| 常駐 `このMacに置く.sh` | launchd に係を登録する（Mac ごとに一回。ログイン時に自動起動、落ちても立ち直る） | `AI用/` |
| `AI用/共有/` | 複数 Mac の合流用。各 Mac が `<Mac名>.json` を書く | 1台なら不要 |

言語は Python 3 の標準ライブラリだけ（`json`・`glob`・`http.server`・`subprocess`・`datetime`・`re`・`html`）。追加のインストールは要らない。

### 2.2 記録の読み方（事実。推測で変えないこと）

#### Claude Code
- 場所: `~/.claude/projects/*/*.jsonl`。ファイルの更新日時が対象期間より古いものは開かない。
- 1行1 JSON。`type` を見る。
  - `custom-title` → `customTitle` を題名にする。
  - `isSidechain` が真の行（部下の会話）と、`type` が `user`／`assistant` 以外の行は飛ばす。
  - `timestamp`（ISO 8601、末尾 `Z`）→ 最初を開始、最後を終了にする。`cwd` はその会話のフォルダ。
- `user` 行（`isMeta` でないもの）: `message.content` は文字列か、`{"type":"text","text":…}` の配列。本文から雑音を除いて「頼んだこと」に積む。雑音とは、空、`<` で始まる、`# AGENTS.md` で始まる、`Caveat:` で始まる、`[Request interrupted` で始まる、冒頭200字に `<command-name>` を含むもの。本文を読む前に `<pasted_content …>` のタグと `<system-reminder>…</system-reminder>` を取り除く。
  - 新しい依頼が来たら、前の回の「質問（ask）」と「まとめ（recap）」は空に戻す。
  - 本文が無く `tool_result` だけの `user` 行は「ツールの途中」とみる。
- `assistant` 行: `message.content` の配列を見る。
  - `text` → 最後のものを「AI の最後の返事」に。
  - `tool_use` で `name` が `update_plan` で終わる → 段取りを再現する。`input.items` があれば全部置き換え（各 `content`・`status`）。`advance` なら `in_progress` を `completed` にし、次の `pending` を `in_progress` に。`done`（1始まりの番号の配列）はそれぞれ `completed`、`start` はその番号を `in_progress`。
  - `tool_use` で `name` が `wrap_up` で終わる → `input.recap` を「まとめ」、`input.ask` を「質問」に。段取りは全部 `completed` にする。
  - その行の `message.stop_reason` が `end_turn` なら「返事を終えた」、それ以外なら「ツールの途中」。
- 「頼んだこと」が一つも無い会話と、終了が対象期間より前の会話は出さない。

#### Codex
- 場所: `~/.codex/sessions/<年>/<月>/<日>/*.jsonl`。題名は `~/.codex/session_index.jsonl`（1行1 JSON、`id` と `thread_name`）から引く。
- 各行は `timestamp`・`type`・`payload`。
  - `type == "session_meta"` → `payload.id`（題名を引く鍵）、`payload.cwd`。`payload.thread_source` が `null` でも `"user"` でもなければ自動審査（guardian_review）の会話なので、丸ごと除く。
  - `payload.type == "task_started"` → 走り出した。`task_complete` → 終わった（終了時刻を更新）。
  - `payload.type == "message"` で `payload.role` が `user` → `payload.content` の `input_text` を「頼んだこと」に（直前と同じ文は積まない。雑音の判定は Claude Code と同じ）。`assistant` → `output_text` を「AI の最後の返事」に。
  - それ以外の行でも、開始済みなら `timestamp` で終了時刻を伸ばす。

#### 読み直しの節約
係は5秒ごとに本体を呼ぶので、ファイルごとに `(mtime, size)` を鍵にして前回の読み取り結果を使い回す。変わったファイルだけ読み直す。

### 2.3 状態の判定（3列への振り分け）
| 列 | 条件 |
|---|---|
| 🟢 実行中（run） | 最後が「人の依頼」か「ツールの途中」（Codex は `task_started` のあと `task_complete` が来ていない）で、かつ最後の動きから 20 分以内 |
| 🟡 あなたの番（you） | 実行中でなく、`wrap_up` の `ask` が残っている |
| ✅ 終わった（done） | それ以外 |

並び順は 実行中 → あなたの番 → 終わった、同じ列の中は新しい順。今日の 0 時より前に終わって走っていない会話は「昨日まで」として隠し、検索のときだけ出す（件数は隅に「昨日まで N 件」と出す）。期間の既定は、静的に作るときは 2 日（昨日の 0 時から）、係は 7 日。

### 2.4 画面の約束
- 上の帯: 題名は `AI管制塔` を太く、横に小さく「運航状況」と `FLIGHT STATUS`（字間を広く）。右下の隅に小さく `by TAKE`。帯には Mac の名前（「A・B（この画面は A）」）、列ごとの件数、Claude／Codex／両方、3列／4／8／16／全部、自動／横／縦、検索窓、更新状態、時計。
- 選んだ表示は `localStorage`（鍵 `kb_n`・`kb_o`・`kb_t`）と URL の `?n=&o=&t=` で覚える。
- 件数表示の格子: 横向き 4=[2列×2行]・8=[4×2]・16=[4×4]、縦向き 4=[1×4]・8=[2×4]・16=[2×8]。「自動」は画面の幅と高さで向きを決める。
- カードに載せる3行: ①題名（`custom-title`／`thread_name`。無ければ最初の依頼の先頭 26 字）、②「頼んだこと：」（中身のある最後の依頼。`@"…"` のファイル指定を除いたうえで 20 字以上のもの。`Background` で始まる引き継ぎは「別の会話から引き継いだ作業」）を 46 字、③列ごとの足: あなたの番なら「❓ 質問」50 字、終わったなら `recap`（無ければ最後の返事）の最初の一文 52 字。段取りがある会話は点の列（完了＝緑、進行中＝点滅）と `k/n`、走っていて残り 1 以下なら「まもなく終わり」、進行中の段を「いま：…」28 字。
- 上の細い行: 状態バッジ、Mac 名、Claude（橙）／Codex（青）の札、フォルダ（パスの末尾 2 階層だけ。`$HOME` と共有フォルダの根元は省く）、「N 分前」。
- 文字を減らす工夫: 題名と各行は 2 行で切る（`-webkit-line-clamp`）。文字の大きさはカードの幅・高さに連動する容器単位（`cqw`・`cqh`）で `clamp` し、カードの高さが 170px を切ったら「頼んだこと」を隠し、110px を切ったら段取りと結果も隠す。
- 更新状態: 係につながっているときは 15 秒以内なら「● リアルタイム」、それを超えたら「● つながっていません（N 分前の状態）」。静的な HTML は 1 分以内「最新」、30 分で警告色。
- 「あなたの番」で Claude で、この画面の Mac のカードだけ、押すと `claude://code/needs-input?source=desktop_action` を開く。他の Mac のカードは押せない（別の機械の一覧を開いても意味が無い）。
- 配色は `:root` の変数で持ち、`prefers-color-scheme: dark` と `data-theme` の両方に対応する。

### 2.5 複数 Mac の合流
- 各 Mac の係は、自分の分の一覧を `<置き場>/AI用/共有/<Mac名>.json` に書く。中身は `{"mac": 名前, "written": 書いた時刻(UNIX秒), "sessions": [...]}`。各会話は `tool`・`id`・`title`・`cwd`・`start`・`end`・`reply`・`recap`・`ask`・`plan`・`running` と、`prompts`（最後の 6 件、各 300 字まで）。`reply` も 300 字。
- 書くのは、中身が変わったときと、変わらなくても前回から 30 秒たったとき（生存の合図）。一時ファイルに書いてから `os.replace` で差し替える（同期中の読みかけを防ぐ）。
- 読むのは `共有/*.json` のうち自分の名前以外。`written` が 3 分より古ければその Mac は眠っているとみなし、`running` を全部 `False` にする（走ったままの表示が残らない）。
- Mac 名は `scutil --get ComputerName`。それが「MacBook Pro」「Mac mini」「iMac」「Mac Studio」のような既定のままなら、`sysctl -n machdep.cpu.brand_string` のチップ名から短い名前を付ける（例: `M4` を含めば `M4Mac`）。
- 共有フォルダは Box・iCloud・Google ドライブのどれでもよいが、**自分だけが見られる場所**にする。

### 2.6 係と常駐
- 係: `ThreadingHTTPServer(("127.0.0.1", 8770), Handler)`。`GET /` は本体の `fill(build(), live=True)` の HTML、`GET /api` は `build()` の JSON（`cards`・`cnt`・`nold`・`gen`・`macs`・`me`）、それ以外は 404。`Cache-Control: no-store`。アクセスログは出さない。本体は同じフォルダの `*_生成_*.py` の一番新しい版を `importlib` で読み込む。
- 画面は 5 秒ごとに `/api` を取り、カードの束を差し替えて描き直す。
- 常駐: `~/Library/LaunchAgents/<ラベル>.plist`（例 `com.example.aikanseito`）を書く。`ProgramArguments` は `python3 -c 'import sys; print(sys.executable)'` で得た python の実体パスと係のフルパス、`RunAtLoad` と `KeepAlive` を真、標準出力と標準エラーは `/tmp/aikanseito.log`。登録は `launchctl bootout gui/$(id -u)/<ラベル>` で古いのを外してから `launchctl bootstrap gui/$(id -u) <plist>`。外すときは bootout して plist を消す。

### 2.7 確かめ方
```
curl -s http://127.0.0.1:8770/api | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["cnt"], d["macs"], d["me"])'
```
`{'run': 0, 'you': 1, 'done': 5} ['MacA'] MacA` のように返れば係は生きている。次にブラウザで `http://127.0.0.1:8770/` を開き、右上が「● リアルタイム」になること、Claude Code の会話を一つ始めると数秒で 🟢 に現れ、返事が終わると ✅ か 🟡 に移ることを見る。静的版は `python3 AI用/生成.py 2` で `<置き場>/00_AI管制塔.html` ができ、標準出力に `{'run':…} 昨日まで N` と出る。

---

## 3. つまずきやすい点（実際に起きたこと）

- **クラウド同期フォルダのスクリプトを launchd から動かすと止まる。** Box・iCloud・Google ドライブは中身を必要なときだけ落とす仕組みなので、裏で動く係はファイルを読めず「Resource deadlock avoided」で止まる。`AI用/` フォルダを「オフラインで使用可能」（常にこの端末に保持）にしておく。
- **同期でシェルの実行の印が消える。** `chmod +x` は同期先で消えることがある。`zsh "<パス>/このMacに置く.sh"` のように、シェルに渡して実行する。
- **初回に macOS の許可が出る。** 「python3 が <フォルダ> 内のファイルにアクセスしようとしています」が出たら本人が「許可」を押す。押すまで係は記録を読めない。
- **ユーザー名が Mac ごとに違う。** パスは `$HOME`（Python なら `os.path.expanduser("~")`、plist は実体パスを生成時に埋める）で書き、固定のパスを書き込まない。
- **127.0.0.1 は「リンクを押したその機械」の意味。** 別の端末からは開かない。2 台目で見たいなら、2 台目にも係を置き、共有フォルダで合流させる。外から見たいなら別の仕組み（例: トンネルと認証）が要るが、この道具の範囲には入れない。
- **Codex の自動審査の会話は除く。** `thread_source` が `user` でないもの（guardian_review）を数えると、頼んでいない会話が並ぶ。
- **Claude Code の部下は数えない。** `isSidechain` の行を読むと、一つの会話が何件にも見える。
- **「いまの状態」は AI の要約。** `recap` や最後の返事を映しているだけなので、確かめるときは元の会話を開く。
- **他人のツールを入れない。** 全部の会話記録を読む道具なので、見た目のアイデアだけ借り、コードは自分の手元で書く。

---

## 4. 空港の見立てで育てる案（任意）

名前は「AI管制塔」、画面は「運航状況 ／ FLIGHT STATUS」。会話は「運航」と呼ぶ。育てるなら、列を 飛行中（実行中）・着陸許可待ち（あなたの番）・到着（終わった）に言い換える、新しく始まった会話を 出発（Departures）として一瞬だけ目立たせる、英字は Flight Board・Departures・Arrivals・Flight Status から選ぶ、全部表示を「全便運航状況」と呼ぶ、といったところ。中身の判定は変えず、言葉と色だけで育てられる。

このあとの一手の候補: ほったらかしの会話の一覧（思い出し版）、AI どうしの連絡帳の未完了欄を帯に載せる、他の AI の記録の読み取り口を足す。

---

本家: by TAKE
