コンテンツにスキップ

HT001 · ターミナル IDE をゼロから書く

2026-09-07、ある agent が flower の下で macOS ターミナル上で動く C/C++ 単一ファイル IDE をゼロから書き上げた。 連続 10.4 時間走り、$171.62 を使い、12,212 行のプロダクトコードと 13,291 行のテストを提出した。 途中で一度ネットワークが切れ、自力で続きを走り切った。このページは成果発表ではない —— フレームワークの各主張を現実規模で一つずつ測ったものであり、 測った結果、成り立たなかった二つも含む:コーディネーターが「権限制限をしばしば無視する」というのは偽で、そして自分自身に対する受け入れ判定のうち一つを誤判定していた。

完全な記録(19MB の transcript を含む)は ChenyuHeee/cppide にある。 以下の数字はすべて tools/analyze_run.pysessions.db から算出したもので、再現可能:

python tools/analyze_run.py /path/to/cppide/runs

このページの数字はどこから来たか

HT001 の runs/ は上記の cppide リポジトリにあり、flower リポジトリの中にはない。したがって本ページの数字は 当時の run 記録からそのまま転記したもので、このページを書く際に sessions.db に対して再計算はしていない —— 再計算するには、まず cppide を clone し、上のコマンドを走らせる必要がある。 HT002 は逆で、生の記録が本リポジトリにあり、あのページの数字は一つずつ再計算済み、 合わなかった二箇所はそのままページに書いてある。

この run の形

項目
タスク 「macOS ターミナルで動く C/C++ 単一ファイル IDE を作る、ICPC 練習用」
コスト $171.62 —— 要件確認 $0.3704 / 5 ターン、作業 $171.2476 / 31 ターン
所要時間 0.06h + 10.44h
main thread のコンテキスト 28.7K → 185.9K、70 モデルターンで単調増加、全期間 compact なし
分担 コーディネーターのツール呼び出し 32 回 vs subagent 1,893 回;本文字数の 94.8% が subagent 側
キャッシュ 総入力 299.4M token、ヒット率 96.1%
成果物 src/ 12,212 行、tests/ 13,291 行(約 3,000 アサーション)、README 44K
バージョン goal guard追加する前の flower、したがってこの run には毎ターンの verdict がない

このページには 2 種類の「ターン」がある

表の 5 ターン / 31 ターンruns/manifest.json が記録した num_turns で、step ごとに 1 つの数値; 下のコンテキスト曲線の「第 70 ターン」が数えているのはmain thread 上のモデルターン、つまり assistant メッセージ 1 通ごと。 両者は別のものを数えているので、互いに換算しないこと。


一、コンテキストの経済学:現場の 94.8% は main thread に入らなかった

これはフレームワークの中核的な主張だ —— コーディネーターには意思決定だけを載せ、 現場は subagent とディスクに沈める。仕組みそのものはコンテキストの経済学を参照。

main thread subagent subagent の割合
モデルターン 70 3.0K 97.7%
本文字数 200.1K 3.6M 94.8%
ツール呼び出し 32 1,893 ——

この run では 23 個の subagent を送り出した。出力 token は最大 184,377、中央値 72,197、最小 11,684。

1,893 回の実作業ツール呼び出し(1,034 Bash / 471 Read / 258 Edit / 126 Write)のうち、コーディネーターの視界に入ったのは 32 回だけ —— 59:1。 一人送り出すごとに平均 82 回のツール呼び出しが見えないままだった。

以前の小規模テストでは 83% だったが、現実規模では 94.8%。規模が大きいほど分担の利得は大きい —— task brief と報告のオーバーヘッドは送り出しごとに固定であるのに対し、 外に締め出される現場はタスクの複雑さとともに増えるからだ。

二、コンテキスト曲線:「long-horizon」の上限がどこにあるかを初めて知った

各 assistant メッセージの input + cache_read + cache_creation が、そのターンでモデルが見たコンテキストだ:

