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.
Files changed (162) hide show
  1. agent_notes/VERSION +1 -0
  2. agent_notes/__init__.py +1 -0
  3. agent_notes/__main__.py +4 -0
  4. agent_notes/cli.py +348 -0
  5. agent_notes/commands/__init__.py +27 -0
  6. agent_notes/commands/_install_helpers.py +262 -0
  7. agent_notes/commands/build.py +170 -0
  8. agent_notes/commands/doctor.py +112 -0
  9. agent_notes/commands/info.py +95 -0
  10. agent_notes/commands/install.py +99 -0
  11. agent_notes/commands/list.py +169 -0
  12. agent_notes/commands/memory.py +430 -0
  13. agent_notes/commands/regenerate.py +152 -0
  14. agent_notes/commands/set_role.py +143 -0
  15. agent_notes/commands/uninstall.py +26 -0
  16. agent_notes/commands/update.py +169 -0
  17. agent_notes/commands/validate.py +199 -0
  18. agent_notes/commands/wizard.py +720 -0
  19. agent_notes/config.py +154 -0
  20. agent_notes/data/agents/agents.yaml +352 -0
  21. agent_notes/data/agents/analyst.md +45 -0
  22. agent_notes/data/agents/api-reviewer.md +47 -0
  23. agent_notes/data/agents/architect.md +46 -0
  24. agent_notes/data/agents/coder.md +28 -0
  25. agent_notes/data/agents/database-specialist.md +45 -0
  26. agent_notes/data/agents/debugger.md +47 -0
  27. agent_notes/data/agents/devil.md +47 -0
  28. agent_notes/data/agents/devops.md +38 -0
  29. agent_notes/data/agents/explorer.md +23 -0
  30. agent_notes/data/agents/integrations.md +44 -0
  31. agent_notes/data/agents/lead.md +216 -0
  32. agent_notes/data/agents/performance-profiler.md +44 -0
  33. agent_notes/data/agents/refactorer.md +48 -0
  34. agent_notes/data/agents/reviewer.md +44 -0
  35. agent_notes/data/agents/security-auditor.md +44 -0
  36. agent_notes/data/agents/system-auditor.md +38 -0
  37. agent_notes/data/agents/tech-writer.md +32 -0
  38. agent_notes/data/agents/test-runner.md +36 -0
  39. agent_notes/data/agents/test-writer.md +39 -0
  40. agent_notes/data/cli/claude.yaml +25 -0
  41. agent_notes/data/cli/copilot.yaml +18 -0
  42. agent_notes/data/cli/opencode.yaml +22 -0
  43. agent_notes/data/commands/brainstorm.md +8 -0
  44. agent_notes/data/commands/debug.md +9 -0
  45. agent_notes/data/commands/review.md +10 -0
  46. agent_notes/data/global-claude.md +290 -0
  47. agent_notes/data/global-copilot.md +27 -0
  48. agent_notes/data/global-opencode.md +40 -0
  49. agent_notes/data/hooks/session-context.md.tpl +19 -0
  50. agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
  51. agent_notes/data/models/claude-opus-4-1.yaml +16 -0
  52. agent_notes/data/models/claude-opus-4-5.yaml +16 -0
  53. agent_notes/data/models/claude-opus-4-6.yaml +16 -0
  54. agent_notes/data/models/claude-opus-4-7.yaml +15 -0
  55. agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
  56. agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
  57. agent_notes/data/models/claude-sonnet-4.yaml +16 -0
  58. agent_notes/data/pricing.yaml +33 -0
  59. agent_notes/data/roles/orchestrator.yaml +5 -0
  60. agent_notes/data/roles/reasoner.yaml +5 -0
  61. agent_notes/data/roles/scout.yaml +5 -0
  62. agent_notes/data/roles/worker.yaml +5 -0
  63. agent_notes/data/rules/code-quality.md +9 -0
  64. agent_notes/data/rules/safety.md +10 -0
  65. agent_notes/data/scripts/cost-report +211 -0
  66. agent_notes/data/skills/brainstorming/SKILL.md +57 -0
  67. agent_notes/data/skills/code-review/SKILL.md +64 -0
  68. agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
  69. agent_notes/data/skills/docker-compose/SKILL.md +318 -0
  70. agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
  71. agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
  72. agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
  73. agent_notes/data/skills/git/SKILL.md +87 -0
  74. agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
  75. agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
  76. agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
  77. agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
  78. agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
  79. agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
  80. agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
  81. agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
  82. agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
  83. agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
  84. agent_notes/data/skills/rails-lib/SKILL.md +101 -0
  85. agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
  86. agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
  87. agent_notes/data/skills/rails-models/SKILL.md +459 -0
  88. agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
  89. agent_notes/data/skills/rails-routes/SKILL.md +804 -0
  90. agent_notes/data/skills/rails-style/SKILL.md +538 -0
  91. agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
  92. agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
  93. agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
  94. agent_notes/data/skills/rails-validations/SKILL.md +108 -0
  95. agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
  96. agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
  97. agent_notes/data/skills/rails-views/SKILL.md +413 -0
  98. agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
  99. agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
  100. agent_notes/data/skills/tdd/SKILL.md +57 -0
  101. agent_notes/data/templates/__init__.py +1 -0
  102. agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
  103. agent_notes/data/templates/frontmatter/__init__.py +1 -0
  104. agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
  105. agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
  106. agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
  107. agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
  108. agent_notes/data/templates/frontmatter/claude.py +44 -0
  109. agent_notes/data/templates/frontmatter/opencode.py +104 -0
  110. agent_notes/doctor_checks.py +189 -0
  111. agent_notes/domain/__init__.py +17 -0
  112. agent_notes/domain/agent.py +34 -0
  113. agent_notes/domain/cli_backend.py +40 -0
  114. agent_notes/domain/diagnostics.py +29 -0
  115. agent_notes/domain/diff.py +44 -0
  116. agent_notes/domain/model.py +27 -0
  117. agent_notes/domain/role.py +13 -0
  118. agent_notes/domain/rule.py +13 -0
  119. agent_notes/domain/skill.py +15 -0
  120. agent_notes/domain/state.py +46 -0
  121. agent_notes/install_state.py +11 -0
  122. agent_notes/registries/__init__.py +16 -0
  123. agent_notes/registries/_base.py +46 -0
  124. agent_notes/registries/agent_registry.py +107 -0
  125. agent_notes/registries/cli_registry.py +89 -0
  126. agent_notes/registries/model_registry.py +85 -0
  127. agent_notes/registries/role_registry.py +64 -0
  128. agent_notes/registries/rule_registry.py +80 -0
  129. agent_notes/registries/skill_registry.py +141 -0
  130. agent_notes/services/__init__.py +8 -0
  131. agent_notes/services/diagnostics/__init__.py +47 -0
  132. agent_notes/services/diagnostics/_checks.py +272 -0
  133. agent_notes/services/diagnostics/_display.py +346 -0
  134. agent_notes/services/diagnostics/_fix.py +169 -0
  135. agent_notes/services/diff.py +349 -0
  136. agent_notes/services/fs.py +195 -0
  137. agent_notes/services/install_state_builder.py +210 -0
  138. agent_notes/services/installer.py +293 -0
  139. agent_notes/services/memory_backend.py +155 -0
  140. agent_notes/services/rendering.py +329 -0
  141. agent_notes/services/session_context.py +23 -0
  142. agent_notes/services/settings_writer.py +79 -0
  143. agent_notes/services/state_store.py +249 -0
  144. agent_notes/services/ui.py +419 -0
  145. agent_notes/services/user_config.py +62 -0
  146. agent_notes/services/validation.py +67 -0
  147. agent_notes/state.py +21 -0
  148. agent_notes-2.0.4.dist-info/METADATA +14 -0
  149. agent_notes-2.0.4.dist-info/RECORD +162 -0
  150. agent_notes-2.0.4.dist-info/WHEEL +5 -0
  151. agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
  152. agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
  153. agent_notes-2.0.4.dist-info/top_level.txt +2 -0
  154. tests/conftest.py +20 -0
  155. tests/functional/__init__.py +0 -0
  156. tests/functional/test_build_commands.py +88 -0
  157. tests/functional/test_registries.py +128 -0
  158. tests/integration/__init__.py +0 -0
  159. tests/integration/test_build_output.py +129 -0
  160. tests/plugins/__init__.py +0 -0
  161. tests/plugins/test_agents.py +93 -0
  162. 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
+ ]