Harness ép kỷ luật, không thay kỹ sư
Giá trị của một harness cho autonomous coding không nằm ở chỗ 'AI tự code', mà ở chỗ nó bắt mình đi qua những bước một kỹ sư tốt thường bỏ qua khi đang vội. Cái thang đó mới là phần portable.
Một harness tốt không làm cho AI thông minh hơn; nó làm cho mình khó cẩu thả hơn.
Nói kết quả trước cho dễ hình dung. Mình mô tả một feature trong một câu, harness phỏng vấn mình vài lượt để chốt scope, rồi nó chạy: chia việc, dispatch worker vào worktree riêng, viết test, review, và merge. Lúc quay lại, cái mình nhận không phải một đống suggestion để tự đi ráp, mà là commit đã test, đã merge, kèm một trace log đầy đủ để audit lại từng tool call. Một người trong vòng lặp để ra quyết định, không phải một người ngồi gõ từng dòng. Đó là outcome mình muốn, và phần còn lại của bài là cách một harness đạt được nó mà không biến thành máy đốt token vô kỷ luật.
Ý tưởng cốt lõi gói trong một câu: lấy pattern orchestrator + worker ở bài trước và đông cứng nó thành một cái thang cố định, mỗi nấc ép một kỷ luật mà mình hay bỏ qua khi vội. Không phải “thêm agent cho nhanh”, mà là “mỗi lần đều đi qua đúng các bước đó”. Cụ thể hoá ý tưởng này là autocode-kit: một portable harness cho autonomous coding, chạy trên Claude Code subscription, không cần API key riêng. Phần dưới mình đi qua cái thang đó, hai safety rail bằng code, chế độ walk-away, và những chỗ mình sẽ không để AI tự quyết.
Ở bài Claude Code làm orchestrator, mình kết lại bằng một câu hỏi khá thực dụng: việc nào xứng đáng tách khỏi đầu orchestrator? Câu trả lời không nằm ở chỗ “cho thêm agent chạy song song”, mà nằm ở việc biến câu hỏi đó thành một kỷ luật cố định. Nếu orchestrator cứ mỗi lần làm một kiểu, hôm nay tự nghĩ PRD, mai nhảy thẳng vào code, mốt quên review scope, thì hệ thống có nhiều agent cũng chỉ là nhiều tay gõ phím hơn. Harness (bộ khung điều khiển AI) đáng giá khi nó bắt mình đi qua cùng một cái thang, dù stack là Rails, Next.js, Laravel hay một repo nội bộ nào đó.
Bài AI harness là gì đã nói harness không phải model. Nó là phần mềm bao quanh model: command, tool, Skills, MCP, sandbox, worktree, policy, log, review. Nếu chỉ nhìn autonomous coding như “AI tự code từ đầu tới cuối” thì hơi lệch, và cũng dễ thất vọng. Điểm mình thấy đáng học hơn là một harness tốt đóng gói những bước một kỹ sư tốt sẽ làm khi còn đủ bình tĩnh, rồi ép quy trình phải đi qua các bước đó ngay cả lúc mình đang muốn nhảy cóc. Cái outcome ở trên chỉ tin được khi cái thang phía dưới đủ chặt.

