Contents
Chào mừng bạn đến với Fx Studio. Bài này là tập Build của loạt AI-Native SDLC: cách playbook của Anthropic bắt agent viết plan trước khi code, những chỗ mình thấy còn hở và một demo nhỏ làm bằng chứng.
Agent code xong trong mười phút. Rồi bạn mất nguyên buổi chiều chỉ để hiểu nó đã sửa những gì và vì sao lại sửa như thế. Chỗ chậm không nằm ở tốc độ code. Nó nằm ở chỗ bạn chỉ được nhìn thấy cách làm khi mọi thứ đã làm xong. Bước Build của The AI-native SDLC playbook (Anthropic, 21/8/2026) đặt một nguyên tắc ngược lại: plan trước, code sau.
Tóm ý: ở bước Build, playbook bắt Claude viết một bản
plan.mdtrong plan mode và một người duyệt bản plan đó trước khi có dòng code nào; đi kèm là auto mode,CLAUDE.md, skill, hook và chạy nhiều session song song. Demo nhỏ của mình (mỗi kịch bản một hoặc hai lượt chạy, nhân vật giả lập) cho thấy với việc nhỏ và spec tốt, plan không làm code đúng hơn bao nhiêu: bản làm luôn và bản có plan cùng đạt 25/26 tiêu chí. Thứ plan mang lại là câu hỏi được đặt ra trước khi code và một bản ghi mà người khác đọc được. Và không lớp kiểm nào bắt được hết.
Bài này là tập thứ tư của loạt AI-Native SDLC, sau tập tổng quan, tập Plan và tập Design. Quy ước vẫn như cũ: chỗ nào playbook nói, mình ghi nguồn. Chỗ nào là nhận xét của mình, mình nói rõ. Toàn bộ code của demo nằm ở repo fx-sdlc-lab.
Bài đi theo ba lớp:
- Playbook nói gì về bước Build: plan mode, auto mode,
CLAUDE.md, skill, hook, chạy song song và cách đo. - Mình thử thế nào: demo trên app todo của các tập trước, có trích nguyên văn
CLAUDE.mdbản Build, mộtplan.mdthật, ba hook, subagentverifiervà file thử kích hoạt skill. - Mình thấy còn hở chỗ nào và vài cách vá ban đầu: bảng phân hạng thay đổi, năm câu chất vấn plan, bộ thử kích hoạt skill.

Plan nằm trong đầu người viết
Ở cách làm quen thuộc, engineer đọc bản thiết kế rồi bắt tay vào code. Sửa file nào, làm theo thứ tự nào, kiểm bằng test gì: tất cả nằm trong đầu người viết. Cùng lắm là một dòng ghi chú trong ticket.
Nên thứ đầu tiên người review nhìn thấy là một diff đã xong. Nếu hướng đi sai từ đầu, lúc đó phát hiện ra thì chỉ còn cách làm lại.

Chuyện thứ hai:
- Một engineer làm một việc một lúc và phần lớn thời gian là chờ: chờ build, chờ test, chờ review.
- Chuyển sang việc khác trong lúc chờ thì mệt vì phải đổi ngữ cảnh, nên ít ai làm.
Cách này không sai. Khi người viết code cũng là người duy nhất hiểu cách làm, giữ plan trong đầu là nhanh nhất. Nó chỉ hết hợp khi người viết code là một agent, chạy nhanh hơn mình đọc.
Playbook gom bước Build vào một nguyên tắc: không dòng code nào được viết khi chưa có plan được duyệt. Đi kèm là sáu thứ:
- Plan mode.
- Auto mode.
- File
CLAUDE.md. - Skill.
- Hook.
- Chạy nhiều session song song.
Plan mode: plan trước, code sau
Thứ đầu tiên là plan mode: ở chế độ này, Claude đọc được cả codebase nhưng không sửa được file nào cho tới khi engineer chấp nhận plan.
Playbook đi qua bảy bước:
Bước 1: engineer mở session ở plan mode.
Bước 2: đưa cho Claude cả intent.md lẫn spec.md đã duyệt, rồi yêu cầu một plan.
- Sửa những file nào.
- Làm theo thứ tự nào.
- Test nào chứng minh thay đổi là đúng.
Bước 3: chất vấn plan.
- Thay đổi này có thể làm hỏng gì?
- Bước nào rủi ro nhất?
- Có phương án nào Claude đã cân nhắc rồi bỏ?
Bước 4: sửa cho tới khi một engineer chưa từng đọc cuộc trò chuyện vẫn làm được, chỉ bằng bản plan.
Bước 5: commit plan thành file plan.md.
Bước 6: chấp nhận plan và để Claude làm. Plan tốt thì thường một lượt là xong.
Bước 7: khi code đi khác plan, sửa plan.md trong cùng commit. Playbook gợi ý có thể dùng hook để ép việc này.
Bản plan.md mẫu của playbook chỉ có bốn mục: file thay đổi, thứ tự làm, rủi ro và bằng chứng. Về governance, plan và mọi lần sửa plan được lưu lại, kèm tên người đã chấp nhận. Việc nào tổ chức xếp vào loại rủi ro cao thì chuyển cho tech lead (playbook ghi thêm architect; ở team nhỏ thường là một người).
Plan sai thì sửa một tài liệu. Code sai thì làm lại. Theo mình, đó là toàn bộ lý do của plan mode: việc review thiết kế dời lên trước khi có code, lúc đổi hướng còn rẻ.

Auto mode: khi nào được bỏ tay ra
Thứ hai là auto mode, playbook còn gọi là auto-accept: engineer duyệt plan một lần, rồi Claude tự sửa file mà không hỏi từng lần.
Nhưng playbook đặt điều kiện: chỉ khi các rào chắn đã đủ chắc.
- File
CLAUDE.mdtốt. - Skill chứa policy.
- Hook chặn hành động nguy hiểm.
- Một bộ test chạy được.
Lúc đó auto mode mới thành mặc định, cho việc thường ngày: spec chặt, phạm vi ảnh hưởng nhỏ, code đã có test bao quanh.
Sự chú ý của con người chuyển chỗ:
- Không còn ngồi xem agent sửa từng dòng.
- Mà là review thành quả sau một phiên làm việc dài.
Đây cũng là điều kiện để chạy song song, phần mình nói ở dưới. Tên gọi và cách bật auto mode trong Claude Code là theo lời playbook (bài ngày 21/8/2026), có thể đã đổi khi bạn đọc bài này. Demo bên dưới không bật auto mode: phiên code chạy headless với chế độ quyền acceptEdits.
File CLAUDE.md: thứ người mới cần biết
Thứ ba là file CLAUDE.md: nó chứa những gì một người mới vào team cần biết trong ngày đầu, gồm lệnh build, lệnh test, quy ước, kiến trúc và những chỗ hay làm sai. Agent đọc file này ở đầu mỗi session.
Cách làm của playbook:
- Chạy lệnh
/initđể Claude sinh bản đầu. - Cắt bớt, chỉ giữ thứ người mới thật sự cần.
- Commit vào gốc repo để cả team dùng chung một bản. Playbook giao file cho cả team cùng giữ (maintained by the whole team) và mỗi lần sửa được duyệt như sửa code.
Và một quy tắc làm việc: Claude sai một lỗi hai lần thì ghi cách sửa vào file.
Kèm theo là một giới hạn: giữ dưới một trang. Claude đọc toàn bộ file mỗi lần, nên dòng nào đã cũ chỉ chiếm chỗ mà không giúp gì.
Skill khuyên, hook ép
Thứ tư là skill: playbook dùng skill để biến tri thức của tổ chức thành chỉ dẫn có phiên bản, ví dụ chuẩn bảo mật, quy ước API, quy tắc brand. Policy đổi thì sửa một chỗ, mọi session đều theo.
Ranh giới giữa các file như sau:
| File | Giữ gì |
|---|---|
CLAUDE.md |
Ngữ cảnh của repo |
| Prompt | Yêu cầu của từng lần |
| Skill | Tri thức phải áp giống nhau ở nhiều nơi |

