fix(criteria): search the whole command in command_executed and bump harness SDKs for Opus 5.5 - #196
Conversation
… the first 2000 chars The ReDoS bound truncated every command to its first 2000 characters, so a checked command at the end of a long heredoc script was scored as never run. Search the rest of the command in bounded, line-aligned windows instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- claude-agent-sdk 0.2.124 -> 0.2.159 (bundles Claude Code 2.1.281) and the image's Claude Code 2.1.177 -> 2.1.281, so the CLI the SDK runs and the one reported in environment_info agree. 2.1.281 knows Opus 5.5 and the current Sonnet 5 price, so claude-code turn costs match list pricing again. - openai-codex / openai-codex-cli-bin 0.144.4 -> 0.156.1 - google-antigravity 0.1.8 -> 0.1.18: workspace containment moved into localharness, so policy.workspace_only() no longer carries a path predicate; the test now checks the resolved workspaces cover the skill roots. - Pi 0.84.4 -> 0.87.1, anthropic 1.0.0 -> 1.8.0, litellm 1.98.0 -> 1.102.1, harbor 0.22.0 -> 0.23.0, uipath 2.10.31 -> 2.14.25 - Ignore pydantic's ReadOnly TypedDict warning from litellm>=1.102 types, and regenerate the codex golden for the two null fields the new SDK serializes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
harbor 0.23.0 annotates BaseAgent.SUPPORTS_ATIF as ClassVar[bool], so the plain bool override fails pyright's reportIncompatibleVariableOverride. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gate CI's safe-chain rejects any package published less than 48 hours ago, so every Linux job failed at install. Relocked with every package cut off at 2026-09-21T23:00Z: claude-agent-sdk 0.2.157 (bundles Claude Code 2.1.277), anthropic 1.7.0, openai-codex and cli-bin 0.155.1, google-antigravity 0.1.17, litellm 1.102.0, uipath 2.14.23; Claude Code image ARG 2.1.277 and Pi 0.87.0. Claude Code 2.1.277 prices Sonnet 5 at list but has no Opus 5.5 entry, so Opus 5.5 runs still cost at its $5/$25 fallback until the next bump to 2.1.280 or later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d take the latest SDK CI's safe-chain holds back any package younger than 48h. claude-agent-sdk bundles the Claude Code CLI, and new-model support (Opus 5.5 pricing landed in 2.1.280) ships there faster than that window, so it joins openai-codex in the exclusion list. The repo secret SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS is set and replaces the literal fallback, so the entry is appended outside the `||` in pr-checks, release and publish-testpypi. The image ARG lists get it too. Lock: claude-agent-sdk 0.2.159 (bundles Claude Code 2.1.281, ships a win_amd64 wheel, which fixes the Windows smoke), openai-codex and cli-bin 0.156.1 (already exempt), google-antigravity 0.1.18 (image-only install, not gated), anthropic 1.8.0, uipath 2.14.24. litellm stays at 1.102.0 and harbor at 0.23.0, the newest that clear the gate. Image ARGs: Claude Code 2.1.281, Pi 0.87.1. The pyproject floors for claude-agent-sdk and openai-codex stay at 0.2.157 and 0.155.1: skills and coder_eval_uipath CI do not exempt these packages, so a higher floor would break their installs of the next release until the new versions age in. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…or E2E Harbor E2E installs the locked deps on the same safe-chain pool but carried no exclusion list, so it blocked claude-agent-sdk 0.2.159. It now uses the same workflow-level expression as pr-checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @bai-uipath's task in 1m 25s —— View job Code Review for PR #196: Fix command_executed criterion and update harness SDKsReview Checklist:
|
uipreliga
left a comment
There was a problem hiding this comment.
Review: coder_eval — pr:196 (branch vs origin/main, 14 files)
PR #196 — fix(criteria): search the whole command in command_executed and bump harness SDKs for Opus 5.5 (author: bai-uipath, base: main)
Change class: complex — changes the command_executed search algorithm (single leading window → multi-window logical-line packing), which alters verdicts of min_count/max_count/exclude_pattern gates, plus harness SDK / CLI pin bumps and CI safe-chain exclusions
The PR is in good shape: types, contracts and tests are clean, and no finding is critical or high. The real risk is that the new windowed whole-command search in command_executed.py can quietly give a different verdict past 2000 characters. Quoted calls inside bash -lc wrappers are missed, anchors match at window edges, and a match that crosses a window boundary is dropped. Fix the window semantics and add the user-facing migration note before merge.
Summary
| Axis | Score | 🔴 | 🟠 | 🟡 | 🔵 | Top Issue |
|---|---|---|---|---|---|---|
| 1. Correctness & Logic | 9.4 / 10 | 0 | 0 | 1 | 1 | Regex anchors (^, $) match at window edges past the 2000-char cap, so anchored patterns mean something different in long commands |
| 2. Code Quality & Simplicity | 9.7 / 10 | 0 | 0 | 0 | 3 | _search_windows builds packed windows from offset 0, then discards them and prepends a raw leading slice; the reason is not stated |
| 3. Types & Contracts | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 4. Test & Validation Health | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 5. Security | 9.9 / 10 | 0 | 0 | 0 | 1 | Search-window fan-out makes regex work scale with agent-controlled command length, and the early-stop watcher repeats that work on every tool-call event |
| 6. Architecture & Design | 9.4 / 10 | 0 | 0 | 1 | 1 | Fourth hand-synced copy of the SAFE_CHAIN exclusion expression, with no single source or parity check |
| 7. Error Handling & Resilience | 9.5 / 10 | 0 | 0 | 1 | 0 | Quote normalization is off in later windows of a wrapped bash -lc command, and no test covers normalization of later windows |
| 8. Interface, Docs & Compatibility | 9.4 / 10 | 0 | 0 | 1 | 1 | The user guide does not record that command_executed now changes verdicts past 2000 chars (max_count/exclude_pattern can flip pass to fail) |
Overall Score: 9.7 / 10 · Weakest Axis: Correctness & Logic at 9.4 / 10
Totals: 🔴 0 · 🟠 0 · 🟡 4 · 🔵 7 across 8 axes reviewed.
Blockers (0 🔴 Critical · 0 🟠 High)
None.
Non-blocking, but please consider before merge
- [Axis 1] Regex anchors (^, $) match at window edges past the 2000-char cap, so anchored patterns mean something different in long commands (
src/coder_eval/criteria/command_executed.py:131) — Keep the anchors bound to the real command. Search the raw text as pattern.search(cmd_text, s, e) for each window (s, e), not on the slice cmd_text[s:e]. Without MULTILINE, '^' then matches only at the true start of the string, and endpos still bounds the ReDoS work. That fixes '^' but not '$', because '$' still matches at endpos. To also bind '$', search only the last window (e == len(cmd_text)) with endpos, or put each window's end on a real line boundary and document that '$' can match there. Apply the same treatment to the normalized haystacks. They are new strings built per window, so either normalize only the window at offset 0 or document that anchors on normalized forms refer to the start and end of the window. Add tests that pin ^git$ (and a '$'-anchored pattern) against a command longer than 2000 characters whose bare 'git' comes after the cap, for both command_pattern and the exclude_pattern / max_count=0 gate. No shipped task under tasks/ or experiments/ uses an anchored pattern, so this is Medium. - [Axis 6] Fourth hand-synced copy of the SAFE_CHAIN exclusion expression, with no single source or parity check (
.github/workflows/harbor-e2e.yml:33) — The same expression is now in four workflows (harbor-e2e.yml:33, pr-checks.yml:46, publish-testpypi.yml:41, release.yml:78), and nothing checks it. The follow-up commit af239b9 was needed because harbor-e2e had NO copy, not because two copies disagreed. So a guard must catch a missing copy as well as a different one. Two ways to fix this. (a) Move the whole value, including claude-agent-sdk, into one repo-level source, for example a repo variablevars.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONSor a small composite action that exports it, and reference that source from every workflow. (b) Add a doc-surface lint class in tests/test_custom_lint.py with two checks. First, every workflow that runsuv sync/uv pip installon auipath-*runner sets SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS, except workflows on an explicit exemption list such as docker-publish.yml. Second, each value is byte-identical to the pr-checks.yml value. If you keep the copies and add no check, put an entry in .claude/harness-candidates.md. - [Axis 7] Quote normalization is off in later windows of a wrapped bash -lc command, and no test covers normalization of later windows (
src/coder_eval/criteria/command_executed.py:153) —_match_haystacksnow callsnormalized = _normalize_shell(window)on each window from_search_windows. Only the first window (cmd_text[:cap]) holds the wrapper's opening quote. A later window starts on a line boundary inside the-lcscript, so it holds only the closing quote.shlex.splitthen raises ValueError,_normalize_shellreturns None without any log, and that window keeps only its raw haystack. Codex commands arrive in this wrapped shape (tests/test_command_executed.py:810 uses'/bin/bash -lc "uip is resources run list ...'). I reproduced this with the local .venv. With patternuip\s+or\s+users\s+list, the 30-charbash -lc 'uip or "users" list'matches (True). The same command placed after a 60-line heredoc inside the same wrapper (4301 chars) does not match (False). The per-window normalize results for the matching case were[(2000, None), (1988, ok), (389, None)], and the last window is the one that holds the command. So a quoted retired call in amax_count: 0gate, or a quotedexclude_patterntoken, now slips through past the bound. It is caught when the command is short, and when the command is not wrapped (the unwrapped heredoc variant matched True). This is the dodge thattest_negative_assertion_not_dodged_by_quotingguards for short commands. It is not a regression against main, which never searched past 2000 chars. But the notes now say 'the whole command is searched', and the degradation is silent. Fix: unwrap the-c/-lcwrapper once on the full text, strip the outer quote, then window the inner script. Another fix: when a later window's shlex fails, retry it with the wrapper's quote character prepended. Add a TestSearchWindows case that places a quoted command after a long heredoc insidebash -lc '...', for bothmax_count: 0andexclude_pattern. Also consider alogger.debugwhen a window fails to normalize, so a missed quoted match can be diagnosed. - [Axis 8] The user guide does not record that command_executed now changes verdicts past 2000 chars (max_count/exclude_pattern can flip pass to fail) (
src/coder_eval/criteria/command_executed.py:105) —_search_windows(line 105) makescommand_pattern,exclude_patternand themax_countgate see the whole command. Before, they saw onlycmd_text[:_MAX_PATTERN_SEARCH_LEN]. The PR body says this change is not purely additive: "max_count: 0on a retired command at the end of a long script ... not seen, check passes -> seen, check fails". The only place in the repo that records this is.claude/notes/contracts.md, which is not auto-loaded and is not a user doc. docs/TASK_DEFINITION_GUIDE.md §command_executed(lines 976-1000) already has a rule for this case. For shell normalization it says "normalization is not purely additive ... Cross-repo suites that hand-encoded quote tolerance in their patterns should re-baseline." This change gets no matching sentence. Add one line to that section: the whole command is searched in bounded windows, so an exclusion or amax_countgate can now catch a call late in a long heredoc script, and suites should re-baseline. The generated plugin reference (make plugin-reference) takes its text from the modelFielddescriptions (src/coder_eval/models/criteria.py:888 onward), and those also do not mention it. Verified withgrep -n -i '2000\|truncat' docs/TASK_DEFINITION_GUIDE.md plugins/coder-eval/reference/criteria.md: no hit for command_executed. Kept at Medium because the old 2000-char cap was not documented either. The problem is the missing migration note for a change that flips verdicts, not a doc that is now wrong.
Nits
- [Axis 1] Packed windows do not overlap, so a multi-line or DOTALL match across a window boundary is silently missed and no test pins it (
src/coder_eval/criteria/command_executed.py:123) —_WINDOW_OVERLAPapplies only when one logical line is longer than the cap is chopped. When lines are packed and flushed atbounds.append((start, end))(line 123), the next window starts exactly atendand nothing overlaps. A DOTALL pattern that spans two short lines, which the criterion's own comment says is intended ('foo.*--bodymust span them'), matches the same text inside the first 2000 chars but misses it later. Reproduced: body (1998 chars) + 32 filler lines +"git commit -m x\n"+"git push --force origin main\n"gives windows[2000, 1997, 29], with the commit line ending window 2 and the push line starting window 3.git commit.*git push --forcereturns False, but it returns True for the same three lines in a short command. Withmax_count: 0this is a false pass of a forbidden-sequence gate, and onexclude_patternit is a missed exclusion. This is not a regression, because pre-PR that text was never searched. But .claude/notes/contracts.md line 99 now says 'the whole command is searched'. Fix: start each packed window a few lines (up to_WINDOW_OVERLAPchars) before the previous window'send, or reword the note and docstring to say that a match that crosses a window boundary is not guaranteed. A related packing quirk:_LOGICAL_LINE_END = re.compile(r"(?<!\\)\n")(line 28) treats\\\n(an escaped backslash followed by a real newline) as a continuation, and it splits a CRLF continuation\\\r\n. Both only move boundaries, so they matter only through this cross-boundary gap. - [Axis 2] _search_windows builds packed windows from offset 0, then discards them and prepends a raw leading slice; the reason is not stated (
src/coder_eval/criteria/command_executed.py:131) —return [cmd_text[:cap]] + [cmd_text[s:e] for s, e in bounds if s > 0]gives the function two modes. The packing loop runs fromstart = end = 0, thenif s > 0drops the first packed window and puts the mid-line cutcmd_text[:cap]in its place. So the characters between the first line boundary andcapare searched twice, and a reader cannot tell from the code whether the special first window matters. Neither the docstring (lines 106-114) nor contracts.md says why the first window is different. The likely reason is to keep every verdict the old single-window code gave, including matches in the normalized form that cross a line boundary. Either state that in one line (in .claude/notes/contracts.md, per the docstring-is-contract rule), or remove the special case:return [cmd_text[s:e] for s, e in bounds]. The one-pass version is also better for_normalize_shell, because every window then starts and ends on a line boundary, and a cut through a quoted string makes shlex fail. - [Axis 2] New module comments repeat what the constant names and the _search_windows docstring already say (
src/coder_eval/criteria/command_executed.py:24) — Lines 24-25 (# Overlap between the pieces of one logical line longer than the bound, so a match up to this long is never split across two pieces.) and line 27 (# A newline not escaped by a trailing backslash: where one logical shell line ends.) say the same thing as the names_WINDOW_OVERLAPand_LOGICAL_LINE_END. The_search_windowsdocstring (line 110 'a backslash-continued line stays whole', line 112 'overlapping by_WINDOW_OVERLAP') says it again. CLAUDE.md says 'default to ZERO comments'. Delete the comment on line 27. Cut the one on lines 24-25 to the part the code cannot show, which is the guarantee 'a match of up to 256 chars is never split', or move that guarantee into the docstring. - [Axis 2] Rewritten antigravity workspace test duplicates the test above it (
tests/test_antigravity_agent.py:150) — The old test called the realgoogle.antigravityworkspace_onlypolicy. The rewritetest_resolved_workspaces_cover_skill_readsnow only assertsany(skill_md.is_relative_to(w) for w in workspaces). That is already implied bytest_resolve_workspaces_includes_workdir_and_skill_roots(line 137), which asserts_resolve_workspaces(...) == [str(tmp_path / "work"), str((repo / "skills").resolve())]: SKILL.md is underrepo/skillsby construction. The other assert,assert not skill_md.is_relative_to(workdir.resolve()), only checks the fixture. Following 'delete before you guard', delete the new test, or move its docstring (why skill roots must be inworkspaces) onto the line-137 test. With it gone, the newfrom pathlib import Pathimport is unused and can go too. - [Axis 5] Search-window fan-out makes regex work scale with agent-controlled command length, and the early-stop watcher repeats that work on every tool-call event (
src/coder_eval/criteria/command_executed.py:150) — Before this PR a command cost at most one 2000-char window. Nowfor window in _search_windows(cmd_text):(line 150) yields about len/1744 windows for a long single line, plus one normalized haystack per window. The work in each window is still bounded, so this is not a ReDoS regression. But the total cost is now (command length) x (pattern cost per window).live_verdict(line 245 onward) re-runs_matching_commandsover the whole accumulated trajectory, and the regexsearchis not memoized (only_normalize_shellhas@lru_cache, line 56). Measured on the local venv: a 5 MB one-line command ("a 'b' " * 833333) gives 3823 haystacks. A super-linear task-authored pattern(a|b|\s)*ztook 17.2 s for one full scan, and early stop repeats that scan on each tool-call event. A benign literal pattern took 0.003 s. The patterns are task-authored and therefore trusted, and the agent controls only the length, so the impact is a slow evaluation, not a security boundary crossing. Fix: add a total cap on searched characters per command (for example, the leading window plus the trailing N windows, or a total of about 64 KB), or memoize the per-command match result across early-stop re-scans. Document the cap in .claude/notes/contracts.md next to the new windowing paragraph. CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L - [Axis 6] The PR bumps the Pi CLI pin, but the Pi parser vocabulary and docstrings still say they were captured from 0.84.4 (
docker/Dockerfile:42) — docker/Dockerfile:42 changesARG PI_VERSION=0.84.4toARG PI_VERSION=0.87.1. The stream reducer this binary feeds still says it was built against 0.84.4: src/coder_eval/agents/pi_agent.py:8 (Three grammar facts that are not obvious from the event names (``pi`` 0.84.4)), pi_agent.py:152 (# The full recognized Pi vocabulary (frompi0.84.4)., above_RECOGNIZED_EVENTS) and models/agent_config.py:365 (Confirmed against ``pi --help`` on Pi 0.84.4). A clean exit that recognizes no event in_RECOGNIZED_EVENTSis scored as a crash, so vocabulary drift in 0.87.1 would change docker-driver Pi results. I did not check 0.87.1's event names, which is why this is Low. The Claude Code pin has a parity test (tests/test_image_from_dockerfiles.py). The Pi pin is only checked for being an exact version. To fix, confirm the 0.87.1 event grammar against a fresh capture, then update the version cited in pi_agent.py and agent_config.py. If the grammar changed, refresh the tests/test_pi_agent.py fixture. The other option is to keep PI_VERSION at 0.84.4 in this dependency-bump PR. - [Axis 8] docs/agents/ANTIGRAVITY.md still says google-antigravity is pinned to 0.1.8 after the bump to 0.1.18 (
pyproject.toml:134) — This PR changes the extra to"google-antigravity==0.1.18",(pyproject.toml:134). docs/agents/ANTIGRAVITY.md:30 still says "This pulls ingoogle-antigravity(pinned to0.1.8)". Update that doc line in the same change. Checked withgrep -rn '0\.1\.8' docs/: this is the only stale pin. The other bumped pins (2.1.177, 0.144.4, 0.22.0, PI_VERSION 0.84.4) have no stale mentions in docs/, README.md, plugins/ or .claude/notes.
What's Missing
Parallel paths & mirrors:
- 🔵 The google-antigravity 0.1.18 bump changed the workspace policy, but src/coder_eval/agents/antigravity_agent.py still describes the old mechanism. I checked the 0.1.18 wheel.
hooks/policy.py:534 workspace_only()now runsdel workspacesand returns markerdenypolicies with nowhenpredicate.local_connection_config.pyno longer adds workspace_only in front of the other policies (no prepend logic is left). The path check now runs inside localharness. The rewritten test docstring in tests/test_antigravity_agent.py says this. The agent does not: antigravity_agent.py:278 (_resolve_workspaces: "Workspace roots for the harness'sworkspace_onlyfile-tool policy") and :344-345 ("File tools are confined toworkspacesby the auto-prepended workspace_only policy") still describe the 0.1.8 Python-side policy. Change both so they say the harness enforcesworkspacesitself. (trigger: pyproject.toml) (restates: Axis 2: Rewritten antigravity workspace test duplicates the test above it) - 🔵 harbor 0.22.0 -> 0.23.0 changed the ATIF models, but the vendored copy and its validation record did not follow. I diffed the two wheels. harbor 0.23.0 adds
ATIF-v1.8toTrajectory.schema_version, and it adds anaudiocontent part (ContentPart.type: Literal['text','image','audio'],source: ImageSource | AudioSource). src/coder_eval/harbor/atif_models.py:39 still hastype: Literal['text','image']underextra='forbid'. Its docstring says the image variant exists "so ATIF documents produced by other agents still parse", but a v1.8 audio part from a harbor 0.23 producer is now rejected. The pin references are also stale. tests/test_atif_models.py:5-14 says "pinned to 0.22.0" and "Last validated: harbor 0.22.0". atif_models.py:70 and packager.py:39 (_HARBOR_SCHEMA_VERSIONcomment) still cite harbor 0.22.0. Run the re-validation steps in tests/test_atif_models.py against 0.23.0, then either add the audio variant or document that it is not supported, and update the version references. (trigger: pyproject.toml)
Tests:
- 🔵 Since 0.1.18, no test checks that
workspacespluspolicies=[policy.allow_all()]still lets the agent read SKILL.md from a skill root outside the workdir. The old test called the SDK's realworkspace_onlypredicate. In 0.1.18 that predicate is gone (the path check runs in the localharness binary), and the rewrite checks only list membership, which the test at line 137 already covers. The one path left that can fail this way is a skill read denied by the harness. Add a gated live or integration check (skipped when the extra or GEMINI_API_KEY is absent) that does one skill read under 0.1.18, or record in docs/agents/ANTIGRAVITY.md that this path has no test. (trigger: tests/test_antigravity_agent.py) (restates: Axis 2: Rewritten antigravity workspace test duplicates the test above it) - 🔵 The PR says "The SDK runs the CLI it bundles, not the image's ... The image pin now equals the bundled version". Nothing enforces that. tests/test_image_from_dockerfiles.py:463 checks only that docker/Dockerfile and docker/Dockerfile.runtime agree with each other. No test compares
CLAUDE_CODE_VERSIONwithclaude_agent_sdk._cli_version.__cli_version__(2.1.281 for the locked 0.2.159). So the next SDK bump can again make Docker runs report one CLI version while another one runs, which is the 2.1.177-vs-2.1.216 case this PR fixes. The pyproject floor>=0.2.157bundles 2.1.277, so a downstream install at the floor already differs from the image label. Add a test that compares the installed SDK's bundled CLI version with the Dockerfile ARG. Skip it when the SDK is absent. (trigger: docker/Dockerfile)
Rollout impact:
- 🔵 The release note will not show the effect on scores and costs. The repo uses squash merges, and semantic-release builds CHANGELOG.md from the PR title (
fix(criteria): ...), so the release gets one patch line. That line will not say that the Claude Code CLI 2.1.177 -> 2.1.281 bump lowers claude-code costs by about 1.5-1.65x for Sonnet 5 / Opus 5.5, and that the codex, antigravity and Pi harnesses changed too. docs/agents/CLAUDE_CODE.md:147 calls aCLAUDE_CODE_VERSIONbump "a score-affecting change", and the PR shows 35 fail-to-pass flips. The PR body explains this, but dashboards and the evalboard that compare cost and pass rate across the release see only the changelog. Put a short summary of the harness bumps and the cost and verdict shift in the squash commit body or in aBREAKING CHANGE/notes footer, so it reaches CHANGELOG.md. (trigger: pyproject.toml)
Guardrails & Automation
Static checks (lint / type / repo-local):
- [repo-local-check] CE067 SAFE_CHAIN exclusion parity. Add a doc-surface lint class
TestCE067SafeChainExclusionParityto tests/test_custom_lint.py, with its logic in a new module tests/lint/safe_chain_parity.py. Follow the CE035 pattern (tests/lint/workflow_outputs.py): the module docstring states the invariant, the scope and the blind spots, and has aRationale: .claude/notes/lint-rules.md § CE067pointer. The rule walks .github/workflows/*.yml. For every job whoseruns-onis auipath-*runner and that has a step runninguv syncoruv pip install, it has two checks. (1) Presence: the job or step env must setSAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS. (2) Parity: the value must be byte-identical to the value in pr-checks.yml, which is the reference copy. AnEXEMPTdict maps a workflow file to its reason, for exampledocker-publish.yml: it leaves the variable out on purpose, as its comment at line 49 says. A stale EXEMPT entry, meaning a workflow that no longer exists or no longer installs packages, is also a finding. Record the defect in .claude/notes/lint-rules.md. The alternative that deletes the pattern instead of guarding it is one repo variable or a composite action that exports the value. If the team takes that fix, the check (2) is not needed, but check (1) still applies. Prevents: Finding 7 (fourth hand-synced copy of the SAFE_CHAIN exclusion, at harbor-e2e.yml:33, pr-checks.yml:46, publish-testpypi.yml:41 and release.yml:78). The presence check also catches the missing copy that af239b9 had to fix: harbor-e2e.yml had no exclusion list, so it blocked claude-agent-sdk 0.2.159. A parity-only check would not have caught that. - [repo-local-check] CE068 pin-citation parity. Add a lint class
TestCE068PinCitationParityin tests/test_custom_lint.py, backed by a tablePIN_CITATIONSin a new tests/lint/pin_citations.py. Each key is a pin source: an exact==pin in pyproject.toml (read with tomllib) or anARG <NAME>_VERSION=in docker/Dockerfile, parsed the same way tests/test_image_from_dockerfiles.py:500 already parses PI_VERSION. Each value is the list of files that cite that version in prose, plus a regex that finds the cited version. Seed entries:google-antigravity-> docs/agents/ANTIGRAVITY.md (patterngoogle-antigravity.*?pinned to .?(\d+\.\d+\.\d+)).PI_VERSION-> src/coder_eval/agents/pi_agent.py (both the module docstring and the_RECOGNIZED_EVENTScomment, patternpi(\d+.\d+.\d+)``), src/coder_eval/models/agent_config.py (Pi (\d+\.\d+\.\d+)), and the tests/test_pi_agent.py capture fixture's recorded version, if it has one. The rule fails when a cited version differs from the pin. A second assert fails when a cited file no longer matches its regex, so a citation cannot drop out of the table without notice. Put the rationale in .claude/notes/lint-rules.md: the rule cannot verify that Pi 0.87.1 kept the event grammar. It only makes a pin bump touch every place that says 'verified against X', so the person who bumps the pin must re-verify or revert the bump. Scope and blind spot for the docstring: it covers only the citations in the table, and a new prose citation needs a new entry. Prevents: Finding 11 (docs/agents/ANTIGRAVITY.md:30 still says google-antigravity is pinned to 0.1.8 after the bump to 0.1.18). Finding 8 (the PI_VERSION bump to 0.87.1 in docker/Dockerfile:42 while pi_agent.py:8, pi_agent.py:152 and agent_config.py:365 still cite 0.84.4). The check fails the same PR that bumps the pin, and the Pi event grammar re-verification is then an explicit step.
Guardrail improvements (not statically reachable):
- Add a metamorphic parity test for windowed search to tests/test_command_executed.py, under TestSearchWindows. The test has a fixed corpus of (pattern, command, expected verdict) cases that are correct on a short command. For each case, it builds long variants that put the command after a filler prefix: a heredoc body, packed short lines, and a single long line. The prefix lengths are chosen so the command lands at each window boundary shape: the first window at offset 0, just past
_MAX_PATTERN_SEARCH_LEN, exactly on a packed-window flush, and inside the overlap of a chopped long line. The test asserts that the long variant gives the same verdict as the short one, throughcommand_pattern,exclude_patternand themax_count: 0gate. The corpus must contain these cases:^git$and a$-anchored pattern after a long heredoc (finding 1),git commit.*git push --forceacross two short lines at a packed boundary (finding 2), anduip\s+or\s+users\s+listagainstbash -lc '<long heredoc>\nuip or "users" list'(finding 9). A known limitation that is kept on purpose goes in as an expected-divergence case with a comment that points to .claude/notes/contracts.md. That way the notes' claim that 'the whole command is searched' is checked by a test and not only written in prose. Why not static: The defects are about regex semantics over windows and shlex-normalized strings that are built at runtime:^/$binding at slice edges, DOTALL spans across non-overlapping windows, and a wrapper quote that is lost per window. Each line of code is correct on its own, so no AST or linter rule can see that a haystack slice changes what a pattern means. Only running the matcher can show it. Prevents: Finding 1 (anchors match at window edges), finding 2 (no overlap between packed windows gives false passes of a forbidden-sequence gate), finding 9 (per-window quote normalization fails in later windows of bash -lc), and the related test-gap findings grouped under them (tests/test_command_executed.py:1113, :1143). It would also keep finding 3's special first window honest, because removing that window must not change any verdict in the corpus. - Add a cost-bound test for command_executed. Assert a hard upper bound on the number of haystacks that
_search_windowsand_match_haystacksmake for one command, for examplelen(list(_search_windows('a ' * 2_500_000))) <= N, with N taken from a named cap constant. Also assert that alive_verdictre-scan over a trajectory that already holds a very long command does not repeat the per-command regex work, either through memoization or a total searched-character cap. Assert on counts, not on wall-clock time, so the test is deterministic. Record the cap in .claude/notes/contracts.md beside the windowing paragraph. Why not static: The defect is total work that scales with the length of agent-controlled input and repeats on each early-stop event. That is a runtime property of the data volume and the call frequency, not a code pattern. The code has no unbounded-loop construct that a rule could flag: every window is bounded, and only the number of windows is not. Prevents: Finding 6 (search-window fan-out makes regex work scale with command length, and live_verdict repeats it on every tool-call event). - Add a process guardrail for criterion changes that flip verdicts. Put an entry in .claude/harness-candidates.md, and a checklist line in the PR template or in the /coder-eval-code-review rubric: 'If this change can move an existing task from pass to fail or from fail to pass for the same trajectory, add a "not purely additive ... re-baseline" sentence to the criterion's section in docs/TASK_DEFINITION_GUIDE.md, and to the model Field description so that
make plugin-referencecarries it.' docs/TASK_DEFINITION_GUIDE.md:998, the shell-normalization paragraph, is the model. Do not promote it to a CE rule. Why not static: Only semantic judgment can tell whether a change alters verdicts. A rule of the form 'a diff to criteria/X.py must touch the X section of the guide' would fire on every refactor, and it would still not check that the right sentence was added. So the boundary is a recorded decision, not an omission. Prevents: Finding 10 (the user guide and the plugin reference do not say that command_executed now searches past 2000 chars, so max_count and exclude_pattern gates can flip). - No mechanical guard for the review-quality findings. The redundant comments at command_executed.py:24-27 and the duplicate test at tests/test_antigravity_agent.py:150 stay review-time checks.
make docs-budgetalready caps comment volume and run length, and it cannot judge whether a comment repeats a name. A 'unique coverage per test' check through pytest-cov dynamic contexts could flag tests that cover no line of their own, but assertion-only tests like the one at line 150 cover the same lines as their neighbours by design, so the signal would be mostly false positives. If the redundant test is deleted, ruff F401 already removes the unusedfrom pathlib import Paththat is left over. Why not static: To decide whether a comment says more than the identifier, or whether one test's assertions are implied by another's, needs semantic judgment about meaning and logical implication. No available linter rule or repo-local AST check can decide this without heavy false positives. Prevents: Finding 4 (module comments repeat constant names and docstring), finding 5 (rewritten antigravity workspace test duplicates the line-137 test), finding 3 (the unexplained special first window in _search_windows is covered only indirectly, by the metamorphic test above).
Top 5 Priority Actions
- Fix the per-window shell normalization at src/coder_eval/criteria/command_executed.py:153. Unwrap the
bash -c/-lcwrapper once on the full text and then window the inner script, or retry a failed window with the wrapper's quote prepended. Add tests for bothmax_count: 0andexclude_patternwith a quoted call after a long heredoc, because today that call slips past a forbidden-command gate with no log. - Bind the window edges to the real command at src/coder_eval/criteria/command_executed.py:123 and :131. Today
^and$match at every window edge, and packed windows do not overlap, so an anchored or DOTALL pattern can give a false hit or a false pass past 2000 characters. Overlap the packed windows by up to_WINDOW_OVERLAP, search with pos/endpos, and state the remaining$and normalized-haystack limits (or drop the special first window). Pin all of this with tests for commands longer than 2000 characters. - Confirm the Pi 0.87.1 event grammar against a fresh capture before bumping docker/Dockerfile:42. If it changed, update
_RECOGNIZED_EVENTSand the cited version in src/coder_eval/agents/pi_agent.py:8/:152 and models/agent_config.py:365, and refresh the tests/test_pi_agent.py fixture. The other option is to keep PI_VERSION at 0.84.4, because a clean exit with no recognized event is scored as a crash. - Add a re-baseline note to the
command_executedsection of docs/TASK_DEFINITION_GUIDE.md (next to line 998) and to the model Field descriptions (src/coder_eval/models/criteria.py:888 onward, thenmake plugin-reference). The note must say that the whole command is now searched, soexclude_patternandmax_countcan flip a pass to a fail. In the same change, fix the stalegoogle-antigravity0.1.8 pin in docs/agents/ANTIGRAVITY.md:30 (pyproject.toml:134 now pins 0.1.18). - Remove the fourth hand-synced SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS copy (.github/workflows/harbor-e2e.yml:33, pr-checks.yml:46, publish-testpypi.yml:41, release.yml:78). Use one repo-level source, or add a doc-surface lint that catches both a missing copy and a copy that differs, and also cap the total searched characters per command in command_executed.py:150 so that early-stop re-scans cannot grow with agent-controlled command length.
Change class: complex — changes the command_executed search algorithm (single leading window → multi-window logical-line packing), which alters verdicts of min_count/max_count/exclude_pattern gates, plus harness SDK / CLI pin bumps and CI safe-chain exclusions
Stats: 0 🔴 · 0 🟠 · 4 🟡 · 7 🔵 across 8 axes reviewed.
Verification: 8 medium+ finding(s) adversarially re-checked · 1 dropped as false positives (13%) · 6 corrected in place · 9 low passed through unverified.
uipreliga
left a comment
There was a problem hiding this comment.
Fix what you agree with and 🚢
A non-Bash tool's params are matched as JSON, so windowing them made a long Write/Edit body count as a command: a doc mentioning a retired command past char 2000 tripped a max_count: 0 gate without a tool_name filter, and later windows dropped the "content" key that skills' api-workflow discovery patterns guard on. Those tools keep the leading 2000 chars. Re-grading is unchanged: 35 fail->pass / 0 pass->fail on the 2026-09-23 Opus 5.5 run, 0/0 on the Sonnet 5 nightly. Also records the long-command behavior in the Task Definition Guide so suites know exclude_pattern and max_count gates now see the whole command, and drops comments that repeated the constant names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pi 0.87.1: same stream event names, identical JSON serializer and the same --help thinking levels as 0.84.4, so the grammar citations move up. - Antigravity 0.1.18 enforces workspaces inside localharness, not through a Python-side workspace_only policy; docstrings and the pinned version in ANTIGRAVITY.md now say so. The rewritten workspace test only restated the test above it, so its rationale moves there and it is deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Unblocks moving the claude-code nightly to Opus 5.5 (UiPath/skills#3514). Three changes:
1.
command_executedsearches the whole commandBefore, the criterion only searched the first 2,000 characters of each command. Opus 5.5 often batches a task into one long Bash call and runs the checked command at the very end:
command_executed: uip agent validatemax_count: 0on a retired command at the end of a long script (same forexclude_pattern)The per-search length bound against ReDoS stays; the rest of the command is searched in bounded windows aligned to shell lines.
Only Bash commands are windowed. Other tools keep the leading 2,000 characters of their JSON params, so a long Write/Edit body is not read as a command.
Re-grading published runs:
2. Harness SDKs and CLIs move to current releases
What this fixes in practice:
Pricing check on this branch,
hello_dateon Opus 5.5 over Bedrock (passes, score 1.0):3. claude-agent-sdk is exempt from CI's 48-hour package-age gate
docker build, so image-only installs (Antigravity, Pi, the Claude Code npm package) were never gated.claude-agent-sdk>=0.2.157andopenai-codex>=0.155.1. skills and coder_eval_uipath CI don't exempt these packages, so a higher floor would break their installs of this release until the new versions age in. The lockfile and the agent image still get the versions above.🤖 Generated with Claude Code