Skip to content

Field Report — TFW-60 / the first external project to run the 2.0.0 update path

Source: KZ-IT-telegram-list, upgraded 1.3.02.0.0-dirty on 2026-08-27 Reporter: coordinator session (via: claude), on behalf of saubakirov Consumer commit: 97dd429 — 66 files, +4271 / −344 Status: advisory. This file states what happened and recommends; it decides nothing, amends no frozen section, and changes no task state.

2.0.0-dirty was cut, in its own words, "so the update path can be exercised against real projects before 2.0.0 is claimed." This is that exercise, reported back. Phase A is closed; Phases B and C are not, so the findings below are written to be actionable there.

1. Outcome

The update completed and every gate passes:

Gate Result
python docs/scripts/gen_index.py --validate exit 0 — 2 tasks against the closed schema
test_gen_index.py + test_migrate_board.py 131 passed, 1 skipped
Consumer schema validation exit 0
Consumer generated-README currency exit 0

Payload applied: 41 .tfw/ files modified, 3 added (status.md, journal_event.md, team_profile.md). State migrated: board retired, BOARD-SNAPSHOT.md captured, one task at PHASES with four phase status.md files, one journal event, one team/ profile, derived 00-INDEX.md.

The file copying took minutes. The rest of the session was reconstructing what to do and in what order. That ratio is the finding this report exists to deliver.

2. Consumer profile

Relevant because it determines which assumptions held:

Property Value
Corpus 4 tasks, one multi-phase and in flight at an owner gate
Board location tasks/README.md under ## Boardnot root README.md
Why root README.md is fully regenerated by scripts/generate_readme.py; a board there is destroyed
Legacy ids TFW-01_…, TFW-02_… (single _), TFW-3__…, TFW-4__… (double __)
Adapters in use Claude Code + Codex; no Cursor, no Antigravity
Upstream source a local working tree at tag v2.0.0-dirty, not the URL in tfw.upstream

3. What worked, and should not be lost

The closed schema made the migration race-tolerant. While this update ran, another agent rewrote the consumer's board three times (G2 pendingapprovedexecuting). The race did not corrupt status.md — because the schema forbids exactly the content that was moving. Volatile gate prose has nowhere to go in the carrier; it stayed in the HL and TS. This is the design paying off under precisely the conditions it was built for, and it is worth stating explicitly somewhere a reader will find it.

migrate_board.py's accounting is honest engineering. Refusing an empty board ("a snapshot of an empty board is not a snapshot, it is a deleted trace"), refusing to overwrite, naming every identifier individually rather than asserting a count, --skip-existing for a corpus that has moved on. The manifest was trustworthy enough to act on.

gen_index.py declares its own non-authority, names its source count and freshness, and routes the reader back to status.md. A derived view that argues for its own irrelevance is the right shape.

A technique the update workflow should recommend but does not: before merging anything, diff every local .tfw/ file against the pristine previous tag (git show v1.3.0:.tfw/<f>). All 38 files here were untouched, which collapsed the three 🟡 "manual merge" files — conventions.md (212 changed lines), glossary.md, project_config.yaml — to zero manual merges. This single check removed most of the update's risk and is absent from update.md.

4. Findings

Ordered by how much they block the next project.

F1 — The migration tooling is not in the payload · blocker

gen_index.py and migrate_board.py live in this repository's docs/scripts/, outside .tfw/. update.md Step 3 and Step 6 never mention them, and no template ships them.

Consequence: the CHANGELOG's own migration instructions are unfollowable. A project that reads "run python docs/scripts/migrate_board.py" does not have that file and has no instruction that tells it to obtain one. This consumer imported them by hand and had to invent a placement rule.

Aggravating detail: gen_index.py resolves the project root as parents[2] of its own file, so the path is not merely conventional — the depth is load-bearing. .tfw/conventions.md and .tfw/workflows/init.md also reference the literal string docs/scripts/, so a project that puts the tools anywhere else must edit .tfw/ and forfeit clean future updates.

F2 — A major breaking release ships no migration guide · high

update.md changed by exactly one line between 1.3.0 and 2.0.0 — for a release that retires a required artifact and relocates live state. Everything procedural lives in CHANGELOG prose, and that prose documents this repository's own migration (task_containers: [workspace, tasks], 7,505 references, 666 files) rather than a general procedure. The ordering constraint that matters most — migrate, then generate, then remove the board — appears once, in a code fence, inside a section a reader reaches after ~150 lines.

