RF — TFW-60 / Phase AA: Portable Delivery¶
Date: 2026-08-27 Author: Claude Code (Executor),
on_behalf_of: saubakirov,via: claudeStatus: 🟢 RF — revision 3, adding §11 for the R4 corrective pass. Written after the phase was closedDONE, because the owner's own closing outcome names its absence: "the pass has no RF of its own." Closes TD-199 Parent HL: HL-TFW-60 Phase HL: HL Phase AA TS: TS Phase AA at revision 4 — §11 answers AC-15 ONB: ONB Phase AA — eight blocking questions answered REVIEW: first 🔄 REVISE · rev2 ✅ APPROVE · rev3 ✅ APPROVE by owner ruling over the reviewer's REVISE Field report: second external update — filed verbatim, not authored here Pinned at:1079020for the phase's measurements; revision 2's own work is measured at its own commit and says so per item. Two files unrelated to this phase (TFW-55, TFW-54) stay dirty in the working tree and are excluded from every commit — ONB Risk 6 ruling.
1. What Was Done¶
Three commits. Every rename is recorded as a rename.
| Commit | What |
|---|---|
f14f744 |
the seven moves, and everything that names their result |
80c2ed5 |
the profile template cut to its own job |
1079020 |
two findings the external fixture produced |
0215aca · 440d6fd |
RF, EV and evidence; then the parser claim replaced by a measurement |
| revision 2 | the review's three items, plus four the review's own generalization surfaced |
Created — 1¶
| Path | What it is |
|---|---|
.tfw/migrations/2.0.0.md |
the migration procedure, written for a project that is not this one |
Two files the TS withdrew at R3 were not created, and their responsibilities went
somewhere that already existed: the team/ README template (the profile template's guidance
comment carries the orientation, and team/ is now created together with its first profile,
so an unexplained directory never appears) and .tfw/scripts/README.md (--help says how,
the guide says when, conventions.md says where).
Moved — 7, all git mv, history verified to follow¶
| From | To |
|---|---|
docs/scripts/gen_index.py |
.tfw/scripts/gen_index.py |
docs/scripts/migrate_board.py |
.tfw/scripts/migrate_board.py |
docs/scripts/test_gen_index.py |
.tfw/scripts/test_gen_index.py |
docs/scripts/test_migrate_board.py |
.tfw/scripts/test_migrate_board.py |
.tfw/templates/team_profile.md |
.tfw/templates/team/profile.md |
.tfw/templates/journal_event.md |
.tfw/templates/journal/event.md |
.tfw/templates/topic_file.md |
.tfw/templates/knowledge/topic.md |
git log --follow at the declared pin 1079020 returns 9 commits for gen_index.py,
6 for migrate_board.py, 3 for team/profile.md, 3 for journal/event.md, 5 for
knowledge/topic.md. Revision 2 corrects the first two: they were 8 and 5, the values at
80c2ed5, taken before the third commit and then reported against a later pin. TS §6 makes
the pin a rule and a measurement has to be taken at the commit it names.
Modified — 25 by the census's method · 31 distinct paths in the table below¶
Both numbers, because they count different things and the earlier heading gave one while the table showed the other. The census classifies the four scripts and three templates as moves and excludes them from the modified count — 25 is that figure, it is the one measured against the budget of 30, and it is the basis the coordinator approved. The table below then lists those same files as rows, because they were modified as well as moved: 31 distinct paths were touched in total.
Neither number is wrong; reporting only the first over a table showing the second is. The
review recorded this as D3, and the census's basis for the classification is where it
genuinely needs a coordinator ruling: the TS predicted the scripts would "relocate rather
than get rewritten … cost a move and their path constants", and gen_index.py changed 507
lines, migrate_board.py 246. That prediction did not survive execution. It belongs in the
next phase's budget table, and this row is the pointer to it.
| Group | Paths | What changed |
|---|---|---|
| Payload tooling | .tfw/scripts/gen_index.py, .tfw/scripts/migrate_board.py |
marker-based root resolution; --check {index,tasks,project}; board locator and committed-revision default on one code path; unmatched directories reported; key-naming validator; ASCII messages; tolerant streams |
| Payload tests | .tfw/scripts/test_gen_index.py, .tfw/scripts/test_migrate_board.py |
+33 tests; the migration fixture is now a Git repository so every existing test runs through the new default |
| Normative | .tfw/conventions.md, .tfw/glossary.md, .tfw/project_config.yaml, .tfw/workflows/init.md, .tfw/workflows/knowledge.md |
payload paths; four new canon sections; the build block's five-line comment deleted |
| Update path | .tfw/workflows/update.md |
Step 3 guide routing, Step 3a pristine-tag diff, the two decisions, the team/ step, local-source form, Step 8 self-check |
| Session naming | .tfw/workflows/plan.md |
Step 0 removed; naming is step 3 of Step 4, after the identifier exists |
| Templates | .tfw/templates/status.md, .tfw/templates/project_config.yaml, .tfw/templates/team/profile.md |
quoted example + worked example; a real verify command; the profile comment cut to its own job |
| Adapter sources | .tfw/adapters/claude-code/CLAUDE.md.template, .tfw/adapters/claude-code/README.md, .tfw/adapters/antigravity/README.md |
TD-11 in all three, not one |
| Docs tooling | docs/scripts/gen_docs.py, docs/scripts/test_integration.py |
cross-directory import bootstrap; +7 tests |
| Release surface | .tfw/VERSION, .tfw/CHANGELOG.md, README.md |
2.0.0-dirty.2, the entry, the payload path |
| Project docs | team/README.md |
the template link the move would have broken |
| Adapter copies | .claude/commands/tfw-{init,update,plan,knowledge}.md, .agent/workflows/tfw-{init,update,plan,knowledge}.md |
re-synced, verified byte-identical |
Deviations from the TS's Affected Files, both raised in the census before acting:
docs/mkdocs.ymlwas listed and needed no change —gen-filesrunsscripts/gen_docs.pyrelative todocs/, which the move does not touch. Modify is 25, not- Verified by
mkdocs buildexit 0. - The three
.agents/skills/entries were listed and needed no change — Codex skills route by path and do not copy workflow content. Verified: all 11 byte-identical to.tfw/adapters/codex/skills/, and none of the five that name a template names a moving one. - Four files the TS's table does not enumerate were modified:
.tfw/workflows/knowledge.mdand its two copies, plusteam/README.md. All four are reached only through R3's own template moves. Raised in census.md § "Group appearing"; no limit crossed at 25 of 30. .tfw/templates/project_config.yamlwas modified beyond a path reference:build.verifybecame a real command instead of a placeholder, because a receiving project can run it from the moment the payload lands.
2. Key Decisions¶
| # | Decision | Why |
|---|---|---|
| D1 | Root resolution walks upward for .tfw/, skipping any path containing .upstream |
update.md Step 0 clones a full .tfw/ into .tfw/.upstream/, which satisfies the marker. Without the skip, a tool run from inside staging would generate a project's index from the upstream clone — silently, with a plausible-looking result |
| D2 | iter_unmatched_task_dirs() as an additive sibling, not a changed return type |
iter_task_dirs is called by gen_docs.py, migrate_board.py and the tests. A tuple return breaks all three for no gain |
| D3 | --board / --board-heading are flags, not configuration keys |
Relocating a board is a fact about one run of a once-per-project act. A key read forever to answer a question asked once is surface with no reader |
| D4 | A new reconciliation class, board-only, directory unresolved |
A row whose directory exists but whose name the grammar rejects is neither matched nor board-only. Reusing either would have made the manifest assert something false, which is the defect being fixed |
| D5 | The unresolved reason talks about the name and nothing else | The only fact available is the directory name. A reason about whether work happened is a claim the source never carried |
| D6 | The migration fixture became a Git repository, rather than passing --working-tree everywhere |
Since the committed revision is now the default, a non-repository fixture would leave the path every real project takes exercised by nothing — a check reported as passing that never ran |
| D7 | Runtime messages are ASCII; content is not | Both reach a console whose codepage nobody chose. A message can be rewritten; a verbatim board block cannot, so streams are made tolerant instead. Enforced by a test over print(/SystemExit( spans rather than by fixing the occurrences one at a time — a heuristic with a stated reach, not a proof over every possible message |
| D8 | The index names its own generator by resolved path | A project that placed the tools elsewhere gets an index naming a command it can actually run. Still deterministic: the same tree produces the same bytes |
| D9 | update.md stayed under the 1200-word ceiling by deleting duplication, not required content |
Step 3a measures which files are customized, so the "typically safe" and "requires merge" lists were guesses standing where a measurement now exists. 835 → 1165 words with five ACs added |
| D10 | team_readme.md was not created at all, and the profile template shrank |
The ONB's naming catch (team_README.md breaks §10.4) was the small answer. R3 asked the larger question, and the same duplication of conventions.md sat in the profile template's comment. 50 lines → 33 |
| D11 | AC-13's fixture was pointed at a commit SHA, not a tag | Q3 ruling (b). git archive <sha> exercises the same mechanics as a tag; a tag adds a name, and asserting releasability is the reviewer's finding |
3. Acceptance Criteria¶
| AC | Verdict | Where |
|---|---|---|
| AC-1 — tooling ships inside the payload | ✅ | E1–E5. Every gate hit classified; the depth test run at three placements |
| AC-2 — a major release ships a migration guide | ✅ | E6–E10 |
| AC-3 — migration finds a board wherever the project keeps it | ✅ | E11–E13. The parser's untouchedness is measured: one changed line, the locator |
| AC-4 — an unmatched directory is reported, never described | ✅ | E14–E18, on the exact corpus that produced the finding |
| AC-5 — a person can hand-author the carrier correctly | ✅ | E19–E22 |
AC-6 — task_containers is presented as a decision |
✅ | E23–E24 |
AC-7 — team/ is delivered, not assumed |
✅ | E25–E27 |
| AC-8 — migration reads a stable input | ✅ | E28–E32 |
| AC-9 — one command answers project consistency | ✅ | E33–E38 |
| AC-10 — the update path carries the technique that made it safe | ✅ | E39–E41 |
| AC-11 — shipped instructions name files that exist | ✅ | E42–E44 |
| AC-12 — the session name carries the task once it exists | ✅ | E45–E46 |
| AC-13 half one — development fixture | ✅ | E47–E50 |
| AC-13 half two — acceptance evidence | ❌ UNMET | E51. The executor cannot close this. See below |
| AC-14 — the release describes what shipped | ✅ | E52–E57. The tag is the coordinator's act (E53) |
AC-13 half two is unmet, and this is the phase's declared outcome¶
The declared outcome of Phase AA is a project other than this one completes the update from the payload alone. What was produced is a development fixture: a clone in a scratch directory, driven by the same agent that wrote the code. It built AC-2, AC-3, AC-4, AC-6 and AC-8 against a real corpus, and it found three defects nothing here could have found. It is not acceptance evidence, because the point of DoD 19 is an operator who is not the author.
Reported unmet and handed back, per the ONB Q1 ruling. What closes it: the owner updating at
least one real external project, filed at task root as
FIELD-REPORT__TFW-60__second_external_update.md.
4. Verification¶
- Lint (
python -m pytest .tfw/scripts/ docs/scripts/ -q --collect-only): 254 tests collected, no collection errors - Tests (
python -m pytest .tfw/scripts/ docs/scripts/ -q): 253 passed, 1 skipped in 303 s. Baseline before the phase: 220 passed, 1 skipped. Net +33 - Verify (
python .tfw/scripts/gen_index.py --check tasks): 53 tasks validate against the closed schema, exit 0 --check project: exit 0 — version2.0.0-dirty.2, 1 participant, creates inworkspace, resolves across[workspace, tasks]mkdocs build --config-file docs/mkdocs.yml: exit 0, built in 264 s. This is the gate forgen_docs.py's cross-directory import, which mkdocs runs withdocs/as its config root- Adapter sync:
cmpover all 22 workflow copies and 11 Codex skills → all identical update.mdword count: 1165, under the §11 ceiling of 1200
Checks that failed first, then passed — recorded because a check whose failing branch was never taken is not a check:
| Check | Failed on | Then |
|---|---|---|
test_every_path_an_adapter_source_names_resolves |
the three real adapter sources naming .tfw/workflows/research.md |
passes after the fix |
test_every_path_an_installed_adapter_copy_names_resolves |
four copies naming the pre-move template paths | passes after the sweep |
test_every_runtime_message_is_ascii |
five real occurrences, including two of my own (✗, ·) |
passes. What it enforces, stated so its silence is not over-read: it scans payload script lines, toggles on a print( or SystemExit( and resets at a line ending in ), then rejects non-ASCII inside that span. That covers a literal handed to a print or a raise — every case in these two files — and not a message assembled into a variable and printed later |
test_the_status_template_examples_parse_and_validate |
written against the template before it was fixed | passes |
test_a_manifest_containing_the_project_own_characters_prints |
reproduced the fixture's UnicodeEncodeError |
passes |
test_the_adapter_path_check_actually_fires |
its own broken-entry fixture, by construction | passes |
5. Evidence¶
See EV file for evidence details.
Evidence verdict: 58/60 VERIFIED, 1 DEFERRED, 0 BLOCKED, 1 N/A
The DEFERRED item is E51, AC-13 half two, with the specific blocker named. The N/A is E53,
the v2.0.0-dirty.2 tag, by the coordinator's Q3 ruling.
6. Observations (out-of-scope, not modified)¶
| # | File | Line(s) | Type | Description |
|---|---|---|---|---|
| 1 | KNOWLEDGE.md |
22 | naming | Names docs/scripts/gen_index.py in the architecture table. The known exception from the ONB Q5 ruling (b): owned by /tfw-docs under the D37 split, so the executor does not write it. AC-1's gate names it explicitly so this RF does not report a green gate over a red one. Closes by /tfw-docs after approval |
| 2 | tasks/BOARD-SNAPSHOT.md |
— | naming | SCREAMING-KEBAB, matching nothing else in the tree. Deliberately not renamed — Phase A's links resolve to it and a rename would break them days after they were established. Recorded per TS §6 so it is a decision rather than an oversight |
| 3 | .tfw/workflows/init.md, .tfw/workflows/plan.md |
whole file | style | 1,897 and 1,598 words against the §11 design ceiling of 1,200. Both were already over before this phase (1,821 and 1,501); this phase added 76 and 97. update.md was brought under the ceiling by deleting duplication rather than by cutting required content, and the same is available here — init.md's tutorial asides and plan.md's worked pseudocode are the candidates. Not attempted: neither file's overrun is in this phase's scope, and cutting a workflow is a change reviewers should see on its own |
| 4 | .tfw/scripts/gen_index.py |
1150–1230 | duplication | check_project re-implements the build.* path check that --check tasks does not have, and gen_docs.py:_find_root restates find_project_root in ten lines. The second is a genuine bootstrap — gen_docs needs the root to locate the module it would import the function from — and the first is small. Both are the honest kind of duplication; recorded so a later reader does not read them as oversights |
| 5 | .tfw/scripts/gen_index.py |
— | missing-test | --check project does not report a retired framework file still sitting in a project's .tfw/ (the fixture found templates/topic_file.md). The migration guide now finds them by command. Not added to the check: payload completeness and payload minimality are two different claims, and the second needs a manifest of what the release ships, which does not exist |
| 6 | docs/scripts/test_integration.py |
248 | perf | The full docs/scripts/ suite takes ~290 s of the 303 s total, almost all of it mkdocs builds inside tests. The payload suite alone is 15 s. A receiving project runs only the payload suite, so this does not reach them — but it makes this repository's own gate slow enough to be skipped |
| 7 | .tfw/CHANGELOG.md |
200–265 | style | The 2.0.0-dirty entry's migration code fence still names docs/scripts/, correctly — it is a record of that tag. The new entry says so and points at the guide. A reader who scrolls to the fence without reading the note above it can still copy a dead command. The structural fix would be a per-release "superseded by" marker, which is a CHANGELOG-format change and not this phase's |
| 8 | docs/scripts/test_integration.py |
428–470 | missing-test | test_no_normative_file_states_a_retired_rule does not scan the payload scripts' own comments and docstrings, and the stale --validate docstring at .tfw/scripts/test_gen_index.py:741 was found by hand rather than by it. The gap is deliberate: a comment explaining that a flag was retired legitimately names it, and no mechanical rule separates that from a docstring that still instructs — a line-proximity heuristic or a line-number allowlist would both rot. The shipped harm is bounded, because a receiving project reads templates and workflows and never reads these test files. Candidate fix if it recurs: require every retired name in payload code to sit inside a span the file marks as historical, which means adding such a marker convention first |
7. Fact Candidates¶
fact-candidates: processed 2026-08-30
| # | Category | Candidate | Source | Confidence |
|---|---|---|---|---|
| 1 | philosophy | When prose is required to distinguish two of your own names, the names are wrong — and deleting the prose without fixing the names hides the symptom. The owner applied this to --check/--validate/--doctor, where a five-line config comment had been accepted as the fix |
Owner, via TS R3 | High |
| 2 | philosophy | Every artifact a phase adds must name the responsibility it absorbs; a new file where an existing document should have gained a line is the defect. Two proposed files were withdrawn on this test after the owner asked the larger question | Owner, via TS R3 | High |
| 3 | process | A development fixture and acceptance evidence are different things, and an AC that conflates them cannot be closed honestly. The executor builds against the first; only an operator who is not the author produces the second | Coordinator, ONB Q1 answer | High |
| 4 | process | The live consumer project must never be written to by an agent working in this repository — it belongs to its owner, and a second agent writing into it is the defect TFW-60 exists to end | Coordinator, ONB Q1 answer | High |
| 5 | constraint | KZ-IT-telegram-list is the owner's real project and the only external TFW consumer. Its pre-update commit c919640 is a reusable fixture: TFW 1.3.0, board at tasks/README.md under ## Board, four task directories with mixed identifier grammar, one container |
Owner's field report + this run | High |
| 6 | environment | The owner's machine runs a console codepage that is not UTF-8, so any shipped tool printing project content must not assume UTF-8 stdout. This is why the migration guide's first command died and why no test here could have caught it | This run, on the owner's machine | High |
| 7 | philosophy | A rename that looks like a correct catch may still be the small answer. team_README.md → team_readme.md was right about the naming rule and wrong about the file existing at all |
Owner, via TS R3 | Medium |
| 8 | process | A rule corrected in the canon is not corrected until every shipped copy of its old wording is found, and the mechanical form of the check is to grep the old sentence rather than the concept. The two files a reviewer reads were right; the missed one was the carrier template a receiving project hand-authors from. Running that grep in revision 2 found three more instances of the same class, one of them in code the same phase had just edited | Reviewer, REVIEW TFW-60/AA §7 | High |
| 9 | process | A review item and the class it belongs to are not the same size. The review filed one file and one line; generalizing the finding it named turned up four more sites and one deliberate exemption worth recording. Closing the item without running its own stated mechanic would have left the class open and looked complete | Reviewer's own fact candidate, applied in revision 2 | High |
8. Strategic Insights (Execution)¶
fact-candidates: processed 2026-08-30
| # | Insight | Category | Source |
|---|---|---|---|
| S1 | The owner's instruction for this phase was "подход изменился в целом в сторону качества архитектуры, а не фиксов ради фиксов" — and it changed what the work is. Under a fix-oriented reading, AC-9 gets a third flag and AC-7 gets a README template, both closing their AC. Under the owner's reading, the third flag is the finding: three names for three questions about three subjects collapse into one flag, and the config comment that existed to tell two of them apart gets deleted rather than extended. Implication: an AC that names a symptom should be read as an invitation to find what the symptom is a symptom of. Phase AA ended up removing more surface than it added — 1 created against 2 withdrawn and 3 pulled out of a flat namespace — and that was the owner's correction, not the plan's | philosophy | User, 2026-08-27, on handing back the amended TS |
| S2 | Testing a delivery mechanism inside the repository that produces it is structurally blind, not merely incomplete. Every finding this phase closed came from one external run; four review rounds here produced none of them — and the reason is mechanical. parents[2] resolves correctly from .tfw/scripts/ by coincidence, so the depth defect would have passed every test written here. Implication: for any capability whose subject is another project, the fixture must be another project, and the phase's own gate should say so in a form an executor cannot satisfy locally. AC-13's split into a development half and an acceptance half is the shape that survives |
process | Coordinator, ONB Q1 answer, extended by this run |
| S3 | A check that reports and exits is cheap to trust and cheap to add; the moment it repairs anything it becomes a second authority over state that a task's own file already owns. --check project was deliberately built as a reporter — and the constraint paid off in an unexpected place: because it writes nothing, test_no_check_subject_writes_anything could byte-compare the entire tree before and after all three subjects, which is a much stronger assertion than any per-check test. Implication: the non-authority constraint is not only a safety rule, it is what makes the check testable as a class |
philosophy | PV 1 (one authoritative owner per truth type), applied |
| S4 | A phase that legislates against a failure mode is the most likely place to commit it. This phase's own DoF names "a check reported as passing that never ran" in four forms, and its review found four claims stated wider than their evidence — mine. It rewrote the absolute UNDECLARED rule and left the sentence standing in the one copy a receiving project reads. It removed depth arithmetic from the tools and left it in the tools' own tests. None of these is carelessness in a different area; each is the phase's declared subject, reappearing in the place the author was not looking. Implication: the finding of a phase should be turned back on the phase before the RF is written — grep the retired string, re-measure the claim at the pin it declares, run the gate against the pre-fix file. Two of the four items above are things a five-minute sweep would have caught before review, and the sweep is now a test rather than an intention |
philosophy | Reviewer + this revision |
9. Diagrams¶
The payload boundary, before and after¶
BEFORE — the release named files the payload did not carry
/tfw-update copies ──► .tfw/ ── rules · templates · workflows · adapters
│
│ these were REQUIRED but OUTSIDE:
docs/scripts/gen_index.py ✖ ─────┤ conventions.md names the path
docs/scripts/migrate_board.py ✖ ─────┤ CHANGELOG says "run this"
team/README.md ✖ ─────┤ no step creates team/
a migration guide ✖ ─────┘ does not exist
and the tools resolved the root as parents[2], so the DEPTH was load-bearing:
a project placing them elsewhere had to edit .tfw/ and forfeit clean updates
AFTER — everything the release asks for is inside the payload,
or already in the receiving project
.tfw/
├── scripts/ gen_index.py · migrate_board.py + their tests
│ root found by walking upward for .tfw/ ── ANY depth
├── migrations/2.0.0.md the procedure, for a project that is not this one
├── templates/
│ ├── team/profile.md ──► team/<handle>.md
│ ├── journal/event.md ──► {task}/journal/<name>.md
│ └── knowledge/topic.md ──► knowledge/<topic>.md
└── workflows/update.md Step 3 ──► the guide, when a major is crossed
Step 3a ──► diff against the source's pristine tag
Step 8 ──► --check project
withdrawn rather than created: team/ README template · scripts/README.md
(each duplicated a document that already existed)
--check: three subjects, one flag¶
┌─────────────────────────────────────────────┐
│ BEFORE: two flags + a 5-line comment │
│ telling them apart, and a third proposed │
└─────────────────────────────────────────────┘
│
▼
gen_index.py --check ─┬─ index is the derived view current?
│ never a gate on a task transition
│
├─ tasks is each task's own state legal?
│ THE BUILD GATE. task-local only, so it
│ cannot be blocked by a stale index
│
└─ project is this project consistent with the release?
payload · team/ · containers · retired
keys · version marker · carrier validity
every subject: reports and exits · writes nothing · is authority over nothing
and states what it did NOT check
How a board reaches task-local state¶
--board PATH ────────┐
--board-heading H ───┼──► read_board() ──► git show REV:PATH ◄── ONE code path.
--board-rev REV ─────┤ │ Reading the path
(default: HEAD) │ │ from the working
--working-tree ──────┘ │ tree while logging
│ a revision would be
▼ false provenance.
parse_board(text, H)
(parser untouched: it already
read a 9-column table)
│
▼
reconcile() ──► ONE classification, read by both consumers
│
┌───────────────────────┼───────────────────────┬──────────────────────┐
▼ ▼ ▼ ▼
matched board-only board-only, directory-only
row + directory no directory directory unresolved no row
│ at all name rejects the grammar │
▼ ▼ ▼ ▼
status.md written snapshot: Backlog NO state file. reported
(unless terminal / Absorbed Reported as unresolved,
or absorbed) in BOTH the manifest and
the index, for the same
stated reason.
A person may rename it;
no tool ever will.
10. Revision 2 — what the review returned, and what generalizing it found¶
REVIEW 440d6fd returned 🔄 REVISE with three items. Two were in one file and one was a
set of four claims stated wider than their evidence. All are closed below.
The review's §7 fact candidate also named the mechanical form of the finding: "grep the old sentence, not the concept, before declaring the rewrite complete." Running that grep found three more instances of the same class that neither the RF nor the review had caught. They are closed here too, because a review item and the class it belongs to are not the same size.
The review's three items¶
| # | Item | What was done |
|---|---|---|
| 1 | .tfw/templates/status.md stated the retired absolute UNDECLARED prohibition — verbatim the sentence f14f744 rewrote in conventions.md §5 and glossary.md |
Replaced with the two-act rule, citing §5 rather than restating its table. The release note's Canon entry now says the rule lives in three files and records that the third was missed on the first pass |
| 2 | Same file: "the four keys that are never prose" listed six | "The six keys that are never prose." Counted |
| 3 | Four claims looser than their evidence | (a) E47 carries the __pycache__ caveat at the row, with the transcript's own 1 quoted and explained. (b) RF §1's git log --follow counts re-measured at the declared pin: 9 and 6, not 8 and 5 — those were the values at 80c2ed5, reported against a later pin. (c) E60 and RF §4 now state what test_every_runtime_message_is_ascii enforces — a print(/SystemExit( span scan that reaches a literal and not a message assembled into a variable. (d) ac3_parser_untouched.txt records both measurements, each re-derived here rather than copied: 40 → 45 whole-function with four differing code lines forming two replacements (signature, locator) and five added docstring lines, and 34 → 34 body-only with one replacement. Stating the second alone was the defect. The review reported 42 → 47 for the first; the gap is trailing blank lines under a different extraction method, and the artifact states its method and says so — a number taken from someone else's run is not one this artifact can vouch for |
What generalizing item 1 found¶
| # | Where | What | Class |
|---|---|---|---|
| 4 | .tfw/scripts/test_gen_index.py:741 |
A docstring reading "Driven through gen_index.main(--validate), the command the build gate runs" — naming a flag this phase retired, in a test whose call site the same phase updated. sed moved the arguments and left the prose |
Exactly item 1's class, in my own code, and neither the RF nor the review caught it |
| 5 | .tfw/scripts/test_gen_index.py:20, test_migrate_board.py:22 |
PROJECT_ROOT = Path(__file__).resolve().parents[2] — depth arithmetic in the test files of the module that stopped depending on depth in this phase. Correct today by the same coincidence AC-1 exists to remove, and it breaks confusingly the moment a project relocates the tools. Now find_project_root(Path(__file__)) |
The review saw it — "parents[2] survives only in a docstring and two test constants" — and did not file it |
| 6 | .tfw/CHANGELOG.md |
My own release note cited [TD-11](../../../reference/tech-debt.md), an identifier TECH_DEBT.md does not carry; the historical TD-11 was an unrelated purged defect. The reviewer filed this as TD-191 with two options, one of which was "drop the ID from the entry" |
Took the executor half: the label is dropped and why is stated. Registering the defect under a fresh ID is TECH_DEBT.md, which is not mine to write, so TD-191 stays open for that decision |
| 7 | .tfw/CHANGELOG.md:168 — the [2.0.0-dirty] entry's "normalizing it away is prohibited" |
Deliberately not changed, and classified rather than passed over silently. A changelog entry records what a release shipped, and 2.0.0-dirty shipped the absolute rule. Rewriting it would make the record describe something that did not happen — the same P9 logic as the eleven provenance comments. Note it is a paraphrase, so the exact-string grep does not reach it; the broader prohibited grep does, which is why both were run |
Recorded so the next person running this grep does not re-derive the exemption |
The class is now detectable rather than remembered¶
test_no_normative_file_states_a_retired_rule checks a named registry of retired wordings —
currently the absolute UNDECLARED sentence, --validate and --doctor — against every
payload file that instructs: templates/, workflows/, migrations/, conventions.md,
glossary.md, README.md, quickstart.md, compilable_contract.md.
CHANGELOG.md is excluded, and the exclusion is a rule rather than a convenience: a
changelog records, and item 7 above is why that distinction has to live in the test rather
than in whoever reads its output.
What it does not reach, said here rather than left to be discovered: the payload scripts' own comments and docstrings. A comment explaining that a flag was retired legitimately names it, and no mechanical rule separates that from a docstring that still instructs — item 4 was found by hand and would be found by hand again. The shipped harm is bounded, because a receiving project reads templates and workflows and never reads these test files. Filed as RF §6 observation 8 with the candidate fix and why it was not taken.
Proven against the real defect, not against a fixture alone. Run over
440d6fd:.tfw/templates/status.md it reports the retired sentence at line 92; run over the
corrected file it is clean. test_the_retired_rule_check_actually_fires covers the empty and
the negative case.
This is what the phase's own PV 1 citation asks for — a rule that cannot reveal its own violation is only advice. The review's fact candidate is the rule; this is its enforcement site.
Not closed in this revision¶
RF §1's Modified — 25 versus 31 distinct paths. The heading now states both numbers and
what each counts, so the RF no longer contradicts its own table. The underlying question is
the coordinator's and stays open: the TS predicted the scripts would "cost a move and their
path constants", and they changed 507 and 246 lines. That is a budget-basis correction for
the next phase's table, exactly as the review placed it.
AC-13 half two. Unchanged and unchangeable here. The payload the owner will run against no longer contradicts itself, which is what made taking this round cheap now rather than after a tag.
11. The R4 corrective pass — actor removed until TFW-54¶
Why this section exists, and why it is late. Review revision 3 returned 🔄 REVISE with three
items, all record-keeping; the owner ruled them not worth a round and carried them as debt, then
closed the phase DONE with an outcome naming this gap in words: "the pass has no RF of its
own." Its first item is exact and I do not soften it:
"The Trust Protocol makes the RF the thing a review is conducted against; for the largest architectural change of the phase there is no declaration to test. In six months the reasoning that makes this defensible lives only in commit messages."
That is right. A review that has to reconstruct the claim from a diff is not reviewing a claim.
This section is the declaration that should have existed at b75bef1, written now rather than
not at all, and it changes no verdict: rev3's APPROVE stands, and the reviewer's analysis stands
beside it unrewritten.
11.1 What was done¶
Measured before the first edit — census_r4.md, at fd85b7c.
| Group | Paths | AC-15 items |
|---|---|---|
| Payload code | .tfw/scripts/gen_index.py |
1, 2, 3, 8 |
| Payload tests | .tfw/scripts/test_gen_index.py |
1, 3, 8 |
| Templates | journal/event.md, team/profile.md, REVIEW.md, project_config.yaml |
1, 2, 4, 9 |
| Created — 1 | .tfw/templates/bindings.yaml |
7 |
| Canon | conventions.md, glossary.md, migrations/2.0.0.md |
2, 4, 7, 8, 11 |
| Workflows — 8 | handoff, init, plan, release, research/base, resume, review, update |
1, 4, 5, 6, 7, 9 |
| Adapter source | .tfw/adapters/claude-code/README.md |
12 |
| Docs tooling | docs/scripts/test_integration.py |
6, 10 |
| Release surface | .tfw/VERSION, .tfw/CHANGELOG.md, .tfw/project_config.yaml |
AC-14 |
| Adapter copies | 8 workflows × .claude/commands/ + .agent/workflows/ = 16 |
5 |
Counts, both methods, because they differ and the difference is the point. By owner ruling S32 — byte-identical adapter copies excluded — 22 modified, 1 created. By distinct paths touched — 38. S32 is the declared method and 22 is under the limit of 30; path-counting crosses it. Raised in the census before any edit, under the return-to-coordinator rule, and recorded here rather than resolved in my favour.
A group appeared and was raised there too: the previous pass modified four workflows, this
one eight, because seven of them restate conventions.md's identity sentence verbatim. A
one-field removal reaching eight files is what that duplication costs.
11.2 Key decisions¶
| # | Decision | Why |
|---|---|---|
| D10 | actor is removed, not renamed and not given a naming convention |
The field held two unrelated jobs — say who wrote this and make the filename unique — and they contradict each other: a distinct writer needs a distinct value, a declared handle needs a profile. The report handed over a symptom with two branches, and both were the small answer. Blessing per-session handles would have shipped a naming convention over an unresolved contradiction. Naming a writer honestly needs a principal that delegates and answers to someone; TFW does not have one until TFW-54, so the field waits for the task that will |
| D11 | An actor already written is tolerated, never required, never rewritten |
Events are immutable and profiles are not — which is exactly how one consumer's gate went red permanently. Any rule about the field would either demand an edit to an immutable file or go red when someone tidies team/. So there is no rule: not a team/ comparison, not a provider list, not a shape check. This is what makes the correction cost every project nothing |
| D12 | The two name shapes are deliberately indistinguishable | __saubakirov.md and __9f2c.md match the same pattern. Nothing has to tell a historical handle from a new token, so nothing does, so no corpus needs classifying or migrating |
| D13 | Naming the token's one job removed machinery rather than adding any | Uniqueness came from the second, so a collision could only be resolved by waiting for time to pass — a retry loop with sleeps. Uniqueness now comes from the token, so a collision is re-drawn and the clock is read once. PROVIDER_FAMILIES was deleted for the same reason: its only reader was the gate that is gone |
| D14 | Two registries for retired text, not one with exemptions | A retired wording is wrong wherever it appears. A retired term is legitimate in prose narrating its retirement — glossary.md says the status legend moved when the board was removed — and never legitimate in a live instruction. One registry plus an exemption list would have buried that distinction |
| D15 | §10.4 was rewritten as a rule, not patched at its example | Swapping the dead filename would have left a rule nine of its own twenty subjects contradict. The reviewer's revision-2 objection is quoted in the canon rather than overridden |
11.3 Acceptance criteria — TS §5 AC-15¶
| # | Item | Status | Evidence |
|---|---|---|---|
| 1 | Two identity fields; the filename's third component is an opaque token | ✅ | EVENT_REQUIRED drops actor; EVENT_NAME group renamed to token; event_token() is 4 hex chars deriving from nothing — asserted by test_the_token_is_opaque_and_carries_no_identity over the function's code with its docstring stripped |
| 2 | No agent profile, and the payload says so | ✅ | team/profile.md, conventions.md §4, glossary.md, migrations/2.0.0.md each state that team/ holds people and why a writer is not named until TFW-54. PROVIDER_FAMILIES deleted — test_the_provider_family_list_is_gone |
| 3 | An already-written actor is tolerated, never required, never rewritten |
✅ | 0 events touched. At the pass's pin b75bef1: 29 events, 28 carrying the field, none edited. Re-checked 2026-08-29 after four later commits by other sessions — all 29 still byte-identical, and the five events written since carry no actor, so the new shape is in use and the old one is undisturbed. Both consumers validated read-only. ac15_actor_tolerated.txt |
| 4 | The canon, templates and guide state the two-field model and when the third returns | ✅ | conventions.md §4, templates/journal/event.md, glossary.md, migrations/2.0.0.md |
| 5 | Step 6 gains the Claude Code row and re-syncs rather than reports | ✅ | Row is first in the table, because it was the one forgotten. The tfw-*-only bound applied to every row |
| 6 | After Step 6 the adapter layer carries no retired vocabulary | ✅ | 0 files. Now a test — test_no_adapter_file_states_a_retired_rule — over both registries, proved to fire |
| 7 | .tfw/templates/bindings.yaml ships with its schema |
✅ | Created; defined in conventions.md §4, where glossary.md already pointed and where nothing was |
| 8 | Per-phase journals are part of the model and are read | ✅ | journal_dirs(); three tests including two phases holding the same event name |
| 9 | installed_from: <source>@<tag> beside tfw.version |
✅ | update.md Step 7, templates/project_config.yaml, this repository's own config |
| 10 | TD-193 — every path any payload file names, in both forms | ✅ and wider | Three forms, not two: the bare filename with no directory was named by neither the TS nor the review, and is how §10.4's dead example survived four releases. 212 references scanned, 0 unresolved. TD-194 closed as a side effect |
| 11 | TD-192 — fix the rule, not the example | ✅ | §10.4 rewritten. Read back against all 20 templates: 0 contradicting |
| 12 | The adapter README states the model that ships | ✅ | Copies are the model, with the two mechanisms that make it true rather than hopeful |
11.4 Verification¶
| Gate | Result |
|---|---|
pytest .tfw/scripts/ docs/scripts/ -q |
260 passed, 1 skipped |
mkdocs build |
exit 0 |
--check tasks · --check project · --check index |
exit 0 · exit 0 · exit 0 |
| Adapter copies vs sources | 0 drifted |
| Adapter layer, retired terms | 0 |
| Payload paths, three reference forms | 212 references, 0 unresolved |
| Naming rule vs its own subjects | 20 templates, 0 contradicting |
| Journal events changed | 0. 29 at the pin, 28 of them carrying actor; all 29 still byte-identical four commits later |
update.md |
1380 words against a ceiling of 1200 — see observation 9 |
The measurement that carries the phase's value, run read-only against both consumers and
recorded in ac15_actor_tolerated.txt:
BEFORE (code shipped at 2.0.0-dirty.2)
KZ-IT-telegram-list 4 tasks validate
innoforce-ai-first actor 'claude-20260828a' is not a declared team/ participant
actor 'claude-20260828b' is not a declared team/ participant
2 problem(s) across 15 tasks <- red, and unfixable from inside
AFTER (2.0.0-dirty.3, same corpora, same bytes)
KZ-IT-telegram-list 4 tasks validate
innoforce-ai-first 15 tasks validate
CHANGED IN EITHER PROJECT: nothing. 0 files edited, 0 events rewritten.
Its gate was red permanently: the events name profiles that were deleted, events are immutable and profiles are not, so no action available inside that project could ever have cleared it.
11.5 Evidence¶
See EV file, rows E64–E66 — the three artifacts this pass produced, indexed there rather than left loose, which was the second half of the review's first item.
11.6 Observations¶
| # | File | Type | Description |
|---|---|---|---|
| 9 | .tfw/workflows/update.md |
style | 1380 words against the §11 ceiling of 1200. History: 835 → 1165 (brought under by deleting duplication, D9) → 1380 after AC-15's four instruction items. Twice reduced by the D9 move; the duplication is now gone and what remains is instruction. Not a matter of shaving further: the honest options are to accept the overrun, or to split the file the way research/base.md was split when research grew — a change to the workflow's shape that no TS authorized. Coordinator ruling needed. This is rev3's F4 |
| 10 | .tfw/scripts/gen_index.py, conventions.md |
missing-test | via is validated by nothing while the canon still presents it as an enumeration — claude, codex, gemini. Under Structural Enforcement a rule that cannot reveal its own violation is advice. It was checked indirectly through PROVIDER_FAMILIES, which this pass deleted with its only reader. Either declare via free-form provider text or check it — one deliberate sentence. This is rev3's F5, and I did not close it because choosing between those two is a model decision |
| 11 | TECH_DEBT.md:113–114 |
duplication | TD-199 is registered twice, as two rows with different sources for the same finding. Not mine to edit — TECH_DEBT.md is the reviewer's and /tfw-docs' territory — recorded so the duplicate is a decision rather than a surprise |
| 12 | .tfw/workflows/handoff.md, init.md, plan.md, review.md |
style | Over the §11 ceiling and were before this pass. Named here only to keep observation 9 honest about what this pass did and did not cause: update.md is the one it pushed across |
11.7 What this pass did not close¶
AC-13 half two was reported unmet by this pass and ruled met by review revision 3 on the already-filed second external report — a real project, updated by an operator who is not the author, 0 files hand-carried, independently re-measured. I record the ruling rather than the claim: an RF asserting the external check passed on its author's own work is the DoF pattern the ONB was told to expect, and it is not this RF's to assert either way.
Review revision 3's item 2 — the TS mandates in AC-15 what its own §1, §7, §8 and §9 forbid —
routed to the coordinator and was closed at ab7093e, "bring the spec into line with its
own ruling": §1 now carries the removal as a declared exception, §7's model-change refusal is
bounded to "beyond the one removal AC-15 declares", and §9 states what Phases B and C inherit.
The reviewer was right that an executor reading §7 before AC-15 is told the pass is a declared
failure; I read them in the order the owner gave and proceeded on the ruling, which is why this
was the coordinator's to fix and not mine.
Review revision 3's item 3 — a version marker naming a release the CHANGELOG did not carry —
was closed at edab067, after the review was written. The [2.0.0-dirty.3] entry exists,
leads with what an updating project must do about the change: nothing, and carries F6: copies
are the declared adapter model and the "commands never duplicate workflow content" rule is
withdrawn, because a consumer had already acted on it.
11.8 Fact candidates from this pass¶
fact-candidates: processed 2026-08-30
| # | Category | Candidate | Source | Confidence |
|---|---|---|---|---|
| 10 | philosophy | Before a field, a file or a flag exists, say the one job it does and what leaves because it arrives. Four entities in this task were caught only after they collided: the task identifier carried four jobs, team_readme carried a directory relationship inside a filename, three flags carried no subject, and actor carried an identity and a filename's uniqueness. The last reached two external projects and left one with a permanently red gate. The question is never what do we call it |
Owner, TS §7 DoF R4 | High |
| 11 | process | A symptom reported from the field usually arrives with two branches, and both are the small answer. The report offered: bless per-session agent handles, or forbid them. Both would have shipped a naming convention over a contradiction. The third move — ask what job the thing actually does — is not in the report because a reporter sees the collision, not the cause | This pass, on the second field report | High |
| 12 | process | Duplication in instructions is measured in files touched per rule changed. Seven workflows restate one canon sentence, so removing one field reached eight files. The cost is invisible until something changes, which is when it is most expensive | census_r4.md, measured |
Medium |
RF — TFW-60 / Phase AA: Portable Delivery | revision 3, 2026-08-29 | §11 closes TD-199 | phase measurements pinned at 1079020, the R4 pass at b75bef1