← 一覧に戻る

Telegram秘書アーキテクチャ設計書

2026年3月27日 06:42 更新
MD から自動変換されたページです。内容について質問があれば右下の ? ボタンからどうぞ。

作成日: 2026-03-26 ステータス: Phase 2 実装済み・運用中 Codexレビュー: 4回実施、LGTM取得。v4はnuchi新情報反映+実地検証結果

この設計書の背景

スマホ(Telegram)からざっくりした指示を飛ばすだけで、PCで動いてるClaude Codeの各セッションに適切にタスクを振ってくれる「秘書」を作りたい。個人利用のツール。

調査で検討・却下した選択肢

選択肢 却下理由
OpenClaw + LINE OpenClawは別ツール。今のClaude Code環境(MCP・memory・スキル)が活かせない
LINE連携(osisdie/claude-code-channels) ⚠️ スター3。MCP pluginではなく毎回 claude -p を起動する設計。会話コンテキストが毎回リセットされる
Claude Code Remote Control 「リモコン」であって「秘書」ではない。文脈補完やオーケストレーション機能がない
claude -p で毎回新規起動 5時間レートリミットを大量消費。Claude Codeは全文を読まない癖があり文脈注入が不確実

採用: Claude Code Channels(Telegram)+ 秘書セッション

前提条件:


アーキテクチャ全体像

[スマホ / Telegram]
       ↓ ざっくり指示(例:「コールリマインダーの課金変えたい」)
[秘書セッション] ← Claude Code + Channels(Telegram)
  │
  │ 1. セッション特定(レジストリ参照)
  │    └── ~/.claude/secretary/registry.json でセッション名を解決
  │
  │ 2. 文脈補完(必要に応じて)
  │    ├── Second-Brain/ → 最近のセッション記録(常時自動記録)
  │    ├── memory/ → 永続知識
  │    └── tmux capture-pane → 該当セッションの今の画面
  │
  │ 3. 曖昧指示の処理(clarify → propose → dispatch)
  │    ├── clarify: セッション特定不能なら候補を提示
  │    ├── propose: 「こう進めるけどOK?」と提案
  │    └── dispatch: 承認後に tmux.py 経由で実行
  │
  │ 4. 進捗報告(edit_message で随時更新)
  │    └── ステータスメッセージを1つ作成し、edit_message で更新し続ける
  │
  │ 5. 結果の取得と報告
  │    ├── tmux.py wait → 完了/質問/permission を検知
  │    ├── tmux.py read → 応答テキスト取得
  │    ├── edit_message でステータス更新(通知なし)
  │    └── 完了/ブロック時は新規 reply で通知(プッシュ通知あり)
  │
[作業セッション群 on tmux]
  ├── line-call-reminder: コールリマインダー開発
  ├── caprate-map: 不動産分析アプリ
  └── ...

セッションレジストリ

tmux session名やSecond-Brainファイル名からの推測だけでは、誤ったセッションに指示を送るリスクがある。

~/.claude/secretary/registry.json

{
  "sessions": [
    {
      "session_name": "line-call-reminder",
      "repo_path": "/Users/aiharataketo/projects/line-call-reminder",
      "aliases": ["コールリマインダー", "LINE Bot", "電話リマインド"]
    }
  ]
}

秘書セッションの設計原則

知識はステートレス、ジョブは外部永続化

秘書の会話文脈はセッション再起動で失われてもいい(外部に全知識がある)。 ただし実行中のジョブは外部ファイルに記録する(何をどこに投げたか忘れると復旧できない)。

知識源(ステートレスでOK)

情報 保存場所
プロジェクトの背景・教訓 memory/
最近のセッション内容 Second-Brain/(常時自動記録)
各セッションの今の状態 tmux capture-pane
ユーザーの好み・ルール CLAUDE.md(起動時に自動読み込み)

ジョブ台帳: ~/.claude/secretary/jobs.json

{
  "jobs": [
    {
      "job_id": "j-20260326-001",
      "message": "課金モデルをサブスク制に変更して",
      "target_session": "line-call-reminder",
      "status": "SENT",
      "chat_id": "7789180125",
      "status_message_id": "100",
      "awaiting_reply_kind": null,
      "blocked_reason": null,
      "created_at": "2026-03-26T22:35:00Z"
    }
  ]
}

