mirror of
https://github.com/pydantic/pydantic-ai-harness.git
synced 2026-07-21 02:45:34 +00:00
* 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.
60 lines
2.6 KiB
Python
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)
|