Skip to content

refactor: centralize optional tools lifecycle with registry-driven install and cleanup#76

Merged
wpfleger96 merged 7 commits into
mainfrom
worktree-wpfleger+fix-managed-component-cleanup
Jun 3, 2026
Merged

refactor: centralize optional tools lifecycle with registry-driven install and cleanup#76
wpfleger96 merged 7 commits into
mainfrom
worktree-wpfleger+fix-managed-component-cleanup

Conversation

@wpfleger96
Copy link
Copy Markdown
Owner

@wpfleger96 wpfleger96 commented Jun 2, 2026

When a managed component is removed from config, several artifact types were left behind — most visibly recall MCP entries persisting in Goose's config and the recall-mcp-server uv tool remaining installed after the feature was disabled in #73. More broadly, the optional tools pipeline had per-tool hardcoded functions (ensure_recall_installed, ensure_statusline_installed) that were structurally isomorphic but duplicated across 100+ lines each, with matching result-handling blocks duplicated in OptionalToolsComponent.

This introduces two complementary registries in bootstrap/registry.py that serve as the single source of truth for tool lifecycle: DEPRECATED_TOOLS (retired tools to clean up) and ACTIVE_TOOLS (tools to install/upgrade). Adding a new tool in either category requires one registry entry — no new functions, no hardcoded if-chains.

  • Adds DeprecatedToolSpec registry with is_still_in_use callback and get_deprecated_mcp_names() for MCP integration, and ActiveToolSpec registry with command_name field and lazy get_install_spec to avoid circular imports
  • Replaces ensure_recall_installed() and ensure_statusline_installed() with a single generic ensure_tool_installed(spec, source, ...) that handles fresh install, upgrade, source switching, local reinstall, and skip_update_check for fast presence-only checks
  • Replaces ensure_recall_uninstalled() with generic ensure_tool_uninstalled(command_name, package_name, ...) using a dual get_tool_source/is_command_available guard to avoid touching non-uv-managed tools
  • Refactors OptionalToolsComponent to iterate both registries in all lifecycle methods — uninstall() now removes both active and deprecated tools, status() correctly labels configured deprecated tools as user-managed
  • Extracts _compute_stale_tool_ids(), _remove_stale_tools(), _install_active_tools(), and _emit_install_result() to eliminate all per-tool duplication
  • Wires GooseMCPManager._previously_managed_names to get_deprecated_mcp_names() instead of a hardcoded frozenset
  • Moves _is_recall_configured from installer.py into registry.py where its only consumer lives; removes all other recall install infrastructure
  • Fixes --only settings validation: select_components now validates against INSTALL_COMPONENTS instead of just semantic components
  • Fixes SettingsComponent.uninstall early return that skipped tracker_path cleanup when cache_dir was outside $HOME
  • Adds uninstall() to ClaudePluginComponent (partial-failure-safe manifest clearing) and SettingsComponent (path containment check)
  • Fixes source-deleted extension cleanup, broken skill symlink removal, missing deprecated symlink cleanup in config uninstall, and Codex empty tracking section cleanup
  • Adds 13 new tests for ensure_tool_installed/ensure_tool_uninstalled and GooseMCPManager unmarked orphan cleanup migration path

Multiple component types left orphaned artifacts when removed from
config — MCP entries without markers escaped cleanup, uv tools were
never uninstalled, and plugins/settings/cache had no uninstall path.
This was a recurring bug class (4th instance in git history).

Adds _previously_managed_names to MCPManager for marker-independent
cleanup, ensure_recall_uninstalled() for uv tool lifecycle, and
uninstall() methods to OptionalToolsComponent, ClaudePluginComponent,
and SettingsComponent. Also fixes source-deleted hook cleanup in
extensions uninstall, broken symlink handling in skills uninstall,
and deprecated symlink cleanup in config uninstall.
… tool registry

The initial cleanup fix hardcoded all logic to the recall use case —
ensure_recall_uninstalled(), if-checks for "recall", and a manual
frozenset tombstone. Adding another retired tool would require
duplicating all of it.

