{
  "generated": "2026-08-26",
  "answerer": "claude-haiku-4-5",
  "judge": "claude-opus-5",
  "total_cost_usd": 1.4007,
  "note": "Answers were generated with no runbooks, no retrieval, and no cluster access. The judge saw the question and the answer, never a reference answer. Generated by scripts/slo-post-experiment.sh.",
  "items": [
    {
      "id": 0,
      "question": "Pod is CrashLoopBackOff with exit code 137. What now?",
      "answers": {
        "a": {
          "text": "Exit code 137 = **SIGKILL** \u2014 almost always **OOM kill** (out of memory).\n\n**Immediate checks:**\n\n```bash\nkubectl describe pod <pod-name> -n <namespace>\n# Look for \"OOMKilled\" in the Last State section\n```\n\n```bash\nkubectl logs <pod-name> -n <namespace> --previous\n# Last log before crash\n```\n\n```bash\nkubectl top pod <pod-name> -n <namespace>\n# Current memory usage\n```\n\n**Quick fixes:**\n1. **Increase memory limit** in deployment spec: `resources.limits.memory`\n2. **Set memory request** if missing: `resources.requests.memory`\n3. **Check node resources** \u2014 if node is starved, pods get evicted\n\n**Example patch:**\n```bash\nkubectl set resources deployment <name> -n <namespace> \\\n  --limits=memory=2Gi --requests=memory=1Gi\n```\n\n**Check node status:**\n```bash\nkubectl top nodes\nkubectl describe node <node-name>\n```\n\nWhat namespace/pod? I can help refine the diagnosis if you have logs.",
          "judge": {
            "reason": "Core claim is accurate: exit 137 = 128+9 = SIGKILL, and OOM kill is by far the most common cause, correctly hedged with \"almost always\" rather than stated as certain. Every command and flag is real and correctly formed \u2014 `kubectl describe pod`, `kubectl logs --previous`, `kubectl top pod/nodes`, and `kubectl set resources deployment --limits=memory=2Gi --requests=memory=1Gi` all exist with that syntax. The diagnostic order is the one a senior SRE would actually use at 3am: describe the pod and read Last State for Reason: OOMKilled (which is what confirms or rules out OOM), then previous-container logs, then live usage, then node-level pressure. That path lands on the cause rather than away from it. Weaknesses are real but not disqualifying: it doesn't name the second-most-common 137 source (liveness-probe restart or a container ignoring SIGTERM past terminationGracePeriodSeconds, which shows Reason: Error, not OOMKilled), it doesn't warn that bumping the limit masks a genuine leak, and \"if node is starved, pods get evicted\" is loosely worded since eviction surfaces as status Evicted while node memory pressure reaching 137 is a kernel OOM kill. None of these are false statements, fabricated flags, or misdirection \u2014 the OOMKilled check the answer explicitly calls out is precisely what distinguishes the cases. Safe to act on.",
            "verdict": "pass"
          },
          "human": {
            "verdict": "pass",
            "reason": "Hedges 137 correctly as 'almost always' OOM rather than always, and points at Last State, which is where OOMKilled actually appears. The kubectl set resources syntax is valid."
          }
        },
        "b": {
          "text": "Exit code 137 = SIGKILL (signal 9). **Almost always OOMKilled** (out of memory).\n\n**Quick checks:**\n\n1. **Check pod events:**\n   ```bash\n   kubectl describe pod <pod-name> -n <namespace>\n   ```\n   Look for \"OOMKilled\" in the Status/Events section.\n\n2. **Check memory limits:**\n   ```bash\n   kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[].resources}'\n   ```\n\n3. **Check current usage vs. limits:**\n   ```bash\n   kubectl top pod <pod-name> -n <namespace>\n   ```\n\n**Fixes (pick one):**\n- **Increase memory limit** in the deployment/pod spec\n- **Reduce traffic/replicas** temporarily\n- **Check for memory leaks** in app logs before crash:\n  ```bash\n  kubectl logs <pod-name> -n <namespace> --previous\n  ```\n\n**If not OOMKilled**, check if the process is being externally killed (node eviction, kubelet restart, etc.) by reviewing node events.\n\nWhat does `kubectl describe pod` show for the Events section?",
          "judge": {
            "verdict": "pass",
            "reason": "Technically accurate and safe. Exit 137 = 128+9 (SIGKILL) and OOMKill is correctly identified as the dominant cause. All commands and flags are real and correctly formed: `kubectl describe pod`, `kubectl top pod`, `kubectl logs --previous`, and the jsonpath `{.spec.containers[].resources}` (empty brackets are accepted by kubectl's jsonpath parser as a full range). The diagnostic order is right \u2014 describe pod surfaces `Last State: Terminated / Reason: OOMKilled`, and `--previous` is the correct flag for a crashlooping container's prior logs. Minor gaps that don't rise to failure: it doesn't explicitly name liveness-probe failure (SIGTERM then SIGKILL after grace period) as a common non-OOM source of 137, nor distinguish cgroup-limit OOM from node-level memory pressure eviction, and `kubectl top` often returns nothing for a pod in backoff. But step 1's events check would surface a \"Liveness probe failed / Killing container\" cause anyway, so the engineer is not sent down a dead-end path. Nothing false or invented; actionable at 3am."
          },
          "human": {
            "verdict": "pass",
            "reason": "Says 'Status/Events section' when OOMKilled lives in the container's Last State, which is imprecise, but describe pod puts all of it on one screen so the engineer still finds it. The jsonpath with empty brackets is valid kubectl syntax; I checked it against kubectl 1.36."
          }
        }
      },
      "pairwise": {
        "ab": {
          "winner": "second",
          "reason": "Both correctly identify 137 as SIGKILL/likely OOMKilled and give real, valid kubectl commands (describe, logs --previous, top). The deciding difference is diagnostic completeness and accuracy of the residual case. The second answer explicitly branches: if the container's Last State is not OOMKilled, look for external kills (node pressure/eviction, kubelet restart, node events) \u2014 which is the correct next step, since 137 also results from any SIGKILL, not just cgroup OOM. It also adds a direct check of the configured limits via jsonpath, which is the number you need before deciding on a bump, and asks for the Events output to continue triage rather than jumping to a change. The first answer instead pushes straight to remediation, including a `kubectl set resources` patch that a 3am on-call could apply before confirming OOMKilled, and its node-resource bullet conflates eviction with in-container OOM kill, which can misdirect. Neither invents flags, but the second is the one that reliably lands on the actual cause. Minor knock on the second: OOMKilled shows in the container's Last State rather than Events, though it says \"Status/Events,\" so the engineer still looks in the right output.",
          "picked": "b"
        },
        "ba": {
          "winner": "second",
          "reason": "Both correctly identify 137 as SIGKILL/likely OOMKilled and both give the essential triage trio (describe, logs --previous, top). The second answer is more precise and fully verifiable: it points to the \\\"Last State\\\" block of `kubectl describe pod`, which is where `Reason: OOMKilled` actually appears (the first answer says \\\"Status/Events\\\", and an OOM kill often produces no Event at all, so that hint can mislead at 3am). Every command in the second answer is real and correctly formed, including `kubectl set resources deployment --limits/--requests`, and it extends triage to the node level (`kubectl top nodes`, `describe node`) to catch node memory pressure/eviction as the alternate cause. The first answer's `-o jsonpath='{.spec.containers[].resources}'` uses an empty-bracket selector rather than the documented `[*]`, which is at best fragile to type under pressure. The first answer's one real advantage \u2014 an explicit \\\"if not OOMKilled\\\" branch \u2014 is largely covered by the second answer's node-starvation check, and neither mentions the other common non-OOM 137 path (liveness-probe restart escalating SIGTERM to SIGKILL), so that does not separate them.",
          "picked": "a"
        }
      },
      "position_flip": true
    },
    {
      "id": 1,
      "question": "How do I find which container in the pod got OOMKilled?",
      "answers": {
        "a": {
          "text": "Use `kubectl describe pod <pod-name> -n <namespace>`. Look in the `Container States` section for any container with:\n\n```\nLast State:\n  Terminated\n    Reason:       OOMKilled\n    Exit Code:    137\n```\n\nOr for a quick JSON query:\n\n```bash\nkubectl get pod <pod-name> -n <namespace> -o jsonpath='{.status.containerStatuses[*].{name:.name,reason:.lastState.terminated.reason,exitCode:.lastState.terminated.exitCode}}'\n```\n\n**Exit code 137** is the telltale sign (128 + 9 = SIGKILL). Check `lastState` if the container restarted, or `state` if it's still dead. The container name and reason will identify which one hit the OOM limit.",
          "judge": {
            "reason": "The `kubectl describe` guidance and the Last State / Reason: OOMKilled / exit 137 explanation are correct, but the \"quick JSON query\" uses invalid syntax: kubectl's JSONPath implementation does not support object-construction expressions like `{.status.containerStatuses[*].{name:.name,reason:...}}` (that is jq/custom-columns-style syntax). Pasted at 3am it errors out rather than returning data, so the answer includes a command that does not exist as written. A correct form would be e.g. `-o jsonpath='{range .status.containerStatuses[*]}{.name}{\"\\t\"}{.lastState.terminated.reason}{\"\\n\"}{end}'` or `-o custom-columns=...`. Minor additional issues: exit 137 is SIGKILL generally, not proof of OOM (it can come from other kills/liveness-probe terminations), and node-level OOM vs cgroup-limit OOM is not distinguished. Under the no-partial-credit standard, the fabricated command fails it.",
            "verdict": "fail"
          },
          "human": {
            "verdict": "fail",
            "reason": "The jsonpath is invented. kubectl has no object-construction syntax, so {.status.containerStatuses[*].{name:.name,...}} exits with 'unrecognized character in action: U+007B'. Verified against kubectl 1.36. It also has lastState and state backwards."
          }
        },
        "b": {
          "text": "Check container status with:\n\n```bash\nkubectl describe pod <pod-name> -n <namespace>\n```\n\nLook at the **Container statuses** section \u2014 the OOMKilled container will show:\n- `Last State: Terminated (OOMKilled)`\n- `Exit Code: 137`\n- `Restart Count: N`\n\nFor a quick dump of all container restart reasons:\n\n```bash\nkubectl get pod <pod-name> -n <namespace> -o jsonpath='{range .status.containerStatuses[*]}{.name}{\"\\t\"}{.lastState.terminated.reason}{\"\\n\"}{end}'\n```\n\nCheck logs (if available before restart):\n\n```bash\nkubectl logs <pod-name> -c <container-name> -n <namespace> --previous\n```\n\nAlso verify resource limits:\n\n```bash\nkubectl describe pod <pod-name> -n <namespace> | grep -A 5 \"Limits\\|Requests\"\n```\n\nThe container with the highest memory usage relative to its limit is likely the culprit. If you need to scale it, increase the memory limit in the pod/deployment spec.",
          "judge": {
            "verdict": "pass",
            "reason": "All commands and flags are real and correctly formed: `kubectl describe pod`, the jsonpath over `.status.containerStatuses[*].lastState.terminated.reason`, and `kubectl logs -c <container> --previous`. The diagnostic facts are accurate \u2014 an OOMKilled container reports Last State: Terminated with Reason: OOMKilled and Exit Code 137, and describe output does show per-container Last State/Reason/Restart Count. This path reliably identifies the specific container that was OOMKilled. Minor nits (the section is labeled 'Containers:' rather than 'Container statuses', it doesn't mention initContainerStatuses, and the closing 'highest memory usage relative to its limit is likely the culprit' heuristic is redundant since the earlier commands already name the container definitively) are cosmetic and don't mislead or send the engineer down a wrong path. Safe to act on at 3am."
          },
          "human": {
            "verdict": "pass",
            "reason": "The range/end jsonpath is exactly right and answers the question in one command: it prints each container name next to its termination reason."
          }
        }
      },
      "pairwise": {
        "ab": {
          "winner": "second",
          "reason": "The first answer's jsonpath expression is invalid \u2014 `{.status.containerStatuses[*].{name:.name,reason:...}}` mixes JMESPath-style object construction into kubectl's jsonpath, which does not support it; the command errors out and sends a paged engineer chasing a broken tool. It also loosely implies exit 137 alone proves OOM. The second answer's `{range ...}{.name}{\\\"\\\\t\\\"}{.lastState.terminated.reason}{\\\"\\\\n\\\"}{end}` is correct, runnable kubectl jsonpath, and it adds genuinely useful next steps (`kubectl logs --previous`, checking limits). Its only weak spot is the vague closing heuristic about 'highest memory usage relative to limit', which is advisory rather than false, and the 'increase the limit' suggestion is standard practice. Nothing in it is fabricated or misleading.",
          "picked": "b"
        },
        "ba": {
          "winner": "first",
          "reason": "The first answer's commands are all real and runnable: the jsonpath range/loop over .status.containerStatuses is valid kubectl syntax, `kubectl logs --previous` is the right way to get the pre-kill logs, and the OOMKilled/exit 137/lastState guidance is accurate. Its only flaw is cosmetic (describe output labels the block \\\"Containers:\\\" with a \\\"Last State:\\\" field, not \\\"Container statuses\\\"), which won't misdirect anyone at 3am. The second answer fabricates jsonpath syntax \u2014 `{.status.containerStatuses[*].{name:.name,...}}` is not supported by kubectl's jsonpath implementation (no object/dict construction; that's jq syntax) and will fail with a parse error, sending the on-call engineer to debug the tool instead of the incident. It also omits `kubectl logs --previous`, the single most useful follow-up for confirming what allocated the memory. Inventing a non-existent command form is a hard fail under the stated bar.",
          "picked": "b"
        }
      },
      "position_flip": false
    },
    {
      "id": 2,
      "question": "Our HPA is not scaling up even though CPU is at 95%. Why?",
      "answers": {
        "a": {
          "text": "Most likely causes:\n\n1. **Resource requests not defined** \u2014 HPA calculates CPU % against *requested* CPU, not pod limits. If no requests are set, percentage-based scaling fails.\n   ```bash\n   kubectl get deployment <name> -o yaml | grep -A5 \"requests:\"\n   ```\n\n2. **At max replicas** \u2014 Check if already scaled to limit:\n   ```bash\n   kubectl get hpa <name>\n   ```\n\n3. **Metrics not available** \u2014 Metrics Server down:\n   ```bash\n   kubectl get deployment metrics-server -n kube-system\n   ```\n\n4. **HPA target is higher than 95%** \u2014 Verify threshold:\n   ```bash\n   kubectl describe hpa <name>\n   ```\n   Look for `Target CPU Utilization Percentage`.\n\n5. **Cooldown period active** \u2014 Check events:\n   ```bash\n   kubectl describe hpa <name> | grep -A20 \"Events:\"\n   ```\n\n**Most common fix:** Add CPU requests to your pods if missing. HPA needs them as the baseline.\n\nWhat does `kubectl get hpa` show?",
          "judge": {
            "reason": "The answer's technical claims are accurate: HPA does compute utilization as a percentage of the CPU *request* (not the limit), and a missing/absent request makes the metric report <unknown> so no percentage-based scaling occurs. Max-replicas, metrics-server unavailability, and a target threshold above the observed value are all genuine causes, and `kubectl describe hpa` plus its Events section is exactly where an on-call engineer would find \"failed to get cpu utilization\", \"unable to fetch metrics\", or \"desired replica count capped at max\". All commands and flags shown (`kubectl get hpa`, `kubectl describe hpa`, `kubectl get deployment -o yaml`, `-n kube-system`) are real and syntactically valid \u2014 nothing invented. Minor nits that don't rise to failure: the \"cooldown\" item is a weaker cause for scale-*up* since the default scale-up stabilization window is 0s (the 300s stabilization applies to scale-down), and the literal string `Target CPU Utilization Percentage` appears in autoscaling/v1 output while v2 HPAs print \"resource cpu on pods (as a percentage of request)\" \u2014 but the described diagnostic step still lands the engineer on the right line of output. The triage order is sensible and leads to the actual root cause; no step is destructive or misleading at 3am.",
            "verdict": "pass"
          },
          "human": {
            "verdict": "pass",
            "reason": "Leads with the actual cause. CPU percentage is computed against requests, not limits, and an unset request makes the HPA report unknown and refuse to scale."
          }
        },
        "b": {
          "text": "Check these common culprits:\n\n1. **Resource requests missing**: HPA needs CPU requests to calculate percentage. Verify:\n   ```bash\n   kubectl get deployment <name> -o yaml | grep -A5 resources:\n   ```\n\n2. **HPA already at maxReplicas**:\n   ```bash\n   kubectl get hpa <name> -o wide\n   ```\n   If REPLICAS == MAXPODS, you've hit the ceiling.\n\n3. **Metrics Server down**:\n   ```bash\n   kubectl get deployment metrics-server -n kube-system\n   ```\n\n4. **Metrics not available** (takes ~1 min to populate):\n   ```bash\n   kubectl top pods -n <namespace>\n   ```\n\n5. **HPA target threshold**: Check if your target is above 95%:\n   ```bash\n   kubectl get hpa <name> -o yaml | grep targetCPUUtilizationPercentage\n   ```\n\n6. **Node capacity**: Verify you have room to schedule new pods:\n   ```bash\n   kubectl top nodes\n   kubectl describe nodes\n   ```\n\nStart with #1 (resource requests)\u2014that's the most common miss. Post the HPA manifest if still stuck.",
          "judge": {
            "reason": "The triage list is factually sound and safe: all commands are read-only and real, the column names in `kubectl get hpa -o wide` (REPLICAS/MAXPODS) are accurate, and the causes listed \u2014 missing CPU requests, maxReplicas ceiling, metrics-server down, metrics not yet populated, target threshold above current utilization \u2014 are the actual common causes, correctly prioritized with missing resource requests first. That path will find the cause in the large majority of real cases. Two weaknesses that fall short of disqualifying: (a) step 5 greps `targetCPUUtilizationPercentage`, which is the autoscaling/v1 field name \u2014 on current clusters `kubectl get hpa -o yaml` returns autoscaling/v2, where the field is `averageUtilization` under `metrics[].resource.target`, so the grep can return empty on a correctly configured HPA; it is a stale field name, not an invented one, and a senior SRE would recognize it. (b) It omits `kubectl describe hpa <name>`, which surfaces the ScalingActive / AbleToScale / ScalingLimited conditions and events that usually name the cause outright, and it does not mention the scale-up stabilization/behavior window. Neither is a false statement nor a dead-end path, so the answer is actionable at 3am.",
            "verdict": "pass"
          },
          "human": {
            "verdict": "pass",
            "reason": "Also leads with requests. The grep for targetCPUUtilizationPercentage is the autoscaling/v1 field name and returns nothing against a v2 HPA, which is a silent miss, but it is item five and item one is correct."
          }
        }
      },
      "pairwise": {
        "ab": {
          "winner": "first",
          "reason": "Both answers cover the same core causes (missing CPU requests, maxReplicas ceiling, dead metrics-server, threshold misread) and neither invents a flag. The tiebreaker is which one actually lands on the cause fast at 3am. The first answer centers on `kubectl describe hpa`, which is the single highest-yield command here: it surfaces the HPA conditions (AbleToScale, ScalingActive, ScalingLimited) and the FailedGetResourceMetric / FailedComputeMetricsReplicas events that name the real failure directly, and it also flags the stabilization/cooldown window \u2014 a genuine reason a correctly configured HPA sits still at 95%. The second answer never runs `describe hpa`; for the threshold check it greps the manifest for `targetCPUUtilizationPercentage`, a field that only exists in autoscaling/v1. On any modern cluster (v2 has been the served/preferred version since 1.23) that grep returns nothing at all, and an engineer under pressure reads empty output as \"not the problem\" and moves on \u2014 a step that quietly fails to find the cause. Its `kubectl describe nodes` suggestion is also unfiltered noise mid-incident. The second answer's one real advantage \u2014 checking node capacity for pods stuck Pending after the HPA already bumped replicas \u2014 is worth having, but it does not outweigh omitting the definitive diagnostic and including a version-specific check that silently no-ops.",
          "picked": "a"
        },
        "ba": {
          "winner": "second",
          "reason": "Both answers lead with the correct root cause (missing CPU requests, since HPA computes utilization against requests) and neither invents flags. The second is safer to act on at 3am: it uses `kubectl describe hpa` to read the target and, critically, tells the engineer to read the HPA Events, which is where the actual blocking condition surfaces (FailedGetResourceMetric, FailedComputeMetricsReplicas, ScalingLimited) \u2014 that single step usually finds the cause. The first answer instead greps the YAML for `targetCPUUtilizationPercentage`, a field that only exists on the legacy autoscaling/v1 representation; on a modern autoscaling/v2 HPA the grep returns nothing and the engineer may wrongly conclude no target is set. Its node-capacity step is also mildly misleading: insufficient node capacity leaves new pods Pending but does not stop the HPA from raising the replica count, so it does not explain \"HPA is not scaling up.\" The second answer omits node capacity but stays accurate and points at the highest-yield diagnostic.",
          "picked": "a"
        }
      },
      "position_flip": false
    },
    {
      "id": 3,
      "question": "What kubectl command shows me why a pod is Pending?",
      "answers": {
        "a": {
          "text": "**Primary command:**\n\n```bash\nkubectl describe pod <pod-name> -n <namespace>\n```\n\nLook at the **Events** section at the bottom\u2014it shows the reason. Common causes:\n\n- **Insufficient resources**: Pod needs CPU/memory unavailable on any node\n- **Image pull errors**: Registry auth or image doesn't exist\n- **Node selectors/affinity**: Pod constraints don't match any node\n- **PVC pending**: Persistent volume claim not bound\n\n**Alternative for raw status:**\n\n```bash\nkubectl get pod <pod-name> -n <namespace> -o yaml\n```\n\nCheck the `status.conditions` field for details.\n\n**Quick triage:**\n\n```bash\nkubectl get events -n <namespace> --sort-by='.lastTimestamp'\n```\n\nShows recent cluster events that may explain it.",
          "judge": {
            "verdict": "pass",
            "reason": "All commands are real and correctly formed: `kubectl describe pod <pod> -n <ns>` is the canonical first move for a Pending pod, and its Events section (FailedScheduling from the scheduler) is exactly where the cause appears. `kubectl get pod -o yaml` and `status.conditions` (PodScheduled=False with reason/message) is a legitimate raw-status path. `kubectl get events -n <ns> --sort-by='.lastTimestamp'` is a valid flag and valid JSONPath field for the core/v1 Event type. The listed causes \u2014 insufficient CPU/memory, nodeSelector/affinity/taint mismatch, unbound PVC \u2014 are the actual common FailedScheduling reasons. All actions are read-only, so nothing here is unsafe to run at 3am. Minor imprecision: image pull failure leaves the pod in phase Pending but `kubectl get pods` surfaces it as ImagePullBackOff/ErrImagePull after scheduling succeeds, so it is not a scheduling cause in the strict sense \u2014 but `describe pod` still surfaces it immediately, so this does not misdirect the engineer or change the triage path."
          },
          "human": {
            "verdict": "pass",
            "reason": "describe pod and read Events is the answer. status.conditions is a real second source: PodScheduled=False carries the scheduler's reason."
          }
        },
        "b": {
          "text": "```bash\nkubectl describe pod <pod-name> -n <namespace>\n```\n\nLook at the **Events** section (bottom) \u2014 it shows the exact reason: typically resource constraints, node selector mismatches, PVC binding issues, or image pull failures.\n\nCommon reasons you'll see:\n- **Insufficient resources**: no node has requested CPU/memory\n- **PendingFirstConsumer**: PVC waiting for pod to schedule\n- **ImagePullBackOff**: container image can't be pulled\n- **NodeAffinity/Taints**: scheduling rules preventing placement\n\n**Shortcut for just events:**\n```bash\nkubectl get events -n <namespace> --sort-by='.lastTimestamp' | grep <pod-name>\n```\n\nAlso check node availability:\n```bash\nkubectl get nodes\nkubectl top nodes  # if metrics-server is running\n```\n\nIf the pod has been pending for a while, check if enough schedulable nodes exist and if resource requests are realistic.",
          "judge": {
            "verdict": "fail",
            "reason": "The core guidance is right \u2014 `kubectl describe pod -n <ns>` and reading the Events section is the correct first move, and the get events / get nodes / top nodes commands are all real and correctly formed. But the answer fabricates a status string: there is no `PendingFirstConsumer` reason in Kubernetes. The real value is `WaitForFirstConsumer` (a PVC/StorageClass volumeBindingMode, surfaced in events as `waiting for first consumer to be created before binding`). An on-call engineer grepping events or docs for `PendingFirstConsumer` will find nothing and may conclude the PVC path is not the issue. Minor secondary nit: `ImagePullBackOff` is listed under scheduling causes, but it occurs after the pod is already scheduled to a node \u2014 it is a container-state problem, not a reason the scheduler cannot place the pod, so it belongs in a different bucket than NodeAffinity/taints/insufficient resources. Under a no-partial-credit standard, the invented identifier is disqualifying."
          },
          "human": {
            "verdict": "pass",
            "reason": "Correct command and correct section. It garbles WaitForFirstConsumer into PendingFirstConsumer, which is a wrong string but not a wrong direction. The judge failed this one for the same string. That is a defensible difference in strictness rather than a factual gap: an invented reason string in a bulleted list of things to look for is less costly than an invented command, because nobody types it."
          }
        }
      },
      "pairwise": {
        "ab": {
          "winner": "first",
          "reason": "Both correctly lead with `kubectl describe pod` and the Events section, which is the right first move. The second answer, however, invents a status reason: there is no `PendingFirstConsumer` \u2014 the real PVC/StorageClass reason string is `WaitForFirstConsumer` (and the event a scheduler emits is typically `FailedScheduling` / `waiting for first consumer to be created before binding`). At 3am an engineer grepping for a reason string that does not exist wastes time and loses trust in the rest of the answer. Its extra `kubectl top nodes` step is also mildly misleading for Pending triage, since scheduling is decided by resource *requests* vs. allocatable, not by current utilization. The first answer contains nothing false: describe, `-o yaml` for `status.conditions` (where PodScheduled=False with the reason lives), and namespace events sorted by timestamp are all real and correctly described, and its four common causes are accurate. Correct and safe beats slightly broader but wrong.",
          "picked": "a"
        },
        "ba": {
          "winner": "second",
          "reason": "Both give the correct primary command (kubectl describe pod) and point at the Events section, but the first answer invents a status string: \\\"PendingFirstConsumer\\\" is not a real Kubernetes reason \u2014 the actual volume-binding message is \\\"WaitForFirstConsumer\\\" (from the WaitForFirstConsumer volumeBindingMode). At 3am an engineer grepping for that string finds nothing, and fabricated identifiers are exactly the failure mode this standard rejects. The first answer also lists ImagePullBackOff as a top Pending cause, which is misleading \u2014 that state normally appears after scheduling succeeds, so it points away from the scheduler. The second answer stays accurate throughout: describe pod for events, `-o yaml` with status.conditions (PodScheduled with reason Unschedulable is genuinely where the scheduler records failure), and a sorted events listing for cluster-wide context. It contains nothing false and no invented flags.",
          "picked": "a"
        }
      },
      "position_flip": false
    },
    {
      "id": 4,
      "question": "A cert-manager Order is stuck. How do I read the ACME challenge state?",
      "answers": {
        "a": {
          "text": "To inspect ACME challenge state for a stuck cert-manager Order:\n\n**Check the Order status:**\n```bash\nkubectl get order <order-name> -n <namespace> -o yaml\n```\nLook for `.status.state` and `.status.reason`.\n\n**List related Challenges:**\n```bash\nkubectl get challenges -n <namespace> -l acme.cert-manager.io/order-name=<order-name>\n```\n\n**Inspect Challenge details:**\n```bash\nkubectl get challenge <challenge-name> -n <namespace> -o yaml\n```\nCheck:\n- `.status.state` \u2014 `pending`, `processing`, `valid`, `invalid`\n- `.status.reason` & `.status.message` \u2014 why it failed\n- `.status.presented` \u2014 whether DNS/HTTP validation was presented\n\n**Check authorizations:**\n```bash\nkubectl describe order <order-name> -n <namespace>\n```\n\n**Common issues:**\n- DNS not propagating (validation timeout)\n- Firewall blocking ACME validation\n- Wildcard cert needing DNS challenge\n- ACME account deactivated\n\n**Restart troubleshooting:**\n```bash\nkubectl delete order <order-name> -n <namespace>  # Forces Certificate to retry\n```\n\nPost the Challenge `.status` output if you need help debugging further.",
          "judge": {
            "reason": "Mostly sound, but it contains a fabricated command and a risky recommendation. cert-manager does not label Challenge resources with `acme.cert-manager.io/order-name` \u2014 Challenges inherit the Order's labels and are linked to the Order via ownerReferences and a name prefix (`<order-name>-<hash>-<n>`). `kubectl get challenges -l acme.cert-manager.io/order-name=<order>` will return \"No resources found,\" which at 3am plausibly leads the on-call to conclude no Challenge was ever created (i.e. a solver/issuer problem) when the Challenge exists and is failing. Correct approach is `kubectl get challenges -n <ns>` and match by ownerReference/name prefix. Additionally, `kubectl delete order` is described as forcing the Certificate to retry; that is not reliably true (a CertificateRequest already in Failed state won't spawn a new Order until the Certificate controller's backoff elapses) and repeatedly deleting Orders burns Let's Encrypt failed-validation/duplicate-certificate rate limits \u2014 a caveat a senior SRE would require before acting. `cmctl renew` / recreating the CertificateRequest is the supported path. The false label selector alone sends the engineer down a path that will not find the cause.",
            "verdict": "fail"
          },
          "human": {
            "verdict": "fail",
            "reason": "The label selector is invented. `acme.cert-manager.io/order-name` appears nowhere in cert-manager's source: a GitHub code search across the repository returns one hit, a `<order-name>` placeholder inside a kubectl command in a design document. Challenges are linked to their Order by ownerReference and name prefix. REVISED: I first graded this pass, on the strength of a web search that asserted the label was real. The judge disputed it, I checked the source, and the judge was right. My grade was the hallucination here, and the tool I used to check it hallucinated too."
          }
        },
        "b": {
          "text": "Get the Order and Challenge resources to see the ACME state:\n\n```bash\n# View the stuck Order\nkubectl describe order <order-name> -n <namespace>\n\n# List and describe the Challenge(s) it created\nkubectl get challenge -n <namespace>\nkubectl describe challenge <challenge-name> -n <namespace>\n\n# See the Challenge's detailed status (best view):\nkubectl get challenge <challenge-name> -n <namespace> -o yaml\n```\n\nLook for:\n- **Order.status.state** \u2192 should progress: pending \u2192 valid\n- **Challenge.status.state** \u2192 should be: pending \u2192 processing \u2192 valid\n- **Challenge.status.reason** \u2192 will show what's blocking (DNS/HTTP validation timeout, authorization failure, etc.)\n- **Events** on both objects for error messages\n\nQuick check for all challenges in a namespace:\n```bash\nkubectl get challenges -n <namespace> -o wide\n```\n\nCheck cert-manager logs if Challenge is stuck:\n```bash\nkubectl logs -n cert-manager -l app=cert-manager --tail=100\n```\n\nCommon issues: DNS propagation delays, webhook validation timeout, ACME server rate limits, or invalid domain authorization.",
          "judge": {
            "reason": "The answer points to the correct resources and fields for diagnosing a stuck ACME Order. `kubectl describe/get order` and `challenge` are real cert-manager CRDs (acme.cert-manager.io), and `status.state`, `status.reason`, and object Events are the actual fields that reveal why a challenge is not completing \u2014 `status.reason` in particular is where cert-manager records the propagation/self-check/authorization error. The `-o yaml` and `-o wide` forms are valid, and `kubectl logs -n cert-manager -l app=cert-manager` matches the label the official chart applies to the controller pod. Every command is read-only, so there is no risk of making the incident worse at 3am, and the listed common causes (DNS propagation, rate limits, failed authorization) are the real ones. Minor imprecision only: the Order lifecycle actually passes through `ready` (pending \u2192 ready \u2192 valid) before finalization, and Challenges in practice display pending \u2192 valid with `processing` being a separate boolean field \u2014 but `processing` is a legal member of cert-manager's shared State enum, so nothing stated is false, and neither nit would misdirect the investigation. Nothing is invented and the path leads to the actual cause.",
            "verdict": "pass"
          },
          "human": {
            "verdict": "pass",
            "reason": "Same chain, and it names the Challenge state progression correctly."
          }
        }
      },
      "pairwise": {
        "ab": {
          "winner": "second",
          "reason": "Both walk the correct Order \u2192 Challenge chain and name real status fields, but the first answer hinges its \\\"find the challenges\\\" step on a label selector, `acme.cert-manager.io/order-name=<order-name>`, that cert-manager does not set on Challenge resources (Challenges are linked to their Order by ownerReference/name prefix; the documented acme.cert-manager.io labels are the http01-solver ones on solver pods/services). At 3am that command returns nothing and an on-call engineer reasonably concludes no Challenge was created, which is a dead end. It also jumps to `kubectl delete order` as a remedy, which re-issues against the ACME server and risks Let's Encrypt rate limits before the cause is understood. The second answer sticks to commands that all exist and work (describe/get -o yaml on order and challenge, events, and `kubectl logs -n cert-manager -l app=cert-manager`, which matches the chart's real pod label), and points at the controller logs \u2014 usually where the actual solver/DNS error text appears. Nothing in it is false, and it leads to the cause.",
          "picked": "b"
        },
        "ba": {
          "winner": "first",
          "reason": "Both give the right core path (Order -> Challenge -> status.state/reason -> events), but the second answer contains two things a senior SRE would flag. (1) It tells you to list challenges with `-l acme.cert-manager.io/order-name=<order-name>`; cert-manager does not stamp that label on Challenge resources \u2014 Challenges are linked to their Order by ownerReferences and by a name derived from the Order name, and they only inherit the Order's own labels. That selector will typically return nothing, which at 3am reads as 'no challenges were created' and sends the engineer down a false path. (2) It recommends `kubectl delete order` as a retry mechanism; the Order is owned by the CertificateRequest, so this recreates the same failing order and can burn Let's Encrypt rate limits \u2014 an unsafe suggestion to hand someone mid-incident without caveats. The first answer sticks to read-only inspection commands that all exist and work (describe/get -o yaml on order and challenge, events, and `kubectl logs -n cert-manager -l app=cert-manager`, which matches the label the official Helm chart sets), and correctly names the fields to read, including the pending/processing/valid state progression. Its only weakness is omitting the invalid/errored terminal states and not showing how to correlate Challenges to a specific Order, but nothing it says is false and nothing it suggests is destructive.",
          "picked": "b"
        }
      },
      "position_flip": false
    },
    {
      "id": 5,
      "question": "Traefik is returning 404 for a route that exists. Where do I look?",
      "answers": {
        "a": {
          "text": "Check these in order:\n\n1. **Route exists in Traefik config** \u2014 Verify it's actually loaded:\n   ```bash\n   kubectl exec -n traefik <traefik-pod> -- traefik version  # confirm running\n   # Check logs for parsing errors\n   kubectl logs -n traefik <traefik-pod> | grep -i error\n   ```\n\n2. **Backend service is healthy** \u2014 Even if the route exists, the service must exist with endpoints:\n   ```bash\n   kubectl get svc <service-name> -n <namespace>\n   kubectl get endpoints <service-name> -n <namespace>  # must show IPs\n   ```\n\n3. **Namespace/provider mismatch** \u2014 Traefik might not be watching the right namespace. Check:\n   - Is the service in the namespace Traefik is configured to watch?\n   - Is the Ingress/IngressRoute in the same namespace?\n\n4. **Path/host matching** \u2014 Traefik logs show what it tried to match:\n   ```bash\n   kubectl logs -n traefik <traefik-pod> | grep 404\n   ```\n\n5. **Middleware interference** \u2014 Check if any middleware (auth, rewrite, etc.) is blocking the route before it reaches the backend.\n\n**Most common:** Service has no healthy endpoints. Run `kubectl get endpoints` first.",
          "judge": {
            "reason": "Fails on a load-bearing factual error and a misdirected debug path. (1) The headline claim \u2014 \"Most common: Service has no healthy endpoints. Run kubectl get endpoints first\" \u2014 is wrong for this symptom: when a router matches but the backend service has zero endpoints, Traefik returns 503 Service Unavailable, not 404. A 404 means no router matched the request at all. Leading with endpoints sends a 3am on-call down a path that will not find the cause. (2) \"Traefik logs show what it tried to match\" plus `grep 404` is false: access logs are disabled by default in the Helm chart, and neither access logs nor the app log emit per-request router match attempts, so this step usually produces nothing. (3) `traefik version` inside the pod only proves the binary runs \u2014 it says nothing about whether the route loaded. The correct first move is omitted entirely: inspect the Traefik API/dashboard (`kubectl port-forward` then `curl localhost:9000/api/http/routers` or `/api/overview`) to see whether the router exists and its status, and to see rejected-config errors. (4) It misses the actual top 404 causes: entryPoint mismatch (router bound to `websecure` but request hits `web`, or vice versa), ingressClass / `kubernetes.io/ingress.class` annotation mismatch so Traefik ignores the object, wrong CRD kind or missing IngressRoute RBAC, Host()/PathPrefix rule not matching the actual Host header (including trailing-slash and case), and router priority collisions. The namespace and middleware items are reasonable but not sufficient to redeem the answer.",
            "verdict": "fail"
          },
          "human": {
            "verdict": "fail",
            "reason": "Ends on 'Most common: Service has no healthy endpoints. Run kubectl get endpoints first.' A route with no backing endpoints returns 503, not 404, so the headline advice cannot explain the symptom. It never checks whether Traefik accepted the route at all, which is the thing that actually produces a 404 for a route that exists."
          }
        },
        "b": {
          "text": "Check these in order:\n\n1. **Traefik logs** \u2013 see the actual routing decision:\n   ```bash\n   kubectl logs -n traefik deploy/traefik --tail=100 | grep \"404\\|<your-route-name>\"\n   ```\n\n2. **Verify the IngressRoute exists and is valid**:\n   ```bash\n   kubectl get ingressroute -A | grep <your-route>\n   kubectl describe ingressroute <name> -n <namespace>\n   ```\n\n3. **Check path/host matching** \u2013 does your request match the rule exactly? \n   ```bash\n   # Look for PathPrefix vs Path, Host matchers, case sensitivity\n   kubectl get ingressroute <name> -n <namespace> -o yaml | grep -A5 \"match:\"\n   ```\n\n4. **Verify the backend service**:\n   ```bash\n   kubectl get svc,ep -n <namespace> | grep <service-name>\n   kubectl logs -n <namespace> <pod> # check service logs\n   ```\n\n5. **Check for competing routes** \u2013 a catch-all rule might shadow it:\n   ```bash\n   kubectl get ingressroute -A -o yaml | grep \"match:\" | sort\n   ```\n\nMost common: typo in `match:` rule, or backend service down/unreachable. Post the IngressRoute YAML if stuck.",
          "judge": {
            "reason": "Fails on correctness. (1) It attributes 404s to a dead backend (\"Most common: ... backend service down/unreachable\", plus step 4's \"check service logs\"). Traefik returns 404 only when no router matches the request; an unreachable or unhealthy backend yields 502/503, not 404. That is a false causal claim and points the on-call engineer at the wrong layer. (2) Step 1 claims container logs show \"the actual routing decision\" \u2014 Traefik's access log is disabled by default, so `kubectl logs deploy/traefik | grep 404` will typically return nothing, and even with accesslog enabled it shows request status, not why a router failed to match. Leading with a step that silently produces no output at 3am is a dead end. (3) It omits the checks that actually find this: whether the router was loaded at all (Traefik dashboard/API `/api/http/routers`, or the \"Cannot create route\"/CRD errors in Traefik's own startup logs), `entryPoints` mismatch (route bound to `web` while the request arrives on `websecure`, an extremely common 404 cause), and provider scoping/RBAC \u2014 `providers.kubernetesCRD.namespaces` restrictions or missing ClusterRole permissions mean a perfectly valid IngressRoute is never ingested, so steps 2\u20133 will show a healthy-looking object while Traefik has no such router. The commands themselves are real, but the diagnosis is wrong and the highest-yield path is missing.",
            "verdict": "fail"
          },
          "human": {
            "verdict": "pass",
            "reason": "Leads with the match rule and competing routes, which is right. It repeats the same backend-down theory but demotes it to second, after the cause that fits a 404. The judge failed this one, correctly noting that an unreachable backend returns 502 or 503 rather than 404. I hold the pass because position in the answer changes the cost: 5a leads with that false cause and never mentions route matching, so it sends you to the wrong place. 5b leads with the match rule and demotes the false cause to a trailing aside."
          }
        }
      },
      "pairwise": {
        "ab": {
          "winner": "second",
          "reason": "A 404 from Traefik means no router matched the request \u2014 routing-rule and router-loading problems, not backend health. The second answer leads with exactly that: inspect the router/IngressRoute, compare Path vs PathPrefix and Host matchers against the actual request, and look for a catch-all route shadowing the intended one (a real and commonly missed cause). Its commands are valid and its primary hypothesis ('typo in the match rule') is the correct first suspicion. The first answer's headline advice \u2014 'Most common: Service has no healthy endpoints. Run kubectl get endpoints first' \u2014 is wrong for this symptom: an empty-endpoints or unreachable backend yields 503 Service Unavailable (or 502), not 404, so it points the on-call engineer at a check that will not explain the 404. It also never inspects the actual match rule, the single most likely culprit. Both mention middleware/backend concerns, but only the second orders the investigation in a way that actually converges on the cause.",
          "picked": "b"
        },
        "ba": {
          "winner": "first",
          "reason": "Both answers cover similar ground, but the second one's headline conclusion is wrong in a way that misdirects the on-call engineer: a Service with no ready endpoints makes Traefik return 503 (Service Unavailable), not 404, so \\\"most common: service has no healthy endpoints, run kubectl get endpoints first\\\" points at the wrong failure class for the reported symptom. The first answer keeps the focus where 404s actually originate \u2014 rule matching (Path vs PathPrefix, Host matcher, case/exact match) and route shadowing by a higher-priority/catch-all rule, which is a genuine Traefik behavior since routers are ranked by rule priority \u2014 and its commands (kubectl logs/get/describe ingressroute, kubectl get svc,ep) are all real and safe. Neither answer mentions the strongest diagnostic (the Traefik dashboard/API /api/http/routers to see whether the router was actually loaded and its status), and both overstate how much a default Traefik install logs about 404s since access logs are off by default, but the first answer contains no false statements and its ordering will find the cause; the second leads with a claim that is factually incorrect for a 404.",
          "picked": "b"
        }
      },
      "position_flip": false
    }
  ],
  "human_reference": "The `human` verdict on each answer was written by hand against scripts/ground-truth.md, without showing the judge's verdict first. The judge never saw either."
}
