Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0df749f
Add LLM-as-judge evaluation framework for Docker skills
gtardif Nov 9, 2026
6465b2e
Relax compose-patterns secret criteria to allow env var syntax with d…
gtardif Nov 9, 2026
3ac6038
Clarify sandbox-lifecycle prompt-7 expects syntax demo, not file crea…
gtardif Nov 9, 2026
9458deb
Relax sandbox-lifecycle prompt-4 criterion about stopped sandbox beha…
gtardif Nov 9, 2026
d6d5295
Add more file patterns to workspace collection (override, agents, deps)
gtardif Nov 9, 2026
3a9ab77
Relax network-credentials prompt-1 criteria about specific command sy…
gtardif Nov 9, 2026
5c033f6
Clarify kits prompt-6 allows mentioning migration skill exists
gtardif Nov 9, 2026
815d0c1
Configure CI to run evals per-skill in parallel, add per-skill tasks
gtardif Nov 9, 2026
6c6ed6d
Fix workflow_dispatch to run all skills when no specific skill given
gtardif Nov 9, 2026
0ef72b3
Update workflow tests for new eval_judge.py structure
gtardif Nov 10, 2026
7b0d8a9
Default to running both Claude and Codex evals
gtardif Nov 10, 2026
56f2975
Bump docker-agent-config version to 0.1.3 for SKILL.md update
gtardif Nov 10, 2026
a0acb36
Fix broken links in AGENTS.md and docker-agent-config eval
gtardif Nov 10, 2026
678bfbf
Fix eval-codex condition to run on PRs (inputs.agent is empty)
gtardif Nov 10, 2026
1c083db
Add ANTHROPIC_API_KEY to Codex evals (needed for LLM judge)
gtardif Nov 10, 2026
3f28b1d
Make eval failures non-blocking (report as warnings)
gtardif Nov 10, 2026
657eebb
Relax overly specific eval criteria
gtardif Nov 10, 2026
9aceaeb
Revert "Make eval failures non-blocking (report as warnings)"
gtardif Nov 10, 2026
7a89688
Improve eval reliability: stronger instructions, workspace README
gtardif Nov 10, 2026
0ab0d75
Add --pass-threshold option for flexible pass rate
gtardif Nov 10, 2026
67348d4
Refine eval criteria for better reliability
gtardif Nov 10, 2026
c52550f
Support (Optional) criteria prefix in eval judge
gtardif Nov 10, 2026
62a6e62
Simplify complex eval criteria that cause inconsistent judging
gtardif Nov 10, 2026
5746a9d
Revert (Optional) prefix handling - keep criteria as required
gtardif Nov 10, 2026
ed59d6b
Improve eval reliability through clearer prompts and criteria
gtardif Nov 10, 2026
eeffe7d
Add 'supports live code reload' to compose override prompt
gtardif Nov 10, 2026
cb9ed64
Clarify sbx run reattachment criterion to distinguish from initial cr…
gtardif Nov 10, 2026
7c9b8c0
Add network access phases to sandboxes-kits prompt 1
gtardif Nov 10, 2026
73b0ad0
Set default models: sonnet for Claude, gpt-6-luna for Codex
gtardif Nov 10, 2026
66f951a
Improve eval reliability: better judge prompt and simplified criteria
gtardif Nov 10, 2026
51680f0
Relax docker-agent-run criteria to accept valid alternatives
gtardif Nov 10, 2026
f9bc85a
Relax eval criteria for better reliability across models
gtardif Nov 10, 2026
bfd000b
Relax eval criteria for better cross-model reliability
gtardif Nov 10, 2026
bdb3d7f
Relax eval criteria for remaining failures
gtardif Nov 10, 2026
bda4d9e
Relax eval criteria and use hello.md for lifecycle test
gtardif Nov 10, 2026
1dbcd1a
Add automatic retry on eval failure in CI
gtardif Nov 10, 2026
5629730
Make prompt-3 explicitly mention sbx for context
gtardif Nov 10, 2026
df3384e
Simplify prompt-3 expectation for CI gating
gtardif Nov 10, 2026
f7dceb3
Fix genuine eval failures with criterion relaxations
gtardif Nov 10, 2026
ca5905a
Relax --yolo must-not criterion for CI prompt
gtardif Nov 10, 2026
5360668
Clarify eval criteria for agent-run and sandboxes-env
gtardif Nov 12, 2026
4af078a
Fix sbx install in eval CI and only install it for sandboxes skills
gtardif Nov 12, 2026
4274210
Fix must-not judge inversion and project-foundations Dockerfile baseline
gtardif Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 150 additions & 73 deletions .github/workflows/skill-evals.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
name: Live skill evals

