HT002 · macOS に入れて動かす¶
2026-09-07、タスクは一言:cppide を clone してきて、 この macOS 上でインストールし、動かす。 結果は $38.24 / 4 ステップ / 約 1 時間、 プログラムは確かに macOS 上で起動した(PID 96040、arm64 Mach-O)が、 判定の結論は到達不能で、人に上げ、人が受け入れを選んだ。
これは初めて目標看守を伴ったrun である —— HT001 の教訓から作り直したもので、その日すべて初めて実 API に乗せた。 HT001 が踏んだ穴を一つ救った一方で、方向がまるで逆の新しい失敗を作り出した: git clone && make && ./cppide を1時間の仕事にしてしまった。 このページで最も価値があるのは後者だ。
生の記録は human-test/HT002/ にあり、 勘定は runs/manifest.json と runs/sessions.db から算出している:
この run の形¶
| 項目 | 値 |
|---|---|
| タスク | cppide を clone し、この macOS 上でインストールして動かし、人が実際に画面を開いて使える状態にする |
| 費用 | $38.24 —— 4 つのステップ、内訳は下記 |
| 所要時間 | 約 1 時間(合計 0.97h) |
| ターン数 | 70(4 ステップの num_turns の和) |
| メインスレッドのコンテキスト | 36.8K → 80.2K、ステップ境界で断崖状に落ちる。単調増加ではない |
| 分担 | 6 個の subagent;コーディネーターのツール呼び出し 7 回 vs subagent 212 回 |
| キャッシュ | ヒット率 90.8% |
| ワークベンチ | 成果物 5 件、ノート 4 件、spill 11 件、再利用可能スクリプト 0 個 |
| 結果 | 起動した(PID 96040、arm64 Mach-O);判定は到達不能、人が受け入れを選択 |
| バージョン | 初めて目標看守を伴った run |
各ステップの費用・ターン数・所要時間¶
| ステップ | 費用 | ターン数(num_turns) | 所要時間 |
|---|---|---|---|
| 要件確認 | $0.5306 | 9 | 0.10h |
| 目標設定 | $0.4117 | 12 | 0.05h |
| 実作業 | $35.8950 | 12 | 0.73h |
| 実作業・判定#1 | $1.4037 | 37 | 0.09h |
| 合計 | $38.2409 | 70 | 0.97h |
所要時間の列は各ステップを四捨五入してから足したもの。生の記録から計算し直すと、manifest.json の 4 つの duration_s の正確な合計は 3471.53 秒 = 0.9643h;本ページは 0.97h を踏襲し、両者の差は 0.006h。
金はほぼ全部「実作業」のステップにある —— $35.8950 で 93.9% を占める。 次の第一節で、この金がどこに消えたか、そしてなぜ本来こう消えるべきではなかったかを説明する。
一、ヘッドライン:git clone && make && ./cppide を1時間にした¶
これが今回いちばん重要な発見で、以下のすべての「機構が通った」より重要だ。
目標設定のステップで、判定者は自分に 15 条の判定チェックリストを書いた (原文は .flower/notes/ 下の 目标.md)。 性質で分類すると:
| 分類 | 条数 | 例 |
|---|---|---|
| 本当に「動くか」を検証している | 約 5 | ビルド exit 0;プロセス生存 ≥30 秒;再起動コマンドが新しい shell で再実行できる |
| 「過程が規則を守ったか」の検証 | 6 | ~/.zshrc / .zprofile / .bash_profile の mtime がすべて今回のセッション以前;brew install / npm -g / sudo を実行していない;.flower/ の mtime が変わっていない、runs/ が人為的に増減していない |
| 原理的に検証できない | 4 | スクリーンショットを撮って目視;devtools console;人手クリック;本人確認 |
「これが本当に使えるか」を検証しているのは3分の1だけ。 6 条は自分が規則を守ったかの検証で —— そのうち1条が調べているのは .flower/ ディレクトリの mtime、つまりフレームワーク自身のディレクトリだ。
.flower/ を調べるあの条項が、自分で自分を不合格にした¶
これは理屈の話ではない。判定のラウンドで、判定者は「未通過」に正直にこう書いた:
.flower/下のファイル mtime がすべてセッション開始より前 —— 文字通り失敗:INDEX.md16:14、artifacts/01–05(15:40–16:12)、notes/决策-项目形态与验收路径.md15:43 はいずれもセッション内に書き込まれている。
原因はこうだ:フレームワークは長い成果物を .flower/artifacts/ に、決定を .flower/notes/ に書くことを必須にしている。 判定チェックリストのあの1条はこのディレクトリを触るなと要求している。二つの規則が真正面から衝突し、しかもそれを作ったのは判定チェックリスト自身だ —— ワーカーが長い成果物を1件も書かなければ、むしろ準拠していたことになる。
ごまかしはしなかった:実作業ステップは最終報告でこの衝突を自分から挙げ、 決定記録にも対処方法を書いた —— 既存ファイルは一つも触らない (需求.md、目标.md、问答记录.md の3件は前後で mtime と MD5 が一致)、新規追加のみ。 判定者は再確認のうえ「この条項が防ごうとした改竄は起きていない」と同意したが、それでも規則どおり未通過に列挙し、人に委ねた。
根本原因その1:境界が判定項目にされた¶
前置確認の段階で聞き出した境界(「プロジェクトディレクトリ内でしかインストールしない」「業務コードは触らない」)は 本来どう作業するかを制約するものだったのに、成果物を検証するチェック項目として書かれてしまった。 その結果 .zshrc の mtime を調べる、.flower/ の mtime を調べる、といった条項が生まれた。
境界 ≠ 判定項目。境界は過程を制約し、判定項目は結果を検証する。 混ざると、境界を1条足すたびに判定が1条増える。 そして境界はまさに確認段階で書き込むことが奨励されている —— CLARIFIER_RULES にはこう明記されている: 「何をしないかを明確にする。この一段が、後段で作業する全員を縛る」。
生の記録を対照すれば、この増幅チェーンの各段が見える: 要件確認書の境界は「カレントディレクトリ下の既存の .flower/ と runs/ に触れない」だったのが、 判定チェックリストでは「.flower/ 下のファイルの mtime がすべて今回のセッション開始より前」に格上げされた —— 「既存のものを触るな」が「ディレクトリ全体に新規ファイルを作るな」に変わった。
根本原因その2:JUDGE_RULES の圧力が一方向しかない —— しかもそれはその日に足したもの¶
その日の早い時間、HT001 の教訓から JUDGE_RULES に次を書き込んだ:
- 「各条はその場で検証できなければならない」「欠けているもの、曖昧なものは、お前が補え」
- 「判定するのは成果物であって、ソースコードではない」(macOS のあの反例つき)
- 「今の環境で検証できない条項は、その場で印をつけろ」
- 「まず自分がどこにいるか見極めろ(
uname -aでよい)」
どれもチェックを増やし、より厳格にする方向に押している。規則全体に「タスクの規模に見合わせろ」という文は一言もない。
つまり:HT001 の「過剰に通過を宣言する」失敗に合わせて最適化したら、まるで逆の失敗 —— 過剰検査を作り出した。 これは自分で作ったものであって、モデルの問題ではない。
根本原因その3:issue #3 と重なった¶
実際に詰まったのは macOS 上の2箇所の可搬性の欠陥だ:
src/proc.cppが::sigemptysetを使っているが、Apple SDK は関数プロトタイプの後にそれを関数形式マクロとして定義しており (#ifndef _ANSI_SOURCEでしか守られていない)、::sigemptyset(&s)が::(*(&s)=0,0)に展開されて構文エラーになるsrc/ui.cppが::_exitを使っているのに<unistd.h>を include していない
3行で直る。 しかしユーザーは確認段階で「業務コードは触らない」と答えており、agent は厳格に実行した。 そこでコンパイル flag を試したり、subagent を派遣したり、決定記録を書いたりと迂回した。 もし途中で「その2行は直していい」と一言挟めていたら、10分で終わっていた。 境界が禁令として書かれると最大限に厳格に解釈され、途中でそれを緩める通路がどこにもない。
生の記録を見れば、この迂回がどれだけ長かったかが分かる:阻害点は全部で4つ、 そのうち純粋なコンパイル flag で解けるのは ::_exit のものだけ;::sigemptyset の方は 6 組の -D/-U の組み合わせを試して全滅 (_POSIX_C_SOURCE 系はさらに st_mtimespec と O_CLOEXEC まで壊す)。 この行き止まり自体が 13.6K の実現可能性レポートを1本産んだ。
ただし公平に:境界がより良い解を絞り出した¶
最終案はソース改変ではなく、Makefile 内で #undef だけを行うシムヘッダを生成し、-include で強制前置するものだった:
$(MACSHIM):
@printf '#include <signal.h>\n#undef sigemptyset\n#undef sigfillset\n...' > $@
CPPFLAGS += -include $(MACSHIM) -include unistd.h
これはソース改変より良い —— 上流リポジトリを1行も触らずに済み、誰が clone してもビルドできる。シムは build/ に生成してリポジトリに入れず、 clean に rm -rf build を足して回収し、make -n UNAME_S=Linux を実測して展開後に -include が無いこと、Linux 経路が汚染されていないことを確認した。 最終的な git diff は Makefile 1ファイルのみ、+14 / −2。
つまり境界が生んだのはより高コストなより良い解だ。問題は間違ったことをした点にではなく、 このトレードオフを人に選ばせなかった点にある —— そして選ばせられない理由がまさに issue #3 だ。
なぜ git status に 64 件あるのにソース改変ゼロと言えるのか
cppide のリポジトリは 30 個の .o、30 個の .d、2 個のバイナリをバージョン管理にコミットしている。しかも Linux ELF aarch64 で、 mtime も .cpp より新しい —— 先に make clean しないと、make はコンパイルを飛ばして ELF をそのままリンクしにいき、必ず落ちる。 だから git status --porcelain の 64 件のうち、63 件は git が追跡しているコンパイル成果物で、残り1件が ?? build/ だ。 拡張子でフィルタすると git diff -- 'src/*.cpp' 'src/*.h' README.md config.sample.json | wc -c は 0。 この正確な言い方はコンプライアンス監査が絞り出したものだ —— コーディネーターは最初ざっくり「src/ は改変ゼロ」と書こうとし、監査がその文は成立しないと指摘した。
二、目標看守が正しくやったこと¶
機構側は全部通り、しかもその日の2箇所の変更が成果物に直接の指紋を残した。
それが書いた判定チェックリストの第1条:
uname -aは Darwin 25.2.0(macOS)を示す……以下すべての「起動する/開く」の条項は、このマシンで 実際に走らせて得た結果でなければならない。「ソースに macOS 分岐があるから動くはずだ」といった推論は受け付けない
これはまさにその日の朝、HT001 の教訓から JUDGE_RULES に書き込んだ文言だ。
その日に追加した [此环境无法验证:…] の印を3回使い、それぞれ理由を付けた —— devtools console(駆動できるブラウザが無く、そのためにヘッドレスブラウザを入れるのは境界を越える)、 人手クリック(人が画面の前にいる必要がある)、本人確認(本人しか出せない)。
判定者の第一声は「その返答を読んで結論は出さない。現場に行く。」 そして:
file cppide → Mach-O 64-bit executable arm64
lsof -p 96040 → txt 指向 /Users/hechenyu/explore/test-ide/cppide/cppide
起于 16:10,16:15 仍活着
自分でこう書いている:「meta の教訓にあった『Linux ELF を納品する』穴は、今回は踏まなかった。 Linux ELF が Darwin 上で PID になることはあり得ない。これは Makefile から推論したものではない。」
ソースが本当に触られていないことも照合した:src/ 下のすべての .cpp / .h の mtime は全部 15:32(clone した時刻)、 変わったのは Makefile(16:06)だけで、.o / .d(16:07)は追跡されているコンパイル成果物が再生成されたもの。
手の届かない部分も正直に申告した。 判定者の Bash は作業ディレクトリ内に制限されていて、 git ls-remote の比較と ~/.zshrc の mtime は実行できず、独立監査の出力を採用するしかなかった —— この2条は別立てにして「自分の手で再実行できなかった」と説明している。
三、「到達不能」と判定し、そして人に聞いた¶
三値判定と人手介入、この一連の経路が初めて実運用で通った。
判定の結論は到達不能で、詰まったのはスクリーンショットの条項だ:screencapture -x も -l <window-id> も could not create image from display を返す —— 画面収録の TCC 権限が付与されておらず、 付与するには人がシステム設定でクリックする必要があり、それ自体が境界で禁じられたシステム変更だ。
「もう一巡 subagent を派遣してもこの画像は出てこない。規則上これは通過にしてはならず、『未達成』にすべきでもない。 これは到達不能であって、止まって人に渡すべきものだ。」
そこでリストの「原理的に検証できない」条項が3条から4条になった —— 15 条のうち4条がこの環境では根本的に検証できない、 しかもそれが分かったのは $35.8950 の実作業と $1.4037 の判定を使った後だ。 目標設定のステップはたった $0.4117 しかかかっておらず、その時点で気づくべきだった。
フレームワークはこの結論を人に問い(問答記録 q7 に記録)、3つの選択肢のうち人は「この結果を受け入れる」を選び、gate が通り、フローが完了した。
代替品も拒否した。 ワーカーは AppleScript でターミナルウィンドウのテキストを読み戻しており、 それは実際にレンダリングされた TUI だった —— 行番号欄、src/main.cpp の本文、─[编译]─[运行]─[AI*]─[输入]─ のパネル欄、 练习模式 │ main.cpp │ 1:1 │ C++ のステータスバー。白画面でもエラー画面でもない。判定者はこう言った:
「作業した側がこれを『画像を見た』と偽らなかった、この自制は正しい。 だがこれはリストが要求する条項の代わりにはならないし、私が彼の代わりに帳尻を合わせることもしない。」
「リスクを開示した」は通過ではない
HT001 が踏んだのはまさにこれだ:「macOS で一度も走らせていない」を納品説明に書いたうえで、なお受け入れ1を通過と判定した。 HT002 では判定者はより良い代替証拠(実際の TUI テキスト)を手にしながら、なおそれでリスト条項を代替することを拒んだ。 この差が判定の三値設計の存在意義そのものだ —— UNREACHABLE は NOT_YET の婉曲表現ではなく、「止まって人に聞くべきだ」という意味だ。
四、allowed_tools はハードなホワイトリストではない¶
今回の run を分析していて、主張と食い違う事実が見つかった。セッションごとにツール呼び出しを分解すると:
| セッション | 実際に使ったもの | ホワイトリストにあるもの |
|---|---|---|
| 要件確認 | WebFetch、Glob、ask×6 | 当時 clarify() は ask + Read/Glob/Grep のみ |
| 目標設定 | Bash ×11 | judge() はデフォルト can_run=False、Bash は無い |
| 実作業・判定 | Bash ×31、WebFetch | 同上 |
| 実作業(コーディネーター) | Bash ×1、Agent ×6 | 正しい |
goal_step には can_run を渡すコードパスが一つも無いので、「目標設定」の行が設定ミスであることはあり得ない。
$0.1 のプローブ検証:allowed_tools=["Read"] の agent にファイルを書かせてみた ——
Write → 权限层:"requested permissions to write ... but you haven't granted it yet"
Bash → 路径安全:"Output redirection was blocked. For security, Claude Code may
only write to files in the allowed working directories"
モデルはホワイトリストに無いツールを呼び出せる。ただ permission 層とパス安全機構に止められているだけだ。つまり:
allowed_toolsは承認不要リストであって、排他的ホワイトリストではない- 当時
clarify()/judge()を実際に守っていたのは、継承したpermission_mode="default"だった - しかもこの2つは
delegate_only=False→delegate_guardの hook が無い。 コーディネーターには hook が付いているが、この2つの役割には当時 flower 自身の機構が何一つ無かった
clarify() の docstring には「書き込みツールを与えない —— だから作業を始められない」と書いてある。 あの $0.8908 の反例実測は丸ごとこの一文のためのものだった。この一文の機構的根拠は誤りだ。
この節の2つの事実はその後どちらも変わった
whitelist_guard は無条件に接続済みだ:delegate_only=False の役割には Runtime がこの hook を自動で装着するようになり、 clarify() / judge() はもう「機構が何も無い」状態ではない。また WebFetch は現在 clarify() のホワイトリストに入っているので、 表の1行目の例はすでに無効だ —— 結論は依然として成立するが、今はその根拠が $0.1 プローブの証拠だけになった。 詳細は本ページ最終節を参照。
五、コンテキスト曲線:ステップ境界でリセットされる¶
4 つのサンプル点のうち3つ(第 1 / 25 / 121 ターン、36.8K / 80.2K / 64.8K)は生の記録と一つずつ一致する; 第 26 ターンの 32.9K は再計算すると合わない:store_key 順にメインスレッドの assistant メッセージをリプレイすると、 26 件目(新セッションの1件目)の input + cache_read + cache_creation は 31,972 token = 32.0K で、 32.9K は同じセッションの2つ目の値(31,970 + 948)だ。本ページは 32.9K を残す —— 差は 0.9K で、 「断崖状に落ちる」という結論はどちらの値でも成立する。
HT001 は単調に 185.9K まで上がった —— 1ステップで 10 時間走ったからだ。 HT002 は4ステップに分かれ、各ステップで新しいセッションを開くので、コンテキストが構造的にリセットされる。 これは「要件確認書と目標は凍結物であり、下流は文書だけを受け取り会話は受け取らない」という設計の直接の効果だ —— 曲線に見えている。
キャッシュヒット率は 90.8%(HT001 は 96.1%)。ヒット率はセッション長とともに上がるので、短いセッションは単価がかえって少し高い —— これは HT001 第三節の結論と同じコインの裏表だ。
六、HT001 との対照¶
| HT001 | HT002 | |
|---|---|---|
| タスク | ゼロから端末 IDE を書く | それを macOS に入れて動かす |
| 費用 / 所要時間 | $171.62 / 10.44h | $38.24 / 0.97h |
| ステップ | 2(目標看守なし) | 4(目標設定 + 判定を含む) |
| subagent | 23 個 | 6 個 |
| コーディネーターのツール呼び出し | 32 | 7(うち 6 回が Agent) |
| subagent のツール呼び出し | 1,893 | 212 |
| メインスレッドのコンテキスト | 28.7K → 185.9K 単調増加 | 36.8K → 80.2K、ステップ境界でリセット |
| キャッシュヒット | 96.1% | 90.8% |
| ワークベンチのスクリプト | 58 個、331 回実行、92% が再利用された | 0 個 |
| 判定 | 無し(コーディネーターの自発的な内部審査) | 三値判定、「到達不能」と判定し、人に聞いた |
最後の行は注目に値する。 HT002 は再利用可能なスクリプトを1つも書かなかった —— タスクは1時間しかなく、蓄積する価値のあるものが無い。 これはワークベンチの価値がタスクの長さとともに増すことを示している。短いタスクでは純粋なオーバーヘッドだ。
生の記録はどんな形か¶
.flower/INDEX.md はこの run が終わった時点のワークベンチのスナップショットで、そのまま突き合わせて見られる:
| ディレクトリ | 内容 |
|---|---|
scripts/ | 0 個 |
artifacts/ | 5 件 —— 01-recon.md(32.6K)、02-build-run.md(19.4K)、03-flag-only-feasibility.md(13.6K)、04-compliance-audit.md(16.4K)、05-final-build-run.md(19.8K) |
notes/ | 4 件 —— 需求.md、目标.md、问答记录.md の3件はフレームワークが書いた凍結物で、agent 自身が書いた決定記録は1件のみ |
spill/ | spill 11 件 |
runs/ の下には manifest.json(ステップ記録4件)と sessions.db がある。 データベースにはセッションが 10 件:メインスレッドのセッション4件と subagent のセッション6件 ——「6 個の subagent」と一致する。
問答記録には合計7問:q1–q6 は前置確認段階で聞いたもの (要件確認ステップの ask 呼び出し6回と一致)、q7 は「到達不能」と判定した後の人手ゲートだ。
この run がフレームワークをどう変えたか¶
| 発見 | 実装された変更 |
|---|---|
| 15 条の判定リストのうち動くかを検証しているのは約5条だけ(第一節) | JUDGE_RULES に逆方向の圧力を追加:リストの長さは失敗のしかたの数で決まり、厳密さの度合いで決まるのではない、そして今回を反例として規則本文に書き込んだ(「一度の『入れて動かす』タスクが 15 条に書かれ、動くかを検証しているのは 5 条だけだった」 —— 規則では整数で 5 と書き、本ページ第一節の分類基準では約 5) |
| 境界が判定項目として書かれた | JUDGE_RULES に 「境界は判定項目ではない」 と直接書き込んだ |
| 4/15 条がこの環境では検証できず、$35.8950 + $1.4037 を使った後に判明した | 目標設定のステップで、検証できない条項にはその場で [此环境无法验证:原因] の接尾辞を付けることを要求 —— 今回は3条で正しく使い、スクリーンショットの1条を漏らした |
clarify() / judge() に hook の保護が無い(第四節) | whitelist_guard を無条件に接続済み:delegate_only=False の役割には Runtime が自動で装着し、ワークベンチの有効・無効に依存しなくなった |
| 副作用:目標設定の judge が Bash を取れなくなった | whitelist_guard が入った後、can_run=True を明示的に渡さないと、JUDGE_RULES の「まず uname -a で自分がどこにいるか見極めろ」が実行できない —— これは修正後に新たに生じたトレードオフだ |
clarify() の docstring の機構的根拠が誤り | 修正済み。WebFetch も clarify() のホワイトリストに入ったので、あの例は無効になり、結論は $0.1 プローブに掛け替えた |
| 途中で一言挟む通路が無い(第一節・根本原因その3) | issue #3 として記録、未クローズ —— 今回はこれが過剰な複雑化と直接重なり、「その2行は直していい」の一言で1時間節約できた |
この run を一言でまとめる:前回の失敗に合わせて最適化すると、逆方向の新しい失敗を作り出しやすい。 HT001 はフレームワークに「安易に通過と判定するな」を教え、HT002 はその規則に「過剰に検査するな」が付いていないと何が起きるかを即座に実演した。 両方とも JUDGE_RULES に書かねばならず、片方欠ければ偏る。