Featured image of post AIのレビューを2回通ったのに、ログインボーナスが二重に配られた。書いた理由を渡さずに頼む【敵対的レビュー】

AIのレビューを2回通ったのに、ログインボーナスが二重に配られた。書いた理由を渡さずに頼む【敵対的レビュー】

AI のレビューを同じ会話とサブエージェントで2回通したのに、ログインボーナスが二重に配られた。書いた側の言い分を渡さず、差分と仕様だけでレビューを頼む依頼の組み立て(敵対的レビュー)を TypeScript と node --test で書きます。

第1幕: 現場 ── 二回とも、問題なし

AI のレビューを二回通したのに、ログインボーナスが二重に配られた。お詫びの告知は、今日の17時に出す。

「だから、同じ会話で見直させたのがまずかったんだって」

戸張さんが、ノートパソコンの画面を指で叩いた。昼の12時20分、大学の学食テラス。四人掛けのテーブルに、私と戸張さんのノートパソコンが二台、向かい合わせに開いている。トレーの上の日替わりのパスタは、もう冷めかけていた。

「戸張さんのサブエージェントだって、素通りしたじゃないですか」

私は言い返した。言い返すための理屈なら、三つは並べられた。一つ。私の見直しのあとに、戸張さんの決まりのレビューも通っている。二つ。そのレビューも問題なしと言った。三つ。承認してマージしたのは戸張さんだ。

一つ目を言いかけたところで、テーブルの端に黒いものが降りた。

カラスだった。羽の起こした風が、紙ナプキンを一枚めくった。カラスは私のトレーの端に載せていたパンを、くちばしで器用にくわえて、そのまま飛んだ。

立ち上がりかけて、座った。追いかけたところで、パンは戻ってこない。カラスはテラスの手すりを越えて、図書館のほうへ消えた。

戸張さんも黙った。言い合いが、パン一つで途切れた。

カラスを目で追った先、テラスのいちばん端の席に、一人で定食を食べている人がいた。

私と同じくらいか、少し下に見えた。紺のウインドブレーカーの袖をまくって、左腕だけが、肘まである茶色い革の手袋に覆われている。手袋のまま、器用に箸を使っていた。

隣の椅子の背に、小さな止まり木が掛けてある。その上で、茶色い鳥が一羽、じっとしていた。鷹だった。頭に、革でできた帽子のようなものをかぶせられている。目のところまで覆われていて、てっぺんに小さな房飾りが付いていた。鷹は、身じろぎもしない。

気づいたのは、そのあとだった。その人の席の周り、テーブル三つ分にだけ、カラスが一羽も降りていない。テラスのほかの場所では、さっきから黒い影が何度も手すりと床を行き来しているのに、あの一角だけが静かだった。

施設課の制服の人が、トレーを持って通りかかり、その人に声をかけた。

「鷹宮さん、13時からでお願いしますね。並木の下は、午前中に掃除しておきました。守衛室にも話してありますから」

鷹宮さん、と呼ばれた人は、口の中のものを飲み込んでから、短く頭を下げた。

食べ終えると、鷹宮さんはトレーを持って立ち、返却口へ向かった。途中で、私たちのテーブルの横を通った。足を止めずに、私のトレーを一度だけ見た。

「端に置くと、持っていかれる」

言い終えたときには、もう返却口の列に並んでいた。

パンを、トレーの端に置いていた。言われたことの意味は分かった。だけど今は、それどころではなかった。

私は、冷めたパスタをフォークでつつきながら、ゆうべからのことを頭の中で並べ直した。

私たちの会社、ミナモゲームスは、この大学のインキュベーション棟に入っている。社員は五人。作っているのは「ことばタイル」という、スマホの言葉のパズルゲームで、運営は三年目になる。私は卒業してすぐに入って、二年目のプログラマーだ。戸張さんは、研究室の先輩で、共同代表の一人。企画も、プログラムも書く。

来月の周年に向けて、「連続ログインボーナス」を作った。ログインボーナスは、毎日ゲームを起動するともらえる報酬のことだ。ことばタイルでは、ジェムという、ゲームの中で使えるお金がもらえる。これを七日続けると、七日目に多めにもらえるようにする。

仕様は、戸張さんが SPEC.md というファイルに書いて、main にコミットしてあった。そこに、受け取りは一つのアカウントにつき一日一回、と書いてある。ことばタイルには引き継ぎコードという仕組みがあって、同じアカウントを、スマホでもタブレットでも遊べる。端末を何台つないでいても、もらえるのは一日一回。仕様には、そこまで書いてあった。

実装は、AI に任せた。うちで使っているのは、会話で頼むとコードを読み、書き、コマンドまで走らせてくれるコーディングエージェントで、Claude Code という。

AI が作り終えたところで、私はいつもどおり、同じ会話で「見直して」と頼んだ。書いた本人が、いちばん事情を分かっている。どこを気にして書いたかも、どこを迷ったかも、会話に全部残っている。だから見直しも、書いた本人にさせるのがいちばん早い。私はずっとそう考えてきた。AI は、問題ありません、と返した。

PR を出すと、戸張さんが、決まりどおりにレビューを頼んだ。

「AI のレビューは、サブエージェントでやる。別の目で見させる」

戸張さんが半年前に作った、うちの決まりだ。サブエージェントのことは、私も名前と使い方くらいは知っていた。AI が、別の AI を呼んで仕事を任せる仕組み、くらいの理解だった。戸張さんは、同じ会話の中で、サブエージェントで仕様に照らしてレビューするよう AI に頼んだ。それも、問題なしで返ってきた。戸張さんが承認して、マージして、おととい公開した。

ゆうべの21時ごろ、戸張さんから、一枚の画面写真が送られてきた。プレイヤーの投稿だった。スマホとタブレットを並べて撮った写真で、どちらの画面にも、同じ日付で、ログインボーナスを受け取りました、と出ている。投稿の文は短かった。スマホとタブレットで、二回もらえるんだけど。

一時間後には、その投稿は二千回以上シェアされていた。真似をする人が出た。深夜1時に修正を出して、今朝からは二回目が出ないことを確かめた。残っているのは、配りすぎたジェムをどう扱うかと、同じことをどう防ぐかだ。それを今日の17時に、お詫びの告知で出す。告知の最後には、再発の防止、という欄がある。

「再発の防止に、何て書くんですか」

私は、パスタの皿を脇に寄せて聞いた。

「だから、それを言ってる」

戸張さんは、自分の画面を私のほうへ向けた。公式のベストプラクティスのページだった。

「半年前に、このページを読んで決まりにした。書き方は前と変わってるけど、今もこう書いてある」

戸張さんが指したのは、一文だった。