Nhưng playbook nói thẳng: skill là “a control, though an advisory one”, một control chỉ mang tính khuyến nghị. Nó làm agent dễ áp đúng policy hơn, nhưng không gì ép một session phải làm theo. Policy nào phải luôn đúng thì cần “something deterministic” đứng sau: một hook, hoặc một lượt review kiểm lại ở pull request.
Skill khuyên. Hook ép.
Thứ năm là hook, lớp bắt buộc đứng sau skill. Ở bước Build, agent chủ yếu sửa file và chạy lệnh, nên đây là chỗ hook được gọi nhiều nhất. Playbook nêu ba việc:
- Chặn sửa những đường dẫn được bảo vệ.
- Chạy formatter và linter sau mỗi lần sửa file.
- Giữ cho mật khẩu hay khoá bí mật không lọt vào diff.
Kèm hai lưu ý:
- Hook lúc build phải nhanh và chỉ kiểm file vừa đổi (“fast and scoped to the file that changed”). Việc nặng như chạy cả bộ test thì để tới lúc commit hay pull request.
- Đừng đặt hook hỏi người duyệt ở bước này. Playbook giải thích: nó “puts a person back on the critical path”, đưa con người quay lại đường găng của mọi session đang chạy. Cổng duyệt để dành cho bước Deploy.
Chạy song song: giới hạn là người review
Thứ sáu là chạy song song và có hai khái niệm hay bị lẫn:
| Session song song | Subagent | |
|---|---|---|
| Là gì | Một Claude Code đầy đủ khác, làm một việc khác, trong một git worktree riêng (một bản sao thư mục làm việc của repo) | Trợ lý phạm vi hẹp chạy bên trong một session, có context riêng, chỉ được dùng một số công cụ |
| Biết về nhau | Không. Thứ chung duy nhất là người điều phối | Làm xong thì báo về session chính |

Cách làm của playbook:
- Dùng plan để tách việc theo file. Việc nào đụng chung file thì làm tuần tự trong một session.
- Mỗi việc song song một worktree.
- Bắt đầu với hai, ba session (“two or three sessions”).
Và câu mình thấy đáng nhớ nhất của cả bước Build: giới hạn thực tế là số luồng một người review được tử tế (“how many streams one person can review properly”). Chỉ thêm session khi việc review vẫn theo kịp.
Việc lặp đi lặp lại thì đóng gói thành subagent, commit vào repo để cả team dùng chung. Playbook đưa ví dụ một subagent tên verifier:
- Chỉ được chạy lệnh và đọc file (Bash và Read).
- Chạy app, thử hành vi vừa đổi và hai luồng gần nhất, rồi báo lại chỗ nào không khớp
plan.md. - Được dặn rõ: “Do not fix anything; report only.” Không sửa gì cả, chỉ báo cáo.
Về governance, hook và quyền hạn áp cho mọi session như nhau. Hoạt động của từng session được ghi lại và gắn với engineer đã chạy nó.
Còn một phần playbook đặt trong bước này: sống chung với hệ thống cũ như Jira. Lời khuyên gọn: mỗi loại tài liệu chỉ chọn một nơi làm nguồn sự thật.
Dấu vết và cách đo
Về dấu vết, playbook ghi lại plan cùng người chấp nhận, hoạt động của từng session cùng engineer đã chạy nó và lượt gọi skill nằm trong log của session. Mỗi thứ có chỉ số riêng:
| Thứ | Chỉ số dẫn (leading) | Chỉ số trễ (lagging) |
|---|---|---|
| Plan mode | Tỉ lệ thay đổi merge được ngay từ lượt code đầu; thời gian từ lúc duyệt plan tới lúc pull request được merge | Số vòng làm lại mỗi thay đổi; diff cuối còn khớp plan.md đến đâu |
CLAUDE.md |
Số lần Claude lặp lại một lỗi mà file lẽ ra đã chặn | Thời gian để một người mới có pull request đầu tiên được merge |
| Skill | Thời gian từ lúc người sở hữu policy duyệt thay đổi tới lúc skill mới được merge | Số finding trong review pull request trích dẫn policy đó |
| Chạy song song | Số session mỗi người chạy được mà chất lượng review vẫn giữ | Số thay đổi merge mỗi engineer mỗi tuần, đọc cùng tỉ lệ làm lại |
Như các bước trước, playbook chưa công bố số đo nào cho những chỉ số này. Auto mode và hook không có dòng chỉ số riêng.
Theo mình, cặp đáng đo đầu tiên là cặp của plan mode, vì nó nói thẳng plan có đáng công viết hay không.
Mình thử trên app todo
Đọc thì gọn, nên mình dựng thử. Phần này không có trong playbook, là thứ mình tự chạy ngày 27/10/2026 bằng Claude Code 2.1.289 chạy headless (claude -p):
- 53 lượt chạy qua script của demo, 2 lượt bộ thử kích hoạt skill, 1 lượt máy chạy thử trên Chromium.
- Phiên code, plan, review và verifier dùng
claude-opus-5-5, phiên đóng vai dùngsonnet. - Tổng khoảng 27,2 USD.
Sản phẩm vẫn là app todo của các tập trước:
- Chạy trên trình duyệt, HTML/JS thuần, không có bước build, không có package npm.
- Dữ liệu chỉ nằm trong
localStoragecủa trình duyệt đó.
Việc cần build là spec “khách không mất việc” của tập Design: sao lưu ra file, nhập lại và báo khi không lưu được.
Nhân vật giả lập có bốn, mỗi người do một phiên Claude riêng đóng, chỉ biết hồ sơ của mình và đúng phần được đưa: Thảo (product owner), Khoa (tech lead, kiêm bảo mật), Linh (UX owner, duyệt chữ trên app) và Mai (privacy owner).
Bộ đồ của bước Build
Mọi thứ agent đọc viết bằng tiếng Anh, như các tập trước:
| Thành phần | Vai trò | Ứng với playbook |
|---|---|---|
CLAUDE.md mục Build |
Lệnh chạy app, test, lint; bảng phân hạng thay đổi; plan bốn mục; mục Things Claude gets wrong | CLAUDE.md cả team giữ |
plan/ |
Plan đã duyệt, cùng tên file với intent và spec. Người đổi Status: draft thành accepted |
plan.md có người chấp nhận |
Hook protect-paths.py · no-secrets.py · check-js.py |
Chặn sửa file đã duyệt; chặn chữ trông như khoá bí mật; node --check sau mỗi lần sửa .js. Mỗi hook ghi thời gian chạy |
Ba việc hook của bước Build |
Hook no-self-approve.py bản 3 |
Thêm plan/ vào danh sách agent không được ghi Status: accepted |
“A human always makes that call” |
Subagent verifier |
Chỉ có Bash và Read: chạy test, thử thay đổi và hai luồng gần nhất, so diff với plan, không sửa gì | Mẫu verifier của playbook |
Git pre-commit |
Commit đổi app/ mà không đổi plan/ thì chặn, trừ khi ghi lý do |
Sửa plan.md trong cùng commit |
triggers.md + skill-triggers.sh |
Năm câu người ta hay hỏi, mỗi câu một phiên, xem skill có được gọi không | Thử skill với nhiều cách hỏi |
Bước 0: đưa spec tới accepted
Ở tập Design, spec sao lưu còn draft với 12 giả định chưa ai xác nhận. Trước khi có gì để build, nó phải được duyệt.
Bốn nhân vật trả lời các giả định của spec. Và cứ mỗi vòng trả lời, lại mọc thêm giả định mới: 12, rồi 4, rồi 3, rồi 1, rồi mới về 0 (một lượt chạy mỗi vòng, nhân vật giả lập). Linh có lần trả lời “Có” rồi rút lại sau khi đọc lời Khoa. Spec được accepted ở commit fdf3791.

