agent-notes 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Agent registry for loading and managing agent configurations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
|
|
8
|
+
from ..config import AGENTS_YAML
|
|
9
|
+
from ..domain.agent import AgentSpec
|
|
10
|
+
from ._base import load_yaml_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentRegistry:
|
|
14
|
+
"""Registry of agent configurations from agents.yaml."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, agents: list[AgentSpec]):
|
|
17
|
+
self._agents = agents
|
|
18
|
+
self._by_name = {a.name: a for a in agents}
|
|
19
|
+
|
|
20
|
+
def all(self) -> list[AgentSpec]:
|
|
21
|
+
"""Return all agents."""
|
|
22
|
+
return self._agents.copy()
|
|
23
|
+
|
|
24
|
+
def get(self, name: str) -> AgentSpec:
|
|
25
|
+
"""Get agent by name. Raises KeyError if unknown."""
|
|
26
|
+
if name not in self._by_name:
|
|
27
|
+
raise KeyError(f"Agent '{name}' not found in registry")
|
|
28
|
+
return self._by_name[name]
|
|
29
|
+
|
|
30
|
+
def names(self) -> list[str]:
|
|
31
|
+
"""Return sorted list of agent names."""
|
|
32
|
+
return sorted(self._by_name.keys())
|
|
33
|
+
|
|
34
|
+
def with_role(self, role: str) -> list[AgentSpec]:
|
|
35
|
+
"""Return agents with the specified role."""
|
|
36
|
+
return [a for a in self._agents if a.role == role]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_agent_registry(yaml_path: Optional[Path] = None) -> AgentRegistry:
|
|
40
|
+
"""Load agents from agents.yaml. Single file, not a directory."""
|
|
41
|
+
if yaml_path is None:
|
|
42
|
+
yaml_path = AGENTS_YAML
|
|
43
|
+
|
|
44
|
+
if not yaml_path.exists():
|
|
45
|
+
raise ValueError(f"Agents file not found: {yaml_path}")
|
|
46
|
+
|
|
47
|
+
data = load_yaml_file(yaml_path)
|
|
48
|
+
|
|
49
|
+
# The YAML has an 'agents' top-level key
|
|
50
|
+
agents_data = data.get("agents", {})
|
|
51
|
+
if not agents_data:
|
|
52
|
+
return AgentRegistry([])
|
|
53
|
+
|
|
54
|
+
# Known top-level keys that are NOT per-backend config
|
|
55
|
+
NON_BACKEND_KEYS = {"description", "role", "mode", "color", "effort", "claude_exclude"}
|
|
56
|
+
|
|
57
|
+
agents = []
|
|
58
|
+
for name, config in agents_data.items():
|
|
59
|
+
# Extract required fields
|
|
60
|
+
if "description" not in config:
|
|
61
|
+
raise ValueError(f"Missing 'description' field for agent '{name}' in {yaml_path}")
|
|
62
|
+
if "role" not in config:
|
|
63
|
+
raise ValueError(f"Missing 'role' field for agent '{name}' in {yaml_path}")
|
|
64
|
+
if "mode" not in config:
|
|
65
|
+
raise ValueError(f"Missing 'mode' field for agent '{name}' in {yaml_path}")
|
|
66
|
+
|
|
67
|
+
# Everything that is NOT in NON_BACKEND_KEYS is treated as per-backend config.
|
|
68
|
+
# This is what makes the loader registry-driven: adding a new CLI named
|
|
69
|
+
# "gemini" just means agents.yaml may have a 'gemini:' key and it flows
|
|
70
|
+
# through automatically.
|
|
71
|
+
backends: dict[str, dict] = {}
|
|
72
|
+
for key, value in config.items():
|
|
73
|
+
if key in NON_BACKEND_KEYS:
|
|
74
|
+
continue
|
|
75
|
+
if isinstance(value, dict):
|
|
76
|
+
backends[key] = value
|
|
77
|
+
else:
|
|
78
|
+
# Non-dict top-level keys are unknown; ignore with no error to be
|
|
79
|
+
# lenient with user-edited YAML (could log a warning later).
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# Backward compat: translate legacy top-level claude_exclude into backends
|
|
83
|
+
# Note: This is the only hardcoded "claude" reference for backward compatibility
|
|
84
|
+
if config.get("claude_exclude"):
|
|
85
|
+
backends.setdefault("claude", {})["exclude"] = True
|
|
86
|
+
|
|
87
|
+
agent = AgentSpec(
|
|
88
|
+
name=name,
|
|
89
|
+
description=config["description"],
|
|
90
|
+
role=config["role"],
|
|
91
|
+
mode=config["mode"],
|
|
92
|
+
color=config.get("color"),
|
|
93
|
+
effort=config.get("effort"),
|
|
94
|
+
backends=backends,
|
|
95
|
+
)
|
|
96
|
+
agents.append(agent)
|
|
97
|
+
|
|
98
|
+
# Sort by name for deterministic order
|
|
99
|
+
agents.sort(key=lambda a: a.name)
|
|
100
|
+
|
|
101
|
+
return AgentRegistry(agents)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@lru_cache(maxsize=1)
|
|
105
|
+
def default_agent_registry() -> AgentRegistry:
|
|
106
|
+
"""Return the default agent registry, cached."""
|
|
107
|
+
return load_agent_registry()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""CLI backend registry for loading and managing CLI descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
|
|
9
|
+
from ..config import DATA_DIR
|
|
10
|
+
from ..domain.cli_backend import CLIBackend
|
|
11
|
+
from ._base import load_yaml_dir, require_fields
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CLIRegistry:
|
|
15
|
+
"""Registry of all loaded CLI backends."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, backends: list[CLIBackend]):
|
|
18
|
+
self._backends = backends
|
|
19
|
+
self._by_name = {b.name: b for b in backends}
|
|
20
|
+
|
|
21
|
+
def all(self) -> list[CLIBackend]:
|
|
22
|
+
"""Return all backends."""
|
|
23
|
+
return self._backends.copy()
|
|
24
|
+
|
|
25
|
+
def get(self, name: str) -> CLIBackend:
|
|
26
|
+
"""Get backend by name. Raises KeyError if unknown."""
|
|
27
|
+
return self._by_name[name]
|
|
28
|
+
|
|
29
|
+
def names(self) -> list[str]:
|
|
30
|
+
"""Return sorted list of backend names."""
|
|
31
|
+
return sorted(self._by_name.keys())
|
|
32
|
+
|
|
33
|
+
def with_feature(self, feature: str) -> list[CLIBackend]:
|
|
34
|
+
"""Return backends where self.supports(feature) is True."""
|
|
35
|
+
return [b for b in self._backends if b.supports(feature)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_registry(cli_dir: Optional[Path] = None) -> CLIRegistry:
|
|
39
|
+
"""Load all *.yaml files from cli_dir (default: agent_notes/data/cli/)
|
|
40
|
+
and return a CLIRegistry. Raises ValueError if directory missing or any YAML invalid.
|
|
41
|
+
"""
|
|
42
|
+
if cli_dir is None:
|
|
43
|
+
cli_dir = DATA_DIR / "cli"
|
|
44
|
+
|
|
45
|
+
if not cli_dir.exists():
|
|
46
|
+
raise ValueError(f"CLI directory not found: {cli_dir}")
|
|
47
|
+
|
|
48
|
+
required_fields = ["name", "label", "global_home", "layout", "features"]
|
|
49
|
+
try:
|
|
50
|
+
items = load_yaml_dir(cli_dir, required_fields)
|
|
51
|
+
except ValueError as e:
|
|
52
|
+
# Maintain backward compatibility for error messages
|
|
53
|
+
if "Registry directory not found" in str(e):
|
|
54
|
+
raise ValueError(f"CLI directory not found: {cli_dir}")
|
|
55
|
+
raise
|
|
56
|
+
|
|
57
|
+
backends = []
|
|
58
|
+
for yaml_file, data in items:
|
|
59
|
+
# Expand global_home path
|
|
60
|
+
global_home = Path(data["global_home"]).expanduser()
|
|
61
|
+
|
|
62
|
+
backend = CLIBackend(
|
|
63
|
+
name=data["name"],
|
|
64
|
+
label=data["label"],
|
|
65
|
+
global_home=global_home,
|
|
66
|
+
local_dir=data["local_dir"],
|
|
67
|
+
layout=data["layout"],
|
|
68
|
+
features=data["features"],
|
|
69
|
+
global_template=data.get("global_template"),
|
|
70
|
+
exclude_flag=data.get("exclude_flag"),
|
|
71
|
+
strip_memory_section=data.get("strip_memory_section", False),
|
|
72
|
+
settings_template=data.get("settings_template"),
|
|
73
|
+
accepted_providers=tuple(data.get("accepted_providers", []))
|
|
74
|
+
)
|
|
75
|
+
backends.append(backend)
|
|
76
|
+
|
|
77
|
+
if not backends:
|
|
78
|
+
raise ValueError(f"No CLI backends found in {cli_dir}")
|
|
79
|
+
|
|
80
|
+
# Sort by name for deterministic order
|
|
81
|
+
backends.sort(key=lambda b: b.name)
|
|
82
|
+
|
|
83
|
+
return CLIRegistry(backends)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@lru_cache(maxsize=1)
|
|
87
|
+
def default_registry() -> CLIRegistry:
|
|
88
|
+
"""Return the default CLI registry, cached."""
|
|
89
|
+
return load_registry()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Model registry for loading and managing model descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
|
|
9
|
+
from ..config import DATA_DIR
|
|
10
|
+
from ..domain.model import Model
|
|
11
|
+
from ._base import load_yaml_file, require_fields
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelRegistry:
|
|
15
|
+
def __init__(self, models: list[Model]):
|
|
16
|
+
self._by_id: dict[str, Model] = {m.id: m for m in models}
|
|
17
|
+
|
|
18
|
+
def all(self) -> list[Model]:
|
|
19
|
+
return sorted(self._by_id.values(), key=lambda m: m.id)
|
|
20
|
+
|
|
21
|
+
def get(self, model_id: str) -> Model:
|
|
22
|
+
if model_id not in self._by_id:
|
|
23
|
+
raise KeyError(f"Model '{model_id}' not found in registry")
|
|
24
|
+
return self._by_id[model_id]
|
|
25
|
+
|
|
26
|
+
def ids(self) -> list[str]:
|
|
27
|
+
return sorted(self._by_id.keys())
|
|
28
|
+
|
|
29
|
+
def by_class(self, class_name: str) -> list[Model]:
|
|
30
|
+
"""All models with model_class == class_name, sorted by id."""
|
|
31
|
+
return sorted(
|
|
32
|
+
[m for m in self._by_id.values() if m.model_class == class_name],
|
|
33
|
+
key=lambda m: m.id,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def compatible_with_providers(self, providers: list[str]) -> list[Model]:
|
|
37
|
+
"""Return models that have at least one alias matching providers list.
|
|
38
|
+
Useful for CLI filtering: pass cli.accepted_providers."""
|
|
39
|
+
result = []
|
|
40
|
+
for m in self._by_id.values():
|
|
41
|
+
if any(p in m.aliases for p in providers):
|
|
42
|
+
result.append(m)
|
|
43
|
+
return sorted(result, key=lambda m: m.id)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_model_registry(models_dir: Optional[Path] = None) -> ModelRegistry:
|
|
47
|
+
"""Load all *.yaml files from models_dir (default: data/models/)."""
|
|
48
|
+
if models_dir is None:
|
|
49
|
+
models_dir = DATA_DIR / "models"
|
|
50
|
+
|
|
51
|
+
if not models_dir.is_dir():
|
|
52
|
+
raise ValueError(f"Models directory not found: {models_dir}")
|
|
53
|
+
|
|
54
|
+
models: list[Model] = []
|
|
55
|
+
for yaml_file in sorted(models_dir.glob("*.yaml")):
|
|
56
|
+
try:
|
|
57
|
+
data = load_yaml_file(yaml_file)
|
|
58
|
+
except ValueError as e:
|
|
59
|
+
# Maintain backward compatibility for error messages
|
|
60
|
+
if "Invalid YAML" in str(e):
|
|
61
|
+
raise ValueError(f"Invalid YAML in {yaml_file.name}: {str(e).split(': ', 1)[1]}")
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
require_fields(
|
|
65
|
+
data, ["id", "label", "family", "class", "aliases"], yaml_file,
|
|
66
|
+
msg_template="Missing field '{field}' in {filename}",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
models.append(Model(
|
|
70
|
+
id=data["id"],
|
|
71
|
+
label=data["label"],
|
|
72
|
+
family=data["family"],
|
|
73
|
+
model_class=data["class"],
|
|
74
|
+
aliases=data["aliases"],
|
|
75
|
+
pricing=data.get("pricing", {}) or {},
|
|
76
|
+
capabilities=data.get("capabilities", {}) or {},
|
|
77
|
+
))
|
|
78
|
+
|
|
79
|
+
return ModelRegistry(models)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@lru_cache(maxsize=1)
|
|
83
|
+
def default_model_registry() -> ModelRegistry:
|
|
84
|
+
"""Cached singleton."""
|
|
85
|
+
return load_model_registry()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Role registry for loading and managing role descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
|
|
8
|
+
from ..config import DATA_DIR
|
|
9
|
+
from ..domain.role import Role
|
|
10
|
+
from ._base import load_yaml_file, require_fields
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RoleRegistry:
|
|
14
|
+
def __init__(self, roles: list[Role]):
|
|
15
|
+
self._by_name: dict[str, Role] = {r.name: r for r in roles}
|
|
16
|
+
|
|
17
|
+
def all(self) -> list[Role]:
|
|
18
|
+
return sorted(self._by_name.values(), key=lambda r: r.name)
|
|
19
|
+
|
|
20
|
+
def get(self, name: str) -> Role:
|
|
21
|
+
if name not in self._by_name:
|
|
22
|
+
raise KeyError(f"Role '{name}' not found in registry")
|
|
23
|
+
return self._by_name[name]
|
|
24
|
+
|
|
25
|
+
def names(self) -> list[str]:
|
|
26
|
+
return sorted(self._by_name.keys())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_role_registry(roles_dir: Optional[Path] = None) -> RoleRegistry:
|
|
30
|
+
if roles_dir is None:
|
|
31
|
+
roles_dir = DATA_DIR / "roles"
|
|
32
|
+
|
|
33
|
+
if not roles_dir.is_dir():
|
|
34
|
+
raise ValueError(f"Roles directory not found: {roles_dir}")
|
|
35
|
+
|
|
36
|
+
roles: list[Role] = []
|
|
37
|
+
for yaml_file in sorted(roles_dir.glob("*.yaml")):
|
|
38
|
+
try:
|
|
39
|
+
data = load_yaml_file(yaml_file)
|
|
40
|
+
except ValueError as e:
|
|
41
|
+
# Maintain backward compatibility for error messages
|
|
42
|
+
if "Invalid YAML" in str(e):
|
|
43
|
+
raise ValueError(f"Invalid YAML in {yaml_file.name}: {str(e).split(': ', 1)[1]}")
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
require_fields(
|
|
47
|
+
data, ["name", "label", "description", "typical_class"], yaml_file,
|
|
48
|
+
msg_template="Missing field '{field}' in {filename}",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
roles.append(Role(
|
|
52
|
+
name=data["name"],
|
|
53
|
+
label=data["label"],
|
|
54
|
+
description=data["description"],
|
|
55
|
+
typical_class=data["typical_class"],
|
|
56
|
+
color=data.get("color", ""),
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
return RoleRegistry(roles)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@lru_cache(maxsize=1)
|
|
63
|
+
def default_role_registry() -> RoleRegistry:
|
|
64
|
+
return load_role_registry()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Rule registry for loading and managing rule descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
|
|
8
|
+
from ..config import RULES_DIR
|
|
9
|
+
from ..domain.rule import Rule
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RuleRegistry:
|
|
13
|
+
"""Registry of rule descriptors from data/rules/*.md."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, rules: list[Rule]):
|
|
16
|
+
self._rules = rules
|
|
17
|
+
self._by_name = {r.name: r for r in rules}
|
|
18
|
+
|
|
19
|
+
def all(self) -> list[Rule]:
|
|
20
|
+
"""Return all rules."""
|
|
21
|
+
return self._rules.copy()
|
|
22
|
+
|
|
23
|
+
def get(self, name: str) -> Rule:
|
|
24
|
+
"""Get rule by name. Raises KeyError if unknown."""
|
|
25
|
+
if name not in self._by_name:
|
|
26
|
+
raise KeyError(f"Rule '{name}' not found in registry")
|
|
27
|
+
return self._by_name[name]
|
|
28
|
+
|
|
29
|
+
def names(self) -> list[str]:
|
|
30
|
+
"""Return sorted list of rule names."""
|
|
31
|
+
return sorted(self._by_name.keys())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_title_from_md(md_path: Path) -> str:
|
|
35
|
+
"""Extract first # heading from markdown file, or return filename stem."""
|
|
36
|
+
if not md_path.exists():
|
|
37
|
+
return md_path.stem
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
content = md_path.read_text()
|
|
41
|
+
lines = content.split('\n')
|
|
42
|
+
|
|
43
|
+
for line in lines:
|
|
44
|
+
line = line.strip()
|
|
45
|
+
if line.startswith('# '):
|
|
46
|
+
return line[2:].strip()
|
|
47
|
+
|
|
48
|
+
# No heading found, use filename
|
|
49
|
+
return md_path.stem
|
|
50
|
+
except Exception:
|
|
51
|
+
# Fallback to filename if we can't read the file
|
|
52
|
+
return md_path.stem
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_rule_registry(rules_dir: Optional[Path] = None) -> RuleRegistry:
|
|
56
|
+
"""Load all rules from data/rules/*.md."""
|
|
57
|
+
if rules_dir is None:
|
|
58
|
+
rules_dir = RULES_DIR
|
|
59
|
+
|
|
60
|
+
if not rules_dir.exists():
|
|
61
|
+
return RuleRegistry([])
|
|
62
|
+
|
|
63
|
+
rules = []
|
|
64
|
+
for rule_file in sorted(rules_dir.glob("*.md")):
|
|
65
|
+
title = _extract_title_from_md(rule_file)
|
|
66
|
+
|
|
67
|
+
rule = Rule(
|
|
68
|
+
name=rule_file.stem,
|
|
69
|
+
path=rule_file,
|
|
70
|
+
title=title
|
|
71
|
+
)
|
|
72
|
+
rules.append(rule)
|
|
73
|
+
|
|
74
|
+
return RuleRegistry(rules)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@lru_cache(maxsize=1)
|
|
78
|
+
def default_rule_registry() -> RuleRegistry:
|
|
79
|
+
"""Return the default rule registry, cached."""
|
|
80
|
+
return load_rule_registry()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Skill registry for loading and managing skill descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Dict, List
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from ..config import SKILLS_DIR
|
|
10
|
+
from ..domain.skill import Skill
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SkillRegistry:
|
|
14
|
+
"""Registry of skill descriptors from data/skills/*/SKILL.md."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, skills: list[Skill]):
|
|
17
|
+
self._skills = skills
|
|
18
|
+
self._by_name = {s.name: s for s in skills}
|
|
19
|
+
|
|
20
|
+
def all(self) -> list[Skill]:
|
|
21
|
+
"""Return all skills."""
|
|
22
|
+
return self._skills.copy()
|
|
23
|
+
|
|
24
|
+
def get(self, name: str) -> Skill:
|
|
25
|
+
"""Get skill by name. Raises KeyError if unknown."""
|
|
26
|
+
if name not in self._by_name:
|
|
27
|
+
raise KeyError(f"Skill '{name}' not found in registry")
|
|
28
|
+
return self._by_name[name]
|
|
29
|
+
|
|
30
|
+
def names(self) -> list[str]:
|
|
31
|
+
"""Return sorted list of skill names."""
|
|
32
|
+
return sorted(self._by_name.keys())
|
|
33
|
+
|
|
34
|
+
def by_group(self) -> Dict[str, List[Skill]]:
|
|
35
|
+
"""Return skills grouped by their group field."""
|
|
36
|
+
groups: Dict[str, List[Skill]] = {}
|
|
37
|
+
for skill in self._skills:
|
|
38
|
+
group_name = skill.group or "uncategorized"
|
|
39
|
+
if group_name not in groups:
|
|
40
|
+
groups[group_name] = []
|
|
41
|
+
groups[group_name].append(skill)
|
|
42
|
+
|
|
43
|
+
# Sort skills within each group
|
|
44
|
+
for skills_in_group in groups.values():
|
|
45
|
+
skills_in_group.sort(key=lambda s: s.name)
|
|
46
|
+
|
|
47
|
+
return groups
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_skill_frontmatter(skill_md_path: Path) -> tuple[str, Optional[str]]:
|
|
51
|
+
"""Parse SKILL.md frontmatter for description and group.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
(description, group) where description is first line if no frontmatter,
|
|
55
|
+
and group is None if not specified.
|
|
56
|
+
"""
|
|
57
|
+
if not skill_md_path.exists():
|
|
58
|
+
return skill_md_path.parent.name, None
|
|
59
|
+
|
|
60
|
+
content = skill_md_path.read_text()
|
|
61
|
+
lines = content.split('\n')
|
|
62
|
+
|
|
63
|
+
# Check for YAML frontmatter
|
|
64
|
+
if lines and lines[0].strip() == '---':
|
|
65
|
+
# Find the closing ---
|
|
66
|
+
end_idx = None
|
|
67
|
+
for i in range(1, len(lines)):
|
|
68
|
+
if lines[i].strip() == '---':
|
|
69
|
+
end_idx = i
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
if end_idx is not None:
|
|
73
|
+
# Parse the YAML frontmatter
|
|
74
|
+
frontmatter_lines = lines[1:end_idx]
|
|
75
|
+
group = None
|
|
76
|
+
description = None
|
|
77
|
+
|
|
78
|
+
for line in frontmatter_lines:
|
|
79
|
+
if ':' in line:
|
|
80
|
+
key, value = line.split(':', 1)
|
|
81
|
+
key = key.strip()
|
|
82
|
+
value = value.strip().strip('"\'')
|
|
83
|
+
if key == 'group':
|
|
84
|
+
group = value
|
|
85
|
+
elif key == 'description':
|
|
86
|
+
description = value
|
|
87
|
+
|
|
88
|
+
# If no description in frontmatter, use first non-empty line after frontmatter
|
|
89
|
+
if not description:
|
|
90
|
+
for line in lines[end_idx + 1:]:
|
|
91
|
+
line = line.strip()
|
|
92
|
+
if line and not line.startswith('#'):
|
|
93
|
+
description = line
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
return description or skill_md_path.parent.name, group
|
|
97
|
+
|
|
98
|
+
# No frontmatter - use first non-empty line as description
|
|
99
|
+
for line in lines:
|
|
100
|
+
line = line.strip()
|
|
101
|
+
if line:
|
|
102
|
+
return line, None
|
|
103
|
+
|
|
104
|
+
# Fallback to directory name
|
|
105
|
+
return skill_md_path.parent.name, None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def load_skill_registry(skills_dir: Optional[Path] = None) -> SkillRegistry:
|
|
109
|
+
"""Load all skills from data/skills/*/SKILL.md."""
|
|
110
|
+
if skills_dir is None:
|
|
111
|
+
skills_dir = SKILLS_DIR
|
|
112
|
+
|
|
113
|
+
if not skills_dir.exists():
|
|
114
|
+
return SkillRegistry([])
|
|
115
|
+
|
|
116
|
+
skills = []
|
|
117
|
+
for skill_dir in sorted(skills_dir.iterdir()):
|
|
118
|
+
if not skill_dir.is_dir():
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
skill_md = skill_dir / "SKILL.md"
|
|
122
|
+
if not skill_md.exists():
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
description, group = _parse_skill_frontmatter(skill_md)
|
|
126
|
+
|
|
127
|
+
skill = Skill(
|
|
128
|
+
name=skill_dir.name,
|
|
129
|
+
path=skill_dir,
|
|
130
|
+
description=description,
|
|
131
|
+
group=group
|
|
132
|
+
)
|
|
133
|
+
skills.append(skill)
|
|
134
|
+
|
|
135
|
+
return SkillRegistry(skills)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@lru_cache(maxsize=1)
|
|
139
|
+
def default_skill_registry() -> SkillRegistry:
|
|
140
|
+
"""Return the default skill registry, cached."""
|
|
141
|
+
return load_skill_registry()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Services — side-effecting operations called by commands."""
|
|
2
|
+
# Re-export commonly used symbols for convenience. Be conservative to avoid namespace pollution.
|
|
3
|
+
from . import fs, ui, state_store, install_state_builder, rendering, diff, diagnostics, validation
|
|
4
|
+
|
|
5
|
+
# Note: installer causes circular import with cli_backend -> registries -> config -> ui
|
|
6
|
+
# Import installer directly when needed: from agent_notes.services import installer
|
|
7
|
+
|
|
8
|
+
__all__ = ["fs", "ui", "state_store", "install_state_builder", "rendering", "diff", "diagnostics", "validation"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Diagnostic checks and fixes for agent-notes installation."""
|
|
2
|
+
|
|
3
|
+
from ._checks import (
|
|
4
|
+
check_stale_files,
|
|
5
|
+
check_broken_symlinks,
|
|
6
|
+
check_shadowed_files,
|
|
7
|
+
check_missing_files,
|
|
8
|
+
check_content_drift,
|
|
9
|
+
check_build_freshness,
|
|
10
|
+
_find_dist_source,
|
|
11
|
+
)
|
|
12
|
+
from ._display import (
|
|
13
|
+
count_stale,
|
|
14
|
+
_print_status,
|
|
15
|
+
print_summary,
|
|
16
|
+
print_issues,
|
|
17
|
+
_cli_base_dir,
|
|
18
|
+
_count_agents,
|
|
19
|
+
_count_skills,
|
|
20
|
+
_count_scripts,
|
|
21
|
+
_count_rules,
|
|
22
|
+
_check_config,
|
|
23
|
+
_check_role_models,
|
|
24
|
+
)
|
|
25
|
+
from ._fix import do_fix
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"check_stale_files",
|
|
29
|
+
"check_broken_symlinks",
|
|
30
|
+
"check_shadowed_files",
|
|
31
|
+
"check_missing_files",
|
|
32
|
+
"check_content_drift",
|
|
33
|
+
"check_build_freshness",
|
|
34
|
+
"_find_dist_source",
|
|
35
|
+
"count_stale",
|
|
36
|
+
"_print_status",
|
|
37
|
+
"print_summary",
|
|
38
|
+
"print_issues",
|
|
39
|
+
"_cli_base_dir",
|
|
40
|
+
"_count_agents",
|
|
41
|
+
"_count_skills",
|
|
42
|
+
"_count_scripts",
|
|
43
|
+
"_count_rules",
|
|
44
|
+
"_check_config",
|
|
45
|
+
"_check_role_models",
|
|
46
|
+
"do_fix",
|
|
47
|
+
]
|