@prateek_ai/agents-maker 1.0.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 +659 -0
- package/agents/architect_agent.md +175 -0
- package/agents/code_agent.md +178 -0
- package/agents/compression_agent.md +226 -0
- package/agents/execution_agent.md +157 -0
- package/agents/orchestrator.md +406 -0
- package/agents/reviewer_agent.md +134 -0
- package/agents/ui_agent.md +147 -0
- package/agents/ux_agent.md +144 -0
- package/bin/cli.js +79 -0
- package/config/agents.yaml +388 -0
- package/config/domain_profiles.yaml +394 -0
- package/config/project.yaml.example +16 -0
- package/config/token_policies.yaml +325 -0
- package/context_loaders/__init__.py +15 -0
- package/context_loaders/__pycache__/__init__.cpython-314.pyc +0 -0
- package/context_loaders/__pycache__/file_chunker.cpython-314.pyc +0 -0
- package/context_loaders/__pycache__/project_summary.cpython-314.pyc +0 -0
- package/context_loaders/__pycache__/repo_tree.cpython-314.pyc +0 -0
- package/context_loaders/file_chunker.py +252 -0
- package/context_loaders/project_summary.py +369 -0
- package/context_loaders/repo_tree.py +203 -0
- package/package.json +46 -0
- package/quickstart.ps1 +252 -0
- package/quickstart.sh +232 -0
- package/requirements.txt +3 -0
- package/skills/analyze_repo.md +86 -0
- package/skills/animated_website.md +285 -0
- package/skills/compare_approaches.md +86 -0
- package/skills/define_data_schema.md +99 -0
- package/skills/design_api.md +126 -0
- package/skills/improve_copy.md +69 -0
- package/skills/review_code.md +83 -0
- package/skills/review_layout.md +89 -0
- package/skills/suggest_next.md +105 -0
- package/skills/summarize_history.md +89 -0
- package/skills/write_process_map.md +105 -0
- package/skills/write_tests.md +97 -0
- package/token_optimization/__pycache__/compressor.cpython-314.pyc +0 -0
- package/token_optimization/compressor.py +502 -0
- package/token_optimization/output_styles.md +80 -0
- package/tools/__pycache__/domain_utils.cpython-314.pyc +0 -0
- package/tools/__pycache__/generate_claude_md.cpython-314.pyc +0 -0
- package/tools/__pycache__/validate_kit.cpython-314.pyc +0 -0
- package/tools/domain_utils.py +141 -0
- package/tools/generate_claude_md.py +269 -0
- package/tools/generate_platform_configs.py +467 -0
- package/tools/generate_prompt.py +461 -0
- package/tools/init_project.py +454 -0
- package/tools/test_kit.py +363 -0
- package/tools/validate_kit.py +504 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
validate_kit.py — Multi-Agent Assistant Kit integrity checker.
|
|
4
|
+
|
|
5
|
+
Run after any change to config files, agents, skills, or domains:
|
|
6
|
+
python tools/validate_kit.py
|
|
7
|
+
|
|
8
|
+
Runs 12 checks:
|
|
9
|
+
1. YAML parse — all 3 config files
|
|
10
|
+
2. Agent .md files exist and have content
|
|
11
|
+
3. Skill .md files exist and have content
|
|
12
|
+
4. Domain coverage in token_policies.yaml
|
|
13
|
+
5. Domain primary_agents reference valid agent_ids
|
|
14
|
+
6. Output style references are defined
|
|
15
|
+
7. Domain detection scoring (8 test messages)
|
|
16
|
+
8. File inventory matches README repository map
|
|
17
|
+
9. Compressor dry-run (PolicyLoader loads 3 workflows)
|
|
18
|
+
10. Skill markdown structure (input, output, token cost sections)
|
|
19
|
+
11. Agent markdown structure (role, goals, context sections)
|
|
20
|
+
12. system_prompt.md freshness (source hash matches current agents + skills)
|
|
21
|
+
|
|
22
|
+
Exit code 0 = all checks pass. Exit code 1 = one or more failures.
|
|
23
|
+
Requires pyyaml (pip install pyyaml).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
import re
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
__version__ = "1.0.0"
|
|
34
|
+
|
|
35
|
+
ROOT = Path(__file__).parent.parent
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Helpers
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def load_yaml(path: str) -> dict:
|
|
43
|
+
import yaml
|
|
44
|
+
full_path = ROOT / path
|
|
45
|
+
try:
|
|
46
|
+
with full_path.open(encoding="utf-8") as f:
|
|
47
|
+
return yaml.safe_load(f) or {}
|
|
48
|
+
except yaml.YAMLError as e:
|
|
49
|
+
fail(f"YAML parse error in {path}: {e}")
|
|
50
|
+
return {}
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
fail(f"File not found: {path}")
|
|
53
|
+
return {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ok(msg: str) -> None:
|
|
57
|
+
print(f" [PASS] {msg}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def fail(msg: str) -> None:
|
|
61
|
+
print(f" [FAIL] {msg}")
|
|
62
|
+
FAILURES.append(msg)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
FAILURES: list[str] = []
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Check 1 — YAML parses
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def check_yaml_parse() -> tuple[dict, dict, dict]:
|
|
73
|
+
print("\n--- YAML Parse ---")
|
|
74
|
+
agents_cfg = domain_cfg = policy_cfg = {}
|
|
75
|
+
for path in ["config/agents.yaml", "config/domain_profiles.yaml", "config/token_policies.yaml"]:
|
|
76
|
+
try:
|
|
77
|
+
data = load_yaml(path)
|
|
78
|
+
ok(f"YAML: {path}")
|
|
79
|
+
if "agents" in path:
|
|
80
|
+
agents_cfg = data
|
|
81
|
+
elif "domain" in path:
|
|
82
|
+
domain_cfg = data
|
|
83
|
+
else:
|
|
84
|
+
policy_cfg = data
|
|
85
|
+
except Exception as e:
|
|
86
|
+
fail(f"YAML: {path} — {e}")
|
|
87
|
+
return agents_cfg, domain_cfg, policy_cfg
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Check 2 — Agent .md files exist
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def check_agent_files(agents_cfg: dict) -> None:
|
|
95
|
+
print("\n--- Agent Files ---")
|
|
96
|
+
registered = list((agents_cfg.get("agents") or {}).keys())
|
|
97
|
+
if not registered:
|
|
98
|
+
fail("agents.yaml has no agents registered")
|
|
99
|
+
return
|
|
100
|
+
missing = []
|
|
101
|
+
empty = []
|
|
102
|
+
for agent_id in registered:
|
|
103
|
+
md = ROOT / "agents" / f"{agent_id}.md"
|
|
104
|
+
if not md.exists():
|
|
105
|
+
missing.append(agent_id)
|
|
106
|
+
else:
|
|
107
|
+
content = md.read_text(encoding="utf-8").strip()
|
|
108
|
+
if len(content) < 100:
|
|
109
|
+
empty.append(f"{agent_id}.md ({len(content)} chars)")
|
|
110
|
+
if missing:
|
|
111
|
+
fail(f"Agent files missing for: {missing}")
|
|
112
|
+
elif empty:
|
|
113
|
+
fail(f"Agent files suspiciously short (< 100 chars): {empty}")
|
|
114
|
+
else:
|
|
115
|
+
ok(f"Agent files: {len(registered)}/{len(registered)} present ({', '.join(registered)})")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Check 3 — Skill .md files exist
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def check_skill_files(agents_cfg: dict) -> None:
|
|
123
|
+
print("\n--- Skill Files ---")
|
|
124
|
+
agents = agents_cfg.get("agents") or {}
|
|
125
|
+
all_skills: set[str] = set()
|
|
126
|
+
for cfg in agents.values():
|
|
127
|
+
for s in (cfg.get("skills") or []):
|
|
128
|
+
all_skills.add(s)
|
|
129
|
+
if not all_skills:
|
|
130
|
+
ok("No skills referenced in agents.yaml (skip)")
|
|
131
|
+
return
|
|
132
|
+
missing = []
|
|
133
|
+
empty = []
|
|
134
|
+
for s in sorted(all_skills):
|
|
135
|
+
skill_path = ROOT / "skills" / f"{s}.md"
|
|
136
|
+
if not skill_path.exists():
|
|
137
|
+
missing.append(s)
|
|
138
|
+
else:
|
|
139
|
+
content = skill_path.read_text(encoding="utf-8").strip()
|
|
140
|
+
if len(content) < 50:
|
|
141
|
+
empty.append(f"{s}.md ({len(content)} chars)")
|
|
142
|
+
if missing:
|
|
143
|
+
fail(f"Skill files missing: {missing}")
|
|
144
|
+
elif empty:
|
|
145
|
+
fail(f"Skill files suspiciously short (< 50 chars): {empty}")
|
|
146
|
+
else:
|
|
147
|
+
ok(f"Skill files: {len(all_skills)}/{len(all_skills)} present")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Check 4 — Domain coverage in token_policies.yaml
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
def check_domain_coverage(domain_cfg: dict, policy_cfg: dict) -> None:
|
|
155
|
+
print("\n--- Domain Coverage in token_policies.yaml ---")
|
|
156
|
+
domains = list((domain_cfg.get("domains") or {}).keys())
|
|
157
|
+
if not domains:
|
|
158
|
+
fail("domain_profiles.yaml has no domains")
|
|
159
|
+
return
|
|
160
|
+
# generic_project_lifecycle is nested under workflows:
|
|
161
|
+
lifecycle = (policy_cfg.get("workflows") or {}).get("generic_project_lifecycle") or {}
|
|
162
|
+
covered = set((lifecycle.get("domains") or {}).keys())
|
|
163
|
+
missing = [d for d in domains if d not in covered]
|
|
164
|
+
if missing:
|
|
165
|
+
fail(f"Domains missing from token_policies generic_project_lifecycle.domains: {missing}")
|
|
166
|
+
else:
|
|
167
|
+
ok(f"Domain coverage: all {len(domains)} domains have overrides ({', '.join(domains)})")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Check 5 — domain primary_agents reference valid agent_ids
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def check_primary_agents(domain_cfg: dict, agents_cfg: dict) -> None:
|
|
175
|
+
print("\n--- Domain primary_agents Validity ---")
|
|
176
|
+
valid_ids = set((agents_cfg.get("agents") or {}).keys())
|
|
177
|
+
domains = domain_cfg.get("domains") or {}
|
|
178
|
+
errors = []
|
|
179
|
+
for d, cfg in domains.items():
|
|
180
|
+
for phase, agent_id in (cfg.get("primary_agents") or {}).items():
|
|
181
|
+
if agent_id not in valid_ids:
|
|
182
|
+
errors.append(f"domain={d} phase={phase}: unknown agent '{agent_id}'")
|
|
183
|
+
if errors:
|
|
184
|
+
for e in errors:
|
|
185
|
+
fail(e)
|
|
186
|
+
else:
|
|
187
|
+
ok("All domain primary_agents reference valid agent_ids")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Check 6 — output_style references exist in output_styles block
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
def check_output_styles(policy_cfg: dict) -> None:
|
|
195
|
+
print("\n--- Output Style References ---")
|
|
196
|
+
defined_styles = set((policy_cfg.get("output_styles") or {}).keys())
|
|
197
|
+
if not defined_styles:
|
|
198
|
+
fail("No output_styles defined in token_policies.yaml")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
refs: set[str] = set()
|
|
202
|
+
# Scan workflows and phases for output_style keys
|
|
203
|
+
for key, val in (policy_cfg.get("workflows") or {}).items():
|
|
204
|
+
if isinstance(val, dict) and "output_style" in val:
|
|
205
|
+
refs.add(val["output_style"])
|
|
206
|
+
|
|
207
|
+
lifecycle = policy_cfg.get("generic_project_lifecycle") or {}
|
|
208
|
+
for phase_key, phase_val in (lifecycle.get("phases") or {}).items():
|
|
209
|
+
if isinstance(phase_val, dict) and "output_style" in phase_val:
|
|
210
|
+
refs.add(phase_val["output_style"])
|
|
211
|
+
for d_key, d_val in (lifecycle.get("domains") or {}).items():
|
|
212
|
+
for phase_key, phase_val in (d_val or {}).items():
|
|
213
|
+
if isinstance(phase_val, dict) and "output_style" in phase_val:
|
|
214
|
+
refs.add(phase_val["output_style"])
|
|
215
|
+
|
|
216
|
+
missing_styles = refs - defined_styles
|
|
217
|
+
if missing_styles:
|
|
218
|
+
fail(f"output_style values referenced but not defined: {sorted(missing_styles)}")
|
|
219
|
+
else:
|
|
220
|
+
ok(f"Output styles: all {len(refs)} referenced styles are defined")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# Check 7 — Domain detection scoring smoke test
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
SCORING_TESTS = [
|
|
228
|
+
("software", "high", "Help me refactor the UserService and add unit tests for the API endpoints."),
|
|
229
|
+
("content", "high", "Write a blog post about AI trends for our marketing newsletter."),
|
|
230
|
+
("data_analytics", "high", "Analyze our sales funnel conversion data and build a dashboard."),
|
|
231
|
+
("general", "low", "Help me document the onboarding process for new engineers."),
|
|
232
|
+
("product_design", "high", "Design a mobile checkout flow for our e-commerce app."),
|
|
233
|
+
("research", "high", "Write a literature review on transformer architectures."),
|
|
234
|
+
("marketing", "high", "Create a go-to-market strategy for our new SaaS product."),
|
|
235
|
+
("general", "low", "I need help with something."),
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _score(message: str, domains: dict, settings: dict) -> tuple[str, str]:
|
|
240
|
+
msg_lower = message.lower()
|
|
241
|
+
scores: dict[str, float] = {}
|
|
242
|
+
for d, cfg in domains.items():
|
|
243
|
+
if d == "general":
|
|
244
|
+
continue
|
|
245
|
+
strong = cfg.get("detection_signals", {}).get("strong", [])
|
|
246
|
+
weak = cfg.get("detection_signals", {}).get("weak", [])
|
|
247
|
+
s = sum(1.0 for sig in strong if sig in msg_lower)
|
|
248
|
+
w = sum(0.4 for sig in weak if sig in msg_lower)
|
|
249
|
+
scores[d] = (s + w) / 3
|
|
250
|
+
|
|
251
|
+
ranked = sorted(scores.items(), key=lambda x: -x[1])
|
|
252
|
+
top_d, top_s = ranked[0] if ranked else ("general", 0.0)
|
|
253
|
+
sec_d, sec_s = ranked[1] if len(ranked) > 1 else ("general", 0.0)
|
|
254
|
+
|
|
255
|
+
conf_t = settings.get("confidence_threshold", 0.40)
|
|
256
|
+
amb_t = settings.get("ambiguity_threshold", 0.10)
|
|
257
|
+
|
|
258
|
+
if top_s < conf_t:
|
|
259
|
+
return "general", "low"
|
|
260
|
+
elif (top_s - sec_s) < amb_t:
|
|
261
|
+
return top_d, "medium"
|
|
262
|
+
else:
|
|
263
|
+
return top_d, "high"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def check_domain_scoring(domain_cfg: dict) -> None:
|
|
267
|
+
print("\n--- Domain Detection Scoring ---")
|
|
268
|
+
domains = domain_cfg.get("domains") or {}
|
|
269
|
+
settings = domain_cfg.get("settings") or {}
|
|
270
|
+
passed = 0
|
|
271
|
+
for exp_domain, exp_conf, msg in SCORING_TESTS:
|
|
272
|
+
got_domain, got_conf = _score(msg, domains, settings)
|
|
273
|
+
if got_domain == exp_domain and got_conf == exp_conf:
|
|
274
|
+
passed += 1
|
|
275
|
+
else:
|
|
276
|
+
fail(
|
|
277
|
+
f"Scoring mismatch: expected ({exp_domain}, {exp_conf}), "
|
|
278
|
+
f"got ({got_domain}, {got_conf}) for: \"{msg[:55]}...\""
|
|
279
|
+
)
|
|
280
|
+
if passed == len(SCORING_TESTS):
|
|
281
|
+
ok(f"Domain scoring: {passed}/{len(SCORING_TESTS)} test messages routed correctly")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# Check 8 — File inventory from README.md repository map
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
# (defined below — keep check order consistent with main())
|
|
289
|
+
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
# Check 9 — Compressor dry-run
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
def check_compressor() -> None:
|
|
295
|
+
print("\n--- Compressor Dry-Run ---")
|
|
296
|
+
try:
|
|
297
|
+
import sys as _sys
|
|
298
|
+
_sys.path.insert(0, str(ROOT))
|
|
299
|
+
from token_optimization.compressor import PolicyLoader
|
|
300
|
+
loader = PolicyLoader(ROOT / "config" / "token_policies.yaml")
|
|
301
|
+
loader.load()
|
|
302
|
+
for workflow in ["feature_implementation", "code_review", "feature_design"]:
|
|
303
|
+
policy = loader.get_workflow_policy(workflow)
|
|
304
|
+
if not policy.output_style:
|
|
305
|
+
fail(f"PolicyLoader.get_workflow_policy('{workflow}') returned empty output_style")
|
|
306
|
+
return
|
|
307
|
+
if policy.max_input_tokens <= 0:
|
|
308
|
+
fail(f"Policy '{workflow}' has invalid max_input_tokens: {policy.max_input_tokens}")
|
|
309
|
+
return
|
|
310
|
+
ok("Compressor: PolicyLoader loads and returns valid policies for 3 workflows")
|
|
311
|
+
except ImportError as e:
|
|
312
|
+
fail(f"Compressor import failed: {e}")
|
|
313
|
+
except Exception as e:
|
|
314
|
+
fail(f"Compressor dry-run failed: {e}")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
# Check 10 — Skill markdown structure
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
_REQUIRED_SKILL_SECTIONS = ["input", "output", "token cost"]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def check_skill_structure(agents_cfg: dict) -> None:
|
|
325
|
+
print("\n--- Skill Markdown Structure ---")
|
|
326
|
+
agents = agents_cfg.get("agents") or {}
|
|
327
|
+
all_skills: set[str] = set()
|
|
328
|
+
for cfg in agents.values():
|
|
329
|
+
for s in (cfg.get("skills") or []):
|
|
330
|
+
all_skills.add(s)
|
|
331
|
+
|
|
332
|
+
if not all_skills:
|
|
333
|
+
ok("No skills to check (skip)")
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
issues: list[str] = []
|
|
337
|
+
for skill_key in sorted(all_skills):
|
|
338
|
+
path = ROOT / "skills" / f"{skill_key}.md"
|
|
339
|
+
if not path.exists():
|
|
340
|
+
continue # already caught by Check 3
|
|
341
|
+
content = path.read_text(encoding="utf-8").lower()
|
|
342
|
+
missing = [s for s in _REQUIRED_SKILL_SECTIONS if s not in content]
|
|
343
|
+
if missing:
|
|
344
|
+
issues.append(f"{skill_key}.md missing sections: {missing}")
|
|
345
|
+
|
|
346
|
+
if issues:
|
|
347
|
+
for issue in issues:
|
|
348
|
+
fail(f"Skill structure: {issue}")
|
|
349
|
+
else:
|
|
350
|
+
ok(f"Skill structure: all {len(all_skills)} skill cards have required sections (input, output, token cost)")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
# Check 11 — Agent markdown structure
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
_REQUIRED_AGENT_SECTIONS = ["## role", "## goals", "context"]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def check_agent_structure(agents_cfg: dict) -> None:
|
|
361
|
+
print("\n--- Agent Markdown Structure ---")
|
|
362
|
+
registered = list((agents_cfg.get("agents") or {}).keys())
|
|
363
|
+
|
|
364
|
+
if not registered:
|
|
365
|
+
ok("No agents to check (skip)")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
issues: list[str] = []
|
|
369
|
+
for agent_id in registered:
|
|
370
|
+
path = ROOT / "agents" / f"{agent_id}.md"
|
|
371
|
+
if not path.exists():
|
|
372
|
+
continue # already caught by Check 2
|
|
373
|
+
content = path.read_text(encoding="utf-8").lower()
|
|
374
|
+
missing = [s for s in _REQUIRED_AGENT_SECTIONS if s not in content]
|
|
375
|
+
if missing:
|
|
376
|
+
issues.append(f"{agent_id}.md missing sections: {missing}")
|
|
377
|
+
|
|
378
|
+
if issues:
|
|
379
|
+
for issue in issues:
|
|
380
|
+
fail(f"Agent structure: {issue}")
|
|
381
|
+
else:
|
|
382
|
+
ok(f"Agent structure: all {len(registered)} agent specs have required sections (role, goals, context)")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
# Check 8 — File inventory from README.md repository map (continued below)
|
|
387
|
+
# ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
def check_file_inventory() -> None:
|
|
390
|
+
print("\n--- File Inventory (README.md repository map) ---")
|
|
391
|
+
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
|
392
|
+
# Find the repository map code block and extract file names
|
|
393
|
+
block = re.search(r"Repository Map.*?```(.*?)```", readme, re.DOTALL)
|
|
394
|
+
if not block:
|
|
395
|
+
ok("No repository map block found in README.md (skip)")
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
files_in_readme: list[str] = []
|
|
399
|
+
for line in block.group(1).splitlines():
|
|
400
|
+
# Allow optional emoji/unicode between tree indicator and filename
|
|
401
|
+
m = re.search(r"(?:├──|└──)\s*[^\w\s./\\-]*\s*([\w][\w.\-]*\.\w+)", line)
|
|
402
|
+
if m:
|
|
403
|
+
files_in_readme.append(m.group(1))
|
|
404
|
+
|
|
405
|
+
if not files_in_readme:
|
|
406
|
+
ok("Repository map found but no file entries extracted (skip)")
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
missing = [f for f in files_in_readme if not list(ROOT.rglob(f))]
|
|
410
|
+
if missing:
|
|
411
|
+
fail(f"Files in README map not found on disk: {missing}")
|
|
412
|
+
else:
|
|
413
|
+
ok(f"File inventory: all {len(files_in_readme)} files from README map exist on disk")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# Check 12 — system_prompt.md freshness
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
def _compute_source_hash(root: Path) -> str:
|
|
421
|
+
h = hashlib.sha256()
|
|
422
|
+
agent_files = sorted((root / "agents").glob("*.md")) if (root / "agents").is_dir() else []
|
|
423
|
+
skill_files = sorted((root / "skills").glob("*.md")) if (root / "skills").is_dir() else []
|
|
424
|
+
for path in agent_files + skill_files:
|
|
425
|
+
try:
|
|
426
|
+
h.update(path.read_bytes().replace(b"\r\n", b"\n"))
|
|
427
|
+
except (OSError, PermissionError):
|
|
428
|
+
pass
|
|
429
|
+
return h.hexdigest()[:16]
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def check_system_prompt_freshness() -> None:
|
|
433
|
+
print("\n--- system_prompt.md Freshness ---")
|
|
434
|
+
sp_path = ROOT / "system_prompt.md"
|
|
435
|
+
if not sp_path.exists():
|
|
436
|
+
ok("system_prompt.md not present — skipping freshness check (run init_project.py to generate)")
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
content = sp_path.read_text(encoding="utf-8")
|
|
441
|
+
except (OSError, PermissionError) as e:
|
|
442
|
+
fail(f"Could not read system_prompt.md: {e}")
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
m = re.search(r"Source hash:\s*([a-f0-9]{16})", content)
|
|
446
|
+
if not m:
|
|
447
|
+
fail("system_prompt.md has no source hash — regenerate with: python tools/init_project.py --update")
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
stored = m.group(1)
|
|
451
|
+
current = _compute_source_hash(ROOT)
|
|
452
|
+
if stored != current:
|
|
453
|
+
fail(
|
|
454
|
+
f"system_prompt.md is stale (stored hash {stored} != current {current}). "
|
|
455
|
+
"Regenerate: python tools/init_project.py --update"
|
|
456
|
+
)
|
|
457
|
+
else:
|
|
458
|
+
ok(f"system_prompt.md is current (source hash: {current})")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# ---------------------------------------------------------------------------
|
|
462
|
+
# Main
|
|
463
|
+
# ---------------------------------------------------------------------------
|
|
464
|
+
|
|
465
|
+
def main() -> int:
|
|
466
|
+
if "--version" in sys.argv or "-V" in sys.argv:
|
|
467
|
+
print(f"validate_kit.py {__version__}")
|
|
468
|
+
return 0
|
|
469
|
+
FAILURES.clear()
|
|
470
|
+
print("=" * 60)
|
|
471
|
+
print(" Multi-Agent Assistant Kit — Integrity Checker")
|
|
472
|
+
print(f" Root: {ROOT}")
|
|
473
|
+
print("=" * 60)
|
|
474
|
+
|
|
475
|
+
agents_cfg, domain_cfg, policy_cfg = check_yaml_parse()
|
|
476
|
+
check_agent_files(agents_cfg)
|
|
477
|
+
check_skill_files(agents_cfg)
|
|
478
|
+
check_domain_coverage(domain_cfg, policy_cfg)
|
|
479
|
+
check_primary_agents(domain_cfg, agents_cfg)
|
|
480
|
+
check_output_styles(policy_cfg)
|
|
481
|
+
check_domain_scoring(domain_cfg)
|
|
482
|
+
check_file_inventory()
|
|
483
|
+
check_compressor()
|
|
484
|
+
check_skill_structure(agents_cfg)
|
|
485
|
+
check_agent_structure(agents_cfg)
|
|
486
|
+
check_system_prompt_freshness()
|
|
487
|
+
|
|
488
|
+
total_checks = 12
|
|
489
|
+
failed = len(FAILURES)
|
|
490
|
+
passed = total_checks - failed
|
|
491
|
+
|
|
492
|
+
print("\n" + "=" * 60)
|
|
493
|
+
if failed == 0:
|
|
494
|
+
print(f" Result: ALL {total_checks} checks PASSED")
|
|
495
|
+
else:
|
|
496
|
+
print(f" Result: {passed}/{total_checks} checks passed. {failed} FAILURE(s):")
|
|
497
|
+
for f in FAILURES:
|
|
498
|
+
print(f" - {f}")
|
|
499
|
+
print("=" * 60)
|
|
500
|
+
return 1 if failed else 0
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
if __name__ == "__main__":
|
|
504
|
+
sys.exit(main())
|