「判断できません」で終わる原因は、依頼文ではなくワークフロー選択だった

「判断できません」で終わる原因は、依頼文ではなくワークフロー選択だった

依頼文には、指一本触れていない。

それでも、結果は正反対になった。

1. 「提示情報のみでは確認できません」

設計文書への反映作業で、対象箇所を特定するための調査をTAKTへ依頼したときのことである。読み取り専用の依頼であり、対象の設計文書も実装ファイルも、すべて手元にあった。

しかし、返ってきたのは調査結果ではなかった。

提示情報のみでは確認できません。

TAKTは、依頼文に書かれた文章だけを読み、設計文書も実装ファイルも一度も開かないまま、この一文だけを返して終了した。

最初は、依頼文の書き方が悪かったのだと思った。

調査観点が曖昧だったのか。参照範囲の指定が弱かったのか。あるいは、読み取り専用であることを強調しすぎたせいで、ファイル確認まで止まってしまったのか。

いくつかの可能性を考えたが、最終的に原因は別の場所にあった。

問題は、依頼文ではなかった。

選んでいたワークフローだった。

2. 同じ依頼文のまま、ワークフローだけを変えた

依頼文を書き直す前に、まずワークフローの選択を疑った。

このときTAKTへ渡していたのは、build-stepだった。

そこで、ワークフローをresearchへ変更した。依頼文は一字も変えていない。設定も同じである。ただ、選ぶワークフローだけを変えて、同じ依頼を再投入した。

結果は一変した。

TAKTは実際にファイルを読み、grep・sedを使った実測に基づく調査結果を返してきた。必要なAPIは既存のレスポンスをそのまま使えること、権限の判定が特定の1箇所に集約されており、全ロールへ自動的に適用されることまで、具体的な行を示して報告した。

依頼文は同じである。

設定も同じである。

変わったのは、選んだワークフローだけだった。

この時点で、原因の場所がはっきりした。

依頼文の表現ではなく、依頼を渡す「入れ物」が合っていなかったのである。

3. build-stepとresearchは、前提が違う

build-stepresearchは、見た目には似ている。

どちらもTAKTへ作業を依頼するためのワークフローであり、どちらも実装や調査の前段で使えそうに見える。

しかし、そもそもの立ち位置が違う。

researchは、TAKTに標準で組み込まれているワークフローである。公式のビルトインカタログに「質問せずに自律的に調査を実行する」ためのものとして、最初から用意されている。

一方、build-stepは、こちらで独自に作ったワークフローである。TAKTの標準には存在しない。実装作業に合わせて、必要な形へカスタマイズしたものだった。

つまり今回の一件は、標準品と自作品を比べていたのではない。標準の調査用ワークフローがあるにもかかわらず、それを使わずに、自作の実装用ワークフローへ調査を依頼していた、ということになる。

前提の違いは、こうなる。

build-step research
主な目的 成果物の生成・修正 情報の収集・調査
想定する状態 これから実装することが決まっている 何を実装すべきかを、これから調べる
情報不足のときの動き 判断できず終了することがある 分かったこと、分からなかったことを報告する
同じ依頼文を渡しても、build-stepは実装対象が定まっている前提のため判断できずそこで終了し、researchはこれから調べる前提のためファイルを開いて実測し報告する、という違いを示した模式図。

build-stepは、実装する対象がすでに定まっていることを前提にした構成になっている。「何を直すか」「どこを変更するか」がある程度決まっており、その作業を進めるためのワークフローである。

一方、researchは、まだ答えが定まっていない段階で使う。どこを見ればよいのか、どのファイルが関係するのか、何が不足しているのかを調べるためのワークフローである。

この違いは、実務上かなり大きい。

特に大きいのは、情報不足のときの返し方である。

researchは、すべての観点に答えを出せなかった場合でも、分かった範囲と分からなかった範囲を分けて報告できる。

一方、build-stepは、この中間的な返し方をあまり想定していない。ひとつでも判断できない要素があると、全体を「情報不足」としてまとめて終わらせてしまうことがある。

つまり、調査を依頼しているつもりでも、build-stepへ渡してしまうと、TAKT側では「実装できるだけの情報が揃っていない」と判断されることがある。

その結果、手元に読むべきファイルがあるにもかかわらず、ファイルを開く前に止まってしまう。

4. 「判断できません」で止まる仕組みは、もともと安全弁だった

では、なぜbuild-stepは情報不足のときに止まるのか。

これは、単なる不具合として生まれたものではない。

もともとは、逆方向の失敗を防ぐための安全弁だった。

以前、TAKTが「判断できない」「情報不足」という結果を出した後も、セッションが終了せず、runningの状態で残り続けることがあった。見た目には回答が返ってきているため、一見すると完了しているように見える。

しかし、内部ではプロセスが終わっていない。

次の作業に進もうとして初めて、まだ止まっていないことに気づく。気づかず放置すれば、無駄な待ち時間が積み重なっていく。

この問題を避けるため、build-steptest-stepdoc-genでは、判断できないときにループし続けるのではなく、「実装未着手(レポートのみ)」として正常終了できる分岐を組み込んだ。

つまり、この仕組みは「情報が足りないまま誤った報告をする」ことを防ぐためではない。

「情報が足りないまま、いつまでも終わらなくなる」ことを防ぐために作られた。

止まれること自体は、必要な設計だった。

ただし今回は、その安全弁が効きすぎていた。

本来はファイルを読んで調査すべき場面でも、build-stepの前提に引っ張られ、調査に入る前に「判断できません」で止まってしまったのである。

5. 「情報不足」には2種類ある

ここで注意したいのは、「情報不足」という回答そのものが、常に誤りではないということだ。

情報不足には、少なくとも2種類ある。

ひとつは、本当に確認手段がない場合である。

たとえば、実データの確認を依頼したとき、対象環境にpsqlコマンドもdockerコマンドも存在せず、データベースへ接続するための環境変数も設定されていないことがあった。

この場合、TAKTは実行すべきSQL文を提示したうえで、「この実行環境ではDBへ接続できず、実データの確認は未実施」「情報不足であり、判断できない」と報告した。

これは適切な振る舞いである。

存在しない接続手段を、存在するかのように装って進めなかったからである。

もうひとつは、手元に確認できる情報があるのに、それを見に行かない場合である。

今回問題にしているのは、こちらである。

設計文書も実装ファイルも手元にある。読み取り専用の依頼であり、確認すべき観点も指定している。それにもかかわらず、ファイルを一度も開かずに「提示情報のみでは確認できません」と返ってくる。

これは、単なる情報不足ではない。

確認できる材料を確認しないまま、情報不足として終わっている。

この2つを混同すると、対処を誤る。

本当に確認手段がない場合は、環境を整えるしかない。ワークフローを変えても、存在しないDB接続手段が急に現れるわけではない。

一方、確認できる材料があるのに見に行かない場合は、ワークフロー選択で解決することがある。

今回の事例は、まさに後者だった。

6. 同じ上限値でも、効き方が違う

自作した3つのワークフロー(build-steptest-stepdoc-gen)には、いずれもmax_steps: 10という同じ値を設定している。1回の実行が進められるステップ数の上限であり、セッションの消費を抑えるために入れたものである。

ただし、同じ10という数字でも、効き方はワークフローによって違う。

doc-genは、実装(この場合は文書生成)を行う単一のステップだけで構成されている。完了するか、完了せずにレポートのみで終わるかのどちらかで、そこから先の往復は無い。1〜2ステップで完結するため、10という上限はほとんど意味を持たない。ドキュメントのみを対象にした、判断を伴わない単純な作業に限定して使っているのは、そのためである。

一方、build-steptest-stepは、実装したものをレビューし、必要なら修正し、また確認するという往復を含む構成になっている。ここでの10は、その往復が際限なく続かないようにする、実質的な歯止めとして機能する。

同じ数字を置いても、構造の違いによって、実際に働くかどうかが変わる。設定値だけを見ても、その仕組みが何を制御しているかは分からない。

7. 依頼文を直す前に、ワークフローを疑う

振り返ると、これまでは同じような現象に対して、依頼文側の対処を重ねてきた。

冒頭に「ツール使用は禁止されていない」旨を明示する。「本ラウンドの前提」という節を新設し、参照範囲を限定する。書式を調整し、それでも改善しなければ実行環境側の設定を疑う。

いずれも、渡す文章そのものに手を加える対処だった。

効くこともあった。

しかし、効かないこともあった。

今回、依頼文をまったく変えずにワークフローだけを切り替えたところ、症状が消えた。

これによって、少なくとも一部のケースでは、原因が依頼文の外にあることが分かった。

TAKTの回答が「提示情報のみでは確認できない」「判断できない」「情報不足」といった内容に終始する場合、すぐに依頼文を書き直すのではなく、まずワークフロー選択を確認した方がよい。

特に、依頼内容が調査であるにもかかわらずbuild-stepを選んでいる場合は、researchへ切り替えるだけで動きが変わる可能性がある。

8. 課題管理表に出てこなかった理由

この種の問題が、課題管理表に紛れ込んでいないかも確認した。

結果として、該当する記録はなかった。

課題管理表には、プロダクト側の不具合や機能要望が並んでいる。一方で、TAKTのワークフロー選択や実行環境の設定に関する気づきは、引き継ぎ資料側の教訓として別に管理されていた。

つまり、「ユーザーに見えるプロダクトの課題」と「開発の進め方そのものの課題」は、自然に別の場所へ分かれていた。

この区分けは、後から見ると都合が良かった。

もし両方が同じ表に並んでいたら、プロダクトの課題なのか、開発運用の課題なのかを、毎回切り分ける必要があったはずである。

今回の件は、プロダクトの不具合というより、開発作業の進め方に関する教訓である。

そのため、課題管理表ではなく、運用上の学びとして扱う方が自然だった。

9. 現在の使い分け

以上を踏まえて、現在の使い分けはシンプルになった。

調査が目的なら、researchを選ぶ。分からない点が残っても、分かった範囲と分からなかった範囲を分けて報告してくれる。

確定済みの処理を実行するだけなら、build-stepでよい。実際、内容が確定しているPythonスクリプトを実行するだけの場面では、build-stepは問題なく機能している。

そして、TAKTの回答が「判断できない」「情報不足」に終始する場合は、依頼文を直す前に、まずワークフロー選択を確認する。

特に重要なのは、次の切り分けである。

