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,272 @@
|
|
|
1
|
+
"""Diagnostic check functions for agent-notes installation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from ...domain.diagnostics import Issue, FixAction
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_stale_files(scope: str, issues: List[Issue], fix_actions: List[FixAction]):
|
|
11
|
+
"""Check for installed files without matching source - DELEGATED to doctor_checks."""
|
|
12
|
+
# This function is kept for backwards compatibility but delegates to the new module
|
|
13
|
+
from ...cli_backend import load_registry
|
|
14
|
+
from ... import install_state, doctor_checks
|
|
15
|
+
from ...state import get_scope
|
|
16
|
+
|
|
17
|
+
registry = load_registry()
|
|
18
|
+
state = install_state.load_current_state()
|
|
19
|
+
if state is None:
|
|
20
|
+
scope_state = None
|
|
21
|
+
else:
|
|
22
|
+
project_path = Path.cwd() if scope == "local" else None
|
|
23
|
+
scope_state = get_scope(state, scope, project_path)
|
|
24
|
+
doctor_checks.check_stale(scope, scope_state, registry, issues, fix_actions)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _find_dist_source(symlink: Path, scope: str) -> Optional[Path]:
|
|
28
|
+
"""Map an installed path back to its dist source for relinking.
|
|
29
|
+
|
|
30
|
+
Iterates all registered backends; returns first dist source whose
|
|
31
|
+
component and filename match the given symlink.
|
|
32
|
+
"""
|
|
33
|
+
from ...cli_backend import load_registry
|
|
34
|
+
from ... import installer
|
|
35
|
+
registry = load_registry()
|
|
36
|
+
|
|
37
|
+
symlink = symlink.resolve() if symlink.exists() else Path(os.path.abspath(symlink))
|
|
38
|
+
name = symlink.name
|
|
39
|
+
parent_name = symlink.parent.name # e.g. "agents", "skills", "rules"
|
|
40
|
+
|
|
41
|
+
# Try each backend's dist source for this component
|
|
42
|
+
for backend in registry.all():
|
|
43
|
+
src_dir = installer.dist_source_for(backend, parent_name)
|
|
44
|
+
if src_dir is None:
|
|
45
|
+
continue
|
|
46
|
+
candidate = src_dir / name
|
|
47
|
+
if candidate.exists():
|
|
48
|
+
return candidate
|
|
49
|
+
|
|
50
|
+
# Config files (global only): check each backend's config_filename
|
|
51
|
+
if scope == "global":
|
|
52
|
+
for backend in registry.all():
|
|
53
|
+
config_fn = installer.config_filename_for(backend)
|
|
54
|
+
if config_fn == name:
|
|
55
|
+
src = installer.dist_source_for(backend, "config")
|
|
56
|
+
if src is not None:
|
|
57
|
+
candidate = src / config_fn
|
|
58
|
+
if candidate.exists():
|
|
59
|
+
return candidate
|
|
60
|
+
|
|
61
|
+
# Scripts
|
|
62
|
+
def _get_bin_home():
|
|
63
|
+
from ...config import BIN_HOME
|
|
64
|
+
return BIN_HOME
|
|
65
|
+
|
|
66
|
+
def _get_dist_scripts_dir():
|
|
67
|
+
from ...config import DIST_SCRIPTS_DIR
|
|
68
|
+
return DIST_SCRIPTS_DIR
|
|
69
|
+
|
|
70
|
+
bin_home = _get_bin_home()
|
|
71
|
+
dist_scripts_dir = _get_dist_scripts_dir()
|
|
72
|
+
|
|
73
|
+
if bin_home and str(symlink).startswith(str(bin_home)):
|
|
74
|
+
if dist_scripts_dir:
|
|
75
|
+
source = dist_scripts_dir / name
|
|
76
|
+
if source.exists():
|
|
77
|
+
return source
|
|
78
|
+
|
|
79
|
+
# Universal skills
|
|
80
|
+
def _get_dist_skills_dir():
|
|
81
|
+
from ...config import DIST_SKILLS_DIR
|
|
82
|
+
return DIST_SKILLS_DIR
|
|
83
|
+
|
|
84
|
+
if parent_name == "skills":
|
|
85
|
+
dist_skills_dir = _get_dist_skills_dir()
|
|
86
|
+
if dist_skills_dir:
|
|
87
|
+
source = dist_skills_dir / name
|
|
88
|
+
if source.exists():
|
|
89
|
+
return source
|
|
90
|
+
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def check_broken_symlinks(scope: str, issues: List[Issue], fix_actions: List[FixAction]):
|
|
95
|
+
"""Check for symlinks with non-existent targets - DELEGATED to doctor_checks."""
|
|
96
|
+
# This function is kept for backwards compatibility but delegates to the new module
|
|
97
|
+
from ...cli_backend import load_registry
|
|
98
|
+
from ... import install_state, doctor_checks
|
|
99
|
+
from ...state import get_scope
|
|
100
|
+
|
|
101
|
+
registry = load_registry()
|
|
102
|
+
state = install_state.load_current_state()
|
|
103
|
+
if state is None:
|
|
104
|
+
scope_state = None
|
|
105
|
+
else:
|
|
106
|
+
project_path = Path.cwd() if scope == "local" else None
|
|
107
|
+
scope_state = get_scope(state, scope, project_path)
|
|
108
|
+
doctor_checks.check_broken(scope, registry, issues, fix_actions, scope_state)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def check_shadowed_files(scope: str, issues: List[Issue], fix_actions: List[FixAction]):
|
|
112
|
+
"""Check for regular files where symlinks are expected - TARGETED check only."""
|
|
113
|
+
from ...cli_backend import load_registry
|
|
114
|
+
from ... import install_state, doctor_checks
|
|
115
|
+
from ...state import get_scope
|
|
116
|
+
|
|
117
|
+
# Get expected paths and check each one individually
|
|
118
|
+
registry = load_registry()
|
|
119
|
+
state = install_state.load_current_state()
|
|
120
|
+
if state is None:
|
|
121
|
+
scope_state = None
|
|
122
|
+
else:
|
|
123
|
+
project_path = Path.cwd() if scope == "local" else None
|
|
124
|
+
scope_state = get_scope(state, scope, project_path)
|
|
125
|
+
|
|
126
|
+
# Only check paths we know should exist
|
|
127
|
+
for src, dst, backend_name, component in doctor_checks.expected_paths_for_install(registry, scope):
|
|
128
|
+
if dst.exists() and not dst.is_symlink():
|
|
129
|
+
# This is a regular file where we expected a symlink (or copy in copy mode)
|
|
130
|
+
# If we're in symlink mode, this is shadowed
|
|
131
|
+
if scope_state is None or scope_state.mode == "symlink":
|
|
132
|
+
issues.append(Issue("shadowed", str(dst),
|
|
133
|
+
"Regular file instead of symlink. Won't receive updates."))
|
|
134
|
+
fix_actions.append(FixAction("RELINK", str(dst),
|
|
135
|
+
f"replace copy with symlink to {src}"))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def check_missing_files(scope: str, issues: List[Issue], fix_actions: List[FixAction]):
|
|
139
|
+
"""Check for source files that aren't installed - DELEGATED to doctor_checks."""
|
|
140
|
+
# This function is kept for backwards compatibility but delegates to the new module
|
|
141
|
+
from ...cli_backend import load_registry
|
|
142
|
+
from ... import doctor_checks, install_state
|
|
143
|
+
from ...state import get_scope
|
|
144
|
+
|
|
145
|
+
registry = load_registry()
|
|
146
|
+
# Pass scope state so opted-out backends aren't flagged as "missing".
|
|
147
|
+
state = install_state.load_current_state()
|
|
148
|
+
scope_state = None
|
|
149
|
+
if state is not None:
|
|
150
|
+
try:
|
|
151
|
+
project_path = Path.cwd().resolve() if scope == "local" else None
|
|
152
|
+
scope_state = get_scope(state, scope, project_path)
|
|
153
|
+
except (ValueError, KeyError):
|
|
154
|
+
scope_state = None
|
|
155
|
+
# Call via kwargs only when we actually have scope_state — tests that
|
|
156
|
+
# replace doctor_checks.check_missing with a narrower signature still work.
|
|
157
|
+
if scope_state is not None:
|
|
158
|
+
doctor_checks.check_missing(scope, registry, issues, fix_actions, scope_state=scope_state)
|
|
159
|
+
else:
|
|
160
|
+
doctor_checks.check_missing(scope, registry, issues, fix_actions)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def check_content_drift(scope: str, issues: List[Issue], fix_actions: List[FixAction]):
|
|
164
|
+
"""Check for copied files that differ from source - DELEGATED to doctor_checks."""
|
|
165
|
+
# This function is kept for backwards compatibility but delegates to the new module
|
|
166
|
+
from ...cli_backend import load_registry
|
|
167
|
+
from ... import install_state, doctor_checks
|
|
168
|
+
from ...state import get_scope
|
|
169
|
+
|
|
170
|
+
registry = load_registry()
|
|
171
|
+
state = install_state.load_current_state()
|
|
172
|
+
if state is None:
|
|
173
|
+
scope_state = None
|
|
174
|
+
else:
|
|
175
|
+
project_path = Path.cwd() if scope == "local" else None
|
|
176
|
+
scope_state = get_scope(state, scope, project_path)
|
|
177
|
+
doctor_checks.check_drift(scope, registry, issues, fix_actions, scope_state)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def check_build_freshness(issues: List[Issue], fix_actions: List[FixAction]):
|
|
181
|
+
"""Check if source files are newer than generated files."""
|
|
182
|
+
from ...config import AGENTS_YAML, AGENTS_DIR, SCRIPTS_DIR, DIST_SCRIPTS_DIR
|
|
183
|
+
agents_yaml = AGENTS_YAML
|
|
184
|
+
|
|
185
|
+
# Check agents.yaml vs generated agents
|
|
186
|
+
if agents_yaml.exists():
|
|
187
|
+
source_time = agents_yaml.stat().st_mtime
|
|
188
|
+
from ...cli_backend import load_registry
|
|
189
|
+
from ...config import dist_dir_for
|
|
190
|
+
|
|
191
|
+
registry = load_registry()
|
|
192
|
+
for backend in registry.with_feature("agents"):
|
|
193
|
+
agents_dir = dist_dir_for(backend) / backend.layout.get("agents", "agents")
|
|
194
|
+
if agents_dir.exists():
|
|
195
|
+
for f in agents_dir.glob("*.md"):
|
|
196
|
+
gen_time = f.stat().st_mtime
|
|
197
|
+
if source_time > gen_time:
|
|
198
|
+
issues.append(Issue("build_stale", str(f), "agents.yaml is newer than generated files"))
|
|
199
|
+
fix_actions.append(FixAction("BUILD", f"agents-{backend.name}/", "regenerate from source"))
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
# Check individual source agents
|
|
203
|
+
source_agents_dir = AGENTS_DIR
|
|
204
|
+
if source_agents_dir.exists():
|
|
205
|
+
from ...cli_backend import load_registry
|
|
206
|
+
from ...config import dist_dir_for
|
|
207
|
+
|
|
208
|
+
registry = load_registry()
|
|
209
|
+
for src_file in source_agents_dir.glob("*.md"):
|
|
210
|
+
source_time = src_file.stat().st_mtime
|
|
211
|
+
|
|
212
|
+
# Check corresponding generated files across all backends with agents
|
|
213
|
+
for backend in registry.with_feature("agents"):
|
|
214
|
+
gen_file = dist_dir_for(backend) / backend.layout.get("agents", "agents") / src_file.name
|
|
215
|
+
if gen_file.exists():
|
|
216
|
+
gen_time = gen_file.stat().st_mtime
|
|
217
|
+
if source_time > gen_time:
|
|
218
|
+
issues.append(Issue("build_stale", str(gen_file),
|
|
219
|
+
f"{src_file} is newer than generated file"))
|
|
220
|
+
fix_actions.append(FixAction("BUILD", str(gen_file), "regenerate from source"))
|
|
221
|
+
|
|
222
|
+
# Check scripts source vs dist
|
|
223
|
+
if SCRIPTS_DIR.exists() and DIST_SCRIPTS_DIR.exists():
|
|
224
|
+
for src_file in SCRIPTS_DIR.iterdir():
|
|
225
|
+
if src_file.is_file():
|
|
226
|
+
dist_file = DIST_SCRIPTS_DIR / src_file.name
|
|
227
|
+
if dist_file.exists() and src_file.stat().st_mtime > dist_file.stat().st_mtime:
|
|
228
|
+
issues.append(Issue("build_stale", str(dist_file),
|
|
229
|
+
f"{src_file} is newer than generated file"))
|
|
230
|
+
fix_actions.append(FixAction("BUILD", str(dist_file), "regenerate from source"))
|
|
231
|
+
|
|
232
|
+
# Check global source files
|
|
233
|
+
from ...cli_backend import load_registry
|
|
234
|
+
from ...config import global_template_path, global_output_path
|
|
235
|
+
|
|
236
|
+
registry = load_registry()
|
|
237
|
+
for backend in registry.all():
|
|
238
|
+
src = global_template_path(backend)
|
|
239
|
+
gen = global_output_path(backend)
|
|
240
|
+
|
|
241
|
+
if src and gen and src.exists() and gen.exists():
|
|
242
|
+
src_time = src.stat().st_mtime
|
|
243
|
+
gen_time = gen.stat().st_mtime
|
|
244
|
+
|
|
245
|
+
if src_time > gen_time:
|
|
246
|
+
issues.append(Issue("build_stale", str(gen), f"{src} is newer than generated file"))
|
|
247
|
+
fix_actions.append(FixAction("BUILD", str(gen), "regenerate from source"))
|
|
248
|
+
|
|
249
|
+
# Check plugin agents are up-to-date with dist agents
|
|
250
|
+
from ...config import ROOT, DIST_DIR
|
|
251
|
+
plugin_agents_dir = ROOT / ".claude-plugin" / "agents"
|
|
252
|
+
dist_claude_agents = DIST_DIR / "claude" / "agents"
|
|
253
|
+
if plugin_agents_dir.exists() and dist_claude_agents.exists():
|
|
254
|
+
for dist_file in dist_claude_agents.glob("*.md"):
|
|
255
|
+
plugin_file = plugin_agents_dir / dist_file.name
|
|
256
|
+
if plugin_file.exists() and dist_file.stat().st_mtime > plugin_file.stat().st_mtime:
|
|
257
|
+
issues.append(Issue("build_stale", str(plugin_file),
|
|
258
|
+
"dist agent is newer than plugin agent — run scripts/build-plugin.sh"))
|
|
259
|
+
fix_actions.append(FixAction("BUILD", ".claude-plugin/agents/", "run scripts/build-plugin.sh"))
|
|
260
|
+
break # one warning is enough
|
|
261
|
+
|
|
262
|
+
# Check user config freshness against dist agents
|
|
263
|
+
from ...services.user_config import config_path
|
|
264
|
+
user_cfg = config_path()
|
|
265
|
+
if user_cfg.exists() and dist_claude_agents.exists():
|
|
266
|
+
cfg_time = user_cfg.stat().st_mtime
|
|
267
|
+
for dist_file in dist_claude_agents.glob("*.md"):
|
|
268
|
+
if cfg_time > dist_file.stat().st_mtime:
|
|
269
|
+
issues.append(Issue("build_stale", str(user_cfg),
|
|
270
|
+
"user config is newer than dist agents — run: agent-notes build"))
|
|
271
|
+
fix_actions.append(FixAction("BUILD", "dist/", "regenerate from source"))
|
|
272
|
+
break
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Display and summary functions for agent-notes diagnostics."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Dict
|
|
5
|
+
|
|
6
|
+
from ...domain.diagnostics import Issue
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _cli_base_dir(backend, scope: str) -> Path:
|
|
10
|
+
"""Get base directory for a CLI backend."""
|
|
11
|
+
if scope == "global":
|
|
12
|
+
return backend.global_home
|
|
13
|
+
else:
|
|
14
|
+
return Path(backend.local_dir)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _count_agents(backend, scope: str) -> tuple:
|
|
18
|
+
"""Count (installed, expected) agents for a CLI backend."""
|
|
19
|
+
from ... import installer
|
|
20
|
+
|
|
21
|
+
if not backend.supports("agents"):
|
|
22
|
+
return 0, 0
|
|
23
|
+
|
|
24
|
+
# Count installed
|
|
25
|
+
agents_dir = installer.target_dir_for(backend, "agents", scope)
|
|
26
|
+
installed = len(list(agents_dir.glob("*.md"))) if agents_dir and agents_dir.exists() else 0
|
|
27
|
+
|
|
28
|
+
# Count expected
|
|
29
|
+
src_dir = installer.dist_source_for(backend, "agents")
|
|
30
|
+
expected = len(list(src_dir.glob("*.md"))) if src_dir and src_dir.exists() else 0
|
|
31
|
+
|
|
32
|
+
return installed, expected
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _count_skills(backend, scope: str) -> tuple:
|
|
36
|
+
"""Count (installed, expected) skills for a CLI backend. Excludes broken symlinks."""
|
|
37
|
+
from ... import installer
|
|
38
|
+
|
|
39
|
+
# Helper to get DIST_SKILLS_DIR
|
|
40
|
+
def _get_dist_skills_dir():
|
|
41
|
+
from ...config import DIST_SKILLS_DIR
|
|
42
|
+
return DIST_SKILLS_DIR
|
|
43
|
+
|
|
44
|
+
if not backend.supports("skills"):
|
|
45
|
+
return 0, 0
|
|
46
|
+
|
|
47
|
+
# Count installed
|
|
48
|
+
skills_dir = installer.target_dir_for(backend, "skills", scope)
|
|
49
|
+
if skills_dir and skills_dir.exists():
|
|
50
|
+
installed = len([d for d in skills_dir.iterdir() if d.is_dir() and d.exists()])
|
|
51
|
+
else:
|
|
52
|
+
installed = 0
|
|
53
|
+
|
|
54
|
+
# Count expected (universal skills)
|
|
55
|
+
dist_skills_dir = _get_dist_skills_dir()
|
|
56
|
+
expected = len([d for d in dist_skills_dir.iterdir() if d.is_dir()]) if dist_skills_dir and dist_skills_dir.exists() else 0
|
|
57
|
+
return installed, expected
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _count_scripts() -> tuple:
|
|
61
|
+
"""Count (installed, expected) scripts in ~/.local/bin/."""
|
|
62
|
+
# Helper functions to get config values
|
|
63
|
+
def _get_bin_home():
|
|
64
|
+
from ...config import BIN_HOME
|
|
65
|
+
return BIN_HOME
|
|
66
|
+
|
|
67
|
+
def _get_dist_scripts_dir():
|
|
68
|
+
from ...config import DIST_SCRIPTS_DIR
|
|
69
|
+
return DIST_SCRIPTS_DIR
|
|
70
|
+
|
|
71
|
+
bin_home = _get_bin_home()
|
|
72
|
+
dist_scripts_dir = _get_dist_scripts_dir()
|
|
73
|
+
|
|
74
|
+
installed = len([f for f in bin_home.iterdir() if f.is_file() and (dist_scripts_dir / f.name).exists()]) if bin_home and bin_home.exists() else 0
|
|
75
|
+
expected = len([f for f in dist_scripts_dir.iterdir() if f.is_file()]) if dist_scripts_dir and dist_scripts_dir.exists() else 0
|
|
76
|
+
return installed, expected
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _count_rules(backend, scope: str) -> tuple:
|
|
80
|
+
"""Count (installed, expected) rules for a CLI backend."""
|
|
81
|
+
from ... import installer
|
|
82
|
+
|
|
83
|
+
# Helper to get DIST_RULES_DIR
|
|
84
|
+
def _get_dist_rules_dir():
|
|
85
|
+
from ...config import DIST_RULES_DIR
|
|
86
|
+
return DIST_RULES_DIR
|
|
87
|
+
|
|
88
|
+
if not backend.supports("rules"):
|
|
89
|
+
return 0, 0
|
|
90
|
+
|
|
91
|
+
# Count installed
|
|
92
|
+
rules_dir = installer.target_dir_for(backend, "rules", scope)
|
|
93
|
+
installed = len(list(rules_dir.glob("*.md"))) if rules_dir and rules_dir.exists() else 0
|
|
94
|
+
|
|
95
|
+
# Count expected
|
|
96
|
+
dist_rules_dir = _get_dist_rules_dir()
|
|
97
|
+
expected = len(list(dist_rules_dir.glob("*.md"))) if dist_rules_dir and dist_rules_dir.exists() else 0
|
|
98
|
+
return installed, expected
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _check_config(backend, scope: str) -> tuple:
|
|
102
|
+
"""Check config files for a CLI backend. Returns (all_installed: bool, description: str, missing: list)."""
|
|
103
|
+
from ... import installer
|
|
104
|
+
|
|
105
|
+
config_file = installer.config_filename_for(backend)
|
|
106
|
+
if not config_file:
|
|
107
|
+
return True, "no config file", []
|
|
108
|
+
|
|
109
|
+
config_dir = installer.target_dir_for(backend, "config", scope)
|
|
110
|
+
if not config_dir:
|
|
111
|
+
return False, "not supported", [config_file]
|
|
112
|
+
|
|
113
|
+
config_path = config_dir / config_file
|
|
114
|
+
if config_path.exists():
|
|
115
|
+
return True, config_file, []
|
|
116
|
+
else:
|
|
117
|
+
return False, "not installed", [config_file]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _check_role_models(state):
|
|
121
|
+
"""Display role→model assignments and check compatibility."""
|
|
122
|
+
from ...model_registry import load_model_registry
|
|
123
|
+
from ...cli_backend import load_registry
|
|
124
|
+
from ...role_registry import load_role_registry
|
|
125
|
+
from ...config import Color
|
|
126
|
+
|
|
127
|
+
model_registry = load_model_registry()
|
|
128
|
+
cli_registry = load_registry()
|
|
129
|
+
role_registry = load_role_registry()
|
|
130
|
+
issues = []
|
|
131
|
+
|
|
132
|
+
print(f"\n{Color.CYAN}Role→Model Assignments:{Color.NC}\n")
|
|
133
|
+
|
|
134
|
+
# Global
|
|
135
|
+
if state.global_install:
|
|
136
|
+
print("Global:")
|
|
137
|
+
for cli_name, backend_state in state.global_install.clis.items():
|
|
138
|
+
try:
|
|
139
|
+
backend = cli_registry.get(cli_name)
|
|
140
|
+
except KeyError:
|
|
141
|
+
print(f" {cli_name} (CLI not found in registry):")
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
print(f" {backend.label}:")
|
|
145
|
+
if not backend_state.role_models:
|
|
146
|
+
print(f" {Color.YELLOW}(no role assignments){Color.NC}")
|
|
147
|
+
else:
|
|
148
|
+
for role_id, model_id in backend_state.role_models.items():
|
|
149
|
+
try:
|
|
150
|
+
role = role_registry.get(role_id)
|
|
151
|
+
model = model_registry.get(model_id)
|
|
152
|
+
|
|
153
|
+
# Check compatibility: first_alias_for returns (provider, alias) | None
|
|
154
|
+
resolved = backend.first_alias_for(model.aliases)
|
|
155
|
+
if resolved is not None:
|
|
156
|
+
_provider, alias_str = resolved
|
|
157
|
+
model_display = f"{model.label} (as {alias_str})"
|
|
158
|
+
print(f" {role.label} → {model_display}")
|
|
159
|
+
else:
|
|
160
|
+
model_display = f"{model.label} (INCOMPATIBLE - {backend.label} doesn't support this provider)"
|
|
161
|
+
print(f" {role.label} → {Color.RED}{model_display}{Color.NC}")
|
|
162
|
+
issues.append(f"Global {backend.label}: {role.label} assigned to incompatible model {model.label}")
|
|
163
|
+
except KeyError as e:
|
|
164
|
+
print(f" {role_id} → {Color.RED}{model_id} (not found: {e}){Color.NC}")
|
|
165
|
+
issues.append(f"Global {backend.label}: Invalid assignment {role_id} → {model_id}")
|
|
166
|
+
print()
|
|
167
|
+
|
|
168
|
+
# Local
|
|
169
|
+
for project_path, local_state in state.local_installs.items():
|
|
170
|
+
print(f"Local ({project_path}):")
|
|
171
|
+
for cli_name, backend_state in local_state.clis.items():
|
|
172
|
+
try:
|
|
173
|
+
backend = cli_registry.get(cli_name)
|
|
174
|
+
except KeyError:
|
|
175
|
+
print(f" {cli_name} (CLI not found in registry):")
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
print(f" {backend.label}:")
|
|
179
|
+
if not backend_state.role_models:
|
|
180
|
+
print(f" {Color.YELLOW}(no role assignments){Color.NC}")
|
|
181
|
+
else:
|
|
182
|
+
for role_id, model_id in backend_state.role_models.items():
|
|
183
|
+
try:
|
|
184
|
+
role = role_registry.get(role_id)
|
|
185
|
+
model = model_registry.get(model_id)
|
|
186
|
+
|
|
187
|
+
resolved = backend.first_alias_for(model.aliases)
|
|
188
|
+
if resolved is not None:
|
|
189
|
+
_provider, alias_str = resolved
|
|
190
|
+
model_display = f"{model.label} (as {alias_str})"
|
|
191
|
+
print(f" {role.label} → {model_display}")
|
|
192
|
+
else:
|
|
193
|
+
model_display = f"{model.label} (INCOMPATIBLE - {backend.label} doesn't support this provider)"
|
|
194
|
+
print(f" {role.label} → {Color.RED}{model_display}{Color.NC}")
|
|
195
|
+
issues.append(f"Local {project_path} {backend.label}: {role.label} assigned to incompatible model {model.label}")
|
|
196
|
+
except KeyError as e:
|
|
197
|
+
print(f" {role_id} → {Color.RED}{model_id} (not found: {e}){Color.NC}")
|
|
198
|
+
issues.append(f"Local {project_path} {backend.label}: Invalid assignment {role_id} → {model_id}")
|
|
199
|
+
print()
|
|
200
|
+
|
|
201
|
+
if issues:
|
|
202
|
+
print(f"{Color.RED}Issues found:{Color.NC}")
|
|
203
|
+
for issue in issues:
|
|
204
|
+
print(f" • {issue}")
|
|
205
|
+
print(f"\nRun '{Color.CYAN}agent-notes set role{Color.NC}' to fix assignments.")
|
|
206
|
+
else:
|
|
207
|
+
print(f"{Color.GREEN}All role assignments look good.{Color.NC}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def count_stale(issues: List[Issue], item_type: str) -> int:
|
|
211
|
+
"""Count stale issues of a specific type."""
|
|
212
|
+
count = 0
|
|
213
|
+
for issue in issues:
|
|
214
|
+
if issue.type == "stale" and item_type in issue.file:
|
|
215
|
+
count += 1
|
|
216
|
+
return count
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _print_status(label: str, installed: int, expected: int):
|
|
220
|
+
"""Print OK/WARN status for a component."""
|
|
221
|
+
from ...config import ok, warn
|
|
222
|
+
if installed == 0 and expected == 0:
|
|
223
|
+
ok(f"{label} (none available)", indent=4)
|
|
224
|
+
elif installed == 0:
|
|
225
|
+
warn(f"{label} (not installed, {expected} available)", indent=4)
|
|
226
|
+
elif installed >= expected:
|
|
227
|
+
ok(f"{label} ({installed} installed)", indent=4)
|
|
228
|
+
else:
|
|
229
|
+
missing = expected - installed
|
|
230
|
+
warn(f"{label} ({installed} installed, {missing} missing)", indent=4)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def print_summary(scope: str):
|
|
234
|
+
"""Print installation summary grouped by CLI."""
|
|
235
|
+
label = "global" if scope == "global" else "local"
|
|
236
|
+
print(f"Checking AgentNotes {label} installation:")
|
|
237
|
+
print("")
|
|
238
|
+
|
|
239
|
+
from ...cli_backend import load_registry
|
|
240
|
+
registry = load_registry()
|
|
241
|
+
for backend in registry.all():
|
|
242
|
+
if not backend.supports("agents"):
|
|
243
|
+
continue
|
|
244
|
+
cli = backend.name
|
|
245
|
+
cli_name = backend.label
|
|
246
|
+
base = _cli_base_dir(backend, scope)
|
|
247
|
+
print(f" {cli_name} ({base})")
|
|
248
|
+
|
|
249
|
+
# Agents
|
|
250
|
+
installed, expected = _count_agents(backend, scope)
|
|
251
|
+
_print_status("agents", installed, expected)
|
|
252
|
+
|
|
253
|
+
# Skills
|
|
254
|
+
installed, expected = _count_skills(backend, scope)
|
|
255
|
+
_print_status("skills", installed, expected)
|
|
256
|
+
|
|
257
|
+
# Config
|
|
258
|
+
from ...config import ok, warn
|
|
259
|
+
all_ok, desc, missing = _check_config(backend, scope)
|
|
260
|
+
if all_ok:
|
|
261
|
+
ok(f"config ({desc})", indent=4)
|
|
262
|
+
elif desc == "not installed":
|
|
263
|
+
warn("config (not installed)", indent=4)
|
|
264
|
+
else:
|
|
265
|
+
missing_str = ", ".join(missing)
|
|
266
|
+
warn(f"config ({desc}) — missing: {missing_str}", indent=4)
|
|
267
|
+
|
|
268
|
+
# Rules (only for backends that support rules)
|
|
269
|
+
if backend.supports("rules"):
|
|
270
|
+
installed, expected = _count_rules(backend, scope)
|
|
271
|
+
_print_status("rules", installed, expected)
|
|
272
|
+
|
|
273
|
+
# Scripts (global only, not per-CLI)
|
|
274
|
+
if scope == "global":
|
|
275
|
+
from ...config import BIN_HOME
|
|
276
|
+
installed, expected = _count_scripts()
|
|
277
|
+
_print_status(f"scripts ({BIN_HOME})", installed, expected)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def print_issues(issues: List[Issue]) -> bool:
|
|
281
|
+
"""Print found issues. Returns True if no issues."""
|
|
282
|
+
from ...config import Color
|
|
283
|
+
|
|
284
|
+
if not issues:
|
|
285
|
+
print("")
|
|
286
|
+
print(f"{Color.GREEN}No issues found.{Color.NC}")
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
# Check if fully not installed
|
|
290
|
+
non_build_issues = [i for i in issues if i.type != "build_stale"]
|
|
291
|
+
if non_build_issues and all(i.type == "missing_group" for i in non_build_issues):
|
|
292
|
+
print(f"\nNot installed. Run '{Color.CYAN}agent-notes install{Color.NC}' to set up.")
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
print("")
|
|
296
|
+
|
|
297
|
+
# Group broken symlinks by directory for cleaner output
|
|
298
|
+
broken_by_dir: Dict[str, int] = {}
|
|
299
|
+
other_issues: List[Issue] = []
|
|
300
|
+
for iss in issues:
|
|
301
|
+
if iss.type == "broken":
|
|
302
|
+
parent = str(Path(iss.file).parent)
|
|
303
|
+
broken_by_dir[parent] = broken_by_dir.get(parent, 0) + 1
|
|
304
|
+
elif iss.type == "missing_group":
|
|
305
|
+
continue # Already shown in summary
|
|
306
|
+
else:
|
|
307
|
+
other_issues.append(iss)
|
|
308
|
+
|
|
309
|
+
display_count = len(broken_by_dir) + len(other_issues)
|
|
310
|
+
print(f"{Color.YELLOW}Warning: {display_count} issue(s) found{Color.NC}")
|
|
311
|
+
print("")
|
|
312
|
+
|
|
313
|
+
# Print grouped broken symlinks
|
|
314
|
+
for dir_path, count in broken_by_dir.items():
|
|
315
|
+
print(f" {Color.RED}✗ Broken symlinks: {Color.NC}{dir_path}/ ({count} broken)")
|
|
316
|
+
print(f" Fix: run '{Color.CYAN}agent-notes install{Color.NC}' to recreate")
|
|
317
|
+
print("")
|
|
318
|
+
|
|
319
|
+
# Print other issues
|
|
320
|
+
for iss in other_issues:
|
|
321
|
+
if iss.type == "stale":
|
|
322
|
+
print(f" {Color.RED}✗ Stale: {Color.NC}{iss.file}")
|
|
323
|
+
print(f" {iss.message}")
|
|
324
|
+
print(f" Fix: run '{Color.CYAN}agent-notes doctor --fix{Color.NC}' to remove")
|
|
325
|
+
elif iss.type == "shadowed":
|
|
326
|
+
print(f" {Color.YELLOW}✗ Shadowed: {Color.NC}{iss.file}")
|
|
327
|
+
print(f" {iss.message}")
|
|
328
|
+
print(f" Fix: run '{Color.CYAN}agent-notes doctor --fix{Color.NC}' to replace with symlink")
|
|
329
|
+
elif iss.type == "missing":
|
|
330
|
+
print(f" {Color.YELLOW}✗ Missing: {Color.NC}{iss.file}")
|
|
331
|
+
print(f" {iss.message}")
|
|
332
|
+
print(f" Fix: run '{Color.CYAN}agent-notes doctor --fix{Color.NC}' or '{Color.CYAN}agent-notes install{Color.NC}'")
|
|
333
|
+
elif iss.type == "drift":
|
|
334
|
+
print(f" {Color.CYAN}✗ Content drift: {Color.NC}{iss.file}")
|
|
335
|
+
print(f" {iss.message}")
|
|
336
|
+
elif iss.type == "build_stale":
|
|
337
|
+
print(f" {Color.YELLOW}✗ Build stale: {Color.NC}{iss.file}")
|
|
338
|
+
print(f" {iss.message}")
|
|
339
|
+
print(f" Fix: run '{Color.CYAN}agent-notes build{Color.NC}'")
|
|
340
|
+
else:
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
print("")
|
|
344
|
+
|
|
345
|
+
print(f"Run '{Color.CYAN}agent-notes doctor --fix{Color.NC}' to resolve these issues.")
|
|
346
|
+
return False
|