Tôi từng để một agent sửa một bug nhỏ trong form validation. Yêu cầu ban đầu nghe vô hại: “nếu user chưa chọn country thì disable nút submit”. Mười phút sau diff có thêm refactor component, đổi text trong modal, sửa import order ở ba file không liên quan, và một helper mới tên nghe rất hợp lý nhưng chưa ai dùng.

Code chạy. Nhưng PR thì không review nổi. Reviewer không biết phần nào là bug fix, phần nào là “tiện tay cải thiện”. Nếu merge, blame sau này cũng mù. Nếu reject, mất công tách lại.

Từ đó tôi coi Git workflow là hàng rào đầu tiên khi dùng AI coding. Không phải vì Git làm model thông minh hơn, mà vì Git buộc công việc phải có biên giới. Branch nói việc này thuộc scope nào. Commit nói quyết định nào đã được đóng gói. PR nói người khác có thể review phần nào mà không cần đọc toàn bộ cuộc hội thoại với AI.

Branch là hợp đồng scope

Khi làm một mình, branch name thường bị xem nhẹ. Với AI coding thì ngược lại. Branch name là một câu nhắc scope rất rẻ.

Tôi thích branch theo dạng:

git checkout -b fix/SCRUM-214-disable-submit-without-country

Tên này nói ba thứ:

  • đây là fix, không phải feature mới;
  • ticket là SCRUM-214;
  • hành vi cần sửa là disable submit khi thiếu country.

Khi prompt agent, tôi nhắc lại đúng scope đó:

Work only on fix/SCRUM-214-disable-submit-without-country.
Fix submit disabled state when country is missing.
Do not refactor unrelated components.
Do not change copy outside this form.

Nghe cứng, nhưng cần. AI rất dễ “giúp thêm” vì nó nhìn thấy pattern xấu gần đó. Trong team bình thường, một developer cũng có thể tiện tay sửa thêm, nhưng người đó hiểu context tổ chức tốt hơn. Agent thì chỉ thấy file và prompt. Branch phải thay bạn giữ kỷ luật.

Nếu scope bắt đầu rộng quá, tôi tách branch. Ví dụ đang sửa validation thì phát hiện API trả schema thiếu field. Đó có thể là bug khác. Tôi không để chung một PR trừ khi hai phần bắt buộc đi cùng nhau để test pass.

Trước khi giao việc, đọc working tree

Lỗi rất phổ biến: prompt agent khi working tree đang bẩn mà không biết bẩn ở đâu. Kết quả là agent edit đè lên scratch change của mình hoặc gom luôn thay đổi của worker khác vào commit.

Command tối thiểu trước khi bắt đầu:

git status --short --branch
git diff --stat

Nếu có nhiều worker song song, tôi còn check file cụ thể:

git diff -- src/components/CheckoutForm.tsx

Quy tắc của tôi: agent chỉ được đụng file mình giao. Nếu trong file đó đã có diff trước khi agent bắt đầu, phải đọc diff đó trước. Có thể đó là thay đổi của mình ở bước trước, cũng có thể là thay đổi của worker khác. Không được “clean up” bằng reset.

Đây là khác biệt lớn giữa AI coding và coding một mình. Một mình bạn có thể nhớ vì sao file đang bẩn. Với nhiều agent, working tree là shared surface. Kỷ luật đọc status không phải nghi thức; nó là cách tránh mất việc của người khác.

Commit nhỏ nhưng không vụn

Tôi không thích một commit cho mỗi micro edit. Nhưng tôi cũng không muốn một commit gom 20 quyết định. Commit tốt trong AI coding thường đóng gói một behavior có thể review độc lập.

Ví dụ tốt:

fix(SCRUM-214): disable submit without country

Diff đi kèm nên gồm:

  • state hoặc selector xác định country đang thiếu;
  • disabled state của button;
  • test hoặc story case nếu repo có pattern sẵn.

Ví dụ kém:

fix(SCRUM-214): update checkout

“Update” không nói gì. Reviewer phải đọc diff để đoán ý định. Với AI coding, commit message càng mơ hồ thì càng khó phát hiện model đã lạc scope.

Tôi cũng tránh commit kiểu:

chore(SCRUM-214): cleanup

Nếu cleanup là cần để fix, nói rõ nó cleanup cái gì. Nếu không cần, đừng làm trong branch này.

Stage bằng mắt, không stage theo thói quen

git add . là lệnh tiện nhất và cũng là lệnh nguy hiểm nhất khi có AI tham gia. Tôi vẫn dùng nó khi repo sạch và scope nhỏ, nhưng mặc định tôi stage theo file:

git add src/components/CheckoutForm.tsx
git add src/components/CheckoutForm.test.tsx
git diff --cached --stat
git diff --cached

