Files
David SFandGitHub 310ffadf09 refactor: graduate capabilities out of experimental (ACP excepted) (#347)
* refactor: graduate capabilities out of `experimental` (ACP excepted)

The `experimental` label suppressed the try->fail->report->improve loop we
want from real usage, so drop it from the harness. Ten capabilities move to
top-level submodules; ACP stays experimental (it may still be reshaped or
moved to core, and can't be generic across agents).

- Old `pydantic_ai_harness.experimental.<name>` paths keep working as
  DeprecationWarning shims (via `warn_moved`), so existing imports don't break.
- Renames to match capability names: authoring -> runtime_authoring,
  overflow -> overflowing_tool_output.
- Capabilities stay in individual submodules with no top-level re-export, so
  importing the root package never pulls in a capability's optional deps.
- Docs drop the experimental framing and the "may be removed in any release"
  language in favor of "API subject to change".

* docs: tell capability authors to ask the user when unsure about a name

Per PR review: a name is a public commitment once shipped, so ask rather than guess.
2026-07-10 21:31:08 -05:00

60 lines
2.6 KiB
Python

"""Locate (not parse) a repo's coding-assistant CE assets."""
from __future__ import annotations
from collections.abc import Sequence
from pathlib import Path
from pydantic import BaseModel, Field
_ROOT_NOTES = {
'.codex': 'Codex uses TOML config; assets are derived from the .claude/.agents setup.',
'.grok': 'Grok setup is derived from the .claude/.agents setup.',
}
class AssetRoot(BaseModel):
"""Where CE assets live under a single root directory (e.g. `.claude`)."""
root: str = Field(description='The root directory name, relative to the workspace, e.g. ".claude".')
exists: bool = Field(description='Whether the root directory is present in the workspace.')
skills: list[str] = Field(default_factory=list, description='Paths to SKILL.md files found under skills/.')
agents: list[str] = Field(default_factory=list, description='Paths to agent .md files found under agents/.')
settings: str | None = Field(default=None, description='Path to settings.json (hooks), if present.')
notes: str | None = Field(default=None, description='Format or derivation notes for this root, if any.')
class AgentContextInventory(BaseModel):
"""A map of where a repo's CE assets live, for an orchestrator to read or translate."""
roots: list[AssetRoot] = Field(default_factory=list[AssetRoot], description='One entry per scanned root directory.')
def _relposix(path: Path, workspace: Path) -> str:
try:
return path.resolve().relative_to(workspace).as_posix()
except ValueError:
return path.as_posix()
def scan_assets(workspace_dir: Path, asset_roots: Sequence[str]) -> AgentContextInventory:
"""Scan `asset_roots` under `workspace_dir`, locating skills, agents, and hooks.
This locates assets only; it does not open or parse SKILL.md, agent `.md`, or
`settings.json` contents.
"""
workspace = workspace_dir.resolve()
roots: list[AssetRoot] = []
for name in asset_roots:
directory = workspace / name
notes = _ROOT_NOTES.get(name)
if not directory.is_dir():
roots.append(AssetRoot(root=name, exists=False, notes=notes))
continue
skills = sorted(_relposix(p, workspace) for p in directory.glob('skills/**/SKILL.md') if p.is_file())
agents = sorted(_relposix(p, workspace) for p in directory.glob('agents/*.md') if p.is_file())
settings_path = directory / 'settings.json'
settings = _relposix(settings_path, workspace) if settings_path.is_file() else None
roots.append(AssetRoot(root=name, exists=True, skills=skills, agents=agents, settings=settings, notes=notes))
return AgentContextInventory(roots=roots)