feat: structured skill packages with config overrides, chaining, and TUI integration

Add a skill package system where each skill is a directory with a skill.yaml
manifest and prompt markdown files. Skills support /command triggers, scoped
config overrides (temperature, max_tokens, tool filtering), chain dependencies
with cycle-safe resolution, and a finish_skill completion signal.

Includes four built-in skills: explore, brainstorm, write-document, and plan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 19:06:05 -05:00
parent 26bcbc6c1f
commit 2ae8294e29
16 changed files with 832 additions and 31 deletions

View File

@@ -20,6 +20,7 @@ class ToolRegistry:
def __init__(self) -> None:
self._tools: dict[str, BaseTool] = {}
self._disabled: set[str] = set()
def register(self, tool: BaseTool) -> None:
"""Register a tool instance. Raises ValueError on duplicate name."""
@@ -29,24 +30,74 @@ class ToolRegistry:
logger.debug("Registered tool: %s", tool.name)
def get(self, name: str) -> BaseTool | None:
"""Look up a tool by name."""
"""Look up a tool by name. Returns None if disabled or not found."""
if name in self._disabled:
return None
return self._tools.get(name)
def get_all(self) -> dict[str, BaseTool]:
"""Return all registered tools."""
return dict(self._tools)
"""Return all registered tools (excluding disabled)."""
return {k: v for k, v in self._tools.items() if k not in self._disabled}
def get_openai_tools_schema(self) -> list[dict[str, Any]]:
"""Return OpenAI function-calling schemas for all registered tools."""
return [tool.get_openai_schema() for tool in self._tools.values()]
"""Return OpenAI function-calling schemas for all active tools."""
return [
tool.get_openai_schema()
for tool in self._tools.values()
if tool.name not in self._disabled
]
def apply_filter(
self,
*,
enable: list[str] | None = None,
disable: list[str] | None = None,
) -> set[str]:
"""Apply a tool filter, returning the previous disabled set for restoration.
Args:
enable: If set, only these tools (plus always-on tools) are available.
disable: Specific tools to disable.
Returns:
The previous disabled set (snapshot for restore).
"""
previous = set(self._disabled)
if enable is not None:
# Whitelist mode: disable everything not in the enable list
self._disabled = {name for name in self._tools if name not in enable}
elif disable is not None:
# Blacklist mode: add to existing disabled set (preserves global disables)
self._disabled = set(self._disabled) | set(disable)
else:
self._disabled = set()
return previous
def restore_filter(self, previous: set[str]) -> None:
"""Restore a previous filter state."""
self._disabled = previous
def all_tool_names(self) -> list[str]:
"""Return all registered tool names (including disabled)."""
return list(self._tools.keys())
def create_default_registry(
workspace_root: Path,
config: AppConfig,
skills_manager: SkillsManager | None = None,
skill_runner: object | None = None,
) -> ToolRegistry:
"""Create a ToolRegistry populated with all built-in tools."""
"""Create a ToolRegistry populated with all built-in tools.
Args:
workspace_root: Workspace root path.
config: Application configuration.
skills_manager: Optional skills manager for skill tools.
skill_runner: Optional SkillRunner for package skill activation.
"""
# Read tools
from app.tools.filesystem import ListDirTool, ReadFileTool
@@ -92,8 +143,11 @@ def create_default_registry(
# Skills (conditional)
if skills_manager is not None:
from app.tools.skills import LoadSkillTool
from app.services.skill_runner import SkillRunner as SkillRunnerType
from app.tools.skills import FinishSkillTool, LoadSkillTool
registry.register(LoadSkillTool(workspace_root, config, skills_manager))
runner = skill_runner if isinstance(skill_runner, SkillRunnerType) else None
registry.register(LoadSkillTool(workspace_root, config, skills_manager, runner))
registry.register(FinishSkillTool(workspace_root, config, runner))
return registry