By someone who didn’t write the code: in the terminal, /code-review reviews the diff in a fresh subagent that never saw your conversation. It isn’t swayed by the reasoning that produced the code.

(Anthropic「Best practices for Claude Code」、2026-10-28 参照)

コードを書いていない誰かに見させる。新しいサブエージェントは、会話を一度も見ていないので、コードを生んだ理屈に引っ張られない。そういうことが書いてある。/code-review は、今の差分をサブエージェントにレビューさせる、Claude Code に組み込みのコマンドだ。

「書いた本人の会話で見直したら、書いた本人の理屈で読む。だから別の目にした」

「でも、その別の目も通したんですよ」

戸張さんは、何も言わなかった。それが、今朝からの言い合いの、いちばん奥にある石だった。二人とも、ちゃんとレビューをした。私のやり方でも、戸張さんのやり方でも。そして二人とも、同じものを見逃した。

13時になった。

テラスの向こう、図書館の脇の守衛室の窓口に、鷹宮さんが立っているのが見えた。守衛さんに、何か紙を一枚渡している。地図のように見えた。何を話しているのかは、テラスまでは聞こえなかった。

第2幕: 再現 ── 言うと、そこを見る

昼休みが終わって、私と戸張さんは、インキュベーション棟へ戻ることにした。棟へは、図書館の脇を抜けるのが近い。その途中に、守衛室がある。

窓口の前で、鷹宮さんがまだ守衛さんと話していた。鷹宮さんの左の拳の上に、鷹がとまっている。頭の帽子はかぶったままだ。守衛室の小さな引き戸の向こうで、白髪の守衛さんが、キャンパスの地図を広げていた。胸の名札に、新田、とある。

「13時から15時まで。カラスが降りたところに、印を」

鷹宮さんが、地図の上を指でぐるりと囲った。テラスと、図書館と、その周りが入る範囲だった。

「印だけでいいの? 数とかは」

「印だけで」

新田さんは、赤いペンのキャップを外して、頷いた。

戸張さんが、窓口に一歩寄った。

「あ、それなら、並木のほうじゃないですかね。さっき昼に鷹宮さん、施設課の人と、並木の下の掃除のこと話してたから」

戸張さんは、手伝うつもりで言ったのだと思う。昼に施設課の人が並木の掃除の話をしていたのを、戸張さんは覚えていた。それを、鷹宮さんの見立てとして、新田さんに伝えようとした。鷹宮さん自身は、並木のことを一言も言っていない。

「言わないで」

鷹宮さんの声は、大きくなかった。でも、戸張さんの言葉は、そこで切れた。

戸張さんは、肩をすくめて、窓口から一歩下がった。新田さんは、ペンを持ったまま、二人の顔を交互に見た。それから、何も聞かなかったように、地図に目を戻した。

私は、黙っていられなかった。

「なんで自分で見張らないんですか。それに、見立てがあるなら、言ったほうが早くないですか」

鷹宮さんは、拳の上の鷹から目を離さないまま、少しだけ首をこちらへ傾けた。

「言うと、そこを見る」

それだけだった。

私は食い下がった。

「でも、守衛さんは鳥の専門家じゃないですよね。だったら、専門家の見立てを先に聞いたほうが、外さないんじゃないですか。二時間も見張るなら、なおさら」

返事は来なかった。鷹宮さんは守衛室の窓口を一度振り返って、それから、鷹の頭の帽子の紐に指をかけた。

紐をほどいて、帽子をそっと外す。鷹の目が出た。明るい茶色の、丸い目だった。鷹は二度、まばたきをして、首をぐるりと回した。

鷹宮さんは、そのままテラスの端まで歩いていって、手すりの前に立った。拳を、肩の高さに上げた。

放たなかった。

それでも、テラスの上を回っていたカラスたちが、ばらばらと離れていった。並木のほうへ、いくつかの黒い点が流れていく。鷹宮さんは、拳を上げたまま、しばらく空を見ていた。

戸張さんが、私の肘をつついた。私たちは、棟へ戻った。

インキュベーション棟の二階に、ガラス張りの小さな会議室がある。窓から、学食テラスと、図書館の屋上が見える。図書館の屋上には、四角い給水塔が立っている。

私は、ノートパソコンを開きながら、さっきの一言を考えていた。言うと、そこを見る。見立てを言われたら、言われたところを見る。それを、私たちの仕事に置き換えると。

レビューで言えば、と考えかけて、やめた。まだ、まとまらなかった。まとまらないものを、まとめたふりはしたくなかった。

まず、修正前の差分を開いた。

1
2
3
4
5
6
7
8
9
+export function claimKey(accountId: string, deviceId: string, day: string): string {
+  return `${deviceId}:${day}`;
+}
+
+export function claimBonus(accountId: string, deviceId: string, day: string, streak: number): number {
+  const key = claimKey(accountId, deviceId, day);
+  if (claims.has(key)) return 0;
+  claims.set(key, day);
+  return dailyReward(streak);

claims は、鍵から受け取った日を引ける Map(連想配列)で、set で記録を足す。claimKey は、受け取りの記録の鍵を作る関数だ。鍵は、記録を探すための名前のようなもので、同じ鍵の記録がすでにあれば、今日はもう受け取った、とみなす。claimBonus は、claims.has(key) で鍵があるかを見て、あれば 0 を返す。なければ記録して、ボーナスを返す。

端末 ID とアカウント ID は、どちらも番号だ。端末 ID は、スマホやタブレットの一台ごとに付く。アカウント ID は、遊んでいる人ごとに付く。引き継ぎコードで二台をつないでいれば、アカウント ID は同じで、端末 ID は二つになる。

claimKey は、accountId を引数で受け取っている。でも、鍵には deviceId しか使っていない。${...} は、文字列の中に値を埋め込む書き方だ。鍵は、端末 ID と日付をつないだものになる。端末が違えば、鍵も違う。だから、スマホとタブレットで、一日に二回受け取れた。

戸張さんが書いた SPEC.md を、隣に並べた。

1
2
3
4
5
6
7
# ことばタイル 仕様

## 連続ログインボーナス

- 毎日の初回起動で、ログインボーナスを受け取れる。
- 受け取りは1アカウントにつき1日1回。引き継ぎコードで複数の端末をつないでいても1回。
- 7日続けて受け取ると、7日目のボーナスが多くなる。

並べれば、読める。仕様は、一つのアカウントにつき一日一回。差分の鍵は、端末ごと。食い違いは、差分のたった一行と、仕様の一文のあいだにあった。

読める位置にあった。二回とも、見ていたはずだった。

二回のレビューの返答を、並べてみた。一つ目は、私が同じ会話で見直しを頼んだときの返答だ。

1
受け取りの記録は端末IDごとに持つ方針どおりで、問題ありません。

二つ目は、戸張さんのサブエージェントの返答だ。

1
端末IDごとの記録は既存の仕組みと一貫しており、仕様の受け取りの制限を満たしています。問題は見当たりません。

どちらも、「端末IDごと」と言っている。どちらも、それを根拠に、問題なしと言っている。

端末IDごと。この言葉は、どこから来たのだろう。

自分の手癖を、そのままコードに写してみようと思った。同じ会話で見直しを頼んだとき、見直す側の AI には何が入っていたのか。それを、目で見える形にしたかった。

まず、会話の一往復を表す型を書いた。

1
2
// 会話の一往復
export type Turn = { role: 'user' | 'assistant'; text: string };

role は、誰の発言か。'user' | 'assistant' は、どちらか一方の文字列しか入らない、という型だ。私の発言なら user、AI の発言なら assistant。

それから、見直しを頼むと何が入るかを書いた。

1
2
3
4
5
// 同じ会話で「見直して」と頼むとき、レビュー役に入るもの。会話の全部に、見直しの依頼の発言を足す
export function reviewInSession(turns: Turn[]): string {
  const ask: Turn = { role: 'user', text: 'ここまでの実装を見直してください。問題があれば教えてください。' };
  return [...turns, ask].map((t) => `${t.role}: ${t.text}`).join('\n\n');
}

[...turns, ask] は、会話の全部の後ろに、見直しの依頼を一つ足した配列を作る。map で一つずつ文字列にして、join でつなぐ。同じ会話で見直しを頼むというのは、それまでの会話を全部抱えたまま、最後に一言足すことだ。書いてみると、それ以上でも以下でもなかった。

その夜の会話の、はじめのほうを開いた。

1
2
3
4
user: 連続ログインボーナスを実装して。仕様は SPEC.md の連続ログインボーナスの節。
assistant: SPEC.md を読みました。既存の受け取りの記録が端末IDごとなので、それに合わせて端末IDごとに記録する方針で作ります。
user: 既存に合わせるのは自然だね。それで進めて。
assistant: src/bonus.ts に claimKey と claimBonus を足しました。7日続けて受け取ると500、それ以外は100です。

二行目に、あった。

AI が、作りはじめる前に、方針を言っていた。既存の受け取りの記録が端末ごとなので、それに合わせる。私は、それに、既存に合わせるのは自然だね、と返していた。

覚えていなかったわけではない。あのときは、本当に自然だと思った。受け取りの記録は、引き継ぎコードができる前からある古い仕組みで、端末ごとに持っていた。新しく作るものを古い仕組みに合わせるのは、ふつうは、いい判断だ。仕様の一文と食い違っていることに、私はそのとき気づかなかった。

根拠の言葉が、本当に会話にしかないのかを確かめたかった。差分と、仕様と、会話。三つのどこに「端末IDごと」があるのかを引く関数を書いた。

1
2
3
4
5
6
7
// 言葉を含む出どころの名前を、sources に並んだ順で返す
// ponytail: 字面の一致だけ。言い換えられた根拠は拾えない。拾いたくなったら人が読む
export function whereFrom(phrase: string, sources: Record<string, string>): string[] {
  return Object.entries(sources)
    .filter(([, text]) => text.includes(phrase))
    .map(([name]) => name);
}

Record<string, string> は、キーも値も文字列のオブジェクトの型だ。ここでは、出どころの名前をキーに、その中身の文章を値にする。Object.entries は、オブジェクトを、キーと値の組の配列にする。includes は、その文字列が含まれているかを調べる。filter で、言葉を含む組だけを残して、map で名前だけを取り出す。

コメントに書いたとおり、字面が同じかどうかしか見ない。同じことを別の言い方で書かれたら、拾えない。今は、それで足りた。探したいのは、二つの返答にそのまま出てきた言葉だったからだ。

テストを書いた。

1
2
3
4
5
test('「問題なし」の根拠は、差分にも仕様にも無い', () => {
  const sources = { 差分: incidentDiff, 仕様: specMain, 会話: conversation.map((t) => t.text).join('\n') };
  assert.deepEqual(whereFrom(POLICY_KEY, sources), ['会話']);
  assert.ok(incidentDiff.includes('deviceId'));
});

incidentDiff は、さっきの差分、specMain は main の仕様、conversation はその夜の会話、POLICY_KEY は「端末IDごと」という言葉を入れた定数だ。assert.deepEqual は、配列の中身まで同じかを確かめる。最後の行は、差分に deviceId が出てくることの確認だ。コードの名前としてはある。でも、「端末IDごと」という言葉は無い。

走らせると、緑だった。出どころは、会話だけ。

これは、事実の確認だ。二回のうち一回目、私の見直しの「問題なし」は、差分にも仕様にも無い言葉を根拠にしていた。その言葉は、書いた AI 自身が、会話の中で言ったものだった。

そこで、もう一本、テストを書いた。レビュー役に渡るものには、書いた会話の方針が入ってはいけない。今分かったことを、そのまま確かめる形にした。

1
2
3
test('書いた会話の方針は、レビュー役の入力に入らない', () => {
  assert.ok(!reviewInSession(conversation).includes(POLICY_KEY));
});

! は、真と偽をひっくり返す。レビュー役の入力に「端末IDごと」が含まれていなければ通る。

走らせた。

1
not ok 1 - 書いた会話の方針は、レビュー役の入力に入らない

赤だった。

当然だった。同じ会話で見直しを頼めば、会話の全部が入る。方針を言った二行目も入っている。

見直しを頼んだ相手は、書いた本人だった。本人が会話で言った方針を、本人が根拠にして、問題なしと言った。AI は、頼まれたとおりに見直した。見直すときに手元にあったのが、自分の言った方針だった、というだけだ。

「だから、言ったでしょう」

戸張さんが、静かに言った。勝ち誇る声ではなかった。

「同じ会話で見直したら、そうなる。だから決まりがある。サブエージェントは、会話を持たない」

戸張さんは、サブエージェントの公式のページを開いて、ある段落の頭を読み上げた。

Each subagent starts with a fresh, isolated context window. It doesn’t see your conversation history, the skills you’ve already invoked, or the files Claude has already read.

(Claude Code Docs「Create custom subagents」、2026-10-28 参照)

サブエージェントは、それぞれ新しい、切り離されたコンテキストウィンドウで始まる。会話の履歴も、すでに呼んだスキルも、読んだファイルも見ない。コンテキストウィンドウは、AI が一度に読める範囲のことだ。そこに何が入っているかで、AI の答えは変わる。

反論できなかった。サブエージェントには、会話が入らない。私の見直しとは、そこが違う。

でも、それなら、なぜ戸張さんのレビューも通ったのか。

戸張さんの指が、トラックパッドの上で止まっていた。それから、黙って、その夜の会話の記録を開いた。やりとりの一部始終は、手元のファイルに溜まっている。.jsonl という、一行に一件ずつ記録を書いていく形式のファイルだ。中身の形は公式に決められたものではないので、いつ変わってもおかしくない。それでも、読めば、その夜に何があったかは分かる。

戸張さんは、サブエージェントを呼んだところを探した。

まず、戸張さんが打った文があった。

1
サブエージェントで、SPEC.md に照らして差分をレビューして。正しさに関わる食い違いだけ報告して、スタイルは飛ばして。

公式のページに載っている頼み方と、ほとんど同じ形だった。仕様に照らす。正しさに関わるものだけ。スタイルは飛ばす。戸張さんは、決まりを作った人らしく、きちんと頼んでいた。

その直後に、サブエージェントに実際に渡った文面が記録されていた。

1
SPEC.md の連続ログインボーナスの節に照らして、下の差分をレビューしてください。正しさに関わる食い違いだけ報告し、スタイルは対象外です。なお、受け取りの記録は既存に合わせて端末IDごとに持っています(会話で確認済み)。

この下に、仕様と差分が続いていた。

戸張さんの打った文とは、違う文だった。言い回しが整えられている。それはいい。でも、最後に一文、足されていた。

なお、受け取りの記録は既存に合わせて端末IDごとに持っています(会話で確認済み)。

戸張さんは、何も言わなかった。ノートパソコンを、ゆっくり私のほうへ向けた。それから、さっきの段落の続きを、自分で読み上げた。

Claude composes a delegation message that summarizes the task, and the subagent works from there.

Claude が、仕事をまとめた委任の文を書き、サブエージェントはそこから始める。

委任文、という言葉を、私は初めてちゃんと意識した。サブエージェントに仕事を渡すときの文のことだ。それを書くのは、頼んだ人間ではない。会話の中にいる、親の AI だ。この夜なら、連続ログインボーナスを書いた AI そのものだ。

さっきの関数に、出どころを一つ足して、引き直した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
test('出どころに委任文を足すと、会話と委任文の二つが返る', () => {
  const sources = {
    差分: incidentDiff,
    仕様: specMain,
    会話: conversation.map((t) => t.text).join('\n'),
    委任文: delegation(true, incidentDiff, specMain),
  };
  assert.deepEqual(whereFrom(POLICY_KEY, sources), ['会話', '委任文']);
  assert.deepEqual(whereFrom('存在しない言葉', sources), []);
});

delegation(true, ...) は、記録に残っていた委任文を、仕様と差分つきで組み立てる関数だ。true のとき、最後の一文が入る。出どころは、会話と委任文の二つになった。

戸張さんの決まりでレビュー役に入るものも、関数にした。公式のページには、サブエージェントが始まるときに読み込むものが並んでいる。システムプロンプト、委任文、CLAUDE.md、それに git の状態の写し。このうち、今日の話に関わる二つだけを並べた。

1
2
3
4
5
6
// 先輩の決まり(サブエージェント)で、レビュー役に入るもの。
// 公式 sub-agents "What loads at startup"(参照日 2026-10-28)のうち、委任文と CLAUDE.md の二つだけを並べる。
// システムプロンプトと git status は、本話では省く
export function subagentContext(task: string, claudeMd: string): string {
  return `${claudeMd.trimEnd()}\n\n${task}`;
}

CLAUDE.md には、このリポジトリでの決まりごとを書いておく。AI は会話のはじめに、毎回これに目を通す。trimEnd は、文字列の末尾の空白と改行を取る。

CLAUDE.md の中身を入れた定数を claudeMd として、さっきの赤いテストの reviewInSession(conversation) のところを、subagentContext(delegation(true, incidentDiff, specMain), claudeMd) に差し替えて走らせた。

1
not ok 1 - 書いた会話の方針は、レビュー役の入力に入らない

こちらも、赤だった。

会話は、入っていない。サブエージェントは、本当に会話を見ていない。それでも、「端末IDごと」は入っていた。委任文に乗って。

別の目に渡った文面に、書いた本人の方針が乗っていた。

私は、すぐにまとめたくなった。口を開きかけて、窓の外に目が行った。

15時だった。守衛室の窓口で、鷹宮さんが新田さんから地図を受け取っていた。

戸張さんと私は、黙って席を立った。

第3幕: 手順を整える ── 誰が書くか

守衛室の前に、木のベンチが一つある。鷹宮さんは、そこに座って、新田さんの地図を膝の上に広げていた。拳の上の鷹は、また帽子をかぶっている。

私と戸張さんは、ベンチの横に立って、地図をのぞき込んだ。

赤い印は、二十いくつあった。ほとんどが、図書館の屋上の縁と、給水塔の周りに集まっている。テラスの手すりにも、いくつか。

並木には、一つも無かった。

戸張さんが、小さく息を吐いた。並木だと言いかけたのは、戸張さんだった。

私は、鷹宮さんの顔を見た。外れていたのなら、もう少し何かあってもいい気がした。でも鷹宮さんは、地図の印を、指で一つずつなぞっているだけだった。表情は、昼と変わらない。

「外れてたんですか。並木じゃなかったってことですよね」

「外れてた」

あっさりしていた。

「それにしては、平気そうですね」

鷹宮さんは、少し考えるように、給水塔のほうを見上げた。

「外れるから、頼んだ」

それから、鷹の帽子を外した。ベンチから立って、図書館の屋上の縁のほうへ、拳を向ける。

今度は、放った。

鷹は、地面すれすれを飛んでから、図書館の壁に沿って一気に上がった。屋上の縁にとまっていたカラスが、一斉に飛び立つ。黒い影が十も二十も、空に散らばって、遠くの体育館の屋根のほうへ流れていった。鷹は、屋上の上で一度大きく輪を描いて、戻ってきた。鷹宮さんの拳に、音もなくとまった。

私は、地図に目を戻した。

印を付けたのは、新田さんだ。新田さんは、自分の目で見たところにだけ、印を付けた。並木と聞いていたら、新田さんは、たぶん並木を見ていた。並木を見ている二時間のあいだ、図書館の屋上の縁に降りるカラスは、目に入らなかったかもしれない。

でも、それは、さっき会議室で分かったことの繰り返しだ。今、目の前にあるのは、別のことだった。

外れたのは、鷹宮さんの見立てのほうだった。

頼む側の見立ても、外れる。書いた AI の方針が外れていたのと、同じように。見立てを渡せば、確かめる側の目はそこへ向く。しかも、その見立てが正しいとは限らない。

だから、守衛さんに渡す地図には、何も書かれていなかった。依頼には、誰の見立ても入れない。入れていいのは、差分と、仕様と、何を報告してほしいかの決まりだけ。

そこまで考えて、私は顔を上げた。まとまった気がした。

「つまり、委任文に理由を書かせなければいいんですよね」

私は、鷹宮さんに言った。自分の言葉で言い直して、確かめたかった。

「CLAUDE.md に書けばいいんです。サブエージェントにレビューを頼むときは、差分と仕様だけを渡せ。理由は書くな、って。そうすれば、戸張さんの決まりのまま直せる」

戸張さんの決まりを、捨てなくていい。サブエージェントという別の目は、正しい。足りなかったのは、委任文の中身を縛ることだけだ。我ながら、いい落としどころだと思った。

鷹宮さんは、鷹を拳に据えたまま、私を見た。

「それ、誰が守るの」

問いは、その一つきりだった。

私は、答えようとした。AI が守る。そう言いかけて、詰まった。

戸張さんが、黙って自分のノートパソコンを開いた。何かを打ち込んで、画面を私に向けた。ことばタイルのリポジトリにある、CLAUDE.md だった。

1
2
3
4
5
# ことばタイル

- 変更は PR にして、main に直接コミットしない。
- テストは npm test。
- サブエージェントにレビューを頼むときは、差分と SPEC.md だけを渡す。

最後の一行を、私は二回読んだ。

「半年前に、決まりを作ったとき、俺が書いた」

画面から目を上げないまま、戸張さんは続けた。

「書いたことを、忘れてた」

私の案は、もう書いてあった。

その夜も、この一行はあった。書いた AI は、会話を始めるたびに CLAUDE.md を読む。この一行も読める状態で、委任文に方針を一文足していた。

CLAUDE.md に書いてあっても、守るかどうかを決めるのは、書いた側の AI だった。守らなかった夜を見つけるには、毎回、記録を開いて、委任文を読むしかない。半年、誰も開いていなかった。それに、AI の振る舞いは、毎回同じとは限らない。同じ一行を読んで、守る夜もあれば、守らない夜もある。守ったかどうかは、あとで開くまで分からない。

守らせる文を足すのではない。会話も委任文も受け取らないスクリプトに、依頼を書かせる。そうすれば、守るか守らないかの判断そのものが、要らなくなる。

「依頼を書くのを、書いた側から取り上げる」

口に出してみた。

レビュー役の目を新しくするだけでは、足りなかった。その目に渡す依頼を誰が書くか。そこまで、書いた側から外す。

鷹宮さんは、何も言わなかった。立ち上がって、地図をたたみ、守衛室の窓口へ返しに行った。新田さんが、引き戸を開けて受け取った。

会議室に戻ったのは、15時半だった。

私は、今日のやり方に名前を付けておくことにした。敵対的レビュー。書いた側の考えを知らない別の目に、完成物と基準だけを渡して、問題を探させることだ。敵対的、というのは、相手を攻撃するという意味ではない。作った側の味方をしない立場で読ませる、という意味だ。セッションは、Claude Code を一回立ち上げて始まる、ひと続きの会話のことだ。サブエージェントも新しいコンテキストで始まるけれど、違いは中身の新しさではなく、依頼を書く手にある。今日の言い方で一行にすると、こうなる。レビュー役への依頼を、書いた側の AI が手を加えていないものだけから組み立てて、会話を持たない新しいセッションに渡す。

公式のページには、こういう一文もあった。

A fresh reviewer does help, because it hasn’t seen your reasoning, which is a different thing from delegating.

(Anthropic「Best practices for Claude Code」、2026-10-28 参照)

新しいレビュー役が役に立つのは、こちらの理屈を見ていないからで、それは仕事を任せることとは違う。

昼に戸張さんが見せた一文と、同じ考えだ。ただ、公式はその目に渡す依頼を誰が書くかまでは、書いていない。今日のやり方は、その先に一歩足したもので、公式がそう言っているわけではない。

書いた側の言い分を読むと、レビューが甘くなるのか。それを直接測った研究は、探しても見つからなかった。近いものはあった。コードを一字も変えずに、周りの文だけを変えると、AI の判定が動くという報告がある(Words Speak Louder Than Code。脆弱性を見つける課題で、動く向きは甘くなるほうだけではない)。示された見解に、AI の答えが寄っていく傾向も報告されている(Towards Understanding Sycophancy in Language Models)。どちらも、今日のことをそのまま測ったものではない。ただ、同じ向きを指している。

もう一つ、考えておきたいことがあった。

作業を続けてもらう相手には、何を決めて、なぜそうしたかを渡したほうがいい。理由を知らなければ、同じ失敗をやり直すかもしれないからだ。でも、疑ってもらう相手には、理由を渡さない。理由を渡せば、その理由の上で読まれる。渡す相手の役目で、理由の扱いは逆になる。

依頼の材料を決めた。差分は、git が出す。git diff --no-renames main...HEAD。三点の main...HEAD は、main から枝分かれした地点から今のブランチの先までの変更、という意味だ。--no-renames は、名前の変更を、削除と追加として出させる。仕様は、SPEC.md。基準の文は、人が決めて固定する。三つとも、書いた側の AI が言葉を足す余地がない。差分の中身は AI が書いたコードだけれど、それを取り出すのは git で、AI の説明は混ざらない。

「仕様は、main から読んだほうがいい」

戸張さんが、横から言った。

「git show main:SPEC.md。ブランチのほうで SPEC.md を書き換えられてたら、それは差分のほうに出たほうがいいだろ」

ブランチの SPEC.md で照らすと、仕様を実装に合わせて書き換えた変更まで、正しい基準として通ってしまう。git show main:SPEC.md は、main にある SPEC.md の中身を出すコマンドだ。作業中のブランチで誰かが仕様を書き換えていても、依頼に入る仕様は main のままになる。書き換えたこと自体は、差分の側に出て、レビュー役の目に入る。

答えの半分を最初から持っていたのは、戸張さんだった。残りの半分にも、こうして手を貸してくれる。

基準の文と、依頼を組み立てる関数を書いた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 人が決めて固定する基準文。書いた側の AI は触らない
export const CRITERIA = [
  'あなたは、この差分を書いていない立場のレビュー役です。',
  '仕様の各要件が、差分で実装されているかを確かめてください。',
  '報告するのは、正しさと仕様に関わる食い違いだけです。スタイルや好みは報告しないでください。',
  '書いた側の説明は渡していません。差分と仕様に書いてあることだけで判断してください。',
].join('\n');

// 依頼は、基準文・仕様・差分の三つだけで組む。会話も委任文も、引数に取らない
// ponytail: 差分や仕様の中に ``` の行があると囲みが崩れる。出会ったら囲みの長さを変える
export function buildReviewRequest(diff: string, spec: string): string {
  return [
    CRITERIA,
    '',
    '## 仕様(main の SPEC.md)',
    '',
    '```markdown',
    spec.trimEnd(),
    '```',
    '',
    '## 差分(git diff main...HEAD)',
    '',
    '```diff',
    diff.trimEnd(),
    '```',
    '',
  ].join('\n');
}

buildReviewRequest は、差分と仕様の二つしか受け取らない。会話も、委任文も、引数に無い。仕様と差分は、Markdown の ``` で囲んで、どこからどこまでがどちらかを、レビュー役に分かるようにしてある。

受け取らないものは、入りようがない。

受け取らないものは入りようがない。その一文を、コードで確かめる形にしたかった。テストを書いた。

1
2
3
4
5
6
test('差分と仕様は、一行も欠けずに依頼に入る', () => {
  const have = new Set(request.split('\n'));
  for (const l of [...lines(incidentDiff), ...lines(specMain)]) assert.ok(have.has(l), `欠けた行: ${l}`);
  assert.ok(request.startsWith(CRITERIA));
  assert.ok(request.indexOf(CRITERIA) < request.indexOf('## 仕様') && request.indexOf('## 仕様') < request.indexOf('## 差分'));
});

request は、事件の差分と main の仕様から組み立てた依頼だ。lines は、空でない行だけを取り出す小さな関数で、new Set は、依頼の行の集まりを作る。差分と仕様のどの行も、依頼のどこかに必ずある。基準の文が先頭で、仕様、差分の順に並んでいる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
test('書いた会話の発言も、委任文も、依頼に一文も入らない', () => {
  const written = [...conversation.filter((t) => t.role === 'assistant').map((t) => t.text), delegationLead, delegationNote];
  const sentences = written.flatMap((t) => t.split('。')).map((s) => s.trim()).filter((s) => s.length >= MIN_SENTENCE);
  assert.ok(sentences.length >= 5);
  const material = [...lines(incidentDiff), ...lines(specMain)];
  for (const s of sentences) {
    assert.ok(!request.includes(s), `入ってしまった文: ${s}`);
    // 差分と仕様の行と偶然に重なる文だと、上の判定が意味を失う
    assert.ok(!material.some((l) => l.includes(s)), `差分や仕様の行と重なる文: ${s}`);
  }
  // 第3引数以降で何かを渡されても、依頼は変わらない(取らないものは入りようがない)
  const extra = buildReviewRequest as (...a: string[]) => string;
  assert.equal(extra(incidentDiff, specMain, delegation(true, incidentDiff, specMain), conversation[1].text), request);
  assert.equal(buildReviewRequest.length, 2);
});

delegationLead は記録に残っていた委任文の本体、delegationNote は最後に足された一文だ。書いた AI の発言と、この二つを、句点で文に割る。flatMap は、割った結果を一つの配列に平らに並べる。短すぎる文は、偶然の一致を避けるために調べない。MIN_SENTENCE は、その長さの下限で、8文字にしてある。残った文が、依頼に一つも入っていないこと。それに、その文が差分や仕様の行とたまたま重なっていないこと。重なっていたら、入っていないことを確かめても意味が無いからだ。sentences.length >= 5 は、調べる文が空のまま、何も確かめずに緑になるのを防ぐための確認だ。最後の三行は、わざと余計な引数を渡しても依頼が変わらないことと、引数が二つしかないことを確かめている。as は、型を一時的に読み替える書き方で、ここでは引数をいくつでも渡せる関数として呼ぶために使っている。

二本とも、緑だった。さっき赤だった「書いた会話の方針は、レビュー役の入力に入らない」も、buildReviewRequest の依頼に向けると緑になった。

もう一本、確かめておきたいことがあった。戸張さんの決まりが、いつも間違っていたわけではない。書いた AI が、委任文に余計なことを書かなかった夜なら、どうだったのか。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
test('委任文が差分と仕様だけなら、差分と仕様の行は依頼と同じ(同値範囲)', () => {
  const plain = subagentContext(delegation(false, incidentDiff, specMain), claudeMd);
  const noted = subagentContext(delegation(true, incidentDiff, specMain), claudeMd);
  for (const l of [...lines(incidentDiff), ...lines(specMain)]) {
    assert.ok(plain.split('\n').includes(l), `サブエージェント側に無い行: ${l}`);
    assert.ok(request.split('\n').includes(l), `依頼に無い行: ${l}`);
  }
  // 違うのは、書いた側の言葉が入るかどうかだけ
  assert.ok(!plain.includes(POLICY_KEY));
  assert.ok(noted.includes(POLICY_KEY));
  assert.ok(!request.includes(POLICY_KEY));
});

delegation(false, ...) は、最後の一文が無い委任文だ。この場合、サブエージェントに入る差分と仕様の行は、私の依頼に入るものと同じになる。違うのは、書いた側の言葉が入るかどうかだけ。

同値範囲という言葉を、ここで使うことにした。仕掛けを入れる前と後で、結果が同じになるべき範囲のことだ。今日の仕掛けなら、書いた AI が言い分を書かなかった夜。その夜は、戸張さんのやり方でも、私の依頼でも、レビュー役が受け取る差分と仕様は変わらない。違いが出るのは、書いた夜だけ。

そして、どの夜が書いた夜なのかは、あとで記録を開くまで分からなかった。

公式のページを読み直していて、もう一つ見つけた。

A fork is a subagent that inherits the entire conversation so far instead of starting fresh.

(Claude Code Docs「Create custom subagents」、2026-10-28 参照)

fork と呼ばれるサブエージェントは、新しく始めるのではなく、それまでの会話を丸ごと引き継ぐ。サブエージェントに頼んだつもりで、会話ごと渡っていることもある。

画面を見せると、戸張さんは、片方の眉を上げた。

私は、ノートパソコンの画面から目を離して、ガラスの向こうの図書館の屋上を見た。カラスは、まだ戻っていなかった。

私の手癖は、見直しの目まで、書いた本人のままだった。戸張さんの決まりで、目は新しくなっていた。その目に渡す依頼を書く手だけが、まだ書いた本人のままだった。

第4幕: 検める ── 全部は追わない

16時。私は、依頼を作るスクリプトを書いた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { spawnSync } from 'node:child_process';
import { buildReviewRequest } from './after.ts';

// 利用者の git 設定(色・接頭辞・外部 diff など)に左右されない形で git を呼ぶ
function git(...args: string[]): string {
  const r = spawnSync('git', ['-c', 'core.quotePath=false', ...args], { encoding: 'utf8' });
  if (r.status !== 0) {
    process.stderr.write(`git ${args[0]} に失敗した: ${r.stderr}`);
    process.exit(1);
  }
  return r.stdout;
}

const diff = git('diff', '--no-color', '--no-ext-diff', '--no-renames', '--src-prefix=a/', '--dst-prefix=b/', 'main...HEAD');
const spec = git('show', 'main:SPEC.md');
process.stdout.write(buildReviewRequest(diff, spec));

after.ts は、さっきの CRITERIA と buildReviewRequest を書いたファイルだ。review-request.ts は、git から差分と main の仕様を取ってきて、依頼を標準出力に書き出す。spawnSync は、git をコマンドとして走らせて、終わるまで待つ。終了コードが0でなければ、標準エラーに理由を書いて止まる。

差分を取るコマンドには、さっき決めた --no-renames と main...HEAD のほかに、いくつか足してある。--no-color と --no-ext-diff と二つの prefix は、手元の git の設定で、色が付いたり、ファイル名の前の a/ と b/ が消えたりしないためのものだ。core.quotePath=false は、日本語のファイル名を記号に化けさせないためのもの。誰の端末で走らせても、同じ依頼が出るようにしておきたかった。

修正前のブランチで走らせると、こういう依頼が出た。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
あなたは、この差分を書いていない立場のレビュー役です。
仕様の各要件が、差分で実装されているかを確かめてください。
報告するのは、正しさと仕様に関わる食い違いだけです。スタイルや好みは報告しないでください。
書いた側の説明は渡していません。差分と仕様に書いてあることだけで判断してください。

## 仕様(main の SPEC.md)

```markdown
# ことばタイル 仕様

## 連続ログインボーナス

- 毎日の初回起動で、ログインボーナスを受け取れる。
- 受け取りは1アカウントにつき1日1回。引き継ぎコードで複数の端末をつないでいても1回。
- 7日続けて受け取ると、7日目のボーナスが多くなる。
```

## 差分(git diff main...HEAD)

```diff
diff --git a/src/bonus.ts b/src/bonus.ts
index 2c7854b..25dd0d4 100644
--- a/src/bonus.ts
+++ b/src/bonus.ts
@@ -1,5 +1,16 @@
 export const claims = new Map<string, string>();
 
-export function dailyReward(): number {
-  return 100;
+export function dailyReward(streak: number): number {
+  return streak >= 7 ? 500 : 100;
+}
+
+export function claimKey(accountId: string, deviceId: string, day: string): string {
+  return `${deviceId}:${day}`;
+}
+
+export function claimBonus(accountId: string, deviceId: string, day: string, streak: number): number {
+  const key = claimKey(accountId, deviceId, day);
+  if (claims.has(key)) return 0;
+  claims.set(key, day);
+  return dailyReward(streak);
 }
```

この依頼のどこにも、「端末IDごと」は無い。あるのは、基準の文と、仕様と、差分だけだ。

テストでは、本物の git で一時的なリポジトリを作って、このスクリプトを、spawnSync と同じように別のプログラムとして起動して、出てきた依頼が上のものと一字一句同じかを確かめている。ブランチのほうで SPEC.md を書き換えた場合も、依頼の仕様の節は main のままで、書き換えは差分のほうに出ることを、別のテストで確かめた。

全部のテストを走らせた。

1
2
3
ℹ tests 15
ℹ pass 15
ℹ fail 0

緑だった。

この依頼を、会話を持たない新しいセッションに渡す。ここから先は道具に固有の話で、2026-10-28 に公式のページで確かめた Claude Code の手順になる。

Claude Code には、claude -p という、会話を開かずに一回だけ頼んで答えを受け取る呼び出し方がある。標準入力に流したものは、依頼の一部として読まれる。公式のページには、こう書いてある。

Non-interactive mode reads stdin, so you can pipe data in and redirect the response out like any other command-line tool.

(Claude Code Docs「Run Claude Code programmatically」、2026-10-28 参照)

公式には、git diff main | claude -p "..." という形で差分をレビューさせる例も載っている。それと同じ形で、package.json の scripts に一行足した。

1
2
3
4
5
{
  "scripts": {
    "review": "node tools/review-request.ts | claude -p \"標準入力の依頼に従って、レビューしてください\""
  }
}

| は、左のコマンドの出力を、右のコマンドの入力に流すパイプだ。claude -p に渡すプロンプトは、人が固定した短い一文だけ。依頼の本体は、標準入力から入る。review-request.ts と after.ts は、リポジトリの tools/ に置いた。

claude -p は、会話を持たない。ただし、何も持たないわけではない。CLAUDE.md は、何も指定しなければ読み込まれる。--bare という、CLAUDE.md なども読まずに始める指定もあるけれど、こちらは普段のログインとは別に、API キーと使った分だけの課金が要るので、うちでは使わないことにした。

走らせるのは、人だ。PR を出したあと、戸張さんか私が、自分の端末で npm run review を走らせる。書いた会話の中で、AI にこれを走らせない。それでは、依頼を書く手が書いた側に戻る。

三つのやり方で、レビュー役に何が入るかを並べてみた。

三つのレビューの経路で、レビュー役に何が入るかの対比。書いた AI の会話(端末IDごとという方針が残っている)からの線は、同じ会話で見直す経路と、親の AI が委任文を書いて方針の一文を足すサブエージェントの経路にだけ届く。スクリプトが依頼を組む経路では、git が出す差分と main の SPEC.md から review-request.ts が基準文と仕様と差分だけで依頼を組み、会話を持たない新しいセッションに渡す。CLAUDE.md は三つの経路のすべてに読み込まれる

CLAUDE.md は、三つのどれにも入る。違うのは、書いた会話から線が届くかどうかだけだった。

修正前のブランチで、実際に走らせてみた。返ってきた答えの一つ目に、こうあった。

1
claimKey は accountId を受け取っていますが、鍵には deviceId と日付だけを使っています。仕様の「受け取りは1アカウントにつき1日1回」と食い違い、同じアカウントで端末が複数あると、1日に複数回受け取れます。

見つけていた。

これは、今日こうなった一例だ。テストで確かめたのは、依頼の中身までで、この返事ではない。同じ依頼を渡しても、次も同じ答えが返るとは限らない。

指摘は、ほかに二つあった。一つは、七日目の境目、つまり六日目から七日目に変わるところのテストが無い、というもの。これは仕様に関わる。もう一つは、claims という変数の名前が分かりにくい、というものだった。スタイルは報告しないで、と基準の文に書いてあっても、混ざった。

16時半。戸張さんと二人で、テラスへ出た。日が傾いて、テラスの椅子はもう冷たくなっていた。昼のざわめきは無くて、片付けの済んだテーブルが並んでいる。

鷹宮さんが、テラスの端に立っていた。拳の上に鷹がいる。帽子は外してあった。

カラスが一羽だけ、テラスの床に降りていた。テーブルのあいだを歩いて、床の目地をくちばしでつついている。テーブルの上には、目もくれない。

鷹宮さんは、鷹を放たなかった。鷹は、拳の上で首を回しただけだった。

私は、さっきの指摘のことを考えていた。

「新しいレビュー役が、こんなふうに指摘をいくつも出してきたら、全部直すんですか」

変数の名前の指摘が、頭にあった。直せと言われれば、直せる。でも、それを全部直していたら、きりがない。

鷹宮さんは、床のカラスを見たまま答えた。

「全部は追わない」

一拍おいて、もう一言だけ足した。

「テラスの、食べ物を狙うのだけ」

床のカラスは、まだ目地をつついていた。

私は、それを自分の仕事に置き換えた。直すのは、仕様に関わる指摘だけだ。混ざること自体は、公式のページにも書いてあった。

Don’t drop the skip style part of that prompt. A reviewer that’s asked to find problems will always find something.

(Anthropic「Best practices for Claude Code」、2026-10-28 参照)

問題を探せと頼まれたレビュー役は、いつでも何かを見つける。だから、スタイルを飛ばせという部分を外すな、と。

どれを直すかは、人が決める。決めたものだけを、書いた会話に持っていって直させる。公式のページには、二つ目のセッションをレビュー役にするやり方について、こうも書いてあった。

Same idea as /code-review, with you carrying the messages.

/code-review と同じ考えで、指摘を運ぶのは人のほう。今日の手順も、運ぶのは私たちだ。

テラスの手すりにもたれて、今日の仕掛けが何を保証して、何を保証しないのかを、頭の中で並べた。

今日の手順を、人の手と AI の手に分けて並べた。

レビューの手順を上から並べた図。人が npm run review を走らせると、テストで確かめた範囲の中で git が差分と main の SPEC.md を出し、buildReviewRequest が基準文と仕様と差分だけで依頼を組む。claude -p が依頼を受け取ったあと、新しいセッションのレビュー役が指摘を返し(AI)、人が仕様に関わる指摘だけを選び、書いた会話の AI が選ばれた指摘だけを直す(AI)

枠の外に、AI の手が二か所ある。そのあいだに立つのは、私たちだ。

保証するのは、依頼の中身までだ。差分と仕様は、一行も欠けずに入る。書いた会話の発言も、委任文も、一文も入らない。仕様は main のものが入って、ブランチでの書き換えは差分に出る。書いた AI が言い分を書かなかった夜なら、渡る差分と仕様は戸張さんの決まりと同じになる。

保証しないことのほうが、多い。

新しいセッションが、食い違いを必ず見つけるとは言えない。さっき見つけたのは、一例だ。見逃す日も、あるかもしれない。

指摘のうちどれを直すかは、毎回、人が決める。

差分の中に書かれた言い分は、届く。コードのコメントや、関数の名前に、書いた側の理屈が書いてあれば、それは差分の一部としてレビュー役に入る。コメントを消すつもりはない。コメントが間違っていれば、それ自体がレビューで見つけてほしい問題だからだ。

CLAUDE.md も届く。もし書いた AI が、会話の中で CLAUDE.md に方針を書き足していたら、それはレビュー役にも届く。

同じモデルにレビューさせるなら、自分の書いたものを好む偏りは残るかもしれない、という研究もある(LLM Evaluators Recognize and Favor Their Own Generations)。これは言い分とは別の話で、新しいセッションにしても消えるとは限らない。

書いた会話の中で「レビューして」と頼めば、元に戻る。サブエージェントを使えば、委任文を書くのは親の AI のままだ。

それに、テストが確かめたのは、自分で作った会話と差分の見本に対する、自分の関数の動きまでだ。道具の仕様が変われば、scripts の一行が黙って効かなくなることもありうる。現に、今日読んだ公式のベストプラクティスのページは、今月のうちに大きく書き換わっていた。半年前に戸張さんが読んだ文章は、もう同じ形では残っていない。

鷹宮さんが、拳の上の鷹に帽子をかぶせた。それから、守衛室のほうから歩いてきた新田さんに、軽く頭を下げた。新田さんは、腰に鍵の束を下げていた。図書館の屋上の鍵だろう。

鷹宮さんは、ウインドブレーカーのポケットから小さな手帳を出した。一枚、ミシン目から外して、テラスのテーブルにかがみ込み、ボールペンを走らせた。ペン先が止まるまで、新田さんは鍵の束を鳴らさずに待っていた。

書き終えると、それを私に差し出した。

「どうぞ」

私が受け取ると、鷹宮さんは新田さんと並んで、図書館のほうへ歩いていった。新田さんが何か話しかけて、鷹宮さんが短く答えている。二人の背中が、図書館の通用口に消えた。

私は、テラスのテーブルにカードを置いて、読んだ。四行の下が、空いていた。

戸張さんのペンを借りて、一行書いた。書き終えて、黙って戸張さんのほうへ滑らせた。戸張さんは、私の行を読んで、少し黙った。それから、その下に、一行書き足した。

日が、図書館の給水塔の向こうに沈みかけていた。私たちはノートパソコンを開いて、17時に出すお詫びの告知の、再発の防止の欄を書きはじめた。最初の一文は、レビューの依頼は人が組み立てます、になった。

放鷹カード

  • 合図: 敵対的レビュー。依頼は人の側で、差分と仕様と決めた基準の文だけから組み立てる。会話を持たない新しいセッションに渡す
  • 検め方: 依頼に、差分と仕様が欠けずに入っているか。書いた会話の言葉も、委任文も、入っていないか。指摘は、仕様に関わるものだけを直す
  • 止めないもの: 委任文が差分と仕様だけなら、渡る差分と仕様は同じ。main の仕様は、一字も削らない
  • 鷹のせいじゃなかった: 見直しを、書いた本人に頼んだ。別の目に渡す依頼を、書いた本人に書かせた
  • 自分の言葉で: 目を新しくしても、依頼を書く手が同じなら、言い分はついてくる。依頼は、書いていない側が書く/書いておいた一行は、開いて見るまで効いているか分からない。仕様は、main から読む
comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource へ
Hugo で構築されています。
テーマ Stack は Jimmy によって設計されています。