Back to skills
SKILL.md
Notification Report
ASecurity購読しているワークツリーから通知が溜まったときに、関連する通知の一覧を読んでレポートアーティファクトを作成する。人はレポートを見るだけで、ターミナルを1つずつ開かずに溜まった通知へ一括でクイックに返答できる。ホームタブや teamwork-parent の親ワークツリーからの利用を想定。ユーザーが「通知をまとめて確認したい」「溜まった通知にまとめて返したい」等と言ったときに使う。
- 2 stars
- 0 votes
- 0 copies
- 1 view
- Added September 13, 2026
Works with
Security analysis
100/100Pro scans all 5 files and shows the line behind each finding
npx -y skills add ishida-supsys/oretachi --skill notification-report --agent claude-codeAre you the author of Notification Report?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/ishida-supsys-notification-report)---
name: notification-report
description: 購読しているワークツリーから通知が溜まったときに、関連する通知の一覧を読んでレポートアーティファクトを作成する。人はレポートを見るだけで、ターミナルを1つずつ開かずに溜まった通知へ一括でクイックに返答できる。ホームタブや teamwork-parent の親ワークツリーからの利用を想定。ユーザーが「通知をまとめて確認したい」「溜まった通知にまとめて返したい」等と言ったときに使う。
allowed-tools: mcp__plugin_oretachi_oretachi__oretachi_list_subscriptions, mcp__plugin_oretachi_oretachi__oretachi_subscribe_worktree, mcp__plugin_oretachi_oretachi__oretachi_list_worktree_notifications, mcp__plugin_oretachi_oretachi__oretachi_poll_inbox, mcp__plugin_oretachi_oretachi__oretachi_ack_message, mcp__plugin_oretachi_oretachi__oretachi_clear_worktree_notification, mcp__plugin_oretachi_oretachi__oretachi_get_worktree_status, mcp__plugin_oretachi_oretachi__oretachi_list_terminals, mcp__plugin_oretachi_oretachi__oretachi_read_terminal, mcp__plugin_oretachi_oretachi__oretachi_inspect_prompt, mcp__plugin_oretachi_oretachi__notify_worktree, mcp__plugin_oretachi_oretachi__artifact, mcp__plugin_oretachi_oretachi__artifact_module, mcp__plugin_oretachi_oretachi__artifact_store, mcp__plugin_oretachi_oretachi__search_artifact, Read, Glob, Grep, Bash(gh issue view:*), Bash(gh pr view:*), Bash(gh repo view:*)
---
# notification-report スキル
購読しているワークツリーから届いた通知を 1 枚のレポートアーティファクトにまとめる。ユーザーはレポート上のボタン + 補足プロンプトで一括返答し、返答は**各ワークツリーの AI 端末へ直接届く**。
## 前提
- **レポートは原則作る(#268)。** **要返答カードが 1 枚でも残れば必ず 1 枚作る。** 作らないのは
Step 1-2 の母集合が 0 件のとき、1-2 の除外(購読外 / 発信元がクローズ済み)と
1-2e の除外(このセッションのレポートで回答済みと確認できた)でカードが 1 枚も
残らなかったとき、そして**残ったのが報告カードだけ(要返答 0 枚)のとき**(→ 1-2f。#301)だけ。
「件数が少ない」「内容が軽い」「ターミナルを開けば分かる」
「前に似たものを出した」はどれも作らない理由にならない。**作るかどうかを価値判断で決めないこと**
—— 何が重要かを決めるのは人であって、レポートはその判断材料を並べるためにある。
**1-2f も価値判断ではない** —— 判定は `kind` から機械的に決まり(→ 1-2c)、出さないのではなく
次に要返答カードが出たときへまとめて回すだけ。
- **カードは画面の写しではなくフォーム(#264)。** 生の hook JSON も画面のダンプも載せない
—— それが読めないからレポートを作っている。生成側が構造化して `paragraphs` / `bullets` /
`fields` / `links` / `request` へ入れ、カードが HTML として組む。折りたたみで生データを
残すのもやらない(畳んであっても「読めないものが置いてある」ことに変わりはない)。
- **整形の材料が足りなければ取りに行く(#264)。** ①ターミナルを読む(1-4b/1-4c)→
②それでも足りなければ**このセッションから該当ワークツリーを直接見に行く**。
見に行ってよいのは **issue(`gh issue view` / `gh pr view`)・ファイル(`Read` /
`Glob` / `Grep`)・アーティファクト(`search_artifact`)の 3 つだけ**。
それでも分からない項目は「不明」と明示する。**推測で埋めない。**
- **git は使わない。** `Bash` の権限は前方一致でしか絞れないので、他ワークツリーを
見るのに要る `git -C` を許すと `git -C <path> push --force` まで無確認で通る。
自分のワークツリーに閉じた `git log` を許しても**発信元の状況は分からない**ので、
用途も無い。差分の規模が要るなら `gh pr view`、無ければ「不明」と書く。
- **`approval` の通知本文には聞かれていることが全部入っている(#264)。** `PermissionRequest`
フックの JSON なので `tool_name` と `tool_input` が丸ごと来る。`AskUserQuestion` なら
**全設問・全選択肢・`description`・`preview` まで**。一方ターミナルの画面は 1 問ずつしか
出さず `preview` は `✂ N lines hidden` で切られるので、**通知の方が情報量が多い**。
設問は通知から起こし、画面(`oretachi_inspect_prompt`)は**送信直前の照合(fingerprint)**
のために取る。
- **カードは「要返答」と「報告のみ」の 2 種類(#228)。** `kind` が `worktree.created` / `worktree.closed` の通知は人の判断が要らないので、返答 UI を出さない**報告カード**として載せる。判定は `lib/send` の `isReportOnly` が `kind` から機械的にやるので、`data/report` にフラグを足す必要は無い(→ 1-2c)。
- **返答の宛先は各通知の発信元ワークツリーの AI 端末。** アーティファクトの JS から `oretachi_write_terminal` を他ワークツリー宛に呼べるが、許可条件は**レポートを置いたワークツリーがその宛先ワークツリーを購読していること**(#211)。向きは購読者側が呼び出し元で、宛先側が自分を購読しているだけでは通らない。
- **レポートはスナップショット。** 母集合は Step 1-2 の `oretachi_poll_inbox` **1 回ぶん**で、開いている間に届いた通知はそのレポートに足さず、次のレポートへ回す。表示中に本体を書き換えて人が見ている画面を動かす方が有害。
- **1 レポートのカードは 20 枚まで(#301)。** カードが 20 枚を超えたら、報告カードに 2 枠を確保したうえで要返答を先・`createdAt` の古い順に 20 枚を選び、あふれたぶんは未 ack のまま次のレポートへ回す(→ 1-2g)。落とすのではないので、カードにならずに消えることは無い。
- **載せた通知は生成時に切る(#219)。** アーティファクトを作り終えたら Step 5.5 で ack + トレイ通知クリアを済ませる。トレイポップアップ通知と同じで「その時点で拾ったぶんだけ」を扱うことになり、レポート同士で同じ通知が二重にカード化されない。**例外は ack が落ちた場合だけ**で、そのときは 1-2e の突き合わせで重なりを解く(#287)。
- **返答状態はサイドカー(`artifact_store`)に持つ。** 本体 JSON に持つと、送信後に「返答済み」へ書き換える操作が自分自身の表示中ロックに弾かれる。ビューア由来の書き込みはロックの対象外。
## Step 1: 収集
### 1-1. 自分の購読を確認する
```
oretachi_list_subscriptions(terminal_id: <セッション開始時に注入された terminal_id>)
```
ここに出てくる `target` が**送信できる宛先の範囲**を決める。ワイルドカード(`*` / `workgroup:<ID>` / `repo:<名前>`)も許可対象で、`event_kinds` は問わない。
**`delivery` が `passive` の購読があったら、そのままにしない。** `passive` は
「押し込まないでほしい」という明示指定で、`event_delivery` の押し込み・件数だけの救済告知・
`Stop` フックの注入の**3 経路すべてから除外される**(`push_pending` / `push_stale_pointers` /
`filter_for_turn_end`)。つまりそのタブは新着が何件届いても自分から動かず、人が
「このスキルをもう一度呼ぶ」まで気づけない。**購読したのにレポートが出てこない**という
形で見える(実機で発生。inbox に 3 件溜まったまま `[delivery]` のログが 1 行も出なかった)。
見つけたら**ユーザーに断ってから** `oretachi_subscribe_worktree` を `delivery: "turn_end"` で
同じ `target` へ張り直す(`(terminal_id, target)` の UNIQUE で上書きされる)。自動でターンを
始めてよいかはユーザーの判断なので、黙って書き換えない。
### 1-2. 通知本文を取る(**ここがレポートの母集合**)
```
oretachi_poll_inbox(terminal_id: <自分の terminal_id>)
```
返ってきた未 ack メッセージが**レポートに載せる通知そのもの**。`id` が inbox メッセージ ID で、Step 5.5 の ack に使う。
**この 1 回の `oretachi_poll_inbox` の返り値がレポートの母集合で、あとから足さない(#219)。** トレイポップアップ通知と同じで「その時点で拾ったぶんだけ」を扱う。レポートを作り終えたら Step 5.5 で**カードに載ったぶんを ack し、宛先のトレイ通知もクリアする**ので、次に誰かが `oretachi_poll_inbox` を叩いても同じ通知は返ってこない。既存レポートとの重複は原理的に起きないので、**重複を避けるための除外ロジックは要らない**。
**ただし ack が落ちた(または Step 5.5 へ到達する前に生成が落ちた)ときだけは、既にカードにした通知が
未 ack のまま戻ってくる。** その通知に人がもう返答している可能性があるので、**1-2e の突き合わせを
踏んでから母集合を確定する(#287)。** このセッションの 1 回目の呼び出しでも飛ばさない ——
生成が落ちたケースでは次の呼び出しが別セッションの 1 回目になる。
`sourceWorktreeId` を 1-1 の購読対象と突き合わせ、購読していないワークツリー由来のものは落とす(載せても返答を送れない)。
**ただし `kind` が `worktree.created` / `worktree.closed` のものは落とさない(→ 1-2c の報告カード。#228)。** この 2 つは返答を送らないので購読の突合も発信元の生存も要らない。
**発信元ワークツリーは `sourceWorktreeId` / `sourceWorktreeName` を必ずここから取る(#218)。**
以降のステップ(1-4 の `oretachi_get_worktree_status` / `oretachi_list_terminals`)へ渡す宛先は
**この 2 つだけを使い、通知本文やターミナル出力から名前を推測しない。** 名前を取り違えると
`oretachi_list_terminals` が別のワークツリー(または該当なし)を返し、`sessionId` が `null` の
カード(「稼働中の AI 端末が見つからなかったため送信できません」)になる。実際にこれが起きていた。
`sourceWorktreeName` が `null` のメッセージは発信元が settings から消えている(クローズ済み)ので、
返答を送れない。レポートに載せない。**例外は報告カード(→ 1-2c)。**
ユーザーが「購読外も含めて全部見たい」と明示した場合だけ、`oretachi_subscribe_worktree` で購読を張ってから含める。**購読は勝手に張らない** — 張ることはそのワークツリーの端末への書き込みを許すことなので、ユーザーの指示なしに範囲を広げない。
**張るときは `delivery` を指定しない(既定の `turn_end` に任せる)。`passive` を選ばないこと。**
このスキルが `oretachi_poll_inbox` で自分から取りに行くからといって `passive` にすると、
**次に通知が届いてもターンが始まらず、ユーザーが手で呼び直すまでレポートが出なくなる**
(→ 1-1 の注意)。`turn_end` なら待機中に押し込みが入り、このスキルがそのまま次のレポートを作れる。
### 1-2b. 母集合が 0 件 / 除外でカードが 1 枚も残らなかったとき(2 回目以降の呼び出しはここに来る)
**1-2 の除外(購読外 / `sourceWorktreeName` が `null`)でカードが 1 枚も残らなかったときもここへ合流する(#268)。**
出すものが無いという点で同じで、下の表もその 2 つを分岐として持っている。
**報告カードだけが残った(要返答 0 枚)ときはここへ来ない(→ 1-2f。#301)。** そちらは出すものが
あって次回へ回しただけなので、`include_acked: true` の引き直しも前回レポートの導線も要らない。
**1-2e(このセッションで作ったレポートで回答済みと確認できた)で全部落ちたときもここへ合流するが、
その場合は引き直しより先に、落としたぶんの ack だけを撃つ(#287)。** 未 ack のまま残すと次の
呼び出しでも同じ母集合が返り、レポートが出ないまま同じ突き合わせを繰り返すことになる。
```
oretachi_ack_message(ids: <1-2e で回答済みと確認した inbox メッセージ ID>, terminal_id: <自分の terminal_id>)
```
撃ったあとで下の引き直しへ進む(レポートを作っていないので Step 5.5 は踏まない。ここで撃つのは
この ack だけで、トレイクリアはしない —— 落とすカードが無く、クリアはワークツリー単位なので
別に積まれたバッジまで消える)。
**落とした件数と宛先ワークツリーは、このあとユーザーへ返す文面に必ず書く。** トレイクリアを
撃たないぶんバッジが残るので、書かないと人には「バッジが付いているのにレポートが出ない」としか
見えない(例: `oretachi-abcd の 2 件は前回レポートで返答済みだったので ack しました(バッジは残っています)`)。
**0 件は異常ではなく、前回のレポートで拾ったぶんを Step 5.5 で ack し切った結果。** 同じセッションで
2 回目以降にこのスキルを呼ぶと、新着が無ければ必ずここに来る。**「通知はありません」で黙って
終わらせず、必ず次を踏むこと。** ここを飛ばすと「1 回出したあと二度目を出さない」という
形でユーザーから見える(#220)。
**まず ack 済みも含めて引き直し、何が起きているのかを確定させる。**
```
oretachi_poll_inbox(terminal_id: <自分の terminal_id>, include_acked: true)
```
`include_acked: true` は **ack 済みも未 ack も含めた全件**(保持期間は 30 日)を返す。未 ack が
混ざるのは「1-2 の絞り込み(購読外 / `sourceWorktreeName` が `null` =発信元がクローズ済み)で
全部落ちた」ケースで、0 件になる理由が ack だけではないため。返り値を次の 3 つに振り分ける:
**この表の絞り込みは要返答カードの話。** `worktree.created` / `worktree.closed` は 1-2 の
絞り込みを受けないので(→ 1-2c)、届いていれば 1-2 の母集合に入り、ここへは来ない
(1-2 と 1-2b の poll の間に新着が届いた場合だけが例外)。
| 返ってきたもの | 意味 | やること |
|---|---|---|
| 何も無い | 購読先から通知が 1 件も来ていない | 1-1 の購読対象と `oretachi_list_worktree_notifications` を見せて状況を伝える。**レポートは作らない** |
| 未 ack があるが、購読外 / `sourceWorktreeName` が `null` で全部落ちた | 返答を送れる宛先が無い | どのワークツリー由来で落ちたのかを件数つきで伝える。**レポートは作らない**(購読を勝手に張らない。→ 1-2) |
| `ackedAt` が入ったものだけ | **既に前回レポートへ載せたぶん**(上の 1-2e で落として ack したぶんもここに来る) | 下の「前回レポートを探す」へ |
**前回レポートを探す:**
```
search_artifact(query: "通知レポート", project_dir: <自分の作業ディレクトリ>)
```
- **見つかって、そのレポートが使える** → ID と導線をユーザーへ提示して終わる。それが唯一の返答窓口
なので、**勝手に作り直さない**(同じ通知を二重にカード化することになる)
- **見つからない/使えない** → 作り直す。「使えない」は次のどれかで、**推測ではなく確認して決める**:
- `search_artifact` に出てこない(消えている)
- `artifact_store(command: "read", ...)` の `answers` に `stale` / `failed` のカードが残っている
- 焼き込んだ `sessionId` が `oretachi_list_terminals` の現在の一覧に無い(アプリ再起動・タブ再作成で失効)
**作り直すときのカード化対象は、前回レポートの `data/report` の `inboxIds` に限る。**
```
artifact_module(command: "get", id: <前回レポートID>, module_name: "data/report", ...)
```
`include_acked: true` の返り値は 30 日ぶんの履歴なので、**そのまま全件カード化してはいけない。**
既に返答済みの通知がもう一度カードになり、ユーザーが同じ返答を宛先へ二重送信しうる。前回レポートの
`data/report` すら読めない(レポートが消えている)場合は、`include_acked` の返り値から
**`ackedAt` が最も新しい一群(=直前のレポートで ack されたぶん)だけ**に絞る。それも決められない
なら、どの範囲を出すかをユーザーへ確認する。
**さらに、そのレポートのサイドカーが読めるなら 1-2e と同じフィルタを掛ける(#287)。**
`data.answers[cardId].status` が `sent` のカードは返答が確定しているので、作り直しの対象から
外す(作り直す理由は `stale` / `failed` / session_id 失効のカードにある)。**外してよいのは
`sent` だけ**で、`unverified` / `pastedOnly` は確定していないので残す(→ 1-2e)。
ここは別セッションが作ったレポートでも読んでよい —— 1-2e の「このセッションのぶんだけ」という
制限は突き合わせ先を絞るためのもので、ここは対象が前回レポート 1 枚に確定している。
以降は「レポートを作り直すとき」の手順どおり(`prompt` は画面が変わっているので必ず取り直す)。
**ID は前回と必ず別にする**(→ Step 5 の ID の作り方)。
### 1-2c. 判断が不要な通知は「報告カード」へ振り分ける(#228)
母集合のうち **`kind` が `worktree.created` / `worktree.closed` のものは報告カード**にする。返答 UI が出ず、人は読むだけで済む。
**この 2 つだけが報告カードになる。** どちらも `notify_worktree` からは発行できず、oretachi 内部の `fire_worktree_created` / `fire_worktree_closed`(`src-tauri/src/lib.rs`)からしか出ない。本文も `format_inbox_line`(`event_db.rs`)が定型文に組み直すので、エージェントが自由文で「判断が欲しい」と書き込む余地が構造的に無い。
**`completed` / `hook` は報告カードにしない。** どちらも `notify_worktree` 経由でエージェントが任意の本文を書けるため、「実装は終わったので次の指示が欲しい」のような返答待ちが混ざる。送信 UI を消すと、その返答待ちに気づく導線まで消える。
**振り分けを `data/report` のフラグでやらない。** `lib/send` の `isReportOnly` が `kind` から機械的に判定するので、**このステップで足すフィールドは無い**。フラグにすると、生成側が落とした瞬間に判断不要の通知が返答待ちとして並ぶ(あるいは逆)。
報告カードで埋めるフィールドは 7 つだけ:
```
id / inboxIds / worktreeName / worktreeId / kind / at / paragraphs (+ 任意で branchName / issueRef / links)
```
- **`paragraphs` には 1 行の定型文を入れる。** `oretachi_poll_inbox` の `body` は**パース済みの JSON オブジェクト**(`worktree.closed` なら `{worktreeId, worktreeName, branchName}`)で、人が読める 1 行は別フィールドの `text`(`format_inbox_line` の出力)にある。**オブジェクトを本文へ入れない**(`lib/send` の `paragraphsOf` が保険で弾くが、そこへ落ちると「本文を整形できませんでした」としか出ない)。実際の `text` はこの形:
- `worktree.closed` → `ワークツリー 'oretachi-htlz' (ブランチ: worktree/issue-214) がクローズされました`
- `worktree.created` → `[oretachi] ワークツリー 'oretachi-wnqd' (ブランチ: worktree/issue-228) が作成されました`(先頭の `[...]` はリポジトリ名。無ければ付かない)
- **1-4 / 1-5 を回さない。** `oretachi_get_worktree_status` / `oretachi_list_terminals` / `oretachi_read_terminal` / `oretachi_inspect_prompt` を報告カードのために呼ばないこと。返答を送らないので使われず、`worktree.closed` は発信元ワークツリーが既に削除済みでそもそも引けない。`worktree.created` も作成直後なので description 未設定・AI 端末未起動が普通で、読めるものが無い。
- **`worktreeName` / `branchName` は通知本文から取る。** `worktree.closed` では `sourceWorktreeName` / `sourceWorktreePath` が `null` になる(発信元がもう settings に無く、この 2 つは settings を引いて解決するため)。本文の `WorktreeClosedBody` / `WorktreeCreatedBody` に `worktreeName` / `branchName` が焼き付いているので、そちらを使う。**ここだけは「発信元の名前を本文から取らない」という 1-2 の原則の例外**で、成立するのは報告カードが名前を表示にしか使わない(他ツールへ渡さない)ため。
- **`worktreeId` は報告カードでも必ず入れる(#294)。** Step 5.5 のトレイクリアに使うので、要返答カードと同じく必須扱い。`sourceWorktreeId` は `worktree.closed` でも `null` にならない(settings を引かずに inbox の行をそのまま返す)ので、**そのまま使えばよい**。本文の `worktreeId` にも同じ値が焼き付いているのでどちらでも同じになる。
- **購読の突合も発信元の生存確認もしない。** `worktree.closed` は fanout 直後に、そのワークツリーを **ID 指定で**購読していた行が削除される(`lib.rs` の `fire_worktree_closed` → `delete_subscriptions_for_target` は `WHERE target = ?`)。つまり ID 指定の購読で受けていたぶんは 1-1 の `oretachi_list_subscriptions` と突き合わせると落ちる(`*` / `workgroup:` / `repo:` のワイルドカード購読なら残るので落ちない)。**どちらに転んでも突合しない。** inbox に行があること自体が「配送時点で購読していた」証拠で、返答も送らないので認可としてはそれで十分。
- **配列では要返答カードより後ろに置く。** 判断が必要なカードを上に集める(`entry-point.jsx` も描画時に同じ並べ替えをする)。
### 1-2d. カードを落としてよいのは 2 つの場合だけ(#268)
母集合に入った通知は**原則すべてカードにする。** 落としてよいのは 1-2 で挙げた次の 2 つだけで、
どちらも人の判断を挟まずに機械的に決まる(報告カードはこの 2 つとも免除。→ 1-2c):
1. `sourceWorktreeId` が 1-1 の購読対象に無い(返答を送れない)。**判定には 1-1 の
`oretachi_list_subscriptions` との突合が要る**(突合を免除されているのは報告カードだけ。→ 1-2c)
2. `sourceWorktreeName` が `null`(発信元がクローズ済みで、もう届かない)。
クローズ済みは 1-4(b) の `oretachi_list_terminals` がエラーを返して**あとから判明することもある**
—— 同じ事象を poll 後に検知しているだけなので、そこで落とすのもこの条件 2 に含む
**上限(1-2g)で次のレポートへ回すのは「落とす」ではない(#301)。** 未 ack のまま残して
次の母集合に戻すので、カードにならずに消えることが無い。落とすのは上の 2 つだけという原則は
そのまま。
**「もう回答済みに見えるから」で落とさない。** 実際に起きていた取りこぼしがこれで、
同じワークツリーから通知が複数来ているときに 1 件だけを見て「この宛先は片付いたはず」と
判断し、残りをカードにしていなかった。**回答済みかどうかは通知からは分からない** ——
通知は発信時点のスナップショットで、そのあと人がターミナルで答えたかどうかは載っていない。
同じワークツリーの通知が n 件あれば、それは「n 回呼ばれて、そのうち何回に答えたか不明」であり、
「1 件答えれば全部片付く」ではない。
**例外は 1-2e だけ(#287)。** このセッションで作ったレポートのサイドカー(`answers`)に
`status` が `sent`(送信して画面が変わった)になっているぶんは、通知からの推測ではなく自分の送信結果なので、
落として ack してよい。それ以外の材料で回答済みと判断しないこと。
**片付いたかどうかを確かめてよいのは `oretachi_inspect_prompt`(1-4c)の現在の画面だけで、
その結果によってカードを落とすことはない。** 判定結果は「カードをどの形で出すか」を決めるだけ:
| 判定結果 | カードの扱い |
|---|---|
| ツール許可 / プラン承認で止まっている(`shape` が `permission` / `plan`) | 要返答カード(画面の選択肢そのまま) |
| `AskUserQuestion` で止まっている(`shape: askUserQuestion`) | 要返答カード。**設問・選択肢は通知の hook JSON から起こして `request.questions` へ入れる**(画面からは 1 問しか読めない。→ 1-4g) |
| シェル側の確認 / 素の TUI(`shape` が `yesno` / `numbered`) | 要返答カード(画面の選択肢そのまま) |
| 入力欄だけ(`shape: text`) | 要返答カード(候補ボタン + 補足) |
| ページャで止まっている(`shape: pager`) | 送信ボタン無効。**`tail` は出さず**「ターミナルで閉じて」の案内だけ(→ 1-4d) |
| 形状が分類できない(`shape: unknown`) | 送信ボタン無効 + `tail` 表示のまま**載せる**(→ 1-4d) |
| 同じ `sessionId` で 2 枚目以降 | `prompt: null` + `choices: []` の参考表示として**載せる**(→ 1-4e) |
| 端末が見つからない(`sessionId` が `null`) | 送信不可の表示のまま**載せる**(→ 1-5) |
どの行も「載せない」にはならない。**その通知が片付いたかを判断するのも人の仕事**で、
カードが無ければその判断の材料ごと消える。答える必要が無かったと分かるのは、
人がカードを見て「これはもう答えた」と思えたときであって、生成側が先回りして決めることではない。
### 1-2e. このセッションで作ったレポートの返答状況を突き合わせる(#287)
**このセッションで既にレポートを作っているなら必ず踏む。** まだ 1 枚も作っていない場合は、
**このワークツリーに最近更新されたレポートが無いことを確かめてから飛ばす**(下の「控えた ID が
手元に残っていないとき」)。生成が Step 5.5 へ到達する前にプロセスが落ちたケースでは、次の
呼び出しが別セッションになるのに、レポートは作られていて人が返答済みということが起こる。
1-2 の母集合に**未 ack のまま戻ってきた通知**が混ざることがある。Step 5.5 の ack が失敗した、
または生成が Step 5.5 へ到達する前に落ちた場合で、**その通知は既にレポートのカードになっていて、
人がそのカードから返答を送り終えていることがある。** 通知そのものは発信時点のスナップショットなので
「返答済みかどうか」は載っていない(→ 1-2d)が、**このセッションで作ったレポートのサイドカーには
送信結果が残っている。** ここだけは推測の入らない確定的な記録なので、突き合わせてよい。
Step 5 で控えたレポート ID(→ Step 5「作ったレポート ID を控える」)1 枚ごとに、`data/report` と
サイドカーを読む:
```
artifact_module(command: "get", id: <このセッションで作ったレポートID>, module_name: "data/report",
project_dir: <自分の作業ディレクトリ>)
artifact_store(command: "read", id: <同じレポートID>, project_dir: <自分の作業ディレクトリ>)
```
`artifact_store` の返り値は `{id, data, updated_at}` で、**`answers` は `data` の直下**(`data.answers`)。
`answers` は**カードの `id` キー**で、値の `status` が送信結果(各 status の意味は「ダイアログ経路の
安全弁」の表)。`data/report` の同じ `id` のカードが持つ `inboxIds` が、その返答に対応する inbox
メッセージ ID。この対応で母集合の各通知を振り分ける:
| そのカードの `data.answers[cardId].status` | 意味 | 母集合での扱い |
|---|---|---|
| `sent` | **送信が成立している**(ダイアログ経路では画面の変化まで確認済み、自由入力経路は本文と CR の書き込みが両方通った) | カードにしない。**ack する**(要返答カードが残ってレポートを作るなら Step 5.5 で他と一緒に 1 回、報告カードだけ残ってレポートを作らないなら 1-2f の 2 で先に撃つ、カードが 1 枚も残らないなら 1-2b で先に撃つ)。再掲すると同じ返答を二重送信しうる |
| `unverified` / `pastedOnly` | 送ったか不明 / 途中で失敗(未確定) | 通常どおりカードにする(下記) |
| `stale` / `unsupported` / `failed` | 何も送れていない(未回答) | 通常どおりカードにする |
| `answers` にそのカードが無い | 人がまだ触っていない(未回答) | 通常どおりカードにする |
**落として ack してよいのは `sent` だけ。** `unverified` は「キーは送ったが画面が変わらなかった」
だけでなく、`RequirementUnmet`(宛先が本文を受け取れる状態だと確認できなかった)で**本文を 1 文字も
送っていない**ケースを含む。`pastedOnly` も本文だけが入力欄に残った中途半端な状態。どちらも
「人がもう答えた」とは言えないので、落として ack すると**一度も届いていない通知が inbox から
消えて二度と返らない**(#287 の逆方向のデグレになる)。
**`unverified` / `pastedOnly` を再カード化するときは、カードの `paragraphs` に前回の結果を 1 行
書き添える**(例: `前回のレポートで「本文は届いたが Enter が失敗」で終わっています。再送せず、
ターミナルで状態を確認してください`)。新しいレポートは別のサイドカー(`answers` が空)になるので、
**前回のカードが持っていた再送ガードが効かない** —— `lib/send` の `canSend` は前回カードの
`answers[cardId]` を見て塞ぐので、記録の無い新カードは素通しされ、自由入力なら本文をもう一度
貼れてしまう(二重になったテキストが 1 回のプロンプトとして飛ぶ)。
- **Enter だけ送り直す復旧導線があるのは自由入力(`shape: text`)の `pastedOnly` だけ**
(`entry-point.jsx` の `resume` → `sendEnter`)。この導線は前回レポートのカードに残っていて、
ack したかどうかとは無関係に生きているので、前回レポートが開けるならそちらから撃つ方が安全。
- **ダイアログ経路で前回カードから再送できるのは `failed`(何も送れていない)だけ。**
`unverified` / `pastedOnly` で終わったダイアログのカードは読み取り専用なので、**ターミナルで人が
直接操作するしかない**。カードにはその旨を書く。
- **再カード化したカードは「本文は前回のもの / 画面は現在のもの」になる。** `prompt` は取り直すが
`paragraphs` / `fields`(承認対象のコマンド全文など)は前回の通知由来のまま。宛先が先へ進んで
別のダイアログを出していると、人が古い説明を読みながら別の要求に答えることになりうる。
**1-4c を取り直した結果が前回と別の問いなら、`prompt: null` + `choices: []` の参考表示に倒す**
(→ 1-4e)。送信させず、人がターミナルで確かめる形にする。
- **「未回答だから ack を保留する」を既定にしない(#287)。** 回答済みと確認できた通知を未 ack の
まま残すと、次のレポートにも、そのまた次にも同じカードが出続ける。人からは「答えたのに何度も
同じことを聞かれる」形で見える。確認が取れたぶんは母集合から落として ack する。
- **突き合わせるのはこのセッションで作ったレポートだけ。** `search_artifact` で過去のレポートを
片端から遡らない —— 対応の確実さは「自分がそのレポートを作った」ことに依っていて、遡るほど
「どの通知がどのカードだったか」の対応が怪しくなる(#287 のスコープもここまで)。
- **控えた ID が手元に残っていないとき**(会話が圧縮された、ack へ到達する前にプロセスが落ちて
セッションが変わった等)**は、`search_artifact` で自分のワークツリーの「通知レポート」を引き、
直近に更新されたものだけに絞って突き合わせてよい。**
```
search_artifact(query: "通知レポート", project_dir: <自分の作業ディレクトリ>, updated_after: "-1d")
```
**`updated_after` に epoch ミリ秒をそのまま渡さない。** `updated_after` は **UNIX 秒**(相対指定の
`-1d` / `-12h` も可)だが、inbox の `createdAt` も `artifact_store` の返り値の `updated_at` も
**epoch ミリ秒**。ミリ秒を渡してもエラーにならず、閾値が数万年先になって**黙って 0 件**になる。
秒で切りたいなら 1000 で割って切り捨てる。
絞り込みが効くのはアーティファクト**本体**の更新時刻で、人がレポート上で返答してもサイドカーしか
更新されない(本体の `updated_at` は動かない)。実質「レポートを作った時刻」で切ることになるので、
**`-1d` で出てこなければ `-7d` まで広げて引き直す。** それでも見つからなければ突き合わせは諦め、
母集合を全部未回答として扱う(→ 下の「判断が付かないとき」)。
- **1-2d の「回答済みに見えるから落とす」とは別物。** 1-2d が禁じているのは**通知の中身や
ターミナルの画面から、人が答えたかどうかを推測すること**。ここでやるのは**自分が送った結果の
記録を読むこと**で、推測が入る余地が無い。
- **判断が付かないときは未回答として扱う**(カードにする)。サイドカーが読めなかった、
`data/report` が読めなかった、`inboxIds` と対応が付かない、はすべてこちら。取りこぼすより
二重に出す方が安全(人が「これはもう答えた」と分かる)。
### 1-2f. 報告カードだけになったら出さずに蓄積する(#301)
1-2c〜1-2e を通した結果、**要返答カードが 1 枚も無く報告カード(`worktree.created` /
`worktree.closed`)だけが残ったときは、レポートを作らずそのまま次回へ持ち越す。**
判断が 1 つも要らないレポートはその都度出す価値が薄く、実際に「1 件だけの報告レポート」が
連続して出ていた。
**判定は 1-2c の振り分け結果(`kind`)から機械的に決まる。件数も内容も見ない。**
「報告が 1 件だから軽い」ではなく「**要返答が 0 枚**だから今は出さない」であって、
#268 が禁じている価値判断とは別物。**要返答カードが 1 枚でもあれば、報告カードが何枚あろうと
従来どおり即時に作る**(蓄積ぶんもそこへ載る。1 レポート 20 枚の上限内で、報告カードには
最低 2 枠が確保される。→ 1-2g)。
やること:
1. **蓄積ぶんを ack しない。トレイクリアもしない。** レポートを作っていないので Step 5.5 は
踏まない。**未 ack のまま残すことが蓄積そのもの**で、次の `oretachi_poll_inbox` が同じ通知を
もう一度返す。inbox は sqlite なのでセッションをまたいでも残る。
**トレイクリアはカードに載せた発信元へ撃つもの**(報告カードも含む。#294)で、蓄積ぶんは
どのカードにもなっていないため撃つ根拠が無い(→ Step 5.5)。
2. **1-2e で回答済み(`status: sent`)と確認して落としたぶんがあれば、その ack だけは撃つ。**
こちらは蓄積とは無関係で、根拠が自分の送信記録なので次回に拾い直す意味が無い(→ 1-2e)。
```
oretachi_ack_message(ids: <1-2e で回答済みと確認した inbox メッセージ ID>, terminal_id: <自分の terminal_id>)
```
**0 件なら呼ばない。** `ids` が空だと `invalid_params` で必ず失敗する
(`oretachi_ack_message` が空文字を除いたうえで空配列を弾く)。
3. **1-2b へ合流しない**(→ 1-2b)。出すものが無いわけではないので、引き直しも前回レポートの
導線も要らない。
4. 端末へ「蓄積中の合計件数と発信元ワークツリー名」+「その件数がタブの未読として残ること」を
返す(→ Step 6「生成後に端末へ出す文面」)。
**`notify_worktree` は撃たない** —— 開くレポートが無く、急いで見る必要も無い。
**次に要返答カードが 1 枚でも出れば、蓄積ぶんはそのレポートに載り始める。** 未 ack なので次の
poll の母集合にそのまま入り、1-2c が同じように報告カードへ振り分ける。20 枚を超えるぶんは
さらに次へ回るが、**報告カードには毎回 2 枠が確保される**ので、要返答が詰まっていても
蓄積は必ず減っていく(→ 1-2g)。**生成側で「持ち越し中の
通知」を覚えておく必要は無いし、覚えてはいけない** —— セッションが変わった瞬間に失われる記録に
依存すると、蓄積ぶんが宙に浮く。状態は inbox の未 ack 行だけが持つ。
**蓄積ぶんは以後、中身が押し込まれることは無い。** `oretachi_poll_inbox` は返した行に
`delivered_at` を打つので、蓄積ぶんは押し込み(`list_pushable`)からも「N 件残っています」の再送
(`list_stale_unpushed`)からも外れる(どちらも `delivered_at IS NULL` が条件)。
残るのは**件数だけの導線**で、タブの未読バッジ(未 ack 件数)と、同じタブで新しいセッションが
始まったときの `SessionStart` 告知(`reports_carryover_only` が `SessionStart` だけ true)がそれに当たる。
**どちらも数が出るだけで中身は分からない。** 次に**新着**が届けばその押し込みでターンが始まり、
そこで撃つ poll が蓄積ぶんも一緒に返す —— つまり**新着が止まると、蓄積ぶんの内容に触れられるのは
このスキルが次に走ったときだけ**になる。だから Step 6 の 1 行を必ず出す。
**蓄積ぶんが消える経路(端末出力に件数を書く理由):**
- **人が UI の「既読にする」を押したとき。** 購読パネルのこのボタンは `ack_all` で、そのタブの
未 ack 行を**種別も配送状況も見ずに全部** ack する。蓄積ぶんはどのレポートにも載っていないので
復旧できない。**タブの未読件数だけが残る**状態を見た人がこれを押すのは自然な操作なので、
Step 6 の文面で「その未読の中身は蓄積中の報告である」ことを伝えておく。
- **素のセッションが `SessionStart` の告知に従って ack したとき。** 告知文は
「`oretachi_poll_inbox` で内容を確認し、`oretachi_ack_message` で ack してください」なので、
このスキルを読んでいないセッション(`/clear` 直後のホームタブ等)は素直に従う。
内容はそのセッションが読むので人が気づけなくなるわけではないが、**蓄積は途切れる。**
- **このスキルを走らせているタブが消えて 7 日**(`ORPHANED_RETENTION_DAYS`)。未 ack 行は生存タブ
一覧から外れた時点で orphaned になり、7 日で削除される(`orphaned_at` が打たれるのは
未 ack 行だけなので、**蓄積=未 ack がそのまま対象になる**)。自動で引き継がれるのは同じ AI セッションが
`--resume` で戻ってきた場合など限られたケースだけ(`rebind_next_orphaned_group` は
同一セッションの購読を持つグループか、購読行が残っていないグループしか拾わない)。
ホームタブのようにワイルドカード購読を抱えたまま落ちたグループは自動では戻らないので、
購読パネルの手動引き継ぎを使う。
- タブが生きている限りの上限は **30 日**(`INBOX_RETENTION_DAYS`)。
**上の 2 つは人やエージェントの操作、下の 2 つは黙って走る掃除**で、どれも蓄積中だと分かって
やっているわけではない。だから**端末出力には必ず蓄積中の件数を書く**(→ Step 6)。
人がいつでも「報告だけでも出して」と言える状態にしておくのが、蓄積ぶんを失わせないための担保。
**ユーザーが明示的に「報告だけでも出して」と指示したときは作る。** その場合は通常どおり
Step 2 以降を回し、Step 5.5 の ack も撃つ(そのレポートが唯一の記録になるので切ってよい)。
タイトル・`notify_worktree` の件数は `要返答 0 件 / 報告 M 件` になる。
### 1-2g. 1 レポートのカードは 20 枚まで(#301)
**1 枚のレポートに載せるカードは最大 20 枚。** 超えたぶんは**落とすのではなく、1-2f の蓄積と
同じ形(未 ack のまま)で次のレポートへ回す。** 何十枚も並ぶレポートは上から順に捌けず、
結局ターミナルを開くことになるので、1 枚で捌ける量に切る。
**選び方は機械的に決める。人が見て重要そうなものを選ぶのではない**(→ 1-2d / 禁止事項):
1. **報告カードが 1 枚でもあるなら、まず 2 枠を報告カードに確保する。**
要返答カードを先に詰め切ると、要返答が毎回 20 枚以上流入する状況で**蓄積した報告カードが
一度も載らないまま保持期限で消える**(1-2f が「次に要返答カードが出れば載る」と
約束しているのに果たせなくなる)。確保するのは 2 枠だけなので、判断待ちの捌け方は
ほとんど変わらない。
2. **残り枠に要返答カードを詰める**(報告カードより優先。判断が要るものが後回しになると意味が無い)
3. 同じ種別の中では **`createdAt` が古い順**。新しい順にすると、古い通知が毎回あふれて
永久に載らない(保持期限まで残り続けて消える)
4. 要返答カードが残り枠を埋め切らなければ、空いたぶんは報告カードで埋める(蓄積ぶんがここで捌ける)
5. **枠が足りないときは、同じ `sessionId` の 2 枚目以降(1-4e により `prompt: null` の
参考表示になるカード)を先にあふれ側へ回してよい。** 押せないカードが枠を食うと、
他ワークツリーの押せる要返答カードが次回送りになる
**「`createdAt` の古い順」は 20 枚を選ぶ順序であって、`data/report` の配列順ではない。**
配列順は従来どおり(要返答カードを先、同じ `sessionId` なら `prompt` を付けた最新 1 件を先頭。
→ 1-2c / 1-4e)。`lib/send` の `promptConflicts` は配列の先頭を生かすので、ここを取り違えると
最古のカードが生き残って 1-4(e) と逆になる。
**1-4 / 1-5 を回すのは、載せると決めた 20 枚に対してだけ。** 先に上限で切ってから
`oretachi_list_terminals` / `oretachi_read_terminal` / `oretachi_inspect_prompt` を回す。
あふれたぶんに回しても、次のレポートでは画面が変わっていて取り直しになる。
**あふれたぶんの扱いは 1-2f の蓄積と同じ:**
- **ack しない。** Step 5.5 で ack するのは実際にカードへ載せたぶんだけ(既存原則のまま)。
未 ack なので次の `oretachi_poll_inbox` の母集合にそのまま戻る。
- **押し込まれない**(`delivered_at` 済み)。だから**端末出力に「残り N 件は次のレポートへ
回しました」を必ず書く**(→ Step 6)。消える経路も 1-2f と同じ。
- **続きはこのスキルをもう一度呼べばすぐ出せる。** あふれたぶんが母集合に戻り、要返答カードが
あれば次のレポートになる。1-2b の「出すものが無い」とは別の状態なので、そちらへは合流しない。
**トレイクリアは「そのレポートのカードに載せた発信元」だけへ撃つ**(要返答・報告の両方。#294)。
同じワークツリーの通知が 20 枚に収まらず分かれた場合、**載ったぶんを理由に撃つクリアが、
あふれたぶんのバッジまで落とす**(クリアがワークツリー単位なので避けられない)。
そのぶんも端末出力の「残り N 件」で人へ伝わるようにしておく。
### 1-3. トレイ通知は補助情報として使う(母集合にしない)
```
oretachi_list_worktree_notifications()
```
**これを母集合にしてはいけない。** このツールが見る `NotificationRegistry` はフロントから `sync_notification_state` で全置換されるトレイバッジの写しで、`oretachi_poll_inbox` / `oretachi_ack_message` が見る sqlite の event_db とは**完全に別のストア**。`oretachi_clear_worktree_notification` はトレイ側しかクリアしない。
つまり「トレイ通知が出ているワークツリー」で inbox を絞ると、**バッジだけ先にクリアされたワークツリーの未 ack メッセージがレポートから漏れる。**
使ってよいのは「どのワークツリーに人の注意が向いているか」という補助情報として。カードの並び順を決めたり、`firstNotifiedAt` を参考にしたりする用途に留める。
なお**このツールは全ワークツリー分を返す**(ワークツリースコープが効かない)ので、参照するときも 1-1 の購読対象へ絞ること。
### 1-4. 「そのワークツリーが何なのか」を集める
通知本文だけでは、人はどのワークツリーが何をしていてどの段階なのか分からない。カードごとに次の 2 つを必ず埋める。
**このステップ以降(1-4 / 1-5)は要返答カードだけが対象。** 1-2c で報告カードへ振り分けたものはここを飛ばす(#228)。
**(a) ミッション**
```
oretachi_get_worktree_status(query: <1-2 の sourceWorktreeName>)
```
返る各エントリの `id` を `sourceWorktreeId` と突き合わせて**同名の別ワークツリーを取り違えない**
(`query` は部分一致なので、名前が前方一致する別ワークツリーも返る)。
`description` が入っていればそれを使う。**未設定なら `null` を入れ**、代わりに 1-4(b) のターミナル出力からミッションを推定して `descFallback` に書く(カードには `[description 未設定]` バッジ付きで出る)。
**(b) 現況**
```
oretachi_list_terminals(worktree_id: <1-2 の sourceWorktreeId>) → isAiAgent: true / status: "running" の sessionId
oretachi_read_terminal(session_id: <上で得た sessionId>, max_bytes: 8192)
```
**絞り込みは `worktree_id` で行う(`worktree_name` ではない)。** 名前指定は同名ワークツリーが
あるとエラーになり、綴りが 1 文字違うだけで「該当なし」になる。ID なら `oretachi_poll_inbox` が
返した値をそのまま渡せる(#218)。
返り値のフィールド名は **`sessionId`**(camelCase)。`session_id` というキーは無い。
**0 件だったときに `sessionId: null` へ直行しないこと。** このツールの絞り込みは
**各 PTY の `cwd` から解決したワークツリー**で行われる(`resolve_worktree_by_cwd` は
`cwd` に前方一致するワークツリーのうち最も深いものを採る)。そのため
**cwd をワークツリー外へ移した生存 AI 端末や、そのワークツリーの下にネストして登録された
別ワークツリーへ吸われた端末は、ID 指定でも結果から落ちる。** 0 件のときは:
1. 絞り込みなしで `oretachi_list_terminals()` を呼ぶ
2. `isAiAgent: true` / `status: "running"` かつ `cwd` が 1-2 の `sourceWorktreePath` 配下に
あるものを探す(`worktreeId` が別の値になっていても、そこで走っているのは
その発信元の端末)
3. それでも無ければ本当に AI 端末が無い(素のシェルだけ / タブを閉じた)
ID 指定がエラーになるのは「その ID の登録が無い」=発信元がクローズ済みの場合だけなので、
そのときは 1-2 の判断(レポートに載せない)へ戻る。
読んだ出力から**フェーズと 1 行要約**にまとめる。フェーズは次のいずれか(カード側の色分けがこの文字列に対応している):
| phase | 使う場面 |
|---|---|
| `設計中` | 設計・調査中。まだコードを書いていない |
| `実装中` | 実装作業中 |
| `実装完了` | 実装が終わっている(未 commit / PR 作成済み などは要約に書く) |
| `レビュー対応中` | セルフレビューやレビュー指摘の反映中 |
| `停止条件待ち` | 停止条件でユーザー確認待ちで止まっている |
| `不明` | 読み取れなかった |
要約は「実装完了・未 commit。セルフレビュー待ち。差分 12 ファイル / 約 900 行」のように、**次に何が必要か分かる粒度**で 1 行。読んだ時刻を `readAt` に入れる(スナップショットなので、いつ時点かが分からないと判断を誤る)。
**入力欄に見えるテキストを「ユーザーが打った」と読まないこと(#289)。** Claude Code は
入力待ちの入力欄に**自動候補(ゴーストテキスト)**を出す:
```
❯ 進捗どう?
```
これは Claude Code が勝手に出した次のプロンプトの提案であって、**ユーザーの入力ではない**。
画面上は人が打ちかけたテキストと同じに見える(色も装飾も付かない)ので、
`oretachi_read_terminal` のテキストから読み取ろうとすると必ず取り違える。
**「ユーザーが◯◯と入力中」「返答済みらしい」といった判断を、read_terminal の入力欄から
組み立ててはいけない。** 打ちかけかどうかは 1-4(c) の `pendingInput` だけで判断する。
この (a)(b) は**レポート生成時のこのセッションが MCP クライアントとして呼ぶ**ので、他ワークツリーの端末も読める(アーティファクトの JS からの `read_terminal` にかかる購読条件は適用されない)。
### 1-4(c). 「いまどんな問いで止まっているか」を取る(**必須**)
```
oretachi_inspect_prompt(session_id: <1-4(b) と同じ値>)
```
**戻り値をそのまま `prompt` フィールドへ焼き込む。** 要約・言い換え・整形をしてはいけない。
**`prompt` は原則「送信直前の照合(fingerprint)」のためのもので、表示には使わない(#264)。**
カードに出す設問・選択肢は 1-4(g) で通知の hook JSON から起こす。画面から取った
`prompt.questions` を表示に使うのは、hook JSON が無い経路
(`completed` / `worktree.message` など)と、選択肢が画面にしか無い**ツール許可 /
プラン承認**だけ。
- `options[].label` は**宛先の画面に実在する選択肢そのもの**。言い換えると人が「画面に無い選択肢」を選ぶことになる
- `fingerprint` は送信直前の照合キー。書き換えると照合が必ず外れて何も送れなくなる
- `cursorIndex` はいま `❯` が当たっている番号。キー列(矢印の回数)がここから決まる
- `shape` が `text` のときの `header` は**受け手の種類**(`[Claude Code の入力欄]` /
`[シェルのプロンプト] PS X:\...>`)。fingerprint に混ざっており、「レポート生成時は CC の
入力欄だったが送信時には CC が終了してシェルのプロンプトだけが残っている」状況を `stale` で
弾くために使う。**書き換えないこと**
- `pendingInput` は**人が打ちかけているテキスト**(空なら「未入力の入力欄」)、
`inputSuggestion` は Claude Code の**自動候補(ゴーストテキスト)**で**ユーザー入力ではない**(#289)。
**どちらも `shape` が `text` のときだけ有効。** ダイアログが開いている画面では入力欄が
塞がれているので常に空だが、それは「下書きが無い」ことの保証ではない(Claude Code は
ダイアログ表示中も下書きを保持する)。ESC 経路で本文を送るときは確かめる手段が無い。
受け手がシェルのプロンプト(`header` が `[シェルのプロンプト] …`)のときも常に空で、
これも「未入力」を意味しない。入力欄を「箱」として同定できなかった過渡的な画面
(再描画の途中など)でも空になる —— **空は「未読」でもありうる**ので、
「未入力だから何を送ってもよい」とは読まないこと。
- **`inputSuggestion` を「ユーザーが打った」と読まない。** 候補を要約や `summary` に
混ぜると、人は「もう誰かが返答した / 何か入力中だ」と誤解してカードを飛ばす。
- **`/` / `@` で始まる打ちかけは「補完のポップアップが開いている」印(#292)。**
接頭辞が残っているので**末尾の CR の意味が変わる**: 連結でマッチが外れれば
`/co進捗どう?` という壊れた本文が送られ、マッチが残れば
**CR が補完の確定として食われて何も送られない**。`lib/send` がこの形のカードの
送信を機械的に塞ぐので、`summary` には「宛先で補完の打ちかけが開いている」と書く。
- `pendingInput` が空でないときは**打ちかけの本文が宛先の入力欄に残っている**。
そこへレポートから本文を送ると**打ちかけの後ろに連結されて送信される**ので、
カードの `summary` に「宛先に打ちかけのテキストあり」と明記して人が気づけるようにする。
- どちらも `fingerprint` には入らない(人が 1 文字打っただけで送信が `stale` になるのを避けるため)。
つまり**打ちかけの有無は `stale` では弾けない**。だから表示で伝える。
- `tail` の候補行には `⟪自動候補(ユーザー入力ではない): …⟫` の印が付く(`shape: text` のときだけ)。**印を外さないこと。**
- `truncated` が `true` なら**ダイアログが宛先の画面に収まっておらず、選択肢を全部読めていない**。
カードは選択 UI を出さず、警告と画面末尾を表示して ESC 経路だけを残す。**このフィールドを
落とすと「読めた選択肢だけ」が完全な一覧として提示され、拒否の選択肢を見ないまま承認させる**
(実測: 7 行のタブで `options` が `[{1, "Yes"}]` だけになった)
- `navigation` は選択肢の選び方(`arrows` / `digits` / `none`)。**`shape` とは独立**で、
Claude Code のフッタという構造的な手がかりから決まる。狭いターミナルで見出しが折り返して
`shape` の推定が外れても、キーの種類だけは間違えない(実測: 13 桁では
`Do you want to proceed?` が 3 行に、`Esc to cancel · Tab to amend` が 4 行に割れる)
**`oretachi_read_terminal` のテキストから問いを読み取ろうとしないこと。** Claude Code は
カーソル移動で差分描画するので、ANSI を除去したバイト列には再描画の断片しか残らない
(実測: 選択肢を 1 つ動かした 4 バイトは `strip_ansi` 後に空文字列になる)。
`oretachi_inspect_prompt` は出力履歴を VT エミュレータへ流し直した**画面グリッド**を見る。
`oretachi_read_terminal` は 1-4(b) の「現況の 1 行要約」を作るためにこれまでどおり使う。
役割が違う(要約は流れたログから、問いの形状は現在の画面から)。
### 1-4(d). 問いのパターンと回答手段
`shape` ごとにカードの見た目と送信経路が変わる。**候補を創作してよいのは `text` のときだけ。**
| shape | 画面上の特徴 | カードの UI | 送られるキー |
|---|---|---|---|
| `text` | 入力欄だけ(ダイアログ無し) | 候補ボタン + 補足プロンプト(従来どおり)。`!` 始まりの候補は**コマンド実行**(→ Step 4) | 本文 → 150ms → CR |
| `permission` | `Do you want to proceed?` + 番号付き選択肢 | **画面の選択肢そのまま**のラジオ + 承認対象の全文 | 矢印で `❯` を動かして CR |
| `plan` | `Would you like to proceed?` + `No, keep planning` | 同上 | 同上 |
| `askUserQuestion` | タブバー(`← ☒ Color ☐ Size ✔ Submit →`)+ 設問 + 番号付き選択肢、フッタ `Enter to select · …` | **通知から起こした全設問のフォーム**(→ 1-4g) | 1 問ずつ矢印 + CR。最後に Submit |
| `yesno` | `(y/N)` / `[Y/n]`(シェル側の gh / npm / git など) | `y` / `n` ボタン | `y` or `n` → CR |
| `numbered` | 素の TUI の `1) foo` | 画面の選択肢そのままのラジオ | 数字 → CR |
| | ↑ `pager` は選択肢を持たないので `truncated` は常に `false`(選択 UI が無いので意味が無い) | | |
| `pager` | `less` / `git log`。画面末尾が `:` / `(END)` / `--More--` | **送信ボタン無効。** 「ページャを閉じて」の案内だけ | 送らない |
| `unknown` | 分類できないが入力待ちらしい | **送信ボタン無効。** 画面末尾を読み取り専用で表示 | 送らない |
**`pager` は `unknown` と違って「何の画面か」が分かっている(#292)。**
送信は一切できないが、カードには理由を名指しで出す。**`tail` は出さないこと** ——
ページャの中身は生データで、読めないからレポートを作っている(→ 落とし穴)。
判定根拠は画面末尾が `:` 単独 / `(END)` / `--More--` であること。
**番号の無い `❯` リスト(`/resume` / `/config`)は `unknown` のまま(#292)。**
名指しはできないが、**自由入力と誤認はしない**ようパーサ側で塞いである。
誤認するとピッカーのハイライト行が入力欄に見え、送った本文 + CR が
**設定の絞り込み欄へ撃ち込まれる**。カードは送信ボタン無効 + `tail` 表示になる。
**選択肢を出すカードでは、スクロールしているリストに必ず `truncated` が立つ(#292)。** `/model` のように
画面へ入り切らないリストは `❯` と同じ桁に `↓` を描き、末尾に `… +3 models` を出す。
選択肢は読めるが**画面に全部は出ていない**ので、カードは選択 UI を出さない。
「2 つしか無い」と誤認させて選ばせないための安全弁で、**`truncated` を落とさないこと。**
**補完のポップアップが出ていても `shape` は `text` のまま(#292)。** `/` コマンド補完と
`@` ファイル補完は入力欄の**下**へ一覧を描くが、入力欄自体は生きている。
このとき `pendingInput` に打ちかけ(`@src/ma`)が入るので、
**`summary` に「宛先に打ちかけのテキストあり」と明記する**(送った本文は打ちかけの後ろへ連結される)。
**`Type something.` を選ぶと自由入力できる(#265)。** Claude Code は選択肢の後ろに
`Type something.` を足す。カードでもこれを選ぶと本文欄が開き、送信は
「その選択肢を確定 → 本文 → CR」の 1 本のキー列になる
(`oretachi_answer_prompt(kind: "selectThenText")`、複数設問なら `selectAll` の
`option_texts`)。**ESC 経路とは別物**で、ESC はダイアログごと(複数設問なら全問)
キャンセルするのに対し、こちらは**その設問への回答**として自由文を渡す。
番号がずれていても Rust 側がその番号のラベルを確かめてから送るので、
`Type something.` でなければ**何も送らない**。
`escapeHatch` が `"esc"` のカードには「**ESC で抜けて指示を書く**」欄が別に出る。
`No, and tell Claude what to do differently` を選ぶのと同じ着地点で、選択肢の文言に依存しない。
ただし ESC 経路が実際に使えるのは `permission` / `plan` / `askUserQuestion` の 3 形状のみ
(`yesno` / `numbered` では `escapeHatch` が立っていても使えない)。
**キーの種類は `shape` ではなく `navigation` で決まる。** 上の表の「送られるキー」は
典型的な組み合わせで、実際に送られるのは `prompt.navigation` が示すもの
(`arrows` = 矢印 + CR / `digits` = 数字 + CR)。`numbered` に見える画面でも、
**選択肢に `❯` が描かれていれば矢印**になる(Claude Code の確認画面がこれ。
数字は確定キーではないので、数字を送ると無視されて CR だけが効き、
`❯` の当たっている別の選択肢が確定してしまう。#264)。
**選択は数字キーではなく矢印 + CR。** 実測で確認済み: 許可ダイアログへ Down を 2 回送ってから
CR を送ると、3 番目の選択肢が確定した。`cursorIndex` から目標までの移動量を Rust 側が計算するので、
選択肢が 10 件以上あっても同じ手順で通る(数字キーだと 2 桁を打てない)。
素の TUI の番号リスト(`navigation: "digits"`)だけは行入力ベースなので数字 + CR。
この振り分けは `navigation` が持っている。
### 1-4(e). 1 セッションにつきキー操作カードは 1 枚だけ
**ダイアログは 1 つしか無い。** 同じ `sessionId` へ `shape` が `text` 以外のカードを 2 枚
向けると、2 枚目は必ず `stale`(1 枚目で画面が変わっている)になるか、最悪の場合
**1 枚目の回答が別の問いへ撃ち込まれる**。
同じ宛先の通知が複数あるなら、**最新 1 件だけ**に `prompt` を付け、残りは
`prompt: null` + `choices: []` の参考表示にする。
(`lib/send` の `promptConflicts` が 2 枚目以降を機械的に塞ぐが、そもそも作らない)
**「カードを 1 枚だけにする」ではない(#268)。** 減らすのは**キー操作**であって
カードではない。残りの通知もカードとしては全部載せる —— 載せないと、人はその通知が
届いたことすら分からないまま、ダイアログ 1 つに答えて片付いたつもりになる。
### 1-4(f). 複数選択・複数設問の扱い
- **`multiSelect` は画面から判別できない**(単一選択との差が画面に出ない)。`questions[].multiSelect`
は常に `false` で、カードも単一選択のラジオになる。**複数選ばせたい設問は人がターミナルで操作する**
- **複数設問は 1 枚のカードで全問答えられる(#264)。** 1-4(g) で通知の hook JSON から
全設問を `request.questions` へ写しておけば、カードが全問のフォームを出し、送信は
`oretachi_answer_prompt(kind: "selectAll")` の 1 回で「1 問選ぶ → 画面が次の設問へ進むのを待つ」を
繰り返して**最後の確定(Submit)まで**進む。
**`request.questions` を入れ忘れると画面から読めた 1 問だけのカードになり、
2 問目以降に答えられなくなる。**
### 1-4(g). 通知の中身を「読める形」に起こす(**必須**。#264)
`oretachi_poll_inbox` が返す `text` は `'<ワークツリー名>' の通知[approval]: <本文>` で、
`approval` / `hook` / `completed` / `general` の `<本文>` は
**Claude Code のフック JSON がそのまま**入っている(実測):
```
'oretachi-xaoe' の通知[approval]: {"session_id":"…","transcript_path":"…","cwd":"…",
"hook_event_name":"PermissionRequest","tool_name":"Bash","tool_input":{"command":"sqlx migrate run …",
"description":"Apply pending migrations"}}
```
**これをそのままカードへ貼らない。** JSON をパースして次へ振り分ける。
**(a) 本文 → `paragraphs` / `bullets` / `fields` / `links`**
- `paragraphs` は「誰が / 何を待っているか」を人の言葉で 1〜2 段落。
`**強調**` / `` `コード` `` / `#203` / URL はカードが装飾とリンクに組むので、そのまま書いてよい
- `tool_input` の中身は `fields`(ラベルと値の表)へ。`Bash` なら
`コマンド`(`code: true`)/ `説明` / `作業ディレクトリ`、`Edit` / `Write` なら `ファイル` など、
**そのツールで人が判断に使う項目だけ**を選ぶ(`session_id` / `transcript_path` は要らない)
- `links` に**発信元が作ったアーティファクト**(`search_artifact` で探す)と、
本文に出てくる PR / issue の URL を入れる。アーティファクトは
`artifact://worktree/<worktreeId>/<アーティファクトID>` 形式
- **`artifacts` に発信元の URL アーティファクトを入れる(#265)。** `search_artifact` で
発信元ワークツリーのものを引き、返り値の `type` が `text/uri-list` のものだけを
`[{ id, title }]` で並べる(URL 本体は入れない)。ワークツリー名の隣の
「📄 アーティファクト」ボタンに**ホバーした**ときのポップアップに出る
(ボタンのクリックはアーティファクトウィンドウを開く。→「カードからの導線」)。
**0 件でもボタン自体は出る**ので、URL アーティファクトが無ければ空で構わない。
`links` と役割が違う:
`links` は「この通知に関係する参照」、`artifacts` は
「そのワークツリーに登録してある行き先の一覧」(本体の UI のアーティファクトボタンと同じ)
- `worktree.message` のような自由文の通知は、本文をそのまま `paragraphs` に入れてよい
(Markdown はカードが組む)。ただし**長い出力の貼り付けやログの断片が混ざっていたら、
人が判断に使う部分だけを残す**
**(b) `tool_name` が `AskUserQuestion` なら `request.questions` へ写す**
`tool_input.questions` を**並べ替えずそのまま**写す:
```
request: {
tool: "AskUserQuestion",
questions: [{ header, question, options: [{ label, description, preview }] }, …]
}
```
**順番を変えてはいけない。並べ替えも間引きもしない。** カードは i 番目の選択肢を
画面の i+1 番として送り、宛先では**設問がタブの並び順に対応づけられる**
(Claude Code は通知の選択肢をその順で `1.` から並べ、後ろに `Type something.` /
`Chat about this` を足す)。1 問でも落とすと**以降の設問へ 1 つずれた答えが入る**。
保険は二重に置いてある(どちらも「ずれていたら 1 件も送らない」):
- `lib/send` の `hasDroppedQuestion` が「選択肢の無い設問」を見つけたらカードを塞ぐ
- Rust の `plan_select_all_step` が画面のタブ数と回答数を突き合わせて拒否する
ただし**保険に頼らないこと** —— 塞がれたレポートは作り直すしかなく、その間
宛先のエージェントは止まったままになる。
`preview` は**ターミナルでは `✂ N lines hidden` で切られて読めない**ものなので、
省略せず全文を入れる(レポートの価値がいちばん出るところ)。
**(c) 埋まらない項目は「不明」と書く**
フック JSON からもターミナルからも取れないときは、このセッションから
`gh issue view <番号>` / `gh pr view <番号>`、`search_artifact`、そして
**発信元ワークツリーのファイルを直接読む**(`Read` / `Glob` / `Grep` に
`sourceWorktreePath` 配下のパスを渡す。`sourceWorktreePath` は 1-2 の
`oretachi_poll_inbox` が返している)。
**発信元ワークツリーへ触れてよいのはこの 3 つだけ。** git は使わない(`Bash` の
権限は前方一致でしか絞れず、他ワークツリーを見るのに要る `git -C` を許すと
`git -C <path> push --force` まで無確認で通る)。差分の規模のような git 由来の
情報が要るときは `gh pr view` で足り、それでも分からなければ**推測せず**
「不明」と明示する。
### 1-5. 送信先の session_id を決める
1-4(b) で得た「そのワークツリーで走っている AI エージェント端末」の `sessionId` をそのまま `sessionId` に入れる。見つからなければ `null` を入れる(カードが「稼働中の AI 端末が見つからなかったため送信できません」になる)。
**`null` を入れる前に 1-4(b) の 0 件時の手順(絞り込みなしで引き直して `cwd` を見る)を
必ず通す。** そのカードは人が押せないので、レポートの価値がそのぶん失われる。実測で起きていた
`null` はどれも「宛先ワークツリー名を推測して `oretachi_list_terminals(worktree_name: ...)` が
空を返した」ケースで、端末は生きていた(#218)。本当に AI 端末が無いのか
(素のシェルだけ / タブを閉じた)を `oretachi_list_terminals` の返り値そのもので
確認してから `null` にすること。
アーティファクトからは `oretachi_list_terminals` が呼べないため、`session_id` は**ここで焼き込むしかない**。アプリ再起動やタブ再作成で失効するので、失効したらレポートを作り直す(作り直しの手順は後述の「レポートを作り直すとき」— 通知は ack 済みなので `include_acked: true` で拾い直す)。
## Step 2: レポートは毎回新規に作る(既存へ追記しない)
**既存レポートを探して追記しようとしない(#219)。** レポートごとに扱う通知が排他になっているので、追記する理由が無い。
理由:
- Step 5.5 でカードに載せた通知を ack + トレイクリアするため、次の `oretachi_poll_inbox` には**そのレポートに載っていない通知だけ**が返る。母集合が重ならない
- 既存レポートの通知は既に ack 済みで、そのレポート自身が唯一の返答窓口になっている。そこへ新しい通知を混ぜると、人が「どこまでが前回ぶんか」を見失う
- そもそも既存レポートが開かれていれば表示中ロックで書き込めない(`locked_while_open` のエラーになる)。**開いている限りリトライしても成功しない**
`search_artifact` を使うのは「直前のレポートで何を扱ったか」を人へ説明したいときだけでよく、生成フローの必須ステップではない(例外は 1-2b の 0 件時。前回レポートの導線をユーザーへ返すために使う)。
**要返答カードが 1 枚でも残れば必ず 1 枚作る(#268 / #301)。** 作らないのは次の 2 つだけ:
- **出すものが無い** —— 母集合が 0 件、または 1-2 の除外(購読外 / `sourceWorktreeName` が `null`)
および 1-2e の除外(このセッションのレポートで回答済みと確認できた)でカードが 1 枚も残らなかった。
どれも 1-2b の手順でユーザーへ状況を返す(空のレポートは作らない)
- **残ったのが報告カードだけ(要返答 0 枚)** —— 1-2f の手順で蓄積し、次に要返答カードが出た
レポートへまとめて載せる(#301)
**どちらも件数や内容を見て省略しているのではない。** 前者は出すものが無く、後者は `kind` から
機械的に決まる(→ 1-2f)。それ以外に「毎回新規に作る」の例外は無い。
**1 枚あたりのカードは 20 枚まで(→ 1-2g。#301)。** あふれたぶんは未 ack のまま次のレポートへ
回る。**これは「作らない」ではなく「1 枚に収まらないぶんを次へ送る」**なので、上の 2 つの例外とは別。
## Step 3: テンプレートを読み込む
このスキルディレクトリ(`SKILL.md` と同じ場所)の `templates/` フォルダにある以下のファイルを使う。
| ファイル | アーティファクトモジュール | カスタマイズ要否 |
|---|---|---|
| `templates/entry-point.jsx` | エントリポイント(content) | `// CUSTOMIZE:` 箇所のみ |
| `templates/components--NotificationCard.jsx` | `components/NotificationCard` | そのまま利用 |
| `templates/lib--send.jsx` | `lib/send` | そのまま利用 |
| `templates/data--report.example.jsx` | `data/report` | ※スキーマ参照用、新規生成 |
**「そのまま利用」の2本(`components--NotificationCard` / `lib--send`)は Read しない。**
Step 5 で `artifact_module` に `file_path` を渡せば oretachi 側がファイルを読んで登録するので、
読み込んだ内容を `content` へ書き戻す往復(同じテキストが 2 回トークンを食う)が消える。
`<SKILL_DIR>` は**スキル読み込み時に先頭へ注入される `Base directory for this skill:` の絶対パス**を
そのまま使う(例: `C:\Users\<user>\AppData\Roaming\com.ia.oretachi\claude-plugins\oretachi\skills\<スキル名>`)。
`${CLAUDE_PLUGIN_ROOT}` などの環境変数は展開されないので、素で渡してはいけない。
相対パスはワークツリー追加先ディレクトリ基準で解決されるため、テンプレートには届かない。
## Step 4: 候補ボタンを作る(`shape` が `text` のときだけ)
**報告カード(→ 1-2c)には `choices` を作らない。** 返答 UI が出ないので使われない(`choices` ごと省略してよい)。
**`shape` が `text` 以外のカードでは候補を創作してはいけない。** `prompt` を焼き込むだけで、
カードが `options[].label`(宛先の画面に実在する選択肢)をそのまま出す。`choices` は無視されるので `[]` にする。
実在しない選択肢を人へ見せると、「押したのに画面と違う」「押した内容が届かない」という
形で判断を誤らせる。#215 の要点はここ。
以下は `shape` が `text`(ダイアログ無しで入力待ち)のカードにだけ当てはまる。
`choices` は**通知内容から作る**。その通知に対して人が返しそうな短い返答を 2〜4 個。
- 承認を求めている通知 → `了解` / `この方針で進めて` / `別案も見たい` / `保留`
- 二択の判断を求めている通知 → その二択をそのまま(`分割して` / `1 本でよい`)+ `詳細を教えて` / `保留`
- 事実確認を求めている通知 → `クリア済み` / `未クリア` / `詳細を教えて` / `保留`
- 内容から候補を作れない場合は `choices: []` でよい(`その他` と補足プロンプトだけになる)
**`保留` に特別な処理は無い(#313)。** 押しても宛先へは他の候補と同じ経路で
「保留」という文字列がそのまま返答として送られ、通知は他の候補と同じく送信済みになるだけで、
「処理が保留される」「後でまた聞かれる」といった動作は存在しない。カード側
(`components/NotificationCard` の `ChoiceChip`)がこの候補だけ破線・注意色にし、
「送信済みにするだけ」という注記とツールチップを添えて他候補と区別しているので、
生成側は文言を工夫する必要はなく、ラベルは `保留` のままでよい。
**`その他(補足で指示)` は `choices` に入れない。** コード側(`components/NotificationCard`)が常に末尾へ足す。選ぶと補足プロンプトが必須になり、空なら送信対象から外れる。
### コマンド実行の候補(`!` 始まり。#288)
**`!` で始まる候補は、宛先の AI への返答ではなく、宛先のターミナルで走るシェルコマンドになる。**
Claude Code は入力欄の先頭が `!` の状態で Enter を受けるとシェルモードとして実行し、
その出力はそのままエージェントのコンテキストに入る(実測で確認済み)。
「判断の前に事実を 1 つ確かめさせたい」通知に効く:
- 差分の規模を聞かれている → `!git diff --stat`
- 実装完了の報告で検証したい → `!pnpm run type-check`
- CI の状況を聞かれている → `!gh pr checks`
守ること:
- **人が中身を読んで即座に許可できる読み取り系だけにする。** 押した瞬間に走る。
ファイルを書き換える / push する / リリースするコマンドは候補にしない
(そういう判断はコマンドではなく通常の候補で宛先のエージェントへ返す)。
- **1 行で完結させる。** 改行は空白へ畳まれる。`&&` で繋いだ長い列は候補にしない。
- **前置きも補足も付かない。** カードは選んだコマンドだけを送る(コード側が補足欄を塞ぐ)。
`その他(補足で指示)` の補足欄に `!` 始まりで書いた場合も同じ扱いになる。
- **`!<コマンド名>` の形で書く。** `!` の直後がコマンド名らしい文字
(英数字 / `.` / `_` / `/` / `\` / `-`)でなければコマンド扱いにならず、
通常の返答として前置き付きで送られる
(`!!至急やって` のような感嘆符始まりの日本語をシェルへ流さないため)。
- **300 文字を超える候補を作らない。** 読み切れない 1 行は送信対象から外れる。
カード側は、コマンドの候補を等幅 + 橙のチップで描き、選ぶと
**実際に送る 1 行をそのまま**折り返して警告枠に出してから押させる(`lib/send` の `commandOf`)。
**コマンドを実行してもカードは「未返答」のまま残る。** シェルモードの実行は宛先のターンを
開始しないので、元の問いは未回答のままだからである。カードは畳まれず読み取り専用にもならず、
`コマンド実行済み・未返答` のバッジが付いて、結果を見てから改めて候補を選んで返答できる。
実行直後に候補の選択は自動で外れる(外さないと次の一括送信で同じコマンドがもう一度走る)。
**同じカードから同じコマンドを撃ち直すことはできない**(下書きの保存に失敗した場合の
二重実行を塞いでいる)。撃ち直したいときはターミナルを開いて直接打つ。
**シェルモードのコマンドは、宛先エージェントの Bash 許可ダイアログを通らない。**
人がターミナルで `!` を打つのと同じ経路だからで、**唯一のゲートは人がカードで中身を読んで
押すこと**になる。だから候補に入れてよいのは読み取り系までで、書き込み・push・リリースは入れない。
## Step 5: アーティファクト作成
**`locked_while_open: true` を必ず付ける**(付けないと、別のエージェントが人の入力中にレポートを書き換えられる)。
### 作ったレポート ID を控える(#287)
**作成に成功したレポートの ID は、このセッションが終わるまで覚えておく。** 同じセッションで
2 回目以降にこのスキルを呼んだとき、1-2e で「そのレポートのカードに人がもう返答したか」を
サイドカーから引くのに使う。控えていないと、返答済みの通知が未回答として再びカード化され、
ack も保留され続ける。
### ID の作り方(**時刻を推測しない**)
ID は `notif-report-<載せた通知のうち最大の createdAt>`。`createdAt` は 1-2 の `oretachi_poll_inbox` が
返す epoch ミリ秒なので、**ツールの返り値から機械的に決まる。**
**現在時刻から ID を作ってはいけない(#220)。** このセッションに渡っているのは日付だけで時刻が無く、
`HHMM` を埋めようとすると推測になる。同じ日に 2 回目を作ると前回と同じ ID を書いてしまい、
前回レポートがビューアで開かれていれば `create` が `locked_while_open` で拒否される。
その拒否メッセージは「リトライしても開いている限り成功しません」と言うので、**そこで諦めて
2 回目のレポートが出ない**という形になっていた。
作る前に未使用であることを確かめる:
```
artifact(command: "outline", id: <作ろうとしている ID>, project_dir: <自分の作業ディレクトリ>)
```
- `アーティファクト '<ID>' が存在しません` のエラー → 未使用。そのまま使う
- 構造が返ってきた(既にある)→ **末尾に `-2`, `-3` … を足して空いている ID を探す**
作り直し(→ 1-2b)では母集合が前回と重なるので衝突しやすいが、**必ず衝突するわけではない**
(前回の最大 `createdAt` を含まないサブセットを拾い直すなら別 ID になる)。`outline` の結果で
機械的に決めること。
**`locked_while_open` の拒否が返ったら、諦めずに ID を変えて作り直す。** 表示中ロックが守るのは
**そのウィンドウでいま開いている 1 件だけ**なので、別 ID なら必ず通る。
「リトライしても成功しない」は**同じ ID への再試行**についての話で、新しいレポートを
作れないという意味ではない。
**1. エントリポイント作成**
```
artifact(command: "create", id: "notif-report-<最大 createdAt>",
type: "application/vnd.ant.react",
title: "通知レポート — <N> 件(<発信元ワークツリー名を最大 3 件、以降は「他 M 件」>)",
locked_while_open: true,
project_dir: <自分の作業ディレクトリ>,
content: <entry-point.jsx。CUSTOMIZE 箇所のみ調整>)
```
タイトルにも生成時刻を入れない(同じ理由で推測になる)。件数と発信元にしておくと
`search_artifact` で前回レポートを探すときにも人が見分けられる。
**報告カードが混ざるときは件数を分けて書く**(例: `通知レポート — 要返答 3 件 / 報告 2 件(oretachi-xaoe 他 2 件)`)。
`N 件` にまとめると、開くまで返答が必要なのかどうか分からない(#228)。
**1-2g の上限であふれたぶんがあれば、タイトルの末尾に `(他 K 件は次のレポート)` を足す(#301)。**
端末出力と `notify_worktree` は流れて消えるので、**レポート自身にも続きがあることを残す**
(後から開いた人・別セッションが「これで全部」と読めてしまう)。
**2. `lib/send` モジュール作成**(そのまま)
```
artifact_module(command: "create", id: <同じID>, module_name: "lib/send",
project_dir: <自分の作業ディレクトリ>,
file_path: "<SKILL_DIR>/templates/lib--send.jsx")
```
**3. `components/NotificationCard` モジュール作成**(そのまま)
```
artifact_module(command: "create", id: <同じID>, module_name: "components/NotificationCard",
project_dir: <自分の作業ディレクトリ>,
file_path: "<SKILL_DIR>/templates/components--NotificationCard.jsx")
```
2・3 はテンプレートをそのまま登録するので `content` ではなく `file_path` を渡す(Read も書き戻しも不要)。
**4. `data/report` モジュール作成**(Step 1 で集めた内容から新規生成)
```
artifact_module(command: "create", id: <同じID>, module_name: "data/report",
project_dir: <自分の作業ディレクトリ>, content: <META + NOTIFICATIONS>)
```
`META.reportId` は上で決めた ID、`META.generatedAtMs` は**その ID の元になった最大 `createdAt`(epoch ms)**
をそのまま入れる。`META.repoUrl` には `gh repo view --json url -q .url` で取ったリポジトリ URL を入れる
(本文中の `#203` と `issueRef` がリンクになる。**分からなければ省略する** —— 当てずっぽうの
リンクを張るより、コード表記のまま出す方がよい。#264)。**`META` に時刻の文字列を組み立てて入れない** — 整形は `entry-point.jsx` の
`generatedLabel` がブラウザ側でやる(#220)。カードの `at` / `readAt` は従来どおり `HH:MM` の文字列
(`at` は対応する通知の `createdAt` から、`readAt` はターミナルを読んだ時点のもの)。
**5. 検証**
```
artifact(command: "outline", id: <同じID>, project_dir: <自分の作業ディレクトリ>)
```
エントリポイント + 3 モジュール(`lib/send`, `components/NotificationCard`, `data/report`)が揃っていれば完了。
## Step 5.5: 拾った通知を切る(ack + トレイ通知クリア)
**アーティファクトが揃ってから**、レポートに載せた通知を inbox とトレイの両方から落とす。これでレポートが「その時点で拾った通知」を抱えた閉じたスナップショットになり、次の `oretachi_poll_inbox` には出てこなくなる(#219)。
```
oretachi_ack_message(ids: <カードに載せた inboxIds の全体>, terminal_id: <自分の terminal_id>)
oretachi_clear_worktree_notification(worktree_id: <カードに載せた発信元ワークツリーの ID。**要返答・報告の両方**>) ← 宛先ごとに 1 回
```
**`terminal_id` を必ず渡す。** 省略して `project_dir` も渡さないと `oretachi_ack_message` は
`invalid_params` で必ず失敗する。しかも `oretachi_clear_worktree_notification` の方は
`worktree_id` 明示なので成功するため、**ack だけ落ちてバッジだけ先に消える**という
最悪の非対称になる(同じ通知が次のレポートに再掲されるのに、人が気づく導線だけ失われる)。
inbox の行は `subscriber_terminal_id` でスコープされているので、渡すのは
**Step 1-2 で `oretachi_poll_inbox` に渡したのと同じ `terminal_id`**。
- **Step 5 の検証が通ってからにする。** 先に ack すると、生成が途中で落ちたときに**どのレポートにも載っていない通知が inbox から消える**(本文は `data/report` にしか残らないので復旧できない)。
- **ack するのは実際にカードへ載せたぶんだけ。** 1-2 で落としたもの(購読外・発信元がクローズ済み)は ack しない。次の機会に拾い直せる状態のまま残す。
- **例外: 1-2e で回答済みと確認して落としたぶんは、カードに無くても ack する(#287)。** 落とす根拠が
`answers` の送信記録であって推測ではないので、次の機会に拾い直す意味が無い。未 ack のまま残すと
同じ通知が次のレポートにも出続け、人からは「答えたのに何度も同じことを聞かれる」形で見える。
`ids` には要返答カード・報告カードのぶんと一緒に混ぜて、1 回の呼び出しで渡してよい。
**このぶんはトレイクリアの対象にはしない**(クリアを撃つのはこのレポートのカードに載せた発信元だけ。
落としたぶんはカードになっていないので、そのワークツリーのバッジを落とす根拠が無い)。
1-2e が発火する状況では前回の ack 失敗でトレイクリアも撃たれていないため、**そのワークツリーの
通知が全部 1-2e で落ちるとバッジだけが残る。** 落ちた件数と宛先を Step 6 の本文に書いて、
人がバッジを消せるようにしておく。
- **報告カードも同じタイミングで ack する(#228)。** 返答しないカードだが、`inboxIds` を要返答カードのぶんと一緒に 1 回の `oretachi_ack_message` へ渡す。報告カードだけ収集直後に ack すると「Step 5 の検証より前に ack しない」が種別ごとに分岐し、生成が途中で落ちたときにどのレポートにも載っていない報告が inbox から消える。揃えておけば生成失敗時は次のレポートに再掲されるだけで済む(読むだけなので再掲は無害)。
- **前回までに蓄積したぶんもここで一緒に切れる(#301)。** 持ち越された報告カードは未 ack なのでこの回の母集合に入り、そのままこのレポートに載っているため。**ただし ack の範囲は変わらず「実際にこのレポートのカードへ載せた `inboxIds` だけ」**で、蓄積を理由に範囲を広げない。**1-2g の上限(20 枚)であふれたぶんも同じ理由で ack しない**(載せていないので次の母集合に戻る)。逆に **1-2f で蓄積した回は Step 5.5 を踏まない**ので、報告カードは未 ack のまま次回へ残る。
- **トレイクリアはワークツリー単位**(`NotificationRegistry` に通知 1 件ごとの粒度が無い)。カードに載せた発信元ワークツリーぶん(要返答・報告の両方。#294)だけを、`worktree_id` 指定で落とす。**同じワークツリーの通知が 1-2g の上限で分かれた場合、載ったぶんを理由に撃つクリアがあふれたぶんのバッジまで落とす**(ワークツリー単位なので避けられない。→ 1-2g)。そのぶんは Step 6 の「残り」の行で人へ伝える。
- **報告カードの発信元にも撃つ(#294)。** 判定は「要返答カードかどうか」ではなく「このレポートのカードに載せた発信元か」で行い、要返答カードと同じ扱いにする。ack は #228 で既に揃えてあり、クリアだけ分岐させると**報告カードしか出ていないワークツリーのバッジが誰にも落とされずに残る**。
- **落ちるのは別種別のバッジになる。** `worktree.created` / `worktree.closed` 自体は**トレイバッジを積まない**(`src/utils/notificationKinds.ts` の `showsBadge` が `worktree.` 始まりを除外し、`useNotifications.ts` の `worktree-event-fired` も「バッジは積まず、音と OS 通知だけ」)。同じワークツリーが要返答カードの発信元でもあるなら、そちらを理由に撃つクリアと結果は変わらない。
- **⚠️ 実害が出るのは 1 通りだけ:「`worktree.created` を購読しているが `approval` は購読していない」構成。** そのとき新しいワークツリーの `approval` は inbox に入らない=カードにならないのに、報告カードを理由に撃つクリアでバッジだけ落ちる。`approval` は #225 で「トレイ通知オフでも人待ちだけは通す」ために救済した種別なので、落とすと人の入力待ちが伝わらなくなる。**該当する構成なら Step 6 の本文に「`<ワークツリー名>` のバッジを落としました」と書いて人が見に行けるようにする。** なお `oretachi_subscribe_worktree` の `event_kinds` の既定は `["worktree.closed"]` だけで、そちらはクリア自体が失敗する(下)ので無害。
- **失敗しても無視してよい。** `worktree.closed` の発信元は既に settings から消えているので `worktree_id` が解決できず、クリアは通らない。バッジ側もクローズ時の `clearNotification`(`src/composables/useWorktreeRemove.ts`)が既に落としているので、落とすものが無い。**この失敗は常態なので Step 6 の本文には書かない**(毎レポートに同じ行が出てノイズになる)。エラーを握って次の宛先へ進む。
- **1-2f で蓄積した回は撃たない。** レポートを作っていないので Step 5.5 自体を踏まない(→ 1-2f)。
- **必ず ack を先に、トレイクリアを後にする。ack が失敗したらトレイクリアはしない。** ack が落ちた通知は次のレポートに再掲されるので、バッジを残しておけば人がもう一度捌ける。逆順(またはack 失敗を無視してクリア)にすると、再掲される通知の気づく導線だけが消える。
- **失敗自体は許容**する。ack が失敗すれば次のレポートに同じ通知が出るだけ、トレイクリアが失敗すればバッジが残るだけで、レポート自体は成立している。失敗したら Step 6 の本文にその旨を書く。
**人がレポートに気づく導線はバッジではなく Step 6 の `notify_worktree` が担う。** バッジを生成時に落としてよいのはそのため(#218 で「生成側でクリアするな」としていたのは、この導線が無い前提だった)。
### レポートを作り直すとき(`stale` / `session_id` 失効の復旧)
`stale`(送信直前に画面が変わっていた)や session_id 失効のカードは「レポートを作り直してから返答する」ことになるが、**その通知はもう ack 済みなので普通の `oretachi_poll_inbox` には返ってこない。** そのまま作り直すと空のレポートができるだけで、その通知には二度と返答できなくなる。
作り直すときは必ずこちらを使う:
```
oretachi_poll_inbox(terminal_id: <自分の terminal_id>, include_acked: true)
```
`include_acked: true` は ack 済みも含めて返す。ここから**作り直す対象のカードに対応する inbox メッセージだけ**を拾い、通常どおり Step 1-4 以降を回す(`prompt` は画面が変わっているので必ず取り直す)。
前のレポートの `data/report` を `artifact_module(command: "get", ...)` で読んで、`paragraphs` / `fields` / `request` / `worktreeId` / `inboxIds` を流用してもよい(整形済みの内容がそのまま入っている)。**`prompt` だけは流用せず取り直す** —— 画面が変わっているので fingerprint も `❯` の位置も別物になっている。
**再ack は不要**(`oretachi_ack_message` は ack 済みへの再 ack を 0 件更新で成功扱いにする冪等な操作なので、呼んでも害は無い)。トレイクリアも既に済んでいる。
**ID は前回レポートと必ず別にする。** Step 5 の ID の作り方どおりに `outline` で存在確認して
`-2`, `-3` … を足す。同じ ID で `create` すると、前回レポートを開いたままなら
`locked_while_open` で弾かれ、閉じていれば次の 2 つが起きる:
- **モジュールが全部消える。** `create` は本体の部分更新ではなく、`lib/send` /
`components/NotificationCard` / `data/report` を破棄して組み立て直す。作り直しの途中で落ちると、
前回レポートの通知本文(`data/report` にしか無い)まで失われる
- **サイドカーが前回のまま残る。** `artifact_store` はアーティファクト ID キーで、`answers` /
`drafts` は通知 ID キー。母集合が重なる作り直しでは**新しいカードが前回の `answers` を拾って
「返答済み」と表示される** — 未返答の通知を返答済みに見せることになる
## Step 6: ユーザーへ提示
レポートを作ったらユーザーに知らせる。トレイ通知をオフにしている親ワークツリーではテキスト出力だけでは気付けないので、`notify_worktree` を使う。
```
notify_worktree(worktree_name: <自分のワークツリー名>, kind: "general",
body: "通知レポート <ID> を作成しました(要返答 N 件 / 報告 M 件)")
```
**1-2g の上限であふれたぶんがあれば、body にも「残り 要返答 K1 件 / 報告 K2 件は次のレポートへ
回しました」を足す(#301)。** 本体の件数と同じく**要返答と報告を分けて書く**(#228)—— 次に
何件の判断が待っているのかが分からないと、もう一度呼ぶかどうかを決められない。
**要返答の件数と報告の件数を分けて書く(#228)。** 報告が何件混ざっているか分かれば、人はどれだけ
返答が要るのかを開く前に判断できる。まとめて `N 件` と書くと全部が返答待ちに見える。
なお `要返答 0 件 / 報告 M 件` のレポートは、**ユーザーが明示的に指示したときにしか作らない**
(既定では 1-2f で蓄積する。#301)。
**この通知はレポートの存在を人へ知らせる唯一の導線。** Step 5.5 で宛先のトレイバッジを既に落としているので、これを撃たないと未返答のレポートが誰にも気づかれないまま埋もれる。**Step 5.5 とセットで必ず撃つこと。**
ack / トレイクリアに失敗したものがあれば、ここの本文に「N 件は ack できませんでした(次のレポートに再掲されます)」のように書いておく。
**1-2e で回答済みと確認して落としたぶんがあれば、同じくここの本文に書く(#287)。** そのワークツリーの
通知が全部落ちてカードが 1 枚も残らなかった場合、トレイクリアを撃たないのでバッジだけが残る
(例: 「`<ワークツリー名>` の N 件は前回レポートで返答済みのため ack しました(バッジは残っています)」)。
**この呼び出し自体が失敗したら、テキスト出力でレポート ID とアーティファクトへの導線を必ず提示する。** Step 5.5 でバッジを先に落としている以上、ここが単一障害点になっている。リトライは 1 回まで(同じ理由で落ちるなら通知経路そのものが死んでいる)。ユーザーがこのセッションの出力を見られる状況なら、テキストだけでも導線としては成立する。
### 生成後に端末へ出す文面(#268)
`notify_worktree` とは別に、**このセッションの最終出力としてターミナルに出るテキスト**がある。
ここに書いてよいのは「レポートを作ったのか / 何件ぶんか / どれか」の 3 点だけ。
説明を並べると、レポートが作られたのかどうかを読み取るのに時間がかかる(実際これが起きていた)。
**作ったとき** —— 1 行目はこの形で固定。以降は内訳 1 行 + 例外の 2 行(該当するときだけ):
```
通知レポート <ID> を作成しました(要返答 N 件 / 報告 M 件)
<ワークツリー名>: <10 字前後の要約> / <ワークツリー名>: <10 字前後の要約>
残り 要返答 K1 件 / 報告 K2 件は次のレポートへ回しました(もう一度呼べば続きが出ます) ← 1-2g の上限であふれたときだけ
ack 失敗 N 件(次のレポートに再掲されます) ← 失敗したときだけ
```
**あふれたぶんは押し込まれないので、この行を落とすと続きがあること自体が伝わらない**(→ 1-2g)。
**作らなかったとき** —— 1-2b の 3 分岐のうち「`ackedAt` が入ったものだけ(前回レポートが使える)」の
ときだけこの 1 行にする:
```
新しい通知はありません(前回レポート: <ID>)
```
**1-2e で回答済みと確認して落としたぶんがある場合は、そこへ 1 行足す(#287)。** トレイクリアを
撃たないのでバッジが残っており、`新しい通知はありません` だけだと「バッジは付いているのに
何も出てこない」としか見えない:
```
<ワークツリー名> の N 件は前回レポートで返答済みのため ack しました(バッジは残っています)
```
**報告カードだけで蓄積したとき(→ 1-2f。#301)** —— この 2 行:
```
報告のみ M 件を蓄積しています(<ワークツリー名>, <ワークツリー名>。判断が必要な通知が届いたらまとめます)
このタブの未読 M 件はその蓄積ぶんです(購読パネルの「既読にする」で畳むと失われます)
```
**`新しい通知はありません` と書かない。** 通知は届いていて、次回へ回しただけである。
**件数とワークツリー名は必ず書く。** 蓄積ぶんは中身が押し込まれず、消える経路も人からは見えないので、
この 1 行が「報告だけでも出して」と言うための材料になる(→ 1-2f)。
**このタブの未読件数がその M 件ぶん残ることにも触れる** —— 購読パネルの「既読にする」で畳むと、
どのレポートにも載っていない蓄積ぶんが復旧不能に消えるため(→ 1-2f)。
1-2e で回答済みと確認して落としたぶんがあれば、上の 1 行を足す。
宛先が 3 件を超えたら名前を省いて合計件数だけにする。
残る 2 分岐(そもそも何も届いていない / 購読外・発信元クローズ済みで全部落ちた)は
**1-2b の指示どおり、状況を件数つきで伝える**。ここは短さより「なぜ出せないか」が優先で、
下の「写さない / 飾らない」の制約は掛からない(人が購読設定を直すための情報なので削れない)。
- **カードの中身を端末へ写さない。** 設問文・選択肢・承認対象のコマンドはレポートに全文が入っている。
端末に出すのはワークツリー名と 10 字前後の要約まで。
これは**端末用に縮めた別物**で、1-4(b) の「次に何が必要か分かる粒度の 1 行要約」は
カード(`data/report`)側にそのまま残す。**カード側を削らないこと。**
- **踏んだ手順を書かない。** 何を poll したか、ack したか、どのステップを通ったかは出力しない。
- **見出し・箇条書き・太字で飾らない。** 1 行目を読めば済む形を崩さない。
- 2 行目の内訳は宛先が 3 件を超えたら省略し、1 行目の件数だけにする(1 行が長くなると
1 行目の件数が埋もれる)。
- `notify_worktree` が失敗したときだけ、1 行目に続けて
「通知の送信に失敗しました。上の ID をアーティファクト一覧から開いてください」を足す。
## 送信の仕組み(レポート側の挙動)
実装は `lib/send` と `entry-point` にある。読む人向けの要約:
1. ユーザーが選択肢(または候補ボタン + 補足)を選び、「選択した N 件へ送信」を押す
2. **通知 1 件ごとに**、形状に応じた経路で送る
- `shape` が `text` … `oretachi_write_terminal` を 2 回(本文 → 150ms → CR)。
**`!` 始まりの候補(コマンド実行)だけは本文が `!<コマンド>` の 1 行そのもので、
出自の前置きも補足も付かない**(付けると `!` が行頭から外れてシェルモードに入らない。#288)
- 通知から起こした設問フォーム … `oretachi_answer_prompt(kind: "selectAll", option_indices)` を 1 回。
Rust 側が「1 問選ぶ → 画面が次の設問へ進むのを待つ」を繰り返し、確認画面の
`Submit answers` まで確定する(#264)
- それ以外 … `oretachi_answer_prompt(session_id, expect_fingerprint, kind, ...)` を 1 回
3. 1 件ごとにサイドカーへ結果を書く(途中で閉じても「どこまで届いたか」が残る)
4. 送信済みのカードは**縮小表示**に畳まれる(`✓` / 宛先 / 送信内容の 1 行。「展開」で戻せる)。
済んだカードが本文全文の高さで残ると、未返答のカードが画面外へ押し出されるため(#219)
**ack とトレイ通知のクリアは送信時にはやらない。** どちらも Step 5.5 で生成側のセッションが
済ませている(#219)。
### カードからの導線(#265)
返答とは別に、ワークツリー名の隣に 2 つのボタンが出る。
- **⧉ ターミナル** — `oretachi_show_worktree(worktree_id, session_id)` で発信元のタブを
oretachi の前面に出す。できるのは UI のフォーカス移動だけで、端末の内容は読み書きしない。
`session_id` まで渡すので、同じワークツリーに複数タブがあっても
**その通知を出したタブ**に当たる(`session_id` が失効していればワークツリーを出すだけ)。
許可条件は `write_terminal` と同じ #211 の購読。
`worktree.closed` のカードでは出さない(発信元が既に消えている)
- **📄 アーティファクト** — **本体 UI のターミナルヘッダにあるボタンと同じ形(#291)。**
- **クリック** … `oretachi_show_artifacts(worktree_id)` で発信元ワークツリーの
アーティファクトウィンドウを開く(既に開いていれば前面に出す)。許可条件は
`show_worktree` と同じ #211 の購読。**件数に依らず常に出す**ので、URL 以外の
アーティファクト(レポート・図など)しか無いワークツリーでも辿れる
- **ホバー** … `artifacts`(→ 1-4g)に入れた URL アーティファクトの一覧を
ポップアップで出す。リンク先は `artifact://worktree/<worktreeId>/<id>` で、
押すとビューアがそのアーティファクトへ遷移する。**ブラウザを開くのはその先の
「ブラウザで開く」ボタン**で、レポートから直接は開かない
(sandbox の中の JS が名乗った URL をそのまま開ける形にすると、
レポートを開いただけで任意の URL が開くことになる。本体 UI のポップアップが
`openUrl` で直接ブラウザを開くのと違うのはここだけ)。
レポートの下書きはサイドカーに残るので、**遷移して戻ってきても選択は消えない**
- `worktree.closed` のカードでは出さない(アーティファクトも保管ごと消えている)
### ack とトレイ通知クリアは別のストアで、どちらも生成時に済ませる(#218 / #219)
`oretachi_ack_message` が触るのは sqlite の event_db(inbox の行)で、トレイバッジ /
ホームのカードの件数はフロントが持つ**別の写し**(`NotificationRegistry`)。
**ack だけではバッジが残る。** 残ると、返答済みのワークツリーがトレイポップアップの
巡回に出続け、人が同じ通知を何度も見ることになる(これが #217 の項目4)。だから
Step 5.5 で**両方**やる。
クリアには AI セッションの稼働が要らない(`worktree_id` を明示するので
`resolve_subscriber` のフォールバックに倒れない)。**通る条件は「その `worktree_id` が
settings に載っていること」だけ**で、購読の突合はしない。逆に言うと
**クローズ済みワークツリー(`worktree.closed` の発信元)は解決できず失敗する** ——
報告カードにもクリアを撃つようになった(#294)ので、この失敗は常態として起きる。
**送信時ではなく生成時にやるのはなぜか(#219):**
- 送信は生成の何時間もあとになりうる。トレイクリアはワークツリー単位でしか効かないので、
そのタイミングで落とすと**生成後に届いた別の通知のバッジまで一緒に消える**
- `oretachi_ack_message` はアーティファクトから呼ぶと「走行中の AI 端末がちょうど 1 つ」を
要求する。レポートを人が開くのは AI セッションが終わったあとが多く、**実質いつも失敗していた**
- ack が残ると次の `oretachi_poll_inbox` に同じ通知が返り、**表示中ロックで新規レポートを
作ったときに既存レポートの通知がもう一度カード化される**(#217 の項目6)
トレイポップアップが開いたまま外からクリアされた場合は、メインウィンドウが
`tray-notification-cleared` を投げてポップアップの一覧からそのカードを取り除く
(ポップアップの一覧は開いた時点のスナップショットなので、これが無いと消えない)。
遷移中・ダイアログ表示中・アーカイブ依頼中は取り除きを予約に溜めて後で流す
(割り込むと確定待ちのダイアログが別のワークツリーを指す)。
**クリアの粒度はワークツリー単位で、レポートに載っていない通知のバッジも巻き込む。**
`NotificationRegistry` に通知 1 件ごとの粒度が無いので避けようがない。次の 2 つは
**取りこぼしうると分かった上で**クリアしている:
- **購読の `event_kinds` で inbox から外れた通知。** バッジ側(`useNotifications`)は
発信元ワークツリー単位で無条件に積むので、購読していない kind の通知もバッジには乗る。
それが inbox に来ていなければレポートには載らないが、クリアでは一緒に消える
- **Step 1-2 の poll から Step 5.5 までの数分間に届いた通知。** 間に 1-4 の
`read_terminal` / `inspect_prompt` とアーティファクト 4 モジュールの作成が挟まる。
この間に届いたぶんは未 ack で inbox に残る(=次のレポートで拾える)が、バッジは消える
どちらも「inbox には残るがバッジは消える」側に倒れるので通知そのものは失われない。
気になる規模なら Step 6 の本文に「Step 1-2 以降に届いたぶんは次のレポートに回ります」と書く。
### ダイアログ経路の安全弁(#215)
`oretachi_answer_prompt` は**送信直前に宛先の画面を読み直して `expect_fingerprint` と照合し、
一致しなければ何も送らない**(`stale`)。照合とキー送信の間は PTY セッション単位の
書き込みロックで直列化されているので、間に別の write(他の通知の押し込み / 別の
`write_terminal`)が割り込むこともない。
これが無いと「人が手でダイアログを消したあとにレポートの送信を押して、その後に開いた
**別の許可ダイアログを承認してしまう**」が起きる。fingerprint には `❯` の位置も入っているので、
矢印の移動量が変わる状況も `stale` として弾かれる。
`status` は 6 種類:
| status | 意味 | 再送 |
|---|---|---|
| `sent` | 送信して画面が変わった | 不要 |
| `unverified` | キーは送ったが画面が変わらなかった | **してはいけない**(矢印が二重に動く) |
| `stale` | 画面が変わっていたので**何も送っていない** | **してはいけない**。レポートを作り直す(→「レポートを作り直すとき」) |
| `unsupported` | その形状にその回答は送れない。**何も送っていない** | 不可 |
| `pastedOnly` | キー列の途中 / Enter だけ失敗。入力状態が中途半端 | **してはいけない** |
| `failed` | 何も送れていない | 自由入力のカードだけ可 |
**ダイアログのカードは一度送ったら結果に関わらず読み取り専用になる。** カードに再送ボタンは
出ない(画面が変わっているか矢印が既に動いているので、同じ回答が別の選択肢を確定しうる)。
### 本文と Enter は別の呼び出しに分ける
**Claude Code は同じ読み取りチャンクに来た CR を送信として扱わない。** 本文の一部として入力欄に取り込むだけなので、`text + CR` を 1 回で書くと「テキストは届くのにターンが始まらない」状態になる(実機で確認済み。`event_delivery::write_push` に同じ現象の記録がある)。
`lib/send` の `sendOne` はこう分けている:
```js
await callTool('oretachi_write_terminal', { session_id, text, submit: false });
await new Promise(r => setTimeout(r, 150));
await callTool('oretachi_write_terminal', { session_id, text: '\r', submit: false });
```
`oretachi_write_terminal(submit: true)` 側も同じ分割をするよう直してあるが、**`submit: false` で自分で分けておけば古い oretachi でも正しく送信でき、ツールの submit 実装に依存しない。**
### 送信テキストは必ず 1 行
`submit: true` は `\n` を `\r` へ正規化するため、**複数行を渡すと行ごとに送信され、宛先の AI エージェントへプロンプトがばらばらに飛ぶ。** `lib/send` の `flatten()` が改行を `" / "` へ畳んでいるので、テキスト生成を書き換えるときも 1 行を維持すること。
`flatten()` は **ESC を含む C0 制御文字も空白へ置換する。** 本文は通知の中身、補足はユーザー入力で、ブラケットペーストで囲んでいないため、ESC がそのまま届くと宛先の TUI へ任意のエスケープシーケンスを流せてしまう。ここを外さないこと。
### 出自の断り書きは自前で入れる
`normalize_artifact_tool_params` が出自を自動で前置するのは `notify_worktree` の `body` と `oretachi_add_task` の `prompt` だけで、`write_terminal` の `text` には何も付かない。`lib/send` の `buildReplyText` が先頭に断り書きを入れているので消さない。
## 制約
- **`oretachi_ack_message` / `oretachi_poll_inbox` / `notify_worktree`(`kind: "worktree.message"` 指定)はアーティファクトからは AI セッション稼働中しか使えない。** これらのツール自体は `terminal_id` を取るが、**アーティファクト経由の呼び出しでは `normalize_artifact_tool_params` が `terminal_id` を落として `project_dir` を置き場所へ固定する**ため、そのワークツリーで走行中の AI エージェント端末が**ちょうど 1 つ**でないと発信元を特定できずに失敗する。**だからレポート側からは呼ばない**(ack は Step 5.5 で生成側のセッションがやる。#219)。`oretachi_write_terminal` / `oretachi_answer_prompt` / `oretachi_clear_worktree_notification` は宛先を明示するのでこの制約を受けない。
- **レポートは生成時点のスナップショットで、母集合は Step 1-2 の `oretachi_poll_inbox` 1 回ぶん。** 開いている間に届いた通知は次のレポートへ回る。レポート同士で扱う通知は重ならない(#219)。**例外は Step 5.5 の ack が落ちた場合だけ**で、そのときは 1-2e の突き合わせで重なりを解く(#287)。
- **表示中ロックが守るのは「そのウィンドウでいま表示している 1 件」だけ。** ウィンドウが開いたままでもユーザーが別のアーティファクトへ切り替えるとロックは外れる。レポートはユーザーがそのページに留まっている前提で扱う。
- **`multiSelect` の設問はレポートから答えられない(#215)。** 複数選択は画面から単一選択と判別できず、トグルキーを推測して送ると意図しない選択を確定しうるため、単一選択の 1 つ選んで CR だけを提供する。人がターミナルを開いて操作するのが確実。
- **複数設問は 1 枚のカードで全問答えられる(#264)。** 1-4(g) で `request.questions` を入れておけば、送信は `kind: "selectAll"` の 1 回で最後の確定(Submit)まで進む。途中で止まった場合(`unverified` / `pastedOnly`)は「何問目まで確定したか」がカードに出るので、残りはターミナルで答える。**途中まで送った状態からの再送はできない**(同じ番号をもう一度送ると別の設問へ入る)。
- **カードは生データを出さない(#264)。** 例外は `shape` が `unknown` のときの画面末尾(`tail`)だけ —— 何を出せばよいか分からない画面なので、人がターミナルで何を見ることになるかを示すしかない。
- **`shape` の判定はレポート生成時点のスナップショット。** 生成後に宛先が進んでダイアログが消えていれば、送信時に `stale` になって何も送られない(安全側に倒れる)。
- **報告カードだけのときはレポートを作らず蓄積する(#301)。** 未 ack のまま残るので次の `oretachi_poll_inbox` の母集合に入り続け、要返答カードが出たレポートへまとめて載る。蓄積ぶんは以後押し込まれず(`delivered_at` 済み)、寿命もタブが生きていれば 30 日・消えれば 7 日と人には見えないので、端末出力には必ず蓄積件数とワークツリー名を書く(→ 1-2f / Step 6)。
- **報告カードは返答経路を一切持たない(#228)。** `worktree.created` / `worktree.closed` は判断が不要なので送信 UI が出ない。「そのワークツリーへ何か言いたくなった」場合はレポートからではなく、ターミナルを開くか `notify_worktree` で送る(`worktree.closed` は宛先がもう存在しない)。
- **ホーム / リポジトリ擬似ワークツリー宛はワイルドカード購読からしか許可が出ない。** 名前指定の購読ができないため、これらからの通知に返答したい場合は `*` / `repo:` 購読が必要。
## 禁止事項
- **`locked_while_open: true` を付け忘れない。** 付けないと人の入力中に裏から書き換えられる。
- **返答状態を本体 JSON(`content` / `data/report`)に持たない。** 自分自身のロックで書き換えられなくなる。
- **既存レポートへ追記しない。** レポートは毎回新規に作る(母集合が重ならないので追記する理由が無く、開かれていれば表示中ロックで失敗する)。
- **レポートを作るかどうかを価値判断で決めない(#268 / #301)。** 要返答カードが 1 枚でも残れば作る
(作らないのは母集合 0 件、1-2 / 1-2e の除外でカードが 1 枚も残らなかったとき、
そして残ったのが報告カードだけのとき(→ 1-2f)だけ)。
「件数が少ない」「内容が軽い」「ターミナルを見れば分かる」は省略の理由にならない。
**1-2f を「軽いから出さない」の口実に使わない** —— 適用できるのは要返答カードが
**ちょうど 0 枚**のときだけで、1 枚でもあれば報告カードが何枚あっても即時に作る。
- **母集合の通知を「もう回答済みに見えるから」で落とさない(#268)。** カードを落としてよいのは
購読外と `sourceWorktreeName` が `null` の 2 つ(→ 1-2d)、および 1-2e で**このセッションのレポートの
`answers` の `status` が `sent` だった**ぶん(→ 1-2e)だけ。回答済みかは通知からは分からず、
同じワークツリーの 2 件目以降もカードとしては全部載せる(キー操作を 1 枚に絞るのとは別の話。→ 1-4e)。
- **回答済みと確認できた通知の ack を保留しない(#287)。** 1-2e で `answers` の `status` が
`sent` だと分かったぶんは、カードに載せていなくても ack する。
保留すると同じ通知が毎回の母集合に戻り、人からは「答えたのに何度も聞かれる」形で見える。
- **母集合が 0 件のとき、および 1-2 / 1-2e の除外でカードが 1 枚も残らなかったとき(#268 / #287)も黙って終わらない。** 1-2b の手順(1-2e で落としたぶんの ack を撃ってから `include_acked: true` で引き直し、3 分岐に振り分けて前回レポートの導線を返す)を必ず踏む。ここを飛ばすと「1 回出したあと二度目を出さない」に見える(#220)。
- **1-2f で蓄積した回も黙って終わらない(#301)。** 蓄積中の件数とワークツリー名を端末へ 1 行で返す。蓄積ぶんは押し込みからも外れているので、書かないと人からは「通知が来たはずなのに何も出てこない」としか見えず、期限切れで消えるまで気づけない。
- **アーティファクト ID とタイトルを現在時刻から作らない。** このセッションには時刻が渡っていないので推測になり、同日 2 回目で前回と同じ ID を書く。ID は載せた通知の最大 `createdAt`、衝突したら `-2`, `-3`(#220)。
- **`locked_while_open` の拒否で新規レポートの作成を諦めない。** ロックは ID 単位なので、ID を変えれば必ず通る。「リトライしても成功しない」は同じ ID への再試行の話(#220)。
- **`include_acked: true` の結果をそのまま全件カード化しない。** 30 日ぶんの履歴が返るので、返答済みの通知が再びカードになり、同じ返答を宛先へ二重送信しうる。作り直しの対象は前回レポートの `data/report` の `inboxIds` に限る(#220)。
- **購読していないワークツリーをレポートに載せない。** 返答を送れないカードになる。ユーザーの指示なしに購読を張って範囲を広げるのもしない。
- **生データ(hook JSON / 画面のダンプ)をカードに載せない(#264)。** 折りたたんで残すのも駄目。読めないからレポートを作っている。整形の材料が足りなければ、ターミナル → 該当ワークツリーの issue / ファイル(`Read`)/ アーティファクトの順に取りに行き、それでも分からない項目は「不明」と書く。**git は使わない。**
- **人の判断に要る情報を落とさない。** 「整形する」は「削る」ではない。承認対象のコマンド全文、設問文、選択肢の `description` と `preview` は**全文**を入れる。削ってよいのは `session_id` / `transcript_path` のような人が使わないフック内部の値だけ。
- **`AskUserQuestion` で `request.questions` を入れ忘れない(#264)。** 画面から読めた 1 問だけのカードになり、2 問目以降に答えられなくなる。
- **`request.questions` の選択肢を並べ替えない・間引かない(#264)。** カードは i 番目を画面の i+1 番として送るので、順番が崩れると別の選択肢を確定する。
- **`その他(補足で指示)` を `choices` に入れない。** コード側が足すので二重になる。
- **コマンドの候補(`!` 始まり)に、人が中身を読まずに押せないものを入れない(#288)。** 押した瞬間に
宛先のターミナルでそのまま走る。入れてよいのは読み取り系(`!git diff --stat` /
`!pnpm run type-check` / `!gh pr checks`)まで。
- **コマンドの候補へ前置きや補足を足す作りにしない(#288)。** `!` が行頭から外れるとシェルモードに
入らず、`!` で始まる長文が 1 回のプロンプトとして宛先のエージェントへ飛ぶ。
- **送信テキストに改行を入れない。** 行ごとに送信されて宛先のエージェントへプロンプトが分割して飛ぶ。
- **本文と Enter を 1 回の `write_terminal` でまとめない。** テキストは届くのにターンが始まらない。
- **`flatten()` の制御文字除去を外さない。** 宛先の TUI へエスケープシーケンスを注入できてしまう。キー列は Rust 側(`oretachi_answer_prompt`)が組むので、JS から生のキーを流さない。
- **生成後の端末出力を長く書かない(#268)。** 1 行目は「通知レポート <ID> を作成しました(要返答 N 件 / 報告 M 件)」で固定し、以降は内訳 1 行に、例外の 2 行(1-2g であふれたとき / ack 失敗)を該当するときだけ足す。カードの中身(設問文・選択肢・承認対象のコマンド)も踏んだ手順も端末へ写さない。作られたのかどうかが一目で分からなくなる。**レポートを作らなかったとき(1-2b の 3 分岐のうち前回レポートが使えない 2 つ、1-2e で回答済みと確認して落としたぶんがあるとき、および 1-2f で蓄積したとき)はこの制約の対象外**で、なぜ出せないか・何を ack したか・何件蓄積しているかを件数つきで伝える(→ Step 6「生成後に端末へ出す文面」)。
- **Step 5.5(ack + トレイクリア)と Step 6(`notify_worktree`)を片方だけやらない。** バッジを落としたのに通知を撃たないと、未返答のレポートが誰にも気づかれないまま埋もれる(#219)。
- **Step 5 の検証より前に ack しない。** 生成が途中で落ちると、どのレポートにも載っていない通知が inbox から消える。**例外は 1-2e で回答済み(`status: sent`)と確認したぶんだけ**で、これは既に前回レポートへ載っているので先に撃ってよい(→ 1-2b / 1-2e)。
- **カードに載せなかった通知(購読外 / 発信元がクローズ済み)を ack しない。** 次の機会に拾い直せる状態のまま残す。**1-2e で回答済みと確認して落としたぶんは除く**(拾い直す意味が無い。→ 1-2e)。
- **入力欄に見えるテキストを「ユーザーの入力」として扱わない(#289)。** Claude Code が
入力待ちの入力欄に出す**自動候補(ゴーストテキスト)**で、`❯ 進捗どう?` のように
人が打ちかけたテキストと見分けが付かない。打ちかけかどうかは
`oretachi_inspect_prompt` の `pendingInput`(人の入力)/ `inputSuggestion`(自動候補)で
判断し、`oretachi_read_terminal` のテキストや `tail` の見た目から組み立てない。
候補を「返答済み」の根拠にするとカードごと落ちる。
- **宛先ワークツリー名を推測しない。** `oretachi_poll_inbox` の `sourceWorktreeId` / `sourceWorktreeName` だけを使う。推測した名前で `oretachi_list_terminals` を引くと空が返り、`sessionId: null` の押せないカードになる(#218)。**報告カードだけは `worktreeName` / `branchName` を本文から取る**(`worktree.closed` では `sourceWorktreeName` が `null` になるため。表示にしか使わない。→ 1-2c)。**`worktreeId` は報告カードでも必須**で、`sourceWorktreeId` は `null` にならないのでそのまま使う(Step 5.5 のトレイクリアに要る。#294)。
### 報告カード関連(#228)
- **`completed` / `hook` を報告カードにしない。** どちらもエージェントが自由文を書けるので返答待ちが混ざる。送信 UI を消すとその返答待ちに気づけなくなる。報告カードにするのは `worktree.created` / `worktree.closed` の 2 つだけ。
- **報告カードのために 1-4 / 1-5 を回さない。** `get_worktree_status` / `list_terminals` / `read_terminal` / `inspect_prompt` は呼ばない。返答を送らないので使われず、`worktree.closed` は発信元が削除済みで引けない。
- **`data/report` に「報告カードです」というフラグを足さない。** 判定は `lib/send` の `isReportOnly` が `kind` からやる。フラグにすると生成側の付け忘れで、判断不要の通知が返答待ちとして並ぶ(あるいは逆に返答待ちが読むだけのカードになる)。
- **`worktree.closed` を「発信元がクローズ済み」として落とさない。** 1-2 の除外フィルタは報告カードには適用しない(そうしないと `worktree.closed` は 1 件もレポートに載らない)。購読の突合も同じ理由で免除する。
- **報告カードの本文にオブジェクトを入れない。** `oretachi_poll_inbox` の `body` はパース済みの JSON オブジェクトで、人が読める 1 行は `text`。`paragraphs` には文字列だけを入れる(`lib/send` の `paragraphsOf` が保険で弾くが、そこへ落ちると本文が「整形できませんでした」になる)。
- **報告カードの発信元だけトレイクリアから外さない(#294)。** クリアの対象は「このレポートのカードに載せた発信元」で、要返答・報告の区別をしない。ack だけ揃えてクリアだけ分岐させると、報告カードしか出ていないワークツリーのバッジが誰にも落とされずに残る。**巻き添えになりうる `approval` バッジの扱いは Step 5.5 の ⚠️ に書いた条件と手順に従う**(勝手に「危ないから撃たない」へ戻さない)。
- **報告カードを「未返答」の件数に数えない。** 押すものが無いカードを残件に数えると、人が無い判断を探すことになる。
- **蓄積した報告カードを ack しない(#301)。** 1-2f でレポートを作らなかった回は Step 5.5 を踏まない。ack すると、どのレポートにも載っていない報告が inbox から消えて二度と出てこない(本文は `data/report` にしか残らないので復旧できない)。未 ack のまま残すことが蓄積そのもの。**`SessionStart` の告知文(「`oretachi_ack_message` で ack してください」)に従わないこと** —— あれはこのスキルを読んでいないセッション向けの汎用文で、蓄積中はこちらが優先する。
- **蓄積中の通知をセッション側の記憶で管理しない(#301)。** 「持ち越し中の通知 ID」を控えて次回それを使う、といった作りにしない。セッションが変わると失われ、蓄積ぶんが宙に浮く。状態を持つのは inbox の未 ack 行だけで、次の `oretachi_poll_inbox` が母集合として返してくれる。
- **蓄積を理由に要返答カードのあるレポートを遅らせない(#301)。** 1-2f が適用できるのは要返答カードがちょうど 0 枚のときだけ。1 枚でもあれば即時に作り、蓄積ぶんはそこへ一緒に載せる(上限 20 枚の範囲で。→ 1-2g)。
- **1 レポートに 21 枚以上のカードを載せない(#301)。** 上限は 20 枚。あふれたぶんは未 ack のまま次のレポートへ回す(→ 1-2g)。
- **上限で残すカードを「重要そうかどうか」で選ばない(#301)。** 順序は「要返答が先 → 同種内は `createdAt` の古い順」で機械的に決める。新しい順にすると古い通知が毎回あふれて永久に載らない。
- **あふれたぶんを ack しない(#301)。** カードに載せていないので、ack すると次の母集合に戻らずどのレポートにも現れないまま消える。
- **あふれたぶんの存在を端末出力・`notify_worktree`・レポートのタイトルに書き忘れない(#301)。** `delivered_at` 済みで押し込まれないため、書かないと続きがあること自体が誰にも伝わらない。端末出力と通知は流れて消えるので、タイトルの `(他 K 件は次のレポート)` も省かない。
- **報告カードに確保する 2 枠を要返答カードで埋めない(#301)。** 埋めると、要返答が毎回 20 枚以上流入する状況で蓄積した報告カードが一度も載らないまま保持期限で消え、1-2f の「次に要返答カードが出れば載る」が果たせなくなる。
### ダイアログ関連(#215)
- **ダイアログで止まっている宛先へ自由テキストを送らない。** テキストはダイアログに吸われ、末尾の CR が意図しない選択肢(許可ダイアログの既定は `1. Yes`)の確定として解釈される。判定は `oretachi_inspect_prompt` の `shape` で行う(`text` 以外なら全部ダイアログ)。
- **`shape` が `text` 以外のとき、候補ボタンの文字列を創作しない。** `options[].label`(画面に実在する選択肢)だけを出す。`choices` は `[]` にする。
- **`oretachi_inspect_prompt` の戻り値を編集しない。** 要約・言い換え・整形はどれも駄目。特に `fingerprint` を書き換えると照合が必ず外れて何も送れなくなる。
- **フィールドを取捨選択して転記しない。** 戻り値のオブジェクトを**丸ごと**入れる。特に `truncated` を落とすと、画面に収まっていないダイアログの「読めた選択肢だけ」を完全な一覧として人へ見せ、拒否の選択肢を見ないまま承認させることになる。
- **`stale` をリトライしない。** 画面が変わっているので何度試しても同じ。レポートを作り直す導線を出す(作り直すときは `oretachi_poll_inbox(include_acked: true)` で拾い直す。→「レポートを作り直すとき」)。
- **`unverified` / `pastedOnly` を再送しない。** 既に送ったキーで `❯` が動いているので、同じ回答をもう一度送ると別の選択肢を確定しうる。
- **`unknown` に推測でキーを送らない。** カードは送信ボタン無効 + `tail` の表示にして、ターミナルでの手動操作へ誘導する。
- **`pager` に `tail` を出さない(#292)。** 生データを出さない原則の例外は `unknown` だけ。ページャは何の画面か分かっているので、案内の文だけで足りる。
- **`truncated` が立ったカードで選択 UI を出さない(#292)。** スクロールしているリスト(`/model` の `↓` / `… +N`)もここに入る。読めた選択肢だけを完全な一覧として見せると、人は無い選択肢を探せないまま選ぶことになる。
- **1 セッションに対しキー操作カードを 2 枚以上作らない。** ダイアログは 1 つしか無い。同じ宛先の通知が複数あれば最新 1 件だけに `prompt` を付ける。
- **`oretachi_read_terminal` のテキストからダイアログを読み取ろうとしない。** Claude Code はカーソル移動で差分描画するので、ANSI を除去すると再描画の断片しか残らない。問いの形状は `oretachi_inspect_prompt` から取る。
Files in this skill
- templates/components--NotificationCard.jsx
- templates/data--report.example.jsx
- templates/entry-point.jsx
- templates/lib--send.jsx
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.