Skip to content

fix(core): a same-directory skill name collision lands in the refreeze receipt - #262

Merged
Max17190 merged 11 commits into
mainfrom
skill-collisions-surface-in-the-receipt
Aug 29, 2026
Merged

fix(core): a same-directory skill name collision lands in the refreeze receipt#262
Max17190 merged 11 commits into
mainfrom
skill-collisions-surface-in-the-receipt

Conversation

@Max17190

@Max17190 Max17190 commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Why

Round-5 ticket T3 (.scratch/selfext-round5/findings.md). skills_by_name keys on the frontmatter name, last-sorted wins, so two files in one skills directory declaring the same name collapse silently. --check catches 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

  • The capture records same-DIRECTORY collisions as (name, ignored path, indexed path) on the snapshot and registry (empty for a manifest-restored registry). Cross-tier shadowing is deliberate precedence (project over global) and stays unrecorded.
  • refreeze_receipt_text gains the collision clause: both declaring files, which one is indexed, and the repair (rename one; verify with openmax --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.
  • Full workspace green (exit 0), clippy zero warnings.

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.

T-Rex T-Rex Logs

What T-Rex did

  • Reviewed the registry's coalescing of same-tier records and the removal of a losing-tier collision when project precedence wins.
  • Verified that winner_indexed is derived from actual prompt-index inclusion, covering both the skill cap and the byte budget.
  • Confirmed that agent.rs emits only when the settled flag is true; otherwise it explicitly says no file of that name is indexed.
  • Observed that the executed test run produced five passing focused tests with the command, working directory, and exit code for each.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (6): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

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.
Comment thread crates/core/src/agent.rs Outdated
…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.
Comment thread crates/core/src/registry.rs Outdated
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.
Comment thread crates/core/src/registry.rs
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.
Comment thread crates/core/src/registry.rs Outdated
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
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
@Max17190
Max17190 merged commit 1c08e06 into main Aug 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant