Field Report — TFW-60 / the first external project to run the 2.0.0 update path¶
Source:
KZ-IT-telegram-list, upgraded1.3.0→2.0.0-dirtyon 2026-08-27 Reporter: coordinator session (via: claude), on behalf ofsaubakirovConsumer 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 ## Board — not 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 pending → approved → executing). 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:31routes/tfw-researchto.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.upstreamassumes 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 intasks/while.tfw/anddocs/were clean.initial_seqremoval 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.0project_config.yamland is not named in the update workflow's list of things to remove. It was dropped here by inference frominit.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.