ลงมือทำ + Feedback loops (Implement, TDD, Debug)
บทที่แล้ว (MP-2) เราคุยกับ agent จนได้ spec ที่ชัด และแตกเป็น tickets ที่ทำได้จริง บทนี้คือขั้นที่สาม — เอา spec/tickets นั้นมาลงมือทำจริง แต่ประเด็นที่ mattpocock/skills เน้นไม่ใช่แค่ "เขียนโค้ดให้จบ" — สิ่งที่เน้นคือ feedback loop ที่ต้องมีอยู่ทุกขั้นตอน ตั้งแต่เขียนโค้ด ไปจนถึงตอนเจอบั๊ก ถ้าไม่มี feedback loop ที่ดี agent (และตัวคุณเอง) จะกลายเป็น "เขียนแล้วหวังว่าคงถูก" ซึ่งเป็นสาเหตุอันดับต้นๆ ที่งานพังตอน production
1. implement — จาก spec/tickets สู่โค้ดจริง
skills/engineering/implement/SKILL.md สั้นมากแค่ไม่กี่บรรทัด แต่คุมทั้ง workflow ของการลงมือทำงาน
สังเกตว่าเป็น skill แบบ disable-model-invocation: true — ต้องถูกเรียกโดยผู้ใช้เอง (เช่นพิมพ์ /implement)
ไม่ auto-trigger เอง เพราะการ "เริ่มเขียนโค้ดจริง" ควรเป็นการตัดสินใจที่คนสั่งชัดเจน ไม่ใช่ agent เดาเอาว่าถึงเวลาแล้ว
// skills/engineering/implement/SKILL.md (เนื้อหาทั้งไฟล์)
Implement the work described by the user in the spec or tickets.
Use /tdd where possible, at pre-agreed seams.
Run typechecking regularly, single test files regularly,
and the full test suite once at the end.
Once done, use /code-review to review the work.
Commit your work to the current branch.
แตกเป็นขั้นตอนที่ agent ต้องทำตามลำดับ:
- รับ input คือ spec หรือ tickets ที่ตกลงกันไว้แล้ว (ผลจาก MP-2) — ไม่ใช่เดาเอาเองว่าต้องทำอะไร
- ขับด้วย
/tddที่ seam ที่ตกลงไว้ล่วงหน้าเท่านั้น — ไม่ใช่ test ทุกจุดที่นึกออก - รัน typecheck บ่อยๆ ระหว่างเขียน (ไม่ใช่รอจบแล้วค่อยรันทีเดียว)
- รัน single test file บ่อยๆ ระหว่างเขียนแต่ละ slice + full test suite ครั้งเดียวตอนจบ ก่อนเคลมว่าเสร็จ
- ปิดด้วย
/code-reviewก่อน commit เสมอ — ไม่ commit งานที่ยังไม่ผ่านสายตาตรวจสอบ - commit ลง branch ปัจจุบัน
สังเกตลำดับ: typecheck + single test ระหว่างทาง (บ่อยๆ, เร็ว) → full suite ครั้งเดียวตอนจบ (ช้ากว่าแต่ครอบคลุม) → code-review → commit นี่คือ pattern ที่ป้องกัน "เขียนไปเรื่อยๆ 2 ชั่วโมงแล้วค่อยรัน test ทีเดียวเจอพังระเบิด"
2. Feedback loop คือหัวใจของทุกขั้นตอน
ทำไม implement ถึงย้ำเรื่องรัน typecheck/test "บ่อยๆ" ขนาดนี้? เพราะสิ่งที่ทำให้ agent (หรือ dev คนหนึ่ง)
ไม่หลงทางระหว่างเขียนโค้ด คือการมี สัญญาณที่บอกถูก/ผิดได้ทันที สามแบบหลักที่ผูกกับงานจริงของคุณ:
| Feedback loop | สัญญาณจากอะไร | ตัวอย่างในงานคุณ |
|---|---|---|
| Static types | implement: "Run typechecking regularly" |
tsc --noEmit ใน NestJS, go build/go vet ใน Go |
| Automated tests | implement: "single test files regularly + full suite once" |
jest retry.test.ts ทีละไฟล์ระหว่างเขียน แล้ว npm test เต็มตอนจบ |
| CLI / browser access | diagnosing-bugs: curl script, CLI invocation, headless browser script |
curl localhost:3000/api/users, Playwright script เช็ค DOM จริง |
3. tdd — red/green loop ที่ทำให้ implement เชื่อถือได้
เมื่อ implement บอกให้ "Use /tdd where possible, at pre-agreed seams" นี่คือจุดที่ไปเปิด
skills/engineering/tdd/SKILL.md ข้อสำคัญที่ต่างจาก TDD แบบทั่วไปที่คุณอาจเคยได้ยิน (red-green-refactor)
คือ mattpocock/skills แยก refactor ออกจาก loop ชัดเจน:
"Refactoring is not part of the loop. It belongs to the review stage (see the code-review skill),
not the red → green implementation cycle."
พูดง่ายๆ คือ ตอนอยู่ใน loop ของ /tdd มีแค่ 2 จังหวะ: RED (เขียน test ให้ fail ก่อน)
→ GREEN (เขียนโค้ดขั้นต่ำให้ผ่าน) แล้ว "refactor" ไปเกิดตอน /code-review ตอนท้ายของ implement
ไม่ใช่แทรกอยู่ระหว่าง cycle เหมือนสูตรคลาสสิก
Seam — ต้องตกลงก่อนเขียน test บรรทัดแรก
Seam คือขอบเขต public ที่ test จะยืนอยู่ — ไฟล์กำชับว่า "Test only at pre-agreed seams" ก่อนเขียน test ต้องเขียนรายการ seam ที่จะ test แล้ว "confirm กับผู้ใช้" ก่อนเสมอ ห้ามเขียน test ที่ seam ที่ยังไม่ได้ตกลง — เหตุผลคือ test ทุกจุดทำไม่ได้ (และไม่คุ้ม) การตกลง seam ล่วงหน้าคือวิธีให้ effort ของการ test ไปตกที่ critical path จริงๆ
ลักษณะของ test ที่ดี vs ไม่ดี
จาก tdd/tests.md: test ที่ดีต้อง ทดสอบผ่าน public interface ไม่ใช่ไปเจาะ internal —
"โค้ดข้างในเปลี่ยนได้หมด แต่ test ไม่ควรต้องแก้ตาม"
// GOOD — ทดสอบ observable behavior ผ่าน public API
test("user can checkout with valid cart", async () => {
const cart = createCart();
cart.add(product);
const result = await checkout(cart, paymentMethod);
expect(result.status).toBe("confirmed");
});
// BAD — ผูกกับ implementation detail (mock internal collaborator)
test("checkout calls paymentService.process", async () => {
const mockPayment = jest.mock(paymentService);
await checkout(cart, payment);
expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
});
// ↑ พอ refactor ภายในโดย behavior ไม่เปลี่ยน test นี้พังทันที
อีก anti-pattern ที่พลาดกันบ่อยคือ tautological test — expected value คำนวณด้วยวิธีเดียวกับโค้ดจริง ทำให้ test ผ่านเสมอโดยไม่ได้พิสูจน์อะไร:
// BAD — expected คำนวณด้วยสูตรเดียวกับโค้ด จึงผ่านเสมอ
const expected = items.reduce((sum, i) => sum + i.price, 0);
expect(calculateTotal(items)).toBe(expected);
// GOOD — expected เป็นตัวเลขจริงที่รู้อยู่แล้ว (independent source of truth)
expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15);
และ anti-pattern ที่สามคือ horizontal slicing — เขียน test ทั้งหมดก่อน แล้วค่อยเขียน implementation ทั้งหมด ไฟล์เตือนว่านี่ทำให้ test กลายเป็นการทดสอบ "รูปร่างที่จินตนาการไว้" ไม่ใช่ behavior จริง แนะนำให้ทำแบบ vertical slice แทน — หนึ่ง test → หนึ่ง implementation → วนซ้ำ แต่ละ test เป็นเหมือน tracer bullet ที่ตอบสนองกับสิ่งที่เพิ่งเรียนรู้จาก cycle ก่อน
กฎของ loop จาก SKILL.md โดยตรง: "Red before green" — เขียน test ที่ fail ก่อนเสมอ แล้วเขียนโค้ดขั้นต่ำให้ผ่านเท่านั้น ห้ามคาดเดา test ในอนาคตหรือใส่ feature ที่ยังไม่มีใครขอ (YAGNI) และ "One seam, one test, one minimal implementation per cycle" — ทีละก้าวเท่านั้น
4. diagnosing-bugs — เมื่อ implement ไปเจอบั๊ก
ระหว่าง implement ถ้าเจอบั๊กที่ไม่ใช่แค่ "เขียน test ยังไม่ผ่าน" ธรรมดา (เช่น bug เก่าที่ซ่อนอยู่ หรือ regression)
จะไปใช้ skills/engineering/diagnosing-bugs/SKILL.md ซึ่งมี 6 phase แต่ phase แรกคือ
หัวใจของ skill ทั้งตัว:
"This is the skill. Everything else is mechanical. If you have a tight pass/fail signal for the bug ... you will find the cause ... If you don't have one, no amount of staring at code will save you."
| Phase | ทำอะไร | เสร็จเมื่อ |
|---|---|---|
| 1. Build feedback loop | สร้างคำสั่งเดียวที่ pass/fail ได้ทันทีบนบั๊กนี้ — failing test, curl script, CLI invocation, headless browser script, replay trace, ฯลฯ | Red-capable + deterministic + fast (วินาที ไม่ใช่นาที) + agent-runnable |
| 2. Reproduce + minimise | รัน loop จนเห็น fail ตรงกับอาการที่ผู้ใช้บอกจริง แล้วตัด input/step ทีละอย่างจนเหลือแค่สิ่งที่ load-bearing | ตัดอะไรออกอีกไม่ได้แล้ว (ตัดแล้ว loop เขียวทันที) |
| 3. Hypothesise | ตั้ง 3–5 สมมติฐาน เรียงลำดับ แต่ละข้อต้อง falsifiable ("ถ้า X คือสาเหตุ เปลี่ยน Y แล้วบั๊กหาย") | โชว์ ranked list ให้ผู้ใช้ดูก่อนเริ่มทดสอบ |
| 4. Instrument | debugger/REPL ก่อน (breakpoint ดีกว่า log สิบบรรทัด) รองลงมาคือ targeted log ที่ tag ด้วย prefix เฉพาะ เช่น [DEBUG-a4f2] |
แยกได้ว่าสมมติฐานไหนถูก เปลี่ยนทีละตัวแปรเท่านั้น |
| 5. Fix + regression test | เขียน regression test จาก repro ที่ minimise แล้ว "ก่อน" แก้จริง — แต่ต้องมี seam ที่ถูกต้องเท่านั้น | test แดงก่อนแก้ → แก้ → test เขียว → รี loop เดิม (ที่ยังไม่ minimise) อีกครั้ง |
| 6. Cleanup + post-mortem | ลบ log ที่ tag ไว้ทั้งหมด (grep prefix เจอหมด), ลบ throwaway harness, สรุปสมมติฐานที่ถูกไว้ใน commit message | repro เดิม (ตัวเต็ม ไม่ใช่ minimised) ไม่ reproduce ซ้ำอีกแล้ว |
จุดที่คนมักข้าม: SKILL.md เตือนตรงๆ ว่า "If you catch yourself reading code to build a theory before this command exists, stop — jumping straight to a hypothesis is the exact failure this skill prevents." พูดง่ายๆ คือ ห้ามเดา root cause ก่อนมี loop ที่รันซ้ำได้จริง ต่อให้มั่นใจแค่ไหนก็ตาม
ผูกกับงานจริง: PostgreSQL constraint fail แบบงงๆ หรือ MongoDB query คืนค่าผิด — วิธีที่ SKILL.md แนะนำลำดับแรกคือ เขียน failing test (unit/integration) ที่ยิง query เดิม ถ้าทำไม่ได้ให้ลองต่อ curl/CLI script ยิง endpoint จริง ก่อนจะไล่ log ทีละบรรทัด — "log ทุกอย่างแล้ว grep" คือสิ่งที่ SKILL.md บอกชัดว่า "Never"
ผูกงานจริง: vertical slice ใน NestJS ด้วย Jest
สมมติ ticket จาก MP-2 บอกว่า "เพิ่ม endpoint คืนสถานะ order ว่า SHIPPED เมื่อมี tracking number แล้ว"
ก่อนเขียน test ต้องยืนยัน seamก่อน (ตามกฎ tdd) — ในที่นี้ตกลงกันว่า seam คือ service method
getOrderStatus() ไม่ใช่ล้วงเข้าไปเช็ค repository โดยตรง
// RED — order.service.spec.ts (เขียนก่อน ยังไม่มี logic นี้)
test("returns SHIPPED when order has a tracking number", async () => {
const order = buildOrder({ trackingNumber: "TH123" });
const status = await service.getOrderStatus(order.id);
expect(status).toBe("SHIPPED");
});
// $ npx jest order.service.spec.ts
// FAIL — getOrderStatus ยังคืน "PENDING" เสมอ ← fail ถูกเหตุผล
// GREEN — order.service.ts (เขียนขั้นต่ำแค่พอผ่าน)
async getOrderStatus(id: string): Promise<OrderStatus> {
const order = await this.repo.findById(id);
if (order.trackingNumber) return "SHIPPED";
return "PENDING";
}
// $ npx jest order.service.spec.ts
// PASS (1/1) → ต่อด้วย slice ถัดไป เช่น "returns CANCELLED when ..."
สังเกตว่านี่คือทีละ slice — ไม่ใช่เขียน test 10 เคสรวดเดียวก่อนเขียน implementation
(นั่นคือ horizontal slicing ที่ SKILL.md เตือนไว้) พอ slice นี้เขียวแล้วค่อยไป slice ถัดไป
และตลอดทางให้รัน tsc --noEmit เป็นระยะ (feedback loop ที่ 1) ก่อนจะรัน npm test เต็มตอนจบ
แล้วเรียก /code-review ก่อน commit — ปิด loop ตาม implement ครบ
ฝั่ง Go หลักการเดียวกัน: เขียน _test.go ก่อน รัน go test ./... ให้เห็น FAIL
ที่มีเหตุผลชัด (เช่น ค่า status ผิด ไม่ใช่ compile error) แล้วค่อยเขียนฟังก์ชันจริง รันซ้ำจนเขียว
แล้ว go vet/go build เป็น feedback loop ฝั่ง static types แทน tsc
ไม่มี feedback loop = agent บินตาบอด — ทั้ง implement, tdd, และ
diagnosing-bugs ผูกกันด้วยหลักการเดียว: ทุกก้าวต้องมีอะไรสักอย่างบอกได้ทันทีว่า "ถูกหรือผิด"
ถ้า agent เขียนโค้ดยาวๆ โดยไม่รัน typecheck/test คั่นเป็นระยะ หรือพยายามแก้บั๊กโดยยังไม่มี loop ที่ reproduce ได้จริง
— นั่นคือสัญญาณให้หยุดแล้วถามว่า "รันคำสั่งอะไรพิสูจน์ได้บ้างว่าที่ทำอยู่ถูก"
- หยิบ ticket เล็กๆ จากบท MP-2 (หรือของโปรเจกต์คุณเอง) แล้วก่อนเขียน test บรรทัดแรก ให้เขียนลงมาก่อนว่า "seam ที่จะ test คืออะไร" (เช่น service method ไหน, endpoint ไหน) แล้วลองถามตัวเอง/เพื่อนร่วมทีมว่าเห็นด้วยไหม
- ทำ vertical slice จริง 1 รอบ: เขียน test ให้ fail ก่อน (แคป output FAIL มาด้วย) แล้วเขียนโค้ดขั้นต่ำให้ผ่าน (แคป output PASS มาด้วย) — ห้ามเขียน test เคสที่สองก่อนที่เคสแรกจะเขียว
- นึกถึงบั๊กล่าสุดที่คุณเจอใน NestJS/Go/MongoDB/PostgreSQL แล้วลองเขียนตาม Phase 1 ของ diagnosing-bugs ว่า "คำสั่งเดียว" อะไรที่ทำให้เห็นบั๊กนั้น red ได้ทันที ถ้านึกไม่ออก ลองไล่ตามลิสต์ 10 วิธีในไฟล์ (failing test → curl → CLI → headless browser → ...) ว่าอันไหนใช้ได้กับเคสของคุณ