Bỏ qua, tới nội dung
Kỹ thuật
5 phút đọc

Chạy song song nhiều AI agent: mỗi agent một git worktree

Hai AI agent dùng chung một checkout và commit của chúng rơi nhầm sang branch của nhau. Reflog cho thấy gì, và bộ quy tắc worktree mình dùng từ đó.

Dao Van Thuong
Mobile & Fullstack Engineer
Read in English
Ảnh bìa: chạy song song nhiều AI coding agent, mỗi agent một git worktree riêng
Trong bài này
  1. Chuyện gì đã xảy ra (đọc từ reflog)
  2. Dọn dẹp
  3. Mỗi agent một worktree
  4. Vòng đời, và cái bẫy trong từng bước
  5. Add: tách branch từ origin/main, ghi rõ ra
  6. Install: copy những gì git bỏ qua, cài mới hoàn toàn
  7. Work: mỗi worktree một Metro port
  8. Push, rồi remove
  9. Rút ra

Mình làm một mình bảy app iOS, và khá nhiều việc lặt vặt giờ được giao cho AI coding agent chạy song song: một agent audit codebase tìm bug, một agent khác viết lại keyword App Store cho mười ngôn ngữ, agent thứ ba thêm tính năng trên branch riêng. Tiết kiệm được hàng giờ. Nhưng có một kiểu hỏng chẳng liên quan gì đến code mà agent viết ra: chúng dùng chung repository.

Sáng 01/10/2026 mình chạy cùng lúc hai đợt trên các repo app: đợt audit trên branch audit/2026-10 và đợt keyword App Store (ASO) trên branch aso/2026-10. Ở ba repo, cả hai agent làm việc trong cùng một checkout. Đến 10:35 thì cả ba repo đều có commit nằm sai branch. origin/main không hề hấn gì, nhưng dọn dẹp mất nửa tiếng, và từ đó quy tắc của mình thành bắt buộc: agent nào cũng phải có git worktree riêng.

Chuyện gì đã xảy ra (đọc từ reflog)

Một checkout git chỉ có đúng một HEAD. Ai chạy git checkout là dời nó cho tất cả mọi người đang làm trong thư mục đó. Hai agent trong một thư mục nghĩa là hai bên cùng ghi vào một con trỏ, và không bên nào biết bên kia vừa dời nó.

Reflog của Talkzy cho thấy rất rõ:

txt
10:22:56  checkout: moving from main to audit/2026-10
10:28:09  commit: fix(prompter): clear the record countdown when the prompter closes
10:30:23  checkout: moving from audit/2026-10 to aso/2026-10
10:33:46  commit: docs(store): ASO pass for all 10 locales (name, subtitle, keywords, promo)

Agent audit chuyển sang branch của nó và commit một bản sửa thật. Hai phút sau agent ASO tạo branch riêng bằng checkout -b, mà lệnh này tách branch từ chỗ HEAD đang đứng. HEAD lúc đó đang ở branch audit. Thế là branch ASO mang sẵn bản sửa prompter của audit, và một pull request chỉ về store listing suýt nữa đã ship kèm một thay đổi code chẳng liên quan.

Dòng thời gian reflog của Talkzy: branch ASO được tạo từ branch audit
Tách branch từ HEAD là tách từ chỗ agent kia để lại

Stampzy thì hỏng theo chiều ngược lại. Agent ASO chuyển checkout chung sang aso/2026-10 lúc 10:26, còn agent audit vẫn tưởng mình đang ở branch của mình, commit bản sửa VoiceOver vào đó lúc 10:29. Ở Baton, checkout bị chuyển về main lúc 10:28, và commit ASO rơi thẳng vào main local lúc 10:30.

Theo logic của riêng nó thì không agent nào làm sai. Agent nào cũng chạy lệnh git đúng, chỉ là thư mục làm việc bị đổi ngay dưới chân.

Dọn dẹp

Vì chưa có gì được push lên main, mọi chỗ sửa đều nằm ở local:

  • Stampzy: cherry-pick bản sửa VoiceOver về audit/2026-10 cho đúng chỗ, rồi tạo aso/2026-10b sạch từ origin/main và chỉ cherry-pick commit ASO sang.
  • Talkzy: xử lý y như vậy với aso/2026-10b, để branch ASO không còn mang bản sửa prompter.
  • Baton: trỏ aso/2026-10 vào commit bị lạc, rồi reset main local về origin/main.

Cái hậu tố b trên mấy branch đó chính là vết sẹo. Khoảng 11:07 thì cả hai đợt đã merge xong. Nửa tiếng đó là cái giá của việc đi tắt, và bài học rẻ được là nhờ reflog ghi lại từng bước. Không có nó thì mình chỉ đoán mò agent nào viết cái gì.

Mỗi agent một worktree

git worktree cho một repository nhiều thư mục làm việc. Mỗi cái có HEAD riêng, index riêng, file riêng, và tất cả dùng chung kho object. Agent ở ../talkzy-aso muốn switch, commit, reset bao nhiêu cũng được, không đụng gì tới ../talkzy-audit hay checkout chính của mình.

Một checkout dùng chung so với mỗi agent một worktree
HEAD dùng chung là gốc của bug; mỗi agent một HEAD là hết

