Skip to content

evals: plugin eval with-arm runs end at 0 turns when a skill's ! pre-compute line fails in the sandbox, and the validity gate still says VALID #9514

Closed

Description

@kyle-sexton

What happened

claude plugin eval (Claude Code 2.1.309, WSL2, bubblewrap and socat present, --scaffold --keep-temp --runs 3 --allow-tools Bash,Write,Edit) ran harness-ops' two morning-brief cases (mb-issue-body-is-data, mb-unreadable-section-stops) twice: once against main's plugin and once against a branch.

  • Every with-arm run, in both passes, ended at 0 turns with costUsd: 0 and error: null. The kept trace shows the cause. The morning-brief skill's ! pre-compute line (command -v gh ...) failed before the first turn with zsh:1: read-only file system: <sandbox>/tmp/claude-1000/cwd-<n>. The shell writes its cwd-tracking file under the sandbox's tmp, which is read-only, so the skill never ran. Every without-arm run completed normally.
  • skills/plugin-eval/scripts/run-validity.py still printed verdict: VALID. Its only warning was skill fired. Both arms scored 0.50 with delta +0.00, which looks like a null result rather than a broken run.

Expected

  • A skill whose ! lines run in the eval sandbox executes. If it cannot, the run is reported as an error rather than a 0-turn success.
  • The validity gate marks a run INVALID, or at least warns, when with-arm runs end at 0 turns or 0 cost while the without-arm runs do not.

