Conversation
Two skills driving the same tool with the same flags compete for the same request. That is normal here and most of the catalog does it - as long as one of them says which is which. vllm-xpu-bench and vllm-xpu-run name each other; torch-xpu-run and torch-xpu-profile never have, and an agent holding both has nothing to route on. So this compares what each skill does rather than what it says: the commands, flags, environment variables, API calls and endpoint paths in its code, then subtracts the pairs where a hand-off is written down. Advisory in CI - which of two overlapping skills should win is a judgement about the catalog, so a finding is annotated on the pull request and the merge is not blocked. The detector's own --self-test does block, because a detector that stopped detecting reports a clean zero for every pair, which reads exactly like a catalog with no duplication. 16 assertions against this tree, and --mutate M1..M10 so each way of breaking it can be shown to turn red. Two of them cover the cheap signals: M9 proves a shared-name precondition would drop 4 of the 14 judgeable pairs here, and M10 proves the hand-off index still agrees with scanning every pair. That index is what makes this affordable - the per-pair scan it replaces was 22.0s of the 22.0s spent at 300 skills, against 0.03s for the action comparison itself.
CodeFactor is clean on every other pull request in this repository and reported one issue here, so the finding is this file's rather than ambient. The only `global` in tools/ becomes a class attribute, and the name/description comprehension that pairs() and lexical() each carried verbatim becomes one helper.
…-string Three findings pylint reports on this file and on no other file in tools/, so they are what CodeFactor sees here and does not see on the tree it already passes: "tee" written twice in GENERIC_CMD, "flag:--user" twice in CONTAINER_PLUMBING, and an f-string with nothing to interpolate in a self-test message. A duplicate in a set literal changes nothing at runtime, which is why it survived review - it is a sign the list was edited twice, not a bug. The mutation switch goes from a one-attribute class to a SimpleNamespace: the class existed only to avoid a `global` statement, and traded one lint for another. Behaviour is unchanged - same 435 scored pairs, same single advisory finding, same 16 self-test assertions, M1-M10 all still red.
…aceback CodeFactor's finding, read out of the page's own model rather than guessed at a third time: Complex Method, cyclomatic complexity 21, on main(). The threshold is visible in what main already carries and nobody counts against a pull request - split_frontmatter 17, check_imports 18, check_catalog 19, check_suites 43. Those are grandfathered; a new one is not. main() was 141 lines doing argument parsing, mutation setup, three output modes and the verdict, so the finding is right. Six functions come out of it - build_parser, mutated_threshold, show_signature, print_ranked, undeclared_pairs, report_undeclared, summarize - and main() drops off radon's list entirely. Behaviour is unchanged, and that is checked rather than asserted: stdout, stderr and the exit code were captured for 21 invocations before the split (--self-test, all ten mutations, the gate, --json, --show, --show of a name that does not exist, --strict, --top/--queue, a run that FAILs, and two runs under GITHUB_ACTIONS) and diffed after. One difference, deliberate: M3 and M9 leave the paraphrased-duplicate fixture sharing no action with anything, and a bare max() over the empty result ended those two mutations in a ValueError traceback. --mutate exists to show which assertion catches each break, so a crash was the wrong evidence. strongest() now exits with the assertion named. Every other stream is byte-identical, all 21 exit codes are unchanged, 16 self-test assertions pass, M1-M10 all still fail.
a9b52a2 to
8a51139
Compare
napetrov
left a comment
There was a problem hiding this comment.
Reviewed at 8a51139. Ran it against skills/ as committed, plus injected skills, to check the claims rather than read them.
What already works, verified locally:
--self-test: 16 assertions, all pass, 0.76 s. Every mutationM1–M10is caught (exit 1). Making that a separate blocking step ahead of the advisory gate is the right shape — the gate can be advisory precisely because the detector's own liveness is not.- The gate itself is 0.20 s over 435 scored pairs, no network, no dependencies.
- On today's tree it produces exactly one finding,
torch-xpu-bench | torch-xpu-runat 0.3636, both imported →WARN. So it merges green today and does not start life noisy. - The hand-off-index shortcut agrees with the naive all-pairs scan on all 528 pairs, and the containment-1.0 rule correctly refuses to let a copy excuse itself with an inherited hand-off. Both are asserted, not just claimed.
Five things to settle before merge. Only the first changes behaviour; the rest are calibration, reporting and scope.
- A verbatim copy of an authored skill merges green. Measured: copy
skills/dpnp-memory/toskills/dpnp-memory-tuning/, reword the description so it does not name the original, and the run reportsREVIEW overlap 1.0000 ... everything the smaller one does, the larger one already does— and exits 0, because the workflow passes--advisory. Containment 1.0 with no hand-off is not the judgement call--advisoryexists to defer; it is the same category as the self-test. Detail inline. --max-overlap 0.65sits 0.035 above the lower edge of the band its own self-test enforces, and the self-test step is blocking. Detail inline.--min-sharedcannot reach below the hardcoded 5-action floor, and 93 of 528 pairs are never scored without that being visible in the summary. Detail inline.--strictcannot be adopted as written: on a clean tree it exits 1 on a pair nobody here can fix. Detail inline.- One scope sentence in the module docstring, so a future reader does not read "0 findings" as "no duplication". The action axis is blind to duplicated prose by construction — that is a boundary worth stating, not a gap to close in this PR. Detail inline.
Exit-code matrix as measured on this tree, for the record:
| flags | clean tree | with an authored 1.0 copy injected |
|---|---|---|
--advisory |
0 | 0 |
--advisory --strict |
1 | 1 |
--strict |
1 | 1 |
| (none) | 0 | 1 |
Nothing here is a redesign request — the action-signature extractor is the part of this that no off-the-shelf library replaces, and it should land.
| return "ok" | ||
| if pair["handoff"] and (pair["containment"] < 1.0 or BROKEN.which == "M8"): | ||
| return "ok" | ||
| return "REVIEW" if pair["authored"] else "WARN" |
There was a problem hiding this comment.
Containment 1.0 on an authored pair should block, even under --advisory.
Measured on this tree: cp -r skills/dpnp-memory skills/dpnp-memory-tuning, change name:, reword description: so it does not name the original. Result:
REVIEW overlap 1.0000 dpnp-memory | dpnp-memory-tuning: everything the smaller one
does, the larger one already does, so the hand-off cannot be what separates them.
$ echo $?
0
--advisory is well argued for the middle of the range — which of two overlapping skills wins really is a reviewer's call. But containment 1.0 with no hand-off is not in that range. The larger skill already does everything the smaller does; there is no division of labour to weigh, which is exactly the reasoning in this docstring. verdict() already separates that case (pair["containment"] >= 1.0 survives a declared hand-off); summarize() then throws the distinction away because --advisory short-circuits on needs_review as a whole.
Suggestion: --advisory suppresses failure for REVIEW below 1.0, and returns 1 for REVIEW at containment >= 1.0 (pair["authored"] only, so an upstream pair stays a WARN). Same argument that makes --self-test a blocking step: subsumption is not a judgement call.
| # bounds the threshold to 1.0-1.5x of it and asks for this number to move up when | ||
| # the catalog grows into it. A verbatim copy scores 1.0 and is reported regardless. | ||
| - name: Skill overlap | ||
| run: python3 tools/lint_skill_overlap.py --max-overlap 0.65 --min-shared 8 --advisory |
There was a problem hiding this comment.
The threshold is 0.035 above the edge of a band the blocking step enforces.
--self-test asserts ceiling < budget <= 1.5 * ceiling where ceiling is the highest judgeable containment in the tree. Measured band today: (0.6154, 0.9231], ceiling set by dpnp-quickstart | dpnp-troubleshooting at 0.6154.
--max-overlap 0.60 -> self-test FAIL (margin 0.97x)
--max-overlap 0.62 -> ok
--max-overlap 0.65 -> ok <- what the workflow passes, margin 1.06x
--max-overlap 0.92 -> ok
--max-overlap 0.93 -> self-test FAIL (margin 1.51x)
So 0.65 leaves 0.035 of headroom below and 0.27 unused above. The dpnp-* family is 8 skills over one API surface; the ninth that reuses dpnp.array/asnumpy/arange raises ceiling past 0.65 and the blocking self-test step fails on a PR that has nothing to do with this linter — and the failure is on the workflow file, not on the skill the author touched.
Two suggestions, either is fine:
- move the number to the middle of the band (~0.75) so ordinary catalog growth does not trip it; or
- keep 0.65 and make the self-test failure self-servicing: print the exact replacement value (
set --max-overlap to 0.75), since the assertion already computesceilingand knows the legal range.
Same value also appears in the module docstring usage line and MAINTAINERS.md; the self-test's "workflow names the threshold exactly once" check covers the workflow but not those copies.
| continue | ||
| shared = left["signature"] & right["signature"] | ||
| smaller = min(len(left["signature"]), len(right["signature"])) | ||
| if smaller < 5: |
There was a problem hiding this comment.
Two floors, and --min-shared cannot reach past this one.
--min-shared is a flag with a default of 8; this smaller < 5 is a second, invisible floor at a different granularity (signature size, not shared count). Measured:
--min-shared 3 -> 435 scored pair(s)
--min-shared 5 -> 435 scored pair(s)
--min-shared 8 -> 435 scored pair(s)
435 of 528, identically. So 93 pairs are never scored, and lowering --min-shared below 5 silently does nothing. Nothing in the summary line says so — 435 scored pair(s) reads as a total, not as a remainder.
To be fair, --self-test does name the affected skills (skills below the min-shared reach are named) and the where actions cannot judge queue surfaces some of these pairs at gate time, which is more than most linters do. Two small things would close it:
- name the constant (
MIN_SIGNATURE = 5) with the reason next to it, and either clamp--min-sharedto it or reject--min-shared < MIN_SIGNATUREinbuild_parser(), so the flag cannot lie about its reach; - add the count to the summary:
435 scored, 93 pair(s) below the 5-action floor. It costs one number and stops "0 findings" from meaning "nothing was compared" for the thin end of the catalog.
Worth noting signature() handles bundled files correctly — linux-perf yields 123 actions because its references/*.md are read. The floor only bites genuinely thin skills, which is the intent; it just needs to be counted out loud.
| "the merge is not blocked.") | ||
| else: | ||
| print(f"OK {summary}, none over {budget:.2f}.") | ||
| return 1 if args.strict and undeclared else 0 |
There was a problem hiding this comment.
--strict as written cannot be turned on.
Measured on this tree with no injections:
$ python3 tools/lint_skill_overlap.py --max-overlap 0.65 --min-shared 8 --strict; echo $?
OK 33 skill(s), 435 scored pair(s), 1 undeclared overlap(s) reported, worst
torch-xpu-bench | torch-xpu-run at 0.3636, none over 0.65.
1
The single undeclared pair is torch-xpu-bench | torch-xpu-run, both imported — the report itself says (upstream - both are imported) and the fix is a description edit in someone else's repo. So --strict fails on something no PR here can resolve, which makes it unusable as a follow-on step rather than a stricter mode.
undeclared counts WARN and REVIEW alike; needs_review already filters on pair["authored"]. Scoping --strict the same way (args.strict and any(p["authored"] for p in undeclared)) makes it adoptable, and keeps the upstream pair visible as a WARN.
Also: --advisory --strict exits 1 — --strict wins. That is defensible, but --advisory's help says "report and exit 0" unconditionally. Four words there ("unless --strict is given") prevents someone reading the help and concluding the pair is contradictory.
|
|
||
| python3 tools/lint_skill_overlap.py # every pair, ranked | ||
| python3 tools/lint_skill_overlap.py --show vllm-xpu-run # one skill's signature | ||
| python3 tools/lint_skill_overlap.py --max-overlap 0.65 --min-shared 8 --advisory |
There was a problem hiding this comment.
One sentence of scope in the docstring.
The docstring is unusually good at saying what the check does; it does not say what it cannot see. Concretely, on this tree the pair with the most near-duplicate prose is invisible to this check:
| pair | duplicated block | containment here | shared actions |
|---|---|---|---|
torch-xpu-bench | vllm-xpu-bench |
the Env vars table, already drifted in wording between the two copies |
0.0833 | 3 |
torch-xpu-profile | vllm-xpu-profile |
same | 0.1250 | 2 |
dpnp-io | dpnp-memory |
shared links table | 0.3000 | 6 |
That is correct behaviour, not a bug: the two skills drive different tools, so an action-signature axis should score them low. But it means "0 findings above 0.65" says nothing about text maintained in two places, and the summary line does not hint at the difference. A sentence like "scope: shared actions, not shared text — two skills can duplicate a whole section and score near zero here" keeps the next reader from over-reading a clean run.
No change to the code, and I would not hold the PR on it.
What this adds
tools/lint_skill_overlap.py, wired intovalidate.ymlas two steps: a blocking self-testand an advisory report.
Two skills driving the same tool with the same flags compete for the same request. That is
normal here and most of this catalog does it — as long as one of them says which is which.
vllm-xpu-benchandvllm-xpu-runname each other;torch-xpu-runandtorch-xpu-profilenever have, and an agent holding both has nothing to route on. That is the only thing this
check looks for: undeclared adjacency, not duplication.
So it compares what each skill does rather than what it says — the commands, flags,
environment variables, API calls and endpoint paths in its code, from fenced blocks, inline
spans and bundled
.sh/.py/.md— then subtracts every pair where a hand-off is writtendown in either description.
It does not block
A finding is annotated on the pull request and the merge proceeds. Which of two overlapping
skills should win — merge, split, or leave alone — is a judgement about the catalog that a
reviewer makes with the contributor, and a threshold that made it for them would be wrong in
both directions: blocking a legitimate composition pair, and passing the restatement in prose
it cannot see at all.
What does block is the detector's own
--self-test. Every way this check breaks is silent —a fence regex that stops matching scores every pair 0, which reads exactly like a catalog
with no duplication — so a detector that has stopped detecting is not a judgement call.
On this tree today the report is one line:
torch-xpu-bench | torch-xpu-run, containment0.3636 over 8 shared actions, undeclared in both directions, both sides imported so the
repair belongs upstream.
Proof it works, not just that it runs
16 assertions in
--self-testagainstskills/as committed, and--mutate M1…M10soeach way of breaking it can be shown to turn red:
PLATFORM = set()flag:regexBeyond the fixtures, seven candidate skills were written into the real tree and the real CI
command run against each: a byte-for-byte copy, a paraphrase, a narrow slice, a declared
duplicate, an imported drift, a genuinely different server, and a prose-only restatement.
The copy, the paraphrase, the slice and the declared duplicate all score containment 1.0000
and are reported; the different server is silent; every run exits 0.
Two defects in this design were found that way rather than by review, and are now fixtures:
vllm-xpu-runcarries that name in its own text, so the hand-off rule silenced it. At containment 1.0
there is no division of labour for a hand-off to describe, so total subsumption is now
reported even when declared (
DECLARED_COPY, M8).leaving a self-test fixable only by editing the test. Replaced with one band read off the
tree:
ceiling < threshold ≤ 1.5 × ceiling.The threshold is not a constant to defend
0.65 is the next 0.05 step above the highest-scoring judgeable pair in the tree
(
dpnp-quickstart | dpnp-troubleshooting, 0.6154), so nothing already merged is reported.That ceiling was measured to rise with catalog size — 0.36 at 12 skills, 0.56 at 20, 0.62 at
33, median over 40 random subsets of real content — so the self-test bounds the threshold to
1.0–1.5× of it and fails when the catalog grows into it, which is the signal to raise the
number. A verbatim copy scores 1.0 and is reported regardless of where the threshold sits.
Name and description: a reading order, not a filter
The cheapest imaginable signal is read, and deliberately given no authority. Measured on this
tree:
validate_skills.pyrequiresnametoequal the directory name, and directories are unique
linux-perf | performance-patterns— M9 keeps that honestsharing 1.0000 of its actions
So they order a short queue of the pairs the action axis cannot judge — the four skills here
with too little code to compare, and any prose-only contribution — printed as a reading order
that settles nothing. It costs 0.2 s at 300 skills, and it is the only affordable way to pick
the few pairs anything downstream that pays per pair should look at.
Cost
The hand-off detector is indexed rather than re-scanned per pair, which is where the cost
turned out to live:
--self-testasserts the index agrees with scanning every pair, rather than trusting that itdoes.
What it cannot do
Stated because the alternative is a check that gets trusted for more than it proves.
compare. Embeddings do not close this: on this catalog
potion-base-8Mscored a legitimatepair 0.9000 and a near-verbatim copy 0.8877 — it ranked the legitimate pair higher. The
instrument that would work is a model reading both, which no keyless gate can have.
on the smaller side, and the imported skills carry far more signature material than the ones
authored here, so today the check's reach is a new contribution and the dpnp family.
the finding, and only a reviewer can tell whether the division of labour it claims is real.
PLATFORMis defensible as a shape rule rather than a fix for a specific pair, and the fixtures plus
M1–M10 are what keep that tuning honest — but it is tuning, and it is recorded here rather
than in a commit message nobody reads.
Not in this PR
Candidate-vs-catalog mode (
against()exists in the code, unwired), delta-vs-base for syncpull requests, a suppression file, and any model-based tier — a fork PR gets no secrets, so a
model tier cannot run where this one does.
Docs updated in the same change:
CONTRIBUTING.md(local gate, and the not-blocking list)and
MAINTAINERS.md(CI table, tools table, and what the check cannot do).Draft: opened for review of the shape before the numbers are argued.