Skip to content

fix(skills): resolve skill-name collisions deterministically (local > external) + tighten kanban liveness check - #13

Merged
sahilm-ti merged 1 commit into
mainfrom
fix/skill-collision-resolution
May 25, 2026
Merged

sahilm-ti merged 1 commit into
mainfrom
fix/skill-collision-resolution

Conversation

@sahilm-ti

@sahilm-ti sahilm-ti commented May 25, 2026 •

Copy link
Copy Markdown
Owner

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() returned success: False with "Ambiguous skill name", which the CLI surfaced as Unknown skill(s): kanban-worker, which crashed every kanban worker for t_8502998b four times before the dispatcher gave up.

Refusing to guess was correct for an interactive skill_view call. 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:

  1. SKILLS_DIR (= $HERMES_HOME/skills) wins by tier.
  2. Each skills.external_dirs entry wins in declaration order (one tier per entry).
  3. Within a tier, the most-recent SKILL.md mtime wins.

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_available now delegates to the existing _resolve_skill_under_home helper, so the liveness check uses the exact same resolver (<home>/skills + skills.external_dirs from <home>/config.yaml) as the worker. The previous bespoke check only scanned <home>/skills, missing profiles like braintrusteng whose 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 (two kanban-worker SKILL.md files at v2.0.0 and v2.2.0; newer wins). WARN-log emission is asserted via caplog.
  • tests/hermes_cli/test_kanban_db.py::TestKanbanWorkerSkillAvailable — new class covering local-only, no-skill, external-only, and the today's-incident collision shape.
  • Full pass: 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

  • ✅ Stale leftover skill copy in a profile's skills/ tree no longer crashes workers.
  • ✅ Today's repro (two kanban-worker SKILL.md files) loads the newer one and emits one WARN line.
  • ✅ _kanban_worker_skill_available is now a real liveness check (delegates to _resolve_skill_under_home).

Out of scope (intentionally)

  • No config knob (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_dirs config untouched.
  • Genuinely-missing skills still error (no silent fallback there).

Closes kanban task t_4a5d78f8.

Summary by CodeRabbit

  • Bug Fixes

    • Skill resolution now properly recognizes skills from externally configured directories.
    • Skill name collisions are resolved deterministically instead of failing, with local skills taking precedence and modification time as a tie-breaker.
  • Tests

    • Added comprehensive test coverage for skill availability detection across various scenarios.
    • Updated collision handling tests to validate deterministic skill selection behavior.

Review Change Stack

…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.
@github-actions

Copy link
Copy Markdown

🔎 Lint report: fix/skill-collision-resolution vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9042 on HEAD, 9042 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 4812 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@sahilm-ti
sahilm-ti merged commit c2bc7a0 into main May 25, 2026
20 of 21 checks passed
@sahilm-ti
sahilm-ti deleted the fix/skill-collision-resolution branch May 25, 2026 15:04
@coderabbitai

coderabbitai Bot commented May 25, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d02dd5f-40d0-4760-82e6-57af1001bd77

📥 Commits

Reviewing files that changed from the base of the PR and between 377ae28 and 50aa661.

📒 Files selected for processing (4)
  • hermes_cli/kanban_db.py
  • tests/hermes_cli/test_kanban_db.py
  • tests/tools/test_skills_tool.py
  • tools/skills_tool.py

📝 Walkthrough

Walkthrough

Skill 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.

Changes

Deterministic skill collision resolution with external_dirs support

Layer / File(s) Summary
Skill view collision resolution implementation and tests
tools/skills_tool.py, tests/tools/test_skills_tool.py
Replaces the previous "ambiguous skill name" error behavior with deterministic tier-based ranking (preferring local SKILLS_DIR over external_dirs entries) and mtime-based tie-breaking within the same tier. Test expectations update from failure/ambiguity checks to successful selection; new test cases cover nested-local-vs-external, top-level-local-vs-external, inter-external declaration-order resolution, and same-tier mtime-based tie-breaking with explicit os.utime manipulation. Collisions now emit WARN log entries naming the chosen candidate and shadowed alternatives.
Kanban worker skill availability refactor
hermes_cli/kanban_db.py, tests/hermes_cli/test_kanban_db.py
Simplifies _kanban_worker_skill_available by delegating to the shared _resolve_skill_under_home("kanban-worker", hermes_home) helper, replacing bespoke logic that scanned only local <home>/skills and the canonical bundled path. Newly added TestKanbanWorkerSkillAvailable regression suite validates behavior for local-only, missing, external-dir-only, and local-vs-external collision scenarios, confirming that the function now correctly accounts for skills.external_dirs configuration entries.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • sahilm-ti/hermes-agent#7: Introduces _resolve_skill_under_home() helper for dispatcher-side skill pre-flight validation; this PR refactors _kanban_worker_skill_available() to reuse that same helper for external_dirs support.

Poem

🐰 A rabbit hops through skill collisions,
No more ambiguous decisions!
Tier by tier, mtime to guide,
Picks the swiftest with a wink and pride. 🎯✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/skill-collision-resolution

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

sahilm-ti added a commit that referenced this pull request May 25, 2026
…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.
sahilm-ti added a commit that referenced this pull request May 28, 2026
…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.
sahilm-ti added a commit that referenced this pull request May 28, 2026
…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.
sahilm-ti added a commit that referenced this pull request May 28, 2026
…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.
sahilm-ti added a commit that referenced this pull request May 29, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jun 3, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jun 5, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jun 15, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jun 17, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jun 22, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 3, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 9, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 10, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 11, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 13, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 15, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 17, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 21, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 23, 2026
…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.
sahilm-ti added a commit that referenced this pull request Jul 28, 2026
…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.
sahilm-ti added a commit that referenced this pull request Aug 24, 2026
…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.
sahilm-ti added a commit that referenced this pull request Sep 2, 2026
…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.
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