SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor

Old Coder: Review evidence, không review code

Tìm hiểu cách Old Coder thay review code bằng SPEC và báo cáo EVIDENCE có thể chạy lại, cùng vai trò của mutation testing vượt ngoài coverage.

Những gì skill Old Coder thực sự thay đổi

Skill Old Coder thay thế việc review code bằng việc review tài liệu. Coding agent của bạn viết SPEC trước khi viết bất kỳ code nào. Bạn phê duyệt tài liệu đó, rồi agent mới triển khai. Sau đó, agent tự kiểm tra phần việc của mình bằng một chuỗi cố định gồm các kiểm tra tự động gọi là gauntlet, rồi gửi cho bạn báo cáo EVIDENCE chứa các command chính xác và số liệu thực tế. Bạn đọc 2 tài liệu. Bạn không bao giờ đọc diff.

Cách làm này chỉ hiệu quả nếu 2 tài liệu đó tạo được mức độ tin cậy mà trước đây diff tạo ra. SPEC tạo được mức độ tin cậy đó vì bạn phê duyệt nó trước khi có code, nên agent không thể định hình SPEC theo code mà nó đã viết. Báo cáo EVIDENCE tạo được mức độ tin cậy đó vì mọi số liệu trong báo cáo đều đến từ một command mà bạn có thể tự chạy và theo dõi để xác nhận cùng một số liệu. Nếu một trong 2 phần bị nới lỏng, bạn đã thay việc review bằng bản tóm tắt của việc review. Cách này còn tệ hơn đọc diff vì nó tạo cảm giác công việc đã hoàn tất.

Skill này chỉ là markdown, nên hoạt động với bất kỳ agent nào tuân theo hướng dẫn bằng văn bản: Claude Code, Codex CLI, Cursor hoặc loop của riêng bạn. Nó thuộc cùng nhóm với skill persona senior developer lười biếng Ponytail. Nếu format file này còn mới với bạn, agent skill là gì và agent load skill như thế nào giải thích các cơ chế liên quan.

SPEC là quyết định duy nhất bạn vẫn phải đưa ra

SPEC là kế hoạch kiểm thử được viết trước khi code tồn tại. Skill file yêu cầu SPEC phải có 4 nội dung.

  • Kịch bản cụ thể: input, output mong đợi, trường hợp biên và trường hợp lỗi. divide(1, 0) raises ZeroDivisionError with message X, không phải "xử lý input không hợp lệ".
  • Ràng buộc phủ định: những thứ không được thay đổi, chẳng hạn các test hiện có và signature của public API.
  • Kế hoạch setup: mọi tool và dependency mới, mỗi mục có một dòng giải thích lý do cần dùng.
  • Đường dẫn file tuyệt đối để bạn có thể mở file mà không phải tìm.

Kế hoạch setup là nơi có thể thấy rõ giới hạn dữ liệu huấn luyện của model, vì một tool hoặc version được pin và nhớ từ trước có thể đã lỗi thời một năm. Vì vậy, nên cung cấp cho agent một instance SearXNG tự host để tìm kiếm trên web và yêu cầu agent kiểm tra version hiện tại trước khi đề xuất.

Hãy approve SPEC, rồi commit nó. Một SPEC có thể bị chỉnh sửa sau khi approve không phải là contract, còn commit là thứ cho phép bạn kiểm tra sau này rằng evidence đã được đo dựa trên đúng nội dung bạn ký.

Đây là quyết định yes hoặc no duy nhất còn lại dành cho bạn. Đó cũng là điểm cốt lõi và rủi ro của quy trình. Hãy xử lý việc này như một thay đổi trên production, vì đúng là như vậy. Nếu bạn đã chạy một approval gate rõ ràng trước các hành động của agent, việc approve SPEC có thể dùng cùng vị trí đó trong workflow của bạn.

Báo cáo EVIDENCE: số liệu kèm command

Báo cáo EVIDENCE là thứ bạn đọc ở cuối. Kỹ năng này yêu cầu ánh xạ từng hành vi trong đặc tả với test kiểm tra hành vi đó, báo cáo từng layer của gauntlet cùng command đã chạy và kết quả thực tế, lấy mọi số liệu từ một lần chạy mới sau lần sửa code cuối cùng, đồng thời liệt kê mọi layer bị bỏ qua cùng lý do. Tính từ không phải là bằng chứng. “Cả 50 test đều pass, coverage đạt 49/49 statement” là một kết quả. “Đã được test kỹ” thì không.