status: PENDINGSENTDONE / FAILED / BLOCKED status_message_id: 進捗報告用のTelegramメッセージID(edit_message の対象。詳細はv4「進捗報告プロトコル」参照)

情報源の優先順位

速い・軽い                                遅い・重い
────────────────────────────────────────────────→
レジストリ → tmux      → Second-Brain → memory → recall
台帳検索    今の画面    最近の全記録    永続知識   最終手段
                       (常時自動記録)

Second-Brainの鮮度

Second-Brainは常時自動記録される(セッション終了時ではない)。

制約:


曖昧指示への対応

ユーザーの入力は雑である。これは前提であり、システムが対応する。

3モード

モード 発動条件 動作
clarify セッション特定不能、指示が曖昧 Telegramで候補を提示して確認
propose セッション特定OK 「こう進めるけどOK?」とTelegramに返す
dispatch ユーザーが承認 tmux.py 経由で実行

秘書は自動でdispatchしない。必ずproposeを挟む。

連投メッセージ

2通目が来た時、1通目のジョブとの関係を推測して:


tmux.py スキルの位置づけ

責務の限定

tmux.py(~/.claude/skills/tmux/scripts/tmux.py)の責務はTUI入出力の安定化のみ。セッション管理やジョブ管理は秘書セッション側の責務。

責務 担当
TUI への確実な入力 tmux.py send
処理完了/ブロック検知 tmux.py wait
応答テキスト取得 tmux.py read(末尾50行。長い応答は途中欠落の可能性あり)
セッション管理 秘書セッション(レジストリ)
ジョブ管理 秘書セッション(ジョブ台帳)

wait の結果 → 秘書のアクション

wait の結果 ジョブ状態 ステータス更新 Telegram通知
ready → DONE edit_message: ✅完了 新規reply: 結果要約(プッシュ通知)
ask_user_question → BLOCKED edit_message: ⚠️質問待ち 新規reply: 質問内容を転送(プッシュ通知)
permission_modal → BLOCKED edit_message: ⚠️権限待ち 新規reply: 権限内容を転送(プッシュ通知)
timeout (stderr) → FAILED edit_message: ❌タイムアウト 新規reply: タイムアウト通知(プッシュ通知)
session_gone (stderr) → FAILED edit_message: ❌セッション消失 新規reply: セッション消失通知(プッシュ通知)

ブロック時の応答フロー

[作業セッション] permission_modal 発生
       ↓ tmux.py wait が "permission_modal" を返す
[秘書セッション] read で内容確認 → Telegram に転送
       ↓
[スマホ] 「OK」と返信
       ↓
[秘書セッション] tmux.py send SESSION "1" → wait で再待機

ask_user_question も同様(read で質問内容を見て、ユーザーの回答を send で転送)。


Permission管理


セキュリティ


Telegramセットアップ手順

1. Telegram で @BotFather → /newbot → トークン取得
2. Claude Code で:
   /plugin marketplace add anthropics/claude-plugins-official
   /plugin install telegram@claude-plugins-official
   /telegram:configure <トークン>
3. セッションを再起動(プラグインはMCPサーバーとして自動起動する。--channels フラグは不要)
4. Telegram から Bot にDM → ペアリングコード取得
5. /telegram:access pair <コード>
6. /telegram:access policy allowlist ← 必須!

注意: Telegramプラグインはグローバルインストールされたプラグインとして全セッションで有効になる。特定のセッションだけに限定する仕組みは現時点では無い。秘書セッションだけがTelegramに応答するよう、秘書CLAUDE.mdで制御する。


常駐化(Persistence)

設計方針

LaunchAgent(macOSのログイン時自動起動の仕組み)は「tmuxセッションを作るだけで即終了」し、tmuxの中で while true ループがClaudeを監視する二層構造。

PC再起動 → ログイン → LaunchAgent起動
                         ↓
                  start-secretary.sh
                    ├── tmux has-session -t secretary → 既存なら何もしない(多重起動防止)
                    └── 新規なら: tmux new-session + run-claude.sh
                                                        ↓
                                               while true ループ
                                                 ├── claude 起動
                                                 ├── クラッシュ検知 → 再起動
                                                 └── 連続失敗5回 → 30分待機

