Skip to content

refactor(skills): bundle search/analytics/geospatial under hotdata + fix CLI drift - #244

Open
eddietejeda wants to merge 3 commits into
mainfrom
refactor/skills-nested-subskills
Open

refactor(skills): bundle search/analytics/geospatial under hotdata + fix CLI drift#244
eddietejeda wants to merge 3 commits into
mainfrom
refactor/skills-nested-subskills

Conversation

@eddietejeda

Copy link
Copy Markdown
Contributor

What

Make only hotdata appear in the Claude Code / agent skill list (autocomplete). The three specialized guides — search, analytics, geospatial — are kept in full but bundled inside the hotdata skill under skills/hotdata/subskills/<name>/ and loaded on demand (progressive disclosure) rather than installed as three separate top-level skills.

Why

Claude Code discovers skills at the first level only (<skills-root>/<name>/SKILL.md); nested SKILL.md files are not registered. Bundling the three under hotdata/subskills/ means they still ship in the release tarball and are Read on demand, but they no longer clutter the skill autocomplete — hotdata is the single entry point that dispatches to them.

Changes

Structure / install

  • Move hotdata-{search,analytics,geospatial}/hotdata/subskills/{search,analytics,geospatial}/ (content preserved).
  • skill.rs: SKILL_NAMES = ["hotdata"]; add RETIRED_SKILL_NAMES + best-effort cleanup so an upgrader's stale top-level hotdata-search / -analytics / -geospatial entries are removed from the store, ~/.agents, and each agent root (global and --project install paths).
  • hotdata/SKILL.md: "Sub-skills (loaded on demand)" section that Reads the nested files by path with their trigger keywords.
  • Cargo.toml: pre-release version bumps point at the nested sub-skill paths.
  • Release tarball (tar -czf skills.tar.gz skills/) already recurses, so the nested files ship; extraction yields exactly one top-level hotdata dir (verified).

CLI drift fixes (verified against the v0.21.0 CLI)

  • connections is not a command — sources are ingest datasources. Replace hotdata connections list/create/refresh with ingest list-datasources / new-datasource / show-datasource.
  • tables list has no --connection-id; indexes list has no -c/--connection-id — use --schema/--table filters.
  • Add managed_load to jobs --job-type; add --result-id to databases load / databases tables load; document query status exit codes (0/1/2/3).
  • Repoint all cross-skill relative links to the new nested locations (all links validated to resolve).

Verification

  • cargo build + cargo test pass.
  • Tarball extraction simulated: only hotdata becomes a top-level skill dir; sub-skills nested under hotdata/subskills/.
  • Every relative markdown link across the skill docs resolves to an existing file.
  • Swept for residual connections / --connection-id / indexes list -c drift: none remain.

…fix CLI drift

Only the top-level `hotdata` skill should appear in the agent skill list
(autocomplete). The specialized guides — search, analytics, geospatial — are now
bundled inside it under `skills/hotdata/subskills/<name>/` and loaded on demand
via progressive disclosure, instead of installing as three separate top-level
skills.

Nested SKILL.md files are not discovered by Claude Code (discovery is first-level
only), so only `hotdata` registers while the three sub-skills ride along inside
the `hotdata` directory in the release tarball.

- skill.rs: `SKILL_NAMES = ["hotdata"]`; add `RETIRED_SKILL_NAMES` + cleanup so
  upgraders' stale top-level `hotdata-search` / `-analytics` / `-geospatial`
  entries are removed from the store, `~/.agents`, and each agent root on
  install/update (global and project paths).
- hotdata/SKILL.md: replace "Bundled sub-skills / install all" with a
  "Sub-skills (loaded on demand)" section that Reads the nested files by path
  with trigger keywords.
- Cargo.toml: point pre-release version bumps at the nested sub-skill paths.

Also fix accumulated CLI drift in the skill docs (verified against the v0.21.0
CLI):
- `connections` is not a command — sources are `ingest` datasources; replace
  `hotdata connections list/create/refresh` with `ingest list-datasources` /
  `new-datasource` / `show-datasource`.