Báo cáo demo trong repository, demo-rate-limiter/evidence.md, xác định trạng thái source bằng commit và tree hash sha256. Dòng đó quan trọng hơn bạn nghĩ, vì nó cho biết chính xác những byte nào đã tạo ra các số liệu đó. Nếu thiếu dòng này, báo cáo có thể âm thầm mô tả một working tree không còn tồn tại. Demo cũng cho thấy chính sự lệch trạng thái đó. Ở commit acc5a89, báo cáo ghi source state là 8b88bda và có 41 test pass. Trên main, tính đến ngày 12 October 2026, báo cáo ghi commit 42528d9, tree hash 5aa96ec5487c957c và 50 test pass; coverage không đổi ở mức 49/49 statement và 20/20 branch.

Ba quy tắc chống gian lận giúp báo cáo trung thực, và file kỹ năng nêu chúng dưới dạng bắt buộc tuyệt đối. Không bao giờ làm yếu một test để test pass: không mở rộng assertion, không tăng tolerance. Không bao giờ sửa test và implementation trong cùng một bước để đạt trạng thái xanh, vì việc sửa đồng thời sẽ che khuất thành phần nào bị sai. Không bao giờ báo cáo một layer chưa chạy: “skipped, no tool available, manual mutation instead” vẫn giữ được độ tin cậy, còn kết quả bịa đặt sẽ phá hỏng toàn bộ cơ chế.

Cài skill và cố định commit đã cài

Repository là AmazingAng/old-coder và được cấp phép theo MIT. README của repository có hướng dẫn cài đặt một dòng bằng skills CLI. Lệnh này chỉ rõ tên skill vì hiện repository cũng cung cấp thêm skill thứ hai, old-coder-api, để thiết kế HTTP và JSON API:

npx skills add https://github.com/amazingang/old-coder --skill old-coder

Lệnh đó cài phiên bản mà main trỏ tới tại thời điểm bạn chạy lệnh, còn bản thân CLI cũng có thể thay đổi. Hãy kiểm tra vị trí các file được cài trước khi cho rằng skill đã hoạt động:

ls ~/.claude/skills/old-coder/

Bạn sẽ thấy SKILL.md và một thư mục references/. Nếu không có các mục này thì skill không nằm ở nơi Claude Code tìm kiếm. skills CLI cài riêng cho từng agent: đường dẫn global của Claude Code là ~/.claude/skills/, còn ~/.agents/skills/ thuộc về các agent khác như Codex. Vì vậy, một skill chỉ được cài ở đó vẫn tồn tại trên disk nhưng Claude Code không bao giờ load nó. Chạy npx skills@latest add ... để tránh dùng bản CLI cũ trong cache.

Nên cài thủ công vì cách này cho phép bạn ghi lại chính xác những gì đã cài:

git clone https://github.com/AmazingAng/old-coder.git
cd old-coder
git checkout acc5a89
git rev-parse HEAD
mkdir -p ~/.claude/skills
cp -r skills/old-coder ~/.claude/skills/

acc5a89 là commit mới nhất của main vào ngày 17 August 2026. Kể từ đó, main đã chuyển sang a0eb529 vào ngày 18 August 2026, và commit này vẫn là commit mới nhất vào ngày 12 October 2026. Hãy tự chọn một commit và ghi lại. Repository đang được phát triển liên tục. Tài liệu tham chiếu gauntlet, các template và protocol của verifier đã được chuyển giữa các file. Nếu các báo cáo EVIDENCE của bạn không ghi rõ phiên bản skill đã chấm chúng, bạn không thể biết thay đổi đến từ code của mình hay từ bộ quy tắc. Lưu hash của commit đó cạnh SPEC, trong cùng repository với code mà nó chi phối.

Với agent không đọc ~/.claude/skills, hãy thêm skills/old-coder/SKILL.md và skills/old-coder/references/gauntlet.md vào system prompt hoặc file rules của agent. Chỉ cần tích hợp như vậy.