Introduces DeprecatedToolSpec registry in bootstrap/registry.py as
the single source of truth. OptionalToolsComponent now iterates the
registry generically, GooseMCPManager._previously_managed_names
derives from it, and ensure_tool_uninstalled() replaces the
recall-specific variant. Also fixes dry-run guards in plugin/settings
uninstall, partial-failure manifest clearing, --only scoping for
SettingsComponent, and stale-vs-missing status semantics.
@wpfleger96 wpfleger96 changed the title fix: complete managed component cleanup across all lifecycle paths refactor: centralize managed component cleanup with deprecated tool registry Jun 2, 2026
…istry

ensure_recall_installed() and ensure_statusline_installed() were
structurally isomorphic 100-line functions with identical branch
structure, return vocabulary, and underlying helpers. Adding a new
tool required writing another per-tool function and duplicating the
result-handling block in OptionalToolsComponent.

Replaces both with a single ensure_tool_installed(spec, source, ...)
that operates on any ToolSpec. Adds ACTIVE_TOOLS registry alongside
the existing DEPRECATED_TOOLS registry — OptionalToolsComponent now
iterates both registries generically. Also removes all recall install
infrastructure (RECALL_GITHUB_REPO, _RECALL_SPEC, recall updater
helpers) since recall is fully deprecated.
@wpfleger96 wpfleger96 changed the title refactor: centralize managed component cleanup with deprecated tool registry refactor: centralize optional tools lifecycle with registry-driven install and cleanup Jun 3, 2026
Crossfire review (4 Claude specialists + Codex + Gemini) surfaced 12
findings. Key fixes:

- Validate `--only` against all INSTALL_COMPONENTS, not just semantic
  components — `--only settings` was advertised by shell completion
  but rejected at runtime
- Extract `_compute_stale_tool_ids()` to eliminate plan/install
  duplication
- Rename `DeprecatedToolSpec.is_configured` to `is_still_in_use` for
  self-documenting semantics at call sites
- Move `_is_recall_configured` from installer.py into registry.py
  (its only consumer) to stop leaking a private function across
  module boundaries
- Add `command_name` to `ActiveToolSpec` and uninstall active tools
  during `uninstall` (was only cleaning up deprecated tools)
- Fix `SettingsComponent.uninstall` early return that skipped
  tracker_path cleanup when cache_dir was outside $HOME
- Use dual `get_tool_source`/`is_command_available` guard in
  `ensure_tool_uninstalled` to avoid touching non-uv-managed tools
- Add `skip_update_check` param to `ensure_tool_installed` so
  `install` is a fast presence-check without blocking network calls
- Fix status messaging for configured deprecated tools (no longer
  reported as missing since install won't satisfy them)
- Add 13 new tests: 11 for ensure_tool_installed/uninstalled, 2 for
  GooseMCPManager unmarked orphan cleanup migration path
…-managed-component-cleanup

* origin/main:
  feat(testing): add cross-platform E2E test suite (#78)
  feat: add native Windows support (#74)
  chore(main): release 0.59.0 (#75)
  docs(agents): add infrastructure awareness, quality gate, git-pull rule, PR cross-referencing; compress 32% (#77)
The crossfire review commit introduced 12 mypy errors: untyped
`_make_spec` helper, `str | None` operand in `in` expression,
`list.append` in expression context, and a `spec` variable shadowing
`ToolSpec` with `DeprecatedToolSpec` in the uninstall loop.

Adds 5 E2E tests exercising the registry-driven lifecycle via the
real CLI binary: install dry-run, status with optional tools, uninstall
with active+deprecated tools, `--only settings` acceptance (crossfire
finding #1), and invalid `--only` rejection.
Add optional tool registry gotcha with ✅/❌ examples, E2E test
commands and fixture pointers, bootstrap/registry.py to project
structure, and add/retire tool entry to key files table.
@wpfleger96 wpfleger96 merged commit 05f9585 into main Jun 3, 2026
9 checks passed
@wpfleger96 wpfleger96 deleted the worktree-wpfleger+fix-managed-component-cleanup branch June 3, 2026 23:59
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