Skip to content

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.3 payload. Payload taken at: edab067 on D:/projects/research/steps-framework, .tfw/VERSION = 2.0.0-dirty.3. There was no tag. git tag in the source lists v2.0.0-dirty.2 as the newest; the CHANGELOG entry for .3 says "tagged locally and not pushed", and that sentence was false at the moment it was read. installed_from therefore names a commit, not a tag.


Разбор полётов: обновление TFW 0.8.7 → 2.0.0-dirty.3

Проект: helpdesk. Дата: 2026-08-29. Источник: локальное рабочее дерево D:/projects/research/steps-framework, HEAD edab067 (тег 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-30bHD-30), — это malformed row, а не match по префиксу. (b) Два ряда, разрешившиеся в один идентификатор, — жёсткий стоп манифеста, не строка в таблице. (c) Инвариант из теста (matched + directory_only == directories) должен быть в самом манифесте, под заголовком «Guarantees checked» — там сейчас написано, что он выполнен, а он не проверялся.

2. Генератор состояния вырезает подчёркивания из прозы

В status.md, снимке и индексе: normalize_text()normalizetext(), field_workerfieldworker, working_daysworkingdays, backfill_metadata.sqlbackfillmetadata.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_from SHA и слово 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

Я — тот самый «оператор, который не автор кода», и мои две ошибки говорят о фреймворке не меньше его дефектов.

  1. Дрейф cwd. cd .tfw/scripts && pytest … без обратного cd, следующая команда писала относительными путями. Событие журнала ушло в .tfw/scripts/tasks/…. Замечено по отсутствию файла и по привычке скриптов печатать корень. Вывод: .tfw/scripts/ — это каталог, в который заходят (запустить тесты), и всякий, кто зайдёт, рискует тем же. python -m pytest .tfw/scripts из корня — единственная форма, которую стоит показывать.
  2. \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: UNDECLAREDDONE событием журнала (см. выше). Единственный из четырёх, где решение не требует суждения.
  • deploy-prod.md (проектная команда, оба адаптера): «Update README.md task 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 во всех 11 status.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 — по порядку исполнения

  1. Проверьте, что тег существует, до того как поверить VERSION. git -C {source} tag -l 'v{target}'. Если пусто — берите SHA и пишите untagged в installed_from.
  2. Зафиксируйте HEAD источника до начала и сверьте в конце. Payload из движущегося дерева — не релиз.
  3. Step 3a — и не верьте CUSTOMIZED на 2–16 строк. Откройте дифф: если локальные формулировки старше тега, это дрейф происхождения. Перезаписывайте.
  4. Запустите python -m pytest .tfw/scripts из корня до --apply — не потому, что процедура велит, а потому что test_repository_accounting_balances сейчас единственное, что ловит схлопывание идентификаторов. После снятия доски он уже ничего не ловит.
  5. Читайте таблицу «Every board identifier, by name» на дубли. Один ID дважды — стоп, даже если строкой выше написано «exactly once».
  6. Grep снимок и status.md на ваши snake_case-имена. Если у вас есть функция normalize_text(), после миграции её будут звать normalizetext().
  7. .agent/rules/tfw.md и .claude/commands/ — проверьте версию руками. Здесь первый стоял на 0.8.5 два релиза подряд, а половина команд отставала от своих же воркфлоу.
  8. build.* перечитайте до --check project, а не после. Инструмент проверит путь к скрипту; make lint он проверить не может.
  9. Не заходите cd в .tfw/scripts/. Все команды — из корня проекта. Скрипты печатают разрешённый корень — читайте эту строку.
  10. UNDECLARED с очевидным ответом (регистр, синоним) разрешайте сразу событием; с неочевидным — оставляйте. Здесь один из четырёх был очевиден, три — нет.

Разбор полётов третьего внешнего обновления. Каждое число получено измерением в сессии, не оценкой. Автор — оператор обновления, не автор payload.


Продолжение: обновление того же проекта до 2.0.0-dirty.4 (2026-08-30) разобрано отдельно в FIELD-REPORT__TFW-60__helpdesk_dirty3_to_dirty4.md (имя с проектом и тегами: ординальное fourth заняла другая сессия в ту же минуту, см. filing note в том файле).