fix(coding-agent): resource collision precedence, user/project skills override package skills

Sort resolved resources by precedence rank (project settings > project auto >
user settings > user auto > package) so first-wins collision logic produces
correct results. Also reorder CLI paths to prepend instead of append.

fixes #2781
This commit is contained in:
Mario Zechner
2026-04-04 21:28:38 +02:00
parent fbd35a4660
commit a7acef92a7
3 changed files with 144 additions and 4 deletions

View File

@@ -118,6 +118,24 @@ interface ResourceAccumulator {
themes: Map<string, { metadata: PathMetadata; enabled: boolean }>;
}
/**
* Compute a numeric precedence rank for a resource based on its metadata.
* Lower rank = higher precedence. Used to sort resolved resources so that
* name-collision resolution ("first wins") produces the correct outcome.
*
* Precedence (highest to lowest):
* 0 project + settings entry (source: "local", scope: "project")
* 1 project + auto-discovered (source: "auto", scope: "project")
* 2 user + settings entry (source: "local", scope: "user")
* 3 user + auto-discovered (source: "auto", scope: "user")
* 4 package resource (origin: "package")
*/
function resourcePrecedenceRank(m: PathMetadata): number {
if (m.origin === "package") return 4;
const scopeBase = m.scope === "project" ? 0 : 2;
return scopeBase + (m.source === "local" ? 0 : 1);
}
interface PackageFilter {
extensions?: string[];
skills?: string[];
@@ -2146,11 +2164,13 @@ export class DefaultPackageManager implements PackageManager {
private toResolvedPaths(accumulator: ResourceAccumulator): ResolvedPaths {
const toResolved = (entries: Map<string, { metadata: PathMetadata; enabled: boolean }>): ResolvedResource[] => {
return Array.from(entries.entries()).map(([path, { metadata, enabled }]) => ({
const resolved = Array.from(entries.entries()).map(([path, { metadata, enabled }]) => ({
path,
enabled,
metadata,
}));
resolved.sort((a, b) => resourcePrecedenceRank(a.metadata) - resourcePrecedenceRank(b.metadata));
return resolved;
};
return {

View File

@@ -413,7 +413,7 @@ export class DefaultResourceLoader implements ResourceLoader {
const skillPaths = this.noSkills
? this.mergePaths(cliEnabledSkills, this.additionalSkillPaths)
: this.mergePaths([...enabledSkills, ...cliEnabledSkills], this.additionalSkillPaths);
: this.mergePaths([...cliEnabledSkills, ...enabledSkills], this.additionalSkillPaths);
this.lastSkillPaths = skillPaths;
this.updateSkillsFromPaths(skillPaths, metadataByPath);
@@ -425,7 +425,7 @@ export class DefaultResourceLoader implements ResourceLoader {
const promptPaths = this.noPromptTemplates
? this.mergePaths(cliEnabledPrompts, this.additionalPromptTemplatePaths)
: this.mergePaths([...enabledPrompts, ...cliEnabledPrompts], this.additionalPromptTemplatePaths);
: this.mergePaths([...cliEnabledPrompts, ...enabledPrompts], this.additionalPromptTemplatePaths);
this.lastPromptPaths = promptPaths;
this.updatePromptsFromPaths(promptPaths, metadataByPath);
@@ -437,7 +437,7 @@ export class DefaultResourceLoader implements ResourceLoader {
const themePaths = this.noThemes
? this.mergePaths(cliEnabledThemes, this.additionalThemePaths)
: this.mergePaths([...enabledThemes, ...cliEnabledThemes], this.additionalThemePaths);
: this.mergePaths([...cliEnabledThemes, ...enabledThemes], this.additionalThemePaths);
this.lastThemePaths = themePaths;
this.updateThemesFromPaths(themePaths, metadataByPath);