Lượt 1: làm luôn, không plan
Lượt đầu mình không dùng plan. Đưa spec đã accepted và bảo Claude làm luôn: Implement spec/2026-10-19-khach-khong-mat-viec.md. Kết quả (một lượt chạy):
- 225 giây, chưa tới bốn phút, code đã chạy: 4 file, 1,14 USD.
- Nó thử cài thư viện
jsdomđể kiểm thử, bị chặn, nên viết script kiểm ở thư mục tạm. Repo không có test nào. - Nó tự quyết ba chỗ spec không nói: câu chữ tự viết, chặn file sao lưu không có việc nào, tiêu đề chỉ có dấu cách coi như rỗng.
Phải công bằng: nó có ghi ba chỗ đó ra để người xác nhận. Đây là đoạn cuối câu trả lời của lượt này, nguyên văn:
**Calls I made that the spec doesn't cover (please confirm with Linh / Khoa)** 1. **Wording I wrote myself:** the refusal messages (too large, can't read, not a backup file, damaged file), the full not-saved sentence around Linh's hint, the "Đang đọc file…" loading text, and the dialog buttons "Huỷ" / "Thay". Focus starts on "Huỷ". 2. **A backup file with zero todos is refused.** The app can never make one (A6), and accepting it would wipe the list. 3. **A title that is only spaces counts as empty**, the same way the existing add form treats it.
Lượt 2: plan mode, cùng spec
Lượt thứ hai mở plan mode (--permission-mode plan), cùng spec đó. Claude chưa viết dòng code nào, mà tự chọn hạng Medium và hỏi sáu câu (một lượt chạy, 135 giây):
- Hạng Medium hay Large?
- Thêm một file
app/backup.jsngoài danh sách của spec được không? - “1 MB” là bao nhiêu byte: 1.048.576 hay 1.000.000?
- File sao lưu hợp lệ nhưng không có việc nào (
todos: []) thì sao? - Bộ nhớ đang giữ chữ
"null"thì coi là đọc lỗi hay danh sách trống? - Các tiêu chí chỉ kiểm được trên trình duyệt thì người kiểm tay có đủ không?
Câu 2, 3 và 4 là đúng những chỗ lượt đầu đã lặng lẽ tự quyết. Ở lượt 1, chúng thành code. Ở lượt 2, chúng thành câu hỏi chờ người trả lời.
Chất vấn plan
Rồi mình chất vấn bản plan bằng năm câu (prompt nguyên văn ở phần “Mình sẽ vá thế nào”). Câu đắt nhất là câu 4: test nào sẽ fail nếu bước rủi ro nhất làm sai? Claude trả lời thẳng (một lượt chạy):
Before the revision, none did.
Trước khi sửa thì không test nào cả. Nó tự thêm test cho đúng bước đó, tách phần quyết định nhập và lưu ra hai hàm không dính giao diện để Node test được rồi thêm một mục “cách đơn giản hơn đã cân nhắc và bỏ”. Bản plan đi từ khoảng 1.900 lên 2.500 từ.
Tech lead chỉ đọc plan
Mình đưa bản plan cho Khoa (tech lead, giả lập), người không đọc cuộc trò chuyện, đúng bước 4 của playbook. Kết quả (một lượt chạy):
- Câu đầu: “Được, với vài chỗ cần sửa.” Một engineer khác làm theo được.
- Nhưng chỉ ra tám chỗ còn thiếu: thứ tự commit tự mâu thuẫn, câu chữ nằm ngoài plan, chưa xử lý khi đọc file lỗi, chưa thu hồi URL tải về, test tĩnh chưa đủ chặt, chưa grep nơi gọi
store, chưa có rollback, kiểm tay không ghi ai chạy và trên trình duyệt nào. - Chốt hạng Lớn và duyệt có điều kiện.
Sau khi Thảo và Linh trả lời phần của mình, Claude sửa plan thêm một vòng và tự tìm ra hai chỗ nữa: thử trên điện thoại cần HTTPS và spec “hạn chót cho việc” sẽ đá với spec này ở dạng dữ liệu. Plan được accepted ở commit 948e1ce, dưới tên Khoa (giả lập).
Đây là plan.md đó, rút gọn: giữ dòng đầu, mục Quyết định, Điều kiện duyệt, Files that change, Order of work và rủi ro đầu tiên; bỏ phần Thiết kế, Câu chữ, Rollback, phần lớn Risks và Proof (bản đầy đủ khoảng 5.200 từ, ở thư mục plan/ của repo demo):
# Plan: Sao lưu ra file, nhập lại, và báo khi không lưu được
Status: accepted · From: spec/2026-10-19-khach-khong-mat-viec.md (accepted) · Size: **Large** [Khoa, Q1] · Date: 2026-10-27
## Quyết định
| Q | Câu hỏi | Trả lời | Ai |
|---|---|---|---|
| Q1 | Size | **Large**: đổi hợp đồng tầng lưu, thêm module, chạm cả 5 file `app/`, bộ test đầu tiên, spec `Risk: high` | Khoa [R2b] |
| Q2 | Thêm `app/backup.js` ngoài danh sách file của spec? | **Có.** Script thường, không ES module, không build; thứ tự `store` → `backup` → `app` | Khoa [R2b] |
| Q3 | 1 MB bằng bao nhiêu byte? | **1.048.576 byte** (1 MiB). Một hằng `MAX_IMPORT_BYTES` cho cả `file.size` khi nhập và `TextEncoder` khi sao lưu; test biên 1.048.576 (nhận) và 1.048.577 (từ chối). Chữ hiển thị vẫn ghi "1 MB" | Khoa [R2b] |
| Q4 | File hợp lệ nhưng `todos: []` | **Chặn.** Câu báo nguyên văn M8 | Thảo (product owner) [demo/sdlc-3/runs/R2c-thao-q4], câu chữ Linh (UX owner) [demo/sdlc-3/runs/R2c-linh-q4]; Khoa đồng ý về kỹ thuật [R2b] |
| Q5 | `todo.v1` = `"null"` | **Đọc lỗi**, theo A13. App không bao giờ ghi `"null"`, nên báo nhầm gần như không xảy ra | Khoa [R2b] |
| Q6 | AC chỉ kiểm được trong trình duyệt do người chạy tay? | **Chấp nhận, có điều kiện:** checklist nằm trong plan này, ghi người chạy, ngày, trình duyệt, đạt/không; tối thiểu Safari iOS máy thật, Chrome Android, một trình duyệt desktop; tải file và chọn file chạy cả ba | Khoa [R2b] |
| Q7 | Các câu chữ **mới** (không có nguyên văn trong spec), mục Câu chữ cột "Đề xuất" | **Linh duyệt**: các câu đề xuất đạt, trừ M11 và M12 (Linh thay, ghi ở mục Câu chữ) | Linh [R2e] |
| – | Câu cảnh báo kiểu "file này chứa việc của bạn" | Plan **không** thêm. Nếu cần thì chuyển Mai (privacy) | Khoa [R2b] |
## Điều kiện duyệt (Khoa)
1. Sửa 7 điểm Khoa nêu: Order of work theo commit A/B (mục Order of work); câu chữ và chú dẫn mã trong plan (mục Câu chữ, Chú dẫn);
lỗi `file.text()` (Thiết kế, M14); `URL.revokeObjectURL` (Thiết kế); cấm bộ nhớ trình duyệt trong cả `backup.js` (Proof, static);
grep nơi gọi `store` (Risks); rollback (mục Rollback). Kiểm tay có người/ngày/trình duyệt (Proof, bảng kiểm tay).
2. Q3, Q5 như trên; Q4 đã có Thảo và Linh trả lời.
3. **Thử bằng dữ liệu thật từ bản cũ trước mọi AC khác**, kể cả 2.000 việc (T0).
4. **Diff của commit B được một người khác review** (không phải người viết code).
5. Không nâng giới hạn 1 MB; khách vượt thì câu M2 báo trước (C2).
## Files that change
| File | Commit | Thay đổi |
|---|---|---|
| `app/backup.js` (mới, Q2) | A | Logic không DOM: nội dung file, tên file, kiểm file nhập, lời xác nhận, `MESSAGES`, hai hàm điều phối `persist` / `importBackup`. Không đụng bộ nhớ trình duyệt; nhận `store` làm tham số |
| `tests/backup.test.js` (mới) | A | Test mọi hàm của `backup.js` với `store` giả, `ask` giả, file giả `{ size, text() }` |
| `app/store.js` | B | `load()` phân biệt chưa có / đọc lỗi; `save()` trả kết quả; khoá ghi khi đọc lỗi; `replace()` cho đường nhập file. Thêm `module.exports` có điều kiện |
| `app/app.js` | B | Chỉ còn nối DOM: nút, `<dialog>`, vùng thông báo, dải báo F5, dòng trống; gọi `persist` / `importBackup` và vẽ kết quả |
| `app/index.html` | B | Nút "Sao lưu ra file", `<input type="file">` có nhãn "Nhập từ file", vùng `role="status" aria-live="polite"`, dải báo F5, dòng trống, dòng chữ F4 (M12), `<dialog>`, `<script src="backup.js">` giữa `store.js` và `app.js` |
| `app/style.css` | B | Kiểu cho dải báo, vùng thông báo, dialog, dòng trống |
| `tests/store.test.js` · `tests/static.test.js` (mới) | B | Bộ nhớ trình duyệt giả (`getItem`/`setItem` ghi lại số lần gọi, ném lỗi được khi cần); kiểm tĩnh `index.html`, `app.js`, `backup.js` |
| `plan/2026-10-19-khach-khong-mat-viec.md` | A, B | Người commit ghi mục Tiến độ và bảng kiểm tay (pre-commit đòi `plan/` đi cùng `app/`) |
## Order of work
Agent viết code và chạy test; **người** review và commit (agent không commit). Mỗi commit kèm thay đổi trong file plan này (pre-commit).
1. **Commit A: `app/backup.js` + `tests/backup.test.js`.** Chưa file nào nạp `backup.js` (chưa có trong `index.html`), nên app chạy y
như hôm nay. Xong khi: `node --test` xanh, `node --check app/backup.js`. Người commit ghi Tiến độ A.
2. **Commit B: `app/store.js` + `app/app.js` + `app/index.html` + `app/style.css` + `tests/store.test.js` + `tests/static.test.js`.**
Đổi kiểu trả về của `load`/`save` và nối app phải đi cùng nhau, nếu tách thì app vỡ ở giữa. Thứ tự trong commit:
1. grep lại nơi gọi `store` / bộ nhớ trình duyệt (Risks); khác kết quả 2026-10-27 thì dừng và báo;
2. `store.js` + `store.test.js`;
3. `index.html` + `style.css` + `static.test.js`;
4. `app.js`;
5. `node --test` xanh, `node --check` từng file `.js`; gọi subagent `verifier` với plan này;
6. **T0 trước** (dữ liệu bản cũ, điều kiện 3), rồi bảng kiểm tay T1–T13 trên ba trình duyệt;
7. **một người khác** review diff (điều kiện 4); người commit ghi Tiến độ B và bảng kiểm tay vào plan trong cùng commit.
## Risks
- **Bước rủi ro nhất: commit B.** Hai cách hỏng:
- *Báo nhầm đọc lỗi với dữ liệu tốt*: khách đang dùng bình thường bỗng thấy danh sách trống + dải báo, và từ đó không gì được lưu.
Luồng hôm nay đang chạy (mở app → thấy việc cũ → thêm, đánh dấu, xoá được lưu) có thể vỡ cho **mọi** khách. Biết bằng:
`store.test.js` (dữ liệu bản cũ, 3 và 2.000 việc) và T0.
- *Ghi đè dữ liệu hỏng* (risk-list dòng 1, C3): một đường ghi lách qua `store`, hoặc nhập file gọi `save` thay vì `replace` (khi đọc
lỗi `save` trả `'skipped'`, app báo "Đã nhập" mà không lưu gì). Biết bằng: `backup.test.js` (`importBackup` không bao giờ gọi
`save`), `store.test.js` (`setItem` 0 lần khi đọc lỗi), `static.test.js` (`app.js`, `backup.js` không có `localStorage`), T9–T10.
Hai chỗ đáng nhìn trong bản plan này:
- Mỗi quyết định có tên người và có đường dẫn tới lượt chạy nơi người đó trả lời. Đó là thứ playbook gọi là “plan và mọi lần sửa plan được lưu lại, kèm tên người đã chấp nhận”.
- Agent viết code, người commit. Plan ghi rõ ngay đầu mục Order of work.
Code theo plan, verifier chỉ báo
Plan được chấp nhận, Claude code theo plan. Kết quả (một lượt chạy): 440 giây, 4,93 USD, 35 test chạy qua.
Có một chi tiết mình thích:
- Lúc duyệt câu chữ, Linh đã sửa một câu cho dễ đọc, nhưng câu mới bỏ mất vế “xoá dữ liệu” nên trượt tiêu chí 23 của spec.
- Claude thấy và nó không tự sửa. Nó báo lại. Plan đã accepted, nên đổi câu là việc của người.
- Mai đề xuất giữ ý “xoá dữ liệu” theo policy riêng tư, Linh chốt chữ “dữ liệu trang web”, Mai xác nhận đạt.
Rồi subagent verifier chạy, chỉ được đọc và chạy lệnh. Kết quả (một lượt chạy):
- 0 lỗi code, 35/35 test.
- Năm chỗ code đi lệch plan, ví dụ thứ tự commit, một dòng bằng chứng không thể xảy ra, plan chưa cập nhật.
- Nói rõ phần nào nó không chạy được: phần nối với trang web. Chỉ có Bash và Read thì không ghi được file script, còn dựng script bằng heredoc trong Bash thì bị từ chối 4 lần. Nó báo điều đó và không tìm cách lách.
Một phiên sau (có quyền Write) viết tests/app.test.js chạy nguyên app.js trong node:vm, đúng phần verifier bó tay: 38 test. Khoa (giả lập) review diff của commit B, cho commit kèm ba điều kiện. Đây là verifier.md của demo, nguyên văn:
--- name: verifier description: Checks a finished change against its plan. Runs the tests, exercises the changed behaviour and the two nearest neighbouring flows, and reports what it ran, what it saw, and anything that does not match plan/<name>.md. Never fixes anything. Use after an implementation pass, before a person reviews the diff. tools: Bash, Read --- You verify; you do not fix. Based on the playbook's sample verifier subagent. Given a plan file `plan/<name>.md` (and its spec `spec/<name>.md`): 1. Read the plan: files that change, order of work, risks, proof. 2. Run the test suite: `node --test`. Record the exact command and the pass/fail counts. 3. Exercise the **changed behaviour** and the **two nearest neighbouring flows** (for the todo app: adding, toggling, deleting a todo; loading the saved list). This app has no browser automation: drive the logic through Node (the tests, or a short `node -e` script that loads the modules), and say plainly what you could not exercise (anything that needs a real browser: file pickers, downloads, dialogs, how the page looks). 4. Compare the diff (`git diff`, `git status`) with the plan: files changed that the plan did not list, planned files left untouched, proof items with no matching test. Report, in this order: - **Ran:** each command and its result. - **Saw:** behaviour observed, one line per flow. - **Does not match plan.md:** each mismatch, with file and line. - **Not checked:** what needs a browser or a person. **Do not fix anything; report only.** Do not edit files, do not commit, do not install packages.
Commit B đi trước bảng kiểm tay T0–T13 trên ba trình duyệt: lệch plan và plan ghi rõ điều đó. Demo không có người cầm ba thiết bị, nên tính năng này chưa nghiệm thu. Máy có chạy một phần T0 và T9 trên Chromium headless, nhưng không thay ô nào trong bảng kiểm tay.
Chấm cả hai bản theo spec
Rồi mình nhờ một phiên Claude chỉ đọc code, chấm cả hai bản theo 26 tiêu chí nghiệm thu của spec rồi mình đối chiếu lại từng chỗ với file. Kết quả làm mình bất ngờ (một lượt chạy):
| Làm luôn | Có plan | |
|---|---|---|
| Tiêu chí đạt | 25/26 | 25/26 |
| Tiêu chí “chưa biết” | Bàn phím và trình đọc màn hình (cần người thử) | Như bên trái |
| Test để lại trong repo | 0 | 38 |
| Câu từ chối file | Mọi câu nói rõ vì sao và khách làm gì tiếp | Năm câu thiếu vế “làm gì tiếp” hoặc “danh sách giữ nguyên” |
| Tải file trên Safari iOS | Thu hồi URL tải ngay, có rủi ro tải hỏng | Chờ 10 giây rồi mới thu hồi |

Bản làm luôn có spec đã accepted trong tay, chỉ không có plan. Với spec đó, nó còn tốt hơn ở một chỗ: câu báo lỗi khi từ chối file. Spec đòi mỗi câu từ chối nói vì sao và làm gì tiếp; năm câu của bản có plan thì không.
Lỗi đó lọt qua plan, qua người duyệt chữ, qua verifier, qua cả tech lead. Chỉ lượt đọc lại so với spec mới thấy. Sau đó Linh viết lại năm câu, Thảo đồng ý. Một lượt sau nữa làm nốt ba việc còn treo (42 test).

Hook chặn ba lần
Mình bảo Claude làm ba việc trong một phiên, ở worktree riêng (một lượt chạy):
| Việc | Hook | Kết quả | Thời gian kiểm bên trong hook |
|---|---|---|---|
| Sửa tiêu chí 23 trong spec đã accepted | protect-paths |
Chặn | 0,5 ms |
| Dán một khoá bí mật giả vào code | no-secrets |
Chặn | 0,1 ms |
| Dán một hàm JS thiếu dấu ngoặc | check-js |
Bắt được ngay sau khi ghi, báo lại cho Claude | 133 ms |
Với cả ba, Claude nói đúng bước tiếp theo: người sửa spec; khoá coi như đã lộ; thêm dấu ngoặc hoặc trả lại file.
Hai lưu ý từ số đo:
- Số mili giây đo bên trong script, chưa tính thời gian khởi động Python.
check-jschạy sau khi ghi: file hỏng đã nằm trên đĩa, hook chỉ báo cho agent sửa. Muốn chặn trước thì phải là hook chạy trước khi ghi.
Qua các lượt code, protect-paths chạy 23 lần (trung bình 0,2 ms), no-secrets 23 lần (trung bình 0,5 ms), check-js 12 lần (trung bình 77 ms, chậm nhất 138 ms). Đây là ba hook đó, nguyên văn. Hai hook đầu chạy trước khi ghi (PreToolUse), hook thứ ba chạy sau (PostToolUse):
#!/usr/bin/env python3
"""PreToolUse hook (Write, Edit, MultiEdit): some paths are not the agent's to change during Build.
Denied:
- docs/ the channel owner's own notes
- tickets/ incoming tickets are evidence, not working files
- intent/, spec/, plan/ files that already say `Status: accepted`: changing an approved decision is a person's call.
(Drafts stay editable. A plan that has to change during the build is updated by a person, in the same commit as the code.)
Fast and scoped: it only looks at the one file the tool call touches.
"""
import json, os, re, sys
sys.path.insert(0, os.path.dirname(__file__))
from _timing import log
ACCEPTED = re.compile(r"Status:\s*accepted", re.I)
def deny(tool, path, reason):
log("protect-paths", tool, path, "deny")
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny",
"permissionDecisionReason": reason}}))
sys.exit(0)
try:
data = json.load(sys.stdin)
tool, path = data["tool_name"], data["tool_input"].get("file_path", "")
except Exception:
deny("?", "?", "protect-paths: could not read the tool call, denying to be safe")
root = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
rel = os.path.relpath(os.path.abspath(path), root)
if re.match(r"(docs|tickets)/", rel):
deny(tool, rel, f"protect-paths: {rel.split('/')[0]}/ is read-only for the agent.")
if re.match(r"(intent|spec|plan)/", rel) and os.path.exists(path):
with open(path, encoding="utf-8") as f:
head = f.read(2000)
if ACCEPTED.search(head):
deny(tool, rel, f"protect-paths: {rel} is accepted. Changing an approved decision is a person's call; "
"describe the change you need and stop.")
log("protect-paths", tool, rel, "allow")
sys.exit(0)
#!/usr/bin/env python3
"""PreToolUse hook (Write, Edit, MultiEdit): keep credentials out of the diff.
Denies writing text that looks like a secret: private keys, cloud/API keys, tokens, `password = "..."`.
Fast and scoped: it only scans the text of this one tool call.
"""
import json, os, re, sys
sys.path.insert(0, os.path.dirname(__file__))
from _timing import log
PATTERNS = [
(r"-----BEGIN [A-Z ]*PRIVATE KEY-----", "a private key"),
(r"\bAKIA[0-9A-Z]{16}\b", "an AWS access key"),
(r"\b(sk|rk|pk)-[A-Za-z0-9_-]{20,}", "an API key"),
(r"\bgh[pousr]_[A-Za-z0-9]{30,}", "a GitHub token"),
(r"\bAIza[0-9A-Za-z_-]{35}\b", "a Google API key"),
(r"(?i)\b(api[_-]?key|secret|token|password|passwd)\b\s*[:=]\s*['\"][^'\"\s]{8,}['\"]", "a hard-coded credential"),
]
try:
data = json.load(sys.stdin)
tool, inp = data["tool_name"], data["tool_input"]
except Exception:
sys.exit(0)
path = inp.get("file_path", "")
text = inp.get("content", "") + inp.get("new_string", "") + "".join(e.get("new_string", "") for e in inp.get("edits", []))
for pat, what in PATTERNS:
if re.search(pat, text):
log("no-secrets", tool, path, "deny")
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny",
"permissionDecisionReason": f"no-secrets: this looks like {what}. "
"Credentials never go into the repo; read them from the environment."}}))
sys.exit(0)
log("no-secrets", tool, path, "allow")
sys.exit(0)
#!/usr/bin/env python3
"""PostToolUse hook (Write, Edit, MultiEdit): lint the file that just changed.
The app has no build step and no npm packages, so the "linter" is `node --check` on the one .js file just edited
(a syntax check). Fast and scoped: one file, no test suite (tests run at commit, by the verifier, or by people).
On failure the agent is told what broke, so it fixes it before moving on.
"""
import json, os, subprocess, sys
sys.path.insert(0, os.path.dirname(__file__))
from _timing import log
try:
data = json.load(sys.stdin)
tool, path = data["tool_name"], data["tool_input"].get("file_path", "")
except Exception:
sys.exit(0)
if not path.endswith((".js", ".mjs")) or not os.path.exists(path):
sys.exit(0)
p = subprocess.run(["node", "--check", path], capture_output=True, text=True)
if p.returncode:
log("check-js", tool, path, "block")
print(json.dumps({"decision": "block", "reason": f"check-js: node --check failed on {path}:\n{p.stderr[-1500:]}"}))
sys.exit(0)
log("check-js", tool, path, "ok")
sys.exit(0)
Cả ba gọi chung một file nhỏ ghi thời gian chạy, _timing.py:
"""Shared by the build-stage hooks: append how long each hook took to .hook-timing.log (git-ignored)."""
import json, os, time
T0 = time.perf_counter()
def log(hook, tool, path, decision):
ms = (time.perf_counter() - T0) * 1000
root = os.environ.get("CLAUDE_PROJECT_DIR", ".")
if path and os.path.isabs(path):
path = os.path.relpath(path, root)
with open(os.path.join(root, ".hook-timing.log"), "a") as f:
f.write(json.dumps({"hook": hook, "tool": tool, "path": path, "decision": decision, "ms": round(ms, 1)}) + "\n")
Và khối khai báo trong .claude/settings.json của repo demo (hook no-self-approve.py có từ tập Design):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/no-self-approve.py\""
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/protect-paths.py\""
},
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/no-secrets.py\""
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-js.py\""
}
]
}
]
}
}
Bước 7 của playbook (“sửa plan.md trong cùng commit”) mình ép bằng một git pre-commit, không phải hook của Claude Code. Nguyên văn:
#!/usr/bin/env bash
# Plan sync (Build stage): "When implementation departs from the plan, update plan.md in the same commit."
# A commit that changes app/ must also change plan/ — unless it says why not.
# Override for trivial changes (the change-size table in CLAUDE.md): PLAN_SKIP="<reason>" git commit ...
set -euo pipefail
app=$(git diff --cached --name-only -- app/ | wc -l | tr -d ' ')
plan=$(git diff --cached --name-only -- plan/ | wc -l | tr -d ' ')
if [ "$app" -gt 0 ] && [ "$plan" -eq 0 ] && [ -z "${PLAN_SKIP:-}" ]; then
echo "pre-commit: this commit changes app/ ($app file) but no plan/ file." >&2
echo " Update the plan in the same commit, or for a tiny change: PLAN_SKIP=\"<why>\" git commit ..." >&2
exit 1
fi
CLAUDE.md: một dòng là đủ
Về file CLAUDE.md, mình thử hai thứ.
Một: lệnh /init. Chạy trên bản repo đã xoá CLAUDE.md (một lượt chạy), nó sinh 72 dòng (bản của repo: 64).
- Hơn bản của repo ở phần kiến trúc app: thứ tự nạp script, chỉ
store.jschạm bộ nhớ. - Thiếu luật của người: demo chỉ ghi thứ đã chạy, dữ liệu là mẫu, không đọc
.env, không push. - Tự bịa hai dòng “Things Claude gets wrong” chưa từng xảy ra.
Phần kiến trúc đã được ghép vào CLAUDE.md sau khi người soát lại. /init đúng là điểm xuất phát, không hơn.
Hai: quy tắc “sai hai lần thì ghi vào file”. Lỗi lặp nhiều nhất qua bốn lượt chạy là Claude cứ cố dựng script nháp bằng lệnh shell (heredoc, sed -i, vòng lặp, chuỗi &&). Trong phiên headless, những lệnh đó cần duyệt nên bị từ chối: 20 lần. Mình ghi đúng một dòng vào CLAUDE.md, rồi cho chạy cùng một việc kiểm biên 1 MB, mỗi bên hai lần:
| Bị từ chối | Tool call | Chi phí | |
|---|---|---|---|
| Không có dòng đó | 2 + 2 lần | 8–9 | 0,30–0,33 USD |
| Có dòng đó | 0 + 0 lần | 5 | 0,23–0,27 USD |

