@event4u/agent-config 1.33.0 → 1.34.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/.agent-src/commands/review-changes.md +13 -8
- package/.agent-src/personas/README.md +12 -21
- package/.agent-src/personas/_template-specialist/persona.md +89 -0
- package/.agent-src/personas/backend-architect.md +96 -0
- package/.agent-src/personas/eloquent-tamer.md +96 -0
- package/.agent-src/personas/frontend-engineer.md +100 -0
- package/.agent-src/personas/qa.md +27 -2
- package/.agent-src/personas/security-engineer.md +100 -0
- package/.agent-src/skills/accessibility-auditor/SKILL.md +132 -0
- package/.agent-src/skills/adr-create/SKILL.md +1 -0
- package/.agent-src/skills/adversarial-review/SKILL.md +1 -0
- package/.agent-src/skills/agent-docs-writing/SKILL.md +1 -0
- package/.agent-src/skills/agents-md-thin-root/SKILL.md +1 -0
- package/.agent-src/skills/ai-council/SKILL.md +1 -0
- package/.agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -0
- package/.agent-src/skills/analysis-skill-router/SKILL.md +1 -0
- package/.agent-src/skills/api-design/SKILL.md +3 -0
- package/.agent-src/skills/api-endpoint/SKILL.md +1 -0
- package/.agent-src/skills/api-testing/SKILL.md +1 -0
- package/.agent-src/skills/architecture-review-lens/SKILL.md +137 -0
- package/.agent-src/skills/artisan-commands/SKILL.md +1 -0
- package/.agent-src/skills/async-python-patterns/SKILL.md +1 -0
- package/.agent-src/skills/authz-review/SKILL.md +4 -0
- package/.agent-src/skills/aws-infrastructure/SKILL.md +1 -0
- package/.agent-src/skills/blade-ui/SKILL.md +1 -0
- package/.agent-src/skills/blast-radius-analyzer/SKILL.md +3 -0
- package/.agent-src/skills/bug-analyzer/SKILL.md +1 -0
- package/.agent-src/skills/check-refs/SKILL.md +1 -0
- package/.agent-src/skills/code-refactoring/SKILL.md +1 -0
- package/.agent-src/skills/code-review/SKILL.md +1 -0
- package/.agent-src/skills/command-routing/SKILL.md +1 -0
- package/.agent-src/skills/command-writing/SKILL.md +1 -0
- package/.agent-src/skills/composer-packages/SKILL.md +1 -0
- package/.agent-src/skills/context-authoring/SKILL.md +1 -0
- package/.agent-src/skills/context-document/SKILL.md +1 -0
- package/.agent-src/skills/conventional-commits-writing/SKILL.md +1 -0
- package/.agent-src/skills/copilot-agents-optimization/SKILL.md +1 -0
- package/.agent-src/skills/copilot-config/SKILL.md +1 -0
- package/.agent-src/skills/dashboard-design/SKILL.md +1 -0
- package/.agent-src/skills/data-flow-mapper/SKILL.md +1 -0
- package/.agent-src/skills/database/SKILL.md +3 -0
- package/.agent-src/skills/dcf-modeling/SKILL.md +1 -0
- package/.agent-src/skills/decision-record/SKILL.md +143 -0
- package/.agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
- package/.agent-src/skills/defense-in-depth/SKILL.md +1 -0
- package/.agent-src/skills/dependency-upgrade/SKILL.md +1 -0
- package/.agent-src/skills/description-assist/SKILL.md +1 -0
- package/.agent-src/skills/design-review/SKILL.md +1 -0
- package/.agent-src/skills/devcontainer/SKILL.md +1 -0
- package/.agent-src/skills/developer-like-execution/SKILL.md +1 -0
- package/.agent-src/skills/docker/SKILL.md +1 -0
- package/.agent-src/skills/dto-creator/SKILL.md +1 -0
- package/.agent-src/skills/eloquent/SKILL.md +3 -0
- package/.agent-src/skills/error-handling-patterns/SKILL.md +1 -0
- package/.agent-src/skills/estimate-ticket/SKILL.md +1 -0
- package/.agent-src/skills/existing-ui-audit/SKILL.md +3 -0
- package/.agent-src/skills/fe-design/SKILL.md +4 -1
- package/.agent-src/skills/feature-planning/SKILL.md +1 -0
- package/.agent-src/skills/file-editor/SKILL.md +1 -0
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +1 -0
- package/.agent-src/skills/flux/SKILL.md +1 -0
- package/.agent-src/skills/form-handler/SKILL.md +145 -0
- package/.agent-src/skills/funnel-analysis/SKILL.md +1 -0
- package/.agent-src/skills/git-workflow/SKILL.md +1 -0
- package/.agent-src/skills/github-ci/SKILL.md +1 -0
- package/.agent-src/skills/grafana/SKILL.md +1 -0
- package/.agent-src/skills/guideline-writing/SKILL.md +1 -0
- package/.agent-src/skills/incident-commander/SKILL.md +140 -0
- package/.agent-src/skills/jira-integration/SKILL.md +1 -0
- package/.agent-src/skills/jobs-events/SKILL.md +1 -0
- package/.agent-src/skills/judge-bug-hunter/SKILL.md +1 -0
- package/.agent-src/skills/judge-code-quality/SKILL.md +1 -0
- package/.agent-src/skills/judge-security-auditor/SKILL.md +3 -0
- package/.agent-src/skills/judge-test-coverage/SKILL.md +1 -0
- package/.agent-src/skills/laravel/SKILL.md +1 -0
- package/.agent-src/skills/laravel-horizon/SKILL.md +1 -0
- package/.agent-src/skills/laravel-mail/SKILL.md +1 -0
- package/.agent-src/skills/laravel-middleware/SKILL.md +1 -0
- package/.agent-src/skills/laravel-notifications/SKILL.md +1 -0
- package/.agent-src/skills/laravel-pennant/SKILL.md +1 -0
- package/.agent-src/skills/laravel-pulse/SKILL.md +1 -0
- package/.agent-src/skills/laravel-reverb/SKILL.md +1 -0
- package/.agent-src/skills/laravel-scheduling/SKILL.md +1 -0
- package/.agent-src/skills/laravel-validation/SKILL.md +1 -0
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -0
- package/.agent-src/skills/lint-skills/SKILL.md +1 -0
- package/.agent-src/skills/livewire/SKILL.md +1 -0
- package/.agent-src/skills/livewire-architect/SKILL.md +158 -0
- package/.agent-src/skills/logging-monitoring/SKILL.md +1 -0
- package/.agent-src/skills/markitdown/SKILL.md +1 -0
- package/.agent-src/skills/mcp/SKILL.md +1 -0
- package/.agent-src/skills/mcp-builder/SKILL.md +1 -0
- package/.agent-src/skills/md-language-check/SKILL.md +1 -0
- package/.agent-src/skills/merge-conflicts/SKILL.md +1 -0
- package/.agent-src/skills/migration-architect/SKILL.md +119 -0
- package/.agent-src/skills/migration-creator/SKILL.md +1 -0
- package/.agent-src/skills/mobile-e2e-strategy/SKILL.md +2 -1
- package/.agent-src/skills/module-management/SKILL.md +1 -0
- package/.agent-src/skills/multi-tenancy/SKILL.md +1 -0
- package/.agent-src/skills/okr-tree-modeling/SKILL.md +1 -0
- package/.agent-src/skills/openapi/SKILL.md +1 -0
- package/.agent-src/skills/override-management/SKILL.md +1 -0
- package/.agent-src/skills/performance/SKILL.md +1 -0
- package/.agent-src/skills/performance-analysis/SKILL.md +1 -0
- package/.agent-src/skills/persona-writing/SKILL.md +1 -0
- package/.agent-src/skills/pest-testing/SKILL.md +1 -0
- package/.agent-src/skills/php-coder/SKILL.md +1 -0
- package/.agent-src/skills/php-debugging/SKILL.md +1 -0
- package/.agent-src/skills/php-service/SKILL.md +1 -0
- package/.agent-src/skills/playwright-architect/SKILL.md +141 -0
- package/.agent-src/skills/playwright-testing/SKILL.md +1 -0
- package/.agent-src/skills/po-discovery/SKILL.md +127 -0
- package/.agent-src/skills/project-analysis-core/SKILL.md +1 -0
- package/.agent-src/skills/project-analysis-hypothesis-driven/SKILL.md +1 -0
- package/.agent-src/skills/project-analysis-laravel/SKILL.md +1 -0
- package/.agent-src/skills/project-analysis-nextjs/SKILL.md +1 -0
- package/.agent-src/skills/project-analysis-node-express/SKILL.md +1 -0
- package/.agent-src/skills/project-analysis-react/SKILL.md +1 -0
- package/.agent-src/skills/project-analysis-symfony/SKILL.md +1 -0
- package/.agent-src/skills/project-analysis-zend-laminas/SKILL.md +1 -0
- package/.agent-src/skills/project-analyzer/SKILL.md +1 -0
- package/.agent-src/skills/project-docs/SKILL.md +1 -0
- package/.agent-src/skills/prompt-engineering-patterns/SKILL.md +1 -0
- package/.agent-src/skills/prompt-optimizer/SKILL.md +1 -0
- package/.agent-src/skills/quality-tools/SKILL.md +1 -0
- package/.agent-src/skills/react-native-setup/SKILL.md +1 -0
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +1 -0
- package/.agent-src/skills/readme-reviewer/SKILL.md +1 -0
- package/.agent-src/skills/readme-writing/SKILL.md +1 -0
- package/.agent-src/skills/readme-writing-package/SKILL.md +1 -0
- package/.agent-src/skills/receiving-code-review/SKILL.md +1 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +1 -0
- package/.agent-src/skills/refine-ticket/SKILL.md +1 -0
- package/.agent-src/skills/repomix-packer/SKILL.md +1 -0
- package/.agent-src/skills/requesting-code-review/SKILL.md +1 -0
- package/.agent-src/skills/review-routing/SKILL.md +1 -0
- package/.agent-src/skills/rice-prioritization/SKILL.md +1 -0
- package/.agent-src/skills/risk-officer/SKILL.md +141 -0
- package/.agent-src/skills/roadmap-management/SKILL.md +1 -0
- package/.agent-src/skills/roadmap-writing/SKILL.md +1 -0
- package/.agent-src/skills/rtk-output-filtering/SKILL.md +1 -0
- package/.agent-src/skills/rule-writing/SKILL.md +1 -0
- package/.agent-src/skills/script-writing/SKILL.md +1 -0
- package/.agent-src/skills/secrets-management/SKILL.md +1 -0
- package/.agent-src/skills/security/SKILL.md +1 -0
- package/.agent-src/skills/security-audit/SKILL.md +1 -0
- package/.agent-src/skills/sentry-integration/SKILL.md +1 -0
- package/.agent-src/skills/sequential-thinking/SKILL.md +1 -0
- package/.agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -0
- package/.agent-src/skills/skill-management/SKILL.md +1 -0
- package/.agent-src/skills/skill-reviewer/SKILL.md +1 -0
- package/.agent-src/skills/skill-writing/SKILL.md +1 -0
- package/.agent-src/skills/sql-writing/SKILL.md +1 -0
- package/.agent-src/skills/stakeholder-tradeoff/SKILL.md +149 -0
- package/.agent-src/skills/subagent-orchestration/SKILL.md +13 -0
- package/.agent-src/skills/systematic-debugging/SKILL.md +1 -0
- package/.agent-src/skills/tailwind-engineer/SKILL.md +130 -0
- package/.agent-src/skills/tech-debt-tracker/SKILL.md +152 -0
- package/.agent-src/skills/technical-specification/SKILL.md +1 -0
- package/.agent-src/skills/terraform/SKILL.md +1 -0
- package/.agent-src/skills/terragrunt/SKILL.md +1 -0
- package/.agent-src/skills/test-driven-development/SKILL.md +1 -0
- package/.agent-src/skills/test-performance/SKILL.md +1 -0
- package/.agent-src/skills/testing-anti-patterns/SKILL.md +1 -0
- package/.agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/.agent-src/skills/token-optimizer/SKILL.md +1 -0
- package/.agent-src/skills/traefik/SKILL.md +1 -0
- package/.agent-src/skills/ui-component-architect/SKILL.md +153 -0
- package/.agent-src/skills/unit-economics-modeling/SKILL.md +1 -0
- package/.agent-src/skills/universal-project-analysis/SKILL.md +1 -0
- package/.agent-src/skills/upstream-contribute/SKILL.md +1 -0
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
- package/.agent-src/skills/validate-feature-fit/SKILL.md +1 -0
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +1 -0
- package/.agent-src/skills/websocket/SKILL.md +1 -0
- package/.claude-plugin/marketplace.json +15 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +41 -0
- package/README.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +17 -3
- package/docs/contracts/file-ownership-matrix.json +506 -0
- package/docs/contracts/persona-schema.md +136 -0
- package/docs/contracts/skill-domains.md +143 -0
- package/docs/decisions/ADR-005-subagent-worktrees.md +120 -0
- package/docs/decisions/ADR-006-skill-tools-python-pilot.md +114 -0
- package/docs/decisions/INDEX.md +3 -0
- package/docs/personas.md +115 -0
- package/package.json +1 -1
- package/scripts/_backfill_skill_domains.py +140 -0
- package/scripts/_emit_domain_table.py +35 -0
- package/scripts/install-hooks.sh +21 -4
- package/scripts/lint_skill_tools.py +168 -0
- package/scripts/schemas/skill.schema.json +6 -1
- package/scripts/skill_linter.py +19 -4
- package/scripts/skill_tools/__init__.py +22 -0
- package/scripts/skill_tools/audit_persona_coverage.py +147 -0
- package/scripts/skill_tools/run_block_d_eval.py +129 -0
- package/scripts/skill_tools/score_skill_relevance.py +169 -0
- package/scripts/skill_tools/suggest_skill_for_task.py +113 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One-shot back-fill: inject `domain:` frontmatter into every SKILL.md.
|
|
3
|
+
|
|
4
|
+
Removed after B3 lands. Source-of-truth = SKILL_DOMAIN_MAP below.
|
|
5
|
+
Fails loudly if the map and on-disk skill set diverge.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
SKILL_DOMAIN_MAP: dict[str, str] = {
|
|
13
|
+
"adr-create": "process", "adversarial-review": "quality",
|
|
14
|
+
"agent-docs-writing": "process", "agents-md-thin-root": "process",
|
|
15
|
+
"ai-council": "process", "analysis-autonomous-mode": "discovery",
|
|
16
|
+
"analysis-skill-router": "discovery", "api-design": "engineering",
|
|
17
|
+
"api-endpoint": "engineering", "api-testing": "quality",
|
|
18
|
+
"artisan-commands": "engineering", "async-python-patterns": "engineering",
|
|
19
|
+
"authz-review": "quality", "aws-infrastructure": "devops",
|
|
20
|
+
"blade-ui": "engineering", "blast-radius-analyzer": "discovery",
|
|
21
|
+
"bug-analyzer": "discovery", "check-refs": "process",
|
|
22
|
+
"code-refactoring": "engineering", "code-review": "quality",
|
|
23
|
+
"command-routing": "process", "command-writing": "process",
|
|
24
|
+
"composer-packages": "engineering", "context-authoring": "process",
|
|
25
|
+
"context-document": "process", "conventional-commits-writing": "process",
|
|
26
|
+
"copilot-agents-optimization": "process", "copilot-config": "process",
|
|
27
|
+
"dashboard-design": "devops", "data-flow-mapper": "discovery",
|
|
28
|
+
"database": "engineering", "dcf-modeling": "product",
|
|
29
|
+
"deep-reading-analyst": "discovery", "defense-in-depth": "quality",
|
|
30
|
+
"dependency-upgrade": "engineering", "description-assist": "process",
|
|
31
|
+
"design-review": "quality", "devcontainer": "devops",
|
|
32
|
+
"developer-like-execution": "process", "docker": "devops",
|
|
33
|
+
"dto-creator": "engineering", "eloquent": "engineering",
|
|
34
|
+
"error-handling-patterns": "engineering", "estimate-ticket": "product",
|
|
35
|
+
"existing-ui-audit": "discovery", "fe-design": "engineering",
|
|
36
|
+
"feature-planning": "product", "file-editor": "process",
|
|
37
|
+
"finishing-a-development-branch": "process", "flux": "engineering",
|
|
38
|
+
"funnel-analysis": "product", "git-workflow": "process",
|
|
39
|
+
"github-ci": "devops", "grafana": "devops",
|
|
40
|
+
"guideline-writing": "process", "jira-integration": "process",
|
|
41
|
+
"jobs-events": "engineering", "judge-bug-hunter": "quality",
|
|
42
|
+
"judge-code-quality": "quality", "judge-security-auditor": "quality",
|
|
43
|
+
"judge-test-coverage": "quality", "laravel": "engineering",
|
|
44
|
+
"laravel-horizon": "engineering", "laravel-mail": "engineering",
|
|
45
|
+
"laravel-middleware": "engineering", "laravel-notifications": "engineering",
|
|
46
|
+
"laravel-pennant": "engineering", "laravel-pulse": "engineering",
|
|
47
|
+
"laravel-reverb": "engineering", "laravel-scheduling": "engineering",
|
|
48
|
+
"laravel-validation": "engineering", "learning-to-rule-or-skill": "process",
|
|
49
|
+
"lint-skills": "process", "livewire": "engineering",
|
|
50
|
+
"logging-monitoring": "devops", "markitdown": "process",
|
|
51
|
+
"mcp": "process", "mcp-builder": "process",
|
|
52
|
+
"md-language-check": "process", "merge-conflicts": "process",
|
|
53
|
+
"migration-creator": "engineering", "mobile-e2e-strategy": "quality",
|
|
54
|
+
"module-management": "process", "multi-tenancy": "engineering",
|
|
55
|
+
"okr-tree-modeling": "product", "openapi": "engineering",
|
|
56
|
+
"override-management": "process", "performance": "engineering",
|
|
57
|
+
"performance-analysis": "discovery", "persona-writing": "process",
|
|
58
|
+
"pest-testing": "quality", "php-coder": "engineering",
|
|
59
|
+
"php-debugging": "engineering", "php-service": "engineering",
|
|
60
|
+
"playwright-testing": "quality", "project-analysis-core": "discovery",
|
|
61
|
+
"project-analysis-hypothesis-driven": "discovery",
|
|
62
|
+
"project-analysis-laravel": "discovery", "project-analysis-nextjs": "discovery",
|
|
63
|
+
"project-analysis-node-express": "discovery",
|
|
64
|
+
"project-analysis-react": "discovery", "project-analysis-symfony": "discovery",
|
|
65
|
+
"project-analysis-zend-laminas": "discovery", "project-analyzer": "discovery",
|
|
66
|
+
"project-docs": "process", "prompt-engineering-patterns": "product",
|
|
67
|
+
"prompt-optimizer": "product", "quality-tools": "quality",
|
|
68
|
+
"react-native-setup": "devops", "react-shadcn-ui": "engineering",
|
|
69
|
+
"readme-reviewer": "quality", "readme-writing": "process",
|
|
70
|
+
"readme-writing-package": "process", "receiving-code-review": "process",
|
|
71
|
+
"refine-prompt": "product", "refine-ticket": "product",
|
|
72
|
+
"repomix-packer": "process", "requesting-code-review": "process",
|
|
73
|
+
"review-routing": "quality", "rice-prioritization": "product",
|
|
74
|
+
"roadmap-management": "process", "roadmap-writing": "process",
|
|
75
|
+
"rtk-output-filtering": "process", "rule-writing": "process",
|
|
76
|
+
"script-writing": "process", "secrets-management": "devops",
|
|
77
|
+
"security": "quality", "security-audit": "quality",
|
|
78
|
+
"sentry-integration": "devops", "sequential-thinking": "process",
|
|
79
|
+
"skill-improvement-pipeline": "process", "skill-management": "process",
|
|
80
|
+
"skill-reviewer": "quality", "skill-writing": "process",
|
|
81
|
+
"sql-writing": "engineering", "subagent-orchestration": "process",
|
|
82
|
+
"systematic-debugging": "discovery", "technical-specification": "product",
|
|
83
|
+
"terraform": "devops", "terragrunt": "devops",
|
|
84
|
+
"test-driven-development": "quality", "test-performance": "quality",
|
|
85
|
+
"testing-anti-patterns": "quality", "threat-modeling": "quality",
|
|
86
|
+
"token-optimizer": "process", "traefik": "devops",
|
|
87
|
+
"unit-economics-modeling": "product", "universal-project-analysis": "discovery",
|
|
88
|
+
"upstream-contribute": "process", "using-git-worktrees": "process",
|
|
89
|
+
"validate-feature-fit": "quality", "verify-completion-evidence": "quality",
|
|
90
|
+
"websocket": "engineering",
|
|
91
|
+
}
|
|
92
|
+
VALID_DOMAINS = {"engineering", "product", "quality", "devops", "process", "discovery"}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main() -> int:
|
|
96
|
+
# Default: source-of-truth tree. Pass --target=compressed to mirror into .agent-src/.
|
|
97
|
+
target = ".agent-src.uncompressed/skills"
|
|
98
|
+
for arg in sys.argv[1:]:
|
|
99
|
+
if arg == "--target=compressed":
|
|
100
|
+
target = ".agent-src/skills"
|
|
101
|
+
skills_root = Path(target)
|
|
102
|
+
on_disk = sorted(p.name for p in skills_root.iterdir() if p.is_dir())
|
|
103
|
+
in_map = sorted(SKILL_DOMAIN_MAP.keys())
|
|
104
|
+
missing = set(on_disk) - set(in_map)
|
|
105
|
+
extra = set(in_map) - set(on_disk)
|
|
106
|
+
if missing or extra:
|
|
107
|
+
print(f"DRIFT: missing={sorted(missing)} extra={sorted(extra)}", file=sys.stderr)
|
|
108
|
+
return 2
|
|
109
|
+
bad = {k: v for k, v in SKILL_DOMAIN_MAP.items() if v not in VALID_DOMAINS}
|
|
110
|
+
if bad:
|
|
111
|
+
print(f"INVALID DOMAIN VALUES: {bad}", file=sys.stderr)
|
|
112
|
+
return 2
|
|
113
|
+
|
|
114
|
+
fm_pat = re.compile(r"^(---\n)(.*?)(\n---\n)", re.DOTALL)
|
|
115
|
+
domain_line_pat = re.compile(r"^domain:[ \t]*[A-Za-z]+[ \t]*$", re.MULTILINE)
|
|
116
|
+
source_line_pat = re.compile(r"^(source:[ \t]*\S+)$", re.MULTILINE)
|
|
117
|
+
edited = 0
|
|
118
|
+
for slug, domain in SKILL_DOMAIN_MAP.items():
|
|
119
|
+
skill_md = skills_root / slug / "SKILL.md"
|
|
120
|
+
text = skill_md.read_text()
|
|
121
|
+
m = fm_pat.match(text)
|
|
122
|
+
if not m:
|
|
123
|
+
print(f"NO FRONTMATTER: {skill_md}", file=sys.stderr)
|
|
124
|
+
return 2
|
|
125
|
+
fm_body = m.group(2)
|
|
126
|
+
if domain_line_pat.search(fm_body):
|
|
127
|
+
new_fm = domain_line_pat.sub(f"domain: {domain}", fm_body)
|
|
128
|
+
elif source_line_pat.search(fm_body):
|
|
129
|
+
new_fm = source_line_pat.sub(rf"\1\ndomain: {domain}", fm_body)
|
|
130
|
+
else:
|
|
131
|
+
new_fm = fm_body.rstrip("\n") + f"\ndomain: {domain}"
|
|
132
|
+
if new_fm != fm_body:
|
|
133
|
+
skill_md.write_text(m.group(1) + new_fm + m.group(3) + text[m.end():])
|
|
134
|
+
edited += 1
|
|
135
|
+
print(f"Back-filled {edited}/{len(SKILL_DOMAIN_MAP)} skills")
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
sys.exit(main())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Emit the per-domain skill list as Markdown for skill-domains.md § 4."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
10
|
+
from _backfill_skill_domains import SKILL_DOMAIN_MAP # noqa: E402
|
|
11
|
+
|
|
12
|
+
ORDER = ["engineering", "product", "quality", "devops", "process", "discovery"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
by_domain: dict[str, list[str]] = defaultdict(list)
|
|
17
|
+
for slug, dom in SKILL_DOMAIN_MAP.items():
|
|
18
|
+
by_domain[dom].append(slug)
|
|
19
|
+
|
|
20
|
+
lines: list[str] = []
|
|
21
|
+
total = 0
|
|
22
|
+
for dom in ORDER:
|
|
23
|
+
skills = sorted(by_domain[dom])
|
|
24
|
+
total += len(skills)
|
|
25
|
+
lines.append(f"### {dom} ({len(skills)})")
|
|
26
|
+
lines.append("")
|
|
27
|
+
lines.append(", ".join(f"`{s}`" for s in skills))
|
|
28
|
+
lines.append("")
|
|
29
|
+
lines.append(f"**Total: {total} skills.**")
|
|
30
|
+
print("\n".join(lines))
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
sys.exit(main())
|
package/scripts/install-hooks.sh
CHANGED
|
@@ -13,14 +13,31 @@ mkdir -p "$HOOKS_DIR"
|
|
|
13
13
|
cat > "$HOOKS_DIR/pre-push" << 'EOF'
|
|
14
14
|
#!/usr/bin/env bash
|
|
15
15
|
# Pre-push hook: verify .agent-src/ is in sync with .agent-src.uncompressed/
|
|
16
|
+
# and that the canonical command count matches README + getting-started docs.
|
|
17
|
+
#
|
|
18
|
+
# The command-count gate exists because three consecutive PRs landed
|
|
19
|
+
# post-CI count-drift fixes (e.g. f2fb0026 "bump command count
|
|
20
|
+
# 101→103"). Catching the drift pre-push stops it from flooding remote
|
|
21
|
+
# CI. Runtime ~0.1s.
|
|
22
|
+
|
|
23
|
+
fail=0
|
|
16
24
|
|
|
17
25
|
echo "🔍 Checking .agent-src/ sync..."
|
|
18
|
-
python3 scripts/compress.py --check
|
|
26
|
+
if ! python3 scripts/compress.py --check; then
|
|
27
|
+
echo "❌ .agent-src/ is out of sync. Run 'task sync' and compress changed .md files, then commit."
|
|
28
|
+
fail=1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo "🔍 Checking command count messaging..."
|
|
32
|
+
if ! python3 scripts/check_command_count_messaging.py; then
|
|
33
|
+
echo "❌ Command-count drift in README / AGENTS.md / getting-started. Run 'task counts-update', stage the changes, then re-commit."
|
|
34
|
+
fail=1
|
|
35
|
+
fi
|
|
19
36
|
|
|
20
|
-
if [
|
|
37
|
+
if [ $fail -ne 0 ]; then
|
|
21
38
|
echo ""
|
|
22
|
-
echo "
|
|
23
|
-
echo "
|
|
39
|
+
echo " Push blocked — fix the failures above and re-push."
|
|
40
|
+
echo " Bypass for a WIP push: git push --no-verify"
|
|
24
41
|
exit 1
|
|
25
42
|
fi
|
|
26
43
|
EOF
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Block D · D1 — meta-linter for ``scripts/skill_tools/*.py``.
|
|
3
|
+
|
|
4
|
+
Enforces the four pilot-tool invariants locked by the Block D council
|
|
5
|
+
verdict (`agents/council-responses/block-d-python-tools-pilot-verdict.md`):
|
|
6
|
+
|
|
7
|
+
1. **stdlib-only** — no third-party imports. Internal package imports
|
|
8
|
+
(``scripts.*``) are allowed.
|
|
9
|
+
2. **--help and --json flags** — every tool must register both via
|
|
10
|
+
``argparse`` so callers can introspect and machine-read.
|
|
11
|
+
3. **naming** — ``snake_case_verb_noun.py`` (≥ 1 underscore, lowercase).
|
|
12
|
+
4. **embedded sample data** — module must define a ``_SAMPLE`` constant
|
|
13
|
+
OR contain ``if __name__ == "__main__"`` with sample-mode handling
|
|
14
|
+
(so the tool can run without external fixtures).
|
|
15
|
+
5. **size cap** — file ≤ 200 LOC (per roadmap D1, applies to D1 itself
|
|
16
|
+
and to D2/D3/D4 with their own caps validated externally).
|
|
17
|
+
|
|
18
|
+
Tool discovery is glob-based (``scripts/skill_tools/*.py`` excluding
|
|
19
|
+
``__init__.py``) per anthropic round-2 critique — never a hardcoded list.
|
|
20
|
+
|
|
21
|
+
Run:
|
|
22
|
+
python3 scripts/lint_skill_tools.py # human-readable
|
|
23
|
+
python3 scripts/lint_skill_tools.py --json # machine-readable
|
|
24
|
+
|
|
25
|
+
Exit codes: 0 clean · 1 violations found · 2 usage error.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import ast
|
|
31
|
+
import json
|
|
32
|
+
import re
|
|
33
|
+
import sys
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Dict, List, Tuple
|
|
36
|
+
|
|
37
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
38
|
+
TOOLS_DIR = ROOT / "scripts" / "skill_tools"
|
|
39
|
+
NAME_RE = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+\.py$")
|
|
40
|
+
SIZE_CAP = 200
|
|
41
|
+
|
|
42
|
+
# Python 3.9-compatible stdlib list. Kept conservative — additions are cheap,
|
|
43
|
+
# false negatives are not. Mirrors `sys.stdlib_module_names` from 3.10+.
|
|
44
|
+
STDLIB = frozenset({
|
|
45
|
+
"__future__", "abc", "argparse", "ast", "base64", "collections", "configparser",
|
|
46
|
+
"contextlib", "copy", "csv", "dataclasses", "datetime", "decimal", "difflib",
|
|
47
|
+
"enum", "errno", "fnmatch", "functools", "glob", "gzip", "hashlib", "heapq",
|
|
48
|
+
"html", "http", "importlib", "inspect", "io", "ipaddress", "itertools", "json",
|
|
49
|
+
"logging", "math", "mimetypes", "os", "pathlib", "pickle", "platform", "posixpath",
|
|
50
|
+
"pprint", "queue", "random", "re", "shlex", "shutil", "signal", "socket",
|
|
51
|
+
"sqlite3", "ssl", "stat", "string", "struct", "subprocess", "sys", "tempfile",
|
|
52
|
+
"textwrap", "threading", "time", "tomllib", "traceback", "types", "typing",
|
|
53
|
+
"unicodedata", "unittest", "urllib", "uuid", "venv", "warnings", "weakref",
|
|
54
|
+
"xml", "zipfile", "zlib",
|
|
55
|
+
})
|
|
56
|
+
PROJECT_PACKAGES = frozenset({"scripts", "skill_tools"}) # internal imports are fine.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _violations_for(path: Path) -> List[str]:
|
|
60
|
+
"""Return a list of violation strings for one tool (empty = clean)."""
|
|
61
|
+
out: List[str] = []
|
|
62
|
+
name = path.name
|
|
63
|
+
if not NAME_RE.match(name):
|
|
64
|
+
out.append(f"naming: `{name}` is not snake_case_verb_noun.py")
|
|
65
|
+
|
|
66
|
+
text = path.read_text(encoding="utf-8")
|
|
67
|
+
loc = sum(1 for ln in text.splitlines() if ln.strip() and not ln.lstrip().startswith("#"))
|
|
68
|
+
if loc > SIZE_CAP:
|
|
69
|
+
out.append(f"size: {loc} LOC > {SIZE_CAP} cap")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
tree = ast.parse(text, filename=str(path))
|
|
73
|
+
except SyntaxError as exc:
|
|
74
|
+
out.append(f"syntax: {exc.msg} at line {exc.lineno}")
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
# Imports — flag any non-stdlib, non-project top-level module + record argparse use.
|
|
78
|
+
imported: set[str] = set()
|
|
79
|
+
for node in ast.walk(tree):
|
|
80
|
+
if isinstance(node, ast.Import):
|
|
81
|
+
for alias in node.names:
|
|
82
|
+
root = alias.name.split(".")[0]
|
|
83
|
+
imported.add(root)
|
|
84
|
+
if root not in STDLIB and root not in PROJECT_PACKAGES:
|
|
85
|
+
out.append(f"stdlib-only: imports `{alias.name}` (third-party)")
|
|
86
|
+
elif isinstance(node, ast.ImportFrom):
|
|
87
|
+
if node.module is None or node.level > 0:
|
|
88
|
+
continue # relative imports — package-internal
|
|
89
|
+
root = node.module.split(".")[0]
|
|
90
|
+
imported.add(root)
|
|
91
|
+
if root not in STDLIB and root not in PROJECT_PACKAGES:
|
|
92
|
+
out.append(f"stdlib-only: imports from `{node.module}` (third-party)")
|
|
93
|
+
|
|
94
|
+
# CLI flags — confirm argparse is imported and `--json` is registered.
|
|
95
|
+
has_argparse = "argparse" in imported
|
|
96
|
+
has_json_flag = re.search(r"['\"]--json['\"]", text) is not None
|
|
97
|
+
if not has_argparse:
|
|
98
|
+
out.append("cli: no `argparse` import detected")
|
|
99
|
+
if not has_json_flag:
|
|
100
|
+
out.append("cli: missing `--json` flag")
|
|
101
|
+
# `--help` is auto-registered by argparse; we sanity-check that
|
|
102
|
+
# add_help isn't disabled.
|
|
103
|
+
if re.search(r"add_help\s*=\s*False", text):
|
|
104
|
+
out.append("cli: `add_help=False` disables --help")
|
|
105
|
+
|
|
106
|
+
# Embedded sample data — accept either a module-level _SAMPLE constant
|
|
107
|
+
# or a __main__ block (the tool can self-demo).
|
|
108
|
+
has_sample = bool(re.search(r"^_SAMPLE\s*[:=]", text, re.MULTILINE))
|
|
109
|
+
has_main = '__name__ == "__main__"' in text or "__name__ == '__main__'" in text
|
|
110
|
+
if not (has_sample or has_main):
|
|
111
|
+
out.append("sample: no `_SAMPLE` constant or `__main__` block")
|
|
112
|
+
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def lint(tools_dir: Path) -> Tuple[int, Dict[str, List[str]]]:
|
|
117
|
+
tools_dir = tools_dir.resolve()
|
|
118
|
+
if not tools_dir.is_dir():
|
|
119
|
+
return 2, {"_error": [f"tools dir missing: {tools_dir}"]}
|
|
120
|
+
findings: Dict[str, List[str]] = {}
|
|
121
|
+
for path in sorted(tools_dir.glob("*.py")):
|
|
122
|
+
if path.name == "__init__.py":
|
|
123
|
+
continue
|
|
124
|
+
viols = _violations_for(path)
|
|
125
|
+
if viols:
|
|
126
|
+
try:
|
|
127
|
+
key = str(path.relative_to(ROOT))
|
|
128
|
+
except ValueError:
|
|
129
|
+
key = str(path)
|
|
130
|
+
findings[key] = viols
|
|
131
|
+
return (1 if findings else 0), findings
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _print_human(findings: Dict[str, List[str]]) -> None:
|
|
135
|
+
if not findings:
|
|
136
|
+
print(f"✅ scripts/skill_tools/ — all tools clean.")
|
|
137
|
+
return
|
|
138
|
+
print(f"❌ scripts/skill_tools/ — {len(findings)} tool(s) with violations:")
|
|
139
|
+
for fp, viols in findings.items():
|
|
140
|
+
print(f" {fp}:")
|
|
141
|
+
for v in viols:
|
|
142
|
+
print(f" - {v}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main(argv: List[str] | None = None) -> int:
|
|
146
|
+
parser = argparse.ArgumentParser(
|
|
147
|
+
description="Lint scripts/skill_tools/*.py against Block D pilot invariants.",
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument("--json", action="store_true", help="emit JSON instead of text")
|
|
150
|
+
parser.add_argument("--quiet", action="store_true",
|
|
151
|
+
help="suppress the clean-pass success line (errors still print)")
|
|
152
|
+
parser.add_argument("--tools-dir", default=str(TOOLS_DIR),
|
|
153
|
+
help="directory to lint (default: scripts/skill_tools)")
|
|
154
|
+
args = parser.parse_args(argv)
|
|
155
|
+
|
|
156
|
+
code, findings = lint(Path(args.tools_dir))
|
|
157
|
+
if args.json:
|
|
158
|
+
json.dump({"exit_code": code, "findings": findings}, sys.stdout, indent=2)
|
|
159
|
+
sys.stdout.write("\n")
|
|
160
|
+
elif findings or not args.quiet:
|
|
161
|
+
_print_human(findings)
|
|
162
|
+
return code
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
_SAMPLE = {"violations": ["stdlib-only: imports `requests` (third-party)"]}
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
raise SystemExit(main())
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"$comment": "Source: agents/docs/frontmatter-contract.md#skills. Keep in sync with inventory_frontmatter.py and the linter's lint_skill().",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
|
-
"required": ["name", "description", "source"],
|
|
8
|
+
"required": ["name", "description", "source", "domain"],
|
|
9
9
|
"properties": {
|
|
10
10
|
"name": {
|
|
11
11
|
"type": "string",
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
"type": "string",
|
|
23
23
|
"enum": ["package", "project"]
|
|
24
24
|
},
|
|
25
|
+
"domain": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["engineering", "product", "quality", "devops", "process", "discovery"],
|
|
28
|
+
"description": "Skill classification axis; see docs/contracts/skill-domains.md. Required for every skill — exactly one of: engineering, product, quality, devops, process, discovery."
|
|
29
|
+
},
|
|
25
30
|
"status": {
|
|
26
31
|
"type": "string",
|
|
27
32
|
"enum": ["active", "deprecated", "superseded"]
|
package/scripts/skill_linter.py
CHANGED
|
@@ -41,15 +41,22 @@ from validate_frontmatter import ( # noqa: E402
|
|
|
41
41
|
Severity = Literal["error", "warning", "info"]
|
|
42
42
|
ArtifactType = Literal["skill", "rule", "command", "guideline", "persona", "unknown"]
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
REQUIRED_PERSONA_SECTIONS_CORE = [
|
|
45
45
|
"Focus",
|
|
46
46
|
"Mindset",
|
|
47
47
|
"Unique Questions",
|
|
48
48
|
"Output Expectations",
|
|
49
49
|
"Anti-Patterns",
|
|
50
50
|
]
|
|
51
|
+
REQUIRED_PERSONA_SECTIONS_SPECIALIST = REQUIRED_PERSONA_SECTIONS_CORE + [
|
|
52
|
+
"Critical Rules",
|
|
53
|
+
"Workflows",
|
|
54
|
+
]
|
|
55
|
+
# Back-compat alias — used by tier-agnostic callers; defaults to the core spine.
|
|
56
|
+
REQUIRED_PERSONA_SECTIONS = REQUIRED_PERSONA_SECTIONS_CORE
|
|
51
57
|
VALID_PERSONA_TIERS = {"core", "specialist"}
|
|
52
|
-
|
|
58
|
+
# Locked in docs/contracts/persona-schema.md § 4: core ≤ 120, specialist ≤ 100.
|
|
59
|
+
PERSONA_LINE_BUDGETS = {"core": 120, "specialist": 100}
|
|
53
60
|
|
|
54
61
|
|
|
55
62
|
REQUIRED_SKILL_SECTIONS = [
|
|
@@ -1437,9 +1444,17 @@ def lint_persona(path: Path, text: str) -> LintResult:
|
|
|
1437
1444
|
f"Persona description is {len(parsed['description'])} chars (target ≤ 160)",
|
|
1438
1445
|
))
|
|
1439
1446
|
|
|
1440
|
-
# Required sections
|
|
1447
|
+
# Required sections — tier-aware (per docs/contracts/persona-schema.md § 3).
|
|
1448
|
+
# Core: 5 sections. Specialist: Core-5 + Critical Rules + Workflows.
|
|
1441
1449
|
sections = extract_sections(text)
|
|
1442
|
-
|
|
1450
|
+
tier = parsed.get("tier")
|
|
1451
|
+
if tier == "specialist":
|
|
1452
|
+
required_sections = REQUIRED_PERSONA_SECTIONS_SPECIALIST
|
|
1453
|
+
else:
|
|
1454
|
+
# Default to core sections when tier is missing or invalid; the
|
|
1455
|
+
# tier-enum check above already raised an error in that case.
|
|
1456
|
+
required_sections = REQUIRED_PERSONA_SECTIONS_CORE
|
|
1457
|
+
for required_section in required_sections:
|
|
1443
1458
|
if required_section not in sections:
|
|
1444
1459
|
issues.append(Issue(
|
|
1445
1460
|
"error",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Block D pilot tools for skill discovery and persona auditing.
|
|
2
|
+
|
|
3
|
+
These four tools (D1 meta-linter + D2/D3/D4 functional pilots) ship under
|
|
4
|
+
this subdirectory per Block D council verdict (D-OQ2 → b). Each tool is:
|
|
5
|
+
|
|
6
|
+
- stdlib-only (no third-party imports)
|
|
7
|
+
- exposes ``--help`` and ``--json`` flags
|
|
8
|
+
- named ``snake_case_verb_noun.py``
|
|
9
|
+
- embeds sample data so it runs without external fixtures
|
|
10
|
+
|
|
11
|
+
Layout chosen to make the pilot reversible. If the D5 eval gate fails
|
|
12
|
+
(< 2 of 3 functional tools pass), the directory and its CI hook can be
|
|
13
|
+
removed in a single commit without touching the wider ``scripts/`` tree.
|
|
14
|
+
|
|
15
|
+
Public entry points:
|
|
16
|
+
|
|
17
|
+
- ``scripts/lint_skill_tools.py`` — D1 meta-linter (lives at scripts/ root
|
|
18
|
+
so ``task ci`` picks it up like other linters).
|
|
19
|
+
- ``score_skill_relevance`` — D2 (this dir).
|
|
20
|
+
- ``audit_persona_coverage`` — D3 (this dir).
|
|
21
|
+
- ``suggest_skill_for_task`` — D4 (this dir).
|
|
22
|
+
"""
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Block D · D3 — audit_persona_coverage.
|
|
3
|
+
|
|
4
|
+
Build a citation matrix of personas across the SKILL.md corpus and flag
|
|
5
|
+
under-cited personas using **tier-aware thresholds** (council iter-1
|
|
6
|
+
D-OQ4 verdict):
|
|
7
|
+
|
|
8
|
+
- **specialist** persona < 3 citations → under-cited
|
|
9
|
+
- **core** persona < 5 citations → under-cited
|
|
10
|
+
|
|
11
|
+
Inputs:
|
|
12
|
+
--skills-dir DIR — directory holding SKILL.md files
|
|
13
|
+
--personas-dir DIR — directory holding persona Markdown files
|
|
14
|
+
--json — machine-readable output
|
|
15
|
+
|
|
16
|
+
Output: per-persona citation count + tier + status (ok / under-cited / orphan).
|
|
17
|
+
Exit code: 0 always (this is an advisory tool, not a CI gate).
|
|
18
|
+
|
|
19
|
+
Stdlib-only. ≤ 120 LOC. Embedded `_SAMPLE` for self-demo.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Dict, List
|
|
29
|
+
|
|
30
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
31
|
+
DEFAULT_SKILLS = ROOT / ".agent-src.uncompressed" / "skills"
|
|
32
|
+
DEFAULT_PERSONAS = ROOT / ".agent-src.uncompressed" / "personas"
|
|
33
|
+
THRESHOLDS = {"core": 5, "specialist": 3}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _read_block(path: Path) -> str:
|
|
37
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
38
|
+
if not text.startswith("---"):
|
|
39
|
+
return ""
|
|
40
|
+
end = text.find("\n---", 3)
|
|
41
|
+
return text[3:end] if end != -1 else ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _frontmatter_value(block: str, key: str) -> str | None:
|
|
45
|
+
m = re.search(rf"^{re.escape(key)}\s*:\s*(.+)$", block, re.MULTILINE)
|
|
46
|
+
if not m:
|
|
47
|
+
return None
|
|
48
|
+
val = m.group(1).strip()
|
|
49
|
+
if val.startswith('"') and val.endswith('"'):
|
|
50
|
+
val = val[1:-1]
|
|
51
|
+
return val
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _frontmatter_list(block: str, key: str) -> List[str]:
|
|
55
|
+
m = re.search(rf"^{re.escape(key)}\s*:\s*$", block, re.MULTILINE)
|
|
56
|
+
if not m:
|
|
57
|
+
return []
|
|
58
|
+
items: List[str] = []
|
|
59
|
+
for line in block[m.end():].splitlines():
|
|
60
|
+
if line.startswith(" - "):
|
|
61
|
+
items.append(line[4:].strip())
|
|
62
|
+
elif line and not line.startswith(" "):
|
|
63
|
+
break
|
|
64
|
+
return items
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_personas(personas_dir: Path) -> Dict[str, str]:
|
|
68
|
+
"""slug → tier (core | specialist | unknown)."""
|
|
69
|
+
personas: Dict[str, str] = {}
|
|
70
|
+
if not personas_dir.is_dir():
|
|
71
|
+
return personas
|
|
72
|
+
for md in sorted(personas_dir.glob("*.md")):
|
|
73
|
+
if md.name.lower() == "readme.md":
|
|
74
|
+
continue
|
|
75
|
+
block = _read_block(md)
|
|
76
|
+
slug = _frontmatter_value(block, "id") or md.stem
|
|
77
|
+
tier = _frontmatter_value(block, "tier") or "unknown"
|
|
78
|
+
personas[slug] = tier
|
|
79
|
+
return personas
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _count_citations(skills_dir: Path) -> Dict[str, int]:
|
|
83
|
+
counts: Dict[str, int] = {}
|
|
84
|
+
if not skills_dir.is_dir():
|
|
85
|
+
return counts
|
|
86
|
+
for skill_md in skills_dir.glob("*/SKILL.md"):
|
|
87
|
+
block = _read_block(skill_md)
|
|
88
|
+
for slug in _frontmatter_list(block, "personas"):
|
|
89
|
+
counts[slug] = counts.get(slug, 0) + 1
|
|
90
|
+
return counts
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def audit(skills_dir: Path, personas_dir: Path) -> List[Dict[str, object]]:
|
|
94
|
+
personas = _load_personas(personas_dir)
|
|
95
|
+
citations = _count_citations(skills_dir)
|
|
96
|
+
rows: List[Dict[str, object]] = []
|
|
97
|
+
for slug, tier in sorted(personas.items()):
|
|
98
|
+
count = citations.get(slug, 0)
|
|
99
|
+
threshold = THRESHOLDS.get(tier, 3)
|
|
100
|
+
status = "under-cited" if count < threshold else "ok"
|
|
101
|
+
rows.append({"persona": slug, "tier": tier, "citations": count,
|
|
102
|
+
"threshold": threshold, "status": status})
|
|
103
|
+
# Surface citations that point at unknown personas (typos, deletions).
|
|
104
|
+
for slug in sorted(citations.keys()):
|
|
105
|
+
if slug not in personas:
|
|
106
|
+
rows.append({"persona": slug, "tier": "unknown",
|
|
107
|
+
"citations": citations[slug], "threshold": 0,
|
|
108
|
+
"status": "orphan"})
|
|
109
|
+
return rows
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _print_human(rows: List[Dict[str, object]]) -> None:
|
|
113
|
+
if not rows:
|
|
114
|
+
print("(no personas found)")
|
|
115
|
+
return
|
|
116
|
+
width = max(len(str(r["persona"])) for r in rows)
|
|
117
|
+
print(f" {'persona':<{width}} tier cites status")
|
|
118
|
+
print(f" {'-' * width} ---------- ----- -----------")
|
|
119
|
+
for r in rows:
|
|
120
|
+
print(f" {str(r['persona']):<{width}} {str(r['tier']):<10} "
|
|
121
|
+
f"{int(r['citations']):>5} {r['status']}")
|
|
122
|
+
flagged = [r for r in rows if r["status"] != "ok"]
|
|
123
|
+
if flagged:
|
|
124
|
+
print(f"\n {len(flagged)} persona(s) flagged "
|
|
125
|
+
f"(under-cited or orphan).")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def main(argv: List[str] | None = None) -> int:
|
|
129
|
+
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
130
|
+
parser.add_argument("--skills-dir", default=str(DEFAULT_SKILLS))
|
|
131
|
+
parser.add_argument("--personas-dir", default=str(DEFAULT_PERSONAS))
|
|
132
|
+
parser.add_argument("--json", action="store_true",
|
|
133
|
+
help="emit JSON instead of text")
|
|
134
|
+
args = parser.parse_args(argv)
|
|
135
|
+
rows = audit(Path(args.skills_dir), Path(args.personas_dir))
|
|
136
|
+
if args.json:
|
|
137
|
+
json.dump({"rows": rows}, sys.stdout, indent=2)
|
|
138
|
+
sys.stdout.write("\n")
|
|
139
|
+
else:
|
|
140
|
+
_print_human(rows)
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
_SAMPLE = {"thresholds": THRESHOLDS}
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
raise SystemExit(main())
|