Status: advisory. It states what happened and recommends; it decides nothing, amends no frozen section and changes no task state. Relates to: TFW-60 Phase AA, AC-13 half two — a third project run by an operator who is not the author of the code, on the
2.0.0-dirty.3payload. Payload taken at:edab067onD:/projects/research/steps-framework,.tfw/VERSION=2.0.0-dirty.3. There was no tag.git tagin the source listsv2.0.0-dirty.2as the newest; the CHANGELOG entry for.3says "tagged locally and not pushed", and that sentence was false at the moment it was read.installed_fromtherefore names a commit, not a tag.
Разбор полётов: обновление TFW 0.8.7 → 2.0.0-dirty.3¶
Проект:
helpdesk. Дата: 2026-08-29. Источник: локальное рабочее деревоD:/projects/research/steps-framework, HEADedab067(тегv2.0.0-dirty.3не существует). Исполнитель: сессия Claude Code от имениsaubakirov, автономный режим — владелец не отвечал по ходу. Учёт доски —helpdesk/MIGRATION.md, снимок —helpdesk/tasks/BOARD-SNAPSHOT.md. Это самый длинный прыжок из трёх внешних обновлений: не 1.3.0 → 2.0.0, а 0.8.7 → 2.0.0, через 1.0 (философия), 1.2 (контракт HL), 1.3 (REJECTED) сразу.
Одной строкой¶
Процедура довела до конца без единого вопроса «а что теперь?» — и по дороге один
инструмент уверенно сообщил неправду. migrate_board.py схлопнул две строки доски (HD-30 и
HD-30b) в одну сущность, записал завершённой задаче живой статус TODO, и напечатал
«Every row and directory accounted for exactly once. Unaccounted: 0». Дефект поймал не
манифест, а отгруженный тест test_repository_accounting_balances (31 ≠ 30) — который для
проекта-получателя никто не велит запускать, и который после снятия доски снова зелёный.
Остальное — ровно то, что обещали два предыдущих отчёта: слой адаптеров сгнил молча (6 из 12
команд .claude/commands/ отставали от собственного 0.8.7 ещё до обновления; 17 файлов несли
словарь доски), build.* указывал в пустоту, Step 3a обнулил ручные слияния. Новое в этот раз —
Step 3a дал 10 ложных срабатываний из 12, потому что проект был установлен не из этого репо.
Что произошло, в числах¶
| Измерение | Значение |
|---|---|
| Файлов в payload v0.8.7 | 49 |
| Файлов в payload 2.0.0-dirty.3 | 69 (новых 25, ретировано 4 + регистр TOPIC_FILE.md) |
Step 3a: файлов, отмеченных CUSTOMIZED |
12 |
| Из них реальных кастомизаций | 2 (knowledge_state.yaml ⚫, project_config.yaml 🟡) |
| Из них — дрейф происхождения (по 2–16 строк, формулировки старше тега) | 10 |
| Ручных слияний фактически | 1 — конфиг |
| Строк доски → каталогов | 32 строки, 30 каталогов → 31 «сущность» по манифесту; реально 32 |
status.md записано |
12 → оставлено 11 (один удалён как ошибочный, см. §Дефект 1) |
UNDECLARED после миграции |
4 → 1 разрешён владельцем (HD-18 ✅ Done), 3 открыты |
| Адаптерных копий ре-синхронизировано | 24 (.claude/commands ×12, .agent/workflows ×12) + .agent/rules/tfw.md (стоял на «TFW 0.8.5») |
| Файлов слоя адаптеров со словарём доски до обновления | 17 |
| Файлов проекта, тронутых руками сверх payload | 6 (CLAUDE.md, AGENTS.md, .agent/rules/agents.md, deploy-prod.md ×2, README.md) |
| Тесты payload до миграции / после | 158 passed 2 failed / 159 passed 1 skipped |
--check index / tasks / project |
все зелёные |
| Ошибок оператора, пойманных гейтом | 2 (обе — --check tasks, обе на одном файле) |
| Коммитов | 0 — правило проекта «не коммитить без явной просьбы»; 105 изменённых путей ждут владельца |
Что сработало отлично¶
Маршрутизация мажора. update.md Step 3 → migrations/2.0.0.md → семь шагов с явным «что
сломается, если перескочить». Прыжок через четыре мажорных/минорных релиза с абсолютно чужой
философией занял одну сессию и ни разу не потребовал реконструировать порядок. Первый
потребитель потратил на это сессию; здесь — ноль.
Step 3a — даже с ложными срабатываниями. conventions.md изменился на 584 строки диффа,
glossary.md на 178 — и оба байт-в-байт равны v0.8.7, слияние не потребовалось. Два
объявленных ручных слияния превратились в ноль. Ложные срабатывания (см. ниже) стоили пяти
минут diff --strip-trailing-cr на каждый — дешевле, чем одно настоящее слияние.
--check tasks — настоящий гейт, дважды. Я испортил HD-18/status.md два раза подряд
(см. «ошибки оператора»), и оба раза --check tasks отверг файл, --check project подхватил
(«1 task(s) carry malformed state»), а индекс честно вынес задачу в «Unresolved inputs» вместо
того, чтобы отрисовать мусор. Ни одна из двух ошибок не дошла бы до коммита.
Скрипты печатают разрешённый корень. Это спасло от тихой катастрофы: после cd
.tfw/scripts && pytest рабочая директория шелла осталась там, и следующая команда записала
событие журнала в .tfw/scripts/tasks/HD-18…/journal/. Строка project root: … в каждом
запуске — та привычка, по которой это было замечено за один шаг, а не за десять.
--working-tree существует и залогирован. Доска в HEAD отставала от рабочего дерева
(строка HD-31 сменилась 🔬 RES → 🟠 ONB без коммита), а правило проекта запрещает коммитить
без просьбы. Флаг закрыл ровно эту ситуацию, и запись «board source: README.md (working tree,
--working-tree)» стоит в логе.
installed_from понадобился немедленно. Тега нет, tfw.upstream смотрит на GitHub, где
нет даже 2.0.0-dirty. Без этого ключа следующий /tfw-update склонировал бы зеркало, нашёл
0.8.x и отчитался «всё в порядке». Ключ добавлен в .2 по отчёту второго потребителя и
пригодился третьему в тот же день.
Step 6 теперь называет .claude/commands/. Именно здесь этот ряд был нужен: проект живёт в
Claude Code и только начинает Antigravity; шесть из двенадцати команд отставали от своих же
воркфлоу 0.8.7 (diff tfw-docs.md — 168 строк), то есть предыдущее обновление 0.8.5 → 0.8.7
их не тронуло. Ряд таблицы, которого не было, — вот цена.
UNDECLARED и путь его разрешения. Четыре статуса вне словаря сохранены дословно. Один
(✅ Done — регистр) разрешён как положено: lifecycle: DONE, outcome, событие transition
с from: UNDECLARED, on_behalf_of: saubakirov, via: claude, четырёхсимвольный токен в
имени. Шаблон journal/event.md дал всё нужное; ничего не пришлось изобретать. Три остальных —
фазовые («Phase A ✅ / Phase B 📚 KNW») — оставлены владельцу: это решение о PHASES и
фазовых status.md, а не о регистре.
Manifest перед записью, --help без лукавства, шаблоны с рабочими примерами. Всё, что
хвалили предыдущие отчёты, держится.
Дефекты — по тяжести¶
1. migrate_board.py схлопнул HD-30b в HD-30 и записал завершённой задаче TODO¶
Доска несла две строки:
| [HD-30](tasks/HD-30__tickets_filters_creator_route_bus/) | … | ✅ DONE … |
| [HD-30b](tasks/HD-30__tickets_filters_creator_route_bus/hd30b/) | … | ⬜ TODO — gates сняты … |
Грамматика PREFIX-N прочла HD-30b как HD-30 и отбросила хвост. Дальше по цепочке:
- манифест:
| 30 | HD-30 | … | 31 | HD-30 |— один идентификатор дважды в таблице «каждый по имени», и над ней: «31 matched … Unaccounted: 0 … Every row and directory is accounted for exactly once»; - записан
tasks/HD-30…/status.mdсlifecycle: TODO,title: Backfill…— для задачи, которую доска закрыла✅ DONEи задеплоила в прод; - индекс отрисовал HD-30 в таблице Closed с колонкой Outcome =
⬜ TODO — gates сняты….
Три уровня, три уверенных утверждения, все ложные, ни одного предупреждения. Это ровно тот класс
дефекта, который релиз .2 объявил хуже молчаливого пропуска: «confidently misdescribing one
is worse, because it reads as a finding».
Поймал отгруженный тест test_migrate_board.py::test_repository_accounting_balances:
assert 31 + 0 == 30. То есть арифметика, которая ловит дефект, в репо есть — но она в
тестах, а не в манифесте, и проекту-получателю никто не велит их запускать (я запустил из
любопытства). После снятия доски тест снова зелёный: 0 строк тривиально сходятся с чем угодно.
Что сделано в проекте: ошибочный status.md удалён (новый файл, обычный откат), HD-30b теперь
существует только в снимке; решение — владельцу (см. «Открыто»).
Рекомендация. (a) Ячейка ID, которая парсится не целиком (
HD-30b≠HD-30), — это malformed row, а не match по префиксу. (b) Два ряда, разрешившиеся в один идентификатор, — жёсткий стоп манифеста, не строка в таблице. (c) Инвариант из теста (matched + directory_only == directories) должен быть в самом манифесте, под заголовком «Guarantees checked» — там сейчас написано, что он выполнен, а он не проверялся.
2. Генератор состояния вырезает подчёркивания из прозы¶
В status.md, снимке и индексе: normalize_text() → normalizetext(), field_worker →
fieldworker, working_days → workingdays, backfill_metadata.sql → backfillmetadata.sql.
Снимок при этом называется дословным — и его нижняя часть действительно дословна (строки
93–94), а учётная таблица и все status.md — нет. Для проекта, где normalize_text() — имя
PL/pgSQL-функции в миграции 027, это не косметика: goal задачи стал называть функцию, которой
нет.
Рекомендация. Снимать Markdown-разметку (
`,**), а не символы идентификаторов. Тест: строка сsnake_caseвнутри backticks должна пережить миграцию неизменной.
3. Тег 2.0.0-dirty.3 не существует, и источник двигался во время обновления¶
VERSION и CHANGELOG говорят 2.0.0-dirty.3, CHANGELOG говорит «tagged locally», git tag
говорит — нет. Step 0 в форме git archive v{target} упал бы; взят HEAD. Между моим git log
(HEAD b75bef1) и git archive (HEAD edab067) прошёл один коммит — тот, что дописывал
CHANGELOG-запись .3, которую я в этот момент читал. Правило покоя из migrations/2.0.0.md
(«не мигрируйте, пока участник в середине гейта») сформулировано для получателя; для
источника его нет, а нужно оно ровно так же: payload, взятый из движущегося дерева, — это
payload, которого не было ни в одном релизе.
Рекомендация. Step 0 для локального источника: проверять, что целевой тег существует, до
archive; если нет — писать вinstalled_fromSHA и словоuntagged, как сделано здесь. И одна строка вupdate.md: «зафиксируйте HEAD источника до Step 0 и сверьте после Step 5».
4. Step 3a ложно срабатывает, когда проект установлен из другой линии¶
Десять из двенадцати CUSTOMIZED — не кастомизации. Локальные файлы 0.8.7 приехали из GitHub-
зеркала trace-first-starter, и его 0.8.7 отличается от steps-framework v0.8.7 на 2–16 строк
более старыми формулировками (PROJECT_CONFIG.yaml вместо project_config.yaml,
research/briefing.md вместо 1_briefing.md). Инструкция «тег принадлежит источнику, не
проекту» верна — но она предполагает, что источник обновления и источник установки совпадают.
До installed_from это было ничем не гарантировано, и для любого проекта, установленного до
.2, останется ничем не гарантировано при следующем обновлении.
Рекомендация. В Step 3a: «
CUSTOMIZEDна 2–15 строк, где локальная формулировка старше тега, — дрейф происхождения; перезаписывать». И — сверять поinstalled_from, когда он есть.
5. Отгруженные тесты содержат тесты состояния репозитория¶
test_the_repository_index_is_readable… требует tasks/00-INDEX.md (его нет до Step 4);
test_repository_accounting_balances читает доску проекта. Второй потребитель поставил
build.test = pytest по .tfw/scripts/, как рекомендовал .2. Такой проект в середине
миграции красный, а после снятия доски тест, единственный поймавший Дефект 1, зелёный
навсегда. Тесты фреймворка о самом фреймворке и тесты о корпусе получателя — разные вещи, и
сейчас они в одном файле без пометки.
6. Grep ретированной лексики бьёт по самому payload¶
update.md Step 6: «Nothing may print». Напечатало: initial_seq в tfw-init.md и
tfw-update.md — потому что payload сам называет ключ, который велит удалить; Task Board в
glossary.md (статья «retired at 2.0.0») и в migrate_board.py. Предупреждение о
самопопадании в тексте есть — и оно же делает «zero, every time» невыполнимым буквально.
Проверка нужна, формулировка «ноль» — нет: нужен allowlist или «ноль вне payload и его копий».
7. Мелочи, которые стоили по минуте каждая¶
--check tasksнаReaderError(U+0082 внутри значения) не назвал ключ — CHANGELOG обещает имя ключа, но дляScannerError. Пришлосьod -c.templates/team/profile.mdв одном файле: «one file per participant, humans and agents alike. That is why it is team/ and not people/» — и через абзац «team/ HOLDS PEOPLE». Правда второе; первое пережило правку.3.scope_budgetsпомечены← FRAMEWORK, но в проекте подняты через/tfw-config(35/18/3500/26). Слияние строго по маркерам сбросило бы их к 30/15/3000/30. Сохранил; маркер должен быть← PROJECT, раз есть воркфлоу, который их меняет.review.default_mode: codeисчез вместе сworkflows/review/{code,docs,spec}.md— но ни CHANGELOG### Removed, ни--check projectего не называют. Ключ удалён по выводу.- Шаблон
antigravity/tfw-rules.md.templateнесёт{version}, а собственный отрендеренный.agent/rules/tfw.mdфреймворка говорит «Version: see.tfw/VERSION». Скопировал второе: шаблон, требующий подстановки версии при каждом обновлении, — вот почему локальный говорил «TFW 0.8.5» два релиза подряд. migrations/2.0.0.md: «Commit, or at least stage». Доска читается изHEAD; staging ничего не меняет. Одно слово вводит в заблуждение.- Новый
adapters/codex/(13 файлов) иcursor/приехали в проект, где нет ни Codex, ни Cursor. Безвредно, ноgit statusдлиннее на 15 строк, которые оператор должен прочитать, чтобы понять, что они безвредны.
Ошибки оператора — как сигнал об UX¶
Я — тот самый «оператор, который не автор кода», и мои две ошибки говорят о фреймворке не меньше его дефектов.
- Дрейф cwd.
cd .tfw/scripts && pytest …без обратногоcd, следующая команда писала относительными путями. Событие журнала ушло в.tfw/scripts/tasks/…. Замечено по отсутствию файла и по привычке скриптов печатать корень. Вывод:.tfw/scripts/— это каталог, в который заходят (запустить тесты), и всякий, кто зайдёт, рискует тем же.python -m pytest .tfw/scriptsиз корня — единственная форма, которую стоит показывать. \2в Windows-пути.globвернулjournal\20260829-…, строка попала вre.subзаменой,\2стал ссылкой на группу → вupdated:оказался U+0082. Гейт поймал, но сообщение было «ReaderError» без строки и ключа. Урок дляstatus.md: временна́я метка читается с часов, а не восстанавливается из имени файла события — что шаблон и говорит; я срезал угол, и шаблон был прав.
Оба раза меня спас --check tasks. Это главный аргумент за то, чтобы build.verify у каждого
проекта был именно им — как и стоит в шаблоне конфига.
Что изменено в проекте сверх процедуры¶
Решения, не механика:
task_containers: [workspace, tasks]— решение владельца, вынесенное сразу после сессии. Оператор в автономном режиме поставил[tasks](«left silent, it is set by whoever ran the update» — сработало буквально); владелец перевернул: новые задачи вworkspace/{ГГГГ}/, корпус HD-N остаётся вtasks/нетронутым. Индекс переехал вworkspace/00-INDEX.md, ссылки на../tasks/…разрешаются,--check project: «creates in 'workspace', resolves across ['workspace', 'tasks']».build.*→ цели Makefile. Былоruff check src/ tests/от корня, где ниsrc/, ниtests/нет (API живёт вcode/api/).Makefileпроекта уже делаетcd code/api && …. Теперьlint: make lint,test: make test-unit,verify: python .tfw/scripts/gen_index.py --check tasks. Предсказание.2о «preserved is not correct» подтвердилось буквально — и заметил его я при чтении, а не--check project:make lintинструмент проверить по пути не может.- HD-18:
UNDECLARED→DONEсобытием журнала (см. выше). Единственный из четырёх, где решение не требует суждения. deploy-prod.md(проектная команда, оба адаптера): «UpdateREADME.mdtask board» →status.md+ событие + регенерация индекса. Не payload, но инструкция вела к таблице, которой нет.README.md: доска снята, на её месте раздел «Задачи» с маршрутом наtasks/00-INDEX.md, снимок иMIGRATION.md.CLAUDE.md,AGENTS.md,.agent/rules/agents.md: пункт 5 порядка загрузки контекста — индекс иstatus.mdзадачи вместо доски.
Открыто — владельцу¶
- ~~HD-30b~~ — решено владельцем: закрытый подпункт закрытого HD-30; «зачем вообще менять закрытое». Остаётся в снимке, состояния не получает.
- ~~HD-20, HD-21, HD-26 —
UNDECLARED; HD-29~~ — решено владельцем: старые задачи не трогаются. Неначатые позже переносятся вworkspace/или пересоздаются как новые аналоги. ТриUNDECLAREDостаются видимыми в индексе — это и есть их корректное состояние до того момента. owner: unassignedво всех 11status.md— доска не несла владельца; при желании — событияownership_changed.knowledge/philosophy.mdупоминает Task Board один раз — территория/tfw-docs, не payload.- Коммит. Ничего не закоммичено. Предлагаемая тема по грамматике §4:
[claude-code/project/update/coordinator] update TFW 0.8.7 -> 2.0.0-dirty.3.
Рекомендации другим пользователям TFW — по порядку исполнения¶
- Проверьте, что тег существует, до того как поверить
VERSION.git -C {source} tag -l 'v{target}'. Если пусто — берите SHA и пишитеuntaggedвinstalled_from. - Зафиксируйте HEAD источника до начала и сверьте в конце. Payload из движущегося дерева — не релиз.
- Step 3a — и не верьте
CUSTOMIZEDна 2–16 строк. Откройте дифф: если локальные формулировки старше тега, это дрейф происхождения. Перезаписывайте. - Запустите
python -m pytest .tfw/scriptsиз корня до--apply— не потому, что процедура велит, а потому чтоtest_repository_accounting_balancesсейчас единственное, что ловит схлопывание идентификаторов. После снятия доски он уже ничего не ловит. - Читайте таблицу «Every board identifier, by name» на дубли. Один ID дважды — стоп, даже если строкой выше написано «exactly once».
- Grep снимок и
status.mdна вашиsnake_case-имена. Если у вас есть функцияnormalize_text(), после миграции её будут зватьnormalizetext(). .agent/rules/tfw.mdи.claude/commands/— проверьте версию руками. Здесь первый стоял на 0.8.5 два релиза подряд, а половина команд отставала от своих же воркфлоу.build.*перечитайте до--check project, а не после. Инструмент проверит путь к скрипту;make lintон проверить не может.- Не заходите
cdв.tfw/scripts/. Все команды — из корня проекта. Скрипты печатают разрешённый корень — читайте эту строку. UNDECLAREDс очевидным ответом (регистр, синоним) разрешайте сразу событием; с неочевидным — оставляйте. Здесь один из четырёх был очевиден, три — нет.
Разбор полётов третьего внешнего обновления. Каждое число получено измерением в сессии, не оценкой. Автор — оператор обновления, не автор payload.
Продолжение: обновление того же проекта до
2.0.0-dirty.4(2026-08-30) разобрано отдельно вFIELD-REPORT__TFW-60__helpdesk_dirty3_to_dirty4.md(имя с проектом и тегами: ординальноеfourthзаняла другая сессия в ту же минуту, см. filing note в том файле).