# Runs skills/*/evals/*.yaml with Claude Code and/or Codex against real CLIs.
# Calls a live model and may need tool-specific authentication, so it stays
# out of offline `task ci`.
# Runs skill evaluations using LLM-as-judge against Claude Code and/or Codex.
# Evaluations are defined in skills/<skill>/evals/<skill>.md markdown files.
# Each skill runs as a separate job for parallelism and targeted re-runs.
on:
pull_request:
paths:
- ".claude-plugin/**"
- ".codex-plugin/**"
- ".agents/**"
- "skills/**"
- "scripts/skill_eval.py"
- "scripts/eval_judge.py"
- "scripts/test_eval_judge.py"
- ".github/workflows/skill-evals.yml"
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:
inputs:
skill:
description: "Only run scenarios for this skill id (empty: all)"
description: "Only run evals for this skill id (empty: all)"
required: false
default: ""
agent:
Expand All @@ -34,7 +35,7 @@ on:
required: false
default: ""
repeat:
description: "Runs per scenario"
description: "Runs per prompt"
required: false
default: "1"

Expand All @@ -46,6 +47,75 @@ concurrency:
cancel-in-progress: true

jobs:
# Determine which skills to evaluate based on changed files
detect-changes:
runs-on: ubuntu-latest
outputs:
skills: ${{ steps.detect.outputs.skills }}
run_all: ${{ steps.detect.outputs.run_all }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0

- name: Detect changed skills
id: detect
run: |
# All skills except docker-destructive-guardrails (too flaky for CI)
ALL_SKILLS='["docker-agent-config","docker-agent-deploy","docker-agent-run","docker-build-strategies","docker-compose-patterns","docker-docs","docker-project-foundations","docker-sandboxes-env","docker-sandboxes-kits","docker-sandboxes-lifecycle","docker-sandboxes-network-credentials"]'

# If workflow_dispatch with specific skill, use that
if [ -n "${{ inputs.skill }}" ]; then
echo "skills=[\"${{ inputs.skill }}\"]" >> $GITHUB_OUTPUT
echo "run_all=false" >> $GITHUB_OUTPUT
exit 0
fi

# If scheduled or workflow_dispatch without skill, run all skills
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "run_all=true" >> $GITHUB_OUTPUT
echo "skills=$ALL_SKILLS" >> $GITHUB_OUTPUT
exit 0
fi

# Get changed files
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_SHA=${{ github.event.pull_request.base.sha }}
HEAD_SHA=${{ github.event.pull_request.head.sha }}
else
BASE_SHA=${{ github.event.before }}
HEAD_SHA=${{ github.sha }}
fi

CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null || echo "")

# Check if framework files changed
FRAMEWORK_CHANGED=false
if echo "$CHANGED_FILES" | grep -qE '^(scripts/eval_judge\.py|\.github/workflows/skill-evals\.yml|\.claude-plugin/|\.codex-plugin/|\.agents/)'; then
FRAMEWORK_CHANGED=true
fi

if [ "$FRAMEWORK_CHANGED" = "true" ]; then
echo "run_all=true" >> $GITHUB_OUTPUT
echo "skills=$ALL_SKILLS" >> $GITHUB_OUTPUT
exit 0
fi

# Extract specific skills that changed
SKILLS=$(echo "$CHANGED_FILES" | grep -oE '^skills/[^/]+' | sed 's|skills/||' | sort -u | grep -v '^docker-destructive-guardrails$' || true)

