D6
Standard: agentskills/agentskills

Day 6: Sandboxed Execution & Stderr Triage

12-Day Series
🗺️ SECTION 1 / SYSTEM BLUEPRINT & INTEGRATION

The "Where & Why": กลไก Stderr Triage และการปิดลูป Agent Self-Correction

🎯 Mental Map (144 Words): ใน Day 5 เราติดตั้งไดเรกทอรี 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 ได้อย่างแม่นยำ
Closed-Loop Stderr Triage & Execution Sandbox Topology Triage Protocol v1.0
┌────────────────────────────────────────────────────────────────────────┐
│ 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     │
└────────────────────────────────────────────────────────────────────────┘
        
PILLAR 01: STREAM ISOLATION

Strict Stdout vs Stderr

Stdout สงวนไว้สำหรับ Clean JSON Artifact เมื่อสำเร็จ ส่วน Stderr ใช้สำหรับส่ง Diagnostic Payload เพื่อให้ Agent ตัดสินใจได้ทันที

PILLAR 02: CONTEXT BUDGETING

< 350 Token Budget

ตัดทิ้ง Maven dependency download logs และ stack traces ส่วนเกิน เพื่อรักษา Token Space และ Attention Focus ของโมเดล

PILLAR 03: EXIT CODE MATRIX

Deterministic Branching

การใช้ Exit Codes จำแนกสาเหตุ (0, 1, 2, 3, 4, 124) ช่วยให้ระบบสั่งการของ Host Agent แตกกิ่งการแก้ไขปัญหาได้อย่างแม่นยำ

⚙️ SECTION 2 / ARCHITECTURAL DEEP DIVE

วิศวกรรม 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 ทิ้งทันที
The Telemetry Blackbox: Token Filtering

เมื่อรัน mvn test ข้อมูลที่ส่งออกมักมีความยาวกว่า 500-1,000 บรรทัด ประกอบด้วย Maven scanning plugins, JVM boot telemetry, และ classloader warnings หากส่งข้อความเหล่านี้ให้ Agent โมเดลจะสูญเสียความสามารถในการ Focus

Raw_Log (50,000 bytes) ──► Triage_Filter ──► Diagnostic_Card (300 bytes / ~70 tokens)

สคริปต์ triage-test-failure.py ทำหน้าที่เป็นวงจรกรองความถี่ต่ำ สกัดเฉพาะ Error Header, File, Line, และ Root Cause Message ส่งออกทาง Stderr เท่านั้น

Timeout Sandboxing & Max-Retry Circuit Breaker

การสร้างระบบ 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
🧩 SECTION 3 / END-TO-END CODE BLUEPRINT

Production Artifact: Stderr Triage Engine & Sandbox Runner

ชุดเครื่องมือที่สมบูรณ์ตามมาตรฐาน agentskills/agentskills ประกอบด้วยข้อกำหนดใน SKILL.md, ตัวกรอง Stderr ภาษา Python, และ Orchestrator Shell Script

Skill Directory Placement
.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]
        
FILE: .github/skills/spring-boot-unit-tester/SKILL.md Tool Remediation Policy
---
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.
          
FILE: scripts/triage-test-failure.py Zero-Dependency Python 3
#!/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()
          
FILE: scripts/run-unit-tests.sh POSIX Bash Runner
#!/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
          
🛠️ SECTION 4 / HANDS-ON SCENARIO & LAB

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 จนมองไม่เห็นจุดที่โค้ดพัง

⚠️ LAB MISSION OBJECTIVES
  1. จำลองข้อผิดพลาดในไฟล์ FxRateCalculatorTest.kt ที่เกิด Type Mismatch ในคำสั่งทดสอบ
  2. รันสคริปต์ run-unit-tests.sh เพื่อพิสูจน์ว่า Log ขนาด 80 KB ถูกบีบอัดเหลือเป็น Diagnostic Report ขนาดเล็กกว่า 350 Tokens
  3. ตรวจสอบว่าสคริปต์ส่งคืน Exit Code 3 (COMPILATION_FAILURE) ทาง Process Status และเขียน Diagnostic Card ออกมาทาง Stderr อย่างถูกต้อง
❌ RAW NOISY OUTPUT (> 500 lines / 80 KB)
[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)
          
✅ FILTERED STDERR TRIAGE REPORT (< 70 tokens)
### 🚨 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
```
          
TERMINAL PROOF: Exit Code Verification Bash Output
$ 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
        
🎯 SECTION 5 / INTERACTIVE ARCHITECTURE & VERIFICATION QUIZ

ทดสอบสถาปัตยกรรม Sandboxed Execution & Stderr Triage (Day 6)

Current Score
0 / 12

ตอบคำถามเชิงสถาปัตยกรรมทั้ง 12 ข้อด้านล่าง ระบบจะตรวจคำตอบและให้ Feedback ทันทีเมื่อคลิกเลือกช้อยส์ พร้อมแสดงคำอธิบายเชิงลึก