Điều gì chạy trong gauntlet

Gauntlet là một stack gồm nhiều layer, được chạy một lần sau khi mọi hành vi trong spec đều đạt. Skill này gồm:

  • Toàn bộ test suite để phát hiện regression. Không được có failure mới; mọi failure đã tồn tại phải được ghi nhận trước làm baseline.
  • Static type, cùng lint và format, để phát hiện cả nhóm lỗi và tình trạng lệch chuẩn.
  • Coverage trên các dòng đã thay đổi. Layer này phải thoát với mã khác 0 khi không đạt threshold.
  • Mutation testing để phát hiện các test không thực sự assert điều gì.
  • Property-based test cho những edge case không ai nghĩ đến.
  • Ngân sách complexity, một lần chạy thực tế chương trình thật, quét supply chain và secrets, cùng một lần chạy kiểm tra sức khỏe suite theo thứ tự ngẫu nhiên.
  • Các layer theo domain, được chọn dựa trên rủi ro của task: stress concurrency, tương thích API, diễn tập rollback, benchmark latency.

Demo nối các layer này vào một script duy nhất, demo-rate-limiter/tools/gauntlet.sh, dùng toolchain được pin trong requirements-dev.txt: pytest, pytest-cov, coverage, hypothesis, mypy, ruff, pip-audit và pytest-randomly. Mỗi tool được pin vào một version cụ thể (pytest 9.1.1, ruff 0.16.0, tính đến tháng 10 năm 2026). Chạy script:

cd demo-rate-limiter
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt -e .
./tools/gauntlet.sh

Script in một banner cho mỗi layer, chẳng hạn === tests + coverage === và === mutation ===, rồi kết thúc bằng === gauntlet: all layers green ===. Script chạy dưới set -e, nên layer đầu tiên bị lỗi sẽ dừng script và banner cuối cùng sẽ không được in. Thấy banner cuối cùng là dấu hiệu kiểm tra: điều đó có nghĩa mọi layer bên trên đều thoát với mã 0. Đó là cách script được thể hiện tại acc5a89. Trên main hiện tại, mỗi layer chạy thông qua một helper run_layer, nên banner chứa id của layer, chẳng hạn === tests-coverage ===. Một bước kết thúc cũng sẽ làm lần chạy fail nếu bất kỳ layer bắt buộc nào chưa chạy, vì vậy layer bị bỏ qua không thể âm thầm pass nữa.

Có một chi tiết trong script này đáng để bạn áp dụng. Coverage layer được viết như sau:

pytest -q --cov=ratelimiter --cov-report=term-missing --cov-fail-under=100

Nếu không có --cov-fail-under, pytest --cov chỉ in phần trăm rồi thoát với mã 0, bất kể coverage đã giảm đến mức nào. Đó là một layer fail-open nằm trong một script mà dòng đầu tiên hứa sẽ dừng ở layer đầu tiên bị lỗi. Một layer trong gauntlet không thể fail thì chỉ là phần trang trí.

Mutation testing bổ sung gì so với coverage

Coverage trả lời một câu hỏi: bộ test có chạy qua dòng này hay không. Nó không trả lời câu hỏi bạn thực sự quan tâm: nếu dòng đó bị sai, có assertion nào bị fail hay không. Một test gọi một function nhưng không kiểm tra gì vẫn báo coverage đầy đủ cho mọi dòng mà nó chạm tới. Coverage phát hiện code chưa được test. Nó không phát hiện các test không kiểm tra gì.

Mutation testing trả lời trực tiếp câu hỏi thứ hai. Nó cố ý thay đổi code, mỗi lần một chỉnh sửa nhỏ, rồi chạy lại bộ test. Nếu bộ test fail, mutant bị kill. Điều đó có nghĩa là một assertion nào đó đang kiểm tra behaviour này. Nếu bộ test vẫn pass, mutant đã sống sót: dòng code đã được chạy nhưng không có gì kiểm tra kết quả.

Demo thực hiện việc này với tools/mutants.py. Nó chèn các chỉnh sửa đơn được đánh số vào src/ratelimiter/__init__.py, chạy pytest sau mỗi chỉnh sửa, rồi khôi phục file. Đây là những lỗi một người mệt mỏi thường mắc: >= trở thành >, hoặc một return value bị bỏ đi.