- `tables list` has no `--connection-id`; `indexes list` has no `-c` /
  `--connection-id` — use `--schema` / `--table` filters.
- add `managed_load` to `jobs --job-type`; add `--result-id` to
  `databases load` / `databases tables load`; document `query status` exit codes
  (0/1/2/3).
- repoint all cross-skill relative links to the new nested locations.
@eddietejeda
eddietejeda requested a review from a team as a code owner August 1, 2026 00:36
@eddietejeda
eddietejeda requested review from shefeek-jinnah and removed request for a team August 1, 2026 00:36
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.51020% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/commands/skill.rs 75.51% 12 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread src/commands/skill.rs
/// `hotdata/subskills/` (see `RETIRED_SKILL_NAMES`). Cleans the store, the
/// `~/.agents` layer, and every detected home agent root. Best-effort: a
/// retired skill that isn't present is simply skipped.
fn remove_retired_skills_global() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove_retired_skills_global() is called from install() (line 572) and install_for_version() (line 287), but not from maybe_auto_update_after_cli_upgrade() — which is the auto-refresh path for users who upgrade the binary out-of-band (Homebrew, curl installer, package manager) and then just run any hotdata command.

Failure scenario: a user on v0.21.0 has ~/.hotdata/skills/hotdata-search plus ~/.claude/skills/hotdata-search from the old layout. They brew upgrade hotdata to the version carrying this change, then run hotdata tables list. maybe_auto_update_after_cli_upgrade() sees installed < current, downloads the new tarball, calls ensure_symlinks() (line 170) — and returns. The retired top-level dirs are never removed. On every subsequent run needs_refresh is now false (line 149-152), so the function early-returns at line 154 and the cleanup never gets a second chance. The stale hotdata-search / -analytics / -geospatial entries stay in the agent skill list permanently unless the user happens to run hotdata skills install by hand — which defeats the stated purpose of this PR.

Fix: call remove_retired_skills_global() right after ensure_symlinks() at line 170.

**Prerequisites:** Authenticate, workspace, and catalog discovery via the **`hotdata`** skill (`connections`, `tables`, `databases`).
**Prerequisites:** Authenticate, workspace, and catalog discovery via the **`hotdata`** skill (`ingest` datasources, `tables`, `databases`).

**Related skills:** **`hotdata-search`** (BM25, vector, retrieval indexes), **`hotdata-geospatial`** (spatial SQL).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: cross-references between sub-skills are still bare names (hotdata-search, hotdata-geospatial) with no path. The name→path mapping only exists in the parent skills/hotdata/SKILL.md:27, and an agent that Reads this file on demand may not have that line in context — it's told to use a skill that is no longer registered anywhere. Same pattern in subskills/search/SKILL.md:13,65,66, subskills/geospatial/SKILL.md:11, and subskills/analytics/references/WORKFLOWS.md:5,27.

Suggest making them relative paths the way references/WORKFLOWS.md:15-17 already does, e.g. **`hotdata-search`** (`../search/SKILL.md`). (not blocking)

Comment thread src/commands/skill.rs

/// Project-scoped counterpart to `remove_retired_skills_global`: clears stale
/// retired skills from `./.agents/skills` and each detected project agent root.
fn remove_retired_skills_project(cwd: &std::path::Path, project_skills_root: &std::path::Path) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove_retired_skills_project is already parameterized on cwd and project_skills_root (unlike its global sibling, which reads $HOME), so it's directly unit-testable with a tempdir — but no test was added. A test that lays down .agents/skills/hotdata-search + .claude/skills/hotdata-search (as a symlink, to exercise the is_symlink() branch in remove_skill_path) and asserts both are gone while hotdata survives would pin the behavior this PR exists to deliver. (not blocking)

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Blocking Issues

  • src/commands/skill.rs:140-192maybe_auto_update_after_cli_upgrade() downloads the new tarball and calls ensure_symlinks() but never calls remove_retired_skills_global(). install() (line 572) and install_for_version() (line 287) both do. Users who upgrade the binary out-of-band (Homebrew, curl installer, package manager) hit only the auto-update path, so their stale top-level hotdata-search / hotdata-analytics / hotdata-geospatial entries are left in ~/.hotdata/skills, ~/.agents/skills, and ~/.claude/skills. Worse, once the auto-update succeeds the version check at line 149-152 makes needs_refresh false forever, so the cleanup never runs on a later invocation either — the retired skills stay in the autocomplete list permanently unless the user manually runs hotdata skills install. That is the exact outcome this PR is meant to prevent.