確認手段が本当にないのか。それとも、確認できる材料があるのに見に行っていないのか。

前者であれば、環境を整える必要がある。後者であれば、ワークフローを選び直すことで解決する可能性がある。

10. まとめ——中身を直す前に、入れ物を疑う

今回分かったのは、依頼文をどれだけ整えても直らない現象があるということだった。

もちろん、依頼文の書き方は重要である。何を調べるのか、どのファイルを参照するのか、どこまでが読み取り専用なのかを明確にすることには意味がある。

しかし、依頼文だけでは解決しないこともある。

今回のように、依頼文は同じでも、build-stepからresearchへ変えただけで結果が正反対になることがある。

これは、文章の問題ではなく、ワークフローの前提の問題である。

build-stepは、実装対象が定まっている前提で動く。researchは、これから情報を集める前提で動く。

調査を依頼しているのに、実装前提の入れ物に入れてしまうと、TAKTは調査へ進む前に止まってしまうことがある。

同じ「情報不足」という言葉が返ってきても、原因はひとつではない。

本当に確認手段がない場合もあれば、確認できる材料があるのに、ワークフローの選択によって見に行けていない場合もある。

原因の場所が違えば、効く対処も違う。

依頼文を直す前に、入れ物を疑う。

今回の一件は、その重要性を確認するための記録である。同じTAKTの運用でも、依存関係の脆弱性対応では、サンドボックスが外部ネットワークへ出られないという、また別の環境側の制約に行き当たっている。原因の場所を切り分ける作業は、形を変えて繰り返し必要になる。

動画作成には自信があります!

サイレント障害——ログインはできるのに、裏側が止まっていた日

サイレント障害——ログインはできるのに、裏側が止まっていた日

これまでの記事は、すべて「実害の前に止まった」話でした。仕様書の検証、完了確認、実機確認。どこかで必ず、書き込みより手前で誤りが拾われていました。

この記事だけは違います。2回とも、問題は本番へ適用した後に見つかりました。

対象は、非同期のジョブ処理を担うライブラリのメジャーバージョン更新です。旧バージョンから新バージョンへの移行を、2度試みました。1度目は障害を起こしてロールバックし、2度目は成功しましたが、その中でもう一段の見落としがありました。検証を尽くしても防げなかったものが何だったかを、そのまま書きます。

1. 気づかれない障害

1度目の適用直後、バックグラウンドの処理系統が丸ごと止まりました。

column "retry_limit" of relation "job" does not exist

ライブラリ内部のテーブルへ、起動時に自動でスキーマを移行する仕組みがあり、それが失敗していました。API本体は動き続けていました。ログインもでき、画面も普通に開けます。 しかし、書類の取り込みや評価シートの抽出といった、裏側のジョブ処理だけが完全に止まっていました。

利用者からは、何も起きていないように見える種類の障害でした。 問い合わせが来て気づくのではなく、こちら側で気づけなければ、静かに処理だけが積み上がり続けるところでした。

2. まず、原因調査を先にした

ここで、慌てて元に戻すことをしませんでした。まず状況を確認し、そのうえでロールバックを決めました。

障害が起きていた時間帯(約8分間)に、新しく作られたはずのジョブが何件あったかを確認しました。0件でした。 実害が無いことを確定させてから、コードを元の状態へ戻し、通常の手順で再デプロイしました。復旧後、内部のジョブもアプリ側のジョブも、正常に完了まで進むことを確認しています。

「早く戻す」より先に「何が起きたかを確定する」を置きました。 これが、後の判断の材料になっています。

3. 根本原因——存在しないはずの移行手順

静的に調べた結果、原因がはっきりしました。

旧バージョンは、テーブルの列名をキャメルケース(retryLimitのような表記)で作ります。新バージョンは、スネークケース(retry_limit)を前提にした移行手順しか持っていません。 その間を変換する手順が、ライブラリ自体に存在しませんでした。

さらに、自動移行の仕組みには一つの前提がありました。「DB側のバージョンが、ライブラリの期待値より低い場合にだけ動く」という設計です。今回は、DB側の値がたまたま「低い」と判定される条件に当てはまり、変換手順が存在しないまま移行が走り、途中で壊れました。「高い場合」を検知して止まる仕組みは、そもそも用意されていませんでした。

加えて、新バージョンの新しいテーブル構造は、既存の通常テーブルを直接作り替えることができない種類のものパーティション化)でした。単純な列名の変更では済まず、新しいテーブル群を作って、データを移し替えるしかない規模の作業だと判明しました。

4. 予算という制約が、進め方を決めた

影響範囲の大きさから、当初は人が直接SQLを組み立てる案も出ました。しかし、「信頼していないのではなく、予算の問題」という説明のもと、通常の進め方(TAKTに作らせ、人が独立して検証する)を採りました。

効率と信頼は、別の軸で判断されています。 人が直接書けば早いかもしれませんが、それは信頼の問題ではなくコストの問題であり、通常どおりの工程を経ることが、この場面でも既定として選ばれました。

段階は2つに分けました。第1段階で新しいテーブル群を作りデータを移し、第2段階で切り替えとバージョン番号の更新を行います。

5. 独立検証が2件の不備を差し戻した

第1段階の設計に対する独立検証で、2つの不備が見つかりました。

  • 初期値の誤りretry_limitのデフォルト値が、旧バージョンの値のまま引き継がれていた
  • 制約の欠落:新しく作る子テーブル側に、主キー・外部キー・インデックス・制約が入っていなかった

両方とも、本番へ適用する前に差し戻して直させています。 ここまでは、これまでの記事で書いてきた工程がそのまま機能していました。

旧テーブルは、削除せずリネームして退避する設計にしました。切り替えに問題があれば、退避したテーブルへ戻せるようにするためです。

通常の工程(仕様書→TAKT実装→独立検証→本番適用→記録)と、今回の実績を対比した模式図。独立検証で2件を差し戻したところまでは通常どおりだったが、本番適用後にキューが機能せず通常の工程には戻らず、唯一TAKTを介さず人が直接SQLを作成した場面が分岐して示されている。
上:通常の工程。下:今回の実績。独立検証での差し戻しまでは通常どおりだったが、本番適用後に1件だけ通常の工程を外れ、人が直接対応した。

6. それでも、本番で見つかった

本番への適用は、データベースのバックアップを取得し、対象サービスだけを止めたうえで行いました。件数の確認、バージョン番号の確認、列名の確認——独立検証で確認した項目は、すべて一致しました。

しかし、最初の書類アップロードが、処理待ちのまま先へ進みませんでした。

原因を調べると、こうでした。新しいテーブル構造では、処理の種類ごとに専用の区画(キュー)を先に用意しておく必要があります。 その区画をどの種類ぶん作るかを、移行の瞬間にDBへ残っていた実データから決めていました。

ところが、移行を実行したちょうどそのとき、書類の抽出処理とCVの抽出処理という2種類については、たまたま処理待ちのジョブが1件も残っていませんでした。 残っていなければ、その種類の区画自体が作られません。存在しないのだから、そこへ来たジョブは行き場を失います。

「今、DBに何が残っているか」ではなく、「アプリケーションのコードが、そもそも何種類の処理を持っているか」を数えるべきでした。 実データを正として使ったことが、たまたまその瞬間の偶然に左右される結果を生みました。

7. このときだけ、工程を外れた

ここが、この記事でいちばん書いておきたい場面です。

本番が稼働している最中の不具合だったため、修正のSQLは、TAKTを介さず人が直接書きました。 理由は明確で、依頼して結果を待ち、検証して適用するという通常の往復にかかる時間を、避ける必要があったからです。

連載でずっと書いてきたのは、仕様書を書き、実装させ、独立に検証してから適用する、という順序でした。 この場面だけは、その順序を意図的に外しています。

外れることを許容できたのは、対応の中身が単純だったからでもあります。足りない区画を2つ追加するだけの、限定された操作であり、既存のデータへ触れる範囲ではありません。通常の工程を外れてよいのは、影響範囲が狭く確認しやすい場合に限られます。 今回はその条件に当てはまっていました。

追加のあと、処理待ちのまま止まっていた2件を再実行し、両方とも正常に完了することを確認しました。進行できないまま孤立していた1件は、失敗として記録し、そこで終わらせています。

8. 消さずに、退避した

最後に、退避しておいた旧テーブルをどうするかという判断が残りました。

すぐに消す案と、しばらく様子を見てから判断する案の2つを検討し、様子見を選びました。 理由は、この移行が2度とも本番で問題を起こしており、通常の稼働下での実績が、まだ移行当日の1日分しかなかったためです。

この判断は、課題として記録する場所には残しませんでした。 課題管理表は「対応すべきこと」を管理する場所であり、「しばらく様子を見る」という状態そのものは、対応すべき課題ではありません。 別の場所(引き継ぎの申し送り事項)で管理することにしました。

どこに何を書くかも、判断の一部です。

9. まとめ——網羅では防げないものがある

2回の障害には、共通する構造があります。どちらも、「存在するはずの手順が、実は存在しなかった」ことが原因でした。 1度目は列名を変換する手順そのものが無く、2度目は区画を用意する根拠が、移行のその瞬間だけの偶然に依存していました。

これは、書き込み前の検証を増やせば防げた種類のものではありません。 独立検証は機能していました。デフォルト値の誤りも、制約の欠落も、本番へ適用する前に見つけて直しています。それでも、本番の実データだけが知っている条件までは、検証の対象にできませんでした。

だから、備え方も変わります。 事前にすべてを見通そうとするのではなく、戻れる余地を残しておくこと——旧テーブルを消さずに退避したこと、限定的な範囲でだけ通常の工程を外れる判断を認めたこと、様子見という状態をそのまま記録に残したこと。すべて、防げなかった前提で組まれています。

網羅できないものがある、という前提に立つと、設計は変わります。それが、この記事で書いておきたかったことです。

1件の修正が別の場所に影響するという構造そのものは、課題管理を扱った別記事でも書いています。あちらは画面の見た目の話でしたが、根っこにある考え方は同じです。

動画作成には自信があります!

5週間前の正しい修正が、今日の不具合になる——4つの課題がつながっていた話

5週間前の正しい修正が、今日の不具合になる——4つの課題がつながっていた話

これまで、AIエージェントと開発を進める工程について書いてきました。仕様書の書き方、報告の検証、完了確認、バックアップ。今回は、1件の課題を起票から設計文書の反映まで、最後まで追いかけた記録です。

