test(ixp): grade CLI calls structurally via cli_called - #2565
Conversation
Regex over the flat call log matched a rendered command string, so every assertion mixed the verb, the positionals and the flags into one pattern whose failure modes were invisible: a `must_match: false` guard passes whenever the pattern fails to match, for ANY reason, including a pattern that was subtly wrong. cli_called (coder-eval 0.9.5) matches per-facet against the recorded argv, so verb, positionals and flags fail independently and legibly. Migrates four smoke tasks and closes two guard holes found while porting: - negative_guards Task 4 (`--corrections` on a boolean): the regex required the literal long spelling, so `-c` slipped past a guard weighted 3.0. Now declared `aliases: [c]`. - negative_guards Task 5 (`mark-missing` on a predicted field): the regex's broad `.*f-200` did catch `-f`; a naive facet port would have narrowed it and reopened the hole. Declared `aliases: [f]` to keep parity. list_model_options asserted the sh redirect `>> "$(dirname "$0")/calls.log"`, a string the mock's Python rewrite deleted — the harness-integrity check has been failing since. Repointed at the JSON sink, and its negative guard moved to cli_called, which errors on an absent log instead of reading a deleted sink as "the forbidden verb never ran". Verified: 21 guard/trajectory pairs replayed through the released 0.9.5 checker agree on both correct and violating argv, including every short-alias spelling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @alexandrujircan's task in 3m 6s —— View job Coder-eval task lint (advisory)4 task YAMLs changed; verdict counts: 0 Critical, 0 High, 0 Medium, 0 Low, 4 OK. Rubric: .claude/commands/lint-task.md. This check is advisory and never blocks merge. Evidence of passing run❌ High — PR body explicitly states "A fresh live run is the one gap in this PR's evidence" and disclaims the earlier runs as predating the rebase and alias fix. The synthetic Per-task lint
|
|
Claude finished @alexandrujircan's task in 3m 38s —— View job PR Review: test(ixp): grade CLI calls structurally via cli_called
SummaryMigrates 11 success criteria across 4 IXP smoke task YAMLs from Change-by-Change Review1.
|
| Area | Status | Notes |
|---|---|---|
| Frontmatter | N/A | No skill frontmatter changed (test tasks only) |
| E2E Tests | OK | All 4 task YAMLs are structurally valid; verbs confirmed in catalog |
| Skill Body | N/A | No skill body changed |
| References & Assets | N/A | No references or assets changed |
| Repo Hygiene | OK | No secrets, no cross-skill refs, no unrelated changes |
Issues for Manual Review
pick_capable_model.yamlpositional tightening: The new guard enforcespositional: ["my_invoices-f1afa9ef-ixp"]which the old regex didn't check. Verify that the skill teachesconfigure-model <project>(positional form), notconfigure-model --project <project>(flag form), to confirm this won't cause false negatives on correct agent runs.- Alias completeness for
--group,--field,--new-name(rename_field.yaml): These flags have noaliasesdeclared. The PR description's alias analysis only covered--fieldsand--corrections— if any of the rename flags have short aliases, the same bypass class applies. Worth a quick check againstuip ixp fields rename --help.
Conclusion
Clean, well-reasoned migration. The alias fixes close real guard holes (one pre-existing on main, one caught during the port). The list_model_options integrity repair fixes a guard that's been broken since #2375. The pick_capable_model positional tightening and potential alias gaps on rename_field are worth a quick manual check but are not blocking. Approve.
`verb` is a literal ordered prefix with no alternation, so `verb: "ixp projects"` cannot express the old pattern's `(list|get)` — it admits all 14 `ixp projects` subcommands. An agent that ran `projects delete` on the project it was told to READ therefore earned 1.0 on a weight-3.0 criterion titled "used the uip ixp CLI". The comment claiming the prefix "covers both list and get" read as an equivalence when the port was strictly weaker. Adds a `max_count: 0` guard on `ixp projects delete` so the destructive case cannot be the invocation that earns channel credit, and rewrites the comment to state the widening instead of hiding it. `list` and `get` both still count, so agent-visible tolerance is unchanged. Residual, now asserted in the differential rather than assumed absent: non-delete mutations (`update-title`, `publish`, `import-taxonomy`, ...) still earn channel credit, as does a hallucinated subcommand. Guarding each is whack-a-mole; the real fix is verb alternation in cli_called. Differential: 27/27 (was 21/21) through the released 0.9.5 checker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same rot @cezara98t flagged on the coder_eval side: "any of the 14 `ixp projects` subcommands" is true of one CLI version, and the catalog is bot-refreshed. The point the comment makes — that the prefix is deliberately broader than the old `(list|get)` — does not depend on the number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops the hand-written mock from the four cli_called smoke tasks and makes the Task 1 channel assertion discriminate, using two features merged upstream after this branch was written. `sandbox.record_cli` (coder_eval 0.9.6) generates the recording shims, seeds the log and PATH-prepends `cli_mocks/` itself, so the four tasks drop `mock_path_dirs`, `template_sources`, and every `log:` line. The shims keep the mock's agent-visible behavior: same stderr text, same exit 1. Two things the hand-written mock could not do come for free — a `.cmd` twin (so these tasks are no longer Linux-only by construction) and a framework-seeded empty log, which is what let `list_model_options` delete its harness-integrity check rather than repoint it. That check asserted mock source text and had been failing since #2375; the guarantee it approximated is now the harness's. `verb_any_of` (unreleased; coder_eval f7e9fda) restores the alternation `verb` could not express. The interim `verb: "ixp projects"` credited ANY subcommand for "fetch the project metadata" — replaying it shows `delete`, `configure-model`, `get-metrics`, `get-taxonomy` and `list-models` all scoring 1.0. The paired delete guard stays, but it is no longer patching that hole. `_shared/mock_template` is untouched; 41 tasks still use it. Its README now says which tasks don't and why its `calls.jsonl` sink has no consumer today. Verification, against the real code at coder_eval f7e9fda (editable install, not a wheel): 47 guard x trajectory pairs agree, replayed through actual generated shims that were executed to produce the log. All 13 asserted verb spellings exist in `assets/uip-catalog-snapshot.json`. The pin now reads 0.9.7, which is not on PyPI yet — `verb_any_of` is merged but unreleased, so CI cannot install a grader that understands it until that version (or an rc of it) is published. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three overlaps, one substantive. `tests/.coder-eval-version` — main bumped the pin to 0.9.6 (#2419); kept 0.9.7, which is what `verb_any_of` needs. `_shared/mock_template/README.md` — auto-merged. Main's rewritten grading guidance (#2568) is intact and this branch's record_cli section follows it; the two agree, since "no positive control, say so in a comment and add no criterion" is what deleting the integrity check does. `smoke/list_model_options.yaml` — #2568 fixed the same breakage independently while this branch was open, deleting the harness-integrity criterion and keeping a flat-log `excludes`. Its comment asked for exactly this branch's direction: "Move this to calls.jsonl when criteria gain structured matching ... a criterion can match the verb path exactly and still exempt a `--help` probe". Resolved to the structured form, absorbing what #2568 established: - its framing, that no positive control exists and none is asserted - its point that correct runs call other verbs, so only the trap verb is forbidden - the seed-line `includes` is dropped, not ported: a missing log now errors - the `--help` exemption it wanted is noted as expressible but left off, since a max_count 0 guard wants the fewest facets and no help probe has been observed Re-verified on the merged tree: 4 YAMLs valid, 47 guard x trajectory pairs agree, and all 10 asserted verbs still present in main's refreshed catalog (#2643, 1465 verbs). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`verb_any_of` shipped in 0.10.0, not the 0.9.7 this branch guessed at — the line went 0.9.6 -> 0.10.0, so 0.9.7 never existed. 0.10.2 is current. Re-verified against the released wheel from PyPI rather than a git checkout: 4 YAMLs valid, 47 guard x trajectory pairs agree. The bump is repo-wide, so what rides along matters. 0.10.0's breaking change (#110, run_limits.max_turns) is scoped to the codex and antigravity harnesses and this repo runs claude-code; 0.10.1 and 0.10.2 are release plumbing only. One thing does touch this PR: #119 evaluates read-only criteria after a terminal agent failure, and cli_called is not on its list, so migrated criteria lose the post-failure diagnostic their regex predecessors had. Scoring is unaffected. Recorded as a follow-up on coder_eval, not worked around here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops the four-times-repeated recorder comment: it restated what `record_cli` does, which the framework documents and the block itself shows. Cuts migration archaeology from the rest — PR references and the before/after regex comparison — which belongs in the PR discussion, not in a file read years from now. What stays is per-criterion reasoning: why `aliases` is load-bearing on a max_count 0 guard, why the UID check stays `command_not_executed`, why a `--help` exemption was considered and left off. Comment lines: 24 -> 17 across the four tasks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Migrates four IXP smoke tasks from regex-based CLI grading to structured cli_called assertions and framework-generated recorders.
Changes:
- Replaces handwritten mocks with
record_cli. - Adopts
verb_any_of, flag aliases, and structured negative guards. - Updates coder_eval to 0.10.2 and documents the new approach.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
tests/.coder-eval-version |
Pins coder_eval 0.10.2. |
tests/tasks/uipath-ixp/smoke/rename_field.yaml |
Migrates rename assertions. |
tests/tasks/uipath-ixp/smoke/pick_capable_model.yaml |
Migrates model-selection assertion. |
tests/tasks/uipath-ixp/smoke/negative_guards.yaml |
Structurally grades adversarial CLI cases. |
tests/tasks/uipath-ixp/smoke/list_model_options.yaml |
Migrates the forbidden-command guard. |
tests/tasks/uipath-ixp/_shared/mock_template/README.md |
Documents record_cli usage. |
Suppressed comments (8)
tests/tasks/uipath-ixp/smoke/rename_field.yaml:45
- This guard currently matches every recorded executable, not only
uip; becausecurlshares the log, curl arguments beginning with this chain would incorrectly trip the guard. Preserve the olduip-only behavior with a tool facet.
verb: "ixp fields delete"
tests/tasks/uipath-ixp/smoke/negative_guards.yaml:105
- With no
toolfacet, this completion check can match either recorded executable; a curl invocation with matching argv can satisfy Task 3 without auipconfirmation. Add the missing executable constraint.
verb: "ixp labellings confirm"
tests/tasks/uipath-ixp/smoke/negative_guards.yaml:81
- Because
tooldefaults to any recorded executable, this guard can be tripped by curl arguments even though only theuipdelete command is forbidden here. Addtool: uipto retain the prior regex's executable constraint.
verb: "ixp projects delete"
tests/tasks/uipath-ixp/smoke/negative_guards.yaml:89
- The shared invocation log contains both
uipandcurl, but this positive assertion does not select a tool. A curl call whose argv starts with this chain and positionals would be credited as a labelling confirmation. Restrict it touip.
verb: "ixp labellings confirm"
tests/tasks/uipath-ixp/smoke/negative_guards.yaml:117
cli_calleddoes not infer the executable from this verb; omittingtoolmatches bothuipandcurlrecords. A curl argv shaped like this command would falsely fail the negative guard. Addtool: uip.
verb: "ixp labellings confirm"
tests/tasks/uipath-ixp/smoke/negative_guards.yaml:128
- This guard scans the shared
uip/curllog without a tool filter, so matching curl arguments can be counted as a forbidden UiPath CLI call. Specifytool: uipto preserve the previous executable-specific check.
verb: "ixp labellings mark-missing"
tests/tasks/uipath-ixp/smoke/negative_guards.yaml:139
- The criterion currently applies to every tool recorded in this sandbox. Consequently a curl invocation with these positionals can trip the title guard even though no
uipcommand used the title. Add theuiptool facet.
verb: "ixp projects get-metrics"
tests/tasks/uipath-ixp/smoke/negative_guards.yaml:148
- This positive criterion can be satisfied by any recorded executable, including curl, because
toolis unset. Requireuipso the task only credits the documented CLI command.
verb: "ixp projects get-metrics"
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot caught a real regression in the port. `record_cli` shims uip AND curl into one shared log, and `cli_called` treats an omitted `tool` as "any tool" — so the argv facets alone decide the match. The regexes this replaced were anchored with `^uip\s+`, and dropping that widened all 12 criteria on an axis nobody intended. Reproduced before fixing, with curl records shaped like the uip verb: 5 positive assertions credited a curl-only trajectory and 2 max_count 0 guards tripped on one. The worst is Task 1, whose entire claim is "used the CLI, not curl" — it scored 1.0 on `curl ixp projects get`, inverting the assertion it exists to make. A real curl call is `curl https://...`, whose argv matches none of these, so no observed run was misgraded. That makes it latent, not harmless: the guard was one plausible agent behavior away from lying. Fixed with `tool: "uip"` on all 12. On a negative guard this adds a facet, which normally weakens a max_count 0 check — it does not here, because it excludes only records that cannot be the forbidden `uip` call. Noted in the file, since that is the distinction between this and the `--help` carve-out left off earlier. Verified: the curl attack drops 7 fooled criteria to 0, and the 47 guard x trajectory pairs still agree on coder-eval 0.10.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Cleared the outstanding review items. Both of the earlier "Issues for Manual Review" resolve as non-issues, checked against the actual CLI rather than reasoned about: Alias completeness on For contrast, the two flags that do have aliases are exactly the two already declared:
An agent writing Still open, and disclosed in the PR body rather than papered over:
|
@cezara98t flagged two criteria labelled "Task 3". The labels are right — Tasks 1, 3 and 6 each pair a rule guard with a completion assertion — but checking it found the completion half asserted less than its own description claimed. It said "confirmed field f-002 on the document" while constraining only the verb and `--fields`, naming no document. So confirming f-002 on Task 2's doc-abc-123 earned Task 3 credit, as did confirming it with no document at all, `document-id` being optional on `confirm`. Three trajectories scored 1.0 that should have scored 0.0; Task 2's own criterion has pinned both positionals all along. Carried over from the regex, which was equally document-blind (`confirm\b[^|;&]*--fields[^|;&]*f-002`), so this predates the migration rather than regressing in it. Fixed by naming doc-def-456 in the positional facet and in the description, which also makes the two Task 3 labels tell themselves apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stacks on #2565 and adopts its structured criterion for all four deployment tasks, replacing stacked-lookahead regexes over the flat calls.log. Per-facet matching is what the name/title trap actually wanted. The slug lives in a positional SLOT, so `positional: [project, "invoices"]` on a max_count 0 guard catches the title in the deployment argument and cannot fire on `invoices-08963f00-ixp` — where the old regex needed a hand-tuned boundary to tell them apart. Likewise `folder-key: {matches_regex: "^(Finance|Archive)"}` scopes to ONE flag value rather than the whole line. `value_flags: [version, folder-key, output]` is load-bearing on the title guard: those flags are not declared under `flags` there, so without it their values stay non-flag, shift the positional slots, and `upgrade --version 14 --folder-key K proj invoices` dodges the guard. Verified — that ordering is one of the cases below. deploy_to_folder drops the overlay for generated recorders (sandbox.record_cli): every value its graded command needs is in the prompt, so nothing is read back and "every call fails offline" is all it wants. The other three keep the overlay, because record_cli cannot replace a dispatcher: `deployments list` is stateful, `create` is folder-aware, and `upgrade` validates the version before the name — per-invocation responses are the whole reason the read-before-write path is gradeable. The overlay is now Python rather than sh, so it emits correct JSON, and writes both sinks in the base mock's exact formats; its criteria carry `log: mocks/calls.jsonl`, since the default points at record_cli's directory. Verified by running coder_eval 0.10.2's own CliCalledChecker over synthetic logs: all four tasks validate under TaskDefinition, and 18 correct/wrong cases discriminate — including the flags-first ordering dodge, a substituted version, an invented --folder-path, and a skipped folder lookup. Also drops the base template README's "calls.jsonl has no consumer today": the overlay's tasks are that consumer now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows #2565's pattern instead of adding infrastructure. All four deployment tasks now declare `sandbox.record_cli` and grade `cli_called` at its default log, so there is no hand-written mock, no `template_sources`, and no per-criterion `log:` line. Removes the `mock_template_deployments` overlay this branch had added, and restores `mock_template/README.md` to #2565's text: with the overlay gone its "calls.jsonl has no consumer today" is true again. Net effect on shared test infrastructure is now nil. Two tasks change shape because recorders cannot serve a read: - upgrade_deployment states the DeploymentName in the prompt alongside the title, the way negative_guards states a project's Name alongside its Title. It still grades the trap that matters — the NAME in the positional slot, never the title, and never `create` to repoint — but no longer proves the agent went and looked the slug up. - deploy_folder_by_name now grades that the agent goes to `or folders list` for the key and invents neither a key nor a `--folder-path`. The lookup fails offline, so the deploy shape with a key in hand stays covered by deploy_to_folder. Also fixes two defects Copilot found in the skill itself: - The "Deploy this model" row told the agent to ask for a folder key even when the user had NAMED a folder, which made deploy_folder_by_name's path unreachable from the skill. A named folder is now resolved via `or folders list`; asking is for when no folder was identified at all. - The 408 row claimed the deployment "stays on its original version" and then said "do not assume the move failed" — incoherent. A timeout leaves the outcome unknown, which is exactly why the caller must re-read `list`. Verified with coder_eval 0.10.2's own CliCalledChecker: every criterion resolves to the default `cli_mocks/calls.jsonl`, and 16 correct/wrong cases discriminate, including the flags-first ordering dodge and a skipped folder lookup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bai-uipath
left a comment
There was a problem hiding this comment.
Approve. Dry run on this branch validated the 0.10.2 pin end to end: 24/24 skills tasks, activation 92.3%, zero errors, GATE PASS (build 13080826, codex / gpt-5.6-luna).
coder_eval_uipathstill declarescoder-eval<0.10.0in its pyproject. The nightly survives it only because the plugin installs with--no-deps; anything resolving with deps now contradicts the pin. Fix: companion bump there, ideally landing first.- The migration itself is clean. Replaying every guard against violating argv, and catching that the Task 5 facet port came out strictly weaker than the regex it replaced, is the right discipline to carry into the remaining 112.
Minor: cli_called is missing from the post-failure evaluation allowlist upstream, so a migrated criterion reports no verdict after a terminal agent failure where its regex predecessor gave one. Scoring is unaffected, and it lands hardest on negative_guards.
|
Merging now so the version bump is in main. |
Resolves the tests/.coder-eval-version conflict: main bumped 0.9.6 -> 0.10.2 (#2565), this branch targets 0.11.0. Kept 0.11.0. The incoming ixp tasks use no reference: block, so the 0.11.0 bump does not hit the removed reference:{file:} shape; verified `coder-eval plan` passes on them under 0.11.0.
Stacks on #2565 and adopts its structured criterion for all four deployment tasks, replacing stacked-lookahead regexes over the flat calls.log. Per-facet matching is what the name/title trap actually wanted. The slug lives in a positional SLOT, so `positional: [project, "invoices"]` on a max_count 0 guard catches the title in the deployment argument and cannot fire on `invoices-08963f00-ixp` — where the old regex needed a hand-tuned boundary to tell them apart. Likewise `folder-key: {matches_regex: "^(Finance|Archive)"}` scopes to ONE flag value rather than the whole line. `value_flags: [version, folder-key, output]` is load-bearing on the title guard: those flags are not declared under `flags` there, so without it their values stay non-flag, shift the positional slots, and `upgrade --version 14 --folder-key K proj invoices` dodges the guard. Verified — that ordering is one of the cases below. deploy_to_folder drops the overlay for generated recorders (sandbox.record_cli): every value its graded command needs is in the prompt, so nothing is read back and "every call fails offline" is all it wants. The other three keep the overlay, because record_cli cannot replace a dispatcher: `deployments list` is stateful, `create` is folder-aware, and `upgrade` validates the version before the name — per-invocation responses are the whole reason the read-before-write path is gradeable. The overlay is now Python rather than sh, so it emits correct JSON, and writes both sinks in the base mock's exact formats; its criteria carry `log: mocks/calls.jsonl`, since the default points at record_cli's directory. Verified by running coder_eval 0.10.2's own CliCalledChecker over synthetic logs: all four tasks validate under TaskDefinition, and 18 correct/wrong cases discriminate — including the flags-first ordering dodge, a substituted version, an invented --folder-path, and a skipped folder lookup. Also drops the base template README's "calls.jsonl has no consumer today": the overlay's tasks are that consumer now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows #2565's pattern instead of adding infrastructure. All four deployment tasks now declare `sandbox.record_cli` and grade `cli_called` at its default log, so there is no hand-written mock, no `template_sources`, and no per-criterion `log:` line. Removes the `mock_template_deployments` overlay this branch had added, and restores `mock_template/README.md` to #2565's text: with the overlay gone its "calls.jsonl has no consumer today" is true again. Net effect on shared test infrastructure is now nil. Two tasks change shape because recorders cannot serve a read: - upgrade_deployment states the DeploymentName in the prompt alongside the title, the way negative_guards states a project's Name alongside its Title. It still grades the trap that matters — the NAME in the positional slot, never the title, and never `create` to repoint — but no longer proves the agent went and looked the slug up. - deploy_folder_by_name now grades that the agent goes to `or folders list` for the key and invents neither a key nor a `--folder-path`. The lookup fails offline, so the deploy shape with a key in hand stays covered by deploy_to_folder. Also fixes two defects Copilot found in the skill itself: - The "Deploy this model" row told the agent to ask for a folder key even when the user had NAMED a folder, which made deploy_folder_by_name's path unreachable from the skill. A named folder is now resolved via `or folders list`; asking is for when no folder was identified at all. - The 408 row claimed the deployment "stays on its original version" and then said "do not assume the move failed" — incoherent. A timeout leaves the outcome unknown, which is exactly why the caller must re-read `list`. Verified with coder_eval 0.10.2's own CliCalledChecker: every criterion resolves to the default `cli_mocks/calls.jsonl`, and 16 correct/wrong cases discriminate, including the flags-first ordering dodge and a skipped folder lookup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(uipath-ixp): cover folder deployments (create/upgrade/list) Teach the skill `uip ixp deployments create`, `upgrade`, and `list` from UiPath/cli#3575, and retire the guidance that folder deployment has no CLI path. Three places told the agent to refuse exactly this request: the "Deploy this model" Unsupported Capabilities row, the publish row's "no `uip ixp` (or other CLI) equivalent" claim, and the cli-reference deployments preamble. Left alone the new verbs would be unreachable — the agent would decline and hand back a docs link. Environments and tenants stay routed out; only folders are supported. Encodes the traps that cost an agent turns rather than restating --help: - `upgrade` takes `DeploymentName` (the backend slug), not the `--title`; passing a title is a clean 404 - the slug's suffix is generated per deployment and cannot be constructed — read it off the create response or `deployments list` - `create` only ever adds; repointing is `upgrade` (409 otherwise, no --force) - branch on `Context.HttpStatus`, not `ErrorCode` — a 409 surfaces as `invalid_argument` - `upgrade` is not a rollback path, and rejected writes leave `DeployedAt` intact Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(uipath-ixp): cover folder deployments with four smoke tasks Adds coder_eval coverage for `uip ixp deployments create`/`upgrade`/`list`, closing the gap the PR review flagged Medium. Four tasks, each trapping a mistake the create/upgrade split exists to prevent: - deploy_to_folder — fresh create; guards against upgrade-as-deploy (a 404, never a fallback) and publish-as-deploy - upgrade_deployment — the repoint; asserts the DeploymentName carried from `deployments list`, and traps both the title in the positional and create-as-repoint - deploy_folder_by_name — folder given by name; asserts the key resolved via `or folders list`, traps a name/path shaped into --folder-key and an invented --folder-path - upgrade_stale_version — asks for a version no longer deployable; the agent must check `list-models` and refuse rather than fire a doomed upgrade Adds _shared/mock_template_deployments, an overlay on the base mock. The base fails every call, which makes the upgrade path ungradeable: DeploymentName is a backend-minted slug that cannot be derived from the title, so with a failing `deployments list` there is nothing to carry into the graded write and an agent that correctly declines to invent it scores zero. The overlay serves the read, and models the service faithfully — stateful `list` so a proving re-read sees its own write, folder-aware `create` (409 in the occupied folder, a different per-deployment slug elsewhere), version-checked-before-name `upgrade`, and the title answering 404. Every criterion grades the mock's call log, not agent prose, and each negative guard pairs with a positive control per the mock template's README. All 14 correct/wrong discrimination cases verified against real logs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(uipath-ixp): correct the stale-version bar to match no-op semantics The first CI run scored skill-ixp-upgrade-stale-version-smoke 0.50. The agent checked `projects list-models` FIRST, saw only 13 and 14, then attempted the move to 9 anyway, took the 404 [ModelVersionNotFoundError], and reported it. That reaches the correct outcome. A rejected upgrade is a documented no-op — ModelVersion and DeployedAt are untouched — so the cost is one harmless call, and the agent gets a precise service error to report rather than an inference from a version list. Hard-failing it at weight 3.0 encoded a preference as a correctness bar, and contradicted the skill's own "rejected writes are no-ops". So the pre-flight check drops to advisory (pass_threshold: 0), which is the documented way to record convention adherence without docking, and a new hard guard covers what IS unambiguously wrong: silently substituting a version the user never asked for. Checking deployability and refusing the `create` fallback stay hard requirements. Verified against the observed CI log: that run now scores 1.00 with the advisory noted, the tidier pre-flight path also scores 1.00, and substituting v14, falling back to create, or never checking deployability each still fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(uipath-ixp): grade folder deployments via cli_called Stacks on #2565 and adopts its structured criterion for all four deployment tasks, replacing stacked-lookahead regexes over the flat calls.log. Per-facet matching is what the name/title trap actually wanted. The slug lives in a positional SLOT, so `positional: [project, "invoices"]` on a max_count 0 guard catches the title in the deployment argument and cannot fire on `invoices-08963f00-ixp` — where the old regex needed a hand-tuned boundary to tell them apart. Likewise `folder-key: {matches_regex: "^(Finance|Archive)"}` scopes to ONE flag value rather than the whole line. `value_flags: [version, folder-key, output]` is load-bearing on the title guard: those flags are not declared under `flags` there, so without it their values stay non-flag, shift the positional slots, and `upgrade --version 14 --folder-key K proj invoices` dodges the guard. Verified — that ordering is one of the cases below. deploy_to_folder drops the overlay for generated recorders (sandbox.record_cli): every value its graded command needs is in the prompt, so nothing is read back and "every call fails offline" is all it wants. The other three keep the overlay, because record_cli cannot replace a dispatcher: `deployments list` is stateful, `create` is folder-aware, and `upgrade` validates the version before the name — per-invocation responses are the whole reason the read-before-write path is gradeable. The overlay is now Python rather than sh, so it emits correct JSON, and writes both sinks in the base mock's exact formats; its criteria carry `log: mocks/calls.jsonl`, since the default points at record_cli's directory. Verified by running coder_eval 0.10.2's own CliCalledChecker over synthetic logs: all four tasks validate under TaskDefinition, and 18 correct/wrong cases discriminate — including the flags-first ordering dodge, a substituted version, an invented --folder-path, and a skipped folder lookup. Also drops the base template README's "calls.jsonl has no consumer today": the overlay's tasks are that consumer now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(uipath-ixp): use record_cli recorders, drop the hand-written mock Follows #2565's pattern instead of adding infrastructure. All four deployment tasks now declare `sandbox.record_cli` and grade `cli_called` at its default log, so there is no hand-written mock, no `template_sources`, and no per-criterion `log:` line. Removes the `mock_template_deployments` overlay this branch had added, and restores `mock_template/README.md` to #2565's text: with the overlay gone its "calls.jsonl has no consumer today" is true again. Net effect on shared test infrastructure is now nil. Two tasks change shape because recorders cannot serve a read: - upgrade_deployment states the DeploymentName in the prompt alongside the title, the way negative_guards states a project's Name alongside its Title. It still grades the trap that matters — the NAME in the positional slot, never the title, and never `create` to repoint — but no longer proves the agent went and looked the slug up. - deploy_folder_by_name now grades that the agent goes to `or folders list` for the key and invents neither a key nor a `--folder-path`. The lookup fails offline, so the deploy shape with a key in hand stays covered by deploy_to_folder. Also fixes two defects Copilot found in the skill itself: - The "Deploy this model" row told the agent to ask for a folder key even when the user had NAMED a folder, which made deploy_folder_by_name's path unreachable from the skill. A named folder is now resolved via `or folders list`; asking is for when no folder was identified at all. - The 408 row claimed the deployment "stays on its original version" and then said "do not assume the move failed" — incoherent. A timeout leaves the outcome unknown, which is exactly why the caller must re-read `list`. Verified with coder_eval 0.10.2's own CliCalledChecker: every criterion resolves to the default `cli_mocks/calls.jsonl`, and 16 correct/wrong cases discriminate, including the flags-first ordering dodge and a skipped folder lookup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(uipath-ixp): serve the folder list so the resolved key is gradeable The recorder declared only `stderr`, so `stdout` defaulted to "" and `exit_code` to 1: `uip or folders list` handed the agent nothing, no key was obtainable, and the task could only assert that the lookup was attempted. This task needs exactly ONE read to succeed, which is the case a single static `stdout` covers — the limitation that rules recorders out for the other tasks does not apply. Serving the folder list restores the assertion that matters: the RESOLVED Archive key on `deployments create`, which also fails an agent that grabs the first entry in the payload instead of matching the requested name. The payload is returned for every `uip` verb, so a later `create` echoes the folder list too. Harmless where grading is on command shape, and noted in the description as the reason a task needing several DIFFERENT reads cannot do this. The prompt's "commands will fail with auth errors" note is dropped, since they no longer do. Verified with CliCalledChecker: correct resolve-then-deploy scores 1.00, while grabbing the Invoices key, shaping the name into --folder-key, inventing --folder-path, or skipping the lookup each fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(uipath-ixp): record the per-verb mocking limitation as a TODO `sandbox.record_cli` shadows an executable, not a verb: RecordedCli is tool/exit_code/stdout/stderr with extra="forbid", and every `uip` verb goes through one binary. Two entries for the same tool pass model validation but die at sandbox setup ("would overwrite cli_mocks/uip"), and a verb in `tool` is rejected by the executable-name pattern. So per-verb responses are not expressible today. Each affected task now says so at its sandbox block, in the repo's existing TODO style, naming the concrete compromise it makes rather than a generic "improve later": - deploy_folder_by_name: the one stdout answers EVERY verb, so a later `create` echoes the folder list too - upgrade_stale_version: `list-models` cannot be served at all, since the exit 0 needed for the read would make the doomed `upgrade --version 9` look successful - upgrade_deployment: the DeploymentName is stated in the prompt, so the slug lookup itself is not proven deploy_to_folder needs no served read and carries no note. The verb-prefix matching this wants already exists in the `cli_called` criterion, so the upstream change is reuse rather than new parsing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(uipath-ixp): drop upgrade_stale_version — not soundly gradeable offline CI scored it FAILURE / 0.55: the agent went straight to `deployments upgrade --version 9` without checking `projects list-models` first, the same behavior as the previous run. Two runs agree, so this is the agent's consistent reading, not a flake. That exposes an incoherence in my own design. Last revision I demoted "did not fire the doomed upgrade" to advisory, on the grounds that a rejected upgrade is a documented no-op — attempting it and reporting the 404 reaches the right outcome, so requiring a pre-flight read is a style preference, not a correctness bar. But I left the mirror image of that same preference ("must call list-models") gating. Both cannot be right. Demoting list-models too would leave no hard positive criterion, and the two real guards (no substituted version, no `create` fallback) are negative. A do-nothing agent would then score 1.00 — precisely the vacuous pass mock_template's README forbids: "negative guards MUST pair with a positive control — a log line a correct run is guaranteed to produce." No such guaranteed call exists here without a served `list-models`, and recorders cannot serve one verb (RecordedCli is per-executable). The README's own rule for that case is to add no criterion rather than a weak one. So the task comes out until per-verb responses land, when serving list-models makes the real assertion possible: that the agent reasoned from the actual deployable versions. The guidance itself is unaffected — "upgrade is not a rollback path" stays in cli-reference's error table and the Task Navigation row. Remaining three all score 1.00 in CI on fc563a5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(uipath-ixp): trim comments to what the criteria cannot say Comments now only explain a mechanism a criterion's `description` cannot carry — matching negative_guards, where load-bearing matcher options are annotated and everything else is left to the description. Dropped: comments restating the criterion right below them, and the per-task paragraph explaining why recorders are used instead of the shared mock (not something the sibling tasks carry). deploy_to_folder needs no comments at all. Kept: the `value_flags` note on the title-in-slot guard (without it the values shift the positional slots and the guard is dodgeable) and the note that one recorder `stdout` answers every verb, which is why a task needing several different reads cannot use recorders. Both TODOs kept, one line each. Two criterion descriptions absorbed the comment above them ("a clean 404", "no such flag on either deployment verb"). No matcher changed. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Migrates the CLI-call criteria of 4 IXP smoke tasks from regex-over-
calls.logtocli_called, then adopts the two coder_eval features merged after this branch was written:record_cli(0.9.6) andverb_any_of(0.10.0).Pin moves
0.9.5→0.10.2, the current release. Both features are published, so nothing here waits on an unreleased build.Why
cli_calledA regex matches a rendered command string, so verb, positionals and flags collapse into one pattern. That makes negative guards fail open:
must_match: falsepasses whenever the pattern doesn't match — including when the pattern was simply wrong.cli_calledmatches per-facet against the recordedargv, so each facet fails independently.Two guard holes closed
--fieldsand--correctionshave short aliases (-f,-c). Replayed through the checker:aliasesmark-missinga predicted field-f f-200--correctionson a boolean-c f-1001.0= themax_count: 0guard passed on the call it forbids.--corrections..*f-200did match-f, and the facet version narrowed it. Declaringaliases: [f]restores it.Worth knowing for future migrations: a facet port that reads tighter can be strictly weaker, wherever the old regex covered an axis by accident.
verb_any_of— Task 1 now discriminatesverbis an ordered prefix, so the port could not express the old pattern's(list|get)and settled forverb: "ixp projects"— deliberately broader, tolerable only because the assertion was about channel (CLI, not curl) and was paired with a delete guard. coder_eval#103, shipped in 0.10.0, removes the compromise:Replaying both forms against the same recorded logs — five subcommands the interim form credited for "fetch the project metadata":
verb_any_ofverb: "ixp projects"projects get p-ixpprojects listprojects get-metrics p-ixpprojects get-taxonomy p-ixpprojects list-models p-ixpprojects delete p-ixp -yprojects configure-model …curl https://example.invalid/…The paired delete guard stays — but on its own merit now, not as a patch for the channel criterion's breadth.
Note this is also narrower than the regex it replaces, whose unanchored
(list|get)acceptedget-taxonomy,get-metricsandlist-modelstoo. None of those answers "fetch the project metadata", andget/listare what the skill's CLI reference documents for reading a project — but it is a real behavioral tightening, so it is the one thing in this PR that smoke CI, not replay, has the final word on.record_cli— the mock is goneFollow-up #1 of this PR's first revision, now shipped in 0.9.6. All 4 tasks drop
mock_path_dirs,template_sources, everylog:line, and themocks/tree:Agent-visible behavior is unchanged: same stderr text, and
exit_codedefaults to 1 like the mock. Two things the hand-written mock couldn't do come for free:.cmdtwin is generated, so these tasks are no longer Linux-only by construction (the Python mock had no Windows twin, documented as a limitation).Merged with main
origin/mainis merged in (9aea9d3). Three files overlapped:tests/.coder-eval-version0.9.6(#2419); kept0.9.7, which is whatverb_any_ofneeds_shared/mock_template/README.mdrecord_clisection follows it, and the two agreesmoke/list_model_options.yamlRe-verified on the merged tree, including against main's refreshed CLI catalog (#2643, 1465 verbs).
list_model_options— converged with #2568The first revision of this PR claimed the
list_model_optionsfix. It no longer does: #2568 landed that fix on main while this branch was open, deleting the same harness-integrity criterion (which asserted mock source text and had been red since the Python mock rewrite in #2375) and keeping a flat-logexcludesguard.That's a happy collision, because #2568's own comment asked for this branch's direction:
The merge resolves to the structured form and keeps what #2568 established:
includes: ["# seeded by mock_template"]is dropped, not ported — it existed to prove the log was readable, which a missing-log error now does--helpexemption it wanted is noted as expressible (flags: {help: {absent: true}}) but left off: amax_count: 0guard wants the fewest facets that capture the forbidden act, and no help probe has been observedWhat the deleted check approximated is now structural — seeding is the sandbox's job and
cli_callederrors on a missing log. Verified directly:max_count: 0)Invocation log 'cli_mocks/calls.jsonl' does not existVerification
Against the released
coder-eval==0.10.2wheel from PyPI — the exact artifact CI installs from the pin:coder-eval plan).argv: each violation must be caught by its own guard while every other criterion stays intact. Covers short aliases (-f,-c),--flag=value, and verb-prefix confusion.render_recorderand executes them, so the log is genuinely recorded by the shipped writer rather than hand-authored JSON. (Worth knowing:coder-eval evaluateregenerates and re-seedscli_mocks/during setup, so a pre-planted log is wiped — replay has to write through the shims.)assets/uip-catalog-snapshot.json, re-checked against main's refreshed catalog.What replay cannot cover: whether the agent actually runs
projects get/projects listfor Task 1 under the tightened criterion. That needs a smoke run.negative_guardsfailed one CI attempt (0.85) and passed on re-run of the same commit. The failing criterion was the guard working — Sonnet 5 took the Task 5 bait and ranmark-missing --fields f-200, violating Critical Rule #12. Not caused by this PR (the agent used the long spelling, which the old regex matched too). Observed 1 fail / 1 pass — too few runs to call a rate. That's a skill issue, not a test issue: either Rule #12 gets strengthened so the trap holds, or the flakiness is accepted knowingly. Left for the skill owners; no guard was weakened and no threshold touched to get green.The pin bump is repo-wide
tests/.coder-eval-versionfeeds every suite, not just these 4 tasks, so0.9.6→0.10.2carries 0.10.0's minor bump for the whole repo. What's in it:feat(agents)!: honor run_limits.max_turns on codex and antigravity(fix: update flow project structure — .flow file is at project root, not flow_files/ #110) — the breaking change behind the minor bump. Scoped to the codex and antigravity harnesses; this repo runsclaude-code.feat(evaluation): preserve criteria after agent failures(Remove github token dependency [PILOT-4040] #119) — see the follow-up below.gh-action-pypi-publishbump, PyPI attestation sidecars).Unrelated observation from running
planon 0.10.2: it now warns thattask_timeout(600s) exceedsturn_timeout(300s). That fires on untouched tasks too — it's measured against coder_eval's built-in defaults, which this repo's experiments override (default.yaml1200/900,smoke.yaml900/900). Not introduced here, and not silenced here.Scope
cli_calledfile_matches_regex11 migrated criteria plus one added guard (Task 1 delete). Still a trial slice, not a sweep — each migration needs its own inverse test (see Task 5), and the two overlay templates can't use
record_cliat all:RecordedCli.stdoutis one static string, so a dispatcher serving per-invocation responses stays a hand-written mock._shared/mock_templateis untouched — 41 tasks still use it. Its README now records which tasks don't, and that itscalls.jsonlsink has no consumer today for that reason.Follow-ups
cli_calledis excluded from post-failure evaluation — a real cost of this migration. 0.10.0's Remove github token dependency [PILOT-4040] #119 evaluates deterministic, read-only criteria after a terminal agent failure so the run still yields a diagnostic vector. It flagsfile_exists,file_contains,file_matches_regex,json_check,file_check,reference_comparisonandclassification_match— but notcli_called, which reads a log file and is every bit as deterministic and read-only. So each criterion migrated here reportsNot evaluated after terminal agent failurewhere its regex predecessor would have reported a verdict. Scoring is unaffected (these results sit outside the canonical scored list), but it costs diagnostics exactly where they're most wanted —negative_guardsis the known-intermittent task. Looks like an upstream oversight; worth a one-linesupports_post_failure_evaluation: ClassVar[bool] = TrueonCliCalledCriterionrather than anything on this side.scripts/check-cli-verbs.pycan't seecli_called— it only parsescommand_pattern, so migrated tasks drop out of verb-reachability linting. Not a regression (file_matches_regexwas equally invisible), but now worth fixing:verb/verb_any_ofare literal strings, far easier to lint than a regex._shared/changes trigger no smoke run —resolve_task_yamlwalks ancestors for a sibling.yamland can't reach_shared/. This is why feat(tests): record structured uip invocations for IXP smoke tasks #2375 merged unverified, and why this PR's README edit is verified by nothing.mock_templateper task as each is converted.🤖 Generated with Claude Code