ma-agents 3.13.2 → 3.14.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.
Files changed (70) hide show
  1. package/lib/agents.js +7 -0
  2. package/lib/bmad-cache/bmb/.claude-plugin/marketplace.json +3 -3
  3. package/lib/bmad-cache/bmb/_git_preserved/index +0 -0
  4. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.idx +0 -0
  5. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/{pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.pack → pack-b388526edf307cf56f8e65673466cff074105abf.pack} +0 -0
  6. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.rev +0 -0
  7. package/lib/bmad-cache/bmb/_git_preserved/packed-refs +1 -1
  8. package/lib/bmad-cache/bmb/_git_preserved/refs/heads/main +1 -1
  9. package/lib/bmad-cache/bmb/_git_preserved/shallow +1 -1
  10. package/lib/bmad-cache/bmb/package-lock.json +2 -2
  11. package/lib/bmad-cache/bmb/package.json +1 -1
  12. package/lib/bmad-cache/bmb/samples/bmad-agent-code-coach/scripts/wake.py +78 -0
  13. package/lib/bmad-cache/bmb/samples/bmad-agent-creative-muse/scripts/wake.py +78 -0
  14. package/lib/bmad-cache/bmb/samples/bmad-agent-sentinel/scripts/wake.py +78 -0
  15. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/customize-template.toml +3 -0
  16. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/init-sanctum-template.py +11 -5
  17. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/report-shell.html +1073 -0
  18. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/wake-template.py +78 -0
  19. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/customize.toml +48 -0
  20. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/count_tokens.py +78 -0
  21. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass.py +258 -0
  22. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/process-template.py +20 -0
  23. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/render_report.py +387 -0
  24. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/adapter-claude-code.json +9 -0
  25. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/aggregate_benchmark.py +236 -0
  26. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/memlog.py +197 -0
  27. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_evals.py +513 -390
  28. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_triggers.py +347 -251
  29. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_env_isolation.py +79 -0
  30. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_trigger_detection.py +123 -0
  31. package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/tests/test-validate-module.py +82 -4
  32. package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/validate-module.py +55 -11
  33. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/customize-template.toml +3 -5
  34. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/report-shell.html +860 -0
  35. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/customize.toml +59 -0
  36. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/count_tokens.py +77 -0
  37. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/init_skill.py +132 -0
  38. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-prompt-metrics.py +186 -221
  39. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/quick_validate.py +117 -0
  40. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/render_report.py +387 -0
  41. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_canon_sync.py +46 -0
  42. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_count_tokens.py +180 -0
  43. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_render_report.py +171 -0
  44. package/lib/bmad-cache/cache-manifest.json +4 -4
  45. package/lib/bmad-extension/.claude-plugin/marketplace.json.template +1 -1
  46. package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +2 -2
  47. package/lib/bmad.js +6 -1
  48. package/lib/templates/instruction-block-git.template.md +25 -25
  49. package/lib/templates/instruction-block-onprem.template.md +86 -86
  50. package/lib/templates/instruction-block-universal.template.md +29 -29
  51. package/package.json +3 -3
  52. package/lib/.bmad-extension-plugin.build-22612-1782454014750/.claude-plugin/marketplace.json +0 -109
  53. package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module-help.csv +0 -62
  54. package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module.yaml +0 -20
  55. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.idx +0 -0
  56. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.rev +0 -0
  57. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/references/sample-init-sanctum.py +0 -274
  58. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/generate-html-report.py +0 -534
  59. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-execution-deps.py +0 -337
  60. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-prompt-metrics.py +0 -425
  61. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-sanctum-architecture.py +0 -385
  62. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-structure-capabilities.py +0 -482
  63. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/Dockerfile +0 -29
  64. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/docker_setup.py +0 -115
  65. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/generate_report.py +0 -184
  66. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/pty_runner.py +0 -171
  67. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/utils.py +0 -260
  68. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/extract-report-json.py +0 -287
  69. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/generate-html-report.py +0 -588
  70. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-execution-deps.py +0 -288