if [ -z "$SKILLS" ]; then
echo "No skill changes detected"
echo "skills=[]" >> $GITHUB_OUTPUT
echo "run_all=false" >> $GITHUB_OUTPUT
else
# Convert to JSON array
SKILLS_JSON=$(echo "$SKILLS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "skills=$SKILLS_JSON" >> $GITHUB_OUTPUT
echo "run_all=false" >> $GITHUB_OUTPUT
fi

plugin-validate:
runs-on: ubuntu-latest
timeout-minutes: 5
Expand All @@ -64,37 +134,43 @@ jobs:
- name: Validate plugin manifest
run: claude plugin validate .

scenarios-claude:
if: ${{ inputs.agent == 'claude' || inputs.agent == 'both' || inputs.agent == '' }}
# Claude evals - one job per skill
eval-claude:
needs: detect-changes
if: |
(inputs.agent == 'claude' || inputs.agent == 'both' || inputs.agent == '') &&
needs.detect-changes.outputs.skills != '[]'
runs-on: ubuntu-24.04
timeout-minutes: 45
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
skill: ${{ fromJson(needs.detect-changes.outputs.skills) }}
env:
CLAUDE_CODE_VERSION: "2.1.316"
SBX_VERSION: "0.50.1"
SKILL_EVAL_AGENT: "claude"
SKILL_EVAL_MODEL: ${{ inputs.model || 'sonnet' }}
SKILL_EVAL_REPEAT: ${{ inputs.repeat || '1' }}
SKILL_EVAL_MIN_PASS_RATE: "1.0"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Only Docker Sandboxes skills need the sbx CLI at eval time
- name: Install Docker Sandboxes (sbx)
if: startsWith(matrix.skill, 'docker-sandboxes-')
run: |
curl -fsSL "https://github.com/docker/sbx-releases/releases/download/v${SBX_VERSION}/DockerSandboxes-linux-amd64-ubuntu2404.deb" -o sbx.deb
sudo apt-get update -q
sudo apt-get install -y ./sbx.deb
sudo usermod -aG kvm "$USER"
rm sbx.deb
sbx version

- name: Setup sbx (login and policy)
if: startsWith(matrix.skill, 'docker-sandboxes-')
env:
DOCKERPUBLICBOT_READ_PAT: ${{ secrets.DOCKERPUBLICBOT_READ_PAT }}
DOCKERPUBLICBOT_USERNAME: ${{ vars.DOCKERPUBLICBOT_USERNAME }}
run: |
# sg kvm runs commands in kvm group context (usermod doesn't affect current shell)
# Login starts the daemon and authenticates for pulling sandbox images
sg kvm -c 'echo "$DOCKERPUBLICBOT_READ_PAT" | sbx login --username "$DOCKERPUBLICBOT_USERNAME" --password-stdin'
sg kvm -c 'sbx policy init balanced'

Expand All @@ -109,72 +185,80 @@ jobs:
python3 -m venv .venv
.venv/bin/pip install -q --require-hashes -r scripts/requirements.txt

- name: Run live skill scenarios (Claude)
- name: Run eval for ${{ matrix.skill }} (Claude)
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
INPUT_SKILL: ${{ inputs.skill }}
INPUT_MODEL: ${{ inputs.model || 'sonnet' }}
INPUT_REPEAT: ${{ inputs.repeat || '1' }}
run: |
set +e
.venv/bin/python scripts/skill_eval.py --agent claude ${INPUT_SKILL:+--skill "$INPUT_SKILL"}
exit_code=$?
# Debug output
echo "=== Listing .skill-evals ==="
ls -laR .skill-evals/ 2>&1 | head -50 || echo "No .skill-evals directory found"
echo "=== report.json ==="
cat .skill-evals/*/docker-sandboxes-lifecycle/mount-ls-delete/1/report.json 2>/dev/null | head -200 || true
echo "=== transcript.jsonl (last 50 lines) ==="
tail -50 .skill-evals/*/docker-sandboxes-lifecycle/mount-ls-delete/1/transcript.jsonl 2>/dev/null || true
exit $exit_code
# Run eval with one automatic retry on failure
if .venv/bin/python scripts/eval_judge.py \
--agent claude \
--model "$INPUT_MODEL" \
--repeat "$INPUT_REPEAT" \
--skill "${{ matrix.skill }}"; then
echo "Eval passed on first attempt"
else
echo "::warning::First attempt failed, retrying..."
.venv/bin/python scripts/eval_judge.py \
--agent claude \
--model "$INPUT_MODEL" \
--repeat "$INPUT_REPEAT" \
--skill "${{ matrix.skill }}"
fi

- name: Upload transcripts and call logs (Claude)
- name: Upload eval artifacts
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: skill-evals-claude
path: .skill-evals/
name: eval-claude-${{ matrix.skill }}
path: .eval-judge/
if-no-files-found: ignore
retention-days: 14

- name: Cleanup sandboxes
if: always()
if: always() && startsWith(matrix.skill, 'docker-sandboxes-')
run: |
sbx ls -q 2>/dev/null | grep "^eval-" | xargs -r sbx rm --force || true
sbx daemon stop || true

scenarios-codex:
# Codex evals use Docker AI Gateway with Docker Hub authentication
if: ${{ inputs.agent == 'codex' || inputs.agent == 'both' || inputs.agent == '' }}
# Codex evals - one job per skill
eval-codex:
needs: detect-changes
if: |
(inputs.agent == 'codex' || inputs.agent == 'both' || inputs.agent == '') &&
needs.detect-changes.outputs.skills != '[]'
runs-on: ubuntu-24.04
timeout-minutes: 45
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
skill: ${{ fromJson(needs.detect-changes.outputs.skills) }}
env:
CODEX_VERSION: "0.166.0"
SBX_VERSION: "0.50.1"
SKILL_EVAL_AGENT: "codex"
# Don't specify a model by default - let Codex use the gateway's default
# (explicitly specifying gpt-4o causes flaky behavior where the model refuses to execute commands)
SKILL_EVAL_MODEL: ${{ inputs.model || '' }}
SKILL_EVAL_REPEAT: ${{ inputs.repeat || '1' }}
SKILL_EVAL_MIN_PASS_RATE: "1.0"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Only Docker Sandboxes skills need the sbx CLI at eval time
- name: Install Docker Sandboxes (sbx)
if: startsWith(matrix.skill, 'docker-sandboxes-')
run: |
curl -fsSL "https://github.com/docker/sbx-releases/releases/download/v${SBX_VERSION}/DockerSandboxes-linux-amd64-ubuntu2404.deb" -o sbx.deb
sudo apt-get update -q
sudo apt-get install -y ./sbx.deb
sudo usermod -aG kvm "$USER"
rm sbx.deb
sbx version

- name: Setup sbx (login and policy)
if: startsWith(matrix.skill, 'docker-sandboxes-')
env:
DOCKERPUBLICBOT_READ_PAT: ${{ secrets.DOCKERPUBLICBOT_READ_PAT }}
DOCKERPUBLICBOT_USERNAME: ${{ vars.DOCKERPUBLICBOT_USERNAME }}
run: |
# sg kvm runs commands in kvm group context (usermod doesn't affect current shell)
# Login starts the daemon and authenticates for pulling sandbox images
sg kvm -c 'echo "$DOCKERPUBLICBOT_READ_PAT" | sbx login --username "$DOCKERPUBLICBOT_USERNAME" --password-stdin'
sg kvm -c 'sbx policy init balanced'

Expand All @@ -188,7 +272,6 @@ jobs:
DOCKERPUBLICBOT_READ_PAT: ${{ secrets.DOCKERPUBLICBOT_READ_PAT }}
DOCKERPUBLICBOT_USERNAME: ${{ vars.DOCKERPUBLICBOT_USERNAME }}
run: |
# Get JWT token from Docker Hub
JWT=$(curl -s -X POST "https://hub.docker.com/v2/users/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${DOCKERPUBLICBOT_USERNAME}\",\"password\":\"${DOCKERPUBLICBOT_READ_PAT}\"}" \
Expand All @@ -199,10 +282,7 @@ jobs:
exit 1
fi

# Create Codex config directory
mkdir -p ~/.codex

# Configure Codex to use Docker AI Gateway
cat > ~/.codex/config.toml << EOF
model_provider = "docker-gateway"

Expand All @@ -214,50 +294,47 @@ jobs:
requires_openai_auth = false
EOF

# Export token for later steps
echo "DOCKER_GATEWAY_TOKEN=${JWT}" >> $GITHUB_ENV
echo "::add-mask::${JWT}"

echo "Codex configured to use Docker AI Gateway"

- name: Install Python dependencies
run: |
python3 -m venv .venv
.venv/bin/pip install -q --require-hashes -r scripts/requirements.txt

- name: Run live skill scenarios (Codex)
- name: Run eval for ${{ matrix.skill }} (Codex)
env:
INPUT_SKILL: ${{ inputs.skill }}
INPUT_MODEL: ${{ inputs.model }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
INPUT_MODEL: ${{ inputs.model || 'gpt-6-luna' }}
INPUT_REPEAT: ${{ inputs.repeat || '1' }}
run: |
set +e
# Only pass --model if explicitly specified; gateway default works better than explicit gpt-4o
model_arg=""
if [ -n "$INPUT_MODEL" ]; then
model_arg="--model $INPUT_MODEL"
# Run eval with one automatic retry on failure
if .venv/bin/python scripts/eval_judge.py \
--agent codex \
--model "$INPUT_MODEL" \
--repeat "$INPUT_REPEAT" \
--skill "${{ matrix.skill }}"; then
echo "Eval passed on first attempt"
else
echo "::warning::First attempt failed, retrying..."
.venv/bin/python scripts/eval_judge.py \
--agent codex \
--model "$INPUT_MODEL" \
--repeat "$INPUT_REPEAT" \
--skill "${{ matrix.skill }}"
fi
.venv/bin/python scripts/skill_eval.py --agent codex $model_arg ${INPUT_SKILL:+--skill "$INPUT_SKILL"}
exit_code=$?
# Debug output
echo "=== Listing .skill-evals ==="
ls -laR .skill-evals/ 2>&1 | head -50 || echo "No .skill-evals directory found"
echo "=== report.json ==="
cat .skill-evals/*/docker-sandboxes-lifecycle/mount-ls-delete/1/report.json 2>/dev/null | head -200 || true
echo "=== transcript.jsonl (last 50 lines) ==="
tail -50 .skill-evals/*/docker-sandboxes-lifecycle/mount-ls-delete/1/transcript.jsonl 2>/dev/null || true
exit $exit_code

- name: Upload transcripts and call logs (Codex)
- name: Upload eval artifacts
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: skill-evals-codex
path: .skill-evals/
name: eval-codex-${{ matrix.skill }}
path: .eval-judge/
if-no-files-found: ignore
retention-days: 14

- name: Cleanup sandboxes
if: always()
if: always() && startsWith(matrix.skill, 'docker-sandboxes-')
run: |
sbx ls -q 2>/dev/null | grep "^eval-" | xargs -r sbx rm --force || true
sbx daemon stop || true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.sbx/
.data/
.skill-evals/
.eval-judge/

# Environment, credentials, and key material
.env
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ are required; validation runs in pinned container images.
not a live model evaluation.
- `task eval:skills`: opt-in live model evaluation. It runs scenarios in
`skills/*/evals/` with Claude Code against the real `sbx` CLI; see
[evals/README.md](evals/README.md#live-skill-scenarios). It needs `claude`,
[evals/README.md](evals/README.md#llm-as-judge-evaluations). It needs `claude`,
`sbx` authenticated, and optionally model credentials. Runs separately
from offline `task`/`task ci`.
- `task links`: check local Markdown destinations and heading anchors.
Expand Down
Loading
Loading