Suggested fix location

  • plugins/evals/skills/plugin-eval/scripts/run-validity.py: add a check for 0-turn or 0-cost runs in one arm.
  • plugins/evals/skills/plugin-eval/SKILL.md § Gotchas, or the sandbox notes: record that a skill whose ! pre-compute lines run under a shell that writes to the sandbox tmp ends at 0 turns. UNVERIFIED whether this is upstream (the eval sandbox's tmp being read-only for the shell) or specific to this host's login shell. Probe it with SHELL=/bin/bash.

Impact

The skill-criteria rule ("a cut to an existing skill ships only when its evals show no loss") cannot be met for skills with ! lines on such a host. The gate currently presents the null result as valid.

Found during G6 part 2 (#9494).

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 23, 2026
  2. github-actions commented on Oct 23, 2026

    @github-actions
    Contributor

    Proposed work class: work-class: scoped
    Basis: automated intake triage; the lanes App applied this class.

    Size: Small
    Complexity: Low
    Uncertainty: Knowable
    Flags: none

    Understood: when a skill's ! pre-compute line fails in the eval sandbox, with-arm runs end at 0 turns with costUsd: 0 and error: null. run-validity.py still prints VALID. The report asks for two changes. First, the validity gate should flag with-arm runs at 0 turns or $0 cost. Second, the SKILL.md Gotchas should record the cause and the unverified SHELL=/bin/bash probe.

    Still to settle by reading a real result file: the exact field names for turns and cost. The check also must not duplicate the existing run-error check. Whether to FAIL or only warn looks like an implementation choice. The issue prefers INVALID, so I took that as the default.

    Related issues: #9414, #7431, #8378. I found no duplicate.

    Automated triage, applied by the lanes App.

  3. added
    priority: mediumReal value, no hard deadline; normal backlog flow.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 23, 2026
  4. added
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.
    status: needs-decisionAwaiting a human or maintainer judgment call.
    and removed
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    on Oct 23, 2026
  5. melodic-automation-lanes commented on Oct 23, 2026

    @melodic-automation-lanes
    Contributor

    Not admitted to the issue lane: no trusted-human acceptance criteria: a listed human must put the confirmed - [ ] criteria in the body or post them as their own comment, or, on an issue a listed human filed, intake's newest refinement comment must hold them unedited. Admission takes the issue once each of those is met, when intake triages it again (.github/actions/resolve-config/admission-rules.json).

  6. melodic-automation-lanes commented on Oct 23, 2026

    @melodic-automation-lanes
    Contributor

    Refinement. A read-only run explored this repository, searched its issues and pull requests, and read the allowed docs to draft what follows. It is model text, to check, not instructions.

    Intended outcome: run-validity.py gains a check that fails or warns when a with-arm run ends at 0 turns or $0 cost while the same case's without-arm runs do not, and SKILL.md Gotchas records the ! pre-compute and read-only sandbox tmp cause.

    Related work

    Open questions

    1. Should a with-arm run at 0 turns or $0 cost with a without-arm that did not make the gate INVALID (FAIL) or only warn?
      RECOMMENDED: FAIL: a 0-turn with-arm run never ran the skill, so its score is not a measurement, and the issue's expected section allows a warning only as a fallback. Basis: judgment
    2. Which fields in the aggregate result or trace hold the turn count and cost for a run, and which exact names should the check read?
      RECOMMENDED: Read cost from the row's costUsd (named in the issue) and the turn count from the trace; the field names must be confirmed against a real result file before coding, since run-validity.py reads neither today. Basis: plugins/evals/skills/plugin-eval/scripts/run-validity.py:59
    3. Should this issue also fix the root cause (a ! line failing under the sandbox's read-only tmp), or only the gate and a Gotchas note?
      RECOMMENDED: Gate and Gotchas note only. The root cause is unverified as upstream or host-specific, and the sandbox is Claude Code's, not this repo's. Record the SHELL=/bin/bash probe as an unverified lead in the note. Basis: judgment
    4. Should a 0-turn run also count when both arms are at 0 turns, or only when the arms differ?
      RECOMMENDED: Flag any with-arm run at 0 turns or $0 with error null, whether or not the without-arm differs. That includes the credential-store case the SKILL.md already describes, which ends with a run error, so the new check must not duplicate check_errors. Basis: plugins/evals/skills/plugin-eval/SKILL.md:83

    Sources read

    • /home/runner/work/_temp/refine-data/context.json: issue 9514 and intake triage; no maintainer comments, no Decision recorded
    • plugins/evals/skills/plugin-eval/scripts/run-validity.py:11-29: the existing checks (run errors, permission denials, skill fired, models, judge votes, ceiling); none looks at turns or cost
    • plugins/evals/skills/plugin-eval/scripts/run-validity.py:728-773: checks are assembled in main(), a FAIL or UNCHECKED gives INVALID and a WARN only adds to the VALID line
    • plugins/evals/skills/plugin-eval/SKILL.md:447-473: Gotchas section has no entry on 0-turn runs or ! pre-compute lines
    • plugins/evals/skills/plugin-eval/SKILL.md:83: an existing note that a credential-store refusal ends every run at 0 turns, with a run error
    • grep of plugins/evals for turns and costUsd found no use of either in run-validity.py

    Automated refinement by the lanes App. Answer with a comment whose first line is **Decision recorded.**.

  7. kyle-sexton commented on Oct 24, 2026

    @kyle-sexton
    MemberAuthor

    Released for re-triage under the owner's 2026-10-24 rule: an issue stops for a person only for an intent or vision question, a security call, a host or attended run, or spend outside prepaid credit. The hold label came off and intake triage runs again now; it re-applies a hold, with a numbered question, when one of those applies.

    To hold this issue again, add needs-human (or status: needs-decision) and say why in a comment.

  8. added
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    status: needs-decisionAwaiting a human or maintainer judgment call.
    and removed
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    on Oct 24, 2026
  9. melodic-automation-lanes commented on Oct 24, 2026

    @melodic-automation-lanes
    Contributor

    Refinement. A read-only run explored this repository, searched its issues and pull requests, and read the allowed docs to draft what follows. On an issue a listed human filed, it counts as the issue's spec once posted, while unedited; it is still model text, so check it.

    Intended outcome: run-validity.py marks a run INVALID when a case's with-arm runs end at 0 turns or $0 cost while its without-arm runs do not, and the plugin-eval SKILL.md Gotchas record the cause and the unverified SHELL probe.

    Acceptance criteria

    • plugins/evals/skills/plugin-eval/scripts/run-validity.py has a new check, listed in the module docstring, that reads each run row's turns and costUsd (field names as in test_noise_report.py:43-44) and keeps them in the constants block near line 81
    • The check returns FAIL, naming the case and run via where(), when a with-arm run has turns 0 or costUsd 0 and error null, while at least one without-arm run of the same case has turns above 0 and costUsd above 0
    • The check does not double-report a run that already has a non-null error, because check_errors (run-validity.py:328) covers it
    • The check does not fire when both arms are at 0 turns, or when the suite's ablation is none so there is no without-arm to compare, and it tolerates a missing turns or costUsd field without raising
    • The check is added to the list in main() beside the run errors check (run-validity.py:773-774), so a FAIL makes the last line verdict: INVALID with exit code 1
    • A unit test beside the existing scripts covers a with-arm run at 0 turns with error null against a normal without-arm (INVALID), a normal result (VALID), both arms at 0 turns (no new FAIL), and a with-arm run with a non-null error (reported once)
    • plugins/evals/skills/plugin-eval/SKILL.md § Gotchas (from line 447) gains one bullet: a skill whose ! pre-compute line fails in the sandbox (zsh: read-only file system on the cwd-tracking file under the sandbox tmp) ends with-arm runs at 0 turns, costUsd 0 and error null, and the validity gate now fails this
    • The new Gotchas bullet marks the cause as UNVERIFIED (upstream read-only sandbox tmp, or this host's login shell) and names SHELL=/bin/bash as the probe to try, without claiming it fixes the problem
    • Running the existing run-validity tests and the repo's plugin validation passes

    Dependencies

    • #​9611 (run-validity calls runs VALID when every Bash call failed to start the sandbox) edits the same script and may land first; rebase on it and keep the two checks distinct

    Related work

    Sources read

    • plugins/evals/skills/plugin-eval/scripts/run-validity.py:328: check_errors only fails on a non-null error, an aborted run, or an error in the trace, so a 0-turn run with error null passes
    • plugins/evals/skills/plugin-eval/scripts/run-validity.py:61-90: result-file field constants; none read turns or costUsd
    • plugins/evals/skills/plugin-eval/scripts/run-validity.py:773: the check list in main() where the new check goes
    • plugins/evals/skills/plugin-eval/scripts/test_noise_report.py:43: run rows carry turns and costUsd
    • plugins/evals/skills/plugin-eval/SKILL.md:83: an existing note that a credential-store refusal ends every run at 0 turns, but with a run error
    • plugins/evals/skills/plugin-eval/SKILL.md:447: the Gotchas section where the new bullet goes

    Automated refinement by the lanes App. Admission counts only criteria a listed human wrote: to confirm these, post them as your own comment whose first line is **Decision recorded.**, which sends the issue back through intake.

  10. kyle-sexton commented on Oct 25, 2026

    @kyle-sexton
    MemberAuthor

    Probe data (Claude Code 2.1.310, WSL2, bubblewrap and socat present), harness-ops mb-* cases, --ablation none --runs 1:

    Run setting with-arm turns / costUsd / error First-turn stderr from the trace
    SHELL=/usr/bin/zsh, default TMPDIR 0 / 0 / null (both cases) zsh:1: read-only file system: <sandbox>/tmp/claude-1000/cwd-7cd7
    SHELL=/bin/bash, default TMPDIR 0 / 0 / null (both cases) /bin/bash: line 1: <sandbox>/tmp/claude-1000/cwd-8a5d: Read-only file system
    TMPDIR = a long path under the repo's .work/ 0 / 0 / null (both cases) Sandbox is required but failed to initialize: Failed to create bridge sockets after 5 attempts
    TMPDIR = /run/user/1000/ev (short, outside /tmp) 5 and 3 turns, 0.33 and 0.28 USD, null none; both cases ran

    What the data shows:

    With the workaround, the #9494 eval re-ran with --runs 3 --ablation none, comparing the plugin before the merge commit with the plugin at it. Both runs were verdict: VALID. With-arm means were 0.875 before and 0.90 after, and no case dropped. The noise report's compare verdict is n too small to call (2 cases).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority: mediumReal value, no hard deadline; normal backlog flow.status: needs-decisionAwaiting a human or maintainer judgment call.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions