Перейти к содержанию

HT002 · Установить на macOS и запустить

2026-09-07, задача в одну фразу: склонировать cppide, установить и запустить его на этой машине с macOS. Итог — $38.24 / 4 шага / около 1 часа, программа действительно запустилась на macOS (PID 96040, arm64 Mach-O), а вердиктнедостижимо; вопрос всплыл к человеку, человек принял результат.

Это первый прогон со стражем цели — то, что было переделано по урокам HT001; в тот же день всё это впервые пошло на реальный API. Он спас от ямы, в которую провалился HT001, и одновременно создал новый провал ровно противоположного направления: превратил git clone && make && ./cppide в час работы. Самое ценное на этой странице — второе.

Исходная запись — в human-test/HT002/, счёт посчитан из runs/manifest.json и runs/sessions.db:

python tools/analyze_run.py human-test/HT002/runs

Форма этого прогона

Пункт Значение
Задача склонировать cppide, установить и запустить на этой машине с macOS так, чтобы человек реально мог открыть интерфейс и пользоваться
Затраты $38.24 — 4 шага, разбивка ниже
Длительность около 1 часа (в сумме 0.97h)
Ходов 70 (сумма num_turns по четырём шагам)
Контекст главного потока 36.8K → 80.2K, обрывается на границах шагов, а не растёт монотонно
Разделение труда 6 subagent; координатор — 7 вызовов инструментов против 212 у subagent'ов
Кэш попаданий 90.8%
Верстак 5 продуктов, 4 заметки, 11 файлов сброса на диск, 0 переиспользуемых скриптов
Результат запустилось (PID 96040, arm64 Mach-O); вердикт — недостижимо, человек принял
Версия первый прогон со стражем цели

Затраты, ходы и длительность по шагам

Шаг Затраты Ходов (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

Колонка длительности — это сумма округлённых значений по шагам. При пересчёте по исходной записи точная сумма четырёх duration_s в manifest.json3471.53 секунды = 0.9643h; на этой странице оставлено 0.97h, разница между ними 0.006h.

Деньги почти целиком ушли на шаг «Работа» — $35.8950, это 93.9%. Первый раздел ниже объясняет, на что они потрачены и почему тратиться так было не нужно.


Первое. Он превратил git clone && make && ./cppide в час работы

Это самое важное наблюдение прогона — важнее всех «механизмы заработали» ниже.

На шаге постановки цели судья написал себе 15 пунктов чек-листа вердикта (оригинал — в 目标.md под .flower/notes/). По характеру они делятся так:

Категория Пунктов Примеры
Реально проверяют, что «оно работает» около 5 сборка exit 0; процесс жив ≥30 секунд; команда перезапуска воспроизводима в новом shell
Проверяют «соблюдение правил процесса» 6 mtime у ~/.zshrc / .zprofile / .bash_profile раньше начала этой сессии; не запускались brew install / npm -g / sudo; mtime .flower/ не изменился, в runs/ ничего не добавлено и не удалено
Принципиально непроверяемые 4 сделать скриншот и посмотреть на него; devtools console; ручной клик; личное подтверждение

Только треть проверяет, работает ли эта штука вообще. Шесть пунктов проверяют, соблюдал ли он сам правила — и один из них смотрит на mtime каталога .flower/, то есть на каталог самого фреймворка.

Пункт про .flower/ сам себя завалил

Это не теоретическая проблема. На круге вердикта судья честно записал в «не пройдено»:

mtime всех файлов под .flower/ раньше начала сессии — буквально провалено: INDEX.md 16:14, artifacts/01–05 (15:40–16:12), notes/决策-项目形态与验收路径.md 15:43 — все записаны внутри сессии.

Причина: фреймворк жёстко требует писать длинные продукты в .flower/artifacts/, а решения — в .flower/notes/. А тот пункт чек-листа требует не трогать этот каталог. Два правила бьются лбами, и породил это столкновение сам чек-лист вердикта — исполнитель не записал ни одного длинного продукта, и от этого стал бы только «соблюдающее».

Он не стал заметать это под ковёр: шаг «Работа» сам перечислил конфликт в итоговом отчёте, а в записи решений описан способ обработки — ни один существующий файл не тронут需求.md, 目标.md, 问答记录.md mtime и MD5 до и после совпадают), только добавлены новые. Судья при перепроверке согласился, что «подмены, от которой защищает этот пункт, не произошло», но всё равно по правилам занёс его в «не пройдено» и отдал решение человеку.

Корень первый: границы превратили в пункты вердикта

Границы, выясненные на этапе уточнения («ставить только внутри каталога проекта», «бизнес-код не трогать»), изначально ограничивают то, как делается работа, а оказались записаны как пункты проверки продукта. Отсюда и взялись пункты про mtime .zshrc и mtime .flower/.