F3 — migrate_board.py hardcodes the board's location · high

BOARD_HEADING = "## Task Board" and read_board() reading root README.md are constants. This consumer's board was legitimately elsewhere, for a documented reason. Run as shipped, the parser returns zero rows — and the refusal message then suggests --board-rev, sending the reader to diagnose a removed board when the real cause is a relocated one.

The row parser itself needed no change: it already reads ID/Task/Status as cells 0–2 and matched this project's nine-column table unmodified. Only the locator was wrong.

Patch available. --board and --board-heading were added additively here, upstream defaults unchanged, and all 60 test_migrate_board.py tests still pass. Offered upstream on request; until accepted it is a local delta that every future update must re-apply.

F4 — Unmatched legacy directories are reported as something false · high

TASK_DIR requires a __ separator. TFW-01_awesome_list_restructure and TFW-02_enhanced_validation use a single _, so the run saw 2 task directories where 4 exist.

Both were then classified as board-only rows and rendered in 00-INDEX.md under:

Backlog"Rows the board carried that never became a task directory. They are ideas, not work in progress."

Both have real directories containing completed HL/TS/RF traces. No trace was lost — both are terminal, so no status.md was due, and both appear in BOARD-SNAPSHOT.md. But a generated artifact now asserts something untrue about real work, and the migration manifest prints the same false reason ("backlog idea, never started").

This contradicts an explicit promise in conventions.md: the index "reports every legacy, malformed or unresolved input rather than dropping it." The mechanism exists — Unresolved inputs — and this path bypasses it. Silently dropping would be bad; confidently misdescribing is worse, because it reads as a finding.

F5 — status.md is easy to write invalidly · medium

The first hand-written batch produced 5 unparseable files. Cause: title and summary values contain ": ", which ends a YAML plain scalar. The template documents bounds (<= 80 code points) but never that values need quoting, and its example line — title: short task name — actively models the unquoted form.

The validator reported only unparseable front matter: ScannerError, with no key named, so the cause had to be found by inspection. This is the one place in 2.0.0 where a person is asked to hand-author a machine-read file with no writer and no worked example.

F6 — task_containers is a new required decision, presented as a preserved value · medium

update.md names it only in the "Project sections (preserve)" list. But it does not exist at 1.3.0 — there is nothing to preserve. It decides where new tasks are created and whether an old corpus path keeps resolving, and relocating or renaming the container is possible. Nothing in the update path surfaces it as a choice, so it gets set silently by whoever runs the update. Reported independently by the consumer's owner.

F7 — team/ cannot be delivered by the update · medium

team/README.md is a tracked file in this repository but sits outside .tfw/, so no step of update.md copies it and no template contains it. An updating project receives a team/ directory with no explanation of what it is — if it creates one at all: update.md never says to create team/. This consumer learned the profile was required only because test_gen_index.py::test_the_repository_itself_declares_a_human failed. Reported independently by the consumer's owner.

F8 — The migration reads a live file by default · medium

read_board() defaults to the working tree; --board-rev exists but is presented as the recovery path for an already-removed board. During this run the source file changed three times underneath the reader. A committed revision is the stable input, and it is the safer default for a migration whose entire value proposition is exact accounting.

There is also no guidance anywhere that says do not migrate while a participant is mid-gate — which is a notable omission in a release whose purpose is concurrent participation.

F9 — No vocabulary for an inbound external record · low, structural

This report is a coordination-relevant event in TFW-60's life, and no kind in the closed journal vocabulary covers it: created, dispatch, handoff, transition, ownership_changed, amendment_escalated, consolidation (reserved). It is not an amendment — it escalates nothing and requests no verdict.

No journal event was written for this file, deliberately: inventing a kind would violate the closed vocabulary, and forcing it into amendment_escalated would misreport it as awaiting an owner ruling. Flagged rather than worked around.

F10 — UNDECLARED resolution is ambiguous · low

status.md says: "Normalizing such a value to a declared one is prohibited." Migration correctly wrote UNDECLARED for this consumer's multi-phase task, since the board carried free prose.

But the correct value was knowable: conventions.md defines PHASES as the task-level lifecycle for a multi-phase task, and the old free text was a phase rollup the same release now prohibits. The coordinator set PHASES and recorded a transition event carrying from: UNDECLARED.

The prohibition reads as absolute, and it is not obvious from the text whether that act is permitted. The distinction worth stating: migration must never normalize; an accountable owner may resolve, through a recorded transition. Left as written, projects will either strand tasks at UNDECLARED — where consumers treat them as non-actionable — or resolve them without a trace.

5. Recommendations

# Recommendation Fixes
1 Ship the tooling inside the payload — .tfw/scripts/ — and reference it by that path in conventions.md and init.md. If it must stay in docs/scripts/, add it to the Step 6 sync table and state the parents[2] depth requirement F1
2 Add .tfw/migrations/<version>.md, and make update.md Step 3 route there when the update crosses a major. Rule: a major release without a migration guide is incomplete F2
3 Read the board's location from configuration, or accept --board / --board-heading (patch available, tests green) F3
4 Route directories the identifier grammar does not match into Unresolved inputs, never into Backlog. Never print a reason the source did not carry F4
5 Quote the values in the status.md template's example, and make --validate name the offending key F5
6 Present task_containers as an explicit decision in update.md, with the two real options (single container; old container second to keep paths resolving) F6
7 Make team/ part of delivery: a README template inside .tfw/, plus an explicit "create the acting profile before the first durable write" step F7
8 Default read_board() to a committed revision, and add a quiescence note to the migration guide F8
9 Ship a post-update self-check — gen_index.py --doctor — answering "is this project consistent with 2.0.0?" The best signal available was two failing framework tests a consumer is never told to run F5, F7
10 State explicitly that migration must not normalize UNDECLARED while an owner may resolve it through a recorded transition F10
11 Decide a kind for inbound external records, or state that they are artifacts without events F9

6. Also observed, outside TFW-60's scope

  • TD-11 is unfixed across two releases. .tfw/adapters/claude-code/CLAUDE.md.template:31 routes /tfw-research to .tfw/workflows/research.md, which does not exist; the real path is .tfw/workflows/research/base.md. A project that reinstalls or re-syncs the Claude Code adapter from source inherits a broken route.
  • tfw.upstream assumes a clonable URL. The real source here was a local working tree. Step 0 has no local-path form, and nothing instructs the operator to verify that the source's own .tfw/ is clean at the tag before trusting the payload — which mattered, since the source tree was dirty in tasks/ while .tfw/ and docs/ were clean.
  • initial_seq removal is correct but silent. It is retired at 2.0.0 because identifiers read no counter, yet it still appears in every pre-2.0.0 project_config.yaml and is not named in the update workflow's list of things to remove. It was dropped here by inference from init.md.

7. What this consumer changed locally, and will have to re-apply

Recorded so the next update does not silently lose it:

Local delta Reason
--board / --board-heading in migrate_board.py F3
team/README.md copied by hand from this repository F7
Tooling placed at docs/scripts/ with a README explaining the constraint F1

Tracked in the consumer as [TD-12](../../reference/tech-debt.md)[TD-18](../../reference/tech-debt.md).

Ещё от меня вижу странность не зна ю в какой момент возникла D:\projects\KZ-IT-telegram-list\tasks\TFW-4__showcase_reorg\phases\phase-d, вот такая папка phases родилась откуда-то, но пустая хотя бы

также обратил внимание, что при создании новой задачи через tfw-plan, теперь название чат сессии меняется, но если раньше там был номер задачи, теперь остался только роль + слаг. Видимо к моменту номер еще неизвестен или незафиксирован. Непродуманно, мне нравится что кодекс сразу знает как назвать чат сессию и правильно называет. Еще мне нравится, что если агент при запуске получил сразу фазу, то он еще её в названии сессии пишет. добавлено чуть позже, мне нравится что план сначала заставил немного разобраться в задаче и задать вопросы, и только потом он начал создавать папку и HL, получается надо просто напомнить про наименование сессии где-то после этого шага, либо же в самом начале он должен определить запиисать а потом не создавая задачу и каталога задавать вопросы и потом только. и при смене слага не забывать сменить. И в конце после создания задачи и HL кодекс молодец не забыл переименовать себя под задачу как надо.


Field report from KZ-IT-telegram-list. Advisory input to TFW-60 Phases B and C. It amends no frozen section and requests no verdict.