chore(skills): improve watcher event observability

This commit is contained in:
William Valentin
2026-02-12 17:40:41 -08:00
parent 333e33f30f
commit bd29afeaff
3 changed files with 49 additions and 12 deletions
+34 -11
View File
@@ -93,25 +93,31 @@ export function initSkills(config: Config, lifecycle?: Lifecycle): SkillsResult
return selected;
};
const applyTargetedSkillChange = (changedPath: string): boolean => {
type TargetedSkillChangeResult =
| { kind: 'upsert' }
| { kind: 'removed' }
| { kind: 'shadowed' }
| { kind: 'ambiguous'; reason: 'unmapped_path' | 'unmapped_removal' };
const applyTargetedSkillChange = (changedPath: string): TargetedSkillChangeResult => {
const resolved = resolveChangedSkillDir(changedPath);
if (!resolved) {
return false;
return { kind: 'ambiguous', reason: 'unmapped_path' };
}
const loaded = loadSkill(resolved.dir, resolved.tier);
if (loaded) {
const existing = skillRegistry.get(loaded.manifest.name);
if (existing && tierPriority[existing.manifest.tier] > tierPriority[loaded.manifest.tier]) {
return true;
return { kind: 'shadowed' };
}
skillRegistry.register(loaded);
return true;
return { kind: 'upsert' };
}
const existingAtDir = skillRegistry.list().find((skill) => resolve(skill.directory) === resolve(resolved.dir));
if (!existingAtDir) {
return false;
return { kind: 'ambiguous', reason: 'unmapped_removal' };
}
skillRegistry.unregister(existingAtDir.manifest.name);
@@ -119,7 +125,7 @@ export function initSkills(config: Config, lifecycle?: Lifecycle): SkillsResult
if (replacement) {
skillRegistry.register(replacement);
}
return true;
return { kind: 'removed' };
};
const skills = loadAllSkills(skillLoadConfig);
@@ -148,15 +154,32 @@ export function initSkills(config: Config, lifecycle?: Lifecycle): SkillsResult
skillDirs,
debounceMs: config.skills.load.watch_debounce_ms,
onSkillsChanged: ({ changedPaths }) => {
let targetedCount = 0;
let upsertCount = 0;
let removedCount = 0;
let shadowedCount = 0;
for (const changedPath of changedPaths) {
if (!applyTargetedSkillChange(changedPath)) {
reloadAllSkills(`ambiguous change path: ${changedPath}`);
const result = applyTargetedSkillChange(changedPath);
if (result.kind === 'ambiguous') {
console.log(
`Skills watcher fallback triggered (reason=${result.reason}, path=${changedPath}, upsert=${upsertCount}, removed=${removedCount}, shadowed=${shadowedCount})`,
);
reloadAllSkills(`ambiguous ${result.reason}: ${changedPath}`);
return;
}
targetedCount += 1;
if (result.kind === 'upsert') {
upsertCount += 1;
} else if (result.kind === 'removed') {
removedCount += 1;
} else {
shadowedCount += 1;
}
}
console.log(`Skills watcher applied targeted updates for ${targetedCount} change(s)`);
console.log(
`Skills watcher event (mode=targeted, paths=${changedPaths.length}, upsert=${upsertCount}, removed=${removedCount}, shadowed=${shadowedCount})`,
);
},
});
skillsWatcher.start();