Граница ≠ пункт вердикта. Граница ограничивает процесс, пункт вердикта проверяет результат. Если их смешать, каждая новая граница даёт лишний пункт вердикта — а границы как раз поощряется писать как можно полнее на этапе уточнения: в CLARIFIER_RULES прямо сказано «чётко зафиксировать, чего не делаем. Этот раздел будет управлять каждым, кто дальше делает работу».

По исходной записи видно каждое звено этого усиления: в границах брифа написано «не трогать уже существующие .flower/ и runs/ в текущем каталоге», а в чек-листе вердикта это превратилось в «mtime всех файлов под .flower/ раньше начала этой сессии» — из «не трогай существующее» получилось «во всём каталоге не должно появиться новых файлов».

Корень второй: JUDGE_RULES давит только в одну сторону — и добавлено это было в тот же день

Раньше в тот же день по урокам HT001 в JUDGE_RULES было дописано:

  • «каждый пункт должен проверяться прямо на месте», «чего не хватает, что размыто — дополняй»
  • «судим продукт, а не исходники» (с тем самым отрицательным примером про macOS)
  • «пункты, которые в текущем окружении не проверить, отмечай сразу»
  • «сначала разберись, где ты находишься (можно uname -a

Каждая строка толкает в сторону большего числа проверок и большей строгости. Во всём наборе правил нет ни одной фразы про соразмерность масштабу задачи.

То есть: оптимизация против провала HT001 («слишком легко объявил пройденным») породила ровно обратный провал — избыточную проверку. Это сделано своими руками, а не моделью.

Корень третий: наложилось на issue #3

Реально мешали два дефекта переносимости на macOS:

  • src/proc.cpp использует ::sigemptyset, а Apple SDK после прототипа функции ещё и определяет её как функциональный макрос (защищённый только #ifndef _ANSI_SOURCE), поэтому ::sigemptyset(&s) разворачивается в ::(*(&s)=0,0) — синтаксическая ошибка
  • src/ui.cpp использует ::_exit, но не включает <unistd.h>

Правки на три строки. Но пользователь на этапе уточнения ответил «бизнес-код не трогать», агент выполнил это строго, и потому пошёл в обход: пробовать флаги компиляции, отправлять subagent'ов, писать записи решений. Если бы он тогда мог вставить реплику «те две строки править можно», всё закончилось бы за десять минут. Граница, записанная как запрет, трактуется максимально строго, а канала ослабить её по ходу дела нет.

По исходной записи видно, насколько длинным был обход: всего четыре точки блокировки, и только ::_exit решался чистыми флагами компиляции; для ::sigemptyset было испробовано 6 комбинаций -D/-U — все провалились (серия _POSIX_C_SOURCE попутно ломает ещё st_mtimespec и O_CLOEXEC). Один этот тупик породил отчёт о выполнимости на 13.6K.

Но справедливости ради: граница выдавила лучшее решение

Итоговое решение — не править исходники, а сгенерировать в Makefile прокладочный заголовок, делающий только #undef, и принудительно подставить его через -include:

$(MACSHIM):
    @printf '#include <signal.h>\n#undef sigemptyset\n#undef sigfillset\n...' > $@
CPPFLAGS += -include $(MACSHIM) -include unistd.h

Это лучше правки исходников — апстрим-репозиторий не меняется ни на строку, любой может склонировать и собрать. Прокладка генерируется в build/, в репозиторий не попадает, в clean добавлен rm -rf build для уборки; make -n UNAME_S=Linux на практике разворачивается без -include, путь Linux не загрязнён. В итоговом git diff только один файл — Makefile, +14 / −2.

То есть граница дала более дорогое, но более удачное решение. Проблема не в том, что он сделал неправильно, а в том, что этот выбор не был отдан человеку — и причина невозможности отдать его как раз issue #3.

Почему в git status 64 записи, но говорится, что исходники не менялись

Репозиторий cppide закоммитил в систему контроля версий 30 файлов .o, 30 .d и 2 бинарника, причём это Linux ELF aarch64, и mtime у них позже, чем у .cpp — без предварительного make clean команда make пропустит компиляцию и пойдёт линковать ELF, что гарантированно упадёт. Поэтому из 64 записей git status --porcelain 63 — отслеживаемые git'ом продукты сборки плюс один ?? build/. После фильтрации по расширениям git diff -- 'src/*.cpp' 'src/*.h' README.md config.sample.json | wc -c даёт 0. Эта точная формулировка выдавлена аудитом соответствия — координатор хотел написать обобщённое «в src/ изменений нет», аудит указал, что такая фраза не выдерживает проверки.


Второе. Что страж цели сделал правильно

Со стороны механизмов заработало всё, и обе правки того дня оставили прямые отпечатки в продуктах.

Первый пункт написанного им чек-листа вердикта:

uname -a показывает Darwin 25.2.0 (macOS)… все пункты ниже про «запустилось / открылось» должны быть результатами реального запуска на этой машине, выводы вида «в исходниках есть ветка под macOS, значит должно работать» не принимаются

Это ровно та формулировка, которая утром того дня была вписана в JUDGE_RULES по урокам HT001.

Он трижды применил добавленную в тот день пометку [此环境无法验证:…], каждый раз с обоснованием — devtools console (нет браузера, который можно вести, а ставить headless-браузер ради этого — выход за границу), ручной клик (нужен человек перед экраном), личное подтверждение (может дать только он сам).

Первая фраза судьи: «Я не делаю выводов по чужому пересказу. Иду на место.» А дальше:

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 не может стать живым PID на Darwin. Это не вывод из Makefile.»

Он также сверил, что исходники действительно не тронуты: mtime у всех .cpp / .h под src/ — 15:32 (момент клонирования), изменён только Makefile (16:06), а .o / .d (16:07) — пересобранные отслеживаемые продукты сборки.

Он честно признал и то, до чего не дотянулся. Bash у судьи ограничен рабочим каталогом, сравнение через git ls-remote и mtime у ~/.zshrc он запустить не смог и вынужден был принять вывод независимого аудита — эти два пункта он выделил отдельно с пометкой «своими руками воспроизвести не смог».

Третье. Вердикт «недостижимо», после чего вопрос человеку

Трёхзначный вердикт плюс вмешательство человека — вся цепочка впервые прошла по-настоящему.

Вердикт — недостижимо, упёрлось в пункт со скриншотом: screencapture -x и -l <window-id> оба возвращают could not create image from display — не выдано TCC-разрешение на запись экрана, а выдать его должен человек кликами в системных настройках, что само по себе запрещённое границей изменение системы.

«Ещё один круг subagent'ов эту картинку не родит. По правилам это нельзя признать пройденным, но и „не достигнуто“ тоже неверно — это именно недостижимо, надо остановиться и отдать человеку.»

Так число «принципиально непроверяемых» пунктов выросло с 3 до 4 — 4 из 15 в этом окружении не проверить вообще, и обнаружилось это после того, как были потрачены $35.8950 на работу и $1.4037 на вердикт. Шаг постановки цели стоил всего $0.4117 — понять это следовало уже тогда.

Фреймворк вынес этот вывод человеку (записано в журнале вопросов и ответов как q7), три варианта; человек выбрал «принять результат», gate пропустил, процесс завершён.

Он также отверг подмену. Исполнитель через AppleScript вычитал текст окна терминала — это реально отрисованный TUI: колонка номеров строк, текст src/main.cpp, панель ─[编译]─[运行]─[AI*]─[输入]─, строка состояния 练习模式 │ main.cpp │ 1:1 │ C++ — не белый экран и не экран с ошибкой. Судья сказал:

«Исполнитель не выдал это за „посмотрел на картинку“, и это самоограничение правильно. Но пункт чек-листа оно не заменяет, и я не стану засчитывать его за него.»

«Раскрыть риск» — не значит «пройдено»

Именно на этом провалился HT001: он вписал «на macOS ни разу не запускалось» в описание поставки — и всё равно признал приёмку 1 пройденной. В HT002 судья получил даже более удачное альтернативное свидетельство (реальный текст TUI) и всё равно отказался подставить его вместо пункта чек-листа. В этом различии — весь смысл трёхзначного вердикта: UNREACHABLE — не вежливая форма NOT_YET, это «пора остановиться и спросить человека».

Четвёртое. allowed_tools — не жёсткий белый список

При разборе этого прогона обнаружился факт, расходящийся с заявленным. Если разложить вызовы инструментов по сессиям:

Сессия Что реально использовано Что есть в белом списке
Уточнение требований 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"] дали задание записать файл —

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"

Модель может вызвать инструмент, которого нет в белом списке, её лишь останавливает слой разрешений и защита путей. Отсюда:

  • allowed_tools — это список того, что не требует подтверждения, а не исключающий белый список
  • реально clarify() / judge() тогда защищал унаследованный permission_mode="default"
  • а у обоих delegate_only=Falsehook delegate_guard отсутствует. Координатора прикрывал hook, а у этих двух ролей тогда не было вообще никакого собственного механизма flower

В докстринге clarify() написано «инструментов записи не даём — начать работу он не сможет», и весь тот отрицательный эксперимент за $0.8908 был поставлен ради этой фразы. Механистическое обоснование этой фразы неверно.

Оба факта этого раздела с тех пор изменились

whitelist_guard уже подключён безусловно: роли с delegate_only=False теперь получают этот hook автоматически от Runtime, так что у clarify() / judge() больше не «никакого механизма». Кроме того, WebFetch теперь входит в белый список clarify(), поэтому пример из первой строки таблицы утратил силу — вывод остаётся в силе, но держится теперь только на свидетельстве зонда за $0.1. Подробнее — в последнем разделе этой страницы.

Пятое. Кривая контекста: границы шагов сбрасывают её

ход 1     36.8K
ход 25    80.2K   ← пик
ход 26    32.9K   ← обрыв: новый шаг = новая сессия (resume_from=None)
ход 121   64.8K

Три из четырёх точек замера (ходы 1 / 25 / 121, 36.8K / 80.2K / 64.8K) сходятся с исходной записью один в один; а 32.9K на 26-м ходу при пересчёте не сходится: если проиграть assistant-сообщения главного потока в порядке store_key, то у 26-го (первого в новой сессии) input + cache_read + cache_creation даёт 31,972 токена = 32.0K, а 32.9K — это второе значение в той же сессии (31,970 + 948). На этой странице оставлено 32.9K — разница 0.9K, вывод про «обрыв вниз» верен при любом из двух значений.

В HT001 контекст монотонно рос до 185.9K — один шаг длиной 10 часов. В HT002 из-за разбиения на четыре шага каждый шаг открывает новую сессию, и контекст сбрасывается структурно. Это прямой эффект решения «бриф и цель — замороженные документы, ниже по течению передаются документы, а не диалог» — на кривой это видно.

Попаданий в кэш 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 не написано ни одного переиспользуемого скрипта — задача заняла час, откладывать в запас нечего. Это значит, что ценность верстака растёт с длиной задачи, а на коротких задачах он — чистые накладные расходы.

Как выглядит исходная запись

.flower/INDEX.md — снимок верстака на момент окончания прогона, с ним можно сверяться напрямую:

Каталог Содержимое
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 — три замороженных документа, написанных фреймворком; собственная запись решений агента всего одна
spill/ 11 файлов сброса на диск

В runs/ лежат manifest.json (4 записи о шагах) и sessions.db. В базе 10 сессий: 4 сессии главного потока плюс 6 сессий subagent'ов — сходится с «6 subagent».

В журнале вопросов и ответов всего 7 вопросов: q1–q6 заданы на этапе предварительного уточнения (сходится с 6 вызовами ask на шаге уточнения требований), q7 — тот самый ручной шлюз после вердикта «недостижимо».

Что этот прогон изменил во фреймворке

Находка Внесённое изменение
Из 15 пунктов чек-листа вердикта работоспособность проверяют около 5 (раздел первый) В JUDGE_RULES добавлено обратное давление: длина чек-листа определяется числом способов провалиться, а не степенью дотошности, и этот прогон вписан в текст правил как отрицательный пример («задача „поставить и запустить“ была расписана на 15 пунктов, из них только 5 проверяют работоспособность» — в правилах округлено до 5, классификация в первом разделе этой страницы даёт около 5)
Границы записали как пункты вердикта В JUDGE_RULES прямо зафиксирована фраза «граница — не пункт вердикта»
4/15 пунктов в этом окружении непроверяемы, а обнаружено это после трат $35.8950 + $1.4037 На шаге постановки цели требуется сразу помечать непроверяемые пункты суффиксом [此环境无法验证:原因] — в этот раз он применил это правильно к 3 пунктам, а пункт со скриншотом упустил
У clarify() / judge() нет hook-защиты (раздел четвёртый) whitelist_guard подключён безусловно: роли с delegate_only=False получают его автоматически от Runtime, это больше не зависит от того, включён ли верстак
Побочный эффект: judge на шаге постановки цели теперь не получает Bash После появления whitelist_guard, если явно не передать can_run=True, пункт JUDGE_RULES «сначала uname -a, разберись, где ты» выполнить невозможно — это новый компромисс, возникший после починки
Механистическое обоснование в докстринге clarify() неверно Исправлено. WebFetch также вошёл в белый список clarify(), поэтому тот пример утратил силу, а вывод теперь держится на зонде за $0.1
По ходу дела нет канала вставить реплику (корень третий, раздел первый) Зафиксировано как issue #3, не закрыто — здесь оно напрямую наложилось на избыточное усложнение, и одна фраза «те две строки править можно» сэкономила бы час

Итог этого прогона одной фразой: оптимизируя против прошлого провала, очень легко создать новый провал противоположного направления. HT001 научил фреймворк «не признавать пройденным слишком легко», а HT002 немедленно показал, что бывает, когда к этому правилу не приложено «и не проверяй избыточно». Обе строки должны быть в JUDGE_RULES — без одной из них перекос.