diff --git a/ci-operator/config/openshift-eng/ai-helpers/openshift-eng-ai-helpers-main.yaml b/ci-operator/config/openshift-eng/ai-helpers/openshift-eng-ai-helpers-main.yaml
index 9e1b1eb55c7f5..e81f60d1bdea6 100644
--- a/ci-operator/config/openshift-eng/ai-helpers/openshift-eng-ai-helpers-main.yaml
+++ b/ci-operator/config/openshift-eng/ai-helpers/openshift-eng-ai-helpers-main.yaml
@@ -17,6 +17,16 @@ resources:
requests:
cpu: 100m
memory: 200Mi
+tests:
+- always_run: false
+ as: eval-payload-analysis
+ optional: true
+ steps:
+ env:
+ EVAL_CONFIG: plugins/ci/evals/eval-payload-analysis.yaml
+ EVAL_MODEL: claude-opus-4-6
+ EVAL_SETUP_SCRIPT: plugins/ci/evals/scripts/extract-payload-analysis-snapshots.sh
+ workflow: openshift-claude-agent-eval
zz_generated_metadata:
branch: main
org: openshift-eng
diff --git a/ci-operator/jobs/openshift-eng/ai-helpers/openshift-eng-ai-helpers-main-presubmits.yaml b/ci-operator/jobs/openshift-eng/ai-helpers/openshift-eng-ai-helpers-main-presubmits.yaml
index c95ecaa06dd1f..1841042666a40 100644
--- a/ci-operator/jobs/openshift-eng/ai-helpers/openshift-eng-ai-helpers-main-presubmits.yaml
+++ b/ci-operator/jobs/openshift-eng/ai-helpers/openshift-eng-ai-helpers-main-presubmits.yaml
@@ -1,5 +1,80 @@
presubmits:
openshift-eng/ai-helpers:
+ - agent: kubernetes
+ always_run: false
+ branches:
+ - ^main$
+ - ^main-
+ cluster: build13
+ context: ci/prow/eval-payload-analysis
+ decorate: true
+ decoration_config:
+ sparse_checkout_files:
+ - images/Dockerfile
+ labels:
+ ci.openshift.io/generator: prowgen
+ pj-rehearse.openshift.io/can-be-rehearsed: "true"
+ name: pull-ci-openshift-eng-ai-helpers-main-eval-payload-analysis
+ optional: true
+ rerun_command: /test eval-payload-analysis
+ spec:
+ containers:
+ - args:
+ - --gcs-upload-secret=/secrets/gcs/service-account.json
+ - --image-import-pull-secret=/etc/pull-secret/.dockerconfigjson
+ - --lease-server-credentials-file=/etc/boskos/credentials
+ - --report-credentials-file=/etc/report/credentials
+ - --target=eval-payload-analysis
+ command:
+ - ci-operator
+ env:
+ - name: HTTP_SERVER_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ image: quay-proxy.ci.openshift.org/openshift/ci:ci_ci-operator_latest
+ imagePullPolicy: Always
+ name: ""
+ ports:
+ - containerPort: 8080
+ name: http
+ resources:
+ requests:
+ cpu: 10m
+ volumeMounts:
+ - mountPath: /etc/boskos
+ name: boskos
+ readOnly: true
+ - mountPath: /secrets/gcs
+ name: gcs-credentials
+ readOnly: true
+ - mountPath: /secrets/manifest-tool
+ name: manifest-tool-local-pusher
+ readOnly: true
+ - mountPath: /etc/pull-secret
+ name: pull-secret
+ readOnly: true
+ - mountPath: /etc/report
+ name: result-aggregator
+ readOnly: true
+ serviceAccountName: ci-operator
+ volumes:
+ - name: boskos
+ secret:
+ items:
+ - key: credentials
+ path: credentials
+ secretName: boskos-credentials
+ - name: manifest-tool-local-pusher
+ secret:
+ secretName: manifest-tool-local-pusher
+ - name: pull-secret
+ secret:
+ secretName: registry-pull-credentials
+ - name: result-aggregator
+ secret:
+ secretName: result-aggregator
+ trigger: (?m)^/test( | .* )eval-payload-analysis,?($|\s.*)
- agent: kubernetes
always_run: true
branches:
diff --git a/ci-operator/step-registry/openshift/claude/agent-eval/OWNERS b/ci-operator/step-registry/openshift/claude/agent-eval/OWNERS
new file mode 100644
index 0000000000000..3259ba3e017ba
--- /dev/null
+++ b/ci-operator/step-registry/openshift/claude/agent-eval/OWNERS
@@ -0,0 +1,14 @@
+approvers:
+- bentito
+- bryan-cox
+- cblecker
+- enxebre
+- prashanth684
+- stbenjam
+reviewers:
+- bentito
+- bryan-cox
+- cblecker
+- enxebre
+- prashanth684
+- stbenjam
diff --git a/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-commands.sh b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-commands.sh
new file mode 100755
index 0000000000000..7723a216ef307
--- /dev/null
+++ b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-commands.sh
@@ -0,0 +1,215 @@
+#!/bin/bash
+#
+# Run agent-eval-harness against Claude Code skills.
+#
+# Required env:
+# EVAL_CONFIG -- path to eval.yaml (relative to repo root)
+#
+# Optional env:
+# EVAL_MODEL -- model for the skill under test (default: claude-sonnet-4-6)
+# EVAL_BASELINE -- run-id of a previous run to compare against
+# EVAL_EXTRA_ARGS -- additional args passed to /eval-run
+# EVAL_SETUP_SCRIPT -- script to run before eval (e.g. snapshot extraction)
+# CLAUDE_MODEL -- model for the eval harness orchestrator (default: claude-sonnet-4-6)
+# MLFLOW_PORT -- port for local MLflow server (default: 5000)
+
+set -o nounset
+set -o errexit
+set -o pipefail
+
+echo "Starting claude-agent-eval"
+
+# The repo is at /opt/ai-helpers; WORKDIR is /workspace
+cd /opt/ai-helpers
+
+echo "Config: ${EVAL_CONFIG}"
+echo "Skill model: ${EVAL_MODEL}"
+
+# -----------------------------------------------------------------------
+# Install dependencies
+# -----------------------------------------------------------------------
+echo "Installing mlflow..."
+export PATH="$HOME/.local/bin:$PATH"
+# mlflow 3.x requires sqlite >= 3.36.0; RHEL 9 ships 3.34.1
+python3 -m pip install --quiet 'mlflow==2.20.2'
+echo "mlflow installed."
+
+# Start local MLflow server in background
+echo "Starting local MLflow server on port ${MLFLOW_PORT}..."
+export MLFLOW_TRACKING_URI="http://127.0.0.1:${MLFLOW_PORT}"
+mlflow server --port "${MLFLOW_PORT}" --host 127.0.0.1 &
+MLFLOW_PID=$!
+
+for i in $(seq 1 30); do
+ if curl -sf "http://127.0.0.1:${MLFLOW_PORT}/health" >/dev/null 2>&1; then
+ echo "MLflow server ready (PID ${MLFLOW_PID})."
+ break
+ fi
+ if ! kill -0 "${MLFLOW_PID}" 2>/dev/null; then
+ echo "ERROR: MLflow server crashed."
+ exit 1
+ fi
+ if [[ $i -eq 30 ]]; then
+ echo "ERROR: MLflow server did not become ready in 30s."
+ exit 1
+ fi
+ sleep 1
+done
+
+# -----------------------------------------------------------------------
+# Verify eval config exists
+# -----------------------------------------------------------------------
+if [[ ! -f "${EVAL_CONFIG}" ]]; then
+ echo "ERROR: EVAL_CONFIG not found at ${EVAL_CONFIG}"
+ exit 1
+fi
+
+# -----------------------------------------------------------------------
+# Run optional setup script (e.g. extract snapshots, populate fixtures)
+# -----------------------------------------------------------------------
+if [[ -n "${EVAL_SETUP_SCRIPT}" ]]; then
+ if [[ ! -f "${EVAL_SETUP_SCRIPT}" ]]; then
+ echo "ERROR: EVAL_SETUP_SCRIPT not found: ${EVAL_SETUP_SCRIPT}"
+ exit 1
+ fi
+ echo ""
+ echo "=== Running setup script: ${EVAL_SETUP_SCRIPT} ==="
+ EVAL_SNAPSHOT_DIR=$(bash "${EVAL_SETUP_SCRIPT}")
+ export EVAL_SNAPSHOT_DIR
+ echo "Snapshot dir: ${EVAL_SNAPSHOT_DIR}"
+fi
+
+# -----------------------------------------------------------------------
+# Install plugins
+# -----------------------------------------------------------------------
+echo ""
+echo "=== Installing plugins ==="
+EVAL_HARNESS_DIR="/tmp/agent-eval-harness"
+git clone --depth 1 https://github.com/opendatahub-io/agent-eval-harness.git "${EVAL_HARNESS_DIR}"
+echo "agent-eval-harness cloned."
+
+# -----------------------------------------------------------------------
+# Artifact copy trap
+# -----------------------------------------------------------------------
+copy_artifacts() {
+ echo "Copying eval artifacts..."
+ if [[ -d "${AGENT_EVAL_RUNS_DIR:-eval/runs}" ]]; then
+ find "${AGENT_EVAL_RUNS_DIR:-eval/runs}" -name "report.html" -exec cp {} "${ARTIFACT_DIR}/eval-report-summary.html" \; 2>/dev/null || true
+ find "${AGENT_EVAL_RUNS_DIR:-eval/runs}" -name "summary.yaml" -exec cp {} "${ARTIFACT_DIR}/" \; 2>/dev/null || true
+ find "${AGENT_EVAL_RUNS_DIR:-eval/runs}" -name "run_result.json" -exec cp {} "${ARTIFACT_DIR}/" \; 2>/dev/null || true
+ fi
+ find . -name "*-summary.html" -exec cp {} "${ARTIFACT_DIR}/" \; 2>/dev/null || true
+
+ # Copy MLflow data
+ if [[ -d "mlruns" ]]; then
+ tar -czf "${ARTIFACT_DIR}/mlflow-data.tar.gz" mlruns/ 2>/dev/null || true
+ fi
+
+ # Archive Claude session for continue-session support
+ CLAUDE_HOME="/home/claude/.claude"
+ if [[ -d "${CLAUDE_HOME}/projects" ]]; then
+ echo "Archiving Claude session logs..."
+ tar -czf "${ARTIFACT_DIR}/claude-sessions-$(date +%Y%m%d-%H%M%S).tar.gz" \
+ -C "${CLAUDE_HOME}" projects/ 2>/dev/null && \
+ touch "${SHARED_DIR}/claude-session-available" || true
+ fi
+
+ # Stop MLflow server and all child processes (gunicorn workers)
+ if [[ -n "${MLFLOW_PID:-}" ]]; then
+ pkill -P "${MLFLOW_PID}" 2>/dev/null || true
+ kill "${MLFLOW_PID}" 2>/dev/null || true
+ fi
+}
+trap copy_artifacts EXIT TERM INT
+
+# -----------------------------------------------------------------------
+# Workaround: --continue + -p is broken (anthropics/claude-code#42376).
+# -----------------------------------------------------------------------
+export CLAUDE_CODE_ENTRYPOINT=sdk-cli
+
+# -----------------------------------------------------------------------
+# Build arguments
+# -----------------------------------------------------------------------
+RUN_ID="ci-$(date +%Y%m%d-%H%M%S)-${EVAL_MODEL}"
+ALLOWED_TOOLS="Bash Read Write Edit Grep Glob Agent Skill"
+
+EVAL_RUN_ARGS="--config ${EVAL_CONFIG} --model ${EVAL_MODEL} --run-id ${RUN_ID}"
+if [[ -n "${EVAL_BASELINE}" ]]; then
+ EVAL_RUN_ARGS="${EVAL_RUN_ARGS} --baseline ${EVAL_BASELINE}"
+fi
+if [[ -n "${EVAL_EXTRA_ARGS}" ]]; then
+ EVAL_RUN_ARGS="${EVAL_RUN_ARGS} ${EVAL_EXTRA_ARGS}"
+fi
+
+# -----------------------------------------------------------------------
+# Run evaluation
+# -----------------------------------------------------------------------
+echo ""
+echo "=== Running eval ==="
+echo "Run ID: ${RUN_ID}"
+echo "Args: ${EVAL_RUN_ARGS}"
+
+EVAL_START=$(date +%s)
+EVAL_EXIT=0
+timeout 7200 claude \
+ --model "${CLAUDE_MODEL}" \
+ --plugin-dir "${EVAL_HARNESS_DIR}" \
+ --allowedTools "${ALLOWED_TOOLS}" \
+ --output-format stream-json \
+ --max-turns 100 \
+ -p "/eval-run ${EVAL_RUN_ARGS}" \
+ --verbose 2>&1 | tee "${ARTIFACT_DIR}/claude-eval.log" || EVAL_EXIT=$?
+EVAL_DURATION=$(( $(date +%s) - EVAL_START ))
+
+echo "eval-run completed in ${EVAL_DURATION}s (exit ${EVAL_EXIT})"
+
+# -----------------------------------------------------------------------
+# Run eval-mlflow to upload results
+# -----------------------------------------------------------------------
+echo ""
+echo "=== Running eval-mlflow ==="
+
+MLFLOW_EXIT=0
+timeout 600 claude \
+ --model "${CLAUDE_MODEL}" \
+ --plugin-dir "${EVAL_HARNESS_DIR}" \
+ --continue \
+ --allowedTools "${ALLOWED_TOOLS}" \
+ --output-format stream-json \
+ --max-turns 20 \
+ -p "/eval-mlflow --action all --run-id ${RUN_ID} --config ${EVAL_CONFIG}" \
+ --verbose 2>&1 | tee "${ARTIFACT_DIR}/claude-eval-mlflow.log" || MLFLOW_EXIT=$?
+
+echo "eval-mlflow completed with exit code ${MLFLOW_EXIT}"
+
+# -----------------------------------------------------------------------
+# Generate JUnit XML
+# -----------------------------------------------------------------------
+JUNIT_FILE="${ARTIFACT_DIR}/junit_claude-eval.xml"
+FAILURE_COUNT=0
+TESTCASE="[sig-claude] Skill evaluation should pass"
+
+if [[ "${EVAL_EXIT}" -ne 0 ]]; then
+ FAILURE_COUNT=1
+ TESTCASES="
+ eval-run exited with code ${EVAL_EXIT}.
+ "
+else
+ TESTCASES=" "
+fi
+
+cat > "${JUNIT_FILE}" <
+
+${TESTCASES}
+
+EOF
+
+echo "JUnit XML written to ${JUNIT_FILE}"
+
+if [[ "${EVAL_EXIT}" -ne 0 ]]; then
+ echo "Evaluation failed."
+ exit 1
+fi
+
+echo "Evaluation complete."
diff --git a/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-ref.metadata.json b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-ref.metadata.json
new file mode 100644
index 0000000000000..631d08f83823f
--- /dev/null
+++ b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-ref.metadata.json
@@ -0,0 +1,21 @@
+{
+ "path": "openshift/claude/agent-eval/openshift-claude-agent-eval-ref.yaml",
+ "owners": {
+ "approvers": [
+ "bentito",
+ "bryan-cox",
+ "cblecker",
+ "enxebre",
+ "prashanth684",
+ "stbenjam"
+ ],
+ "reviewers": [
+ "bentito",
+ "bryan-cox",
+ "cblecker",
+ "enxebre",
+ "prashanth684",
+ "stbenjam"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-ref.yaml b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-ref.yaml
new file mode 100644
index 0000000000000..872e7ba5a425c
--- /dev/null
+++ b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-ref.yaml
@@ -0,0 +1,45 @@
+ref:
+ as: openshift-claude-agent-eval
+ from_image:
+ namespace: ci
+ name: claude-ai-helpers
+ tag: latest
+ commands: openshift-claude-agent-eval-commands.sh
+ credentials:
+ - namespace: test-credentials
+ name: hypershift-team-claude-prow
+ mount_path: /var/run/claude-code-service-account
+ env:
+ - name: EVAL_CONFIG
+ default: "eval.yaml"
+ - name: EVAL_MODEL
+ default: "claude-sonnet-4-6"
+ - name: EVAL_BASELINE
+ default: ""
+ - name: EVAL_EXTRA_ARGS
+ default: ""
+ - name: EVAL_SETUP_SCRIPT
+ default: ""
+ - name: MLFLOW_PORT
+ default: "5000"
+ - name: CLAUDE_MODEL
+ default: "claude-sonnet-4-6"
+ - name: CLAUDE_CODE_USE_VERTEX
+ default: "1"
+ - name: CLOUD_ML_REGION
+ default: "global"
+ - name: ANTHROPIC_VERTEX_PROJECT_ID
+ default: "itpc-gcp-hybrid-pe-eng-claude"
+ - name: GOOGLE_APPLICATION_CREDENTIALS
+ default: "/var/run/claude-code-service-account/claude-prow"
+ resources:
+ requests:
+ cpu: 1000m
+ memory: 2Gi
+ timeout: 3h0m0s
+ grace_period: 1m0s
+ documentation: |-
+ Runs skill evaluations using the agent-eval-harness. Installs the
+ eval harness plugin, starts a local MLflow server, runs /eval-run
+ and /eval-mlflow with the specified config and model, and produces
+ JUnit XML + artifacts. MLflow data is archived for download.
diff --git a/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-workflow.metadata.json b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-workflow.metadata.json
new file mode 100644
index 0000000000000..10beb8f7d974e
--- /dev/null
+++ b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-workflow.metadata.json
@@ -0,0 +1,21 @@
+{
+ "path": "openshift/claude/agent-eval/openshift-claude-agent-eval-workflow.yaml",
+ "owners": {
+ "approvers": [
+ "bentito",
+ "bryan-cox",
+ "cblecker",
+ "enxebre",
+ "prashanth684",
+ "stbenjam"
+ ],
+ "reviewers": [
+ "bentito",
+ "bryan-cox",
+ "cblecker",
+ "enxebre",
+ "prashanth684",
+ "stbenjam"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-workflow.yaml b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-workflow.yaml
new file mode 100644
index 0000000000000..f0542d59b2c0f
--- /dev/null
+++ b/ci-operator/step-registry/openshift/claude/agent-eval/openshift-claude-agent-eval-workflow.yaml
@@ -0,0 +1,10 @@
+workflow:
+ as: openshift-claude-agent-eval
+ steps:
+ test:
+ - ref: openshift-claude-agent-eval
+ post:
+ - ref: openshift-claude-post
+ documentation: |-
+ Runs skill evaluations using the agent-eval-harness and generates
+ a continue-session page in artifacts for resuming locally.