fix(core): a same-directory skill name collision lands in the refreeze receipt - #262
Merged
Merged
Conversation
When an approved external tool's manifest is edited into invalid TOML, it drops out of the registry's tools and into `broken`, where the refreeze receipt names it under "NOT loaded: X: <reason>". classify_added_tools then also saw the name vanish from tools and, finding the old approval still in the ledger, listed it under "Removed approved tools: X - the approval outlives the file (identical bytes would run without a card)". Two clauses of one receipt described the same file as gone and as present-but-broken, and the "removed" framing invites a rewrite from scratch instead of a one-line parse fix. A manifest still on disk (its path in new_registry.broken) is present, not removed, so skip it in the removed classification and let the broken-file clause speak. Only a manifest that left disk entirely is a removed tool.
…broken list Greptile: a malformed project manifest that shadows an approved global tool of the same name withholds the global one, but records only the PROJECT path in `broken`. The first cut of this fix suppressed the removed-tool clause only when the broken path equalled the tool's own source path, so the shadow case (paths differ) still reported the withheld global tool as removed alongside the broken project file's "NOT loaded" clause. Key on the tool's own manifest still being on disk instead: an edited-to-broken manifest and a valid-but-shadowed one are both present, so neither is removed; only a manifest that left disk entirely is. Add a deleted-manifest test so the presence check cannot swallow a genuine removal.
Greptile: exists() returns true for a directory dropped at a manifest's .toml path, so replacing the file with a directory suppressed the removed-tool reporting even though the manifest is gone. Key on the path being a FILE, not merely existing, and add a directory-replacement test.
…isk probe A tool absent from the incoming registry was classified as removed by probing the old manifest path with is_file() at classify time. That probe races the capture it claims to describe and cannot see WHY a name is absent: a directory dropped over the .toml was reported both NOT loaded and removed in one receipt, and a broken project namesake shadowing a deleted approved global manifest still drew "identical bytes at that path would run without a card", which the withheld name makes false. The classifier now consults only what the new capture observed: the paths it read, and its broken tool entries keyed by the name each occupies (the declared name, or the stem fallback the withhold pass already uses). A removal is reported only when the capture found no trace of the path or the name, so every absent name gets exactly one clause.
…e receipt Two files in one skills directory declaring the same frontmatter name collapse to whichever sorts last. --check names it (mark_shadowed), but the refreeze receipt that follows the write was silent, so the author read the index line assuming the older file is what loaded (round-5 ticket T3). The capture now records same-directory collisions (name, ignored path, indexed path) - cross-tier shadowing stays silent, it is deliberate precedence - and the receipt names the collision with both paths and the winner.
…ndexed The collision was captured before the 50-skill truncation, so a winner the cap dropped was still announced as "only X is indexed". The indexed flag is settled against the final capped list, and the receipt says the cap dropped the winner instead of pointing at an index line that does not exist.
Per-pair records let a three-file collision report the intermediate winner as cap-dropped while the final entry said the name IS indexed, a contradictory receipt about one name. Collisions coalesce to one record per name: the final winner plus every path it displaced, indexed settled after the cap as before; the receipt lists all declarers and speaks once.
Two global namesakes collide, a project definition then takes the name by precedence, and the stored record's winner pointed at the displaced global path: the post-cap settle called the name unindexed while the project skill is active. Cross-tier replacement now retires any collision record for the name; precedence itself stays silent as designed.
Membership in the 50-skill list was not enough: the 3000-byte index budget drops lines first-fit at prompt render, so a winner inside the list could be announced as indexed while its line never made the prompt. The settle now asks skill_index_costs, the render's own decision (zero cost = no line), and the receipt's dropped wording covers both droppers and points at openmax --check to name which.
Max17190
changed the base branch from
receipt-truth-broken-not-removed
to
main
August 29, 2026 17:47
# Conflicts: # crates/core/src/agent.rs # crates/core/src/registry.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Round-5 ticket T3 (.scratch/selfext-round5/findings.md).
skills_by_namekeys on the frontmatter name, last-sorted wins, so two files in one skills directory declaring the same name collapse silently.--checkcatches it, but the same-turn refreeze receipt after the write that caused the collision said nothing, so the author reads the unchanged index line and assumes the older file is what loaded.Stacked on #248 (the capture-snapshot fields this adds sit beside the ones #248 introduced; merging this first would conflict).
Summary
refreeze_receipt_textgains the collision clause: both declaring files, which one is indexed, and the repair (rename one; verify withopenmax --check).Test Plan
a_same_tier_skill_namesake_is_recorded_and_precedence_is_not: two project subdirs declaring one name yield exactly one recorded collision with the sorted-last winner; the same name split across tiers records nothing. Red against a no-record variant.refreeze_receipt_names_a_skill_shadowed_by_a_same_tier_namesake(the ticket's red-test name): the receipt names the collision, both project-relative paths, the winner, and the verify command. Red against a clause-less receipt.Greptile Summary
Skill-collision receipts now reflect the final active definition and its actual prompt visibility. Same-tier collisions are coalesced around the final winner, project-level overrides remove irrelevant global collision history, and winners excluded by either the skill cap or prompt byte budget are reported as not indexed.
Focused Rust tests exercised the 50-skill cap, three-file same-name collision, project override, prompt byte-budget exclusion, and receipt wording. All passed, disproving the previously reported collision receipt failures.
Confidence Score: 5/5
No blocking failure remains.
The exercised collision and receipt paths produce the expected active-skill and prompt-visibility outcomes.
What T-Rex did
Reviews (6): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile