mirror of
https://github.com/github/spec-kit.git
synced 2026-07-07 14:39:00 +08:00
fix: register enabled extensions for agent on integration use/upgrade (#2949)
* fix: register enabled extensions for agent on integration install/upgrade install and upgrade only set up the integration's own core commands; only switch re-registered the enabled extensions' commands for the target agent. A second integration added via install (or refreshed via upgrade) was therefore silently missing the extension commands the existing agents already had (e.g. the bundled agent-context extension). Extract switch's registration into a shared _register_extensions_for_agent helper and call it from install and upgrade too, so every installed agent ends up with every enabled extension's commands — full parity with switch. Closes #2886 * test: pin skills-mode secondary-agent registration; document #2948 limitation Extension skill rendering is scoped to the active agent (init-options track a single ai / ai_skills pair), so a skills-mode agent registered while not active (e.g. Copilot --skills installed as a secondary integration) gets command files rather than skills. install/upgrade match extension add here; only switch renders skills, because it activates the target first. Add a regression test pinning this behavior and document the limitation on the shared helper. Per-agent skills parity is tracked separately in #2948. * fix: don't re-render the active agent's skills when registering a non-active agent register_enabled_extensions_for_agent runs an active-agent-scoped skills pass (_register_extension_skills resolves the skills dir from init-options["ai"], ignoring the passed agent). Routing install/upgrade of a secondary integration through it re-rendered the *active* skills-mode agent's extension skills as a side effect — resurrecting skill files the user had deliberately deleted. Gate the skills pass on the target being the active agent; switch is unaffected because it activates the target first. Also harden the skills-mode install test (assert a core skill so --skills is load-bearing, drop a vacuous registered_skills assertion) and add a regression test. Surfaced by review of the PR; skills parity for non-active agents stays tracked in #2948. * refactor: share the extension-op scaffold and run (un)registration post-commit Review cleanups, no behavior change on the success path: - Extract the best-effort ExtensionManager scaffold (lazy import, instantiate, except -> _print_cli_warning) into _best_effort_extension_op. Both _register_extensions_for_agent and a new _unregister_extensions_for_agent delegate to it, removing the duplicate block left inline in switch. - Invoke the best-effort extension registration AFTER the install/switch/upgrade try/except has committed, so a failure in it can never trigger the rollback (install and switch teardown on except). * docs: clarify extension registration parity scope * fix(integrations): defer extension registration until use * fix(tests): remove redundant shutil import * fix(integrations): backfill extensions for installed switch targets
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
import typer
|
||||
|
||||
@@ -387,6 +387,93 @@ def _set_default_integration_or_exit(*args: Any, **kwargs: Any) -> None:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Extension (un)registration helpers (shared by use / switch / upgrade)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _best_effort_extension_op(
|
||||
project_root: Path,
|
||||
agent_key: str,
|
||||
op: Callable[[Any, str], None],
|
||||
*,
|
||||
phase: str,
|
||||
continuing: str,
|
||||
) -> None:
|
||||
"""Run a best-effort ``ExtensionManager`` operation for ``agent_key``.
|
||||
|
||||
``op`` receives the ``ExtensionManager`` and ``agent_key``. Any failure is
|
||||
surfaced as a warning via ``_print_cli_warning`` and never aborts the
|
||||
surrounding integration operation. ``continuing`` describes what already
|
||||
succeeded so the warning makes the partial outcome clear.
|
||||
"""
|
||||
try:
|
||||
from ..extensions import ExtensionManager
|
||||
|
||||
ext_mgr = ExtensionManager(project_root)
|
||||
op(ext_mgr, agent_key)
|
||||
except Exception as ext_err:
|
||||
from .. import _print_cli_warning
|
||||
|
||||
_print_cli_warning(phase, "integration", agent_key, ext_err, continuing=continuing)
|
||||
|
||||
|
||||
def _register_extensions_for_agent(
|
||||
project_root: Path,
|
||||
agent_key: str,
|
||||
*,
|
||||
continuing: str,
|
||||
) -> None:
|
||||
"""Register all enabled extensions' commands/skills for ``agent_key``.
|
||||
|
||||
``use`` / ``switch`` re-register enabled extensions for the agent they
|
||||
activate; ``upgrade`` backfills them for the refreshed agent. Plain
|
||||
``install`` deliberately does not call this helper so adding a secondary
|
||||
integration has no extension side effects until it is selected or upgraded.
|
||||
See issue #2886.
|
||||
|
||||
Known limitation: extension *skill* rendering is scoped to the active
|
||||
agent (init-options track a single ``ai`` / ``ai_skills`` pair). A
|
||||
skills-mode agent registered while it is *not* the active agent (e.g.
|
||||
Copilot ``--skills`` registered while non-active) therefore
|
||||
receives command files rather than skills here — matching ``extension
|
||||
add``'s multi-agent behavior. ``use`` / ``switch`` avoid this because they
|
||||
make the target the active agent first. Per-agent skills parity is tracked in
|
||||
#2948.
|
||||
|
||||
Best-effort: never aborts the surrounding integration operation. Callers
|
||||
invoke it *after* the use/upgrade/switch transaction has committed so a
|
||||
failure here cannot trigger a rollback.
|
||||
"""
|
||||
_best_effort_extension_op(
|
||||
project_root,
|
||||
agent_key,
|
||||
lambda mgr, key: mgr.register_enabled_extensions_for_agent(key),
|
||||
phase="register extension artifacts for",
|
||||
continuing=continuing,
|
||||
)
|
||||
|
||||
|
||||
def _unregister_extensions_for_agent(
|
||||
project_root: Path,
|
||||
agent_key: str,
|
||||
*,
|
||||
continuing: str,
|
||||
) -> None:
|
||||
"""Best-effort removal of ``agent_key``'s extension artifacts.
|
||||
|
||||
Used by ``switch`` when uninstalling the previous integration so its
|
||||
extension command/skill files don't linger as orphans in the old agent's
|
||||
directory.
|
||||
"""
|
||||
_best_effort_extension_op(
|
||||
project_root,
|
||||
agent_key,
|
||||
lambda mgr, key: mgr.unregister_agent_artifacts(key),
|
||||
phase="clean up extension artifacts for",
|
||||
continuing=continuing,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI formatting helpers (re-exported from _commands.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user