Giá trị nằm ở cái thang, không nằm ở nút bấm
Pipeline của kit nhìn qua khá dài: idea → PRD → ADR(s) → journey → ExecPlan → task-graph → task-card → dispatch → review → merge. Mỗi bước là một slash command, phỏng vấn mình một chút rồi draft artifact tương ứng. Nếu chỉ đọc như feature list thì sẽ thấy hơi nghi thức. Nhưng cách đọc đúng hơn là: mỗi nấc chặn một kiểu lười biếng rất quen thuộc trong coding với AI. Lười viết rõ scope, lười ghi trade-off, lười định nghĩa hành vi quan sát được, lười viết test trước, lười giới hạn file mà worker được đụng vào.
PRD (product requirements document, mô tả yêu cầu sản phẩm) không cho qua brief quá mỏng. Dưới 20 chữ thì bị từ chối, vì “làm một dashboard đơn giản” không phải yêu cầu, nó là một mong muốn chưa chịu trách nhiệm. Quan trọng hơn, PRD bắt phải có Scope OUT không rỗng: những thứ cố ý không làm. Đây là một chi tiết nhỏ nhưng rất người. Khi làm với AI, scope creep thường không đến như một quyết định lớn; nó đến qua vài câu “tiện thể thêm luôn”, rồi agent kéo theo migration, UI state, API mới, test mới, và cuối cùng ticket ban đầu biến thành một bụi dây.
ADR (architecture decision record, ghi lại quyết định kiến trúc) cũng vậy. Mỗi quyết định phải có ít nhất hai alternative được cân nhắc, và phải viết rõ accepted negatives, tức là mình biết mình đang chấp nhận mất gì. Trạng thái mặc định là proposed, không phải accepted, để nhắc rằng đây vẫn là một đề xuất cho tới khi đủ căn cứ. Với con người, phần “mất gì” thường bị bỏ qua vì nó làm mình chậm lại. Với AI, bỏ qua phần đó còn nguy hiểm hơn, vì model rất giỏi làm cho một hướng nghe hợp lý trong ngữ cảnh hẹp của prompt hiện tại.
Từ hành vi người dùng tới việc có thể dispatch
Nấc journey bắt viết Given/When/Then và chỉ nhận outcome quan sát được. Nó từ chối implementation language. Nói cách khác, mình phải mô tả người dùng làm gì và thấy gì, không được lén nhét “service X gọi repository Y” vào một user journey. Đây là ranh giới quan trọng, vì nếu hành vi đã bị trộn với implementation, test và review sau đó sẽ chỉ xác nhận rằng code làm đúng theo một tưởng tượng kỹ thuật, chứ chưa chắc sản phẩm chạy đúng với người dùng.
ExecPlan (execution plan, kế hoạch thực thi theo pha) tiếp tục kéo mình về mặt đất. Phase 0 luôn là viết failing E2E trước, rồi xác nhận nó fail đúng lý do. Không phải “nên có test nếu kịp”, mà là pha đầu tiên. Kit cũng không cho kế hoạch phình ra quá 8 phase, vì một plan dài lê thê thường là dấu hiệu mình chưa chia việc đủ rõ hoặc đang cố nhét quá nhiều uncertainty vào một artifact. Mình thích constraint này vì nó không cố tỏ ra thông minh. Nó chỉ đặt một thanh chắn đủ cứng để mình không tự ru ngủ bằng một kế hoạch đẹp nhưng không dispatch được.
Đến task-card, discipline chuyển từ planning sang execution boundary. Một card dispatch được phải có frontmatter, allowed_files rõ ràng, acceptance criteria, và “how to verify locally”. allowed_files là chỗ rất đáng chú ý. Khi worker chạy trong isolated git worktree (một bản checkout riêng của repo để làm việc độc lập), nó không được tự do đụng mọi nơi chỉ vì “có vẻ liên quan”. Card tốt giống một ticket cho contractor: đây là vùng được sửa, đây là kết quả cần có, đây là cách chứng minh. Nếu cần mở rộng scope, đó là tín hiệu quay lại orchestrator, không phải để executor tự quyết trong lúc đang code.
Review không nên chỉ là một đoạn văn hay
Review-pass trong kit có ba kết quả: PASS, REQUEST-CHANGES, hoặc ESCALATE. Phần thú vị là review không hoàn toàn giao cho LLM. Một bash runner chạy scope check và verify commands một cách deterministic (xác định được, cùng input thì cùng output). LLM đi qua acceptance criteria và convention, nhưng chuyện worker có sửa ngoài allowed_files hay lệnh verify có pass không thì để code kiểm. Đây là một thiết kế mình thấy đúng hướng: dùng model ở nơi cần judgment, dùng chương trình ở nơi cần luật cứng.
Hai safety rail cũng theo tinh thần đó. Thứ nhất là trace capture: hook log mọi tool call vào traces/YYYY-MM-DD/, để một unattended run (lượt chạy không có người ngồi canh từng bước) có thể audit lại được. Không phải “AI nói nó đã làm gì”, mà là có dấu vết tool call để xem. Thứ hai là check_invariants.py, một blocking gate. File invariants.yaml ship rỗng, với rules: [], và điều đó không phải thiếu sót. Bạn chỉ thêm rule khi một ADR hoặc một bug đã bắt được làm nó đủ cụ thể. Mỗi rule là blocking, không có mức warn cho vui.
Điểm này quan trọng vì nhiều hệ thống AI dễ rơi vào kiểu policy trang trí. Có một file quy tắc dài, nghe rất trưởng thành, nhưng không ai biết rule nào thật sự chặn được lỗi. autocode-kit chọn cách ngược lại: ban đầu không có rule nào, nhưng rule đã vào thì executor và review-pass đều phải chạy qua. Điều này làm invariant trở thành ký ức kỹ thuật của dự án, không phải manifesto. Một bug lặp lại hai lần thì đừng chỉ dặn AI “lần sau cẩn thận”; hãy biến nó thành test hoặc invariant.
Walk-away không có nghĩa là bắn đi rồi cầu may
/mindful trong kit là router/dashboard: nó nhìn xem mình đang ở đâu trên ladder và command tiếp theo là gì. /mindful go thì bắt đầu tự động hơn: dispatch card ready tiếp theo, monitor executor, review, merge, đọc lại state, rồi dispatch card mới được unblock. Nó lặp tối đa 10 iteration, như một guard để bug không dispatch mãi. Từ “monitor” ở đây cần hiểu chính xác: nó không fire-and-forget. Nó poll worktree/PR theo timer để biết executor xong, bị block, hay timeout.
/mindful block đi xa hơn: nén cả planning ladder và execution loop vào một run, không có wall-clock cap, với trần tự nhiên khoảng 25 cards. Đây là chế độ walk-away đúng nghĩa hơn, nhưng không nên tưởng tượng nó như một scheduler daemon chạy nền. Orchestration loop vẫn là một Claude session đang điều khiển /mindful go|block. Nếu sau này outgrow kiểu này, scheduler.py là bước build tự nhiên. Nhưng bản thân kit không giả vờ rằng nó đã là một hệ thống điều phối nền hoàn chỉnh.
Caveat lớn nhất phải nói thẳng: trong go/block mode, planner review và merge work của chính các executor mà nó dispatch. Không có human gate thứ hai ở điểm merge. Với greenfield hoặc low-stakes work, đây có thể chính là điều mình muốn kiểm nghiệm: liệu pipeline, tests, invariant và review-pass có đủ để cho hệ thống tự đi một đoạn hay không. Nhưng với code nhạy cảm, dữ liệu thật, billing, auth, security, migration nguy hiểm, hoặc repo đang có nhiều người đụng, mình sẽ không để AI tự merge. Cách hợp lý hơn là dispatch và review unattended, nhưng merge bằng tay; hoặc ít nhất supervise run khi nó đi qua các đoạn rủi ro.
Không hoàn chỉnh là một lựa chọn thiết kế
autocode-kit cũng không có auto-learning loop. Nó không tự bắt bug rồi tự thêm regression test, không tự siết CLAUDE.md, không tự cập nhật invariant sau mỗi lần sai. Nghe có vẻ thiếu, nhưng mình nghĩ đây là một sự trung thực tốt. Vòng học tự động là thứ rất dễ demo, rất khó làm đúng, và nếu làm sai thì nó âm thầm tích lũy rule tệ. Practice vẫn giữ được bằng tay: mỗi bug nghiêm túc phải có failing test; mỗi lỗi lặp lại phải siết invariant hoặc instruction; mỗi decision quan trọng phải quay về ADR.
Rule build của kit cũng rất đáng giữ: làm thủ công ít nhất 3 lần trước khi build skill cho nó. Đây là một nguyên tắc nhỏ nhưng chống lại bản năng automation rất mạnh của vibecoder. Thấy một thao tác lặp lại một lần là muốn viết command. Nhưng nếu chưa làm đủ vài lần, mình chưa biết phần nào là pattern thật, phần nào chỉ là tình cờ của một repo. Build skill quá sớm thường tạo ra một automation biết chạy, nhưng chưa biết chịu trách nhiệm.
Mình thích cách incomplete-by-design này vì nó kéo trọng tâm về kỷ luật, không phải ảo tưởng tự trị. No scheduler nghĩa là bạn vẫn biết ai đang điều khiển loop. No auto-learning nghĩa là bạn vẫn phải quyết định lỗi nào xứng đáng trở thành rule. invariants.yaml rỗng nghĩa là mỗi dự án bắt đầu với sự khiêm tốn, không vay mượn luật của một context khác. Và khi rule xuất hiện, nó xuất hiện vì dự án đã trả tiền cho hiểu biết đó bằng một ADR hoặc một bug thật.
Thứ portable là hình dạng, không phải nội dung
Phần machinery của harness có thể copy gần như nguyên: slash command, ladder, dispatch vào worktree, trace capture, review-pass, invariant gate. Nhưng artifact thì không portable theo kiểu bê nguyên qua dự án khác. PRD, ADR, journey, ExecPlan, cards đều phải generate mới theo repo, team, constraints và risk profile của dự án đó. Đây là khác biệt giữa template và discipline. Template cho bạn một form; discipline bắt bạn điền form bằng những quyết định thật.
Vì vậy takeaway của mình không phải là “hãy để AI code thay mình”. Câu đó vừa mơ hồ vừa dễ làm sai. Takeaway gần hơn là: hãy thiết kế harness sao cho nó bắt mình làm những việc mình thường bỏ qua khi đang vội. Bắt mình viết Scope OUT trước khi hào hứng. Bắt mình ghi lại cái mình chấp nhận mất khi chọn một hướng kiến trúc. Bắt mình mô tả hành vi quan sát được trước khi nghĩ tới code, viết failing E2E trước khi viết logic, giới hạn file mà worker được đụng, và biến mỗi bug lặp lại thành một invariant chặn cứng thay vì một lời dặn dò.
Cũng nói thật luôn cho rõ: kit này đang trong quá trình phát triển mỗi ngày, và nó được may đo cho cách mình làm việc, nên không chắc hợp với mọi người. Với lại nó tốn token lắm. Khi nào nó ổn định hơn mình sẽ giới thiệu chi tiết hơn; còn hiện tại bạn có thể lấy về để nắm concept và dùng thử. Bản kit hiện chỉ chia sẻ cho Insider.
Nếu làm đúng, giá trị còn lại sau khi đổi model, đổi editor, đổi stack, thậm chí đổi executor, vẫn là cái thang đó. Automation giúp đi nhanh hơn, nhưng cái làm mình tin được vào một unattended run là các ràng buộc nhỏ, khô khan, kiểm được. Mình đang nghiêng về cách nhìn harness như một hệ thống ép kỷ luật hơn là một hệ thống thay thế kỹ sư. Còn bạn, trong ladder từ idea tới merge, nấc nào là nấc bạn dễ bỏ qua nhất khi để AI code cùng mình — và bạn có thật sự dám để nó tự merge work của chính nó không?