Lần này, ghi một dòng là đủ, chưa cần tới hook. Giả thuyết ban đầu của mình là “ghi rồi vẫn sai thì phải thành hook”; demo không đi tới bước đó.
Đây là CLAUDE.md bản Build của repo demo, nguyên file (dòng cuối là dòng vừa nói):
# CLAUDE.md — fx-sdlc-lab (demo workspace) Demo workspace for the Fx Studio video series **"AI-Native SDLC"**. The product is a tiny todo app in `app/`. Each episode adds the pieces one SDLC stage needs, so the repo grows stage by stage. Humans read and write Vietnamese here; agent-facing files (`.claude/`, templates) are in English. ## Layout | Path | What | |---|---| | `app/` | The product: static HTML/JS todo app, data in `localStorage` only (no server, no account) | | `tickets/` | Incoming tickets (sample data) | | `intent/` | Plan stage output: one `intent.md` per change, `<YYYY-MM-DD>-<slug>.md` | | `spec/` | Design stage output: one spec per accepted intent, same file name as the intent | | `plan/` | Build stage output: one plan per accepted spec, same file name | | `tests/` | `node --test` tests (no packages) | | `.claude/skills/write-intent/` | Skill: template + checklist + rules for turning an idea into an intent | | `.claude/agents/intent-reviewer.md` | Read-only subagent that prepares an intent for the product owner | | `.claude/skills/write-spec/` | Skill: intent → spec, template + risk list + checklist | | `.claude/skills/policy-*/` | The team's policies (privacy, security, UX) as versioned skills (sample data) | | `.claude/agents/spec-reviewer.md` | Read-only subagent that prepares a spec for the product owner and tech lead | | `.claude/hooks/no-self-approve.py` | Hook: the agent cannot merge, rebase, push, commit on `main`, or set `Status: accepted` | | `.claude/hooks/protect-paths.py` · `no-secrets.py` · `check-js.py` | Build hooks: read-only paths, no credentials, `node --check` after each edit | | `.claude/agents/verifier.md` | Subagent: runs tests, checks the change against its plan, reports only | | `scripts/` | Spec job: `install-hooks.sh` adds a git post-commit hook; accepting an intent runs `/write-spec` headless | | `demo/sdlc-<n>/` | Evidence for each episode: personas, run script, raw transcripts, findings (Vietnamese) | | `docs/` | The channel owner's own notes on the playbook. **Do not edit.** | ## Rules - **Everything lives on `main`.** Intents and specs start as `Status: draft`. **A person approves by changing it to `Status: accepted` and committing.** The agent writes drafts and stops: it never commits, merges, or sets `accepted`. The hook enforces it. (Until SDLC-1, approval was a merge from an `intent/<slug>` branch; those branches are merged as drafts.) - **Demos record only what actually ran.** Transcripts are saved verbatim, never edited. - All people, tickets and numbers in `tickets/` and `demo/` are **sample data**, not real users. No code, tickets or names from real client projects. - Never read or print `.env`, `.secrets/`, tokens. - Never push, create releases, or change remotes. Publishing is the repo owner's call. ## Build - **Run the app:** `python3 -m http.server -d app 8000`, open http://localhost:8000. **Tests:** `node --test` (Node ≥ 18, no packages; tests live in `tests/`). **Lint:** `node --check <file>`, run by a hook after every edit. - Plain HTML/JS, no build step, no npm dependency (`policy-security#S4`). User text via `textContent` (`#S3`). UI text Vietnamese. - **App architecture** (taken from a `/init` run, `demo/sdlc-3/runs/R5a-init/`, checked by a person): plain scripts sharing globals, loaded `store.js` → `backup.js` → `app.js` (pinned by `tests/static.test.js`). `store.js` is the only file that touches `localStorage` (key `todo.v1`); after a read error `save()` returns `'skipped'` and only `replace()` (import) may write. `backup.js` is DOM-free logic and takes `store` as a parameter. `app.js` only wires the DOM; `tests/app.test.js` runs it unchanged in a `vm`. - **Size the change first** and say which size you picked: | Size | Example | Plan | |---|---|---| | Trivial | wording, a colour, a constant | no plan mode | | Small | one well-located bug, one file | plan mode, plan not committed | | Medium | a small feature, a few files | plan mode, `plan/<name>.md` committed | | Large | many modules, architecture | `plan/<name>.md` + tech lead approves before code | - A plan has four sections: **Files that change · Order of work · Risks · Proof** (which test proves each step). Write it as `Status: draft`; a person accepts it. Implement only an accepted plan. If the code has to depart from it, stop and say how: a person updates the plan in the same commit (a git pre-commit hook checks this). ## Things Claude gets wrong One line per mistake, with the month it was added. Added after the **same** mistake happens twice. If it still happens after the line is here, the line becomes a hook and is removed from this list. - 2026-10: Building throwaway scripts in Bash (heredocs, `sed -i`, `for` loops, `&&` chains, `echo $?`). Those need approval and get refused in a headless run (R0, R3, R3b, R3d: 20 refusals). Write the script with the Write tool, then run it with one plain `node <file>`. One command per Bash call.
Skill có được gọi không
Mình viết năm câu đáng lẽ phải gọi skill policy-security, mỗi câu chạy một phiên plan mode mới, cả bộ chạy hai lần. Kết quả:
- Cả hai lần, không câu nào gọi skill qua công cụ Skill: 0/5.
- Lần hai mình tách kỹ hơn: 2/5 câu Claude tự mở file
SKILL.mdra đọc (policy được đọc, nhưng skill không kích hoạt), 3/5 câu không đụng tới policy nào. - Phiên thử chạy trên máy có skill riêng của mình, nên môi trường không sạch; ở máy khác kết quả có thể khác.
Đây là file triggers.md của skill đó, nguyên văn:
# Trigger set · policy-security Five ways people ask for work this policy must govern. Each line should make Claude load `policy-security`. Re-run after adding a skill or changing the model: `scripts/skill-triggers.sh policy-security`. 1. Plan how to let users import their todo list from a JSON file. 2. Add a "clear completed" button that removes every finished task at once. 3. Show the task title in a tooltip when the mouse hovers over it. 4. Can we pull in a date-picker library from a CDN for the due-date field? 5. Khách muốn dán một danh sách việc từ Excel vào app, làm sao cho nhanh?
Và kết quả lần hai của scripts/skill-triggers.sh, nguyên văn (LOADED là skill được gọi, READ là chỉ mở file, MISSED là không đụng tới):
MISSED 1. Plan how to let users import their todo list from a JSON file. READ 2. Add a "clear completed" button that removes every finished task at once. MISSED 3. Show the task title in a tooltip when the mouse hovers over it. READ 4. Can we pull in a date-picker library from a CDN for the due-date field? MISSED 5. Khách muốn dán một danh sách việc từ Excel vào app, làm sao cho nhanh? policy-security: 0/5 loaded by the Skill tool · 1.4373 USD
Chạy song song: hỏi trước rồi mới mở worktree
Còn ba việc đang chờ: A sửa năm câu từ chối, B một ca lỗi Khoa nêu khi review, C tính năng hạn chót (spec còn nháp). Trước khi mở worktree, mình hỏi một phiên plan mode: việc nào đụng chung file? Kết quả (một lượt chạy):
- A và B chạy song song được.
- A với C, B với C thì không nên: đụng chung
backup.jsvà cùng mở rộng bộ giả lập test. - C chưa nên bắt đầu: spec còn
draft, chưa có plan. - Nó còn sửa lại mô tả lỗi của B: ca Khoa kể không xảy ra; ca thật là hai lần nhập file chồng nhau, hoặc xoá việc trong lúc đang đọc file. Mình đọc code để kiểm: đúng.
Rồi hai session chạy cùng lúc, mỗi session một worktree trên một nhánh tạm:
- A: Claude tự xếp hạng Trivial, 38 test. Nó tự nhắc câu M8 từng do Thảo duyệt nên cần Thảo đồng ý (Thảo đồng ý).
- B: hạng Small, viết test fail trước (38/39) rồi mới sửa (39/39).
Người commit từng nhánh kèm plan, gộp về main: không xung đột, 39 test chạy qua, rồi xoá nhánh tạm.
Demo cho thấy gì
Mỗi kịch bản mình chạy một hoặc hai lần, người trả lời và người review là nhân vật giả lập. Phiên chấm điểm, verifier và Claude viết code đều cùng họ model. Đây là ví dụ thật, không phải thống kê.
Nhưng demo đủ để thấy hai điều:
- Với một việc nhỏ như app này và một spec đã tốt, plan không làm code đúng hơn bao nhiêu. Làm luôn cũng đạt.
- Thứ plan mang lại là câu hỏi được đặt ra trước khi code (sáu câu đúng chỗ lượt đầu tự quyết) và một bản ghi mà người khác đọc được (tech lead không cần đọc hội thoại vẫn duyệt được, tìm ra tám chỗ thiếu).
- Plan còn để lại test trong repo (0 → 38) và chặn trước một rủi ro trên Safari iOS.
- Không lớp kiểm nào bắt được hết.
- Mỗi người duyệt một lớp: plan, câu chữ, verifier, tech lead.
- Vẫn cần một người đọc lại từ đầu, so với spec.
Hai giả thuyết ban đầu của mình ra ngược và mình ghi lại đúng như vậy:
- Bản làm luôn không kém bản có plan về độ đúng.
- “Ghi rồi vẫn sai thì thành hook” không xảy ra, vì ghi một dòng là hết sai.
Và một điều mình chưa làm được: bảng kiểm tay T0–T13 trên ba trình duyệt chưa ai chạy. Tính năng sao lưu trong demo chưa nghiệm thu.
Chỗ playbook chưa trả lời
Từ đây trở xuống là nhận xét của mình, không có trong bài gốc.
- Plan cho mọi việc dễ thành thủ tục.
- Bắt viết và commit một bản plan cho việc sửa một dòng chữ là lãng phí.
- Playbook nói plan mode là mặc định, nhưng không nói khi nào được bỏ qua.
- Không có ranh giới thì team sẽ hoặc làm cho có, hoặc lặng lẽ bỏ luôn.
- File
CLAUDE.mdcũ dần mà không ai để ý.- Quy tắc “sai hai lần thì ghi vào” làm file dài ra. Quy tắc “dưới một trang” bắt file phải ngắn lại.
- Playbook giao file cho cả team cùng giữ, nên không ai cụ thể được giao việc dọn.
- Vài tháng sau, file chứa những dòng chỉ đúng với code cũ.
- Không biết skill có thật sự được gọi.
- Playbook khuyên thử bằng nhiều cách hỏi. Nhưng thử một lần lúc viết skill thì chưa đủ.
- Đổi model, thêm skill mới, là skill cũ có thể không còn được chọn mà không ai hay.
- Ở demo, năm câu đáng lẽ phải gọi skill bảo mật, không câu nào gọi (hai lần chạy).
- Song song va vào chuyện file chung.
- Với app iOS, file project của Xcode (
project.pbxproj) đổi gần như mỗi lần thêm file. Hai session cùng thêm file là xung đột ở đó, dù code chẳng liên quan gì nhau. - Build mobile lại chậm, simulator ngốn máy. Ba session trên một máy có khi còn chậm hơn một.
- Ở demo, hai việc gộp lại không xung đột, nhưng đó là vì mình hỏi trước và vì app chỉ có vài file.
- Với app iOS, file project của Xcode (
Mình sẽ vá thế nào
Đây là vài cách vá ban đầu của mình, mới thử ở một demo nhỏ, nên bạn xem như điểm xuất phát. Bảng phân hạng thay đổi, năm câu chất vấn, việc giao CLAUDE.md cho một người giữ và bộ thử kích hoạt skill đều là của mình, không có trong playbook.
Cách 1: phân hạng thay đổi để quyết định mức plan
| Hạng | Ví dụ | Plan |
|---|---|---|
| Rất nhỏ | Sửa chữ, đổi màu, đổi hằng số | Không cần plan mode |
| Nhỏ | Sửa một bug khoanh vùng rõ, một file | Plan mode, không cần commit plan |
| Vừa | Tính năng nhỏ vài file | Plan mode, commit plan.md |
| Lớn | Đụng nhiều module, đổi kiến trúc | plan.md + tech lead duyệt trước khi code |

Ghi bảng này vào CLAUDE.md (bản tiếng Anh ở mục Build phía trên), để agent tự đề xuất hạng ngay khi nhận việc. Ở demo, mình đưa bốn yêu cầu cỡ khác nhau, đáp án chốt trước khi chạy. Claude xếp đúng cả bốn (một lượt chạy):
- Đổi màu nền nút “Thêm” → Rất nhỏ.
- Dòng đếm ghi sai số việc chưa xong (lỗi giả định) → Nhỏ. Claude đọc code, nói hai dòng đếm trông đúng và sẽ đổi hạng nếu nguyên nhân nằm chỗ khác.
- Nút “Xoá các việc đã xong” → Vừa.
- Gắn nhãn cho từng việc và lọc theo nhãn → Lớn.
Bốn câu này rõ ràng. Ca lưng chừng thì chưa thử.
Cách 2: năm câu chất vấn plan
Dùng cố định mỗi lần duyệt plan:
- File nào thay đổi, có file nào bất ngờ không?
- Thay đổi này có thể làm hỏng luồng nào đang chạy?
- Bước nào rủi ro nhất và làm sao biết nó hỏng?
- Test nào sẽ fail nếu làm sai?
- Có cách nào đơn giản hơn mà Claude đã bỏ qua?
Đây là prompt mình dùng trong demo, nguyên văn, để bạn chép:
Before I accept, five questions about your plan. Answer each briefly, then revise the plan wherever an answer shows a gap: 1. Which files change, and is any of them a surprise? 2. Which flow that works today could this change break? 3. Which step is riskiest, and how would we know it broke? 4. Which test fails if that step is done wrong? 5. Is there a simpler way you considered and dropped?
Ở demo, riêng câu về test đã làm bản plan có test. Thêm một điều demo cho thấy: người duyệt nên đọc plan không kèm hội thoại, như người sẽ làm theo nó.
Cách 3: chăm CLAUDE.md như chăm code
- Một người chịu trách nhiệm, thường là tech lead.
- Mỗi dòng trong mục “những chỗ Claude hay sai” ghi kèm tháng thêm vào.
- Mỗi tháng rà một lần, dòng nào ba tháng không còn tác dụng thì xoá.
- Ghi rồi mà vẫn sai thì đừng ghi thêm lần nữa. Chuyển thành hook.
Ở demo, một dòng là đủ. Nhưng mình vẫn giữ luật này cho lỗi nào ghi rồi còn tái. Chạy /init thì soát lại kỹ: đừng chép nguyên mục “Things Claude gets wrong” nó tự sinh.
Cách 4: một bộ thử kích hoạt skill
- Với mỗi skill quan trọng, viết năm câu yêu cầu theo năm cách khác nhau, lưu thành một file (như
triggers.mdở trên). - Mỗi lần thêm skill hay đổi model thì chạy lại, xem skill còn được gọi không.
- Ra 0/5 như demo thì đừng trông skill tự kích hoạt: gọi tên nó trong prompt, hoặc ghi vào plan.
Về sau, bộ này chính là một phần của eval ở bước Test.
Còn với dự án mobile, có bộ hook riêng và cách chia việc song song theo module thay vì theo màn hình. Chỗ này mình để dành một tập riêng.
Tuần đầu tiên
Nếu muốn thử bước Build ở team mình, đây là tuần đầu mình đề xuất:
- Chạy lệnh
/init, cắt fileCLAUDE.mdxuống dưới một trang, thêm bảng phân hạng thay đổi. - Thêm hai hook đơn giản nhất: chặn đường dẫn được bảo vệ và chạy formatter sau mỗi lần sửa.
- Làm ba việc hạng vừa bằng plan mode và commit plan. Rồi đếm xem bao nhiêu việc merge được ngay từ lượt code đầu.
- Ghi lại mọi lỗi Claude lặp lại. Cuối tuần, mỗi lỗi thành một dòng trong
CLAUDE.md, hoặc thành một hook. - Thử hai session song song, trên hai phần code không đụng nhau, ít nhất một ngày.
Muốn bắt đầu nhanh thì chép mục Build của CLAUDE.md, ba hook trong .claude/hooks/, verifier.md, scripts/pre-commit và scripts/skill-triggers.sh từ repo demo, rồi sửa danh sách đường dẫn và lệnh kiểm cho hợp repo của bạn trước tiên.
Tạm Kết
Playbook dời việc review thiết kế lên trước khi có code: Claude viết plan.md trong plan mode, một người duyệt, rồi mới cho code; CLAUDE.md, skill và hook giữ cho agent đi đúng đường khi người không ngồi canh.
Bước Build không làm agent code chậm lại. Nó dời chỗ con người đứng: từ ngồi canh từng dòng code, sang duyệt một bản plan trước khi có code và review thành quả sau khi agent làm xong. Demo nhỏ của mình thêm một điều: với việc nhỏ và spec tốt, plan không làm code đúng hơn, nhưng nó làm cho câu hỏi đến trước code và để lại một bản ghi người khác đọc được. Còn câu chữ sai thì vẫn lọt qua cả bốn lớp, nên vẫn cần một người đọc lại từ đầu, so với spec.
Tập sau của loạt AI-Native SDLC là Test: agent tự kiểm việc của mình và vì sao chính cấu hình của agent cũng phải được test.
Nguồn
- Louis Claxton, Anthropic, The AI-native SDLC playbook (21/8/2026), phần Stage 3: claude.com
- Demo SDLC-3 của mình, chạy ngày 27/10/2026: 53 lượt chạy cộng 2 lượt bộ thử kích hoạt skill, bốn nhân vật giả lập, mỗi kịch bản một hoặc hai lần. Code, transcript nguyên văn và kết luận: github.com/fx-studio/fx-sdlc-lab (thư mục
demo/sdlc-3/).
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Fan page
Tags
Recent Posts
- AI-Native SDLC · Build: plan trước, code sau
- AI-Native SDLC · Design: intent.md thành spec.md
- AI-Native SDLC · Plan: Ý tưởng thành intent.md
- Linus Torvalds dùng Claude Code: chuyện thật sau 8 bản vá
- AI-Native SDLC · Tổng quan: Code nhanh rồi, sao team vẫn chậm?
- Code rẻ đi thì điểm nghẽn dời về đâu?
- Cửa sổ ngữ cảnh: nạp lại không phải là nhớ (Tui hỏi AI một câu ngu – Tập 1)
- Kill switch cho AI agent: guard in “Blocked”, lệnh vẫn chạy
- Nghĩa địa skill – Tập 5: xoá skill không dùng
- Nghĩa địa skill – Tập 4: vì sao AI agent chọn công cụ sai
You may also like:
Archives
- October 2026 (5)
- September 2026 (9)
- August 2026 (2)
- July 2026 (7)
- June 2026 (13)
- May 2026 (2)
- April 2026 (5)
- March 2026 (5)
- February 2026 (1)
- January 2026 (10)
- December 2025 (1)
- October 2025 (1)
- September 2025 (4)
- August 2025 (5)
- July 2025 (10)
- June 2025 (1)
- May 2025 (2)
- April 2025 (1)
- March 2025 (8)
- January 2025 (7)
- December 2024 (4)
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)








Leave a Reply