@event4u/agent-config 1.33.0 → 1.35.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/commands/roadmap/process-full.md +17 -15
- package/.agent-src/contexts/execution/roadmap-process-loop.md +11 -10
- 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/discovery-lead.md +99 -0
- package/.agent-src/personas/eloquent-tamer.md +96 -0
- package/.agent-src/personas/frontend-engineer.md +100 -0
- package/.agent-src/personas/product-owner.md +71 -52
- package/.agent-src/personas/qa.md +27 -2
- package/.agent-src/personas/revops-maintainer.md +100 -0
- package/.agent-src/personas/security-engineer.md +100 -0
- package/.agent-src/personas/tech-writer.md +99 -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/competitive-positioning/SKILL.md +152 -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/customer-research/SKILL.md +116 -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 +218 -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/discovery-interview/SKILL.md +152 -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/launch-readiness/SKILL.md +156 -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/release-comms/SKILL.md +123 -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 +2 -1
- 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 +237 -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/voc-extract/SKILL.md +164 -0
- package/.agent-src/skills/websocket/SKILL.md +1 -0
- package/.agent-src/templates/roadmaps.md +9 -0
- package/.claude-plugin/marketplace.json +21 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +75 -0
- package/README.md +2 -2
- package/docs/architecture.md +2 -2
- package/docs/catalog.md +21 -4
- package/docs/contracts/context-spine.md +133 -0
- package/docs/contracts/file-ownership-matrix.json +616 -0
- package/docs/contracts/mental-models.md +336 -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/guidelines/cross-role-handoff.md +127 -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_context_spine_usage.py +133 -0
- package/scripts/lint_roadmap_complexity.py +37 -0
- package/scripts/lint_skill_tools.py +168 -0
- package/scripts/schemas/skill.schema.json +15 -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,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())
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Block D · D5 — eval gate runner.
|
|
3
|
+
|
|
4
|
+
Runs D2 (`score_skill_relevance`), D3 (`audit_persona_coverage`), and
|
|
5
|
+
D4 (`suggest_skill_for_task`) against the corpora in
|
|
6
|
+
`agents/eval-corpora/block-d/` and emits a pass/fail summary per the
|
|
7
|
+
council verdict targets:
|
|
8
|
+
|
|
9
|
+
- **D2**: ≥ 85 % of corpus tasks have an `expected_top3` skill in
|
|
10
|
+
the actual top-3 ranking.
|
|
11
|
+
- **D3**: ≥ 2 personas flagged as `under-cited`.
|
|
12
|
+
- **D4**: ≥ 3 / 5 blind tasks where suggestion #1 matches the
|
|
13
|
+
human-curated top-1.
|
|
14
|
+
|
|
15
|
+
Pilot pass = ≥ 2 / 3 tools pass. Anything less → kill switch.
|
|
16
|
+
|
|
17
|
+
Stdlib-only. Embedded `_SAMPLE` for self-demo.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Dict, List
|
|
26
|
+
|
|
27
|
+
from skill_tools.audit_persona_coverage import audit # type: ignore
|
|
28
|
+
from skill_tools.score_skill_relevance import ( # type: ignore
|
|
29
|
+
DEFAULT_SKILLS_DIR,
|
|
30
|
+
rank,
|
|
31
|
+
)
|
|
32
|
+
from skill_tools.suggest_skill_for_task import suggest # type: ignore
|
|
33
|
+
|
|
34
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
35
|
+
CORPUS_DIR = ROOT / "agents" / "eval-corpora" / "block-d"
|
|
36
|
+
PERSONAS_DIR = ROOT / ".agent-src.uncompressed" / "personas"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _eval_d2(corpus: Path, skills_dir: Path) -> Dict[str, object]:
|
|
40
|
+
data = json.loads(corpus.read_text(encoding="utf-8"))
|
|
41
|
+
tasks = data["tasks"]
|
|
42
|
+
hits, misses = 0, []
|
|
43
|
+
for t in tasks:
|
|
44
|
+
ranked = rank(t["task"], skills_dir)[:3]
|
|
45
|
+
names = [n for n, _, _ in ranked]
|
|
46
|
+
if any(e in names for e in t["expected_top3"]):
|
|
47
|
+
hits += 1
|
|
48
|
+
else:
|
|
49
|
+
misses.append({"id": t["id"], "expected": t["expected_top3"],
|
|
50
|
+
"got": names})
|
|
51
|
+
pct = hits / len(tasks) if tasks else 0.0
|
|
52
|
+
return {"hits": hits, "total": len(tasks), "pct": round(pct, 3),
|
|
53
|
+
"passed": pct >= 0.85, "misses": misses}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _eval_d3(skills_dir: Path, personas_dir: Path) -> Dict[str, object]:
|
|
57
|
+
rows = audit(skills_dir, personas_dir)
|
|
58
|
+
flagged = [r["persona"] for r in rows if r["status"] == "under-cited"]
|
|
59
|
+
return {"flagged": flagged, "count": len(flagged),
|
|
60
|
+
"passed": len(flagged) >= 2}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _eval_d4(corpus: Path, skills_dir: Path,
|
|
64
|
+
personas_dir: Path) -> Dict[str, object]:
|
|
65
|
+
data = json.loads(corpus.read_text(encoding="utf-8"))
|
|
66
|
+
tasks = data["tasks"]
|
|
67
|
+
hits, misses = 0, []
|
|
68
|
+
for t in tasks:
|
|
69
|
+
out = suggest(t["task"], skills_dir, personas_dir, top=1)
|
|
70
|
+
got = out[0]["skill"] if out else None
|
|
71
|
+
if got == t["expected_top1"]:
|
|
72
|
+
hits += 1
|
|
73
|
+
else:
|
|
74
|
+
misses.append({"id": t["id"], "expected": t["expected_top1"],
|
|
75
|
+
"got": got})
|
|
76
|
+
return {"hits": hits, "total": len(tasks),
|
|
77
|
+
"passed": hits >= 3, "misses": misses}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_all(skills_dir: Path, personas_dir: Path,
|
|
81
|
+
corpus_dir: Path) -> Dict[str, object]:
|
|
82
|
+
d2 = _eval_d2(corpus_dir / "d2-tasks.json", skills_dir)
|
|
83
|
+
d3 = _eval_d3(skills_dir, personas_dir)
|
|
84
|
+
d4 = _eval_d4(corpus_dir / "d4-tasks.json", skills_dir, personas_dir)
|
|
85
|
+
passes = sum(1 for r in (d2, d3, d4) if r["passed"])
|
|
86
|
+
return {"D2": d2, "D3": d3, "D4": d4,
|
|
87
|
+
"tools_passed": passes,
|
|
88
|
+
"pilot_passed": passes >= 2}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _print_human(report: Dict[str, object]) -> None:
|
|
92
|
+
icons = {True: "✅", False: "❌"}
|
|
93
|
+
for key in ("D2", "D3", "D4"):
|
|
94
|
+
r: Dict[str, object] = report[key] # type: ignore[assignment]
|
|
95
|
+
print(f" {icons[bool(r['passed'])]} {key}: {_summary(key, r)}")
|
|
96
|
+
overall = bool(report["pilot_passed"])
|
|
97
|
+
print(f"\n pilot: {report['tools_passed']}/3 tools passed → "
|
|
98
|
+
f"{'PASS' if overall else 'FAIL'}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _summary(key: str, r: Dict[str, object]) -> str:
|
|
102
|
+
if key == "D2":
|
|
103
|
+
return f"{r['hits']}/{r['total']} ({float(r['pct']) * 100:.0f}%) ≥ 85% target"
|
|
104
|
+
if key == "D3":
|
|
105
|
+
return f"{r['count']} under-cited personas (≥ 2 target)"
|
|
106
|
+
return f"{r['hits']}/{r['total']} top-1 hits (≥ 3/5 target)"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main(argv: List[str] | None = None) -> int:
|
|
110
|
+
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
111
|
+
parser.add_argument("--skills-dir", default=str(DEFAULT_SKILLS_DIR))
|
|
112
|
+
parser.add_argument("--personas-dir", default=str(PERSONAS_DIR))
|
|
113
|
+
parser.add_argument("--corpus-dir", default=str(CORPUS_DIR))
|
|
114
|
+
parser.add_argument("--json", action="store_true")
|
|
115
|
+
args = parser.parse_args(argv)
|
|
116
|
+
report = run_all(Path(args.skills_dir), Path(args.personas_dir),
|
|
117
|
+
Path(args.corpus_dir))
|
|
118
|
+
if args.json:
|
|
119
|
+
json.dump(report, sys.stdout, indent=2)
|
|
120
|
+
sys.stdout.write("\n")
|
|
121
|
+
else:
|
|
122
|
+
_print_human(report)
|
|
123
|
+
return 0 if report["pilot_passed"] else 1
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
_SAMPLE = {"corpus_dir": str(CORPUS_DIR)}
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Block D · D2 — score_skill_relevance.
|
|
3
|
+
|
|
4
|
+
Rank skills by relevance to a free-form task description.
|
|
5
|
+
|
|
6
|
+
Heuristic (council iter-1 D-OQ1 verdict (b) — discovery-story tool 1):
|
|
7
|
+
|
|
8
|
+
score = keyword_overlap * 70 + persona_match * 30
|
|
9
|
+
|
|
10
|
+
where:
|
|
11
|
+
- keyword_overlap = |task_terms ∩ skill_terms| / |task_terms|
|
|
12
|
+
(skill_terms = tokens from `name` + `description`)
|
|
13
|
+
- persona_match = 1.0 if any persona on the skill is named or
|
|
14
|
+
role-mentioned in the task, else 0.0
|
|
15
|
+
|
|
16
|
+
Inputs:
|
|
17
|
+
--task TEXT — task description (required)
|
|
18
|
+
--skills-dir DIR — directory holding SKILL.md files (default: package skills)
|
|
19
|
+
--top N — emit only top-N ranked skills (default: all non-zero)
|
|
20
|
+
--json — machine-readable ranked output
|
|
21
|
+
|
|
22
|
+
Output: ranked list with integer scores 0–100, descending. Ties break on name.
|
|
23
|
+
|
|
24
|
+
Stdlib-only. ≤ 180 LOC. Embedded `_SAMPLE` for self-demo (`python3 -m … --json`).
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Dict, Iterable, List, Tuple
|
|
34
|
+
|
|
35
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
36
|
+
DEFAULT_SKILLS_DIR = ROOT / ".agent-src.uncompressed" / "skills"
|
|
37
|
+
TOKEN_RE = re.compile(r"[a-z][a-z0-9]+")
|
|
38
|
+
STOPWORDS = frozenset({
|
|
39
|
+
"the", "a", "an", "and", "or", "but", "of", "for", "with", "to", "in",
|
|
40
|
+
"on", "at", "by", "from", "as", "is", "are", "was", "were", "be", "been",
|
|
41
|
+
"this", "that", "these", "those", "it", "its", "use", "when", "even",
|
|
42
|
+
"via", "via:", "into", "onto", "use:", "skill", "skills", "task", "tasks",
|
|
43
|
+
"code", "file", "files", "doing", "make", "do", "go", "get", "set",
|
|
44
|
+
"not", "no", "yes", "any", "some", "all", "one", "two", "new", "old",
|
|
45
|
+
"user", "users", "our", "your", "their", "they", "we", "you", "i", "me",
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _tokenize(text: str) -> set:
|
|
50
|
+
return {t for t in TOKEN_RE.findall(text.lower()) if t not in STOPWORDS and len(t) > 2}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_frontmatter(path: Path) -> Dict[str, object]:
|
|
54
|
+
"""Minimal YAML-frontmatter reader (stdlib-only). Returns {} on parse miss."""
|
|
55
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
56
|
+
if not text.startswith("---"):
|
|
57
|
+
return {}
|
|
58
|
+
end = text.find("\n---", 3)
|
|
59
|
+
if end == -1:
|
|
60
|
+
return {}
|
|
61
|
+
block = text[3:end]
|
|
62
|
+
out: Dict[str, object] = {}
|
|
63
|
+
current_list_key: str | None = None
|
|
64
|
+
for raw in block.splitlines():
|
|
65
|
+
line = raw.rstrip()
|
|
66
|
+
if not line or line.startswith("#"):
|
|
67
|
+
continue
|
|
68
|
+
if current_list_key and line.startswith(" - "):
|
|
69
|
+
out.setdefault(current_list_key, []).append(line[4:].strip()) # type: ignore[union-attr]
|
|
70
|
+
continue
|
|
71
|
+
current_list_key = None
|
|
72
|
+
m = re.match(r"^([a-zA-Z_][\w-]*)\s*:\s*(.*)$", line)
|
|
73
|
+
if not m:
|
|
74
|
+
continue
|
|
75
|
+
key, val = m.group(1), m.group(2).strip()
|
|
76
|
+
if val == "":
|
|
77
|
+
current_list_key = key
|
|
78
|
+
continue
|
|
79
|
+
if val.startswith('"') and val.endswith('"'):
|
|
80
|
+
val = val[1:-1]
|
|
81
|
+
out[key] = val
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _load_skills(skills_dir: Path) -> List[Dict[str, object]]:
|
|
86
|
+
skills: List[Dict[str, object]] = []
|
|
87
|
+
for skill_md in sorted(skills_dir.glob("*/SKILL.md")):
|
|
88
|
+
fm = _parse_frontmatter(skill_md)
|
|
89
|
+
name = str(fm.get("name") or skill_md.parent.name)
|
|
90
|
+
desc = str(fm.get("description") or "")
|
|
91
|
+
personas = fm.get("personas") or []
|
|
92
|
+
if isinstance(personas, str):
|
|
93
|
+
personas = [personas]
|
|
94
|
+
skills.append({
|
|
95
|
+
"name": name,
|
|
96
|
+
"description": desc,
|
|
97
|
+
"personas": list(personas),
|
|
98
|
+
"terms": _tokenize(name + " " + desc),
|
|
99
|
+
})
|
|
100
|
+
return skills
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _score(task_terms: set, skill: Dict[str, object]) -> int:
|
|
104
|
+
if not task_terms:
|
|
105
|
+
return 0
|
|
106
|
+
skill_terms = skill["terms"] # type: ignore[index]
|
|
107
|
+
overlap = len(task_terms & skill_terms) / max(len(task_terms), 1) # type: ignore[arg-type]
|
|
108
|
+
persona_hit = 0.0
|
|
109
|
+
task_lower = " ".join(task_terms)
|
|
110
|
+
for persona in skill["personas"]: # type: ignore[union-attr]
|
|
111
|
+
slug = str(persona).lower()
|
|
112
|
+
if slug in task_lower or any(part in task_terms for part in slug.split("-")): # type: ignore[operator]
|
|
113
|
+
persona_hit = 1.0
|
|
114
|
+
break
|
|
115
|
+
return round(overlap * 70 + persona_hit * 30)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def rank(task: str, skills_dir: Path) -> List[Tuple[str, int, List[str]]]:
|
|
119
|
+
task_terms = _tokenize(task)
|
|
120
|
+
skills = _load_skills(skills_dir)
|
|
121
|
+
rows: List[Tuple[str, int, List[str]]] = []
|
|
122
|
+
for s in skills:
|
|
123
|
+
score = _score(task_terms, s)
|
|
124
|
+
if score > 0:
|
|
125
|
+
rows.append((str(s["name"]), score, list(s["personas"]))) # type: ignore[arg-type]
|
|
126
|
+
rows.sort(key=lambda r: (-r[1], r[0]))
|
|
127
|
+
return rows
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _print_human(rows: Iterable[Tuple[str, int, List[str]]], top: int | None) -> None:
|
|
131
|
+
rows = list(rows)
|
|
132
|
+
if top:
|
|
133
|
+
rows = rows[:top]
|
|
134
|
+
if not rows:
|
|
135
|
+
print("(no relevant skills found)")
|
|
136
|
+
return
|
|
137
|
+
width = max(len(r[0]) for r in rows)
|
|
138
|
+
for name, score, personas in rows:
|
|
139
|
+
persona_str = ", ".join(personas) if personas else "—"
|
|
140
|
+
print(f" {score:3d} {name:<{width}} {persona_str}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main(argv: List[str] | None = None) -> int:
|
|
144
|
+
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
145
|
+
parser.add_argument("--task", required=False, default="",
|
|
146
|
+
help="task description (required unless --sample is used)")
|
|
147
|
+
parser.add_argument("--skills-dir", default=str(DEFAULT_SKILLS_DIR),
|
|
148
|
+
help="directory holding SKILL.md files")
|
|
149
|
+
parser.add_argument("--top", type=int, default=0, help="emit only top-N rows")
|
|
150
|
+
parser.add_argument("--json", action="store_true", help="emit JSON instead of text")
|
|
151
|
+
parser.add_argument("--sample", action="store_true", help="run against the embedded sample task")
|
|
152
|
+
args = parser.parse_args(argv)
|
|
153
|
+
task = _SAMPLE["task"] if args.sample else args.task
|
|
154
|
+
if not task:
|
|
155
|
+
parser.error("--task is required (or pass --sample)")
|
|
156
|
+
rows = rank(task, Path(args.skills_dir))
|
|
157
|
+
if args.json:
|
|
158
|
+
payload = [{"name": n, "score": s, "personas": p} for n, s, p in (rows[:args.top] if args.top else rows)]
|
|
159
|
+
json.dump({"task": task, "ranked": payload}, sys.stdout, indent=2)
|
|
160
|
+
sys.stdout.write("\n")
|
|
161
|
+
else:
|
|
162
|
+
_print_human(rows, args.top or None)
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
_SAMPLE = {"task": "build a livewire component for the user dashboard with reactive state"}
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Block D · D4 — suggest_skill_for_task.
|
|
3
|
+
|
|
4
|
+
CLI wrapper that combines D2 (`score_skill_relevance`) with the persona
|
|
5
|
+
matrix from D3 (`audit_persona_coverage`) and emits the top-3 skill +
|
|
6
|
+
persona combos with a one-line justification each.
|
|
7
|
+
|
|
8
|
+
Inputs:
|
|
9
|
+
--task TEXT — task description (required)
|
|
10
|
+
--skills-dir DIR — SKILL.md directory
|
|
11
|
+
--personas-dir DIR — persona Markdown directory
|
|
12
|
+
--top N — emit top-N combos (default: 3)
|
|
13
|
+
--json — machine-readable output
|
|
14
|
+
|
|
15
|
+
Output: ranked combos with `skill`, `score`, `personas[]`, and `why`.
|
|
16
|
+
|
|
17
|
+
Stdlib-only. ≤ 100 LOC. Embedded `_SAMPLE` for self-demo via `--sample`.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Dict, List
|
|
26
|
+
|
|
27
|
+
from skill_tools.audit_persona_coverage import audit # type: ignore
|
|
28
|
+
from skill_tools.score_skill_relevance import ( # type: ignore
|
|
29
|
+
DEFAULT_SKILLS_DIR,
|
|
30
|
+
rank,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
34
|
+
DEFAULT_PERSONAS = ROOT / ".agent-src.uncompressed" / "personas"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _persona_status(rows: List[Dict[str, object]]) -> Dict[str, str]:
|
|
38
|
+
return {str(r["persona"]): str(r["status"]) for r in rows}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _justify(name: str, score: int, personas: List[str],
|
|
42
|
+
status: Dict[str, str]) -> str:
|
|
43
|
+
if score >= 70:
|
|
44
|
+
head = "high keyword + persona match"
|
|
45
|
+
elif score >= 40:
|
|
46
|
+
head = "strong keyword overlap"
|
|
47
|
+
else:
|
|
48
|
+
head = "partial overlap — confirm with reviewer"
|
|
49
|
+
if personas:
|
|
50
|
+
tier_hits = ", ".join(
|
|
51
|
+
f"{p} ({status.get(p, 'unknown')})" for p in personas
|
|
52
|
+
)
|
|
53
|
+
return f"{head}; lenses: {tier_hits}"
|
|
54
|
+
return f"{head}; no persona declared on `{name}`"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def suggest(task: str, skills_dir: Path, personas_dir: Path,
|
|
58
|
+
top: int = 3) -> List[Dict[str, object]]:
|
|
59
|
+
ranked = rank(task, skills_dir)[:top]
|
|
60
|
+
persona_rows = audit(skills_dir, personas_dir)
|
|
61
|
+
status = _persona_status(persona_rows)
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
"skill": name,
|
|
65
|
+
"score": score,
|
|
66
|
+
"personas": personas,
|
|
67
|
+
"why": _justify(name, score, personas, status),
|
|
68
|
+
}
|
|
69
|
+
for name, score, personas in ranked
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _print_human(combos: List[Dict[str, object]]) -> None:
|
|
74
|
+
if not combos:
|
|
75
|
+
print("(no skill suggestions for this task)")
|
|
76
|
+
return
|
|
77
|
+
for i, c in enumerate(combos, 1):
|
|
78
|
+
personas = ", ".join(c["personas"]) if c["personas"] else "—" # type: ignore[arg-type]
|
|
79
|
+
print(f" {i}. {c['skill']} ({c['score']}/100)")
|
|
80
|
+
print(f" personas: {personas}")
|
|
81
|
+
print(f" why: {c['why']}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main(argv: List[str] | None = None) -> int:
|
|
85
|
+
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
86
|
+
parser.add_argument("--task", default="",
|
|
87
|
+
help="task description (required unless --sample)")
|
|
88
|
+
parser.add_argument("--skills-dir", default=str(DEFAULT_SKILLS_DIR))
|
|
89
|
+
parser.add_argument("--personas-dir", default=str(DEFAULT_PERSONAS))
|
|
90
|
+
parser.add_argument("--top", type=int, default=3)
|
|
91
|
+
parser.add_argument("--json", action="store_true",
|
|
92
|
+
help="emit JSON instead of text")
|
|
93
|
+
parser.add_argument("--sample", action="store_true",
|
|
94
|
+
help="run against the embedded sample task")
|
|
95
|
+
args = parser.parse_args(argv)
|
|
96
|
+
task = _SAMPLE["task"] if args.sample else args.task
|
|
97
|
+
if not task:
|
|
98
|
+
parser.error("--task is required (or pass --sample)")
|
|
99
|
+
combos = suggest(task, Path(args.skills_dir),
|
|
100
|
+
Path(args.personas_dir), args.top)
|
|
101
|
+
if args.json:
|
|
102
|
+
json.dump({"task": task, "suggestions": combos},
|
|
103
|
+
sys.stdout, indent=2)
|
|
104
|
+
sys.stdout.write("\n")
|
|
105
|
+
else:
|
|
106
|
+
_print_human(combos)
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_SAMPLE = {"task": "review a livewire component for accessibility and reactive state"}
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
raise SystemExit(main())
|