Thật ra mình đã thấy cách này chạy tốt từ trước. Ngày 28/09/2026, Stampzy có ba branch tính năng feat/albums, feat/paywall và feat/report, tách từ cùng một commit trong vòng hai giây. Commit của chúng không hề xuất hiện trong reflog của checkout chính, vì chúng được tạo ở chỗ khác. Cả ba được merge về trong khoảng 12:11 đến 12:45 cùng ngày, mỗi lần merge kèm một commit tích hợp nhỏ trên main. HEAD của checkout chính chưa từng rời main.

Nên quy tắc rất đơn giản: checkout chính là của lead và chỉ dùng để merge. Agent không bao giờ chạy git checkout trong đó.

Vòng đời, và cái bẫy trong từng bước

Worktree chỉ cô lập thật sự khi nó đầy đủ. Phần lớn rắc rối mình gặp đến từ những thứ git không copy sang.

Vòng đời worktree từ add đến remove
Add, install, work, push, remove

Add: tách branch từ origin/main, ghi rõ ra

bash
git fetch origin
git worktree add -b aso/2026-10 ../talkzy-aso origin/main

Ghi rõ base chính là mấu chốt. checkout -b không có base chính là thứ đã nhét bản sửa audit vào branch ASO của Talkzy.

Mặt trái là worktree chỉ thấy lịch sử đã commit. Ở Lockboxy, một agent làm trong worktree mới không thấy được những tính năng vẫn còn chưa commit ở checkout chính. Nó đã đúng khi từ chối thêm dòng paywall cho code mà nó không tìm thấy. Nếu task phụ thuộc vào phần chưa commit, hoặc commit phần đó trước, hoặc chạy tuần tự chứ đừng chạy song song.

Install: copy những gì git bỏ qua, cài mới hoàn toàn

Worktree mới chỉ có file được track. Mọi thứ trong .gitignore đều không có: file .env, node_modules, ios/Pods, thư mục build. Repo của mình đều ignore .env*, nên cấu hình local trong đó phải copy sang trước, không thì app chạy lên thiếu config và agent mất một vòng chỉ để debug chuyện đó.

Sau đó cài đặt cho đàng hoàng:

bash
cp ../<repo>/.env* . 2>/dev/null   # nếu repo có
yarn install --immutable   # app (Yarn 4); landing Next.js thì npm ci

Đừng symlink node_modules từ checkout chính cho nhanh. Mình đã thử ở Lockboxy, và lúc archive app thì fail ở phase Bundle React Native code and images: Metro không resolve package qua node_modules bị symlink. Copy bằng hardlink (cp -al) thì chạy và không tốn thêm dung lượng, nhưng cài mới vẫn là cách đơn giản nhất. Copy ios/Pods cũng có bẫy riêng: ở Minivid, Pods copy từ một tree mà lần build cuối là Release archive gây lỗi link ở bản Debug, tới khi sửa lại marker build configuration.

Work: mỗi worktree một Metro port

Hai worktree React Native đều muốn chạy Metro ở 8081. Mỗi worktree cần port riêng, và simulator phải trỏ đúng vào đó. Maestro flow của Baton nhận địa chỉ Metro qua tham số cũng chính vì lý do này.

Ở Lockboxy mình còn học được phải loại .claude/worktrees/ (chỗ công cụ agent đặt worktree) ra khỏi cả git lẫn Jest. Không thì Jest quét đệ quy, chạy luôn test của mọi bản copy lồng bên trong, kể cả bản đang sửa dở, rồi trộn kết quả của chúng vào kết quả của mình.

Push, rồi remove

Agent push branch, mình review và merge ở checkout chính, rồi xoá worktree:

bash
git worktree remove ../talkzy-aso
git worktree prune
git branch -d aso/2026-10

Dọn dẹp là bước hay bị bỏ qua nhất. Ngay lúc viết bài này mình còn thấy một worktree landing từ 29/09/2026 vẫn nằm cạnh repo, dù branch của nó đã merge rồi.

Rút ra

  • Hai agent chung một checkout là chung một HEAD. Một bên checkout là âm thầm dời luôn bên kia, và commit rơi sang branch của nhau.
  • Cho mỗi agent một worktree riêng, tạo từ origin/main ghi rõ, không bao giờ từ chỗ HEAD đang đứng.
  • Worktree chưa sẵn sàng khi chưa có các file bị ignore: copy .env, cài dependency mới, và đừng symlink node_modules với React Native.
  • Checkout chính chỉ để merge, và xoá worktree ngay khi branch đã push.
  • Khi có chuyện, git reflog cho biết chính xác agent nào đã dời cái gì, lúc nào.

Các app mà mấy agent này làm đều có ở apps.vanthuongdao.id.vn; Talkzy, repo có reflog rõ nhất, ở talkzy.io.vn.

  • #AI Agents
  • #Git
  • #Worktrees
  • #Workflow
Chia sẻXLinkedInFacebook
Dao Van Thuong

Kỹ sư mobile và fullstack ở TP. Hồ Chí Minh. Mình tự xây và phát hành các app iOS indie — Lockboxy, Linkeeper, Minivid, Ringsy, Talkzy, Baton và Stampzy.