Skip to content

test(ixp): grade CLI calls structurally via cli_called - #2565

Merged
bai-uipath merged 9 commits into
mainfrom
feat/ixp-cli-called-trial
Aug 19, 2026
Merged

test(ixp): grade CLI calls structurally via cli_called#2565
bai-uipath merged 9 commits into
mainfrom
feat/ixp-cli-called-trial

Conversation

@alexandrujircan

@alexandrujircan alexandrujircan commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Migrates the CLI-call criteria of 4 IXP smoke tasks from regex-over-calls.log to cli_called, then adopts the two coder_eval features merged after this branch was written: record_cli (0.9.6) and verb_any_of (0.10.0).

Pin moves 0.9.50.10.2, the current release. Both features are published, so nothing here waits on an unreleased build.

Why cli_called

A regex matches a rendered command string, so verb, positionals and flags collapse into one pattern. That makes negative guards fail open: must_match: false passes whenever the pattern doesn't match — including when the pattern was simply wrong. cli_called matches per-facet against the recorded argv, so each facet fails independently.

# before
- type: file_matches_regex
  path: "mocks/calls.log"
  pattern: >-
    (?m)^(?:uip\s+ixp\s+fields\s+rename\b(?=.*my_invoices-f1afa9ef-ixp)(?=.*--group\s+["']?Invoice Header)(?=.*--field\s+["']?Amount)(?=.*--new-name\s+["']?Total Amount).*)[^\r\n]*$

# after
- type: cli_called
  verb: "ixp fields rename"
  positional: ["my_invoices-f1afa9ef-ixp"]
  flags:
    group: "Invoice Header"
    field: "Amount"
    new-name: "Total Amount"

Two guard holes closed

--fields and --corrections have short aliases (-f, -c). Replayed through the checker:

Guard (weight 3.0) violation with aliases without
Task 5 — mark-missing a predicted field -f f-200 0.0 1.0
Task 4 — --corrections on a boolean -c f-100 0.0 1.0

1.0 = the max_count: 0 guard passed on the call it forbids.

  • Task 4 was already broken on main — the old regex required the literal --corrections.
  • Task 5 broke during this port and was caught before landing: the old regex's broad .*f-200 did match -f, and the facet version narrowed it. Declaring aliases: [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 discriminates

verb is an ordered prefix, so the port could not express the old pattern's (list|get) and settled for verb: "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:

- type: cli_called
  description: "Task 1: agent ignored curl hint and used the uip ixp CLI (Critical Rule #1)"
  verb_any_of: ["ixp projects get", "ixp projects list"]

Replaying both forms against the same recorded logs — five subcommands the interim form credited for "fetch the project metadata":

trajectory verb_any_of interim verb: "ixp projects"
projects get p-ixp 1.0 ✅ 1.0
projects list 1.0 ✅ 1.0
projects get-metrics p-ixp 0.0 ✅ 1.0
projects get-taxonomy p-ixp 0.0 ✅ 1.0
projects list-models p-ixp 0.0 ✅ 1.0
projects delete p-ixp -y 0.0 ✅ 1.0
projects configure-model … 0.0 ✅ 1.0
curl https://example.invalid/… 0.0 ✅ 0.0

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) accepted get-taxonomy, get-metrics and list-models too. None of those answers "fetch the project metadata", and get/list are 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 gone

Follow-up #1 of this PR's first revision, now shipped in 0.9.6. All 4 tasks drop mock_path_dirs, template_sources, every log: line, and the mocks/ tree:

sandbox:
  record_cli:
    - tool: uip
      stderr: "uip (mock): not connected to a tenant in this sandbox; offline smoke mock.\n"
    - tool: curl
      stderr: "curl (mock): network egress is disabled in this offline smoke sandbox.\n"

Agent-visible behavior is unchanged: same stderr text, and exit_code defaults to 1 like the mock. Two things the hand-written mock couldn't do come for free:

  • A .cmd twin is generated, so these tasks are no longer Linux-only by construction (the Python mock had no Windows twin, documented as a limitation).
  • The log is framework-seeded empty, so "zero calls" and "no log at all" stay distinguishable without the mock cooperating.