@@ -1,425 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Deterministic pre-pass for prompt craft scanner (agent builder).
3
-
4
- Extracts metrics and flagged patterns from SKILL.md and prompt files
5
- so the LLM scanner can work from compact data instead of reading raw files.
6
-
7
- Covers:
8
- - SKILL.md line count and section inventory
9
- - Overview section size
10
- - Inline data detection (tables, fenced code blocks)
11
- - Defensive padding pattern grep
12
- - Meta-explanation pattern grep
13
- - Back-reference detection ("as described above")
14
- - Config header and progression condition presence per prompt
15
- - File-level token estimates (chars / 4 rough approximation)
16
- - Prompt frontmatter validation (name, description, menu-code)
17
- - Wall-of-text detection
18
- - Suggestive loading grep
19
- """
20
-
21
- # /// script
22
- # requires-python = ">=3.9"
23
- # ///
24
-
25
- from __future__ import annotations
26
-
27
- import argparse
28
- import json
29
- import re
30
- import sys
31
- from datetime import datetime, timezone
32
- from pathlib import Path
33
-
34
-
35
- # Defensive padding / filler patterns
36
- WASTE_PATTERNS = [
37
- (r'\b[Mm]ake sure (?:to|you)\b', 'defensive-padding', 'Defensive: "make sure to/you"'),
38
- (r"\b[Dd]on'?t forget (?:to|that)\b", 'defensive-padding', "Defensive: \"don't forget\""),
39
- (r'\b[Rr]emember (?:to|that)\b', 'defensive-padding', 'Defensive: "remember to/that"'),
40
- (r'\b[Bb]e sure to\b', 'defensive-padding', 'Defensive: "be sure to"'),
41
- (r'\b[Pp]lease ensure\b', 'defensive-padding', 'Defensive: "please ensure"'),
42
- (r'\b[Ii]t is important (?:to|that)\b', 'defensive-padding', 'Defensive: "it is important"'),
43
- (r'\b[Yy]ou are an AI\b', 'meta-explanation', 'Meta: "you are an AI"'),
44
- (r'\b[Aa]s a language model\b', 'meta-explanation', 'Meta: "as a language model"'),
45
- (r'\b[Aa]s an AI assistant\b', 'meta-explanation', 'Meta: "as an AI assistant"'),
46
- (r'\b[Tt]his (?:workflow|skill|process) is designed to\b', 'meta-explanation', 'Meta: "this workflow is designed to"'),
47
- (r'\b[Tt]he purpose of this (?:section|step) is\b', 'meta-explanation', 'Meta: "the purpose of this section is"'),
48
- (r"\b[Ll]et'?s (?:think about|begin|start)\b", 'filler', "Filler: \"let's think/begin\""),
49
- (r'\b[Nn]ow we(?:\'ll| will)\b', 'filler', "Filler: \"now we'll\""),
50
- ]
51
-
52
- # Back-reference patterns (self-containment risk)
53
- BACKREF_PATTERNS = [
54
- (r'\bas described above\b', 'Back-reference: "as described above"'),
55
- (r'\bper the overview\b', 'Back-reference: "per the overview"'),
56
- (r'\bas mentioned (?:above|in|earlier)\b', 'Back-reference: "as mentioned above/in/earlier"'),
57
- (r'\bsee (?:above|the overview)\b', 'Back-reference: "see above/the overview"'),
58
- (r'\brefer to (?:the )?(?:above|overview|SKILL)\b', 'Back-reference: "refer to above/overview"'),
59
- ]
60
-
61
- # Suggestive loading patterns
62
- SUGGESTIVE_LOADING_PATTERNS = [
63
- (r'\b[Ll]oad (?:the |all )?(?:relevant|necessary|needed|required)\b', 'Suggestive loading: "load relevant/necessary"'),
64
- (r'\b[Rr]ead (?:the |all )?(?:relevant|necessary|needed|required)\b', 'Suggestive loading: "read relevant/necessary"'),
65
- (r'\b[Gg]ather (?:the |all )?(?:relevant|necessary|needed)\b', 'Suggestive loading: "gather relevant/necessary"'),
66
- ]
67
-
68
-
69
- def count_tables(content: str) -> tuple[int, int]:
70
- """Count markdown tables and their total lines."""
71
- table_count = 0
72
- table_lines = 0
73
- in_table = False
74
- for line in content.split('\n'):
75
- if '|' in line and re.match(r'^\s*\|', line):
76
- if not in_table:
77
- table_count += 1
78
- in_table = True
79
- table_lines += 1
80
- else:
81
- in_table = False
82
- return table_count, table_lines
83
-
84
-
85
- def count_fenced_blocks(content: str) -> tuple[int, int]:
86
- """Count fenced code blocks and their total lines."""
87
- block_count = 0
88
- block_lines = 0
89
- in_block = False
90
- for line in content.split('\n'):
91
- if line.strip().startswith('```'):
92
- if in_block:
93
- in_block = False
94
- else:
95
- in_block = True
96
- block_count += 1
97
- elif in_block:
98
- block_lines += 1
99
- return block_count, block_lines
100
-
101
-
102
- def extract_overview_size(content: str) -> int:
103
- """Count lines in the ## Overview section."""
104
- lines = content.split('\n')
105
- in_overview = False
106
- overview_lines = 0
107
- for line in lines:
108
- if re.match(r'^##\s+Overview\b', line):
109
- in_overview = True
110
- continue
111
- elif in_overview and re.match(r'^##\s', line):
112
- break
113
- elif in_overview:
114
- overview_lines += 1
115
- return overview_lines
116
-
117
-
118
- def detect_wall_of_text(content: str) -> list[dict]:
119
- """Detect long runs of text without headers or breaks."""
120
- walls = []
121
- lines = content.split('\n')
122
- run_start = None
123
- run_length = 0
124
-
125
- for i, line in enumerate(lines, 1):
126
- stripped = line.strip()
127
- is_break = (
128
- not stripped
129
- or re.match(r'^#{1,6}\s', stripped)
130
- or re.match(r'^[-*]\s', stripped)
131
- or re.match(r'^\d+\.\s', stripped)
132
- or stripped.startswith('```')
133
- or stripped.startswith('|')
134
- )
135
-
136
- if is_break:
137
- if run_length >= 15:
138
- walls.append({
139
- 'start_line': run_start,
140
- 'length': run_length,
141
- })
142
- run_start = None
143
- run_length = 0
144
- else:
145
- if run_start is None:
146
- run_start = i
147
- run_length += 1
148
-
149
- if run_length >= 15:
150
- walls.append({
151
- 'start_line': run_start,
152
- 'length': run_length,
153
- })
154
-
155
- return walls
156
-
157
-
158
- def parse_prompt_frontmatter(filepath: Path) -> dict:
159
- """Parse YAML frontmatter from a prompt file and validate."""
160
- content = filepath.read_text(encoding='utf-8')
161
- result = {
162
- 'has_frontmatter': False,
163
- 'fields': {},
164
- 'missing_fields': [],
165
- }
166
-
167
- fm_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
168
- if not fm_match:
169
- result['missing_fields'] = ['name', 'description', 'menu-code']
170
- return result
171
-
172
- result['has_frontmatter'] = True
173
-
174
- try:
175
- import yaml
176
- fm = yaml.safe_load(fm_match.group(1))
177
- except Exception:
178
- # Fallback: simple key-value parsing
179
- fm = {}
180
- for line in fm_match.group(1).split('\n'):
181
- if ':' in line:
182
- key, _, val = line.partition(':')
183
- fm[key.strip()] = val.strip()
184
-
185
- if not isinstance(fm, dict):
186
- result['missing_fields'] = ['name', 'description', 'menu-code']
187
- return result
188
-
189
- expected_fields = ['name', 'description', 'menu-code']
190
- for field in expected_fields:
191
- if field in fm:
192
- result['fields'][field] = fm[field]
193
- else:
194
- result['missing_fields'].append(field)
195
-
196
- return result
197
-
198
-
199
- def scan_file_patterns(filepath: Path, rel_path: str) -> dict:
200
- """Extract metrics and pattern matches from a single file."""
201
- content = filepath.read_text(encoding='utf-8')
202
- lines = content.split('\n')
203
- line_count = len(lines)
204
-
205
- # Token estimate (rough: chars / 4)
206
- token_estimate = len(content) // 4
207
-
208
- # Section inventory
209
- sections = []
210
- for i, line in enumerate(lines, 1):
211
- m = re.match(r'^(#{2,3})\s+(.+)$', line)
212
- if m:
213
- sections.append({'level': len(m.group(1)), 'title': m.group(2).strip(), 'line': i})
214
-
215
- # Tables and code blocks
216
- table_count, table_lines = count_tables(content)
217
- block_count, block_lines = count_fenced_blocks(content)
218
-
219
- # Pattern matches
220
- waste_matches = []
221
- for pattern, category, label in WASTE_PATTERNS:
222
- for m in re.finditer(pattern, content):
223
- line_num = content[:m.start()].count('\n') + 1
224
- waste_matches.append({
225
- 'line': line_num,
226
- 'category': category,
227
- 'pattern': label,
228
- 'context': lines[line_num - 1].strip()[:100],
229
- })
230
-
231
- backref_matches = []
232
- for pattern, label in BACKREF_PATTERNS:
233
- for m in re.finditer(pattern, content, re.IGNORECASE):
234
- line_num = content[:m.start()].count('\n') + 1
235
- backref_matches.append({
236
- 'line': line_num,
237
- 'pattern': label,
238
- 'context': lines[line_num - 1].strip()[:100],
239
- })
240
-
241
- # Suggestive loading
242
- suggestive_loading = []
243
- for pattern, label in SUGGESTIVE_LOADING_PATTERNS:
244
- for m in re.finditer(pattern, content, re.IGNORECASE):
245
- line_num = content[:m.start()].count('\n') + 1
246
- suggestive_loading.append({
247
- 'line': line_num,
248
- 'pattern': label,
249
- 'context': lines[line_num - 1].strip()[:100],
250
- })
251
-
252
- # Config header
253
- has_config_header = '{communication_language}' in content or '{document_output_language}' in content
254
-
255
- # Progression condition
256
- prog_keywords = ['progress', 'advance', 'move to', 'next stage',
257
- 'when complete', 'proceed to', 'transition', 'completion criteria']
258
- has_progression = any(kw in content.lower() for kw in prog_keywords)
259
-
260
- # Wall-of-text detection
261
- walls = detect_wall_of_text(content)
262
-
263
- result = {
264
- 'file': rel_path,
265
- 'line_count': line_count,
266
- 'token_estimate': token_estimate,
267
- 'sections': sections,
268
- 'table_count': table_count,
269
- 'table_lines': table_lines,
270
- 'fenced_block_count': block_count,
271
- 'fenced_block_lines': block_lines,
272
- 'waste_patterns': waste_matches,
273
- 'back_references': backref_matches,
274
- 'suggestive_loading': suggestive_loading,
275
- 'has_config_header': has_config_header,
276
- 'has_progression': has_progression,
277
- 'wall_of_text': walls,
278
- }
279
-
280
- return result
281
-
282
-
283
- def scan_prompt_metrics(skill_path: Path) -> dict:
284
- """Extract metrics from all prompt-relevant files."""
285
- files_data = []
286
-
287
- # SKILL.md
288
- skill_md = skill_path / 'SKILL.md'
289
- if skill_md.exists():
290
- data = scan_file_patterns(skill_md, 'SKILL.md')
291
- content = skill_md.read_text(encoding='utf-8')
292
- data['overview_lines'] = extract_overview_size(content)
293
- data['is_skill_md'] = True
294
- files_data.append(data)
295
-
296
- # Detect memory agent
297
- is_memory_agent = False
298
- assets_dir = skill_path / 'assets'
299
- if assets_dir.exists():
300
- is_memory_agent = any(
301
- f.name.endswith('-template.md') for f in assets_dir.iterdir() if f.is_file()
302
- )
303
-
304
- # Prompt files at skill root
305
- skip_files = {'SKILL.md'}
306
-
307
- for f in sorted(skill_path.iterdir()):
308
- if f.is_file() and f.suffix == '.md' and f.name not in skip_files and f.name != 'SKILL.md':
309
- data = scan_file_patterns(f, f.name)
310
- data['is_skill_md'] = False
311
-
312
- # Parse prompt frontmatter
313
- pfm = parse_prompt_frontmatter(f)
314
- data['prompt_frontmatter'] = pfm
315
-
316
- files_data.append(data)
317
-
318
- # Also scan references/ for capability prompts (memory agents keep prompts here)
319
- refs_dir = skill_path / 'references'
320
- if refs_dir.exists():
321
- for f in sorted(refs_dir.iterdir()):
322
- if f.is_file() and f.suffix == '.md':
323
- data = scan_file_patterns(f, f'references/{f.name}')
324
- data['is_skill_md'] = False
325
-
326
- pfm = parse_prompt_frontmatter(f)
327
- data['prompt_frontmatter'] = pfm
328
-
329
- files_data.append(data)
330
-
331
- # Resources (just sizes, for progressive disclosure assessment)
332
- resources_dir = skill_path / 'resources'
333
- resource_sizes = {}
334
- if resources_dir.exists():
335
- for f in sorted(resources_dir.iterdir()):
336
- if f.is_file() and f.suffix in ('.md', '.json', '.yaml', '.yml'):
337
- content = f.read_text(encoding='utf-8')
338
- resource_sizes[f.name] = {
339
- 'lines': len(content.split('\n')),
340
- 'tokens': len(content) // 4,
341
- }
342
-
343
- # Aggregate stats
344
- total_waste = sum(len(f['waste_patterns']) for f in files_data)
345
- total_backrefs = sum(len(f['back_references']) for f in files_data)
346
- total_suggestive = sum(len(f.get('suggestive_loading', [])) for f in files_data)
347
- total_tokens = sum(f['token_estimate'] for f in files_data)
348
- total_walls = sum(len(f.get('wall_of_text', [])) for f in files_data)
349
- prompts_with_config = sum(1 for f in files_data if not f.get('is_skill_md') and f['has_config_header'])
350
- prompts_with_progression = sum(1 for f in files_data if not f.get('is_skill_md') and f['has_progression'])
351
- total_prompts = sum(1 for f in files_data if not f.get('is_skill_md'))
352
-
353
- skill_md_data = next((f for f in files_data if f.get('is_skill_md')), None)
354
-
355
- return {
356
- 'scanner': 'prompt-craft-prepass',
357
- 'script': 'prepass-prompt-metrics.py',
358
- 'version': '1.0.0',
359
- 'skill_path': str(skill_path),
360
- 'timestamp': datetime.now(timezone.utc).isoformat(),
361
- 'status': 'info',
362
- 'is_memory_agent': is_memory_agent,
363
- 'skill_md_summary': {
364
- 'line_count': skill_md_data['line_count'] if skill_md_data else 0,
365
- 'token_estimate': skill_md_data['token_estimate'] if skill_md_data else 0,
366
- 'overview_lines': skill_md_data.get('overview_lines', 0) if skill_md_data else 0,
367
- 'table_count': skill_md_data['table_count'] if skill_md_data else 0,
368
- 'table_lines': skill_md_data['table_lines'] if skill_md_data else 0,
369
- 'fenced_block_count': skill_md_data['fenced_block_count'] if skill_md_data else 0,
370
- 'fenced_block_lines': skill_md_data['fenced_block_lines'] if skill_md_data else 0,
371
- 'section_count': len(skill_md_data['sections']) if skill_md_data else 0,
372
- },
373
- 'prompt_health': {
374
- 'total_prompts': total_prompts,
375
- 'prompts_with_config_header': prompts_with_config,
376
- 'prompts_with_progression': prompts_with_progression,
377
- },
378
- 'aggregate': {
379
- 'total_files_scanned': len(files_data),
380
- 'total_token_estimate': total_tokens,
381
- 'total_waste_patterns': total_waste,
382
- 'total_back_references': total_backrefs,
383
- 'total_suggestive_loading': total_suggestive,
384
- 'total_wall_of_text': total_walls,
385
- },
386
- 'resource_sizes': resource_sizes,
387
- 'files': files_data,
388
- }
389
-
390
-
391
- def main() -> int:
392
- parser = argparse.ArgumentParser(
393
- description='Extract prompt craft metrics for LLM scanner pre-pass (agent builder)',
394
- )
395
- parser.add_argument(
396
- 'skill_path',
397
- type=Path,
398
- help='Path to the skill directory to scan',
399
- )
400
- parser.add_argument(
401
- '--output', '-o',
402
- type=Path,
403
- help='Write JSON output to file instead of stdout',
404
- )
405
- args = parser.parse_args()
406
-
407
- if not args.skill_path.is_dir():
408
- print(f"Error: {args.skill_path} is not a directory", file=sys.stderr)
409
- return 2
410
-
411
- result = scan_prompt_metrics(args.skill_path)
412
- output = json.dumps(result, indent=2)
413
-
414
- if args.output:
415
- args.output.parent.mkdir(parents=True, exist_ok=True)
416
- args.output.write_text(output)
417
- print(f"Results written to {args.output}", file=sys.stderr)
418
- else:
419
- print(output)
420
-
421
- return 0
422
-
423
-
424
- if __name__ == '__main__':
425
- sys.exit(main())