そして、この1件で分かったことがあります。5週間前に入れた正しい修正が、今日の不具合の原因になっていました。

1. 「見出し」の一語が通じない

発端は、画面キャプチャ3枚でした。社員情報一覧・評価一覧・職位一覧の3画面について、赤い矢印で一箇所を指し示したうえで、こういう指摘をいただきました。

一覧の項目名が、下へスクロールすると本文と一緒に流れて隠れてしまう。スマートフォンやタブレットから見るときに特に困る。

要求は明確です。列の名前が並んだ行を、スクロールしても上に留めてほしい。

ところが、着手する前にひとつ確認が要りました。「見出し」という語が、指しているものが一致していなかったのです。

対話AI(ひかり)は「見出し」を一覧内の列名が並んだ行の意味で使っていました。しかし画面には、そう呼びうるものが他にもあります。画面の一番上にある「職位一覧」というタイトルも、日本語としては見出しです。実際、「見出しというのは画面タイトルを指すのでは」という確認が入りました。

画像で場所を指し示していても、言葉の側で取り違えが起きかけます。 赤い矢印がどの行を指しているかは見れば分かります。だが、それを何と呼ぶかは、見ただけでは決まりません。

着手前に確認して一致させました。この一往復がなければ、画面タイトルを固定する実装が出来上がっていた可能性があります。

2. 画像が対象範囲を決めた

もう一度、画像が判断を決めた場面があります。対象範囲の確定です。

3画面のうち職位一覧には、表示形式が2種類あります。ひとつは行と列のマトリクス形式、もうひとつは段組みのカード形式です。カード形式には、そもそも列名の行が存在しません。 固定すべきものが無いのだから、対象になり得ません。

これを画面キャプチャで確認していただき、対象を「3画面、うち職位一覧はマトリクス形式のみ」と確定しました。

文章で「表形式の表示のみを対象とする」と書くこともできました。 しかし、その画面に表示形式の切り替えがあること自体を知らなければ、その文は書けません。画像を見て初めて、書くべき条件が分かりました。

3. 実装は、先例に倣った

方針は単純でした。同じ仕組みが、すでに別の画面で使われています。

評価シートのテンプレート指定を行う画面で、行と列を固定する書き方がすでに実装されていました。新しい書き方を発明せず、その先例に合わせます。 対象の3画面それぞれの列名行に、同じ指定を追加します。

職位一覧のマトリクス形式だけは、少し違います。上の行と、左の列の両方を固定する必要があります。

見出し行と職位列の固定を示した模式図。左が修正前でスクロールにより見出しが流れて消える状態、右が修正後で見出し行と職位列が固定された状態。
左:スクロールすると見出し行も職位列も流れて消える。右:両方が固定され、セルだけが動く。左上のセルだけは上下左右とも固定する必要がある。

図のとおり、左上のセルは行の固定と列の固定を両方受けます。この1セルだけ、指定が他と異なります。

4. 保守フェーズと、依頼の単位

ここで、進め方について触れておきます。8月15日以降、この開発は保守フェーズに入り、実装エージェント(TAKT)の実装主体をCodexへ切り替えています。

工程は一つも書き換えていません。 調査→仕様書→実装→独立検証→テスト→デプロイという流れは、これまで書いてきたそのままです。変わったのは、1回の依頼に載せられる作業量です。

選定で重視したのは、処理能力ではなくセッションの消費でした。毎回すべてを読み直すのではなく差分で管理される点に着目し、加えて、より高性能なモデルではなく、汎用的なモデルを積極的に選びました。

その結果、以前は「仕様書と実装」「テスト」といった単位に分けて投入していたものが、まとめて1回で通るようになりました。今回の設計文書への反映も、4文書への追記が1回の依頼で完了しています。 処理そのものには10分以上かかりますが、人が手を動かすのは1回です。

これは、以前に書いた「工程数は、AIの作業回数ではなく、指示を出す人が実際に実行した回数で数える」という指標での改善にあたります。 4回分の投入が1回になったということは、確認と貼り付けの往復も4分の1になったということです。

ただし、何でも1回に載るわけではありません。4文書の全文を読み込んで自己矛盾を洗い出すような依頼は、今もトークンの上限に触れるため、観点ごとに分割しています。 今回1回で通ったのは、仕様書の段階で反映箇所が特定済みだったからです。読む範囲が定まっていれば1回で通り、全文走査を伴うなら分割が要ります。

プロバイダを切り替えたときに何が変わり、何が変わらなかったかについては、別記事(番外編)に書いています。

なお、Claude CodeとCodexの使い分けについては、「立ち上げは前者、精密な修正は後者」といった整理が一般に語られています。ただし本稿は、その優劣を検証したものではありません。 ここに書いたのは、保守フェーズという段階で、セッションの消費を抑える目的で選んだという、運用上の判断だけです。

5. 全工程を通過したのに、そうは見えなかった

実装は、これまで書いてきた工程どおりに進みました。

  • 独立検証:仕様書どおりの差分(列名行への指定の追加のみ)であることを確認
  • 対象範囲のテスト:失敗0件
  • 全体テスト:失敗0件
  • デプロイ:成功

そして実機で確認したところ、見出し行は流れて消えました。

再びキャプチャ3枚をいただきました。スクロールした状態で、見出し行が画面から消えています。「固定されていません」という一文より、この3枚のほうが速く、正確でした。

すべての検証を通過した実装が、意図した見え方になりませんでした。 テストが検証できるのは「書いたとおりに指定が入っているか」であって、「画面上でそう見えるか」ではありません。両者は別のことです。

6. 原因は、5週間前の自分たちの修正だった

再調査を依頼して、原因が確定しました。

固定の指定を入れた要素の親に、横スクロールを許す指定overflow-x: auto)が入っていました。CSSの仕様上、横方向にこの指定をすると、縦方向も実効的に同じ扱いになります。 その結果、固定の基準がページ全体ではなく、その親要素そのものへ移っていました。

親要素には高さの制限が無く、内部でスクロールが起きません。基準が動かないのだから、固定しても何も起きません。 指定は正しく入っていて、効果だけが消えていました。

そして、この overflow-x: auto は、5週間前に自分たちで入れたものでした。

7. 4つの課題を、時系列で並べる

さかのぼると、同じ領域を4度通っています。

課題時期何をしたか
A7月前半全画面共通のレイアウトを整備。左メニューと上部ヘッダーを固定し、本文だけがスクロールする構造を作った。このとき、画面タイトルは固定ヘッダーに含めず、スクロール領域に残す方針を決めている
B7月20日画面右上の操作がスクロールバーと重なる問題を是正。Aで作った構造を、設計文書に明記した
C7月21日「見出しのスクロール時の挙動が他画面と揃っているか確認する」課題。調査の結果、共通の仕組みに乗っており対応不要と判断して閉じた
D7月22日表が画面幅を超えると右端が見切れる問題を是正。テーブルを横スクロール可能な要素で包む構造を、共通の型として設計文書に新設した
今回8月31日見出し行を固定。Dで入れた構造が原因で、機能しなかった

Aは、この記事の冒頭にもつながっています。 画面タイトルを固定ヘッダーに含めないという方針は、このとき明示的に決めたものです。つまり画面上には「固定されている上部の領域」と「固定されていない画面タイトル」が同居しています。 「見出し」という語がどちらを指すか一致しなかったのは、言葉の曖昧さというより、画面の構造がそうなっているからです。

Dの対応は、当時として正しいものです。 右端が見切れて操作できないという実害があり、それを解消しました。既存の画面と同じ構造に揃え、同じ原因を持つ画面を横断的に洗い出し、対象外とする画面の理由まで記録し、設計文書にも反映しています。やるべきことは、すべてやっています。

それでも5週間後、別の要求と衝突しました。

Cについても触れておきます。見出しの挙動を確認する課題は、実際に7月に存在しました。 ただしそれは「他画面と揃っているか」を問うもので、列名行を固定してほしいという要求ではありません。調査の結果、共通の仕組みに乗っており差分は無かったのです。当時の観点では、対応不要は妥当な判断です。

4件とも、それぞれの時点では正しいものです。 しかし積み重なると、後から入る要求を妨げる構造ができあがっていました。

8. 是正と、許容の判断

是正の方針は2案ありました。

  • 案1:親要素の縦方向の指定を打ち消す → CSSの仕様上、解消できないことを確認済み
  • 案2:一覧を囲む要素に高さの上限を設け、その中で独立してスクロールさせる

案2を採りました。 高さの基準値は、固定ヘッダーや余白を考慮して実装エージェントが実測し、3画面共通の値として決定しました。この値を人が決めていたら、画面ごとに違う数字を指定していたかもしれません。

そして実機確認で、1画面だけ二重スクロールが発生しました。 ページ全体のスクロールと、一覧内のスクロールが同時に存在する状態です。

これは事前に「起こさないこと」という条件を付けていませんでした。起きるかどうか、起きたときにどの程度気になるかは、実際に見なければ判断できないと考えたためです。

実際に見たうえで、許容範囲と判断しました。 見出し行が固定される利点のほうが大きい、という判断です。

9. コミットせずに、本番へ反映されていた

もう一件、この課題で起きたことを書いておきます。

是正の実装が終わり、独立検証もテストも通り、デプロイして実機確認も合格しました。課題管理表にも完了として記録しました。その時点で、変更はまだ一度もコミットされていませんでした。

発覚したきっかけは、「pushは実施しましたっけ?」という一言でした。確認すると、3ファイルの差分が作業ツリーに残ったままでした。

原因は、デプロイの仕組みにあります。 この環境のデプロイはファイルの直接転送であり、gitを経由しなくても本番へ反映できてしまいます。 そのため、「テストが通った」「本番で動いた」という事実は、コミットされたことを何も保証しません。

独立検証とテストが完了した直後に、必ずコミットを挟む。 これを工程として明示しました。「デプロイの後で git status を見たら空でなかった」という事後の発覚に頼りません。

10. 過去の記述を、打ち消さずに残す

最後に、設計文書への反映で下した判断を書きます。

調査の結果、Aの課題で書いた「本文が唯一のスクロール要素である」という記述が、4つの設計文書すべてに存在し、今回の3画面に限って例外が生じることが分かりました。 記述と実装が食い違います。

打ち消し線を引いて訂正することもできました。 しかし、そうしませんでした。