第 1轮   28.7K   ← 起動フロア(以前の実測は約 34k、一致した)
第20轮   35.2K
第35轮  108.6K
第50轮  158.2K
第70轮  185.9K   ← 終了
  • 70 ターン単調増加、傾きは約 2.2K/ターン
  • 全期間compact は一度も発生していない(DISABLE_AUTO_COMPACT=1;唯一の落ち込みはネットワーク切断ターンの空レコード)
  • 1M ウィンドウの 18.6% を使用

この傾きで外挿すると、壁にぶつかるのは約 440 ターン。 今回は 70 ターンで 12K 行のコードを産出したので、 現在の形態における long-horizon の実際の上限は今回の約 6 倍。 これは意思決定に使える数字であり、以前は推測するしかなかった。

これを突破するには、main thread の増加を劣線形にする必要がある —— より多くをディスクに沈め、より少なくを対話に残す。

三、キャッシュヒット 96.1% —— $171.62 が成立するのは全部これのおかげ

総入力 299.4M token
  キャッシュヒット  287.5M  (96.1%)
  キャッシュ新規作成 11.8M
  未キャッシュ        6.2K
出力 2.3M(うち thinking 653.9K)

299M の入力 token が $171 で済んだのは、96% がキャッシュ価格で通ったからだ。

帰結:long-horizon な run の経済性はキャッシュヒット率の上に成り立ち、キャッシュヒット率はコンテキスト接頭辞の安定性に依存する。 「コンテキストを並べ替える」たぐいの最適化 —— 古い内容を退避する、前方に新しいシステムプロンプトを差し込む —— はいずれもキャッシュを貫通させ、 節約した token は失ったキャッシュ割引にはるかに及ばない可能性がある。これは以前に測ったことがなく、いま数字がある。

これは flower が compact ではなくhandoffを使う付随的な理由でもある: handoff は新しい session を開いてキャッシュを一から作り直すもので、一世代の中では接頭辞が常に安定している。

四、workbench の再利用:61 個のスクリプト、書き捨てはゼロ

workbench の主張は「スクリプトは一度書けば、以後はそのまま走らせ、書き直さない」だ。 検証方法は agent に尋ねることではなく、.flower/scripts/ のスクリプトが transcript 内で Write/Edit された回数と Bash で実行された回数を数えることだ:

61 個のスクリプト:95 回書かれ、331 回実行された
実行回数 > 1(実際に再利用された):56 個、92%
書かれたが一度も実行されなかった:0 個
実行/書き込み比 3.48

wave5-verify.sh は 5 回書かれ、45 回走った —— 反復して磨き上げ、それから繰り返し使う、まさに望んでいた形だ。 書き捨てられたスクリプトは一つもない。

五、コーディネーターの実作業が拒否されたのは 1 回、しかもホワイトリスト側の誤り

着手前の疑いはこうだった:「コーディネーターは権限制限をしばしば無視し、大量の拒否呼び出しを引き起こす」。実測はこの前提を打ち砕いた。

32 回のツール呼び出しのうち拒否 1 回(3.1%)、しかもその 1 回はこれだ:

which g++ clang++ make pkg-config 2>&1; echo ---; ls /usr/include/ncurses.h 2>&1; echo ---; ls /work/runs

区間ごとの判定:echo は許可、ls は許可、which は遮断 —— ephemeral command の表になかった

コマンド全体は純粋に読み取り専用で、標準的な環境探査だ。モデルの挙動は完全に正しく、ホワイトリストが不完全だった。 そのせいで which g++ 一行のために subagent 起動コスト(約 4.3k token)を一度払わされた。

修正済み:which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers を追加。commandtype問い合わせ形式のみ許可 —— 裸の command rm -rf / は実行であり、-v を書き落とせばそれは裏口を開けるに等しい。 tests/glance.py に 7 本の敵対サンプルが番をしている。

ここで最も記録に値するのはそのパッチではなく、方法だ。 長く続いていたアーキテクチャ議論の出発点の前提が、実データでは 1/32 で、 しかも帰属が逆だった —— 問題はモデルではなく、ホワイトリストにあった。まず測れ。

