feat(ci): register cross-plugin skill leaf-name collisions - #721
Conversation
Eight leaf names are carried by more than one plugin -- setup by 33, audit by 6, and check/clean/diagnose/plan/workflow/write by 2 each. Every one is separately invocable, so none of this is a correctness problem and none of it is a rename mandate. What namespacing does not cover is the listing. The picker labels a row by the leaf name and keeps <plugin>:<skill> as a hidden alias, so two colliding skills read identically and are told apart only by the (<plugin-name>) prefix on the description. That cost is invisible from inside any one plugin, and the grammar's own collision rule governs siblings within a namespace, not across plugins -- so a new collision appears silently and nothing forces a decision. Mirrors the check-cross-plugin-source-drift idiom: discover mode lists the collisions, --check fails on an unregistered one, and a stale guard drops entries that no longer collide so the registry cannot rot into pre-authorization for a future name. The registry seeds with all eight, each carrying the grounds it is accepted on rather than a bare name. Closes #720 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ogVV7z2Heg9ojJ88nqNxh
|
Claude finished @kyle-sexton's task in 2m 35s —— View job PR Review: feat(ci): register cross-plugin skill leaf-name collisions
Overall this is a clean, well-reasoned addition. The architecture — discover mode, CorrectnessRegistry parsing is correct. The trimming idiom on lines 64–66 of The stale guard is the right call. Without it a renamed skill leaves a dangling entry that silently pre-authorizes the next collision on that name. Its test (case 6) verifies both the exit code and the error string.
One factual inconsistencyThe CI step ordering in the new job is self-test → check, with the comment:
That rationale is correct — if the detector is broken, the self-test fails loudly before the check can produce a silent false-pass. However, the PR description claims this ordering follows "the established idiom" from the sibling job. It does not. The sibling # cross-plugin-source-drift (existing)
- name: Check for unregistered or drifted cross-plugin source clusters
run: scripts/check-cross-plugin-source-drift.sh --check # ← check first
- name: Run cross-plugin-source-drift tests
run: bash scripts/check-cross-plugin-source-drift.test.sh # ← self-test second# skill-leaf-name-gate (this PR)
- name: Run skill-leaf-name tests
run: bash scripts/check-skill-leaf-names.test.sh # ← self-test first
- name: Check for unregistered cross-plugin skill leaf-name collisions
run: scripts/check-skill-leaf-names.sh --check # ← check secondThe new ordering is arguably better (a broken detector is diagnosed immediately, not obscured by a passing check), but it is not mirroring the sibling — it inverts it. The description should either not invoke the sibling as the authority for this ordering, or the sibling should be updated to match. Worth a quick note in the PR or a follow-up to align the sibling. See: Minor UX nit
state="UNREGISTERED"
[[ -n "${registered[$leaf]:-}" ]] && state="registered"
No concerns
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 420d0e12e2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Registering a bare name meant the first registration silently pre-authorized every later owner: once `audit` was accepted for six plugins, a seventh could add one and --check stayed green, while the entry's rationale still enumerated the original six. The gate exists to force a conscious decision, and that was the one case it waved through. Entries now carry their owner set and --check fails when it changes, so a new owner is argued on its own grounds. `*` covers a name whose owner set is fixed by contract — `setup`, where re-arguing each new plugin would be ceremony with no decision in it. Two bugs surfaced while verifying, both now covered by tests: unquoted array splitting pathname-expanded the literal `*` against the cwd, and only the discovered side was sorted, so a hand-ordered registry entry failed a comparison documented as set-vs-set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ogVV7z2Heg9ojJ88nqNxh
|
Claude finished @kyle-sexton's task in 3m 2s —— View job PR Review: feat(ci): register cross-plugin skill leaf-name collisions
The fixup commit ( Fixup commit analysis
Empty owner-field path — correct but note the discover-mode signal. The Test coverageTwelve named assertions; the new owner-set scenarios are fully covered:
The gap from the previous review — no test for Confirmed from previous reviewCI step ordering (PR description inconsistency) — still present, still minor. Capitalization in discover output — now coherent. No concerns
Overall: Clean, well-reasoned, and the fixup is correct. The Codex concern is fully addressed. Ready to merge. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93241eeaec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Under `set -u` an associative array that was declared but never assigned
is unbound, so `${#collisions[@]}` and `${!collisions[@]}` aborted with
"collisions: unbound variable" the moment the last cross-plugin collision
was removed. Reproduced with a single unique fixture skill and an empty
registry.
That is exactly the state the stale-entry guard exists to shepherd the
repo into, so it was the one outcome the gate could not express — a
clean tree read as a crash.
Tracks a plain counter and an indexed leaf list alongside the map, and
guards the remaining expansions. Both zero-collision paths are now
covered by tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ogVV7z2Heg9ojJ88nqNxh
|
Claude finished @kyle-sexton's task in 1m 49s —— View job PR Review: feat(ci): register cross-plugin skill leaf-name collisions
This is a focused review of the latest fixup commit ( Zero-collision fix — correct and completeThe Codex P2 concern was: with The fix introduces two shadow variables: # scripts/check-skill-leaf-names.sh L64–74
declare -A collisions
collision_leaves=() # plain indexed array — safe under set -u when empty
collision_count=0 # integer — always bound
for leaf in "${!leaf_owners[@]}"; do
((${#owners[@]} >= 2)) || continue
collisions["$leaf"]="${leaf_owners[$leaf]}"
collision_leaves+=("$leaf")
collision_count=$((collision_count + 1))
doneEvery downstream reference is updated consistently:
The fix is mechanically complete. Every path that could touch Test coverage for the fix — thoroughTest 7b builds a separate clean fixture tree (no collisions, empty registry) and verifies both modes:
The clean fixture is isolated in its own Remaining note (documentation only, not a merge blocker)The CI step comment says:
That rationale is correct. The PR description says this ordering follows "the established idiom" of the sibling SummaryThe zero-collision crash is fixed, the fix is applied consistently across every affected path, test 7b covers both failure directions for that state, and the grammar introduced ( |
Closes #720
Summary
Eight skill leaf names are carried by more than one plugin. Every one is separately invocable — namespacing guarantees that — so this is not a rename mandate and nothing here changes a single skill name. It makes each collision explicit, dated, and argued, so the next one is a conscious call rather than a discovery.
Fix
scripts/check-skill-leaf-names.sh— mirrors thecheck-cross-plugin-source-drift.shidiom exactly, since the problem shape is the same (a repo-wide pattern no single plugin's review can see):--check— fails on an unregistered collision, and on a registry entry that no longer collides. The stale guard is the load-bearing half: without it, a leftover entry silently pre-authorizes a future collision on that name.scripts/skill-leaf-name-registry.txt— seeded with all eight, each carrying the grounds it was accepted on rather than a bare name.setupis contract-mandated byPLUGIN-PHILOSOPHY.md(and not consolidatable —${CLAUDE_PLUGIN_ROOT}resolves per containing plugin, with no cross-plugin skill inheritance);audit/check/clean/writefollow the fixed verb table;diagnose/plan/workfloware D19 keeps..github/workflows/ci.yml— newskill-leaf-name-gatelane, added to theci-statusneeds:list so the aggregate picks it up. Self-test runs before the check, per the established idiom, so a broken detector cannot mask a regression.Verification
Discover, against the real tree:
Green on today's tree, so the lane lands without turning unrelated PRs red.
Self-test — 8/8, covering both failure directions and the false-positive direction:
shellcheckon both scripts — clean.actionlinton the modified workflow — clean. Both scripts committed100755, matching the existingscripts/check-*.shconvention (the lane invokes the checker directly, not viabash).Related
name-matches-directory gate.