ファイル構成

ファイル 配置先 役割
com.aiharataketo.secretary.plist ~/Library/LaunchAgents/ ログイン時にstart-secretary.shを実行
start-secretary.sh ~/.claude/secretary/ tmuxセッションの存在確認 + 作成
run-claude.sh ~/.claude/secretary/ Claude起動 + クラッシュ時の再起動ループ

起動コマンド

claude \
  --name "secretary" \
  --channels plugin:telegram@claude-plugins-official \
  --allowedTools "Read,Write,Glob,Grep,Bash,mcp__plugin_telegram_telegram__reply,mcp__plugin_telegram_telegram__react,mcp__plugin_telegram_telegram__edit_message,mcp__plugin_telegram_telegram__download_attachment"

--channels plugin:telegram@claude-plugins-official でTelegramチャンネルを有効化する。このフラグがないとTelegramメッセージを受信できない。

--dangerously-skip-permissions は使わない。 --allowedTools で必要なツールだけ許可する方が安全。秘書の安全弁は propose モードであり、OS レベルの権限バイパスではない。

Write が必要。 jobs.json の更新に使う。Agent / WebSearch / WebFetch は秘書の責務外なので含めない。

--permission-mode auto は使えない。 Claude Max プランでは非対応(Team plan 限定)。

LaunchAgent plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.aiharataketo.secretary</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>/Users/aiharataketo/.claude/secretary/start-secretary.sh</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <false/>
  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key>
    <string>/Users/aiharataketo/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    <key>HOME</key>
    <string>/Users/aiharataketo</string>
    <key>LANG</key>
    <string>ja_JP.UTF-8</string>
  </dict>
  <key>StandardOutPath</key>
  <string>/Users/aiharataketo/.claude/logs/secretary-launchagent.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/aiharataketo/.claude/logs/secretary-launchagent.err.log</string>
  <key>ThrottleInterval</key>
  <integer>30</integer>
</dict>
</plist>

KeepAlive=false の理由: スクリプトはtmuxセッションを作って即終了する設計。KeepAlive=true だと「終了のたびに再起動」で無限ループになる。tmuxが独立デーモンとして動くので不要。

クラッシュリカバリー(run-claude.sh の設計)

条件 判定 アクション
正常終了(30秒以上稼働) exit後 5秒待って再起動
クラッシュ(30秒未満で終了) restart_count++ 10秒待って再起動
連続クラッシュ5回 restart_count >= 5 30分待機後にリセット

認証の落とし穴

Claude CodeはmacOS Login Keychainに認証を保存する。LaunchAgentはGUIログイン後に起動するため通常はkeychainにアクセスできる。

状況 keychainアクセス 備考
通常ログイン後 OK 問題なし
スリープ→復帰 OK tmuxセッション生存
再起動後ログイン前 LaunchAgent未起動 設計通り(RunAtLoad)

フォールバック: keychainが問題になった場合は claude setup-token で生成したトークンを CLAUDE_CODE_OAUTH_TOKEN 環境変数としてplistの EnvironmentVariables に追加する。

スリープ時の挙動

運用コマンド

# セットアップ
chmod +x ~/.claude/secretary/start-secretary.sh ~/.claude/secretary/run-claude.sh
launchctl load ~/Library/LaunchAgents/com.aiharataketo.secretary.plist

# 状態確認
launchctl list | grep secretary
tmux has-session -t secretary && echo "alive" || echo "dead"

# 手動再起動
tmux kill-session -t secretary
bash ~/.claude/secretary/start-secretary.sh

# ログ確認
tail -f ~/.claude/logs/secretary.log

未解決の課題

課題 ステータス 備考
レジストリの自動登録 未実装 手動で十分なら手動のまま(YAGNI)
秘書の死活監視 設計済み(v4) LaunchAgent + while trueループ。上記「常駐化」参照
permission relay 対応確認 検証済み nuchiが実動作確認。Telegram上でAllow/Denyボタンが表示される
recall の依存境界 要明記 nuchi-skills のスキル経由。常設CLIではない
スリープ復帰後のTelegram再接続 要検証 自動再接続するか、Claude再起動が必要か
秘書CLAUDE.md 未設計 秘書セッション固有のルール(Phase 0で検証しながら作成)
Telegramからの画像読み取り 要検証 image_path属性 → Readツール → マルチモーダル認識の経路

実装フェーズ

Phase 0: read-only 検証 ✅ 完了

Phase 1: レジストリ + propose ✅ 完了

Phase 2: dispatch ← 現在ここ(2026-03-27)

Phase 3: 拡張


Codexレビューで却下した指摘(YAGNI判断)

指摘 却下理由
per-session の allowed_chat_ids / allowed_tools_profile 個人ツール。ユーザーは1人
list/claim/lock/abort/ack/inspect API 現時点で不要。必要になったら追加
5フェーズ分割 4フェーズで十分
状態遷移図の明記 実装時に決める
deny-by-default のper-session権限境界 propose モードが安全弁。個人ツールにエンタープライズ権限は不要

v3 修正(Codex 3回目レビュー反映)