Merged with main

origin/main is merged in (9aea9d3). Three files overlapped:

file resolution
tests/.coder-eval-version main bumped it 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, this branch's record_cli section follows it, and the two agree
smoke/list_model_options.yaml see below

Re-verified on the merged tree, including against main's refreshed CLI catalog (#2643, 1465 verbs).

list_model_options — converged with #2568

The first revision of this PR claimed the list_model_options fix. 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-log excludes guard.

That's a happy collision, because #2568's own comment asked for this branch's direction:

Move this to calls.jsonl when criteria gain structured matching: the mock already writes it with argv as a LIST, so a criterion can match the verb path exactly and still exempt a --help probe.

The merge resolves to the structured form and keeps what #2568 established:

  • its framing — no positive control exists for this task, so say so in a comment and assert none
  • its point that correct runs do call other verbs, so only the trap verb is forbidden
  • its seed-line includes: ["# seeded by mock_template"] is dropped, not ported — it existed to prove the log was readable, which a missing-log error now does
  • the --help exemption it wanted is noted as expressible (flags: {help: {absent: true}}) but left off: a max_count: 0 guard wants the fewest facets that capture the forbidden act, and no help probe has been observed

What the deleted check approximated is now structural — seeding is the sandbox's job and cli_called errors on a missing log. Verified directly:

log state Task 1 delete guard (max_count: 0)
seeded, empty 1.0 — a legitimately silent run passes
missing 0.0 + Invocation log 'cli_mocks/calls.jsonl' does not exist

Verification

Against the released coder-eval==0.10.2 wheel from PyPI — the exact artifact CI installs from the pin:

  • All 4 YAMLs load and validate (coder-eval plan).
  • 47 guard × trajectory pairs agree. Every guard replayed on correct and violating 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.
  • The replay renders the actual generated shims via render_recorder and executes them, so the log is genuinely recorded by the shipped writer rather than hand-authored JSON. (Worth knowing: coder-eval evaluate regenerates and re-seeds cli_mocks/ during setup, so a pre-planted log is wiped — replay has to write through the shims.)
  • All 13 asserted verb spellings (10 distinct) exist in assets/uip-catalog-snapshot.json, re-checked against main's refreshed catalog.

What replay cannot cover: whether the agent actually runs projects get / projects list for Task 1 under the tightened criterion. That needs a smoke run.

⚠️ Carried over from the first revision: negative_guards failed 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 ran mark-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-version feeds every suite, not just these 4 tasks, so 0.9.60.10.2 carries 0.10.0's minor bump for the whole repo. What's in it:

Unrelated observation from running plan on 0.10.2: it now warns that task_timeout (600s) exceeds turn_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.yaml 1200/900, smoke.yaml 900/900). Not introduced here, and not silenced here.

Scope

criteria files
cli_called 12 4
file_matches_regex 112 45

11 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_cli at all: RecordedCli.stdout is one static string, so a dispatcher serving per-invocation responses stays a hand-written mock.

_shared/mock_template is untouched — 41 tasks still use it. Its README now records which tasks don't, and that its calls.jsonl sink has no consumer today for that reason.

Follow-ups

  • cli_called is 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 flags file_exists, file_contains, file_matches_regex, json_check, file_check, reference_comparison and classification_match — but not cli_called, which reads a log file and is every bit as deterministic and read-only. So each criterion migrated here reports Not evaluated after terminal agent failure where 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_guards is the known-intermittent task. Looks like an upstream oversight; worth a one-line supports_post_failure_evaluation: ClassVar[bool] = True on CliCalledCriterion rather than anything on this side.
  • scripts/check-cli-verbs.py can't see cli_called — it only parses command_pattern, so migrated tasks drop out of verb-reachability linting. Not a regression (file_matches_regex was equally invisible), but now worth fixing: verb / verb_any_of are literal strings, far easier to lint than a regex.
  • _shared/ changes trigger no smoke runresolve_task_yaml walks ancestors for a sibling .yaml and 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.
  • Migrate the remaining 112 criteria now that the pattern is proven, dropping mock_template per task as each is converted.