Điểm quan trọng là đọc staged diff, không chỉ đọc working diff. Rất nhiều lỗi lọt vào commit vì developer xem diff trước khi stage, sau đó git add . gom thêm file generated, file env, hoặc note tạm.

Với AI coding, tôi đặc biệt grep staged diff cho debug log:

git diff --cached | rg "console\\.log|debugger|TODO|FIXME"

Không phải TODO nào cũng sai, nhưng phải có chủ ý. console.log thì gần như luôn phải bỏ. Nếu codebase có rule “try/catch phải console.error trước fallback” thì đó là error log có chủ ý, khác với log debug.

PR là artifact review, không phải nhật ký chat

Một PR tốt không cần kể agent đã nghĩ gì. Nó cần nói người review phải kiểm gì.

Template tôi thường dùng:

## Summary

Fix submit disabled state in checkout when country is missing. This keeps the existing form flow and only changes the button eligibility check.

## Changes

- Add country presence to the submit eligibility condition.
- Cover missing-country state in the existing checkout form test.

## Testing

- npm test -- CheckoutForm
- npm run lint

Không có câu “generated by AI”. Không có apology. Không có transcript. Reviewer cần behavior, risk, test. Nếu phần nào model làm còn tôi chỉ review, tôi vẫn chịu trách nhiệm như code của mình.

Điều tôi tránh là PR description quá rộng:

This PR improves checkout validation and cleans up some related code.

“Related” là từ nguy hiểm. Nó cho phép bất kỳ diff nào chui vào.

Review PR như review đồng đội thật

AI có thể tạo code đúng syntax nhưng sai intent. Vì vậy tôi review theo thứ tự này:

  1. File list có đúng scope không.
  2. Public behavior có đúng request không.
  3. Existing pattern có bị phá không.
  4. Test có cover đường bug thật không.
  5. Error handling có tự chế thêm không.

Tôi không đọc code từ trên xuống ngay. Tôi đọc file list trước. Nếu branch fix form mà đụng src/api/client.ts, dừng lại hỏi vì sao. Có thể hợp lý, nhưng phải có lý do.

Sau đó tôi đọc test. Test là chỗ dễ thấy model có hiểu bug không. Một test chỉ snapshot UI sau khi render không chứng minh submit bị disable đúng lúc. Một test gọi trực tiếp helper nhưng bỏ qua form state cũng có thể lệch behavior thật.

Khi nào squash, khi nào giữ commit

Với AI coding, tôi thường squash merge PR nhỏ. Lý do đơn giản: quá trình làm có thể có commit sửa prompt, commit fix lint, commit adjust test. Những commit đó không có giá trị lịch sử lâu dài.

Nhưng trong branch lớn có nhiều quyết định độc lập, giữ commit có thể tốt hơn. Ví dụ:

  • commit 1: thêm API hook mới đã tồn tại trong backend contract;
  • commit 2: nối UI vào hook;
  • commit 3: thêm analytics event.

Nếu ba phần này review độc lập được, giữ commit giúp rollback và blame dễ hơn. Nếu commit chỉ phản ánh vòng thử-sai của agent, squash.

Operational detail: đừng để CI là reviewer đầu tiên

Một thói quen xấu là để agent push PR rồi đợi CI báo lỗi. CI cần chạy, nhưng không nên là người đầu tiên đọc diff. Trước khi mở PR, tôi chạy tối thiểu:

git diff --check
npm run lint
npm test -- --runInBand

Tùy repo, test command khác nhau. Nhưng git diff --check gần như luôn rẻ và đáng chạy. Nó bắt trailing whitespace, conflict marker, lỗi rất nhỏ nhưng làm PR xấu.

Nếu build mất 20 phút, tôi không luôn chạy full build cho từng edit nhỏ. Đây là tradeoff. Với thay đổi CSS copy nhỏ, lint và targeted test đủ. Với thay đổi routing, auth, payment, hoặc migration, phải chạy build rộng hơn. AI không miễn trừ chuyện đó.

Chốt lại bằng nguyên tắc

AI coding không làm Git workflow bớt quan trọng. Nó làm Git workflow quan trọng hơn, vì tốc độ tạo diff tăng lên rất nhiều. Khi tốc độ tăng, boundary phải rõ hơn.

Branch giữ scope. Commit giữ quyết định. PR giữ review surface. Nếu ba thứ này mơ hồ, agent sẽ tạo cảm giác nhanh trong 30 phút đầu và tạo nợ review trong phần còn lại của ngày.

Tôi không cần agent viết code “giống người”. Tôi cần nó tạo diff nhỏ, đúng scope, có thể review, có thể revert. Git là nơi tôi ép yêu cầu đó thành hiện thực.