採ったのは、既存の記述はそのまま残し、その隣に追記するという方針です。 理由は明確で、Aの時点では、その記述は正しかったからです。誤りではなく、後から例外ができました。打ち消し線は「間違っていた」という意味になり、当時の判断まで否定してしまいます。

反映後、削除された行は版数と更新日の4行のみで、本文からは1文字も消えていないことを確認しました。追記だけです。

11. まとめ——正しい修正は、正しいまま原因になる

この1件で確認できたことを、3つ書きます。

第一に、正しい修正が不具合の原因になります。 5週間前の対応に、手抜きも見落としもありません。それでも、後から入る要求と衝突しました。参照関係のある領域では、1件を直すと隣が動きます。 これは避けられるものではなく、起きたときに追跡できるようにしておくしかありません。4件をさかのぼって原因を特定できたのは、それぞれの課題に対応の内容と理由が記録されていたからです。

第二に、検証を全部通過しても、意図した見え方になるとは限りません。 独立検証も、対象範囲のテストも、全体テストも通りました。それでも実機で見るまで、分かりませんでした。 テストが保証するのは「書いたとおりに動くこと」であり、「意図したとおりに見えること」ではありません。最終防衛線は、人が実物を見ることです。

第三に、画像が言葉より速いことがあります。 要求の指定、対象範囲の確定、不具合の報告——この課題では3か所とも、判断を決めたのは文章ではなくキャプチャでした。 ただし、それでも冒頭のように語の取り違えは起きます。画像は場所を指せますが、名前は決められません。 両方が要ります。

そして、設計文書の記述を打ち消さずに残したことは、この記事の第一の結論への向き合い方そのものです。 過去の判断を「間違いだった」と塗り潰せば、記録は整うかもしれません。しかし、なぜその構造になっているかを、次に読む人が追えなくなります。

正しかったものが、正しいまま合わなくなる。それを訂正ではなく履歴として残せるかどうかが、5週間後の自分を助けます。

動画作成には自信があります!

【番外編】エージェントを乗り換えて、何が変わり、何が変わらなかったか

エージェントを乗り換えて、何が変わり、何が変わらなかったか

第8回の最後に、こう書きました。

より賢いモデルが出れば不要になる工夫は、この中に一つも無い。

連載を書き終えたあと、実装エージェント側のプロバイダを切り替えました。 TAKT の runtime.yaml で、provider.profiles の既定を Claude から Codex(gpt-5.6-luna)へ変更し、対話AI側は Claude Code のまま残しました。

その後6日間・11セッションを回した結果を、この番外編で書きます。 期せずして、連載の主張を著者自身が検証する形になりました。

0. 先に断っておくこと

これは「Claude Code と Codex のどちらが優れているか」という記事ではありません。

比較には対照実験が要ります。しかし切り替えの前後では、扱っている課題も、文書の規模も、開発のフェーズも違います。同じ作業を両方でやって比べたわけではないので、優劣は書けません。 連載で「実測してから書く」と繰り返しておきながら、ここで印象論を書くわけにはいきません。

書けるのは「切り替えたときに、何が影響を受け、何が影響を受けなかったか」だけです。 それでも、そこには一つはっきりした結果が出ました。

1. 変わらなかったもの——工程のすべて

結論から書くと、第1回から第8回までに書いた工程は、一つも書き換えていません。

R0調査 → 仕様書 → build-step → 独立検証 → コミット
→ デプロイ → 実機確認 → 課題管理表の更新

この流れも、三層構造(人・対話AI・実装エージェント)も、仕様書を条件で書くことも、完了確認で止めることも、そのまま通用しました。

それどころか、連載で書いた原則が必要になる場面が、切り替え後も同じ頻度で発生しています。

連載で書いたこと切り替え後に起きたこと
第3回:報告値をそのまま転記しない「フェンス行数364で不変」という報告に対し、実測は前後とも356だった
第3回:報告の状態表示を信じないステータスが「実装未着手」でも、実測すると正しく反映されていた(複数回)
第8回:発見は広く、是正は狭く対象を2課題に限定した依頼で、指示していない別課題の食い違いまで4文書すべてで自主的に是正された
第5回:既存の失敗と自分の変更を切り分ける変更を退避した状態でも同じ5スイート29件が失敗することを確認し、無関係と実証した

プロバイダが変わっても、報告値と実測値は食い違います。ステータス表示は実態とずれます。指示範囲を超えた自主是正は起きます。 これらは特定のモデルの癖ではなく、「エージェントに作業させて報告を受け取る」という構図そのものに付随する性質であると考えるほうが、実測に合っています。

2. 変わったもの①——トークン上限という物理的な壁

一方で、切り替えによって新しく必要になった工夫が2つあります。1つ目は、依頼の分割です。

まず、対象の規模を示しておきます。実装ファイルのみ(テストを除く)で161ファイル・54,678行・2.21MBです。内訳の主なものは次のとおり。

領域ファイル数行数
frontend/src/pages1912,035
backend/src/routes2211,735
backend/src/services256,161
frontend/src/components163,731
backend/src/lib242,214
(ほか)5518,802
合計16154,678

画面1本あたり、エンドポイント1本あたりのファイルが厚いのです。 frontend/src/pages は1ファイル平均633行、backend/src/routes は同533行です。第1回で書いた設計文書4本(約84万字)と合わせると、「関連しそうなものをまとめて読ませる」という依頼の出し方が、そもそも成立しない規模になっています。

その前提で、設計文書4本を横断して自己矛盾を洗い出すラウンドを依頼したところ、2回連続で異常終了しました。

Request too large for gpt-5.6-luna ... tokens per min (TPM): Limit 200000

詳細設計書1本だけで6,700行を超えるため、全文読み込みを前提とした依頼が分あたりのトークン上限に抵触していました。

対処は、依頼を「1回1観点」まで絞ることでした。 全文を読ませるのをやめ、確認したい観点ごとに該当する章・節番号を明示し、部分的に読ませる形へ分割しました。これで解消しました。

この経験から得た一般則は、意外と単純です。4文書を横断する走査ラウンドは、最初から観点ごとに分割して依頼するほうが、手戻りが少ない。「まとめて1回で」を優先すると、かえってやり直しの回数が増えます。そして「やり直しの回数」は、第7回で書いたとおり、人の実行回数として跳ね返ります。

3. 変わったもの②——プロンプトの問題に見えて、設定の問題だった

2つ目のほうが、教訓としては大きいものです。

切り替え後、依頼文に禁止の文言を一切含めていないにもかかわらず、エージェントが「ツール使用が禁止されている」という趣旨の報告だけを返し、調査も実装も一切せずに終了するという事象が、複数回発生しました。

当初の対処は、運用での回避でした。依頼文の冒頭に、次の一文を足します。

本タスクではファイルの読み取り・調査・編集にツールを使用してください。ツール使用は禁止されていません。

これで実際に解消しました。 解消したのだから、原因は依頼文にあると考えたくなります。

しかし、原因は依頼文ではありませんでした。 後日、TAKT のワークフロー設定4ファイルを見直したところ、runtime.yamlprovider.profiles.*.options.allowed_tools——エージェントに許可するツール(ReadGlobGrepEditWriteBash)を列挙する箇所——に不足があった可能性が浮上しました。

許可リストに BashEdit が無ければ、エージェントの側からは「一部の操作を禁止されている」ように見えます。 依頼文に何の制約も書かれていなくても、です。設定を見直したあとのセッションでは、この事象は再発していません。

この一件から、運用の目安を一つ加えました。

依頼文の書式を2回調整して改善しなければ、3回目を試す前に、ワークフロー・ランタイム設定の側を点検する。

「プロンプトを工夫すれば直る」ように見える不具合が、実は実行環境の設定に由来していることがあります。 そして回避策が効いてしまうと、根本原因は見つからないまま残り続けます。回避策が効いたことは、原因が特定できたことを意味しません。

4. 導入時の前提条件

実務的な注意点を一つ。TAKT から Codex を使うには、ChatGPT の契約とは別に、エージェント向けの契約が必要になります。

runtime.yamlprovider.profiles はプロファイル単位で構成されており、既定プロファイルを Codex に、別プロファイルを Claude に割り当てて併存させることもできます。切り替えは設定ファイル1本の変更で済みますが、その手前に契約の確認が要ります。 試す前に把握しておくと、段取りが変わります。

5. 現在の構成

保守フェーズに入った現在は、次の組み合わせに落ち着きつつあります。

使用しているもの
対話AI(仕様書・検証条件の設計)Claude Code
実装エージェント(実装・テスト・走査)Codex(TAKT経由)

この2つを分けていること自体は、連載第1回で書いた三層構造そのままです。 変わったのは、三層目に入るものが差し替わっただけで、層の設計は動いていません。

6. まとめ——工程は、乗り換えのコストを下げる

6日間・11セッションを回して、はっきりしたことがあります。

工程を「ツールの使い方」として組んでいたら、乗り換えのたびに組み直しになっていました。 そうならなかったのは、連載で書いた原則がどれも「報告を実測で検証する」「範囲を超えたら止める」「壊れる前に確かめる」という、エージェントの実装に依存しない形をしていたからです。

切り替えで新しく必要になったのは、トークン上限への対処と、ランタイム設定の点検という2点だけでした。どちらも工程の話ではなく、環境の話です。

第8回で「より賢いモデルが出れば不要になる工夫は一つも無い」と書きました。今のところ、この主張は取り下げなくてよさそうです。 ただしこれは6日間・11セッションの結果にすぎません。さらに回した先で覆るなら、そのときはまた実測を添えて書きます。


参考リンク

動画作成には自信があります!

AIが人の一覧を超えるとき

AIが人の一覧を超えるとき

第2回で、仕様書を「行番号の一覧」ではなく「条件」で書くようにした話を書きました。その狙いは、人が見落とした該当を、実装を全文走査するエージェントの側で拾わせることでした。

最終回は、それが実際に起きた場面を書きます。TAKTが、人の作った一覧にも仕様書にも無かった乖離を、自ら見つけた実例が3つあります。

1. 誰も挙げていなかった一覧

設計文書への反映作業で、走査が2件の該当を検出しました。

  • ある章の設定キー一覧に、新設した2つのキーが載っていなかった
  • 別の節の記述が、処理の起動時刻を固定値かつ特定のタイムゾーン固定として述べていた(実際には設定変更が可能になっていた)

この2件が示すことは、単に「見落としがあった」ということではありません。調査ラウンドの報告も、その報告を読んで書いたひかりの仕様書も、どちらもこの章を一度も挙げていませんでした。

つまり、人(ひかり)が2段階の工程を経て作った一覧に、この2件は最初から入っていなかったのです。 条件による走査でなければ、そのまま取りこぼしていました。

2. 仕様書に書いていないことを、TAKTが直した

もう一つは、性質が異なります。

あるテストの是正作業で、期待値が旧い仕様のまま残っていた箇所を直す指示を出しました。仕様書には「改めた箇所にコメントを残す」としか書いていませんでした。

ところがTAKTは、そのファイルの冒頭にあるコメントが、旧い仕様を断定的に説明していることを自ら見つけ、あわせて是正しました。

期待値だけを直していれば、そのコードの冒頭に、誤った前提を説明するコメントが残り続けるところでした。 テストは通りますが、次にそのファイルを開いた人は、冒頭のコメントを読んで誤解します。最も見つかりにくい種類の負債です。

3. 判断を仰いだ場面

三つ目は、TAKTが「直さなかった」例です。

同じ作業で、TAKTはある群のテストについて「これは仕様判断を要する」と報告し、そこで止まりました。実装を直すべきか、テストの期待値を直すべきかが、コードだけからは判定できないという理由でした。

この報告は正しいものでした。 課題管理表を確認すると、その論点についての方針が、過去の課題の記録として残っていました。コードには書かれておらず、資料の側にしか存在しない情報でした。

ここで重要なのは、TAKTが「判定できない」と正しく述べたことです。もし推測で片方を選んでいたら、確定済みの方針と食い違う実装になっていた可能性があります。

4. 3つの例から見える線引き

この3つは、それぞれ違うことを示しています。

場面TAKTの振る舞いなぜ正しかったか
一覧に無い該当を発見条件に該当するものとして報告仕様書が「条件」で書かれていたため、一覧の外まで探せた
コメントの旧仕様を是正仕様書に無いことを、自ら是正是正の目的(旧仕様の記述を残さない)に照らして一貫していた
仕様判断が必要と報告判断せず、止まって報告コードの外にある情報が必要であり、推測すれば誤りえた

2番目だけが「仕様書に無いことをした」例です。 これを許容してよかったのか——連載を通して「指示にないことは独自に判断させない」と書いてきた立場からすると、矛盾するように見えるかもしれません。

しかし、2番目は「是正の対象を広げた」のではなく、「同じ是正を、同じ理由が当てはまる箇所に適用した」ものです。 旧い仕様の記述を残さないという目的は、期待値にもコメントにも等しく当てはまります。目的の範囲内での適用は、範囲の逸脱ではありません。

一方、3番目で止まったのは、目的そのものが定まっていなかったためです。「実装が正しいのか、テストが正しいのか」は、是正の目的の定義に関わる問いであり、コードからは決められません。

この線引きは、実は第7回で書いた人間側の作法とまったく同じ形をしています。 任された範囲では決める。任されていない範囲では止まって報告する。同じ原則が、人にも、対話AIにも、実装エージェントにも適用されています。

5. 走査を「書き込みの後」に置く

ただし、この自律性には条件があります。走査は、書き込みの前ではなく後に置きます。

これは経験から定まった順序です。先に走査させると、TAKTがその結果を踏まえて、編集範囲を自ら広げる余地が生まれます。 走査で見つかった該当を、そのまま「ついでに」直してしまえる状態になります。

そこで、書き込みを終えてから走査させ、該当が見つかっても「是正せず報告する」という形にしています。冒頭で挙げた2件も、この形で報告され、次のラウンドとして人が判断してから是正しました。

自律的に発見させることと、自律的に是正させることは、分けて設計できます。 発見の範囲は広く取り、是正の範囲は狭く保つ。この2つを混ぜないことが、走査を安心して任せられる条件になっています。

6. 連載のまとめ——8回で書いてきたこと

全8回を通して書いてきたことを、最後に整理します。

  1. 三層構造——人が決め、対話AIが仕様と検証条件を設計し、実装エージェントが実装する
  2. 仕様書は条件で書く——一覧は書いた人が見つけた範囲でしかない
  3. 報告ではなく検証手段を疑う——誤っているのは、たいてい確認コマンドの側である
  4. 完了確認は「止まる条件」まで書く——数値を報告させるだけでは足りない
  5. バックアップは作業の外側に置く——gitの挙動を把握しないまま復元に使わない
  6. 誤りは消せない、拾う——1セッションで7件、すべて書き込み前に検知された
  7. 工程は人の実行回数で数える——最適化すべきはAIの手間ではない
  8. 発見は広く、是正は狭く(本稿)

この8つに共通するのは、どれもAIの性能の話ではないということです。 より賢いモデルが出れば不要になる工夫は、この中に一つもありません。むしろ、エージェントが賢くなるほど、「どこまでを任せ、どこで止めるか」の線引きは重要になります。

第1回に書いたとおり、この連載は成功談ではありません。書いてきたのは、誤りの記録と、その誤りが実害になる前に止まった仕組みの話です。 誤りが7件あったセッションを「うまくいった日」と呼べるのは、7件すべてがどこかで止まったからにすぎません。

AIと開発するとは、AIに開発させることではありません。 誤りうる主体を複数並べ、それぞれが違う種類の誤りを拾えるように工程を組み、そのうえで人が決める——そういう作業のことです。

この連載が、同じ問題に取り組んでいる方の参考になれば幸いです。 8回にわたりお読みいただき、ありがとうございました。


参考リンク

動画作成には自信があります!

人間側の作法——指示は字面でなく実体で/工程は「人の実行回数」で数える

人間側の作法——指示は字面でなく実体で/工程は「人の実行回数」で数える

これまでの6回は、AIの側の設計を書いてきました。仕様書の書き方、報告の検証、完了確認、バックアップ、そして誤りの実例。第7回は視点を変えます。対話AI(ひかり)の側にも、繰り返し是正されてきた作法があります。 その一部を書きます。

結論を先に書きます。この開発でいちばん高くつくのは、AIの誤りではなく、人の実行回数です。

1. 指示は字面ではなく実体で受け取る

ある指示に対し、指示文中の語をそのまま検索キーワードとして扱い、見つからないと「実行できません」と述べたことがありました。見つからなかった時点で書式を変えて検索をやり直し、それでも見つからず、また書式を変える——これを4回繰り返しました。

このとき指摘を受けた一言があります。

字面が無い=指示が特定できない、と結論づけたことが誤りです。

指示の実体は、別の言い回しで文書の中に存在していました。 探し方を変えて何度も検索を繰り返すことは、一見粘り強く見えますが、実際には指示を出した人の確認の手間を増やしているだけです。 以降、こうしています。

  • 見つからないと分かった時点で、探索をやめる
  • 語を変えて検索を繰り返さない
  • 「どういう状態を指しているか」を、その場で尋ねる

字面と実体がずれる場面は、大きな開発ほど増えます。同じ対象を指す呼び方が、文書ごとに、時期ごとに変わっていくためです。探すことに時間を使うより、尋ねることに時間を使うほうが、双方にとって早いのです。

2. 質問は冒頭に、1回に1件

長い報告や考察の末尾に質問を置くと、その質問がどこにあるか自体を、相手が探さなければなりません。

質問までの、回答、コメントが長くなるほど、事故が発生します。

これ以降、次の3つを徹底しています。

  1. 質問は、応答の冒頭に置く
  2. 1回の応答で、質問は1件に絞る。 複数の質問を並べると、いただいた返答がどの質問への答えなのかが判定できなくなる
  3. 選択肢は、相手にしか決められないことに限って並べる。 選べる形で並べること自体が、論点を不必要に増やす

3. 貼り付けだけを受け取ったときは、止まっていることを明示する

調査結果や実行結果の貼り付けは、それ自体では質問への回答になりません。 貼り付けを受け取っただけで作業を先へ進め、実は先に出していた質問への回答をまだ受け取っていなかった、という事故が実際に起きました。

以降、「これは質問への回答ではないため、止まっています」と明示することにしています。黙って待つのではなく、止まっていることそのものを言葉にします。指示を出す側も、時に取り違えることがあります。双方の食い違いを防ぐのは、沈黙ではなく明示です。

4. 止まることを目的化しない

作法を是正した直後、今度は逆方向に振れたことがありました。何でも案を並べて判断を仰ぐようになり、任せられた範囲まで止まって確認を求めるようになりました。

では、このまま何も対応しない意思と捉えます。

止まるべきは、指示の範囲を超えるときです。「決めるのが不安なとき」ではありません。 任された範囲では決めます。判断を仰ぐのは、相手にしか決められないこと(設計判断・仕様の解釈・運用の方針)に限ります。

第1〜6回で書いてきた「TAKTは仕様書に無い乖離を独自に是正しない」という設計は、実はこの原則の裏返しでもあります。任された範囲は進め、任されていない範囲は止まって報告する。 この線引きは、AIエージェントにも、対話AI自身にも、同じ形で適用しています。

5. 気づいた不整合を、その場で課題として立てない

作業中に、本題とは別の小さな不整合に気づくことがあります。それをその場で課題として提起しないことにしています。

1点でもほころびがあると、課題にするため、未整理が増え、そもそも忘れてしまう。

引き継ぎ資料に記録するだけに留め、整理する時機は、指示を出す側が決めます。 気づいたことを逐一提起するのは、一見誠実に見えますが、受け取る側の検討事項を無制限に増やす行為でもあります。

同じ理由で、新しい運用・新しい分類・新しい欄を、こちらから提案しないようにもしています。既存の枠の中で解く方法を、まず探します。運用を増やす提案は、課題を新規に作るのと同じだけの負担を、後々まで残します。

6. 定着している運用を、確認せずに変えない

ある更新作業で、「相手が内容を編集する」という前提で手順を組み立てたことがありました。実際には、その更新は一貫してこちら側の担当であり、相手はスクリプトを実行するだけという運用が、最初から定着していました。

今まで一度も私が更新したこともありません。

定着している運用は、書き換える前に必ず確認します。 良かれと思って前提を変えることは、相手の作業のやり方そのものを覆すことがあります。

7. 調査は、原則としてTAKTに依頼する

視点を変えて、もう一つ。「範囲が絞れているように見える調査」を、手元のコマンドで済ませてしまうことがありました。「TAKTの報告はどのみち実測で検証するのだから、最初から自分で実測したほうが早い」という判断でした。