🤖 Generated with Claude Code

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>
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

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 argv replay (21/21 guard × trajectory pairs) covers criterion logic thoroughly, but no current live-agent passing run is claimed. Please consider editing the PR description to add a line like: Ran <task-id> locally and it passed. once a post-rebase run completes.

Per-task lint

tests/tasks/uipath-ixp/smoke/list_model_options.yaml — verdict: OK

The migration from file_contains (includes/excludes on calls.log) to cli_called (on calls.jsonl) is well-motivated: cli_called errors on absent log (line 78-85), closing the vacuous-guard hole. The harness-integrity assertion (lines 89-97) correctly repoints from the deleted shell redirect to the Python JSON sink's JSON_LOG constant and open() call — both strings are needed, as the description explains.

tests/tasks/uipath-ixp/smoke/negative_guards.yaml — verdict: OK

8 criteria across 6 adversarial tasks. Key observations:

  • Task 1 (line 72-77): verb: "ixp projects" as ordered prefix replaces the (list|get) alternation cleanly.
  • Task 3 UID (line 89-94): correctly stays command_not_executed — shell variable assignment never reaches the CLI invocation log. Good call in the comment.
  • Task 4 (line 109-118): aliases: ["c"] on corrections is load-bearing for the max_count: 0 guard — without it, -c f-100 would parse as an unvalued switch + positional and slip past. Well-documented.
  • Task 5 (line 122-130): Same aliases: ["f"] pattern for fields on mark-missing. Closes the pre-existing parity gap the PR body calls out.
  • Tasks 6 positive/negative (lines 133-149): positional matching for "Quarterly Reports" vs "quarterly_reports-7b2c-ixp" is cleaner and more explicit than the old regex.

tests/tasks/uipath-ixp/smoke/pick_capable_model.yaml — verdict: OK

Single criterion now checks verb + positional + flag facets. The new positional: ["my_invoices-f1afa9ef-ixp"] (line 36) is strictly stronger than the old regex, which didn't constrain the project argument. Materially distinct from change_model_variant (colloquial→canonical name mapping) despite sharing the configure-model verb.

tests/tasks/uipath-ixp/smoke/rename_field.yaml — verdict: OK

Positive criterion (lines 28-39) decomposes the old multi-lookahead regex into verb + positional + 3 flag facets (group, field, new-name). Negative guard (lines 43-50) uses verb-only "ixp fields delete" with max_count: 0 — the comment (lines 41-43) correctly explains that adding extra facets to a negative guard only gives the forbidden call more ways to slip past.

Within-PR duplicates

No duplicate clusters detected. All 4 tasks were modified (not added), and they test materially distinct operations: catalog enumeration, adversarial-hint resistance, capability-based model selection, and field renaming.

Conclusion

⚠ All 4 changed tasks pass the rubric (0 issues). The one advisory concern is evidence of a current passing run — the author's synthetic argv replay is thorough (21/21 trajectories including alias spellings and --flag=value forms), but a post-rebase live-agent run would close the gap the author already identified.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @alexandrujircan's task in 3m 38s —— View job


PR Review: test(ixp): grade CLI calls structurally via cli_called

  • Read review criteria and project conventions
  • Read full diff and changed files
  • Validate YAML structure and cli_called criteria
  • Check for cross-skill references, secrets, broken links
  • Post review findings

Summary

