@tw93/waza 3.25.0
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.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/package.json +35 -0
- package/rules/anti-patterns.md +38 -0
- package/rules/chinese.md +18 -0
- package/rules/durable-context.md +27 -0
- package/rules/english.md +14 -0
- package/scripts/build_metadata.py +360 -0
- package/scripts/check_routing_drift.py +82 -0
- package/scripts/dispatcher-template.md +43 -0
- package/scripts/dispatcher.md +53 -0
- package/scripts/package-skill.sh +71 -0
- package/scripts/packaging_filter.py +55 -0
- package/scripts/setup-rule.sh +109 -0
- package/scripts/setup-statusline.sh +127 -0
- package/scripts/skill_checks.py +483 -0
- package/scripts/skill_frontmatter.py +110 -0
- package/scripts/statusline.sh +321 -0
- package/scripts/validate_package.py +66 -0
- package/scripts/verify_skills.py +100 -0
- package/skills/RESOLVER.md +91 -0
- package/skills/check/SKILL.md +338 -0
- package/skills/check/agents/reviewer-architecture.md +39 -0
- package/skills/check/agents/reviewer-security.md +39 -0
- package/skills/check/references/persona-catalog.md +56 -0
- package/skills/check/references/project-context.md +107 -0
- package/skills/check/references/public-reply.md +14 -0
- package/skills/check/scripts/audit_signals.py +485 -0
- package/skills/check/scripts/run-tests.sh +19 -0
- package/skills/design/SKILL.md +134 -0
- package/skills/design/references/design-aesthetic-quality.md +67 -0
- package/skills/design/references/design-data-viz.md +34 -0
- package/skills/design/references/design-reference.md +278 -0
- package/skills/design/references/design-tokens.md +53 -0
- package/skills/design/references/design-traps.md +43 -0
- package/skills/health/SKILL.md +231 -0
- package/skills/health/agents/inspector-context.md +119 -0
- package/skills/health/agents/inspector-control.md +84 -0
- package/skills/health/agents/inspector-maintainability.md +55 -0
- package/skills/health/scripts/check-agent-context.sh +5 -0
- package/skills/health/scripts/check-doc-refs.sh +8 -0
- package/skills/health/scripts/check-maintainability.sh +8 -0
- package/skills/health/scripts/check-verifier-output.sh +5 -0
- package/skills/health/scripts/check_agent_context.py +407 -0
- package/skills/health/scripts/check_doc_refs.py +110 -0
- package/skills/health/scripts/check_maintainability.py +629 -0
- package/skills/health/scripts/check_verifier_output.py +116 -0
- package/skills/health/scripts/collect-data.sh +760 -0
- package/skills/hunt/SKILL.md +197 -0
- package/skills/hunt/references/failure-patterns.md +75 -0
- package/skills/hunt/references/ime-unicode.md +58 -0
- package/skills/hunt/references/logging-techniques.md +72 -0
- package/skills/hunt/references/rendering-debug.md +34 -0
- package/skills/learn/SKILL.md +128 -0
- package/skills/read/SKILL.md +108 -0
- package/skills/read/references/read-methods.md +110 -0
- package/skills/read/references/save-paths.md +33 -0
- package/skills/read/scripts/fetch.sh +105 -0
- package/skills/read/scripts/fetch_feishu.py +246 -0
- package/skills/read/scripts/fetch_local.py +218 -0
- package/skills/read/scripts/fetch_weixin.py +107 -0
- package/skills/think/SKILL.md +155 -0
- package/skills/write/SKILL.md +129 -0
- package/skills/write/references/write-en.md +197 -0
- package/skills/write/references/write-zh-bilingual.md +60 -0
- package/skills/write/references/write-zh-prose.md +48 -0
- package/skills/write/references/write-zh-release-notes.md +38 -0
- package/skills/write/references/write-zh.md +645 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Summarize the agent-instruction surface for a project.
|
|
3
|
+
|
|
4
|
+
Inventories AGENTS.md / CLAUDE.md / Codex / Copilot / Gemini instruction files,
|
|
5
|
+
parses Codex config.toml for project trust + plugin/feature state (with sensitive
|
|
6
|
+
values redacted), and flags drift between Claude and Codex surfaces.
|
|
7
|
+
|
|
8
|
+
Run as: python3 check_agent_context.py [ROOT] [summary|deep]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
SENSITIVE_RE = re.compile(r"(api[_-]?key|token|secret|password|credential)", re.IGNORECASE)
|
|
21
|
+
PROJECT_RE = re.compile(r'^\[projects\."(.+)"\]\s*$')
|
|
22
|
+
TABLE_RE = re.compile(r'^\[([A-Za-z0-9_.@"\-/]+)\]\s*$')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def rel(path: Path, root: Path) -> str:
|
|
26
|
+
try:
|
|
27
|
+
return path.resolve().relative_to(root).as_posix()
|
|
28
|
+
except ValueError:
|
|
29
|
+
return path.as_posix()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read(path: Path, limit: int | None = None) -> str:
|
|
33
|
+
try:
|
|
34
|
+
data = path.read_text(encoding="utf-8", errors="replace")
|
|
35
|
+
except OSError:
|
|
36
|
+
return ""
|
|
37
|
+
return data[:limit] if limit else data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def yes(path: Path) -> str:
|
|
41
|
+
return "yes" if path.exists() else "no"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def print_list(title: str, items: list[str], empty: str = "(none)", limit: int | None = None) -> None:
|
|
45
|
+
print(f"{title}:")
|
|
46
|
+
shown = items if limit is None else items[:limit]
|
|
47
|
+
if not shown:
|
|
48
|
+
print(f" {empty}")
|
|
49
|
+
return
|
|
50
|
+
for item in shown:
|
|
51
|
+
print(f" {item}")
|
|
52
|
+
if limit is not None and len(items) > limit:
|
|
53
|
+
print(f" ... {len(items) - limit} more")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_json(path: Path) -> tuple[object | None, str | None]:
|
|
57
|
+
if not path.is_file():
|
|
58
|
+
return None, None
|
|
59
|
+
try:
|
|
60
|
+
return json.loads(read(path)), None
|
|
61
|
+
except json.JSONDecodeError as exc:
|
|
62
|
+
return None, f"{path.name}: invalid JSON at line {exc.lineno}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def redact_sensitive_entries(value: object, prefix: str = "") -> list[str]:
|
|
66
|
+
entries: list[str] = []
|
|
67
|
+
if isinstance(value, dict):
|
|
68
|
+
for key, child in value.items():
|
|
69
|
+
child_prefix = f"{prefix}.{key}" if prefix else str(key)
|
|
70
|
+
if SENSITIVE_RE.search(str(key)):
|
|
71
|
+
entries.append(f"{child_prefix}=[REDACTED]")
|
|
72
|
+
continue
|
|
73
|
+
entries.extend(redact_sensitive_entries(child, child_prefix))
|
|
74
|
+
elif isinstance(value, list):
|
|
75
|
+
for index, child in enumerate(value):
|
|
76
|
+
entries.extend(redact_sensitive_entries(child, f"{prefix}[{index}]"))
|
|
77
|
+
return entries
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def string_list(value: object) -> list[str]:
|
|
81
|
+
if isinstance(value, list):
|
|
82
|
+
return [str(item) if not SENSITIVE_RE.search(str(item)) else "[REDACTED]" for item in value]
|
|
83
|
+
if isinstance(value, dict):
|
|
84
|
+
return sorted(str(key) for key in value)
|
|
85
|
+
if isinstance(value, str):
|
|
86
|
+
return ["[REDACTED]" if SENSITIVE_RE.search(value) else value]
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def skill_root_count(path: Path, include_root_md: bool) -> int:
|
|
91
|
+
if not path.is_dir():
|
|
92
|
+
return 0
|
|
93
|
+
count = len(list(path.rglob("SKILL.md")))
|
|
94
|
+
if include_root_md:
|
|
95
|
+
count += len([p for p in path.glob("*.md") if p.name != "SKILL.md"])
|
|
96
|
+
return count
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def project_instruction_files(root: Path) -> list[Path]:
|
|
100
|
+
files = [
|
|
101
|
+
root / "AGENTS.md",
|
|
102
|
+
root / "CLAUDE.md",
|
|
103
|
+
root / ".github" / "copilot-instructions.md",
|
|
104
|
+
root / "GEMINI.md",
|
|
105
|
+
]
|
|
106
|
+
instructions_dir = root / ".github" / "instructions"
|
|
107
|
+
if instructions_dir.is_dir():
|
|
108
|
+
files.extend(sorted(instructions_dir.glob("*.md")))
|
|
109
|
+
return [path for path in files if path.is_file()]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def claude_delegates_to_agents(path: Path) -> bool:
|
|
113
|
+
text = read(path, 20_000)
|
|
114
|
+
if not text:
|
|
115
|
+
return False
|
|
116
|
+
meaningful = [
|
|
117
|
+
line.strip()
|
|
118
|
+
for line in text.splitlines()
|
|
119
|
+
if line.strip() and not line.strip().startswith("#")
|
|
120
|
+
]
|
|
121
|
+
return any("AGENTS.md" in line for line in meaningful)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_codex_config(
|
|
125
|
+
path: Path,
|
|
126
|
+
) -> tuple[dict[str, str], list[str], list[str], list[str], list[str]]:
|
|
127
|
+
projects: dict[str, str] = {}
|
|
128
|
+
features: list[str] = []
|
|
129
|
+
plugins: list[str] = []
|
|
130
|
+
marketplaces: list[str] = []
|
|
131
|
+
redacted: list[str] = []
|
|
132
|
+
if not path.is_file():
|
|
133
|
+
return projects, features, plugins, marketplaces, redacted
|
|
134
|
+
|
|
135
|
+
section = ""
|
|
136
|
+
for raw in read(path).splitlines():
|
|
137
|
+
line = raw.strip()
|
|
138
|
+
if not line or line.startswith("#"):
|
|
139
|
+
continue
|
|
140
|
+
project_match = PROJECT_RE.match(line)
|
|
141
|
+
if project_match:
|
|
142
|
+
section = f'projects."{project_match.group(1)}"'
|
|
143
|
+
projects.setdefault(project_match.group(1), "")
|
|
144
|
+
continue
|
|
145
|
+
table_match = TABLE_RE.match(line)
|
|
146
|
+
if table_match:
|
|
147
|
+
section = table_match.group(1)
|
|
148
|
+
marketplace_match = re.match(r'marketplaces\.([A-Za-z0-9_.@-]+)$', section)
|
|
149
|
+
plugin_match = re.match(r'plugins\."?([^"]+)"?$', section)
|
|
150
|
+
if marketplace_match:
|
|
151
|
+
marketplaces.append(marketplace_match.group(1))
|
|
152
|
+
if plugin_match:
|
|
153
|
+
plugins.append(plugin_match.group(1))
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
if SENSITIVE_RE.search(line):
|
|
157
|
+
key = line.split("=", 1)[0].strip() if "=" in line else "sensitive"
|
|
158
|
+
redacted.append(f"{key}=[REDACTED]")
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
if "=" not in line:
|
|
162
|
+
continue
|
|
163
|
+
key, value = [part.strip() for part in line.split("=", 1)]
|
|
164
|
+
if section == "features" and value.lower() == "true":
|
|
165
|
+
features.append(key)
|
|
166
|
+
elif section.startswith('projects."') and key == "trust_level":
|
|
167
|
+
project = section[len('projects."'): -1]
|
|
168
|
+
projects[project] = value.strip('"')
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
projects,
|
|
172
|
+
sorted(set(features)),
|
|
173
|
+
sorted(set(plugins)),
|
|
174
|
+
sorted(set(marketplaces)),
|
|
175
|
+
sorted(set(redacted)),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def project_trust(projects: dict[str, str], root: Path) -> str:
|
|
180
|
+
root_text = root.as_posix()
|
|
181
|
+
if root_text in projects:
|
|
182
|
+
return f"exact:{projects[root_text] or 'configured'}"
|
|
183
|
+
candidates = []
|
|
184
|
+
for project, level in projects.items():
|
|
185
|
+
try:
|
|
186
|
+
project_path = Path(project).expanduser().resolve()
|
|
187
|
+
except OSError:
|
|
188
|
+
continue
|
|
189
|
+
if project_path == root:
|
|
190
|
+
return f"exact:{level or 'configured'}"
|
|
191
|
+
try:
|
|
192
|
+
root.relative_to(project_path)
|
|
193
|
+
except ValueError:
|
|
194
|
+
continue
|
|
195
|
+
candidates.append(
|
|
196
|
+
(len(project_path.as_posix()), level or "configured", project_path.as_posix())
|
|
197
|
+
)
|
|
198
|
+
if candidates:
|
|
199
|
+
_, level, project = sorted(candidates, reverse=True)[0]
|
|
200
|
+
return f"inherited:{level} from {project}"
|
|
201
|
+
return "missing"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def summarize_pi_surface(root: Path, home: Path) -> tuple[str, list[str]]:
|
|
205
|
+
global_settings = home / ".pi" / "agent" / "settings.json"
|
|
206
|
+
project_settings = root / ".pi" / "settings.json"
|
|
207
|
+
settings_sources = [
|
|
208
|
+
("global_settings", global_settings),
|
|
209
|
+
("project_settings", project_settings),
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
configured_skills: list[str] = []
|
|
213
|
+
configured_packages: list[str] = []
|
|
214
|
+
redacted_entries: list[str] = []
|
|
215
|
+
findings: list[str] = []
|
|
216
|
+
malformed = False
|
|
217
|
+
|
|
218
|
+
for label, path in settings_sources:
|
|
219
|
+
data, error = load_json(path)
|
|
220
|
+
if error:
|
|
221
|
+
malformed = True
|
|
222
|
+
findings.append(error)
|
|
223
|
+
continue
|
|
224
|
+
if not isinstance(data, dict):
|
|
225
|
+
continue
|
|
226
|
+
configured_skills.extend(
|
|
227
|
+
f"{label}.skills: {item}" for item in string_list(data.get("skills"))
|
|
228
|
+
)
|
|
229
|
+
configured_packages.extend(
|
|
230
|
+
f"{label}.packages: {item}" for item in string_list(data.get("packages"))
|
|
231
|
+
)
|
|
232
|
+
redacted_entries.extend(
|
|
233
|
+
f"{label}.{item}" for item in redact_sensitive_entries(data)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
package_path = root / "package.json"
|
|
237
|
+
package_pi_skills: list[str] = []
|
|
238
|
+
data, error = load_json(package_path)
|
|
239
|
+
if error:
|
|
240
|
+
findings.append(error)
|
|
241
|
+
elif isinstance(data, dict):
|
|
242
|
+
pi_manifest = data.get("pi")
|
|
243
|
+
if isinstance(pi_manifest, dict):
|
|
244
|
+
package_pi_skills = string_list(pi_manifest.get("skills"))
|
|
245
|
+
|
|
246
|
+
pi_skill_dirs = [
|
|
247
|
+
("global_pi_skill_roots", home / ".pi" / "agent" / "skills", True),
|
|
248
|
+
("project_pi_skill_roots", root / ".pi" / "skills", True),
|
|
249
|
+
("global_agents_skill_roots", home / ".agents" / "skills", False),
|
|
250
|
+
("project_agents_skill_roots", root / ".agents" / "skills", False),
|
|
251
|
+
]
|
|
252
|
+
skill_counts = [
|
|
253
|
+
f"{label}: {skill_root_count(path, include_root_md)}"
|
|
254
|
+
for label, path, include_root_md in pi_skill_dirs
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
has_pi_surface = (
|
|
258
|
+
global_settings.is_file()
|
|
259
|
+
or project_settings.is_file()
|
|
260
|
+
or bool(package_pi_skills)
|
|
261
|
+
or any(not line.endswith(": 0") for line in skill_counts)
|
|
262
|
+
or bool(configured_skills)
|
|
263
|
+
or bool(configured_packages)
|
|
264
|
+
)
|
|
265
|
+
if not has_pi_surface:
|
|
266
|
+
findings.append("no Pi settings, package manifest, or skill directories found")
|
|
267
|
+
|
|
268
|
+
status = "WARN" if malformed else "PASS"
|
|
269
|
+
lines = [
|
|
270
|
+
"=== PI SURFACE ===",
|
|
271
|
+
f"pi_status: {status}",
|
|
272
|
+
f"global_settings_json: {yes(global_settings)}",
|
|
273
|
+
f"project_settings_json: {yes(project_settings)}",
|
|
274
|
+
f"package_json: {yes(package_path)}",
|
|
275
|
+
]
|
|
276
|
+
lines.extend(skill_counts)
|
|
277
|
+
lines.append("package_pi_skills:")
|
|
278
|
+
lines.extend(f" {item}" for item in (package_pi_skills or ["(none)"]))
|
|
279
|
+
lines.append("configured_skills:")
|
|
280
|
+
lines.extend(f" {item}" for item in (configured_skills or ["(none)"]))
|
|
281
|
+
lines.append("configured_packages:")
|
|
282
|
+
lines.extend(f" {item}" for item in (configured_packages or ["(none)"]))
|
|
283
|
+
lines.append("redacted_pi_entries:")
|
|
284
|
+
lines.extend(f" {item}" for item in (redacted_entries or ["(none)"]))
|
|
285
|
+
lines.append("pi_findings:")
|
|
286
|
+
lines.extend(f" {item}" for item in (findings or ["(none)"]))
|
|
287
|
+
return status, lines
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def main() -> int:
|
|
291
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
292
|
+
parser.add_argument("root", nargs="?", default=".", help="Repo root (default: cwd)")
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
"mode", nargs="?", default="summary", choices=("summary", "deep"),
|
|
295
|
+
help="Output detail level",
|
|
296
|
+
)
|
|
297
|
+
args = parser.parse_args()
|
|
298
|
+
root = Path(args.root).resolve()
|
|
299
|
+
mode = args.mode
|
|
300
|
+
home = Path(os.environ.get("HOME", str(Path.home()))).expanduser()
|
|
301
|
+
|
|
302
|
+
if not root.is_dir():
|
|
303
|
+
print(f"Repo root not found: {root}", file=sys.stderr)
|
|
304
|
+
return 2
|
|
305
|
+
|
|
306
|
+
instruction_files = project_instruction_files(root)
|
|
307
|
+
agents = root / "AGENTS.md"
|
|
308
|
+
claude = root / "CLAUDE.md"
|
|
309
|
+
claude_delegates = claude_delegates_to_agents(claude)
|
|
310
|
+
github_instructions_dir = root / ".github" / "instructions"
|
|
311
|
+
github_instruction_count = (
|
|
312
|
+
len(list(github_instructions_dir.glob("*.md"))) if github_instructions_dir.is_dir() else 0
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
instruction_findings: list[str] = []
|
|
316
|
+
if not instruction_files:
|
|
317
|
+
instruction_findings.append("no project agent instruction files")
|
|
318
|
+
if agents.is_file() and claude.is_file() and not claude_delegates:
|
|
319
|
+
claude_lines = len(read(claude).splitlines())
|
|
320
|
+
agents_lines = len(read(agents).splitlines())
|
|
321
|
+
if claude_lines > 20 and agents_lines > 20:
|
|
322
|
+
instruction_findings.append(
|
|
323
|
+
"AGENTS.md and CLAUDE.md both contain substantial guidance without delegation"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
global_codex_agents = home / ".codex" / "AGENTS.md"
|
|
327
|
+
codex_config = home / ".codex" / "config.toml"
|
|
328
|
+
projects, features, plugins, marketplaces, redacted = parse_codex_config(codex_config)
|
|
329
|
+
trust = project_trust(projects, root) if codex_config.is_file() else "unavailable"
|
|
330
|
+
codex_findings: list[str] = []
|
|
331
|
+
if not global_codex_agents.is_file() and not codex_config.is_file():
|
|
332
|
+
codex_findings.append("Codex surface not found")
|
|
333
|
+
elif codex_config.is_file() and trust == "missing":
|
|
334
|
+
codex_findings.append("current project is not configured in Codex trust table")
|
|
335
|
+
|
|
336
|
+
global_claude = home / ".claude" / "CLAUDE.md"
|
|
337
|
+
project_settings = root / ".claude" / "settings.local.json"
|
|
338
|
+
project_rules = root / ".claude" / "rules"
|
|
339
|
+
project_skills = root / ".claude" / "skills"
|
|
340
|
+
global_skills = home / ".claude" / "skills"
|
|
341
|
+
claude_findings: list[str] = []
|
|
342
|
+
if claude.is_file() and claude_delegates:
|
|
343
|
+
claude_findings.append("CLAUDE.md delegates to AGENTS.md")
|
|
344
|
+
if not global_claude.is_file() and not claude.is_file():
|
|
345
|
+
claude_findings.append("Claude instruction surface not found")
|
|
346
|
+
|
|
347
|
+
conflict_findings: list[str] = []
|
|
348
|
+
if agents.is_file() and claude.is_file() and not claude_delegates:
|
|
349
|
+
conflict_findings.append("AGENTS.md and CLAUDE.md both exist; verify they do not diverge")
|
|
350
|
+
|
|
351
|
+
instruction_status = "FAIL" if not instruction_files else ("WARN" if instruction_findings else "PASS")
|
|
352
|
+
codex_status = "WARN" if codex_findings else "PASS"
|
|
353
|
+
claude_status = (
|
|
354
|
+
"WARN"
|
|
355
|
+
if claude_findings and "surface not found" in " ".join(claude_findings)
|
|
356
|
+
else "PASS"
|
|
357
|
+
)
|
|
358
|
+
conflict_status = "WARN" if conflict_findings else "PASS"
|
|
359
|
+
|
|
360
|
+
print("=== AGENT INSTRUCTION SURFACE ===")
|
|
361
|
+
print(f"agent_instruction_status: {instruction_status}")
|
|
362
|
+
print(f"mode: {mode}")
|
|
363
|
+
print(f"AGENTS.md: {yes(agents)}")
|
|
364
|
+
print(f"CLAUDE.md: {yes(claude)}")
|
|
365
|
+
print(f"claude_delegates_to_agents: {'yes' if claude_delegates else 'no'}")
|
|
366
|
+
print(f".github/copilot-instructions.md: {yes(root / '.github' / 'copilot-instructions.md')}")
|
|
367
|
+
print(f".github/instructions/*.md: {github_instruction_count}")
|
|
368
|
+
print(f"GEMINI.md: {yes(root / 'GEMINI.md')}")
|
|
369
|
+
print_list("instruction_files", [rel(path, root) for path in instruction_files])
|
|
370
|
+
print_list("instruction_findings", instruction_findings)
|
|
371
|
+
|
|
372
|
+
print("=== CODEX SURFACE ===")
|
|
373
|
+
print(f"codex_status: {codex_status}")
|
|
374
|
+
print(f"global_agents_md: {yes(global_codex_agents)}")
|
|
375
|
+
print(f"global_config_toml: {yes(codex_config)}")
|
|
376
|
+
print(f"project_trust: {trust}")
|
|
377
|
+
print_list("features", features, limit=20 if mode == "summary" else None)
|
|
378
|
+
print_list("enabled_plugins", plugins, limit=20 if mode == "summary" else None)
|
|
379
|
+
print_list("marketplaces", marketplaces, limit=20 if mode == "summary" else None)
|
|
380
|
+
print_list("redacted_config_entries", redacted)
|
|
381
|
+
print_list("codex_findings", codex_findings)
|
|
382
|
+
|
|
383
|
+
print("=== CLAUDE SURFACE ===")
|
|
384
|
+
print(f"claude_status: {claude_status}")
|
|
385
|
+
print(f"global_claude_md: {yes(global_claude)}")
|
|
386
|
+
print(f"project_claude_md: {yes(claude)}")
|
|
387
|
+
print(f"settings_local_json: {yes(project_settings)}")
|
|
388
|
+
rule_count = len(list(project_rules.glob('*.md'))) if project_rules.is_dir() else 0
|
|
389
|
+
local_skill_count = len(list(project_skills.glob('*/SKILL.md'))) if project_skills.is_dir() else 0
|
|
390
|
+
global_skill_count = len(list(global_skills.glob('*/SKILL.md'))) if global_skills.is_dir() else 0
|
|
391
|
+
print(f"project_rules: {rule_count}")
|
|
392
|
+
print(f"project_skills: {local_skill_count}")
|
|
393
|
+
print(f"global_skills: {global_skill_count}")
|
|
394
|
+
print_list("claude_findings", claude_findings)
|
|
395
|
+
|
|
396
|
+
_, pi_lines = summarize_pi_surface(root, home)
|
|
397
|
+
for line in pi_lines:
|
|
398
|
+
print(line)
|
|
399
|
+
|
|
400
|
+
print("=== INSTRUCTION CONFLICTS ===")
|
|
401
|
+
print(f"conflict_status: {conflict_status}")
|
|
402
|
+
print_list("conflict_findings", conflict_findings)
|
|
403
|
+
return 0
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
if __name__ == "__main__":
|
|
407
|
+
sys.exit(main())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check that doc references (@path, ~/.claude/..., docs/..., references/...) in
|
|
3
|
+
AGENTS.md, CLAUDE.md, .claude/rules/*.md, and .claude/skills/*/SKILL.md resolve
|
|
4
|
+
to real files. Prints `doc references: ok` on success, otherwise lists every
|
|
5
|
+
MISSING reference with source location.
|
|
6
|
+
|
|
7
|
+
Run as: python3 check_doc_refs.py [ROOT]
|
|
8
|
+
ROOT defaults to the current working directory.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
REF_RE = re.compile(
|
|
21
|
+
r"(?<![\w/.-])("
|
|
22
|
+
r"@[A-Za-z0-9_~/.-]+(?:\.md|/)|"
|
|
23
|
+
r"~/\.claude/[A-Za-z0-9_/.-]+(?:\.md|/)|"
|
|
24
|
+
r"(?:docs|references)/[A-Za-z0-9_/.-]+\.md"
|
|
25
|
+
r")"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_ref(source: Path, raw: str, root: Path, home: Path) -> Path:
|
|
30
|
+
ref = raw[1:] if raw.startswith("@") else raw
|
|
31
|
+
|
|
32
|
+
if ref.startswith("~/"):
|
|
33
|
+
return (home / ref[2:]).resolve()
|
|
34
|
+
|
|
35
|
+
path = Path(ref)
|
|
36
|
+
if path.is_absolute():
|
|
37
|
+
return path.resolve()
|
|
38
|
+
|
|
39
|
+
if raw.startswith("@"):
|
|
40
|
+
return (root / ref).resolve()
|
|
41
|
+
|
|
42
|
+
if ref.startswith("docs/"):
|
|
43
|
+
return (root / ref).resolve()
|
|
44
|
+
|
|
45
|
+
if ref.startswith("references/"):
|
|
46
|
+
source_parts = source.relative_to(root).parts
|
|
47
|
+
if len(source_parts) >= 4 and source_parts[:2] == (".claude", "skills"):
|
|
48
|
+
skill_root = root.joinpath(*source_parts[:3])
|
|
49
|
+
return (skill_root / ref).resolve()
|
|
50
|
+
return (root / ref).resolve()
|
|
51
|
+
|
|
52
|
+
return (source.parent / ref).resolve()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def collect_scan_files(root: Path) -> list[Path]:
|
|
56
|
+
scan_files: list[Path] = []
|
|
57
|
+
for candidate in (root / "AGENTS.md", root / "CLAUDE.md"):
|
|
58
|
+
if candidate.is_file():
|
|
59
|
+
scan_files.append(candidate)
|
|
60
|
+
for pattern in (".claude/rules/*.md", ".claude/skills/*/SKILL.md"):
|
|
61
|
+
scan_files.extend(sorted(root.glob(pattern)))
|
|
62
|
+
return scan_files
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> int:
|
|
66
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
67
|
+
parser.add_argument("root", nargs="?", default=".", help="Project root (default: cwd)")
|
|
68
|
+
args = parser.parse_args()
|
|
69
|
+
root = Path(args.root).resolve()
|
|
70
|
+
home = Path(os.environ.get("HOME", "")).expanduser()
|
|
71
|
+
|
|
72
|
+
scan_files = collect_scan_files(root)
|
|
73
|
+
|
|
74
|
+
missing: list[str] = []
|
|
75
|
+
seen: set[tuple[Path, int, str]] = set()
|
|
76
|
+
for path in scan_files:
|
|
77
|
+
in_fence = False
|
|
78
|
+
for lineno, line in enumerate(
|
|
79
|
+
path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1
|
|
80
|
+
):
|
|
81
|
+
stripped = line.lstrip()
|
|
82
|
+
if stripped.startswith("```") or stripped.startswith("~~~"):
|
|
83
|
+
in_fence = not in_fence
|
|
84
|
+
continue
|
|
85
|
+
if in_fence:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
for match in REF_RE.finditer(line):
|
|
89
|
+
raw = match.group(1)
|
|
90
|
+
key = (path, lineno, raw)
|
|
91
|
+
if key in seen:
|
|
92
|
+
continue
|
|
93
|
+
seen.add(key)
|
|
94
|
+
|
|
95
|
+
target = resolve_ref(path, raw, root, home)
|
|
96
|
+
exists = target.is_dir() if raw.endswith("/") else target.is_file()
|
|
97
|
+
if not exists:
|
|
98
|
+
source = path.relative_to(root)
|
|
99
|
+
missing.append(f"MISSING: {source}:{lineno} -> {raw}")
|
|
100
|
+
|
|
101
|
+
if missing:
|
|
102
|
+
print("\n".join(missing))
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
print("doc references: ok")
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
sys.exit(main())
|