Action Required

Call remove_retired_skills_global() after ensure_symlinks() in maybe_auto_update_after_cli_upgrade() (line 170).

Everything else checks out — I verified the CLI drift claims against the source (ingest list-datasources/new-datasource/show-datasource exist; tables list and indexes list have no --connection-id, with indexes.rs:1368 asserting rejection; --result-id on both load paths; managed_load in the --job-type value parser; EXIT_INCOMPLETE_RESULT = 3), confirmed every relative markdown link across the moved files resolves, and confirmed dist-workspace.toml:27 tars skills/ recursively so the nested sub-skills still ship.

Address review on #244.

- `maybe_auto_update_after_cli_upgrade()` (the auto-refresh path hit by
  Homebrew/curl/package-manager upgraders) refreshed symlinks but never called
  `remove_retired_skills_global()`, and its version gate means it never runs
  again — so stale top-level `hotdata-search`/`-analytics`/`-geospatial` entries
  would persist permanently. Call cleanup right after `ensure_symlinks()`.
- Add a unit test for `remove_retired_skills_project` (real dir + symlink →
  both removed, `hotdata` survives).
- Give sub-skill cross-references relative paths (e.g. `../search/SKILL.md`) so a
  sub-skill loaded on demand can reach its siblings without the parent's
  name→path mapping in context.
@eddietejeda

Copy link
Copy Markdown
Contributor Author

Addressed in 975b5aa:

  • Blockingmaybe_auto_update_after_cli_upgrade() now calls remove_retired_skills_global() right after ensure_symlinks(), so out-of-band upgraders (Homebrew/curl/package manager) get the stale top-level hotdata-search/-analytics/-geospatial entries removed on the same run the tarball refreshes. Good catch on the permanent-persistence path.
  • Nit (test) — added remove_retired_skills_project_clears_stale_top_level_dirs: lays down a retired skill as a real dir in .agents and a symlink in .claude (exercises both remove_skill_path branches), asserts both are gone and hotdata survives.
  • Nit (cross-refs) — sub-skill cross-references now carry relative paths (e.g. ../search/SKILL.md), so a sub-skill loaded on demand can reach its siblings without relying on the parent's name→path mapping being in context.

cargo test + cargo fmt --check pass; all relative markdown links re-validated.

Comment thread src/commands/skill.rs
return;
}
let _ = if path.is_symlink() {
fs::remove_file(path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: on Windows, fs::remove_file fails for directory symlinks (DeleteFileW rejects directories) — fs::remove_dir is the one that removes a dir symlink. ensure_symlink_or_copy creates these with symlink_dir (line 330), so on a Windows box with Developer Mode/admin enabled the ~/.agents/skills/hotdata-search and ~/.claude/skills/hotdata-search links are directory symlinks and this removal silently no-ops (the error is swallowed by let _ =). The store copy is a real dir so it goes away, but the retired entries stay in the agent skill list — the exact failure this PR is fixing. Windows users without symlink privilege get copies, which remove_dir_all handles fine, so it only bites the symlink-capable case.

Suggested change
fs::remove_file(path)
let _ = if path.is_symlink() {
fs::remove_file(path).or_else(|_| fs::remove_dir(path))
} else {

(ensure_symlink_or_copy at line 316 has the same pattern, but there the error surfaces to the caller rather than being dropped.) (not blocking)

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prior blocking issue resolved (retired-skill cleanup now runs on the out-of-band upgrade path), and both nits addressed. Verified the CLI-drift doc changes against the v0.21.0 source. One non-blocking nit inline.

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