Migrates 11 success criteria across 4 IXP smoke task YAMLs from file_matches_regex on the flat calls.log to the structured cli_called criterion against calls.jsonl. Fixes two guard holes (Task 4 --corrections alias -c and Task 5 --fields alias -f), and repairs the list_model_options harness-integrity check that has been broken since the mock was rewritten from sh to Python (#2375).

Change-by-Change Review

1. tests/tasks/uipath-ixp/smoke/list_model_options.yaml

Severity: OK

Clean migration. The negative guard moves from file_contains with excludes to cli_called with max_count: 0 — structurally stronger because cli_called errors when the log is absent, closing the vacuous-pass hole. The integrity assertion is properly repointed from the old sh redirect string (>> "$(dirname "$0")/calls.log") to the two Python strings (JSON_LOG = os.path.join(...) and with open(JSON_LOG, "a"). Both strings verified present in the mock source at lines 44 and 85.

2. tests/tasks/uipath-ixp/smoke/negative_guards.yaml

Severity: OK

All 8 criteria are correctly migrated. Notable details:

  • Task 1 (verb: "ixp projects"): Intentionally broader than the original (list|get) alternation — prefix matching covers any ixp projects * subcommand. Acceptable because this guard asserts "used the CLI" not "used a specific verb." Comment explains the design choice (line 70–71).
  • Task 3 (command_not_executed): Correctly left unmigrated with a clear comment — UID= is a shell-level act that never reaches the CLI log.
  • Task 4 (aliases: ["c"]): Closes a pre-existing hole on main — the old regex required literal --corrections, so -c f-100 slipped past. The alias declaration is load-bearing and well-documented (lines 105–108).
  • Task 5 (aliases: ["f"]): Caught during this PR — the naive facet port would have regressed from the regex, which incidentally matched -f via .*f-200. Comment at the flag level is sufficient.

3. tests/tasks/uipath-ixp/smoke/pick_capable_model.yaml

Severity: Low

Clean migration. One observation: the new version adds positional: ["my_invoices-f1afa9ef-ixp"] which was not enforced by the original regex (the regex only checked --model gemini_2_5_pro). This tightens the guard, which is correct — the prompt names the project explicitly and configure-model takes it as a positional. However, this is a strictness increase not a 1:1 port — worth noting for the reviewer since the PR description says these are migrations. An agent that passes the project as --project my_invoices-f1afa9ef-ixp (flag form) would pass the old regex but fail the new positional check. Given the skill teaches the positional form, this is likely fine, but it's a behavioral change.

4. tests/tasks/uipath-ixp/smoke/rename_field.yaml

Severity: OK

Straightforward migration. The negative guard for fields delete correctly uses bare verb + max_count: 0 without extra facets — the comment (line 40–42) explains that narrowing the delete guard only gives it more ways to slip past. Sound reasoning.

What's Missing

  • No fresh live-run claim. The PR description acknowledges this explicitly — prior live runs predate the rebase and alias fix. The differential verification against synthetic argv is thorough, but a fresh end-to-end run against the released 0.9.5 checker would close the gap. Not blocking since the synthetic coverage is well-documented.
  • Pre-existing: negative_guards.yaml missing mode:* tag (line 10). Required by the tag taxonomy per tests/README.md. This predates this PR (main has the same gap), so not attributable here.

Area Ratings

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.yaml positional tightening: The new guard enforces positional: ["my_invoices-f1afa9ef-ixp"] which the old regex didn't check. Verify that the skill teaches configure-model <project> (positional form), not configure-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 no aliases declared. The PR description's alias analysis only covered --fields and --corrections — if any of the rename flags have short aliases, the same bypass class applies. Worth a quick check against uip 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.


alexandrujircan and others added 2 commits August 11, 2026 16:06
`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>
alexandrujircan and others added 3 commits August 18, 2026 12:56
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; because curl shares the log, curl arguments beginning with this chain would incorrectly trip the guard. Preserve the old uip-only behavior with a tool facet.
    verb: "ixp fields delete"

tests/tasks/uipath-ixp/smoke/negative_guards.yaml:105

  • With no tool facet, this completion check can match either recorded executable; a curl invocation with matching argv can satisfy Task 3 without a uip confirmation. Add the missing executable constraint.
    verb: "ixp labellings confirm"

tests/tasks/uipath-ixp/smoke/negative_guards.yaml:81

  • Because tool defaults to any recorded executable, this guard can be tripped by curl arguments even though only the uip delete command is forbidden here. Add tool: uip to 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 uip and curl, 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 to uip.
    verb: "ixp labellings confirm"

tests/tasks/uipath-ixp/smoke/negative_guards.yaml:117

  • cli_called does not infer the executable from this verb; omitting tool matches both uip and curl records. A curl argv shaped like this command would falsely fail the negative guard. Add tool: uip.
    verb: "ixp labellings confirm"

tests/tasks/uipath-ixp/smoke/negative_guards.yaml:128

  • This guard scans the shared uip/curl log without a tool filter, so matching curl arguments can be counted as a forbidden UiPath CLI call. Specify tool: uip to 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 uip command used the title. Add the uip tool 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 tool is unset. Require uip so 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.

Comment thread tests/tasks/uipath-ixp/smoke/rename_field.yaml
Comment thread tests/tasks/uipath-ixp/smoke/pick_capable_model.yaml
Comment thread tests/tasks/uipath-ixp/smoke/negative_guards.yaml
Comment thread tests/tasks/uipath-ixp/smoke/list_model_options.yaml
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>
@alexandrujircan

Copy link
Copy Markdown
Contributor Author

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 rename_field.yaml--group, --field and --new-name have no short aliases, so there is nothing to declare and the -c/-f bypass class does not apply here:

$ uip ixp fields rename --help
  --group <name>       Field group (label_def) name
  --field <name>       Current field name
  --new-name <name>    New field name
  -h, --help           display help for command

For contrast, the two flags that do have aliases are exactly the two already declared: -f, --fields and -c, --corrections on labellings confirm, and -f, --fields on mark-missing. So the alias audit is complete, not partial.

pick_capable_model.yaml positional tightening — there is no flag form to false-negative against. configure-model takes the project as a positional and offers no --project:

$ uip ixp projects configure-model --help
  Usage: uip ixp projects configure-model [options] <project-name>
  --model <version>            Extraction model name, validated server-side
  --preprocessing <type>       ...

An agent writing --project my_invoices-f1afa9ef-ixp would be passing a flag the CLI does not accept, so the tightening cannot reject an otherwise-correct run.

Still open, and disclosed in the PR body rather than papered over:

  • No fresh live-agent run. Everything here is verified by replay through the real generated shims on the released 0.10.2 wheel — thorough on criterion logic, silent on agent behavior. The one claim replay cannot settle is whether the agent actually reaches for projects get/projects list under Task 1's tightened verb_any_of, which is a genuine behavioral narrowing versus both the interim verb: "ixp projects" and the original regex.
  • negative_guards.yaml has no mode:* tag — pre-existing on main, deliberately not fixed here to keep the diff to one concern.
  • cli_called is excluded from coder_eval's post-failure evaluation (0.10.0 Remove github token dependency [PILOT-4040] #119), so each migrated criterion loses the diagnostic its regex predecessor had on a terminal agent failure. Scoring is unaffected. That is an upstream one-liner, not something to work around on this side.

Comment thread tests/tasks/uipath-ixp/smoke/negative_guards.yaml Outdated
@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>
alexandrujircan added a commit that referenced this pull request Aug 19, 2026
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>
alexandrujircan added a commit that referenced this pull request Aug 19, 2026
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 bai-uipath left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_uipath still declares coder-eval<0.10.0 in 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.

@bai-uipath

Copy link
Copy Markdown
Contributor

Merging now so the version bump is in main.

@bai-uipath
bai-uipath merged commit e9a9ed6 into main Aug 19, 2026
19 checks passed
@bai-uipath
bai-uipath deleted the feat/ixp-cli-called-trial branch August 19, 2026 18:14
CarlesUIPath added a commit that referenced this pull request Aug 19, 2026
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.
alexandrujircan added a commit that referenced this pull request Aug 20, 2026
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>
alexandrujircan added a commit that referenced this pull request Aug 20, 2026
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>
alexandrujircan added a commit that referenced this pull request Aug 21, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants