The "Where & Why": กลไก Stderr Triage และการปิดลูป Agent Self-Correction
scripts/ และ Runner CLI เพื่อสั่งรันการทดสอบจริงนอกตัวโมเดลปัญหาคอขวดที่พบ: เมื่อการทดสอบหรือการคอมไพล์ล้มเหลว Build Tools เช่น Maven หรือ Kotlinc จะพ่น Output ขนาดมหึมากว่า 50-100 KB การส่ง Raw Output ทั้งหมดกลับเข้าสู่ Prompt จะเกิด Context Saturation และกลืนกิน In-Context Attention ทำให้ Host Agent สับสนและเดาสุ่มแก้โค้ดผิดจุด
เป้าหมายของ Day 6: เราจะสร้าง Closed-Loop Stderr Triage Pipeline ที่แยกแยะ Streams, กำหนด Exit Code Matrix (0, 1, 2, 3, 4, 124), และกรองเฉพาะ Actionable Diagnostic Payload ส่งกลับทาง Stderr ในงบไม่เกิน 350 Tokens เพื่อให้ Agent ทำการ Self-Correction ได้อย่างแม่นยำ
┌────────────────────────────────────────────────────────────────────────┐
│ HOST AGENT CODE GENERATION & REPAIR LOOP │
│ Target: src/test/kotlin/.../OrderSettlementServiceTest.kt │
└───────────────────────────────────┬────────────────────────────────────┘
│
▼ (Tool Call: Non-interactive Shell)
┌────────────────────────────────────────────────────────────────────────┐
│ EXECUTION SANDBOX: scripts/run-unit-tests.sh <TestFile.kt> │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ STAGE 1: FAST-FAIL AST/REGEX GATE (Execution time < 50ms) │ │
│ │ Scans for forbidden Mockito / Spring Runners │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ├─► [Violation Found] ──► Exit Code 2 (STATIC_FAIL) │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ STAGE 2: SANDBOXED MAVEN EXECUTION (Timeout Guard: 45s) │ │
│ │ Executes targeted test execution inside local container/runner │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ├─► [Exceeds 45s] ──► Exit Code 124 (TIMEOUT) │
│ ├─► [Compilation Err]──► Exit Code 3 (COMPILATION) │
│ └─► [Assertion Fail] ──► Exit Code 4 (ASSERTION) │
└───────────────────────────────────┬────────────────────────────────────┘
│
▼ (Log Pipe to Stderr Filter)
┌────────────────────────────────────────────────────────────────────────┐
│ 🎯 SCRIPTS/TRIAGE-TEST-FAILURE.PY [TODAY'S ARTIFACT] │
│ - Strips 50KB Maven scanning noise & JVM lifecycle traces │
│ - Extracts File, Line, and Root Cause Exception │
│ - Writes compact Diagnostic Card (< 350 tokens) strictly to STDERR │
└───────────────────────────────────┬────────────────────────────────────┘
│
▼ (Clean Diagnostic Feedback)
┌────────────────────────────────────────────────────────────────────────┐
│ HOST AGENT CONTEXT INGESTION & SELF-CORRECTION │
│ Reads Exit Code + Clean Stderr ──► Repairs Target Code Accurately │
└────────────────────────────────────────────────────────────────────────┘
Strict Stdout vs Stderr
Stdout สงวนไว้สำหรับ Clean JSON Artifact เมื่อสำเร็จ ส่วน Stderr ใช้สำหรับส่ง Diagnostic Payload เพื่อให้ Agent ตัดสินใจได้ทันที
< 350 Token Budget
ตัดทิ้ง Maven dependency download logs และ stack traces ส่วนเกิน เพื่อรักษา Token Space และ Attention Focus ของโมเดล
Deterministic Branching
การใช้ Exit Codes จำแนกสาเหตุ (0, 1, 2, 3, 4, 124) ช่วยให้ระบบสั่งการของ Host Agent แตกกิ่งการแก้ไขปัญหาได้อย่างแม่นยำ
วิศวกรรม Sandboxed Streams: จาก Raw Terminal Output สู่ Agentic Interface
ในโลกของการพัฒนาซอฟต์แวร์แบบดั้งเดิม Terminal Output ถูกออกแบบมาเพื่อให้ มนุษย์ (Human Eyes) กวาดสายตาอ่าน แต่ในสถาปัตยกรรม Agent Skills เครื่องมือใน scripts/ กำลังสื่อสารกับ Large Language Models ซึ่งมีข้อจำกัดเรื่อง Context Window และความอ่อนไหวต่อข้อมูลรบกวน (Noise Sensitivity) เราจึงต้องสร้าง Data Contract ระหว่าง Process Output กับ In-Context Reasoning
| Exit Code | Semantic Category | Underlying Root Cause | Host Agent Expected Action |
|---|---|---|---|
| 0 | SUCCESS | เทสต์คอมไพล์ผ่านและรันผ่าน 100% ตามข้อกำหนด | ส่งมอบผลลัพธ์ให้ผู้ใช้ทันที ห้ามรันสคริปต์ซ้ำ |
| 1 | ENVIRONMENT_ERROR | ไม่มี mvn ใน PATH, Java runtime ผิดเวอร์ชัน หรือไฟล์หาย |
Abort ทันที และแจ้งปัญหาต่อผู้ใช้ (ห้าม Agent พยายามแก้โค้ดเอง) |
| 2 | STATIC_INVARIANT_FAIL | พบการละเมิด Negative Boundaries (นำเข้า Mockito หรือ Spring Context) | ลบ Import และ Annotation ต้องห้ามออกทันที แล้วรันตรวจซ้ำ |
| 3 | COMPILATION_FAILURE | Kotlinc error: Type mismatch, Unresolved reference, Syntax พัง | ตรวจพิกัดไฟล์และบรรทัดใน Stderr แก้ไข Type/Signature ทันที |
| 4 | ASSERTION_FAILURE | MockK verification หรือ AssertJ assertion ไม่ตรงกับ State จริง | ปรับแต่ง Fixtures, Stubbing Return Value หรือ Assert Statement |
| 124 | EXECUTION_TIMEOUT | เทสต์ทำงานเกิน 45 วินาที (เกิด Deadlock หรือแอบบูต Spring Context) | ตัดวงจร ลบ Blocking Operations หรือ Context Initializer ทิ้งทันที |
เมื่อรัน mvn test ข้อมูลที่ส่งออกมักมีความยาวกว่า 500-1,000 บรรทัด ประกอบด้วย Maven scanning plugins, JVM boot telemetry, และ classloader warnings หากส่งข้อความเหล่านี้ให้ Agent โมเดลจะสูญเสียความสามารถในการ Focus
สคริปต์ triage-test-failure.py ทำหน้าที่เป็นวงจรกรองความถี่ต่ำ สกัดเฉพาะ Error Header, File, Line, และ Root Cause Message ส่งออกทาง Stderr เท่านั้น
การสร้างระบบ Autonomous Self-Correction มีความเสี่ยงต่อการเกิด Infinite Repair Loops และอาการ Process ค้าง:
- 1. POSIX timeout 45s: บังคับตัดวงจรทันทีหากเทสต์ทำงานช้าเกินกว่าเกณฑ์ In-Memory Isolation (< 50ms) ป้องกันกรณี Agent แอบบูต Testcontainers หรือ Spring Data
- 2. Max 3 Retries Policy: กำหนดใน
SKILL.mdให้ Agent พยายามซ่อมแซมได้ไม่เกิน 3 รอบ หากยังไม่ผ่าน ให้ส่งข้อความแจ้ง Error ต่อ Developer ทันทีเพื่อป้องกัน Token Burnout
Production Artifact: Stderr Triage Engine & Sandbox Runner
ชุดเครื่องมือที่สมบูรณ์ตามมาตรฐาน agentskills/agentskills ประกอบด้วยข้อกำหนดใน SKILL.md, ตัวกรอง Stderr ภาษา Python, และ Orchestrator Shell Script
.github/skills/spring-boot-unit-tester/
├── SKILL.md ◄── [Host Specification & Remediation Policy]
└── scripts/
├── run-unit-tests.sh ◄── [TODAY'S ARTIFACT: Sandbox Orchestrator with Timeout]
└── triage-test-failure.py ◄── [TODAY'S ARTIFACT: Stderr Noise Filter & Parser]
---
name: spring-boot-unit-tester
schema-version: "1.0.0"
description: >
Generates and refactors deterministic, isolated Kotlin unit tests for Spring Boot 3.x
using MockK and AssertJ. MUST NOT load Spring ApplicationContext.
tools:
- name: run-unit-tests
description: Runs static linting and sandboxed Maven test execution with automatic failure triage.
file: scripts/run-unit-tests.sh
metadata:
domain: "backend-quality"
target-framework: "Spring Boot 3.x / Kotlin 1.9+"
---
# Spring Boot Unit Test Architect Specification
## 1. Tool Execution & Closed-Loop Remediation Contract
Whenever generating or refactoring a test file, the Host Agent MUST trigger verification:
`bash .github/skills/spring-boot-unit-tester/scripts/run-unit-tests.sh <TARGET_TEST_FILE>`
### 1.1 Exit Code Evaluation Matrix
- **Exit Code 0 (SUCCESS)**: Verification passed. Return finalized test code to user immediately.
- **Exit Code 1 (ENVIRONMENT_ERROR)**: Missing dependencies or invalid target. Abort and alert developer.
- **Exit Code 2 (STATIC_INVARIANT_FAIL)**: Negative boundary violated. Read stderr, purge Mockito or Spring runner.
- **Exit Code 3 (COMPILATION_FAILURE)**: Compiler error. Parse location and type error from stderr, fix syntax immediately.
- **Exit Code 4 (ASSERTION_FAILURE)**: Test assertion failed. Adjust MockK stubs or assertion criteria to align with domain logic.
- **Exit Code 124 (EXECUTION_TIMEOUT)**: Execution exceeded 45s. Purge blocking operations or heavy context loaders.
### 1.2 Maximum Retries Limit
The Host Agent MUST NOT invoke this tool more than 3 consecutive times per turn. If unresolved after 3 attempts, halt and output the exact diagnostic trace to the user.
#!/usr/bin/env python3
"""
scripts/triage-test-failure.py
Extracts compiler and assertion failures from noisy build logs.
Emits a compact diagnostic markdown card strictly to stderr (< 350 tokens).
"""
import sys
import re
# // [TODAY'S LESSON PLUGS IN HERE: REGEX STREAM PARSING]
def parse_build_diagnostics(raw_log: str) -> dict:
diagnostics = {
"category": "BUILD_FAILURE",
"file": "Unknown",
"line": "0",
"message": "Unclassified build or test failure.",
"root_cause_snippet": []
}
lines = raw_log.splitlines()
# Pattern A: Kotlinc Compilation Failure (e.g. [ERROR] /path/File.kt:22:38: error: type mismatch)
kt_pattern = re.compile(r"\[ERROR\]\s+(/.+?\.kt):(\d+):(\d+):\s+error:\s+(.+)")
# Pattern B: MockK Unmocked / Contract Failure
mockk_pattern = re.compile(r"io\.mockk\.MockKException:\s+(.+)")
# Pattern C: AssertJ / JUnit Assertion Error
assert_pattern = re.compile(r"org\.opentest4j\.AssertionFailedError:\s+(.+)")
for line in lines:
match_kt = kt_pattern.search(line)
if match_kt:
diagnostics["category"] = "COMPILATION_FAILURE"
diagnostics["file"] = match_kt.group(1).split("/")[-1]
diagnostics["line"] = match_kt.group(2)
diagnostics["message"] = match_kt.group(4)
diagnostics["root_cause_snippet"].append(line.strip())
return diagnostics
match_mockk = mockk_pattern.search(line)
if match_mockk:
diagnostics["category"] = "MOCKK_CONTRACT_VIOLATION"
diagnostics["message"] = match_mockk.group(1)
diagnostics["root_cause_snippet"].append(line.strip())
return diagnostics
match_assert = assert_pattern.search(line)
if match_assert:
diagnostics["category"] = "ASSERTION_FAILURE"
diagnostics["message"] = match_assert.group(1)
diagnostics["root_cause_snippet"].append(line.strip())
return diagnostics
# Fallback: Capture last 3 compiler error lines
error_tail = [l.strip() for l in lines if "[ERROR]" in l][-3:]
if error_tail:
diagnostics["root_cause_snippet"] = error_tail
return diagnostics
def format_agent_card(diag: dict) -> str:
"""Formats output into an ultra-dense markdown block tailored for LLM ingestion."""
return f"""### 🚨 DETERMINISTIC TRIAGE REPORT
- **Category**: `{diag['category']}`
- **Location**: `{diag['file']}:{diag['line']}`
- **Error**: {diag['message']}
- **Action Required**: Resolve the violation indicated above and re-execute runner.
```text
{chr(10).join(diag['root_cause_snippet'][:3])}
```"""
def main():
raw_input = ""
if len(sys.argv) > 1:
with open(sys.argv[1], "r", encoding="utf-8", errors="ignore") as f:
raw_input = f.read()
else:
raw_input = sys.stdin.read()
diag = parse_build_diagnostics(raw_input)
# CONTRACT: Always write triage report strictly to stderr
sys.stderr.write(format_agent_card(diag) + "\n")
if __name__ == "__main__":
main()
#!/usr/bin/env bash
# ==============================================================================
# scripts/run-unit-tests.sh
# Standard: agentskills Standard v1.0 (Phase 2 Deterministic Tooling)
# Implements Closed-loop Stderr Triage Pipeline, Timeout Guard, and Exit Codes
# ==============================================================================
set -euo pipefail
TARGET_FILE="${1:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TRIAGE_SCRIPT="${SCRIPT_DIR}/triage-test-failure.py"
LOG_FILE=$(mktemp /tmp/mvn_test_run.XXXXXX.log)
# Safe Cleanup on exit
trap 'rm -f "${LOG_FILE}"' EXIT
# 1. Environment & Argument Verification
if [[ -z "${TARGET_FILE}" ]] || [[ ! -f "${TARGET_FILE}" ]]; then
echo "### 🚨 DETERMINISTIC TRIAGE REPORT" >&2
echo "- **Category**: \`ENVIRONMENT_ERROR\`" >&2
echo "- **Error**: Target test file '${TARGET_FILE}' does not exist or was not specified." >&2
exit 1
fi
CLASS_NAME=$(basename "${TARGET_FILE}" .kt)
# 2. Stage 1: Fast-Fail Static Invariant Linter (Negative Constraints)
if grep -En "org\.mockito" "${TARGET_FILE}" >&2; then
echo "### 🚨 DETERMINISTIC TRIAGE REPORT" >&2
echo "- **Category**: \`STATIC_INVARIANT_VIOLATION\`" >&2
echo "- **Error**: Forbidden Mockito import detected in pure MockK test file." >&2
exit 2
fi
if grep -En "@SpringBootTest|@DataJpaTest|@WebMvcTest" "${TARGET_FILE}" >&2; then
echo "### 🚨 DETERMINISTIC TRIAGE REPORT" >&2
echo "- **Category**: \`STATIC_INVARIANT_VIOLATION\`" >&2
echo "- **Error**: Spring ApplicationContext runner detected. Unit tests must use mockk()." >&2
exit 2
fi
# 3. Stage 2: Sandboxed Maven Execution with Timeout Guard (Max 45s)
# // [TODAY'S LESSON PLUGS IN HERE: TIMEOUT SANDBOX & STDERR TRIAGE PIPELINE]
set +e
timeout 45s mvn test -B --no-transfer-progress -Dtest="${CLASS_NAME}" > "${LOG_FILE}" 2>&1
BUILD_STATUS=$?
set -e
if [[ ${BUILD_STATUS} -eq 124 ]]; then
echo "### 🚨 DETERMINISTIC TRIAGE REPORT" >&2
echo "- **Category**: \`EXECUTION_TIMEOUT\`" >&2
echo "- **Error**: Test execution exceeded 45s limit. Potential deadlock or heavy context initialization." >&2
exit 124
elif [[ ${BUILD_STATUS} -ne 0 ]]; then
# Pipeline raw noisy log into Python triage parser and output strictly to stderr
python3 "${TRIAGE_SCRIPT}" "${LOG_FILE}"
# Classify exit code based on build failure nature
if grep -q "Compilation failure" "${LOG_FILE}"; then
exit 3
else
exit 4
fi
fi
# 4. Success Path: Clean Structured JSON to stdout, zero stderr
echo "{\"status\": \"SUCCESS\", \"class\": \"${CLASS_NAME}\", \"verified\": true}"
exit 0
Enterprise Incident: The 80 KB Build Log Attention Blackout
สถานการณ์จริง: Host Agent กำลังเขียน Unit Test ให้กับ FxRateCalculator.kt แต่ตัวโมเดลเผลอส่งพารามิเตอร์ประเภท String แทนที่จะเป็น BigDecimal ในคำสั่ง calculator.computeRate(...) ทำให้ Kotlinc ปฏิเสธการคอมไพล์ หากไม่มี Stderr Triage ตัว Agent จะได้รับ Maven Output ขนาดยักษ์กว่า 80 KB ซึ่งเต็มไปด้วยข้อความดาวน์โหลด dependencies และ warning plugins จนมองไม่เห็นจุดที่โค้ดพัง
- จำลองข้อผิดพลาดในไฟล์
FxRateCalculatorTest.ktที่เกิด Type Mismatch ในคำสั่งทดสอบ - รันสคริปต์
run-unit-tests.shเพื่อพิสูจน์ว่า Log ขนาด 80 KB ถูกบีบอัดเหลือเป็น Diagnostic Report ขนาดเล็กกว่า 350 Tokens - ตรวจสอบว่าสคริปต์ส่งคืน Exit Code 3 (COMPILATION_FAILURE) ทาง Process Status และเขียน Diagnostic Card ออกมาทาง Stderr อย่างถูกต้อง
[INFO] Scanning for projects...
[INFO] Inspecting build environment in /workspace
[INFO] -------------------< com.enterprise:trading-service >-------------------
[INFO] Building Trading Service 2.4.0-SNAPSHOT
[INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ trading
[INFO] --- kotlin-maven-plugin:1.9.22:test-compile (test-compile) @ trading ---
[ERROR] /workspace/src/test/kotlin/FxRateCalculatorTest.kt:22:38: error: type mismatch: inferred type is String but BigDecimal was expected
[ERROR] val rate = calculator.computeRate("USD", "1.0500")
[ERROR] ^
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
... (350 lines of plugin execution stack trace suppressed)
### 🚨 DETERMINISTIC TRIAGE REPORT
- **Category**: `COMPILATION_FAILURE`
- **Location**: `FxRateCalculatorTest.kt:22`
- **Error**: type mismatch: inferred type is String but BigDecimal was expected
- **Action Required**: Resolve the violation indicated above and re-execute runner.
```text
[ERROR] /workspace/src/test/kotlin/FxRateCalculatorTest.kt:22:38: error: type mismatch: inferred type is String but BigDecimal was expected
```
$ bash .github/skills/spring-boot-unit-tester/scripts/run-unit-tests.sh src/test/kotlin/FxRateCalculatorTest.kt
### 🚨 DETERMINISTIC TRIAGE REPORT
- **Category**: `COMPILATION_FAILURE`
- **Location**: `FxRateCalculatorTest.kt:22`
- **Error**: type mismatch: inferred type is String but BigDecimal was expected
- **Action Required**: Resolve the violation indicated above and re-execute runner.
```text
[ERROR] /workspace/src/test/kotlin/FxRateCalculatorTest.kt:22:38: error: type mismatch: inferred type is String but BigDecimal was expected
```
$ echo $?
3
ทดสอบสถาปัตยกรรม Sandboxed Execution & Stderr Triage (Day 6)
ตอบคำถามเชิงสถาปัตยกรรมทั้ง 12 ข้อด้านล่าง ระบบจะตรวจคำตอบและให้ Feedback ทันทีเมื่อคลิกเลือกช้อยส์ พร้อมแสดงคำอธิบายเชิงลึก