jobs.json 拡張(High #1 対応)

ジョブに awaiting_reply_kindblocked_reason を追加。Telegram返信をどの待機状態に結びつけるかを永続化する。

{
  "job_id": "j-20260326-001",
  "message": "課金モデル変更して",
  "target_session": "line-call-reminder",
  "status": "BLOCKED",
  "chat_id": "7789180125",
  "status_message_id": "100",
  "awaiting_reply_kind": "permission_modal",
  "blocked_reason": "Do you want to run npm install?",
  "created_at": "2026-03-26T22:35:00Z"
}

awaiting_reply_kind: null / propose_approval / permission_modal / ask_user_question / clarify status_message_id: 進捗報告用のTelegramメッセージID(v4「進捗報告プロトコル」参照)

dispatch前のセッション実在確認(High #2 対応)

dispatch前に tmux has-session -t SESSION_NAME を実行。存在しなければdispatchせず、Telegramに「セッションが見つからない」と返す。tmux.pyの変更は不要。

最小状態表(Medium #2 対応)

status awaiting_reply_kind 意味 遷移先
PENDING clarify セッション特定待ち → PENDING(propose) or キャンセル
PENDING propose_approval ユーザー承認待ち → SENT or キャンセル
SENT null 実行中 → DONE / BLOCKED / FAILED
BLOCKED permission_modal permission応答待ち → SENT(再待機)
BLOCKED ask_user_question 質問応答待ち → SENT(再待機)
DONE null 完了 終端
FAILED null 失敗(timeout/session_gone) 終端

Phase 1 は実験扱い(Medium #1 への回答)

Phase 1 は clarify/propose の精度検証が目的。秘書が落ちてもpending proposalは「もう一回送って」で済む。本格的なジョブ永続化はPhase 2から。

tmux.py の利用文脈(Low #1 への注記)

tmux.py は「新規セッション作成 + テスト」を主導線として設計されているが、秘書では既存セッションへの send/wait/read のみ使用する。setup は使わない。

アクティブジョブの制限(High #1 最終対応)

未解決の対話中ジョブは常に1件まで。 PENDING または BLOCKED のジョブが存在する間は、新しいジョブをdispatchしない。新しい指示が来た場合は「今 [セッション名] で作業中。終わってから対応するわ」と返す。

これにより、Telegram返信がどのジョブに対するものか曖昧にならない。Phase 3 で並列化が必要になった時点で reply_to_message_id を導入する。


v4 修正(nuchi新情報反映 + 実地検証結果)

--channels フラグ

--channels plugin:telegram@claude-plugins-official で Telegram チャンネルを有効化する。このフラグを指定すると、セッションが Telegram メッセージを受信できるようになる。

claude --channels plugin:telegram@claude-plugins-official

注意: v4初稿では「--channels は存在しない」と記載していたが誤り。実地検証で動作確認済み(2026-03-27)。

sendプロトコルの矛盾解消

3つの記述が矛盾していた:

ソース 方式 正しさ
MEMORY.md Enter → Escape → sleep → Enter ❌ 古い(手動tmux send-keys時代の記述)
tmux-claude-debugging スキル Enter Escape → sleep → Enter ⚠️ 動くがAskUserQuestion状態で問題
tmux.py send send-keys -l PROMPT + Enter(Escapeなし) ✅ 正

tmux.pyのコメントに理由が明記されている:

AskUserQuestion state consumes Escape as "cancel question", preventing the prompt from being sent.

秘書は tmux.py の send サブコマンドのみ使用する。 直接 tmux send-keys を使ってはならない。

Permission relay → 検証済み

nuchiが実動作確認済み。TelegramプラグインはAllow/Denyボタンをインラインで表示する。設計書の「要検証」ステータスを解消。

Auto mode → 設計から除外

--permission-mode auto は Claude Max プランでは使用不可(Team plan 限定)。秘書の権限管理は --allowedTools で必要なツールのみ許可する方式に確定。

画像読み取り → 動作確認済み(2026-03-27)

Telegramから送信された画像は以下の経路で読める:

<channel ... image_path="/path/to/image.jpg"> → Read(image_path) → マルチモーダル認識

秘書がスクリーンショットを受け取って画面状態を判断するユースケースが可能。

進捗報告プロトコル

秘書は1つのステータスメッセージを edit_message で更新し続け、ユーザーのアクションが必要な時だけ新規 reply(プッシュ通知あり)を送る。

原則

ツール 通知 使うタイミング
reply → ステータスメッセージ作成 あり ジョブ開始時に1回だけ
edit_message → ステータス更新 なし 進捗が変わるたびに(何度でも)
reply → 新規メッセージ あり ユーザーのアクションが必要な時(承認待ち・質問・完了・失敗)

フロー例

User: 「コールリマインダーの課金変えたい」

Secretary: reply →
  "🔍 セッション特定中..."                              ← msg_id: 100 を記録

Secretary: edit_message(100) →
  "🔍 line-call-reminder を特定
   📋 提案を作成中..."

Secretary: edit_message(100) →
  "🔍 line-call-reminder を特定
   📋 提案: 「課金モデルをサブスク制に変更」を送る
   ⏳ 承認待ち"

User: 「OK」

Secretary: edit_message(100) →
  "🔍 line-call-reminder を特定
   📋 提案: 承認済み
   ⚡ dispatch中..."

Secretary: edit_message(100) →
  "🔍 line-call-reminder を特定
   📋 提案: 承認済み
   ⚡ 実行中..."

--- ここで完了またはブロック ---

【完了の場合】
Secretary: edit_message(100) →
  "🔍 line-call-reminder を特定
   📋 提案: 承認済み
   ✅ 完了"
Secretary: reply(新規) →                               ← プッシュ通知!
  "✅ 課金モデルの変更が完了したわ。
   変更内容: ..."

【ブロックの場合(permission_modal)】
Secretary: edit_message(100) →
  "🔍 line-call-reminder を特定
   📋 提案: 承認済み
   ⚠️ 権限の承認待ち"
Secretary: reply(新規) →                               ← プッシュ通知!
  "⚠️ line-call-reminder が権限を求めてるわ:
   「npm install を実行していい?」
   OK / NG で返して"

ステータス絵文字

絵文字 意味
🔍 セッション特定中 / 情報収集中
📋 提案作成 / 承認待ち
dispatch / 実行中
ユーザー応答待ち
完了
⚠️ ブロック(要アクション)
失敗

jobs.json への反映

ステータスメッセージの message_id をジョブに紐づけて永続化する。秘書が再起動しても、どのメッセージを edit_message すればよいか分かる。

{
  "job_id": "j-20260326-001",
  "message": "課金モデル変更して",
  "target_session": "line-call-reminder",
  "status": "SENT",
  "chat_id": "7789180125",
  "status_message_id": "100",
  "awaiting_reply_kind": null,
  "blocked_reason": null,
  "created_at": "2026-03-26T22:35:00Z"
}

status_message_id: ステータスメッセージのTelegram message_id。edit_message の対象。ジョブ完了/失敗で不要になるが、履歴として残す。

📝 質問モード — テキストを選択してね
✓ 質問を送信しました