fix(skills): resolve skill-name collisions deterministically (local > external) + tighten kanban liveness check - #13
Conversation
…nal)
Stale per-profile skill copies were crashing every kanban worker spawn:
`skill_view('kanban-worker')` returned success=False with 'Ambiguous
skill name' when two SKILL.md files matched the bare name, which the
CLI's --skills preload path surfaced as 'Unknown skill(s):
kanban-worker' and aborted before the agent loop ran. Today's incident:
stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/
(v2.0.0) collided with the canonical ~/.hermes/skills/devops/
kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher
gave up.
Refusing to guess made sense for an interactive skill_view() call —
not for a CLI preload where there's no human to disambiguate. Pick
deterministically and warn loudly so operators still see the stale copy.
Resolution order (in tools/skills_tool.py):
1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier.
2. external_dirs in declaration order, one tier per entry.
3. Within a tier, most-recent SKILL.md mtime wins.
A WARN log names the chosen path and the shadowed candidates so the
operator can clean up.
Part 2: _kanban_worker_skill_available delegated to the existing
_resolve_skill_under_home helper, which already walked the full
<home>/skills + skills.external_dirs set the worker would actually use.
The bespoke check missed profiles (like braintrusteng) that keep an
empty per-profile skills/ and route every lookup through external_dirs.
Tests:
- tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection
rewritten — local-wins-by-tier, external-wins-by-declaration-order,
same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on-
silent-resolve.
- tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable
added — local-only, external-only, and the today's-incident shape
(local v2.0.0 + external v2.2.0 collision) all return True.
416 tests in the affected modules pass (415 + 1 unrelated skip).
Closes kanban task t_4a5d78f8.
🔎 Lint report:
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughSkill collision detection changes from refusing ambiguous matches with an error to deterministically selecting the top-ranked candidate by tier then modification time, with WARN logging. The kanban-worker availability check is simplified to reuse the shared resolver, gaining support for external skill directories configured in profile settings. ChangesDeterministic skill collision resolution with external_dirs support
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
…nal) (#13) Stale per-profile skill copies were crashing every kanban worker spawn: `skill_view('kanban-worker')` returned success=False with 'Ambiguous skill name' when two SKILL.md files matched the bare name, which the CLI's --skills preload path surfaced as 'Unknown skill(s): kanban-worker' and aborted before the agent loop ran. Today's incident: stale ~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/ (v2.0.0) collided with the canonical ~/.hermes/skills/devops/ kanban-worker/ (v2.2.0). Four crash cycles burned before the dispatcher gave up. Refusing to guess made sense for an interactive skill_view() call — not for a CLI preload where there's no human to disambiguate. Pick deterministically and warn loudly so operators still see the stale copy. Resolution order (in tools/skills_tool.py): 1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier. 2. external_dirs in declaration order, one tier per entry. 3. Within a tier, most-recent SKILL.md mtime wins. A WARN log names the chosen path and the shadowed candidates so the operator can clean up. Part 2: _kanban_worker_skill_available delegated to the existing _resolve_skill_under_home helper, which already walked the full <home>/skills + skills.external_dirs set the worker would actually use. The bespoke check missed profiles (like braintrusteng) that keep an empty per-profile skills/ and route every lookup through external_dirs. Tests: - tests/tools/test_skills_tool.py: TestSkillViewCollisionDetection rewritten — local-wins-by-tier, external-wins-by-declaration-order, same-tier-wins-by-mtime, explicit-path-still-works, WARN-emitted-on- silent-resolve. - tests/hermes_cli/test_kanban_db.py: TestKanbanWorkerSkillAvailable added — local-only, external-only, and the today's-incident shape (local v2.0.0 + external v2.2.0 collision) all return True. 416 tests in the affected modules pass (415 + 1 unrelated skip). Closes kanban task t_4a5d78f8.
Why
Stale per-profile skill copies were crashing every kanban worker spawn. Today's incident: stale
~/.hermes/profiles/braintrusteng/skills/devops/kanban-worker/(v2.0.0) collided with the canonical~/.hermes/skills/devops/kanban-worker/(v2.2.0).skill_view()returnedsuccess: Falsewith "Ambiguous skill name", which the CLI surfaced asUnknown skill(s): kanban-worker, which crashed every kanban worker fort_8502998bfour times before the dispatcher gave up.Refusing to guess was correct for an interactive
skill_viewcall. It was wrong for the--skills <name>CLI preload path, where there's no human to disambiguate.What
Part 1 —
tools/skills_tool.py: resolve collisions deterministically instead of returning an error.Resolution order:
SKILLS_DIR(=$HERMES_HOME/skills) wins by tier.skills.external_dirsentry wins in declaration order (one tier per entry).A WARN log names the chosen path AND the shadowed candidates so operators can still spot and clean up the stale copy during normal use.
Explicit categorized paths (
category/skill-name) still bypass the bare-name resolution entirely, so users who want pinning still get it.Part 2 —
hermes_cli/kanban_db.py:_kanban_worker_skill_availablenow delegates to the existing_resolve_skill_under_homehelper, so the liveness check uses the exact same resolver (<home>/skills+skills.external_dirsfrom<home>/config.yaml) as the worker. The previous bespoke check only scanned<home>/skills, missing profiles likebraintrustengwhose own skills dir is empty and every lookup is routed through external_dirs.Tests
tests/tools/test_skills_tool.py::TestSkillViewCollisionDetection— rewritten to assert the new behavior across all five fixture shapes (nested-local-wins, top-level-local-wins, explicit-path, two-externals-by-declaration-order, same-tier-by-mtime). The same-tier-by-mtime case reproduces the original incident (twokanban-workerSKILL.md files at v2.0.0 and v2.2.0; newer wins). WARN-log emission is asserted viacaplog.tests/hermes_cli/test_kanban_db.py::TestKanbanWorkerSkillAvailable— new class covering local-only, no-skill, external-only, and the today's-incident collision shape.tests/tools/test_skills_tool.py,tests/hermes_cli/test_kanban_db.py,tests/hermes_cli/test_kanban_core_functionality.py— 415 passed, 1 unrelated skip.Acceptance vs the task body
kanban-workerSKILL.md files) loads the newer one and emits one WARN line._kanban_worker_skill_availableis now a real liveness check (delegates to_resolve_skill_under_home).Out of scope (intentionally)
skills.collision_strategy). The deterministic order is simple enough that a knob would just invite drift between profiles. Easy to add later if a user objects to local-wins.skills.external_dirsconfig untouched.Closes kanban task t_4a5d78f8.
Summary by CodeRabbit
Bug Fixes
Tests