sáu bài học · đo được, có ngày, có trích
Sáu bài học tôi đo được
Sáu sự cố dưới đây không phải rủi ro giả định. Mỗi sự cố có ngày, có số lần đo, và có một file
đang mang luật sinh ra từ nó. Tôi xếp chúng theo stage mà lỗi rơi ra chứ không theo mức nặng
nhẹ, vì thứ cần nhớ là chỗ nào trong vòng đời hay thủng. Phần đáng đọc là cách chữa và cái
giá của nó. Không cách chữa nào miễn phí.
Ba agent trên một checkout làm mất việc mà không ném lỗi.
Ba session chạy trên cùng một checkout, không lỗi nào được ném ra, một buổi làm việc
mất trắng và thêm một buổi nữa để tìm nguyên nhân.
Cách chữa thô nhất: mọi agent được spawn mà có quyền chạy git đổi trạng thái đều phải có
checkout riêng, kể cả agent chỉ đọc. Ngoại lệ “chỉ đọc” là giả định gây ra sự cố này. Đánh
đổi: mỗi Builder tốn thêm một worktree trên disk và vài giây setup, rẻ hơn nhiều so với một buổi
làm việc mất trắng.
Bảy stage đánh dấu bảy chỗ đã có thứ rơi ra.
Ban đầu quy trình chỉ có hai stage: agent làm, tôi review. Review kéo từ năm tới mười bốn
vòng, và phần lớn vòng sau dùng để dọn phần vòng trước để lại. Lỗi không nằm ở reviewer.
Lỗi nằm ở khâu trước đó: những quyết định chưa từng được chốt được đưa thẳng vào code, rồi
mới chốt ở khâu tốn kém nhất của quy trình.
Tôi đẩy vòng lặp lên đầu quy trình và cắt phần còn lại thành bảy stage có tên. Bảy stage có
tên để mỗi lỗi đo được gắn vào đúng một stage; không có tên thì lần sau lỗi rơi vào chỗ không
ai chỉ ra được. Đánh đổi: quy trình dài hơn, và ba trong bảy stage vẫn trống vì tôi chưa đo
được lỗi nào ở đó.
claim
The queue drained and nothing asked whether a slot was free
Measured on an adapted project running continuously by the owner's instruction: **after two merges, two of four Builder slots sat idle while twelve claimable tickets waited.** Nothing errored. Every step performed was performed correctly.
The router's loop was notification → verify → merge → report → wait, and no step in it asks how many Builders are working. …
Queue còn mười hai ticket claim được trong khi hai trên bốn slot Builder ngồi không, và không
có gì báo lỗi. Vòng lặp của router là nhận thông báo, xác minh, merge, báo cáo, chờ; không
bước nào hỏi có bao nhiêu Builder đang chạy.
Tôi sửa bằng cách cho frontier query một mục tiêu chứ không chỉ một trigger: sau mỗi lần
merge, router phải hỏi nên claim thêm bao nhiêu ticket, không chỉ hỏi ticket nào claim được.
Đánh đổi. Router bận hơn và thỉnh thoảng claim quá tay. Tôi chấp nhận đánh đổi đó, vì một slot
ngồi không thì không phát ra tín hiệu nào.
brief
chưa đo được lỗi nào ở đây
build
TERMINAL:done means the turn ended, not that the work finished
Found by workspace-app-inception Thomas, measured three times on one pane in one session. A builder launches a long background process (a test suite, a build), ends its TURN while waiting for the completion notification, and the pane reads done. The watcher faithfully reports TERMINAL:done. …
Một Builder khởi động tiến trình nền dài rồi kết thúc lượt trong lúc chờ. Pane hiện done,
watcher báo TERMINAL:done, và bảng nhánh trong dispatch-ticket nói builder đã xong. Tôi
suýt báo ticket đó bỏ dở, trong khi Builder đang ở phút thứ hai mươi của một việc tử tế.
Thứ cứu được tình huống này không phải protocol, mà là artifact tự mâu thuẫn: file bị sửa chỉ
có thêm đúng một dòng comment. Từ đó tôi bỏ hoàn toàn niềm tin vào trạng thái pane, và mỗi
chữ done phải đọc diff trước khi kết luận.
Đánh đổi. Mỗi lần đóng ticket tốn thêm một vòng đọc.
Builder stops after writing code but before committing — pane reads done, cleanup deletes the work
Measured in the field: 5 instances, 3 different Builder sessions, runtime claude, dispatched via herdr pane. Each time the Builder wrote substantial work (93-433 lines), pane status settled to done or idle, watcher returned TERMINAL:done, but git status on the worktree showed uncommitted changes.
The danger: Thomas's cleanup in dispatch-ticket runs git worktree remove, which silently deletes all uncommitted files. …
Vẫn là chữ đó, và lần này hậu quả nặng hơn. Builder viết xong code rồi dừng trước khi commit,
pane vẫn về done, và bước cleanup chạy git worktree remove đè lên. Năm lần trên ba
session, mỗi lần 93 tới 433 dòng, không lần nào lấy lại được.
Tôi đặt chốt chặn ngay tại bước nguy hiểm thay vì trông vào việc Builder commit cẩn thận hơn.
Cleanup phải đọc git status của worktree trước khi xoá; thấy dirty là dừng và đưa lại cho
người.
Đánh đổi. Worktree mồ côi tồn đọng và thỉnh thoảng phải dọn tay. So với mất một ngày làm việc
thì giá đó rẻ.
code-review
chưa đo được lỗi nào ở đây
simplify
chưa đo được lỗi nào ở đây
arm
Export step committed live secrets + buyer PII
Same-vendor correctness review PASSED it; cross-vendor caught it (P1) — the two lenses catch different classes. A value that touched a tracked file is burned — rotate it. Bound: .claude/rules/no-secrets-in-exports.md (gone) (always-on). CI secret-scan still TODO.
Một bước export commit thẳng secret đang dùng thật và PII của người mua vào file được track. Vòng
review same-vendor đọc qua và cho pass. Vòng cross-vendor bắt được và xếp mức P1.
Đó là lý do cuối phase vẫn còn một vòng arm chạy bằng vendor khác, dù tốn thêm tiền và thời
gian. Hai lăng kính bắt hai loại lỗi khác nhau, và loại mà same-vendor bỏ sót là loại đắt nhất
khi lọt.
Đánh đổi. Một giá trị đã chạm vào file được track thì coi như cháy và phải rotate. Phía sau
không có phương án nào rẻ hơn.
merge
A tracker measured only against itself cannot detect its own drift
Reported upstream from an adapted project's own Thomas, via a handoff, rather than found in this package directly — the first entry with that provenance, recorded because the class it names is general and the reachability checks have no way to see a tracker's *content* going stale, only a document's.
Four tickets in that project sat claimed and in-progress with a live assignee **after their code had merged to the base branch**, the oldest by a full day. …
Bốn ticket nằm in-progress với assignee vẫn còn sau khi code đã merge, ticket cũ nhất trễ trọn
một ngày. Không có gì báo lỗi: merge chạy, phần ghi frontier ngược lại thì không, và không
artifact nào ghi lại chỗ thiếu đó.
Không check nào chỉ nhìn tracker mà bắt được lỗi này, vì một trạng thái sai vẫn nhất quán với
chính nó. Tracker phải được đối chiếu với Git sau mỗi lần merge, thay vì tự xác nhận chính
nó.
Đánh đổi. Thêm một bước reconcile không ai vui khi chạy, và phần lớn thời gian nó không tìm ra
gì.
A blocking edge expresses order, not exclusion
The frontier asks which tickets have no open blocker and no assignee. It never asks what each ticket will WRITE, and nothing else did either. Two tickets went out together, correct by every rule the package stated. One existed to correct WIRE-CONTRACT.md; …
Hai ticket không có blocking edge nào giữa chúng, đúng mọi luật tôi đã viết, và cùng sửa ba
dòng của một file. Ticket thứ nhất merge trước. Ticket thứ hai dựng trên commit trước cú merge
đó, đẻ ra hai khối conflict, và một cú merge sai chiều sẽ revert phần đã review mà không phát
ra tín hiệu nào.
Một worktree cho mỗi Builder chỉ giải quyết va chạm ở checkout. Nó dời va chạm xuống merge,
chỗ tìm ra muộn và phải xử tay. Vì vậy ticket phải khai write-set, và những ticket có
write-set giao nhau bị xếp tuần tự kể cả khi không có gì buộc chúng theo thứ tự.
Đánh đổi. Số ticket chạy song song giảm xuống, đúng thứ mà cả hệ thống này tồn tại để tăng
lên.