fix: stop the palette silently dropping and mis-sourcing skills
Three verified defects in slash-command and skill discovery, each with a test that fails without its fix. A symlinked skill directory was dropped. scanSkillDirBudget rejected any entry whose DirEntry.IsDir() was false, which is every symlink, so a skill symlinked in from a dotfiles repo vanished from the palette with no diagnostic - not even a suppression entry. Measured against a real ~/.claude/skills: 40 skills on disk, 39 discovered plus 1 legitimately suppressed only after this change; before it, one was unaccounted for. scanSkillDirFormat in the same file already followed symlinks and de-duplicated by resolved path, so this adopts that convention rather than inventing one, and de-duplicates the same way now that two entries can name one real skill. walkDirBudget deliberately keeps skipping symlinks. A command tree is namespaced by directory, so following a link that resolves inside the same tree would publish one file twice under two namespaces, with os.ReadDir order deciding which name wins. TestWalkSymlinkDirSkipped and TestWalkSymlinkFileSkipped pin that; the comment now says why the two scanners differ. It does gain a regular-file check: a FIFO named *.md reached fileFrontmatter, whose os.ReadFile blocks forever on a pipe with no writer - a permanent hang in a service that polls. An empty ctx.Home made the relay scan its own working directory. ctx.Home is the service's os.UserHomeDir() and is empty whenever that lookup fails, so filepath.Join(ctx.Home, ".claude", "skills") yields a relative path that os.ReadDir resolves against the headless service's arbitrary cwd - and the results were labelled "personal" as if they were the user's. agentroots already refuses a non-absolute base for this exact reason; the providers' hand-rolled joins bypassed it. omp, Pi and Kimi now reject one, and Kimi no longer reads a config.toml from there either. A relative omp customDirectories entry now resolves against the pane's cwd, matching how Kimi treats extra_skill_dirs, and is skipped when that cwd is unknown. Two omp config.yml shapes silently discarded a user's bans. A block sequence written at its parent's own column - legal YAML - cleared disabledExtensions and then dropped every item, because the indent == 0 branch reset the parser mode before inspecting the line. A trailing comment on a section header made the value "#..." rather than empty, so neither "skills:" nor "disabledExtensions:" was recognised as introducing a block, discarding the whole section including skills.enabled: false. Both left the palette offering skills the user had explicitly disabled, which is the one place failing open is wrong. Not fixed, deliberately: claude.go and qoder.go walk project directories outermost-first, so a large git-root scope can exhaust the shared file budget before the pane's own directory is reached. Reversing the loop would also reverse precedence, because apply() is last-wins, trading a budget ordering nit for a command precedence regression. It needs its own change.
K
Kiryl Berasneu committed
9191443aed6b98accd57cb4c890fb5582cc36cc9
Parent: 1d092c4