Quy tắc kill trong runner đó là phần mà hầu hết mutation script tự viết đều xử lý sai. Chỉ exit code 1 của pytest mới được tính là kill, vì 1 có nghĩa là các test đã chạy và ít nhất một test bị fail. Exit code 0 có nghĩa là mutant đã sống sót. Mọi giá trị khác, chẳng hạn lỗi collection hoặc không thu thập được test nào, có nghĩa là chưa có gì được xác minh và không được tính. Script coi mọi giá trị “nonzero” là kill sẽ tính cả các lần tự crash của nó là thành công, khiến con số đó chỉ tăng lên.

Trong cùng file còn có 2 chi tiết quan trọng khác. Mutant M11 bị loại khỏi danh sách vì nó là equivalent mutant: xóa một entry đã hết hạn thay vì xóa tất cả entry đã hết hạn tạo ra cùng behaviour có thể quan sát được khi clock tăng đơn điệu, nên không test nào có thể kill nó. Runner đặt PYTHONDONTWRITEBYTECODE=1, còn gauntlet xóa mọi __pycache__ trước, vì 2 mutant có cùng kích thước được ghi trong cùng một giây có thể dùng chung một .pyc đã cache, khiến mutant thứ hai kế thừa verdict của mutant thứ nhất.

Rủi ro đó là lý do gauntlet chạy một negative control trước mutation pass thực tế:

.venv/bin/python tools/mutants.py --negative-control
.venv/bin/python tools/mutants.py

Control chạy 2 mutant với modification time cố định: một mutant chắc chắn phải bị kill và một mutant equivalent hoàn toàn chắc chắn phải sống sót. Nếu cả 2 đều trả về kết quả killed, bytecode cache đã bị rò rỉ giữa các lần chạy và mọi kill count trong report đều bị thổi phồng. Đây là cách cụ thể hóa quy tắc của skill về checker. pytest và mypy đã được kiểm chứng qua behaviour khi fail trong nhiều năm. Một script bạn viết tuần trước thì chưa, vì vậy hãy chứng minh nó có thể fail trước khi tin vào kết quả pass của nó, đồng thời ghi lại bằng chứng đó trong EVIDENCE.

Biểu đồMutants killed by each suite run alone (demo-rate-limiter evidence.md, August 2026)
Dữ liệu đằng sau biểu đồ này
[
  {
    "label": "Scenario tests",
    "mutants_killed": 22,
    "mutants_run": 22
  },
  {
    "label": "Property tests",
    "mutants_killed": 3,
    "mutants_run": 22
  }
]

Report EVIDENCE của demo cho thấy vì sao một con số tổng hợp duy nhất vẫn che giấu nhiều thông tin. Bộ test scenario đã kill 22 trên tổng số 22 mutant. Các test property-based, khi chạy lại riêng trên cùng các mutant, đã kill 3. Một kill được gán cho test nào fail trước, vì vậy tổng số hoàn hảo xác nhận cả bộ test nhưng không cho biết gì về bất kỳ layer riêng lẻ nào bên trong. Các property đó vẫn xứng đáng có mặt, vì chúng bắt được những dạng input không ai liệt kê. Chúng không gánh phần chính của việc kiểm tra correctness, và bạn chỉ biết điều đó khi đo riêng từng layer.

Chạy toàn bộ kiểm tra trên server, không phải laptop

Báo cáo EVIDENCE là một khẳng định rằng một số lệnh đã tạo ra một số con số. Khẳng định đó chỉ có thể được kiểm tra nếu người khác tạo ra đúng các con số đó, và laptop là nơi tệ nhất để thử. Bản Python của bạn có patch release khác, còn PATH của bạn chứa các tool mà máy tiếp theo sẽ không có. Mutation testing còn làm vấn đề nghiêm trọng hơn vì nó chạy lại toàn bộ test suite một lần cho mỗi mutant. Vì vậy, chỉ riêng danh sách trong bản demo đã có nghĩa là chạy thêm 22 lần test suite.

