Repository navigation
Conversation
|
Hi @rominf, can you provide a human-written description of why this here is needed? Is this a regression in functionality caused by another PR? |
|
Hi @danielholanda. Partly:
This PR specifies setup session instructions in readable, declarative format, which is suitable for both: sandboxed and default engines. After the merge, amd/skills' |
johnl-amd
left a comment
There was a problem hiding this comment.
Nice work, this is careful code. CI is green, 579 tests pass on 3.10, 3.12 and 3.13 with nothing skipped, and I went looking for a test that couldn't fail without finding one.
Approving. Two small asks, one scope question, and a list for follow-ups.
Two small asks
The rule that check passing suppresses env needs writing down as the author's obligation, not just described as a mechanism. The docs say what happens but never say "write check so it passes only when the agent would work without this env". Without that, here's the failure: a runner has system ROCm with the Python bindings but doesn't export ROCPROF_TRACE_DECODER_LIB. check exits 0, nothing installs, the env is never set, and every case fails because rocprofv3 --att can't find the library. The better-provisioned machine is the one that breaks and the bare runner passes. Worse, [prerequisites] skill: 1 to provide prints identically either way, so the log doesn't tell you which branch you took. One sentence in docs/authoring-evals.md, plus ideally a line saying which prerequisites were satisfied by the environment versus installed.
PATH isn't reserved. The docs flag it as a hazard, pointing out PATH: ${SKILL_CACHE_DIR}/bin would leave the agent with just that directory, but _RESERVED_ENV only covers ANTHROPIC_, CLAUDE_ and IS_SANDBOX. shutil.which("claude") has already resolved so the CLI is fine, but anything it spawns isn't.
Scope question
SKILL_SOURCE_DIR is host only by construction, and prerequisites.py:433 hands the container leg a hardcoded "a container has no copy of the skill's source tree". But thread-trace builds its decoder from the rocm-systems tree, so under --engine claude-code it has to bake into the image, and then check passes and the declaration contributes nothing there. So the container half, most of the new module plus a test class and a Docker CI job, serves no skill that exists. That also undercuts the argument for this over a host-only hook, since thread-trace maintains the compose file either way and the drift happens anyway.
Not asking you to rip it out. But what are you picturing as the first real consumer? The design assumes its install is a plain download with no source tree. And /opt/skillscope/cache is fixed precisely so people can bake images against it, so it freezes the first time anyone does. A docs line saying that path is provisional until a skill uses it would keep the room to move it.
Follow-ups
_named_filescompares resolved paths by equality andPath.resolve()doesn't canonicalise case, so on a case-insensitive hostinstall: [cat, EVALS.JSON]gets copied in.verify.require()refuses Windows andlocalbut not macOS, where APFS is case-insensitive by default.- Nothing tests the
evals_dir in candidate.parentstraversal guard, the only branch of_named_fileswith no coverage. Pairs with the above. _prerequisite_gapssilently skips anyos:label it doesn't recognise, soos: [Liunx]defeats the "a command for every platform inos" guarantee with no diagnostic. Root cause is_string_listnever constrainingos:, which predates you, but the new guarantee is what makes it bite._ENV_NAMEuses$rather than\Z, so"FOO\n"passes as an env name, contradicting the error text next to it.fullmatchfixes it._argvonly validatesvalue[0], so["cmd", ""]is accepted while the schema'sminLength: 1on every item rejects it._live_steps.add(proc.pid)happens after the spawn, so if the hard timeout fires in that window the child survivesos._exitand gets reparented, being a session leader.- Not yours, but adjacent:
selection.matrix_entriescallsmachine_planwith noSystemExitguard, unlike every other caller, so one badmachine.ymltakes out the whole matrix.
|
@johnl-amd The two asks are in, and the cache path is marked provisional.
or
On the container half: the install it can run is a command plus the files named beside The follow-ups (case-insensitive |
|
@johnl-amd The follow-ups are in too.
Signed-off-by is on |
|
@johnl-amd I dismissed your approval from 28 Oct. It predates the three follow-up commits: 6a0a18c covers your two asks and 3a183f9 covers the follow-ups I listed in my last reply. Could you take another look when you have time? The checks on 3a183f9 are green. |
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review · review-code · head ecd9a989
Review — needs work
Full review of the whole change (no narrowing — this is a first automated pass). No ticket named; the description is an accurate, detailed account of the change and matches what the diff actually does.
Blocking
selection.plan'sgateslist is no longer deduplicated —skillscope/selection.py:287
"gates": sorted(leg["gate"] for leg in skipped if leg["gate"])replaced the previoussorted({leg["gate"] for leg in skipped}). The old code deduplicated via a set; the new code is a plain generator expression, so every skipped leg contributes its gate name again. Sincescoped_gateis a single repo-wide string (skillscope/config.py:123), not per-skill, any run with two or more scoped skills held back by the same missing gate now reports that gate's name once per skipped skill instead of once.
Confirmed by running the realselection.plan(["alpha", "beta"], ...)with both skills gated behind the samescoped_gate(reusing the existingTestSelectionfixtures):gatescomes back as['enable_gpu_ci', 'enable_gpu_ci']instead of['enable_gpu_ci']. No test exercises two skipped legs sharing a gate —test_a_gated_leg_is_reported_rather_than_runonly gates a single skill, andtest_one_bad_machine_yml_does_not_drop_the_matrixonly checks that the empty-gate placeholder (from an unreadablemachine.yml) is filtered out, not that real gates are deduplicated.
Confidence 95 · logic · Fix: wrap the generator back in a set, e.g.sorted({leg["gate"] for leg in skipped if leg["gate"]}).
Non-blocking
None
Decisions for the author
None
Positive signals
skillscope/datasets.py'sPrerequisite/_prerequisite_gaps/_ENV_NAME/_argvvalidation is unusually careful: every argv item (not just index 0) is checked against the schema'sminLength: 1,_ENV_NAMEusesfullmatchso a trailing-newline name is correctly rejected, and an unrecognisedos:label is reported rather than silently skipping the "a command for every platform" guarantee — all three are exercised by dedicated table-driven tests inTestAPrerequisiteDeclarationIsCheckedUpFront.TestMachineSchema.test_documented_keys_match_the_parserasserts the JSON schema's property sets equal the parser'sMACHINE_KEYS/PREREQUISITE_KEYS/COMMAND_PLATFORMSconstants directly, so the schema and the parser cannot silently drift apart.- The watchdog/process-cleanup tests in
TestPrerequisitesOnTheDefaultEngine(e.g.test_the_watchdogs_hard_exit_kills_a_running_install_and_its_children,test_a_stop_during_an_install_fails_the_skill_and_keeps_the_report) actually spawn real child processes and poll/proc/tasklistto confirm they are dead, rather than asserting on mocked calls — this is real verification of a hard-to-get-right concurrency path, not a simulation of it.
Deployment notes
/opt/skillscope/cache(skillscope/engine/prerequisites.py:71-72,CONTAINER_CACHE) is a fixed container path so a baked image'sinstallcan land at the same placechecklater looks for it. The code and docs both flag this as provisional ("nothing pins the path yet"); once an image is built against it, moving it becomes a breaking change for that image.skillscope/engine/hooks.py'ssetup_sessionentry point is now refused unconditionally (replaced byprerequisites:); any skill still shipping one will fail with the new refusal message rather than running, which is the intended migration but is worth calling out to downstream skill authors.
What this covered
Every changed file read in full against the PR head, plus surrounding/calling code: skillscope/datasets.py, skillscope/schema/machine.schema.json, docs/authoring-evals.md, skillscope/engine/prerequisites.py, skillscope/engine/hooks.py, skillscope/engine/behavioral.py, skillscope/engine/no_sandbox.py (including the harness-abstraction env-layering added in the newest commit), skillscope/engine/verify.py, skillscope/cli.py, skillscope/selection.py, skillscope/structure.py, tools/sandbox_smoketest.py, .github/workflows/selftest.yml, docs/usage.md, README.md, pyproject.toml, and the ~1,290 new/changed lines of tests/test_skillscope.py plus the new ordering test in tests/test_harness.py. Commit range 39eff043(current main tip)…ecd9a989 (PR head); the head moved from the orchestrator-supplied 3a183f92 to ecd9a989 mid-review (one new commit, "Pin that a harness's own env wins over a skill's prerequisites") — re-diffed and confirmed every previously-read file was byte-identical except skillscope/engine/no_sandbox.py and tests/test_harness.py, which were reviewed fresh, including a live mutation test confirming the new environment-layering test actually fails when the fix is reverted. Also ran the full unit test suite at the PR head (587 tests, all green on Python 3.11) and confirmed via gh pr view --json statusCheckRollup that every CI check (unit tests on 3.10/3.12 × ubuntu/windows, sandbox-machinery Docker job, unused-imports, action-against-a-throwaway-repo) is green.
Degraded fan-out: this review ran single-context. Every attempt to spawn independent workers (grouped by coupling: declaration/schema, host+container engine, CLI/selection/CI/docs) failed with "Concurrent subagent limit reached" after two attempts, so this pass was done by one reviewer reading every file directly rather than via independent parallel passes. Coverage was kept as close to full-breadth as a single context allows (every file read in full, cross-file contracts traced by hand, tests executed and mutated), but the cross-cutting defects that a true multi-worker consolidation step is best at surfacing may be under-covered. Recommend re-running with fan-out available.
Did not run: the "prior changes" pass (no visibility into earlier review threads on these files was sought, by design) and the "agent-instruction adherence" pass (repo has no CLAUDE.md/AGENTS.md). History (git log/git blame) was checked for the pre-existing files touched (datasets.py, hooks.py, behavioral.py, no_sandbox.py, verify.py, cli.py, selection.py, structure.py); nothing in it contradicted this change.
Not reconciled against any discussion or prior review on this PR — this pass was kept blind to the existing human review on purpose, per its instructions.
Not a human review. This is an automated code review; please verify findings independently before acting on them.
Review — needs workFull review of the whole change (base Blocking
Non-blocking
Decisions for the author
Positive signals
Deployment notes
What this coveredEvery file in the diff, read completely, against Did not run: agent-instruction adherence (no 🤖 Automated review by an agent. |
|
Thanks to both reviews of ecd9a98: the automated review of 4 Nov, and @johnl-amd's review comment of the same day. Six commits on top of ecd9a98, pushed as a fast-forward, no force-push. Each new test was seen to fail when the code it covers is broken. Blocking
Non-blocking
Decisions for the author: follow-ups, not in this PR
The description now covers the One correction to my 30 Oct comments: the commits I cited there, 6a0a18c and 3a183f9, were rewritten by a later force-push. The same patches are now 2aa2226 (the two asks) and 8aab806 (the follow-ups). |
A skill whose behavioral cases need something the runner may not have -- a library to build, a package to install -- had one way to say so: an `evals/hooks.py` `setup_session`, which these engines refuse. It ran on the host only, so a skill that also ran under `--engine claude-code` had to describe its environment a second time for the container, and the two descriptions drifted. `evals/machine.yml` now takes `prerequisites:`, each a `check` and an `install` command (optionally per platform) plus the `env` the agent needs once it is installed. Both engines apply the same declaration where their agent runs: once per skill on the host, in each case's container under `claude-code`. `check` runs first; only if it fails does `install` run, then `check` again with `env` applied, and only then is `env` handed to the agent. Steps see `SKILL_SOURCE_DIR` and `SKILL_CACHE_DIR`, and nothing a skill installs reaches another skill. A prerequisite that cannot be provided fails its own skill's cases with the step's output and is never counted as a provider outage. A stop from `--timeout` ends a running install and everything it started, on both the graceful and the hard path. Under `claude-code` only the files a step names are copied into the container, the answer key never, and they are removed before the agent starts. The structural check validates the declaration up front, including a command for every platform the skill runs on. The `setup_session` refusal now points at `prerequisites:`. Signed-off-by: Roman Inflianskas <[email protected]>
`check` exiting 0 drops that prerequisite's `env`, and the log said only how many there were, so a runner that already had the tool and one that installed it looked the same. The line now names each branch. The docs state the obligation that goes with it: `check` passes only when the agent would work without the `env`. `PATH` is refused. A value replaces the variable, the CLI is already resolved, and everything the agent starts would see only that directory. `/opt/skillscope/cache` is documented as provisional until a skill's image is built against it. Signed-off-by: Roman Inflianskas <[email protected]> Co-authored-by: Cursor <[email protected]>
A case-different path to the answer key compared unequal after resolve(), so on a case-insensitive volume it was copied into the container. The file is what is compared now, and a path that resolves outside evals/ is covered by a test. An os label that is not a platform was skipped, so Liunx met the command-for-every-platform rule by never being checked. A newline in an env name passed `$`, and an empty argv item passed a check that only looked at the first one. The watchdog snapshot and the pid registration share a lock. A step that appears after the snapshot is killed by the spawner, because os._exit will not look again. A machine.yml that does not parse no longer drops the matrix for every skill after it. Signed-off-by: Roman Inflianskas <[email protected]> Co-authored-by: Cursor <[email protected]>
`harness_solver` layers the agent's environment as the runner's, then the env a skill's prerequisites produced, then what the harness itself sets, so a prerequisite cannot undo the harness's config isolation. Nothing tested that order: dropping the last layer, or moving the prerequisites' env to the top, left the whole suite green. The dataset parser refuses Claude Code's own names, but that refusal does not hold for any other harness, so the order is the only guard there. Drive the solver with the non-Claude test harness, give it a config dir of its own, and hand it prerequisite env that names the same variable. The agent must see the harness's value and the prerequisite's other variable. Both of the reorderings above, and dropping the prerequisites' env altogether, now fail the test. Also say in the code why `env(spec)` is applied again after `host_env` already holds it, so the extra layer does not read as redundant. Signed-off-by: Roman Inflianskas <[email protected]>
`plan()` builds `gates` from the held-back legs, and since unreadable machine.yml files were added to those legs with an empty gate, the set that deduplicated it had become a plain generator. Two skills held back by the same scoped gate listed it twice. One label releases both, so it is named once again. Signed-off-by: Roman Inflianskas <[email protected]>
`_read_machine` turned bad YAML into the per-skill SystemExit that `matrix_entries` and `structural_errors` catch, but only a YAMLError. `read_text` can fail before YAML is involved: a file this process may not open raises OSError, and one that is not UTF-8 raises UnicodeDecodeError. PyYAML itself raises ValueError for a timestamp that is no date, and RecursionError for nesting deeper than the interpreter allows. None of those was caught, so planning still stopped for every skill after it, which is what holding the one skill back was meant to prevent. All of them are now reported as that skill's error, the same way bad YAML is. The file-mode test is skipped on Windows and as root, where modes do not stop the read; a second test denies the read itself, so the OSError path runs on every platform. Signed-off-by: Roman Inflianskas <[email protected]>
Every step in a container goes through `run()`, which turns a sandbox exec that raises (output too large, not text, a container gone) into a failed step named after its prerequisite. Creating the steps directory beforehand and removing it before the agent starts called `box.exec` directly, so the same failures there escaped as a bare sandbox error. That is also the error the provider-outage heuristic reads, where a PrerequisiteError is kept out of it. Both now report through the same exceptions as a PrerequisiteError, with the existing messages and the exception named. Signed-off-by: Roman Inflianskas <[email protected]>
The `--timeout` hard exit removes the host caches it knows of and then calls `os._exit`. A cache was made by `mkdtemp` and recorded afterwards, outside the lock the watchdog already takes for the running steps, so a hard exit landing between the two missed the directory and left it on disk. The directory is now made and recorded under that lock, and is not made at all once the watchdog has closed the sets, the same way a step that starts after the snapshot is killed by its spawner. The watchdog takes its snapshot of the caches under the lock too. Signed-off-by: Roman Inflianskas <[email protected]>
`select` now holds back a skill whose machine.yml it cannot plan, with the reason in an `error` field, and plans the rest. The `results` job still read every held-back entry as a missing gate label: it warned "held back for a missing gate label" over the raw list and passed. Only gated entries are warned about now. An entry with an error gets an error annotation carrying it, and fails the job: a missing label is a run nobody asked for, and a broken machine.yml is not. The structural step in `discover` usually fails on the same file first, so this is the case where it did not. Signed-off-by: Roman Inflianskas <[email protected]>
The parser refuses `SKILL_SOURCE_DIR` and `SKILL_CACHE_DIR` as `env` keys, since skillscope sets those names for the steps itself, but the docs and the schema listed only PATH and the harness's own names as refused. An author found the third rule by tripping it. The refusal said skillscope supplies the two "to every step", which a container contradicts: it has no source tree, so `SKILL_SOURCE_DIR` is not set there. It now says skillscope sets them itself. Signed-off-by: Roman Inflianskas <[email protected]>
|
Rebased onto main (7a8c00b) to clear the conflicts with #34 and #40. The new head is 65ae187.
Only the first commit changed in the rebase; the other nine replayed as they were. The full suite (714 tests) and |
`_read_machine` caught YAMLError, ValueError and RecursionError, but those are not all the ways `yaml.safe_load` rejects input: a tag whose value it cannot construct raises AttributeError (`!!timestamp x`) or KeyError (`!!bool x`). Either still ended planning for every skill with a traceback. `safe_load` of a string does nothing else that can fail, so every exception it raises is the file's, and is now reported as that skill's "not valid YAML" error. The test covers both tags, and its nesting case uses 2,000 brackets rather than 100,000, which raises the same RecursionError in a fraction of the time. Signed-off-by: Roman Inflianskas <[email protected]>
|
One more commit, 1340852: |
johnl-amd
left a comment
There was a problem hiding this comment.
🤖 Automated review · review-code · head 13408520
Review — needs work
Full review of the whole change (base 7a8c00b9…head 13408520e8e91c262a85e2d08331aa3325710afe, +2934/-110 across 19 files, 11 commits), split across four independent fan-out workers grouped by coupling (the declaration schema, parsing and docs; the host engine core; CLI, selection and CI gating; the container engine and its CI smoketest), each blind to the others and to this PR's existing comment thread (and to any earlier review round on a prior head of this branch). No ticket named as closed; the description is a long, detailed account of the change, and is unusually candid about its own limits — it lists explicit "Not verified" items and a "Scope"/follow-ups section rather than overclaiming, and this review's findings below were checked against both.
Blocking
- Some prerequisite-provisioning failures are not translated into the friendly
PrerequisiteError, and on the host engine this crashes the whole command instead of failing just the one skill —skillscope/engine/prerequisites.py:233-243(_make_cache, host path:tempfile.mkdtemp()),skillscope/engine/prerequisites.py:536(claude_code_agent's copy loop, container path:box.write_file(...)/path.read_bytes())
The module exists specifically so a prerequisite problem reads as a clear per-skill message rather than an ugly traceback (PrerequisiteError's own rationale: inspect would otherwise report a sample's error viarepr(), putting escaped newlines and the class name in the report)._make_cachecallstempfile.mkdtemp()with no try/except around it; if it raises (disk full, no permission, no/tmp), the rawOSErroris not aPrerequisiteError.behavioral.run()'s per-skill exception handling only catchesPrerequisiteError/KeyboardInterrupt/SystemExit, so this propagates out ofrun()entirely — crashing the whole command instead of degrading just the one skill, which directly contradicts the PR's own stated contract ("Failures stay local... fails its own skill's cases, and only those"). The same gap exists independently on the container engine's copy loop (unwrappedbox.write_file/path.read_bytesfailures), which two independent reviewers found without being shown each other's work — corroborating that this is a real, recurring gap in the new module's error-wrapping discipline rather than a one-off.
Confidence 78 · architectural · Fix: wrap both call sites' expected failure modes (OSErrorat minimum) and re-raise asPrerequisiteErrorwith the underlying message, matching the pattern already used forcheck/installsubprocess failures elsewhere in this same module.
Non-blocking
-
A container-side file-inclusion check for prerequisite steps compares paths case-sensitively, inconsistent with this change's otherwise careful handling of platform-dependent name matching —
skillscope/engine/prerequisites.py:458(_named_files'sevals_dir not in candidate.parentsguard)
_is_answer_keyelsewhere in this same function explicitly guards against case-insensitive filesystems viasamefile, with its own docstring notingPath.resolve()does not canonicalise case — but the "is this candidate insideevals_dir" membership test is a plain, case-sensitivePath/string comparison. On a case-insensitive-but-case-preserving filesystem (e.g. macOS, where a contributor could build aclaude-codetask locally), a prerequisite argv path differing in case from the on-disk name could resolve to a real file insideevals_diryet fail this membership check, silently excluding it fromnamedwithout ever being copied. This fails toward not copying rather than toward a security hole, and is narrow/platform-dependent — not something this review executed.
Confidence 70 · logic · Fix: resolve both sides consistently (e.g. compare resolved absolute paths viasamefile/os.path.normcase-aware logic, mirroring the existing_is_answer_keyguard) rather than relying onPath.parentsmembership. -
machine.schema.json's own disclaimer of what it doesn't express omits two rules the parser actually enforces —skillscope/schema/machine.schema.json:54(theenvproperty description)
The description says the parser enforces "a command for every platform inos, and unique names, none of which this schema expresses" — but_prerequisite_env(skillscope/datasets.py:726-768) also enforces that env keys match_ENV_NAME(letters/digits/underscore, not starting with a digit) and that values contain no NUL byte, neither of which the schema'senvproperty ("additionalProperties": {"type": "string"}, nopropertyNamesconstraint) expresses, and neither of which the disclaimer lists.
Confidence 70 · documentation · Fix: add the env-name-shape and NUL-byte rules to the disclaimer alongside the ones already listed. -
A test exercising ordinary outage-classification logic is misplaced inside the prerequisites test class, and doesn't exercise prerequisites at all —
tests/test_skillscope.py:218-223(test_a_failed_skill_is_an_outage_only_when_its_detail_reads_as_one, insideTestProvidingAPrerequisite)
This test callsengine_behavioral._failed(...)and checksrouting_core.is_provider_error-driven outage classification — it never callsprovide()or touches any prerequisites machinery, and would still pass unchanged ifprovide()were deleted entirely.
Confidence 75 · test-quality · Fix: move it to whichever test class already coversbehavioral._failed/outage classification. -
Nested-subdirectory file copying for prerequisite steps is only validated against an in-memory fake sandbox, never a real one —
tests/test_skillscope.py(test_files_named_by_either_step_arrive_at_their_relative_paths,TestPrerequisitesInAContainer), the fake_Box.write_file(a plain dict assignment with no real filesystem semantics)
The real CI container smoke test (.github/workflows/selftest.yml'sprereq-skillfixture) only names a top-level file, never a nested path (e.g.tools/check.sh). Whether the real sandbox'swrite_fileauto-creates intermediate directories for a nested relative path is therefore asserted only by a mock standing in for that behavior, not proven end-to-end.
Confidence 72 · test-coverage · Fix: add a nested-path file to the CI smoketest's fixture skill, or otherwise exercise this against a real sandbox rather than the fake.
Decisions for the author
PREREQUISITE_INPUTS's override-protection check is case-sensitive, letting a differently-cased env key (e.g.skill_source_dir) bypass it on Windows — answered-by-intent.skillscope/datasets.py:741(if name in PREREQUISITE_INPUTS:) is a plain, case-sensitive tuple membership test, unlike_RESERVED_ENVa few lines above it (datasets.py:558-564), which is deliberately compiledre.IGNORECASEspecifically because "Windows environment names are case-insensitive." This review verified directly that_RESERVED_ENValready handles case correctly, whilePREREQUISITE_INPUTS's separate protection (guardingSKILL_SOURCE_DIR/SKILL_CACHE_DIRspecifically) does not — so a prerequisite could silently set the very inputs the design says it "may not override," on a case-insensitive platform. This would otherwise have been a non-blocking logic finding (confidence ~78), but the PR description's own "Scope" section already discloses: "Windows environment names are compared case-sensitively against the reserved names." Confirm whether that note is intended to cover this exact gap (it reads as the same symptom, even though the specific set involved —PREREQUISITE_INPUTSrather than_RESERVED_ENV— isn't named) before treating it as already tracked.- Schema is looser than the parser on "stripped non-empty" requirements — non-blocking-improvement.
_argv'sargv[0]andmachine_prerequisites'snameboth require.strip()to be non-empty, but the JSON schema only has"minLength": 1on the corresponding properties, so e.g.check: [" ", "foo"]orname: " "would validate against the schema and then be rejected by the parser. PREREQUISITE_INPUTSand_ENV_INPUT's regex duplicate the same two names by hand — non-blocking-improvement. Nothing enforces that a future third input added to one is reflected in the other.contextlib.suppress(Exception)around the container engine's best-effort cleanup doesn't suppressBaseException(e.g.asyncio.CancelledErrorfrom a second, overlapping cancellation) — tradeoff. If the cleanuprm -rfitself is cancelled, the original prerequisite failure's message is replaced by a bareCancelledError, which contradicts the comment immediately above this code ("never in place of the error being raised") — but this is a narrow double-failure race and may be a deliberate "don't spend more time on cleanup" choice.
Positive signals
TestAPrerequisiteDeclarationIsCheckedUpFront's full parametrized table was traced branch-by-branch against the parser (unknown/missing keys, the$-expansion rule, case-insensitive reserved-name rejection, name-format/NUL-byte rules, per-platform gap detection); every case hits the specific branch its position implies, and would fail outright if the corresponding parser code were reverted._ENV_NAME'sfullmatch(correctly rejecting a trailing-newline name, wherematchwould accept it) and the${SKILL_SOURCE_DIR}/${SKILL_CACHE_DIR}stripping regex were both verified directly against adversarial inputs and behave exactly as documented, with no bypass found.- The watchdog/hard-exit tests (
test_the_watchdogs_hard_exit_kills_a_running_install_and_its_children,test_a_step_that_appears_after_the_snapshot_is_killed_too) block on real process-liveness polling rather than asserting against mocks, so they would hang or fail — not silently pass — if the kill logic were broken. - Env isolation is real, not assumed: a fresh env is computed per skill (host) or per case (container),
os.environis never mutated, and nothing a skill installs is visible to the next one. - The CI container smoke test's fixture (
argv-listcheck/install,${SKILL_CACHE_DIR}-onlyenv) matches the real parser's contract exactly, and is correctly gated toubuntu-latestonly, consistent with the Linux-only assumption this feature documents.
Deployment notes
setup_sessionentry points inevals/hooks.pyare now refused unconditionally in favor ofprerequisites:— re-verified this round (_require_hook_support's message,hooks.py's own docstring, anddocs/authoring-evals.mdall agree). This is the intended migration, but is a breaking change for any skill still shipping the old hook (ROCm/rocm-systems'thread-trace, named in the PR description, is migrated here; itsdecode-and-triagesibling, using acheckhook, is explicitly left refused).- GitHub reports
mergeable_stateasblocked(review required) rather than a merge conflict, at the time of this review. - The description's own "Not verified" and "Scope" disclosures (no full ROCm GPU pass, container leg not tested against real Docker outside CI, Windows/macOS host branches only unit-tested) are honest rather than overclaiming, and line up with real gaps this review independently found (e.g. the nested-path container-copy test only using a fake sandbox).
What this covered
Every changed file read in full against the PR head by four independent fan-out workers grouped by coupling: skillscope/datasets.py, skillscope/schema/machine.schema.json, docs/authoring-evals.md, docs/usage.md (schema/parsing/docs group); skillscope/engine/prerequisites.py (host path), skillscope/engine/hooks.py, skillscope/engine/no_sandbox.py (host engine group); skillscope/cli.py, skillscope/selection.py, .github/workflows/skill-evals.yml, README.md, pyproject.toml (CLI/selection/CI group); skillscope/engine/prerequisites.py (container path), skillscope/engine/verify.py, tools/sandbox_smoketest.py, .github/workflows/selftest.yml (container engine group); plus the relevant classes in tests/test_skillscope.py and tests/test_harness.py read by whichever group owned the production code they exercise. All 8 CI checks on this head commit are green (unit tests on ubuntu/windows × Python 3.10/3.12, sandbox machinery on both OSes, unused-imports, action-against-a-throwaway-repo).
Did not run: agent-instruction adherence (no CLAUDE.md/AGENTS.md exists in this repository). No worker executed the test suite directly against a checkout of this head (production code was read via git show, not checked out, per the review method's rule against checking out a PR head) — conclusions about test behavior on revert are from tracing, not execution, except where a worker explicitly built a separate scratch clone to run something (noted inline above where that happened). Not reconciled against this PR's existing 9-comment thread, or against any earlier review round on a prior head of this branch — this pass was kept blind to both by design, per the review method's independence rule.
Not a human review. This is an automated code review; please verify findings independently before acting on them.
Review — no blocking findingsFull review of the whole change (19 files, +2934/-110 across 11 commits) at head BlockingNone Non-blocking
Decisions for the author
Positive signals
Deployment notes
What this coveredEvery file in the change read completely at the head commit, plus the surrounding code needed to judge it: the full Did not run: the "prior changes" source-reading pass (no record of review comments on earlier, separate changes to these files exists outside this PR's own discussion, which the method keeps out of a blind review). "History" ran as ordinary Not reconciled against any PR discussion, including a prior automated review on this same PR — this is a blind review. 🤖 Automated review by an agent. |
There was a problem hiding this comment.
Review — needs work
Full review of the whole change (base 7a8c00b9…head 13408520, +2934/-110 across 19 files, 11 commits). No ticket named; the description is a long, detailed, and unusually candid account of the change, including explicit "Not verified" and "Scope" sections. Judged against it at Step 9: its claim that a prerequisite failure "fails its own skill's cases, and only those... It is never counted as a provider outage" is contradicted by the two unwrapped-exception sites in Blocking below.
Blocking
-
Two prerequisite-provisioning failure paths escape as raw exceptions instead of
PrerequisiteError, contradicting the PR's own stated contract that a prerequisite failure stays local to one skill and is never read as a provider outage —skillscope/engine/prerequisites.py:241(_make_cache'stempfile.mkdtemp(...), called atprerequisites.py:389beforeon_host's owntry/finallybegins) andskillscope/engine/prerequisites.py:535-538(the container copy loop,for relative, path in named.items(): await box.write_file(...), inside a bareexcept BaseException: ...; raisethat re-raises unchanged rather than converting toPrerequisiteError).
Traced both to their actual consumers:behavioral.py's per-skill handling (behavioral.py:377/385/394) only catchesPrerequisiteError/KeyboardInterrupt/SystemExit, so a rawOSErrorfrommkdtemp(disk full, no/tmp, permission denied) propagates out of thefor skill in skills:loop and crashes the whole command instead of failing just that skill. On the container side,behavioral.py's_is_outage()decides "not an outage" only by finding the qualifiedPrerequisiteErrorclass name in the sample's traceback — an unwrappedwrite_file/read_bytesfailure has no such name in it, so it can be misclassified as a provider outage, the opposite of the documented intent. No test makes either call raise.
Confidence 85 · architectural · Fix: wrap both call sites' expected failure modes and re-raise asPrerequisiteError, matching the pattern already used forcheck/installfailures and theexec_errors/housekeeping()catches elsewhere in this same module. -
skill-evals.yml's new "machine.yml could not be planned" handling in theresultsjob is unreachable through this workflow's own job graph —.github/workflows/skill-evals.yml:760-779,skillscope/selection.py:179-193(matrix_entries'sexcept SystemExitwraps onlydatasets.machine_plan(skill)),skillscope/datasets.py:457-508(structural_errors, called repo-wide viacli.py:300'scmd_structural→cli.py:240's_structural_or_exit()with no skill scoping).
Traced the full call graph:structural_errorscatchesSystemExitfrommachine_plan(skill)in the same try block as_prerequisite_gaps(...), over every skill in the repo (declared_skills()), and this runs indiscover's unconditional "Check every skill's structure" step.matrix_entries(whatselectuses to buildresults'sSKIPPEDlist) only ever catches aSystemExitfrommachine_plantoo — it never callsmachine_prerequisites/_prerequisite_gapsat all, so it cannot even report a brokenprerequisites:section, only a broken top-levelos/labels/unknown-key error. Sincestructural_errorsis a strict superset check run repo-wide ahead ofselect, any failure that could makeselect'sSKIPPEDlist carry anerrorentry would already have faileddiscover's structural step — andresults's scriptsys.exits onDISCOVER != "success"(line just above this hunk) before ever reaching the newunplanned/::error::code. The code's own comment ("usually the structural step has already failed... this is for when it has not") does not hold given this call graph: this reviewing context found no path that producesDISCOVER == "success"alongside aSKIPPEDentry carryingerror, and the diff's own test for this path constructs that exact combination manually, which the real pipeline cannot.
Confidence 85 · architectural/test-coverage · Fix: either confirm there is a real caller this defends (e.g.selectinvoked outside this workflow withoutstructuralfirst) and say so in the comment, or drop the now-dead branch. -
_argv's whitespace-only-first-argv-item rejection has no test anywhere in the suite —skillscope/datasets.py:689-701(thenot value[0].strip()arm of theor-chain).
Every other branch of this new validation is covered by a dedicated subTest inTestAPrerequisiteDeclarationIsCheckedUpFront; this is the one with none — confirmed by reading that class's full parametrized table. Reverting just thisorarm leaves the suite green.
Confidence 82 · test-coverage · Fix: add a case likeinstall=[" ", "x"]. -
test_a_gate_shared_by_two_held_skills_is_named_oncedoes not exercise what this diff actually changed —tests/test_skillscope.py:1234-1241,skillscope/selection.py:287.
The diff's only change toplan()'sgatesline is addingif leg["gate"]to an already-existing set comprehension (sorted({leg["gate"] for leg in skipped if leg["gate"]})). This test gives both skipped legs the same non-empty gate, so the pre-diff code (sorted({leg["gate"] for leg in skipped}), dedup via the set alone) produces the identical["enable_gpu_ci"]— reverting theif leg["gate"]filter leaves this test green. The filter itself is exercised elsewhere (test_one_bad_machine_yml_does_not_drop_the_matrix, which checks the empty-gate placeholder is excluded), so there is no live coverage gap, only a misleadingly-named test.
Confidence 85 · test-quality · Fix: rename it to reflect what it actually asserts (plain gate dedup, pre-existing), or add a legs-with-one-empty-and-one-real-gate case that the filter alone decides.
Non-blocking
machine.schema.jsondoesn't disclose thatargv/nameare stripped-non-empty, unlike its ownenvproperty, which does disclose its parser-only rules —skillscope/schema/machine.schema.json(theenvdescription lists what the schema can't express; the siblingargv/nameminLength: 1properties have no equivalent note, though_argvandmachine_prerequisites'snameboth additionally require.strip()). The schema is not runtime-enforced anywhere, so this is a documentation-accuracy gap, not a functional one. Confidence 75 · documentation · Fix: add the same kind of disclaimer next toargv/name.selection.py's new module/matrix_entriesdocstrings say a leg is held back when itsmachine.yml"could not be read", but the mechanism also covers a file that parses fine yet fails validation (bados/labels/unknown key) — confirmed by the diff's own non-UTF8/unreadable/YAML-rejection test battery, which includes cases that are readable text but still fail. Confidence 75 · documentation · Fix: reword to "could not be read or planned."- The real CI/Docker container smoketest (
selftest.yml'sprereq-skillfixture) never exercises nested-subdirectory file copying — only the unit-test suite's in-memory fake sandbox (TestPrerequisitesInAContainer's_Box.write_file, a plain dict assignment) does. Some test does reach this path, so it is not an uncovered-behaviour gap in the strict sense, just unverified against a real sandbox's directory-creation semantics for behaviour the module's own docstring calls out as important. Confidence 70 · test-coverage · Fix: add a nested-path file to the CI fixture.
Decisions for the author
CONTAINER_ROOT = "/opt/skillscope"is a fixed path baked into the image contract before any concrete skill is confirmed to depend on the container leg — tradeoff. The PR's own motivating example (thread-trace) is a host-engine case; nothing in this diff's tests or docs names a skill that actually needs--engine claude-codeprerequisites today. Pinning the path now buys a stable target for whoever bakes the first image against it; it also means moving it later is a breaking change for that image. Worth a docs line confirming the path is provisional until a real consumer exists, if that is still the intent.- A later prerequisite in the same skill's list can silently overwrite an earlier one's identically-named
envkey — non-blocking-improvement.provide()'s accumulation (applied.update(env)) has no duplicate-key check across steps. The function's own framing treats later-wins as intentional layering (same pattern as the harness-vs-prerequisite layering elsewhere in this PR), so this is not obviously wrong, just unguarded. contextlib.suppress(Exception)around the container engine's best-effort cleanup (prerequisites.py:557) does not shieldBaseException(e.g.asyncio.CancelledError) — tradeoff. If the cleanuprm -rfis itself cancelled while handling an earlier failure, the original, more informative error (e.g. aPrerequisiteErrorcarrying the real install output) is replaced by a bareCancelledError, arguably at odds with the adjacent comment ("never in place of the error being raised"). This requires two overlapping cancellations to trigger and the existing tests only exercise the first; given the "best effort" framing already applied to this cleanup elsewhere, this may be an accepted gap rather than an oversight — confirm.
Positive signals
TestAPrerequisiteDeclarationIsCheckedUpFront's full parametrized table traces nearly everySystemExitbranch in the new validation to a dedicated, wording-specific subTest, including a defensivekey=strsort for mixed-type YAML keys.- A new bidirectional test in
tests/test_harness.py(test_a_skills_prerequisites_reach_the_agent_under_what_the_harness_sets) proves the harness-vs-prerequisite environment layering order both ways: it fails if the layering is reverted and if the merge order is swapped, not just on one mutation. - The container engine's check-vs-install distinction and the
PrerequisiteError/_is_outageclassification are verified end-to-end through a realinspect_eval()call rather than mocked at the boundary. matrix_entries's new per-skilltry/exceptisolating one brokenmachine.ymlfrom the rest of the CI matrix is covered by six distinct, well-targeted scenarios (invalid YAML, non-UTF-8, unreadable, several non-YAMLErrorPyYAML rejections).
Deployment notes
evals/hooks.py'ssetup_sessionentry point is now refused unconditionally in favor ofprerequisites:. Per the PR description, ROCm/rocm-systems'thread-traceis migrated; itsdecode-and-triagesibling (acheckhook) is explicitly left refused as a named follow-up.CONTAINER_ROOT/CONTAINER_CACHEunder/opt/skillscopeis a fixed container path; once any image is baked against it, moving it becomes a breaking change for that image (see Decisions for the author).- All 8 CI checks on this head are green (confirmed via
gh pr view --json statusCheckRollup): unit tests on ubuntu/windows × Python 3.10/3.12, sandbox machinery on both OSes, unused-imports, and action-against-a-throwaway-repo.
What this covered
Every file in the change read completely by four independent fan-out workers grouped by coupling — declaration/schema/docs (skillscope/datasets.py, skillscope/schema/machine.schema.json, docs/authoring-evals.md, docs/usage.md); host engine core (skillscope/engine/prerequisites.py's host path, skillscope/engine/hooks.py, skillscope/engine/no_sandbox.py, skillscope/engine/behavioral.py, tests/test_harness.py); CLI/selection/CI (skillscope/cli.py, skillscope/selection.py, skillscope/structure.py, .github/workflows/skill-evals.yml, README.md, pyproject.toml); container engine + smoketest (skillscope/engine/prerequisites.py's container path, skillscope/engine/verify.py, tools/sandbox_smoketest.py, .github/workflows/selftest.yml) — plus the relevant classes in tests/test_skillscope.py each group owned. Commit range 7a8c00b9…13408520.
This reviewing context then independently re-traced every candidate finding reported at confidence ≥70 directly against the primary source before including it here (not taking any relayed summary at face value), including full function-level call-graph tracing for both Blocking findings above.
Provenance note, in the interest of full disclosure: this task's own duplicate-review check (confirming no existing review on this PR carries this reviewing setup's required marker) required reading the full text of the three existing reviews on this PR before any findings-producing work began. The four fan-out workers were never shown that content and were explicitly instructed not to read any PR discussion, and this report's findings were independently re-derived and verified from source rather than copied — but this reviewing context cannot claim to have been fully blind to that prior content when scoping the workers' instructions, and several findings above (the unwrapped-exception sites, the CONTAINER_ROOT question) converge with points already raised in the existing human review at this same head. The job-topology dead-code finding and the weak-test finding are not present in the prior reviews read.
Did not run: agent-instruction adherence (no CLAUDE.md/AGENTS.md exists in this repository, confirmed directly). Prior-changes pass did not run — no record of review comments on earlier, unrelated changes to these files was sought. History pass was light-touch only (git log on the core touched files); nothing in it contradicted this change.
Not reconciled against this PR's existing review threads' specific arguments or rebuttals — only their presence/absence of the required marker was checked, per this task's own instructions.
Automated review by an agent.
Two provisioning steps still raised past the per-skill handling. On the host, an OSError from making the skill's SKILL_CACHE_DIR (a full disk, an unwritable temp directory) reached `behavioral.run` as a bare exception and ended the run for every skill after it. In a container, a failure to read or copy a file a step names came out as a bare sandbox error, which the outage heuristic could read as a provider outage. Both are now a PrerequisiteError naming what failed, and the copied files are still removed. The cleanup comment now says the one thing that can replace the error being raised: a cancellation of the removal itself, which is left to propagate.
An env name that only differed in case from `SKILL_SOURCE_DIR` or `SKILL_CACHE_DIR` got past the check that keeps a prerequisite from setting them, although Windows reads it as the same variable. The same case rule now covers them as it already covered PATH and the harness names. Two prerequisites setting one variable, or one setting two names that differ only in case, is refused too: which value the agent got would depend on which prerequisites the machine already had, or on the host's case rules, and the log would not say. `COPILOT_*` joins the reserved names: the Copilot harness sets some of them, and on the host its value wins, so a declared one was silently dropped. The docs, the schema and the parser's message now say what is reserved and why, that nothing else is, and what setting Copilot's GH_TOKEN or GITHUB_TOKEN does. `_ENV_INPUT` is built from `PREREQUISITE_INPUTS`, so the two cannot drift.
- The `results` job's check for a skill `select` could not plan says that, in this workflow, the structural step stops discover on the same file first; it is a backstop for if the two ever check different skills. - `select`'s docstrings say a leg is held back when its machine.yml could not be read or planned, which is what the code does. - The docs say a prerequisite's steps may run once or many times, and on a machine that has it or not, so they have to give the same result. - The container smoke test names a step file below evals/, so the copy into a real container has to create the directory. - The test that `gates` names a shared gate once says it pins the set. The outage-classification test sits with the other tests of a skill that cannot run.
|
Thanks for the three reviews of 1340852: the automated review of 9 Nov, the review comment of 10 Nov, and the AgentHub review of 10 Nov. Three commits on top, pushed as a fast-forward, no force-push. Each new test was seen to fail when the code it covers is broken. Blocking
Non-blocking
Decisions for the author
The description is updated: the failure-locality and env bullets, the Scope list, and the test count. |
johnl-amd
left a comment
There was a problem hiding this comment.
Review — no blocking findings
Full review of the whole change (14 commits, 19 files, +3072/-110). No ticket named. The PR's own description is thorough and detailed, and matches what the review found: every disclosed limitation in "Scope" and "Not verified" corresponds to real, correctly-scoped behavior in the code, and no claimed-but-missing or undisclosed-but-present work was detected.
Blocking
None.
Non-blocking
verify.require()'s inspect-swe version-floor guard checks only foreffort, not for the newenvparameter the same call now always sends —skillscope/engine/verify.py:91-99(guard),skillscope/engine/verify.py:250-255(build_task'sagent()closure, which now unconditionally callsclaude_code(..., env=env or None)for every skill, prerequisites or not). The PR's own test,test_a_current_inspect_swe_is_accepted, builds a fakeclaude_codewith signature(skills=None, cwd=None, effort=None)— noenv— and assertsrequire()accepts it as current. The guard's own comment explains exactly why this matters ("the failure arrives as a TypeError underneath a traceback... observed exactly that way on a runner whose inspect-swe predated the argument and so satisfied the old floor without upgrading"), but that protection was not extended to the second capability this PR now depends on. I independently fetchedinspect_swe's source at its pinned floor tag (0.2.71, github.com/meridianlabs-ai/inspect_swe) and confirmedenvis already a parameter there, so no installation satisfying today's stated floor can currently trigger this — it's a latent gap in the guard's completeness, not a live bug.
Confidence 70 · logic · Fix: extend the check to"effort" not in params or "env" not in params, with a combined message.- The "missing input" explanation attached to a failed prerequisite install is a heuristic substring search, not a check of what the prerequisite's own
envactually references —skillscope/engine/prerequisites.py, insideprovide()(missing = "".join(... if name in said), around lines 188-196). In the container engine,unavailableunconditionally includesSKILL_SOURCE_DIRfor every skill (claude_code_agent, ~line 560), so any install failure whose output happens to literally contain the stringSKILL_SOURCE_DIRgets an unrelated "(was not available: ...)" note appended, while a genuine missing-input failure whose script doesn't print the variable name gets none. The code's own comment acknowledges this is approximate ("for an input the step mentions"), so this is a documented trade-off rather than an unrecognized defect, but it's a real source of misleading error text.
Confidence 70 · logic · Fix: key the explanation off whether the failing prerequisite's own declaredenvmap references the unavailable token (item.env, already available at the call site), not off a string search over argv/output. _kill_pid's Windows branch runssubprocess.run(["taskkill", ...], timeout=5)synchronously inside the async event loop —skillscope/engine/prerequisites.py,_kill_pid, called from_host_runner's coroutine (if _track_step(proc.pid): _kill_pid(proc.pid)). In the narrow race where the--timeoutwatchdog has already fired exactly as a new step is spawned, this blocks the event loop for up to 5s on Windows (the POSIX branch is a single non-blockingos.killpgsyscall). Not independently verified on Windows hardware; no test exercises this specific interleaving.
Confidence 70 · logic · Fix: offload toasyncio.to_threadif this path needs to coexist with other in-flight installs on Windows.
Decisions for the author
- An
oslabel typo (e.g.Liunx) is only caught when the skill also declaresprerequisites— tradeoff.datasets.py's_prerequisite_gapsreturns immediately for a skill with no declared prerequisites, somachine_plannever validatesosagainst the known platform set otherwise. This is explicitly disclosed indocs/authoring-evals.md("Anoslabel that is not linux, windows, or macos is reported too, when this skill has prerequisites: a typo would otherwise skip that check") and in the PR's own "Scope" section, so it is a deliberate, documented boundary rather than an accidental gap. A stronger version would validate every skill'soslabel unconditionally inmachine_planitself. - Prerequisite
nameuniqueness is compared case-sensitively, unlike every other identity check in the same function (env-var names, the two reserved inputs, and Windows path-name collisions are all folded case-insensitively) — non-blocking-improvement.nameis used only for display/log purposes, so two prerequisites namedtoolandToolbeing treated as distinct is low-impact, but it is an inconsistency in an otherwise very deliberately case-folded piece of code.
Positive signals
provide()(the function implementing "check, then install, then re-check with env") is small, pure, and exhaustively unit-tested on its own, independent of either engine's plumbing — the PR's central claim, "one declaration, applied identically by both engines," is enforced by sharing a single tested function rather than by convention between two parallel implementations.- The hard-exit watchdog's cache/process bookkeeping (
_make_cache,_track_step,_kill_live_steps,_discard_all_caches, all under one lock) correctly closes the race its own comments describe, and is specifically exercised by tests that interleave a realmkdtempcall with the watchdog firing mid-call — a genuinely difficult concurrency surface to get right, and to test at all. - Across the 14-commit series, every fix a later commit claims to make for a bug an earlier commit left behind (case-insensitive answer-key bypass, the "Liunx" os-label gap, a cache-creation/watchdog lock race, case-insensitive env-name collisions, unhandled container copy/mkdir failures) was independently traced and confirmed present and correct at HEAD — none were found only half-applied.
- Command execution throughout (
_host_runner, the containerrun()closure) always uses argv lists viaasyncio.create_subprocess_exec/box.exec, never an interpolated shell string. - The answer-key exclusion when copying files into a case's container (
_is_answer_key'ssamefile()-based check) is tested against a real hard link, not just a path-string comparison, proving the identity check is load-bearing rather than decorative.
Deployment notes
- A
prerequisites:step'sinstall/checkcommands run with the same trust level as the rest of a skill: unsandboxed on the host under the default engine, same as the agent itself already runs there. This is explicitly documented ("steps are trusted like the rest of the skill") and is not a new trust boundary relative to what the engine already does, but is worth restating for anyone pointing the default engine at untrusted skill submissions. - The container engine's cache path (
/opt/skillscope/cache) and steps path (/opt/skillscope/steps) are a new fixed convention; a skill's own pre-builtsandbox:image that wants to avoid repeatinginstallper case needs to bake artifacts at that exact path, and both the code and docs call the path "provisional." - No behavior change for any existing skill that does not declare
prerequisites:— confirmed the empty-declaration path is a no-op on both engines.
What this covered
Full change read completely across four independent fan-out passes grouped by coupling: (1) declaration/schema/docs — skillscope/schema/machine.schema.json, skillscope/datasets.py, docs/authoring-evals.md, docs/usage.md, README.md; (2) host engine core — skillscope/engine/prerequisites.py, no_sandbox.py, hooks.py, behavioral.py; (3) CLI/selection/CI — skillscope/cli.py, selection.py, structure.py, pyproject.toml, .github/workflows/skill-evals.yml; (4) container engine + CI smoketest — skillscope/engine/verify.py, tools/sandbox_smoketest.py, .github/workflows/selftest.yml (with prerequisites.py and engine/sandbox.py read in full as shared/callee context). All four workers read their full assigned scope and reported no inability to do so. Diffed 7a8c00b9ea153c5efc5679f60d7a5e9df0deb056…f6ed7bbc8628e6195841e47524ed3bc6dc951f07, fetched read-only and never checked out.
Every test class the PR added or changed, in both tests/test_skillscope.py and tests/test_harness.py, was read completely and given an explicit "would it still pass if the corresponding production change were reverted" / "does it exercise the specific branch it names" verdict; several were independently confirmed via mutation testing in disposable scratch clones outside the reviewed repository. CI for this exact head commit was read directly (gh pr view --json statusCheckRollup): all 8 checks (unit tests on Linux/Windows × Python 3.10/3.12, sandbox machinery, unused imports, action-against-a-throwaway-repo) report SUCCESS.
Did not run: agent-instruction-adherence (no CLAUDE.md/AGENTS.md exists in this repository) — recorded as skipped in every pass, not silently omitted. Prior-changes pass (review comments on earlier changes to these files) did not run: no mechanism was available to retrieve review comments on other, earlier PRs touching these files, as distinct from this PR's own discussion, which this review's independence rule forbids reading at any point. Not reconciled against any discussion on this PR — no review comments, review threads, or prior reviews on PR #29 were read before or during this review, per its governing instructions.
Not a human review. This is an automated code review; please verify findings independently before acting on them.
|
Thanks for the review of f6ed7bb. Nothing blocking, so no new commits. Answers to each point:
|
|
@johnl-amd your re-review of f6ed7bb found nothing blocking, and CI is green on that head. Would you approve it so it can merge? |
Why
A skill whose behavioral cases need something the runner may not have, such as a library to build or a Python package to install, had one way to say so: a
setup_sessionentry point inevals/hooks.py. The inspect engines refuse that entry point, so such a skill can no longer run its behavioral leg. ROCm/rocm-systems'thread-traceskill is one: its hook builds the rocprof trace decoder and exports the variables the agent needs.Re-adding the hook would not have been enough. A hook runs in this process, so it can only prepare the host. A skill that also runs under
--engine claude-codewould have to describe its environment a second time, in a compose file for the container, and the two descriptions would drift.What changes
evals/machine.ymltakes aprerequisites:list. Every engine reads that one declaration and applies it where its agent runs.checkruns first. If it passes, the prerequisite is already there and itsenvis not applied. Otherwiseinstallruns, thencheckruns again withenvapplied, and only then doesenvreach the agent.--engine claude-code, in each case's container, with the agent built per case from the resulting env. A command may differ per platform, and a container counts aslinux.SKILL_SOURCE_DIR(the skill's source tree, host only) andSKILL_CACHE_DIR(scratch space, removed after the skill). Anenvvalue may name those two and nothing else. It replaces a variable rather than extending it. It cannot setPATH, the names the agent's harness and model client read (ANTHROPIC_*,CLAUDE_*,COPILOT_*,IS_SANDBOX), or the two inputs themselves, in any case, and no variable may be set twice.os.environis never changed.claude-codeonly the files a step names are copied into the container, and they are removed before the agent starts. A step that namesevals.jsonorextended_evals.jsonis refused.machine.ymlholds back only its skill. One that cannot be read (not UTF-8, or not readable by the runner) or does not parse is reported as that skill's error, andselectand the structural check go on to the other skills rather than stopping at it.selectlists the skill as held back with the error, and theresultsjob inskill-evals.ymlfails on it with that error instead of reporting it as a missing gate label. A gate that holds back several skills is named once.--timeoutstop ends a running install and every process it started, whether the run stops gracefully or the watchdog kills it outright. Results already graded are kept.oshas a command.setup_session. Its refusal now points atprerequisites:.docs/authoring-evals.mddocuments the key, including how to bake an install into asandbox:image so a container does not repeat it for every case.solver_factorythatbehavioral.build_tasktakes now receives(skill_dir, env). The CLI andtools/sandbox_smoketest.pyare updated.Verification
ruff --select F401is clean.machine.ymlholding back only its skill, and theresultsjob's reading of it;--modeland--effort.eval(), with a stubclaudethat records the environment it was started with. A stop during an install is delivered for real, and the test checks that both the install and the child it started are gone.prerequisites:declaration, the skill is no longer refused on the default engine. Run in a sparse checkout that lacks the decoder source, its install fails, and the skill's cases are reported failed with the install's own message.Not verified:
thread-tracepass: it needs a ROCm GPU.Scope
Left out, as follow-ups:
--timeoutbounds a hung install. A step also waits for every process holding its output open, so an install that leaves a server running does not finish. The docs say both.checkpasses.linuxcommand. It checks the platforms inos, but a container is alwayslinux, so amacOS-only declaration passes there and fails underclaude-code.claude-codetask is built, before any case runs, not by the structural check.checkhooks inevals/hooks.pystay refused. ROCm/rocm-systems'decode-and-triageuses one, and replacing it needs its own design.