これは誤りでした。 減っていたのは、対話AI側の工程数にすぎません。指示を出す側のコマンド実行の回数は、むしろ増えていました。1件の調査に4往復かかり、うち2回は対話AI側の誤りによる作り直しでした。

8. 工程数は「人の実行回数」で数える

ここが、この回でいちばん大事な点です。工程数を、対話AIやAIエージェントの作業回数ではなく、指示を出す人が実際にコマンドを実行した回数で数えます。

この観点に立つと、評価が逆転することがあります。「対話AIが自分で調べれば1工程減る」という判断は、対話AIの工程を1つ減らす代わりに、人の実行回数を増やしていました。 逆に、TAKTへ調査を依頼すれば、対話AIの工程は1つ増えますが、人が実行するコマンドは減ります。

以降、次の形にしています。

種別担当
コードの調査(実装・依存関係・テスト・呼び出し関係・規模)TAKT(実装エージェント)
本番のデータの実測(データベース・実ファイルなど)人が実行
手元に持ち込めた成果物の解析(添付ファイルなど)対話AI

手元のコマンドで代替してよいのは、1〜2本のコマンドで判定が終わる場合に限ります。 それを超える調査は、範囲が絞れているように見えても、TAKTへ依頼します。

依頼する範囲は、意図的に広く取ります。 往復を減らすため、後続の判断に必要になりそうな事項(依存の有無・規模・既存のテストの所在など)まで、最初の1回の依頼にまとめて含めます。細かく分けて何度も依頼するほうが、結果として人の確認の回数を増やします。

9. まとめ——最適化すべきは、AIの手間ではない

この回で書いた作法は、どれもAIの性能とは無関係です。指示の受け取り方、質問の出し方、止まる基準、報告の作法、そして工程の数え方——これらはすべて、指示を出す人の負担をどう小さくするかという一点に集約されます。

第6回で「AIは誤る前提で工程を組む」と書きました。この回で書いたのは、その裏側にある原則です。AIの側の工程をどれだけ効率化しても、人の確認の回数が増えていれば、開発全体としては遅くなっています。 最適化すべきは、AIの手間ではなく、人が実際に手を動かす回数のほうです。


次回(第8回・最終回):AIが人の一覧を超えるとき。TAKTが、人の作った一覧や仕様書には無かった乖離を自ら発見した実例と、そこまで任せてよい範囲について書きます。

動画作成には自信があります!

AIは誤る前提で工程を組む

AIは誤る前提で工程を組む

第1回の最後に、こう書きました。「このセッションで、AIは7回誤った。そのすべてが、ファイルへ書き込まれる前に検知されている。」第6回は、その7件を実際に開き、なぜ実害が生じなかったのかを書きます。

結論を先に書きます。誤りを無くす設計はしていません。誤りが実害になる前に止まる設計をしています。 この違いは小さくありません。

1. 実測せずに書いた「8個」は、実は12個だった

あるスクリプトで、追記文に含まれるバッククォートの個数を、実測せずに「8個」と書きました。 実際は12個でした。

これは自己検査によって検知されました。第4回で書いた完了確認の考え方を、スクリプト自身の自己申告にも適用しています。冒頭に「この追記文にはバッククォートが何個含まれるか」を定数として書き、実行前にその数を数え直して照合します。 数が合わなければ、そこで止まります。

ここでの教訓は、対象が広いものです。「実測してから期待値を書く」は、ファイルの実測値だけでなく、自分自身が書いた文字列の属性にも当てはまります。 自分で書いた文章の中の記号の数を、自分は正確に把握していない——これは、人がコードレビューで見落とす感覚と同じものです。

2. アンカーの末尾の記号を、目の前にありながら写し落とした

ある文書の行末をアンカーとして使う際、実測の出力には行末が太字の記号(**)で終わっていることが表示されていました。それを見ながら、末尾の記号を落として書きました。

結果、アンカーは0件でヒットし、そこで停止しました。実測値が画面に出ていたにもかかわらず、写し取る段階で情報が欠落しました。 これは「実測していない」誤りではなく、「実測した結果を正確に転記できなかった」誤りです。両者は原因が異なるため、対策も異なります。前者は実測を義務化すれば防げますが、後者は転記後にもう一度、実測値と転記結果を突き合わせる工程が要ります。

3. 手元の写しへ適用した事実を、報告し忘れた

ある起票用のスクリプトを、本番ではなく手元の写しへ適用したまま、その事実を伝えずに次の作業へ進もうとしました。

技術的には何も壊れていません。しかし、さくらが把握している状態と、実際の状態が食い違ったまま進行するという、性質の異なるリスクです。是正は、原本へ前段の手順を順に再現し、ハッシュ値が一致することで写しの状態が正当であることと、本番が未変更であることを確認する形で行いました。

これはコードの誤りではなく、報告の誤りでした。 「何をしたか」を漏れなく報告することも、検証対象の一部として扱っています。

4. 「数が等しいこと」で書いた検証条件が、正常な編集を止めた

課題管理表を更新するスクリプトで、「置換の前後で強調記号の数が等しいこと」を事前の検証条件に置きました。ところが、旧が4個、新が8個——数としては変わっている(4≠8)が、どちらも偶数であるという編集で、条件に引っかかって停止しました。

ファイルは正常であり、誤っていたのは検証条件の側でした。 正しい条件は「数が等しいこと」ではなく「偶奇が保たれること」でした。第4回で書いた「正しい条件が、正しい作業を止める」構図の、初出の一件です。

5. 表の本数を、実測せずに「8本」と述べた

ある文書のある行にあるパイプ記号(表の区切り)を「8本」と述べました。実際は6本でした。 8本は別の章にある別の表の本数であり、参照する行を取り違えていました。

この誤りは、検証用に作った合成ファイル(本番に似せた再現用のファイル)を組み立てる段階で検知されました。 実測すべき値を、別の実測値と混同するという誤りは、単純ですが起きます。件数を扱うときは、「どの行の、どの表の」本数かを、値と一緒に書くようにしています。

6. 実文を確認していない箇所を、参考所在に含めた

仕様書の「参考所在」に、実際の文章を確認しないまま2箇所を含めました。 一つは表示順序についての記述で、是正条件には該当しないものでした。もう一つは画面の説明文であり、設定項目の列挙ではありませんでした。

どちらも、調査ラウンドの報告の要約だけを見て書いたことが原因です。ここで機能したのが、第2回で書いた「除外条件」の設計でした。仕様書に「参考値であり網羅ではない」と明記していたため、TAKTは独自に是正せず、対象外として報告しただけで済みました。

もし参考所在を「行番号の一覧」として断定的に書いていたら、この2件はそのまま是正されていたはずです。 条件で書くという設計そのものが、仕様書を書いた側の誤りを吸収しています。

7. 実文を確認せずに、追随の指示を仕様書へ書いた

最後の一件は、性質が少し異なります。ある版数の追随作業で、対象の記述を「版数だけを差し替えればよい」と仕様書へ書きました。しかし実文を確認すると、それは版数の数字だけの行ではなく、「その版が何を反映したか」を述べる488字の説明文でした。 版数だけを機械的に差し替えると、内容と版数が食い違った、意味の通らない文章になるところでした。

この誤りは、TAKTの走査ではなく、ひかり自身が実文を確認して気づきました。 気づいた時点で作業を止め、判断を仰ぎました。仕様書はすでにコミット済みだったため、書き換えるのではなく、次のラウンドの指示文の冒頭で、その一文を無効化する形で対応しました。

8. 7件を並べて分かること

7件を並べると、原因は一つではないことが分かります。

誤りの種類検知した仕組み
自分の書いた文字列を実測しなかった自己検査(定数と実測の照合)
実測値を見ながら転記を誤ったアンカー0件による停止
実施した事実を報告しなかった—(気づいて自主的に是正)
検証条件の立て方を誤った偶奇ではなく数の一致で判定していたことによる誤検知
実測値を別の値と混同した合成ファイルでの試走
要約だけを見て断定した除外条件の明記(TAKT側の設計)
実文を確認せず指示を書いた実文確認(人=ひかり自身の気づき)

検知した仕組みは、毎回同じではありません。 自己検査で拾われたもの、TAKTが仕様書どおりに動いたことで実害化しなかったもの、そして人(ひかり)自身が実文を見て気づいたもの——複数の異なる仕組みが、それぞれ違う種類の誤りを拾っています。

これは偶然ではありません。第2回から第5回まで書いてきた「条件で書く仕様書」「実測に基づく検証」「壊れる前に止める完了確認」「gitに頼らないバックアップ」は、それぞれ単体でも機能しますが、どれか一つが漏れても、別の仕組みが拾う設計になっています。 7件のうち、もし1つの仕組みしか無かったら、少なくとも数件は書き込みまで進んでいたはずです。

9. まとめ——工程は、誤りを消すためではなく、拾うためにある

AIは誤ります。これは前提であって、対策の対象ではありません。対策の対象は、誤りがどこで拾われるかです。

7件の誤りのうち、6件はAI(TAKTまたはひかり自身が書いたスクリプト・仕様書)の側で生じ、1件(検証条件の設計)は人が事前に立てた条件そのものの誤りでした。人が組む工程もまた、誤りうるのです。 だからこそ、一つの層だけに検知を頼りません。

この回で書いた7件は、特別な事故ではありません。この開発では、こうした誤りが日常的に起き、そのつど、書き込みより手前のどこかで止まっています。 それが、この連載で「成功談ではない」と繰り返し書いてきたことの、具体的な中身です。


次回(第7回):人間側の作法——指示は字面ではなく実体で受け取る。工程は「人の実行回数」で数える。AIの誤りだけでなく、人の側の指示の出し方についても、実測に基づいて型を作ってきた経緯を書きます。

動画作成には自信があります!

バックアップと復旧——git checkout を使わせない理由と git worktree の使いどころ

バックアップと復旧——git checkout を使わせない理由と git worktree の使いどころ

これまでの4回で、仕様書の書き方・報告の検証術・完了確認の設計を書きました。それでも壊れることはあります。 第5回は、壊れたときにどこまで戻れるようにしておくかという話です。

結論を先に書きます。壊れたときの復元手段として、gitを使わせていません。 一見遠回りですが、理由があります。

1. バックアップが要る操作は、決まっている

TAKTにファイル全体を書き換えうる操作をさせるときは、必ず事前バックアップを指示しています。対象は次の4種類です。

  • インプレース一括置換(perl -ised -i など、-i を伴うもの)
  • 変異(ミューテーション)テストでの実装ファイルの改変
  • 版数の更新など、巨大な行を含む設計文書への文字列置換
  • リダイレクトによる上書き(... > 対象ファイル

指示文には、次の形を定型にしています。

対象ファイルを編集する前に /tmp へコピーしてバックアップを取り、md5sumwc -l を控えてください。編集後に問題が起きた場合の復元は、このバックアップからのコピーのみで行ってください。git checkoutgit restoregit stash は使用しないでください。 復元後は md5sumwc -l の一致を確認し、バックアップを取った時刻・復元を行ったかどうかを報告に含めてください。

md5sum だけでなく wc -l(行数)も控えさせているのには理由があります。md5の不一致だけでは、切り詰められたのか、単に差分があるのかを区別できないからです。

そして、復元を行ったかどうかを必ず報告させます。 復元が成功していれば実害は無いのですが、報告が無ければ、事故が起きたこと自体に人が気づけません。これから書く2つの事故は、いずれもTAKTの自己申告によって発覚しています。

2. なぜ gitからの復元に頼らせないか

一見、git checkout -- <file> で戻せば済むように思えます。しかし、これを禁止しているのには明確な理由があります。

git checkout -- <file> はHEADの内容へ戻します。 未コミットの成果がそのファイルに同居していれば、今回の事故だけでなく、そのラウンドで積み上げた成果ごと巻き戻ってしまいます。 実際に、あるtest-stepの作業でこれが起きたことがあります。

もう一つ、git show HEAD:<file> からの復元は、直前にコミットがある場合しか成立しません。 前のラウンドを未コミットのまま次のラウンドへ進んでいれば、そのラウンドの成果は復元できません。

この2つの弱点が、実際に同時に牙をむいたことがあります。設計文書への反映作業の第4ラウンドで、版数更新に使った perl -i -pe の中に last という命令がありました。これは -p の暗黙ループそのものを終了させる命令であり、5,879行あった設計文書が、5行に切り詰められました。

TAKTは git show HEAD:docs/detail-design.md を一時ファイルへ出力し、md5がHEADと一致することを確認したうえで、cp で復元しました。git checkoutrestorestash はいずれも使っていません。この対応そのものは正しく、実害は生じませんでした。

しかし、復元が成立したのは、直前の第3ラウンドをコミット済みだったからにすぎません。 もし第4ラウンドが、未コミットのまま第3ラウンドの続きとして進んでいたら、git show HEAD: はコミット済みの版にしか戻せないため、第3ラウンドの成果ごと失っていたことになります。

gitからの復元は、直前の状態を正確に再現できる保証がありません。 バックアップは、必ず作業の外側(/tmp 配下のファイルコピー)に置きます。

3. 「先に確かめ、確かめてから書く」を型にする

事後の復元だけでなく、事前の予防も型にしています。ファイルを一括で書き換えるときは、次の4段階を踏み、いきなり書き込ませません。

  1. バックアップを取る
  2. 変更を加えない試行:対象の全件が同じ形かどうかを、書き込む前に確認する。同じ形でない行があれば個別に確認する
  3. 検証が成立した場合のみ書き込む:変更行数・行数の不変・文字数の増減・破損検査(連載第4回の完了確認項目)をすべて満たしたときだけ書き込む。1つでも満たさなければ、無変更のまま止まる
  4. 独立した確認:md5の変化・行数・破損検査・git diff --stat が、想定どおりであること

ある文書で強調記号の対応が19行にわたって崩れていたときは、この型で是正しました。19行すべてが同じ形であることを、置換の前にまず確かめ、4項目の検証がすべて揃ったときだけ書き込みました。このとき git diff --stat に想定外の変更が現れなかったことが、意図しない箇所に手が入っていないことの、独立した証拠になっています。

4. あわせて守っていること

事故を未然に防ぐための運用も、2つ定型化しています。

  • 事故の起きやすい対象へ進む前にコミットする。 ラウンドごとにコミットを挟みます。前節のとおり、gitからの復元が成立するのは直前のコミットがある場合に限られるためです
  • 最初のラウンドは最小の対象に置く。 指示文が実際に効くかどうかを、最小の被害範囲で確かめます。実際、196行の小さな文書から始め、5,879行の文書は最後に回しています

リスクの低い対象で型を検証してから、リスクの高い対象へ進みます。 順序そのものが安全装置になっています。

5. git worktree を、比較のために使う

最後に、gitを禁止しているわけではないという話を書きます。復元の手段としては使いませんが、比較の手段としては積極的に使っています。

「このテストの失敗は、自分の変更が原因か、それとも変更前から失敗していたのか」を切り分けたいとき、git worktree を使うと現在の作業ツリーに一切触れずに実測できます。

git worktree add /tmp/<名前> <比較したいコミット>
ln -s <本体の絶対パス>/frontend/node_modules /tmp/<名前>/frontend/node_modules
npm --prefix /tmp/<名前>/frontend test 2>&1 | tail -6
  • git stashgit checkoutgit restore を使わずに済みます。未コミットの成果は、現在の作業ツリーにそのまま残ります
  • node_modules はシンボリックリンクで共有します。これにより、依存関係の再インストールが不要になります。シンボリックリンクはあくまで参照であり実体ではないため、撤去してもリンク先が消えることはありません

撤去の手順には注意が要ります。 シンボリックリンクを先に外さずに git worktree remove を実行すると、node_modules が未追跡ファイルとして検出され、削除がエラーで拒否されます。ここで --force は使いません。中身を見ずに強制削除するコマンドであり、シンボリックリンク以外の何かが紛れ込んでいても気づけないためです。git status --porcelain で未追跡がシンボリックリンク1件のみであることを確認してから、rm-rf ではない)で外します。

そして、撤去を忘れません。 残したまま作業を終えると、次のセッションの git worktree list に見慣れないものが現れます。以降、セッション冒頭の確認コマンドに git worktree list を含めています。

実際にこの手法で、フロントエンドのテストが41件失敗したときの原因切り分けを行ったことがあります。過去のあるコミットを取り出して実行したところ、38件は変更前から失敗していることが実測できました。 増分の3件だけが自分の変更に起因すると確定でき、残る38件は別の課題として切り出しました。

6. まとめ——復元は「戻す」ためではなく「戻せる」ために設計する

この回で書いたことは、要するにgitの機能を信用していないという話ではありません。 git checkout が「直前のコミットへ戻す」という、それ自体は正しい仕様を持っているからこそ、未コミットの成果を巻き込むという副作用も正確に起きてしまいます。 禁止しているのは、gitではなく、gitの挙動を正確に把握しないまま復元に使うことです。

一方で git worktree は、現在の作業ツリーを一切変更せずに別のコミットを実行できるという性質を、復元ではなく比較のために使っています。 同じgitでも、目的が違えば使い方も変わります。

バックアップを取る理由は、壊れたものを元に戻すためだけではありません。「本当に自分の変更が原因なのか」を、あとから正確に問い直せる状態を残しておくためでもあります。


次回(第6回):AIは誤る前提で工程を組む。あるセッションで、AIは7回誤りました。そのすべてが、ファイルへ書き込まれる前に検知されています——なぜ検知できたのかを、これまでの5回で書いた仕組みを振り返りながらまとめます。

動画作成には自信があります!

壊れる前に止める——完了確認11項目の設計

壊れる前に止める——完了確認11項目の設計

前回まで、仕様書の書き方とAIの報告の検証術を書きました。第4回は、その両方が機能しなかったときの最後の砦——書き込みの直前に置く「完了確認」の話です。

結論を先に書きます。完了確認は、数値を報告させるだけでは足りません。異常時に「止まる」という振る舞いまで、条件として明記して初めて機能します。

1. 定型の8項目から始まった

大きな設計文書(要件定義書・設計書など、1本あたり数千行を超える)を編集する作業では、TAKTに次の8項目を確認させてから書き込ませる形を定型にしています。

項目止まる条件
バックアップの md5sumwc -l
編集前後の wc -l(行数)減少していたら止まる
編集前後の見出し数減少していたら止まる
編集前後の wc -m(総文字数)減少していたら止まる
差分のあった各ハンクの文字数(前→後)減少していたら止まる
diff の変更箇所の種別d(削除)が出たら止まる
打ち消し線記号の総数奇数なら止まる
打ち消し線が奇数個の行の件数0件でなければ止まる

「減っていたら報告して止まる」「奇数なら報告して止まる」と、止まったあとにどうするかまで書きます。 ここまでが土台です。

2. 8項目では検知できない破損があった

ある日、設計文書の一節で、アンカーとして行の前半だけを使いました。93字の行が、その途中で切断され、後半の断片が別の行の末尾へくっついてしまいました。

この破損を、8項目のどれも検知できませんでした。 行数は変わりません。見出し数も変わりません。総文字数もほぼ変わりません。打ち消し線の数も変わりません——行が分割されただけでは、これらの指標はすべて保存されます。

気づいたのは、別の作業(第2回で書いた是正条件による走査)が、その断片を条件に該当するものとして拾い上げたときでした。完了確認は素通りしていたのです。

3. 9〜11番目の項目を追加する

この経験から、3項目を追加しました。

項目止まる条件
強調記号が奇数個の行の件数増えていたら止まる
アンカー行そのものの文字数編集前と異なれば止まる
差分の種別(insertreplace か)insert を期待した箇所に replace が出たら止まる

これらは、行の分割を検知するための項目です。 行の途中へ追記するとき、アンカーの切り方を誤ると行が分割されます。

  • アンカー行の文字数の運用:追記文を改行で始まる形にすれば、アンカー行そのものの文字数は変わらないはずです。編集前の実測値を、そのまま期待値にします
  • 差分の種別の運用:行末への追記は insert、行の一部の置換は replace になります。どちらを期待するかを、ラウンドごとに明記します

「差分の組数」(いくつのハンクに分かれるか)は、値そのものでは止めていません。試走で作った再現ファイルと、本番の実ファイルとでは、diffアルゴリズムが変更をまとめる単位が変わり、組数が食い違うことがあるためです。止めるのは組数ではなく、種別(delete の有無・insert/replace の別)です。

4. 正しい条件が、正しい作業を止めることがある

11項目が揃っても、それで終わりではありません。条件の立て方を誤ると、今度は正常な作業のほうを止めてしまいます。

旧い記述を打ち消し線で別の場所へ移す作業では、移動元のハンクは必ず短くなります。「減少していたら止まる」をそのまま適用すると、この正常な移動のたびに作業が止まります。

実際、この例外を指示文に書き落としたことがありました。TAKTは、減少した2つのハンク(−17字・−197字)を「形式上は止まる条件に該当する」として正しく報告し、そこで作業を止めました。移動元と移動先を合算した値(1,053字→1,629字、+576字)も併せて報告しており、TAKTの振る舞いは正しく、原因は指示文の側にありました。

以降、この種の作業には次の一文を必ず添えています。

移動元と移動先を合算した文字数で判定してよい。合算しても減少していたら止まること。合算で判定した場合は、その旨と合算値を必ず報告すること。

同じ構造の例外は、識別子をより短い名前へ改名する場合にも起きます。ある回では既存の識別子をより短い名前に改名する作業で、1つのハンクが必ず数字減りました。ここでもTAKTは独自に例外扱いをせず、「形式上は止まる条件に該当する」とだけ報告し、例外にするかどうかの判断は人に委ねました。

この設計は意図的です。 「事前に算出できる減少だから、自動的に許容してよい」という判断を、ひかり(対話AI)が独自に行うことはしません。TAKTには止まる条件として淡々と報告させ、例外にするかどうかは、必ず人が判断します。

5. 「0件であること」が、正しい記述を止めることがある

もう一つ、頻発したパターンがあります。「〇〇が0件であること」という絶対条件が、編集前から存在していた正当な記述に反応して止まるというものです。

課題管理表の更新スクリプトで、ある列の値を絶対条件(件数がゼロ)で検証したところ、編集前から同じ形の記述が存在する項目にひっかかり、書き込み前に停止しました。同種の再発は、記録されているだけで通算7回起きています。日付の記述、識別子の名称、件数のスナップショットなど、是正の記述そのものが言及する語は、過去の記録として既に文書内に存在しうるのです。

対処の一般形はこうなります。

検証条件は「編集前後の集合が一致すること」で書く。「0件であること」ではなく「編集前と同じ集合であること」。数ではなく、どの行かを突き合わせる。

絶対条件(件数がゼロ)をどうしても使いたい場合は、「編集前が0件であったこと」自体を、独立した検証項目として先に置きます。 前提を検証項目として明示すれば、絶対条件も安全に使えます。

6. 完了確認は、指示文の他の部分と衝突することがある

最後に、完了確認そのものではなく、完了確認と、指示文の他の部分が矛盾するケースを挙げます。

ある回で、「旧記述の逐語保存を確認する」という条件と、「表はセルごとに打ち消し線で囲む」という指示を同時に出したことがありました。この2つは両立しません。表のセルを個別に打ち消し線で囲むと、Markdownの表としての描画が壊れ、結果として「逐語保存」の確認方法自体が崩れるためです。

TAKTは、止まる条件を優先して行まるごとを打ち消し線で囲む方式を採り、その代償(表としての描画が失われること)を明記して報告しました。 矛盾する指示の中では、最も正しい処理だったと考えています。

完了確認の項目は、指示文の他の部分と1つずつ照合してから書きます。 これが定型化した理由です。

7. まとめ——完了確認は仕様書の裏側である

11項目まで積み上げても、その大半は「行の分割」というたった一種類の破損を検知するために追加されたものです。そして条件を厳しくするたびに、今度は正常な作業を止めてしまう副作用が生まれ、その都度、例外を明文化してきました。

完了確認とは、結局のところ仕様書の裏側にあります。 仕様書が「何を直すか」を書くものだとすれば、完了確認は「直した結果が、意図しない形に壊れていないか」を機械的に問うものです。そしてその機械的な問いかけ自体も、書き方を誤れば正しい作業を止め、あるいは誤った作業を通してしまいます。両方とも、実測してから書く以外に近道はありません。


次回(第5回):バックアップと復旧——git checkout を使わせない理由と、git worktree の使いどころ。壊れたときに、どこまで戻れるようにしておくかという設計を書きます。

動画作成には自信があります!

AIの報告を、そのまま信じないための検証術

AIの報告を、そのまま信じないための検証術

「テストは全件成功しました」という報告と、実際に全件が実行されたかどうかは別の事柄です。前々回、そう書きました。第3回は、その差をどう埋めているかという話です。

結論を先に書きます。検証コマンドそのものが誤っていることのほうが、TAKTの報告が誤っていることより多いのです。

1. 「0件でした」は、実行できていないことの言い換えかもしれない

型チェック(tsc --noEmit)のエラー件数を grep -c 'error TS' で数え、「0件」という結果を得たとします。安心していいでしょうか。

安心できません。 tsc がそもそも起動していなくても、エラー行は0件になります。「エラーが無い」と「実行できていない」を、件数の確認では区別できません。

実際にこの環境では、npx tsc --noEmit を実行すると npx という名前の npm スクリプトへ解決されてしまうという癖がありました。TAKT の側からも「npx tsc は shim に阻まれて実行できない」という報告が出ていたにもかかわらず、grep -c による確認では「0件」という、矛盾に気づけない結果が返ってきました。

対処は、件数ではなく終了コードで判定することでした。

npm --prefix backend run build   # 終了コード0を確認する
npm --prefix frontend run build

npm run buildtsc を含み、型チェックとビルドの成立を同時に確かめられます。判定は必ず exit=0 かどうかで行い、エラー件数の grep は判定に使いません。

2. 報告の数字には「範囲」が付いていないことがある

TAKTが「12.6節の3エンドポイントは5件」と報告したとします。この「5件」を、そのままファイル全体の期待値として書いてよいでしょうか。

よくありません。 実際にこの数字をそのままファイル全体の期待値として書いたところ、実測は6件(節内5件+節外1件)でした。誤っていたのは期待値の側であり、TAKTの報告は最初から正確でした。「節内で5件」なのか「ファイル全体で6件」なのか、スコープを書かずに数字だけを転記したことが原因です。

これ以降、期待値には必ず範囲を添えています。「12.6節内で5件」「ファイル全体で6件」のように、どこを数えた数字かを明示します。

3. grep -c が向いていない場面がある

検証コマンドの誤りは、思わぬところに潜んでいます。実際に踏んだ落とし穴を3つ挙げます。

落とし穴何が起きるか
grep -c "文字列" で1行あたりの出現回数を数える1行に複数回現れても「1行」としか数えない。実測は str.count() を使う
grep -c で「削除されたか」「網羅されているか」を判定する削除済みの語がコメントや注記に残っていれば0件にならない。該当行を表示して目で確かめる必要がある
行頭を ^ で始まる正規表現で数える引用行(> で始まる行)やインデント行を取りこぼす

さらに、「文字列を含む行数」と「その文字列が処理されている箇所の数」は別物です。 ある回では、「403エラーは2箇所」というTAKTの報告から grep -c "ACCOUNT_DISABLED" の期待値を2と書きましたが、実測は5でした。内訳は返却が2、JSDocコメントが1、通常のコメントが2。grep -c は文字列を含む行を機械的に数えるだけで、それがコード上どの種類の記述かは判別しません。

これらはすべて独立した事例として起きましたが、共通する結論は一つです。実測が期待値と食い違ったとき、まず疑うのは検証コマンドの側です。

4. 負の結論は、検索の書式を変えてから述べる

grep -ln "vi.mock('react')" を実行し、0件という結果を得たとします。「このテストの書き方は存在しない」と結論づけてよいでしょうか。

よくありません。 実際にこの検索で0件だったため、そう述べかけたことがありました。しかし書式の揺れ(引用符の種類・スペースの有無など)を含めて検索し直すと、対象のコードベース全体で60件以上見つかり、当初参照していた該当ファイルも実在しました。

「存在しない」「前例がない」という負の結論は、一度の検索だけで述べません。 別の書式で再検索してから述べます。これは前節の原則の具体化でもあります——検索条件そのものが、見たいものを取りこぼしていないかを先に疑います。

一覧を絞り込んで確認するときも同様の失敗が起きます。マイグレーションファイルの一覧を grep '006' で絞り込んだところ、0070 が視界から外れ、TAKTの「既存の最大は0070」という報告を誤って疑ったことがありました。実測すると0070は実在し、TAKTの報告は正しかったのです。絞り込みの条件が、見たいものそのものを除外していないかを確かめます。

5. すべてを検証し直すわけではない

ここまでの原則は「報告値を無検証で期待値へ写さない」という話であって、報告のすべてを追試するという意味ではありません。 TAKTが実行して結果が出ているコマンドを、こちらで流し直しても同じ結果が出るだけで、往復が増えるだけです。

検証する範囲は、次の3つに限定しています。

対象
報告に書かれていない事項「警告メッセージが2種類あるか」が報告に無い
TAKTが「未確認」と明示した事項「ライブラリの挙動は未確認」と書かれている
結論を左右する数値(期待値へ転記するもの)件数・行番号・イメージID

TAKTが実行済みのコマンド(型チェック・テストの実行)は再実行しません。

この線引きには経緯があります。ある回で、build-stepの報告に対して7ブロックもの検証コマンドを提示したことがありましたが、大半はTAKTが実行済みの内容をなぞる追試でした。「TAKTと同じテストを実施し、当然同じ結果という繰り返しが続いている」という指摘を受け、手を動かすのは人であり、工程数は人の実行回数で数えるという考え方に是正しました。この考え方は第7回で改めて扱います。

6. まとめ——疑うべきは、報告ではなく確認手段

この回で挙げた事例に共通する構造は、実はどれも同じです。AIの報告が誤っていたのではなく、それを検証するために書いたコマンドの側が、数えたいものと違うものを数えていました。

grep -c は行数を数え、str.count() は出現回数を数えます。^ で始まる正規表現は、引用符やインデントの前に別の文字がある行を取りこぼします。件数の一致は、実行できたことの証明にはなりません。それぞれ、道具の性質を忘れて「数字が合えば正しい」と早合点したところに誤りが生まれています。

検証術とは、結局のところ「何を数えているか」を常に問い直す作業です。


次回(第4回):壊れる前に止める——完了確認11項目の設計。誤りが実害になる前に検知されている理由を、書き込み前の自己検査という仕組みの側から書きます。

動画作成には自信があります!

PAGE TOP