Đặt nó trong một container trên VPS. Container cố định hệ điều hành và interpreter, requirements-dev.txt được pin cố định các tool, còn VPS cung cấp một máy không đồng thời chạy browser của bạn.

FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends \
      python3 python3-venv git ca-certificates \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /work
docker build -t gauntlet:24.04 .
docker run --rm -v "$PWD:/work" -w /work/demo-rate-limiter gauntlet:24.04 \
  sh -c 'python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt -e . && ./tools/gauntlet.sh'

Một lần chạy thành công kết thúc tại === gauntlet: all layers green ===. Có 2 bước cần network outbound: pip cài các tool đã được pin, và layer pip-audit kiểm tra dependency của bạn với một dịch vụ lỗ hổng. Một lần chạy offline không âm thầm bỏ qua layer đó. Nó sẽ fail, đúng với hành vi bạn muốn ở một gate.

Để chạy sau mỗi push, cùng script đó trở thành một bước trong một CI job. Repository tự chạy toàn bộ kiểm tra trên GitHub Actions tại ubuntu-latest bằng Python 3.12, được kích hoạt bởi các push đến main và bởi pull request. Trỏ runs-on đến một self-hosted runner để job chạy trên VPS của bạn thay thế, và khi làm vậy hãy bỏ trigger pull_request:

name: gauntlet
on:
  push:
    branches: [main]
jobs:
  gauntlet:
    runs-on: self-hosted
    defaults:
      run:
        working-directory: demo-rate-limiter
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: python -m venv .venv && .venv/bin/pip install -r requirements-dev.txt -e .
      - run: ./tools/gauntlet.sh

Chỉ giữ trigger cho các push đến những branch do bạn kiểm soát. Self-hosted runner đồng thời build pull request từ fork sẽ chạy code của người lạ trên server của bạn bằng credential của runner. Lý do tương tự cũng áp dụng cho chính agent: cấp cho agent một VM dùng một lần mà bạn có thể hủy bỏ thay vì workstation của bạn. Nếu muốn kết quả kiểm tra được gửi đến nơi mọi người thảo luận về thay đổi, hãy kết nối nó với một self-hosted PR review agent.

Khi cách tiếp cận này thất bại

Thất bại đầu tiên mang tính cấu trúc, và không công cụ nào có thể loại bỏ nó. Gauntlet biến các ràng buộc trong SPEC thành bằng chứng có thể thực thi. Nó không thể cho biết SPEC có đúng hay không. Nếu phê duyệt một spec mã hóa sai yêu cầu, bạn sẽ nhận được báo cáo EVIDENCE hoàn hảo cho sai chương trình: độ bao phủ đầy đủ, mọi mutant đều bị loại, mọi layer đều xanh, nhưng phần mềm lại thực hiện điều bạn không muốn. Mỗi giờ bạn tiết kiệm được do không đọc diff nên dành để đọc spec.

Vấn đề thứ hai là các checker. Repository tự thừa nhận điều này trong file bằng chứng của chính nó. Protocol xác minh độc lập đã chạy 6 vòng, vòng 6 trả về failed, và các bản sửa sau vòng đó chưa bao giờ được xác minh lại. Vì vậy, trạng thái được release chưa được xác minh từ đầu đến cuối. Một layer lint cho shell được ghi nhận là không khả dụng, thay vì được tính là pass. Các vòng trước đã phát hiện những lỗi hành vi thực sự và một mutation runner không đáng tin cậy, dù các trạng thái trước đó đã báo xanh. Một gauntlet xanh không tự xác thực.

Vấn đề thứ ba là phạm vi. Danh sách mutant viết thủ công chỉ bao phủ các lỗi mà ai đó đã nghĩ đến và cố tình cài vào. Mỗi equivalent mutant bị loại khỏi danh sách cũng là một phán đoán mà bạn đang tin tưởng. Các mutation tool có sẵn trên thị trường (mutmut, cosmic-ray, Stryker, PIT) tạo mutant một cách có hệ thống và là lựa chọn mặc định tốt hơn nếu ngôn ngữ của bạn có tool phù hợp.

Điều chỉnh mức nỗ lực theo rủi ro

Skill này định nghĩa 3 cấp độ và yêu cầu agent nêu rõ đã chọn cấp nào.

  • Cấp 1, đơn giản: lỗi gõ, comment hoặc giá trị cấu hình. Chạy đầy đủ test suite và lint, không viết test mới, đồng thời nêu trong một câu lý do không cần thêm test.
  • Cấp 2, thông thường: sửa bug hoặc thêm một feature nhỏ. Thực hiện đầy đủ quy trình. Với bản sửa bug, phải bắt đầu bằng một test đang fail để tái hiện bug, để bug của hôm qua trở thành regression test của ngày mai.
  • Cấp 3, rủi ro cao: tiền, authentication, mất dữ liệu, concurrency hoặc public API. Bắt đầu bằng failure model liệt kê các cách mà thay đổi này có thể gây hại. Thêm một lớp kiểm tra gauntlet cho từng chế độ lỗi. Sau đó chạy đầy đủ quy trình cùng property test, mutation testing và một lượt kiểm tra riêng để tấn công implementation bằng các input hostile.

Cấp 3 còn có một bước thử nghiệm. Một agent thứ hai với context mới chỉ được xem task contract, SPEC đã được phê duyệt và trạng thái source hiện tại. Agent này cố phá vỡ phần đã hoàn thiện trước khi EVIDENCE được ký. Agent đó không sửa gì, chỉ báo cáo, còn một người sẽ đánh giá những gì nó phát hiện. Cách này giảm correlation do dùng chung task context. Nó không giảm correlation do dùng chung model.

Nếu bạn muốn tự xây dựng một thứ theo cấu trúc này thay vì dùng skill này, viết skill agent của riêng bạn trình bày layout của file và trường description quyết định thời điểm agent load skill.

FAQ

Mutation testing phát hiện được gì mà code coverage bỏ sót?

Coverage ghi nhận một dòng đã được chạy. Nó không thể ghi nhận liệu assertion có thất bại nếu dòng đó bị sai hay không. Vì vậy, một test chỉ gọi function mà không kiểm tra gì vẫn báo coverage đầy đủ. Mutation testing cố ý thay đổi code, mỗi lần một thay đổi, rồi chạy lại test suite. Mutant còn tồn tại nghĩa là dòng đó đã được thực thi nhưng không có gì xác minh kết quả. Vì thế, skill này liệt kê quy tắc tuyệt đối là "không bao giờ chạy theo các con số coverage" và xác định mutation là lớp kiểm tra phát hiện việc đối phó với chỉ số.

Tôi vẫn cần đọc code mà agent viết sao?

Theo workflow này, bạn đọc SPEC trước khi code, đọc report EVIDENCE sau đó, rồi kiểm tra nhanh report bằng cách chạy lại các command được dẫn trong đó. Diff trở thành tùy chọn. Điểm cần lưu ý là toàn bộ phán đoán của bạn giờ tập trung vào một tài liệu, vì một spec chứa sai requirement sẽ tạo ra một gauntlet màu xanh cho chương trình mà bạn không hề muốn. Hãy dùng thời gian tiết kiệm được để kiểm tra spec.

Tôi có thể chạy gauntlet Old Coder trong CI trên server của mình không?

Có, và đó là nơi phù hợp hơn để chạy nó. Cài toolchain đã pin từ requirements-dev.txt bên trong một container, rồi chạy script gauntlet của project như một job step. Trên GitHub Actions, thiết lập runs-on: self-hosted và đăng ký một runner trên VPS của bạn. Chỉ bật trigger cho các lần push lên những branch bạn kiểm soát, vì self-hosted runner xây dựng pull request từ fork sẽ thực thi code không đáng tin cậy bằng credential của runner.

Tôi nên cài version nào của skill?

Hãy pin một version cụ thể. Repository vẫn đang được phát triển tích cực, và các file tham chiếu của nó đã được tách và di chuyển. Vì vậy, một report được tạo vào tháng trước có thể đã được chấm theo các rule khác. Clone repository, checkout một commit cụ thể, copy skills/old-coder vào ~/.claude/skills/, rồi ghi commit hash đó cạnh SPEC của bạn. Khi đó, thay đổi trong evidence của bạn sẽ phản ánh một thay đổi trong code.