Repository navigation
Conversation
A report says which prompts passed and nothing about what was grading them. `model` in the meta block is the alias the caller asked for, and the target behind `opus` moves; the CLI that discovers and activates a skill updates on its own schedule and is not recorded at all. Two runs a week apart can disagree and the artifacts hold nothing that separates a skill getting worse from a dependency that changed. Three fields, taken where they are already free: the engine as a named constant, the CLI's own version, and the model the API actually served, read out of the preflight call that every graded run already pays for. The preflight reports usage per model rather than naming the one that ran, and bills auxiliary work to a second model in the same block, so the alias that was asked for is what picks the right entry. Signed-off-by: John Lybeck <john.lybeck@amd.com>
The extraction is the part that can be quietly wrong. A preflight result reports usage per model rather than naming the one that ran, and the CLI bills auxiliary work to a second model in the same block, so reading the first entry reports a model the run never asked for. That is not a hypothetical: it is what the first version did against a live API. Eleven cases covering which entry an alias claims, the canonical name winning over the key it was billed under, the fallback when no alias was given, output that names no model at all, an absent CLI, and the three fields arriving in every report. Signed-off-by: John Lybeck <john.lybeck@amd.com>
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review · review-code · head 86d8d161
Review — needs work
Full review of the whole change.
No ticket named. PR #21 describes adding engine/agent_cli_version/model_resolved to the JSON meta block of routing and behavioral reports; the shipped diff matches that description exactly (same 3 files, same field set, and the PR's own before/after example illustrates the feature purely via the JSON artifact, not the markdown report).
Blocking
resolved_model()'s docstring overclaims whatNonemeans —skillscope/agent.py:127-135, corroborated byskillscope/agent.py:180-217(check_api_reachable)
The docstring says: "This is None when the preflight was skipped, which is honest: nothing has spoken to the API, so nothing knows." Butcheck_api_reachablereturns(True, "ok")based solely onproc.returncode == 0(line ~213), independent of whether_model_from_result(proc.stdout or "", model)(line 217) actually extracts a model. If the preflight reaches the API successfully but the JSON payload doesn't parse or carries no usablemodelUsageentry (a shape_model_from_resultalready has gracefulreturn Nonepaths for),_RESOLVED_MODELis set toNoneeven though the API was reached.resolved_model()then returnsNone, which the docstring wrongly equates exclusively with "preflight was skipped." The PR description repeats the identical framing in prose ("It isnullwhen--skip-preflightwas passed, which is honest...") while separately acknowledging elsewhere that malformed/emptymodelUsagealso degrades tonull— so the author knows these are different cases, but the documented contract doesn't say so, inviting a future caller to treat "didn't ask" and "asked, got an ambiguous answer" as the same thing.
Confidence 78 · logic · Fix: soften the docstring to "Nonemeans the API was never asked, or was asked but didn't say which model answered" rather than tyingNonesolely to "skipped"; optionally havecheck_api_reachablesurface the parse failure distinctly.
Non-blocking
ENGINE = "legacy"collides with a term the codebase already uses for something else —skillscope/cli.py:86(and its comment at ~84-85) vs.skillscope/routing.py:5,34andskillscope/behavior.py:13
routing.py's module docstring opens "Routing engine: does the right skill fire..." and closes "...this module is the engine.";behavior.py's docstring likewise ends "...this module is the engine." Those are two pre-existing, distinct "engines" (one per grading module).agent.py's own new comment on_RESOLVED_MODEL("every engine wants it in its report") uses the word in that same established plural sense.ENGINE = "legacy"introduces a third, singular, repo-wide sense (a harness-generation label), and bothcmd_routingandcmd_behavioralreports carry the identical"engine": "legacy"value regardless of which of the two established "engines" produced the report — so the field cannot answer "which engine ran this," only "which harness generation ran this," despite reusing a name the codebase already assigns a narrower meaning.
Confidence 72 · architectural · Fix: rename the new field (e.g.harnessorgrading_engine_version) to avoid the collision, or fold the command type into the value.- New ambient module-global has no setter, unlike the codebase's established convention —
skillscope/agent.py:101(_RESOLVED_MODEL),:127-135(resolved_model()),:216-217(mutation insidecheck_api_reachable);tests/test_skillscope.py:2520-2521, 2568, 2575
The codebase has two prior precedents for "ambient state for the current process":deadline.py's_active/active()/use(bound)andconfig.py's_active/active()/use(config)— both pairs install a new value through a public setter that returns the previous one, and every existing test goes through that setter. This PR adds a third such global (_RESOLVED_MODEL) with only a getter (resolved_model());check_api_reachablemutates it via a bareglobalstatement, and the new test class is the only place in the suite that reaches into a module's private ambient-state variable directly (agent._RESOLVED_MODEL = None, restored viaaddCleanup(setattr, ...)). No production path is affected today (every realskillscopeinvocation is a fresh process), but the inconsistency is visible and the test workaround it produces is distinctive.
Confidence 75 · architectural · Fix: add ause_resolved_model(value)mirroringdeadline.use/config.use, and have the test class call it instead of poking the private attribute.
Decisions for the author
- New provenance fields never reach the markdown report, only the JSON artifact — answered-by-intent
_provenance()(skillscope/cli.py:89-103) is spliced into themetadict atcli.py:422and:473, butrouting.render_markdown()(routing.py:701+) andbehavior.render_markdown()only ever readmeta['model']/meta['effort']/meta['skills']— neverengine,agent_cli_version, ormodel_resolved._write_report(cli.py:120-138) writes the fullsummarydict (meta included) to the JSON file, but only the markdown string to stdout and$GITHUB_STEP_SUMMARY— the channel README.md:71 documents as the human-facing report. This would otherwise have been Blocking (confidence 85, logic): the only surface most readers see never shows data the change's own rationale frames as being for exactly that kind of reader. It is retired by the PR's own "What it looks like" section, which illustrates the feature entirely through JSON: "Before, two reports a week apart:routing-20260907-143545.json 6 of 8/routing-20260914-101200.json 4 of 8" and an "After" example shown as a raw JSON meta-block snippet ("model": "sonnet", "engine": "legacy", ...). The author's own comparison workflow is file-to-file JSON diffing, not the markdown summary. Confirm that's still the intended usage surface — if reviewers are in practice expected to compare via CI's step summary rather than downloaded JSON artifacts, this gap reopens. - Alias-substring matching has no tie-break for multiple hits — tradeoff
_model_from_result's alias branch (agent.py:138-176) returns the firstmodelUsageentry (in dict/insertion order) whose name contains the alias substring; the no-alias branch instead disambiguates by highestoutputTokens. If a preflight's billing block ever contains two entries both matching the alias (e.g. two dated snapshots of the same model family), the alias branch silently picks whichever came first rather than the one that did the most work. Given today's model-naming conventions this is unlikely to collide, and simplicity has value — but the no-alias branch already shows the more robust option is cheap to apply here too. Confirm the current asymmetry is deliberate. - Process-lifetime
lru_cacheoncli_version()— non-blocking-improvement
cli_version()is decorated@lru_cache(maxsize=1)(agent.py:104), caching for the lifetime of the Python process. Harmless for the one-shot CLI entry point (one process, one invocation), but any future caller importingskillscope.agentas a library in a longer-lived process (a notebook, a service, a test harness callingmain()repeatedly) would get a silently stale CLI version with no automatic reset — unlike_RESOLVED_MODEL, which is at least reassigned on every preflight. Worth a short note if this module is ever expected to be used outside a fresh-process CLI invocation.
Positive signals
_model_from_result's alias-disambiguation logic is tested against a real, previously-encountered failure (per the PR's test plan: an early version returned an auxiliary session-title model for asonnetpreflight against a live API) rather than a hypothetical — the regression test is pinned to that behavior, not to the code's shape.- Test isolation discipline is solid despite the new mutable module state:
setUp/addCleanupsave and restoreagent._RESOLVED_MODELand explicitly clearagent.cli_version'slru_cache, and the mocks patch real dependencies (shutil.which,subprocess.run) rather than the functions under test, so the tests exercise real parsing logic.
Deployment notes
- GitHub reports this PR's
mergeable_stateasdirty(merge conflicts against currentmain) at the time of this review —mainhas moved substantially past this PR's merge-base for unrelated reasons, so a rebase will be needed before merging regardless of this review's findings. - Purely additive: three new JSON keys in an already-schemaless
metadict (noadditionalProperties: falseconstraint anywhere on the run-report shape, unlike the unrelatedskillscope/schema/machine.schema.jsonwhich governs a different file). No flag, config, or migration changes.
What this covered
Read completely, at pr21-base (0f734b2dd344cd72b5e016ad6c3b1ac2ce36f6f3) and pr21-head (86d8d16134d8deb82553c16dc3263724459ad59f): skillscope/agent.py, skillscope/cli.py, and tests/test_skillscope.py in full (not just the diff hunks), plus skillscope/routing.py and skillscope/behavior.py's summarize()/render_markdown(), docs/usage.md, README.md, pyproject.toml and tools/compare_skill.py (checked as possible consumers of the new fields; none found). Three independent background workers ran the code pass (4a) and the history, prior-changes, and code-comments passes (4b), each blind to the PR description and to each other's output; all three completed and reported findings, none failed or needed re-spawning. Findings were consolidated and verified against source directly by the orchestrator before the PR description was read (Step 9).
Did not run: agent-instruction-adherence pass — no CLAUDE.md or AGENTS.md exists anywhere in this repository at pr21-head (confirmed by tree listing), so there is nothing to check. Did not independently execute the test suite; relied on GitHub's check-runs API for commit 86d8d16134d8deb82553c16dc3263724459ad59f, which reports all 5 checks (Unit tests on ubuntu/windows × Python 3.10/3.12, plus Action against a throwaway repo) as completed/success, taken at face value.
Not reconciled against any discussion: this PR has no existing reviews or review comments, so there was nothing to reconcile.
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 coveredRead completely: Did not run: agent-instruction adherence (no 🤖 Automated review by agent-hub on AMD AgentHub. |
Summary
metablock of every routing and behavioral report:engine,agent_cli_version, andmodel_resolved. New helpersagent.cli_version()andagent.resolved_model(), joined bycli._provenance()and splatted into both meta dicts.modelis the alias the caller asked for, and the target behindopusmoves. The CLI that does the discovering and activating updates on its own schedule and is not recorded at all. Two runs a week apart can disagree and the artifacts hold nothing that separates a skill getting worse from a dependency that changed.What it looks like
Before, two reports a week apart:
After:
Same model, new CLI build, so the CLI is where to look.
Where each field comes from
engineis a constant. There is one engine, and naming it means a reader does not have to date a report to work out what graded it.agent_cli_versionisclaude --version, cached for the process. One subprocess, no tokens.model_resolvedis read out of the preflight call every graded run already pays for, so it costs nothing. It isnullwhen--skip-preflightwas passed, which is honest: nothing spoke to the API, so nothing knows.The preflight result reports usage per model rather than naming the one that ran, and the CLI bills auxiliary work to a second model in the same block (a run asking for
sonnetcan come back with a session-title model listed beside it). The alias that was asked for is what picks the right entry; with no alias, the entry that did the most work.Test plan
TestARunRecordsWhatProducedIt. Full suite 237, green onpython -m unittest discover -s tests -t ..gpt-5.5for a run that asked forsonnet, against a live API. Reverting the fix with the tests in place turns that test red and nothing else, so it is pinned to the behaviour rather than to the shape of the code.skillscope routing --skills-dir 'skills/*' --routing-room all --only finetune-on-laptop --model sonnet, meta block as shown above.modelUsage, an empty one, a list where an object belongs, and unparseable output. All degrade tonullrather than raising.