六、ネットワーク切断からの再開:実障害で初めて検証された

01:52:40  tool_result: "Agent terminated early due to an API error"   ← 一つの subagent が中断された
01:55:41  assistant:   "API Error: ENOTFOUND"                          ← main thread が受信
          ↓  プローブがぶら下がったままネットワーク復旧を待つ
          resumed=True、同じ session で継続、さらに 8 時間以上走って完了

manifest には attempts=2 / resumed=True / ok=True と記録されている。10 時間の作業を最初からやり直さずに済んだ。 これ以前、resilience はずっと「未検証」に置かれていた —— ネットを落とすには sudo で hosts を書き換える必要があり、本番の run 中にそれをやりたい者はいない。

その合成された "API Error" メッセージは PruningSessionStore が load 時に取り除いており、モデルはそれを見ていない —— prune の設計も併せて実障害で検証された。

露呈した二つの問題(issue #2 を参照):

  1. 復旧がフレームワーク層で起きており、トランスポート層ではない。代償はコンテキスト全体の再送であり、あのときのコンテキストは大きかった。
  2. 中断されたその subagent の作りかけの作業は失われた。 resume が救うのは main thread であり、 subagent 単位の中断復旧は別の問題だ。

七、その自己検証は本物か

ここが最も疑うべきところだ:agent が自分でテストを書き、自分で走らせ、自分で合格を宣言する —— 形だけの儀式に容易に堕する。

事後に独立した監査 subagent を送り出し、58 本のスクリプトと 22 件の成果物を静的に読み切らせた。結論は本物、しかも厳格さは希少だった:

  • 恒真アサーションは 0 箇所。 リポジトリ全体に [ 1 = 1 ] の類のパターンはない。
  • || true は計 8 箇所、7 箇所は正当(grep -c がマッチなしで 1 を返す、ulimit のフォールバックなど)。
  • 判定しているのは振る舞いであって、ファイルの存在ではない。 audit-idle-cost.sh:53 は IDE を pty 内で 60 秒アイドルさせ、 ローカルの偽サーバーが実際に受けたリクエスト件数を数え、-le 2 で初めて PASS。fix-p1-idle-verify.sh:100-110 はさらに進んで、 アイドルがちょうど 1 回であることをアサートし、かつ ghost が依然として表示されることをアサートする —— 「レート制限の変更が機能ごと殺してしまう」のを防ぐため。
  • 本当に子プロセスを起こす。 wave5-pty-drive.cpp:316forkpty で実際に TUI を走らせる;audit-fake-ai-server.py は本物の socket サーバーで、/err500 /err401 /badjson /drop /slow の 5 種の障害注入をサポートする; すべての AI 経路で CPPIDE_API_KEY を強制的に空にしている。

予想を超えたことが二つある。

自分の検証器に対して陽性・陰性コントロールを作った。 验收审计.md:29-34:まず ZZZ_NEVER_APPEARS_ZZZ を食わせてドライバが FAIL することを確認し、 次に本物の文字列を食わせて PASS することを確認する —— 「常に PASS する偽の装置で監査する」ことを避けるためだ。これは検証器そのものを検証している。

自分の弱いアサーションを自ら開示した。 wave5-b4-ai-states.sh:114for m in $must"AI 错误 请求超时" を空白で 3 つの部分文字列に分割してそれぞれ grep するが、AI错误 はどの画面にも必ず現れる —— 名目上は「3 条通過」だが、実際に識別力があるのは 1 条だけ。これは agent が自ら 验收审计.md:576-582 に書き込んだものであり、 監査員が掘り出したものではない。

同種のものとして:wave5-verify.sh:296-302 は 7 行のコメントで B3b のあの一条がなぜ SKIP に降格したかを説明し (最終画面のタイミングアサーションであり、プロダクトコードとは無関係)、「判定ロジックと許容範囲は一字も変えていない」と付記している; :236-238自分が以前書いた偽アサーションを発見して修正したことを記録している —— if pgrep ...pgrep のない軽量イメージでは黙って else に落ち、常に PASS になっていた。

再利用の定性的証拠は第四節の数字と一致する:audit-fake-ai-server.py7 本のスクリプトで再利用され; wave5-pty-drive.cpp5 本以上のコンパイルで再利用され、[ -x "$DRIVE" ] || でコンパイル結果をキャッシュしている; wave4 → wave5 のドライバは書き直しではなく拡張だ(diff は 1 行だけ変更、343 行追加、wave4 の 281 行はそのまま保持)。

これらの振る舞いはすべて goal guard のないバージョンで起きている。 唯一の制約は brief にあった 10 条の判定可能な受け入れ基準だ。これが示すのは:「完了」を判定可能な形で書くこと自体が、自己監査と自己開示を誘発するということだ。 goal guard の限界価値はこれをベースラインにして測るべきで、ゼロを基準にしてはいけない。

しかし次節が示す:自己監査は検証技術としては非常に強いが、最も重要な一条の最終判定を誤った。

ここでいう「監査員」は judge ではない

第七、第八節でいう独立監査員は、コーディネーターが自発的に送り出した subagent であって、 goal guardjudge ではない —— この run にはそもそも goal guard がない。両者は読み味が似ているが、やることは違う:監査員は事後の振り返りであり、 judge は各ターン終了時の verdict ゲートだ。

八、自分でつけた点数は正しいか —— 受け入れ 1 は誤判定だった

さらに二人目の独立監査員を送り出し、あの 10 条の受け入れ基準をコードと一条ずつ突き合わせて判定させた。

受け入れ 2–10 はすべて事実で、コードの証拠が揃っている。練習モードでコードを挿入しないことに至っては構造的保証だ: 唯一の sink 決定点 ai.h:117 + 唯一の着地 switch app.cpp:590-615 + 4 本の early-return editor.cpp:679-683、散在した if ではない。自己監査で見つけた P1–P4 もすべて事実、修正もすべて着地済み (P4 は半分事実:あの青い pair の行はデッドコードで、ghostAttr() はもともと A_DIM を通っていた)。

しかし受け入れ 1 は誤判定だった。

標準原文:  "编译出一个独立可执行文件,在 macOS 终端直接运行"
納品された成果物: ELF 64-bit LSB pie executable, ARM aarch64, GNU/Linux
その判定:  通過

この一条は人間が自分の手で file を走らせて確認した。

隠していたわけではない —— 交付说明.md §4 の見出しはそのまま「最大の未検証リスク:macOS で一度も走らせていない」であり、 監査 §6 も macOS 側の未検証な 6 面を挙げている。リスクは開示したが、その事実を受け入れ 1 の判定に接続し直すことは一度もなかった。 そして受け入れ 1 の文面上の要求は、まさにそのことだったのだ。

だがこれは「独立した judge が必要だ」という証拠ではない

これを「だから goal guard が要る」と読み流すのは簡単だ。それは違うし、そう読むこと自体が先に矢を射て後から的を描くことだ —— まさに前節で批判したばかりの動機づけられた推論だ。

当時のバージョンの goal guard は、デフォルト設定ではおそらくこの一条を見逃した。 judge() はデフォルト can_run=False、 ツールは Read / Glob / Grep のみ —— file を走らせられない。 ソースを読みに行き、Makefile:25-38 に確かに Darwin 分岐があるのを見て、「達成」と判定しただろう。自己監査と全く同じ失敗だ。

本当の構造的事実

受け入れ 1 は、この run の環境ではそもそも検証不可能だった。

それが主張しているのは「成果物の macOS 上での振る舞い」であり、run 全体は Linux コンテナの中にあった。 どんな judge であれコンテナ内で macOS バイナリを検証することはできない、自己監査であれ独立であれ。 問題は「誰が判定するか」ではなく、この一条が普通の判定可能な基準として扱われ、そして判定されたことにある。

監査員がさらに掘り出した三点(自己監査はいずれも触れていない)

  • doTab() のクォータ意味論:app.cpp:909acceptGhost() の後に無条件で noteActivity()gen_++ → クォータを開き直す。その結果、Tab で受け入れるたびに 500ms 後にもう一度自動リクエストが発火する。P1(アイドルでの課金)の分析は setMode / cancelInFlight / askNow の 3 つのキャンセル点だけを見ており、この最も高頻度の課金経路を見落としていた。
  • 受け入れ 6 の「グレー」は 256 色ターミナルでのみ文字通り成立する;8 色レンジでは A_DIM(暗くする、グレーではない)に固定されている。
  • 验收审计.md §7 の小見出しは「9 条の受け入れ基準」と書いているが、表には 10 行ある —— 誤記だが、総評の表頭に出ている。

九、spill と workbench の最終規模

103 回の spill、791.4K 文字がパスのポインタに置き換わり、コンテキストに常駐していない。

.flower/ の最終状態:58 本のスクリプト(250K)、22 件の成果物(330K)、5 件のノート(43K)。

残存リスク

納品ターゲットは macOS だが、検証はすべて Debian aarch64 コンテナ内で完了しており、macOS 実機での検証はゼロ。 これはコンテナ isolation 方式の実際の副作用だ:安全のために Linux に閉じ込め、しかし納品対象は macOS。 プロジェクト自身の README §10.24 はこれを正直に記録し 4 つのリスク点を挙げているが、閉じてはいない。

このギャップは後に HT002 が埋めた —— あの run は、まさにこのバイナリを実際に macOS にインストールするものだ。

意外な利点:コンテナ内からは /work しか見えないため、19MB の transcript の中で ホストのユーザー名 /Users/hechenyu の漏洩はゼロ(/work は 11,495 回出現)。 isolation はついでにパスも隔離した。

この run で検証できなかったこと

  • goal guard:この run はそれを追加する前だ。コーディネーターは自発的に独立監査を組織し、検証技術は非常に硬かったが、 最も重要な一条で誤判定した(第八節)。二つの結論を併せて記録すべきだ —— brief に判定可能な基準を明記するだけで自己監査は誘発できる、だから goal guard のベースラインはゼロではない; そして当時のデフォルト設定の goal guard もあの一条を見逃した、だからそれも万能薬ではない。
  • 1M ウィンドウでの振る舞い:18.6% しか使っておらず、境界に触れていない。
  • DISABLE_AUTO_COMPACT=1 下でのマイクロ compact の振る舞い:全期間 compact が発火せず、依然として推測のまま。

この run がフレームワークに何を変えさせたか

発見 着地した変更
which が遮断された(第五節) is_ephemeral()which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers を追加;tests/glance.py に 7 本の敵対サンプルを加え command / type の問い合わせ形式を守る
受け入れ 1 の誤判定(第八節) JUDGE_RULES の第 1 条を、判定対象はソースではなく成果物に変更 —— 走らせられるなら走らせ、走らせられない場合も成果物そのものを調べる(プラットフォーム、サイズ、ロードできるか)
「リスクは開示済み」が通過と引き換えになった 判定は「達成していない」と「ここでは検証できない」を区別しなければならない。Verdict.parse は現在、検証不可 / 検証できない / 判定不可を第三の状態 unreachable として認識する → 人に上げて問う、通過の判定は絶対に許さない
受け入れ 1 はこの環境では原理的に検証できない 目標を設定するステップで、この環境では検証できない項目をその場で明示することを要求する。作業を終えてから気づくのでは遅い
ネットワーク復旧がフレームワーク層、subagent の作りかけが失われる(第六節) issue #2 として記録、未クローズ

最初の三条は HT002 で初めて実 API に乗り、そのうち「ソースではなく成果物を判定する」はその場で一度救った —— しかし同じルールが新しい失敗も一つ生み出した。