harness-eval 5.0.0__py3-none-any.whl

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 (142) hide show
  1. harness_eval/__init__.py +3 -0
  2. harness_eval/analysis/__init__.py +0 -0
  3. harness_eval/analysis/budget.py +74 -0
  4. harness_eval/analysis/context_utilization.py +90 -0
  5. harness_eval/analysis/dependencies.py +111 -0
  6. harness_eval/analysis/system.py +95 -0
  7. harness_eval/analysis/triggers.py +63 -0
  8. harness_eval/analysis/types.py +40 -0
  9. harness_eval/cli/__init__.py +13 -0
  10. harness_eval/cli/_helpers.py +14 -0
  11. harness_eval/cli/lint.py +223 -0
  12. harness_eval/cli/review.py +166 -0
  13. harness_eval/cli/security.py +446 -0
  14. harness_eval/cli/skill.py +229 -0
  15. harness_eval/config/__init__.py +0 -0
  16. harness_eval/config/presets.py +214 -0
  17. harness_eval/core/__init__.py +0 -0
  18. harness_eval/core/discoverers/__init__.py +5 -0
  19. harness_eval/core/discoverers/base.py +62 -0
  20. harness_eval/core/discoverers/claude.py +264 -0
  21. harness_eval/core/discoverers/copilot.py +97 -0
  22. harness_eval/core/discoverers/cursor.py +154 -0
  23. harness_eval/core/discoverers/gemini.py +64 -0
  24. harness_eval/core/discoverers/opencode.py +88 -0
  25. harness_eval/core/discoverers/registry.py +25 -0
  26. harness_eval/core/discoverers/third_party.py +89 -0
  27. harness_eval/core/fingerprint.py +89 -0
  28. harness_eval/core/setup.py +138 -0
  29. harness_eval/core/types.py +53 -0
  30. harness_eval/data/__init__.py +25 -0
  31. harness_eval/data/builtins.json +8 -0
  32. harness_eval/data/tautological_patterns.json +29 -0
  33. harness_eval/inspection/__init__.py +3 -0
  34. harness_eval/inspection/engine.py +728 -0
  35. harness_eval/inspection/fixer.py +66 -0
  36. harness_eval/inspection/parsers.py +340 -0
  37. harness_eval/inspection/registry.py +37 -0
  38. harness_eval/inspection/rules/__init__.py +184 -0
  39. harness_eval/inspection/rules/agents/__init__.py +0 -0
  40. harness_eval/inspection/rules/agents/constraint_body_match.py +95 -0
  41. harness_eval/inspection/rules/agents/data_exfiltration.py +43 -0
  42. harness_eval/inspection/rules/agents/description_required.py +42 -0
  43. harness_eval/inspection/rules/agents/disallowed_tools_parseable.py +47 -0
  44. harness_eval/inspection/rules/agents/model_specified.py +46 -0
  45. harness_eval/inspection/rules/agents/no_credential_access.py +48 -0
  46. harness_eval/inspection/rules/agents/no_prompt_injection.py +45 -0
  47. harness_eval/inspection/rules/agents/obfuscation_detection.py +43 -0
  48. harness_eval/inspection/rules/agents/referenced_skills_exist.py +44 -0
  49. harness_eval/inspection/rules/agents/reverse_shell_detection.py +45 -0
  50. harness_eval/inspection/rules/claude_md/__init__.py +0 -0
  51. harness_eval/inspection/rules/claude_md/exists.py +39 -0
  52. harness_eval/inspection/rules/claude_md/generic_advice.py +61 -0
  53. harness_eval/inspection/rules/claude_md/skill_duplication.py +58 -0
  54. harness_eval/inspection/rules/commands/__init__.py +0 -0
  55. harness_eval/inspection/rules/commands/data_exfiltration.py +43 -0
  56. harness_eval/inspection/rules/commands/description_required.py +45 -0
  57. harness_eval/inspection/rules/commands/duplicate_detection.py +64 -0
  58. harness_eval/inspection/rules/commands/no_credential_access.py +51 -0
  59. harness_eval/inspection/rules/commands/no_prompt_injection.py +45 -0
  60. harness_eval/inspection/rules/commands/obfuscation_detection.py +43 -0
  61. harness_eval/inspection/rules/commands/references_nonexistent_skill.py +59 -0
  62. harness_eval/inspection/rules/commands/reverse_shell_detection.py +45 -0
  63. harness_eval/inspection/rules/commands/script_exists.py +51 -0
  64. harness_eval/inspection/rules/commands/shadows_builtin.py +43 -0
  65. harness_eval/inspection/rules/commands/skill_overlap.py +54 -0
  66. harness_eval/inspection/rules/content/__init__.py +0 -0
  67. harness_eval/inspection/rules/content/broken_references.py +91 -0
  68. harness_eval/inspection/rules/content/circular_references.py +110 -0
  69. harness_eval/inspection/rules/content/duplicate_detection.py +65 -0
  70. harness_eval/inspection/rules/content/token_budget.py +74 -0
  71. harness_eval/inspection/rules/frontmatter/__init__.py +0 -0
  72. harness_eval/inspection/rules/frontmatter/description_quality.py +107 -0
  73. harness_eval/inspection/rules/frontmatter/description_required.py +40 -0
  74. harness_eval/inspection/rules/frontmatter/format_valid.py +54 -0
  75. harness_eval/inspection/rules/hooks/__init__.py +0 -0
  76. harness_eval/inspection/rules/hooks/dangerous_command.py +58 -0
  77. harness_eval/inspection/rules/hooks/env_leakage.py +63 -0
  78. harness_eval/inspection/rules/hooks/network_access.py +57 -0
  79. harness_eval/inspection/rules/hooks/script_boundary.py +70 -0
  80. harness_eval/inspection/rules/hooks/valid_structure.py +91 -0
  81. harness_eval/inspection/rules/mcp/__init__.py +0 -0
  82. harness_eval/inspection/rules/mcp/duplicate_server.py +65 -0
  83. harness_eval/inspection/rules/mcp/no_wildcard_tools.py +59 -0
  84. harness_eval/inspection/rules/mcp/suspicious_endpoint.py +73 -0
  85. harness_eval/inspection/rules/mcp/valid_config.py +106 -0
  86. harness_eval/inspection/rules/quality/__init__.py +0 -0
  87. harness_eval/inspection/rules/quality/_patterns.py +20 -0
  88. harness_eval/inspection/rules/quality/example_gap.py +80 -0
  89. harness_eval/inspection/rules/quality/imprecise_instruction.py +93 -0
  90. harness_eval/inspection/rules/quality/negative_only.py +94 -0
  91. harness_eval/inspection/rules/quality/redundant_guidance.py +216 -0
  92. harness_eval/inspection/rules/quality/scope_overreach.py +89 -0
  93. harness_eval/inspection/rules/quality/stale_references.py +134 -0
  94. harness_eval/inspection/rules/quality/trigger_manipulation.py +106 -0
  95. harness_eval/inspection/rules/quality/unfinished_content.py +176 -0
  96. harness_eval/inspection/rules/security/__init__.py +0 -0
  97. harness_eval/inspection/rules/security/_shared.py +141 -0
  98. harness_eval/inspection/rules/security/ast_behavioral.py +206 -0
  99. harness_eval/inspection/rules/security/bash_taint_tracking.py +168 -0
  100. harness_eval/inspection/rules/security/coercive_override.py +73 -0
  101. harness_eval/inspection/rules/security/cve_lookup.py +202 -0
  102. harness_eval/inspection/rules/security/data_exfiltration.py +57 -0
  103. harness_eval/inspection/rules/security/mcp_least_privilege.py +160 -0
  104. harness_eval/inspection/rules/security/mcp_tool_poisoning.py +210 -0
  105. harness_eval/inspection/rules/security/no_credential_access.py +78 -0
  106. harness_eval/inspection/rules/security/no_prompt_injection.py +91 -0
  107. harness_eval/inspection/rules/security/obfuscation_detection.py +57 -0
  108. harness_eval/inspection/rules/security/prompt_exfiltration.py +76 -0
  109. harness_eval/inspection/rules/security/reverse_shell_detection.py +56 -0
  110. harness_eval/inspection/rules/security/stealth_persistence.py +61 -0
  111. harness_eval/inspection/rules/security/taint_tracking.py +236 -0
  112. harness_eval/inspection/rules/security/yara_scan.py +160 -0
  113. harness_eval/inspection/rules/structural/__init__.py +0 -0
  114. harness_eval/inspection/rules/structural/skill_md_exists.py +34 -0
  115. harness_eval/inspection/suppression.py +87 -0
  116. harness_eval/inspection/types.py +218 -0
  117. harness_eval/output/__init__.py +0 -0
  118. harness_eval/output/metadata.py +60 -0
  119. harness_eval/output/report.py +353 -0
  120. harness_eval/output/sarif.py +83 -0
  121. harness_eval/py.typed +0 -0
  122. harness_eval/rubric/__init__.py +0 -0
  123. harness_eval/rubric/dimensions.py +158 -0
  124. harness_eval/rubric/prompts/adjudication.py +56 -0
  125. harness_eval/rubric/prompts/batch-template.md +37 -0
  126. harness_eval/rubric/prompts/issue-template.md +93 -0
  127. harness_eval/rubric/prompts/system.md +39 -0
  128. harness_eval/rubric/prompts.py +61 -0
  129. harness_eval/rubric/scorer.py +272 -0
  130. harness_eval/rubric/types.py +36 -0
  131. harness_eval/utils/__init__.py +0 -0
  132. harness_eval/utils/llm.py +93 -0
  133. harness_eval/utils/parsing.py +100 -0
  134. harness_eval/utils/paths.py +35 -0
  135. harness_eval/utils/similarity.py +17 -0
  136. harness_eval/utils/tokens.py +57 -0
  137. harness_eval/watch.py +135 -0
  138. harness_eval-5.0.0.dist-info/METADATA +124 -0
  139. harness_eval-5.0.0.dist-info/RECORD +142 -0
  140. harness_eval-5.0.0.dist-info/WHEEL +4 -0
  141. harness_eval-5.0.0.dist-info/entry_points.txt +2 -0
  142. harness_eval-5.0.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,3 @@
1
+ """harness-eval: Evaluate and compare AI agent setups."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,74 @@
1
+ """Token budget analysis for the setup as a whole."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from harness_eval.core.types import ComponentType, ParsedComponent, Setup
8
+
9
+
10
+ @dataclass
11
+ class BudgetReport:
12
+ """Token budget breakdown for a setup."""
13
+
14
+ total_tokens: int = 0
15
+ always_loaded_tokens: int = 0
16
+ on_demand_tokens: int = 0
17
+ always_loaded_ratio: float = 0.0
18
+ by_type: dict[str, int] = field(default_factory=dict)
19
+ by_component: list[tuple[str, str, int]] = field(default_factory=list)
20
+ heaviest_component_name: str = ""
21
+ heaviest_component_ratio: float = 0.0
22
+
23
+
24
+ _EXCLUDED_FROM_BUDGET = {ComponentType.UNCATEGORIZED}
25
+
26
+
27
+ def _is_always_loaded(comp: ParsedComponent) -> bool:
28
+ if comp.component_type == ComponentType.HOOKS:
29
+ return True
30
+ if comp.component_type != ComponentType.CLAUDE_MD:
31
+ return False
32
+ if comp.source_tool != "cursor":
33
+ return True
34
+ return bool(comp.frontmatter and comp.frontmatter.get("alwaysApply") is True)
35
+
36
+
37
+ def analyze_budget(setup: Setup) -> BudgetReport:
38
+ """Analyze token budget distribution across a setup."""
39
+ by_type: dict[str, int] = {}
40
+ by_component: list[tuple[str, str, int]] = []
41
+ always_loaded = 0
42
+ on_demand = 0
43
+ heaviest_name = ""
44
+ heaviest_tokens = 0
45
+
46
+ for comp in setup.components:
47
+ if comp.component_type in _EXCLUDED_FROM_BUDGET:
48
+ continue
49
+ type_key = comp.component_type.value
50
+ by_type[type_key] = by_type.get(type_key, 0) + comp.token_count
51
+ by_component.append((type_key, comp.name, comp.token_count))
52
+
53
+ if _is_always_loaded(comp):
54
+ always_loaded += comp.token_count
55
+ else:
56
+ on_demand += comp.token_count
57
+
58
+ if comp.token_count > heaviest_tokens:
59
+ heaviest_tokens = comp.token_count
60
+ heaviest_name = f"{type_key}/{comp.name}"
61
+
62
+ total = always_loaded + on_demand
63
+ by_component.sort(key=lambda x: x[2], reverse=True)
64
+
65
+ return BudgetReport(
66
+ total_tokens=total,
67
+ always_loaded_tokens=always_loaded,
68
+ on_demand_tokens=on_demand,
69
+ always_loaded_ratio=always_loaded / total if total else 0.0,
70
+ by_type=by_type,
71
+ by_component=by_component,
72
+ heaviest_component_name=heaviest_name,
73
+ heaviest_component_ratio=heaviest_tokens / total if total else 0.0,
74
+ )
@@ -0,0 +1,90 @@
1
+ """Context utilization analysis: how much of each model's context window the setup consumes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from harness_eval.analysis.budget import BudgetReport
8
+ from harness_eval.core.types import Setup
9
+
10
+
11
+ @dataclass
12
+ class ModelSpec:
13
+ """A model with a known context window size."""
14
+
15
+ name: str
16
+ context_window: int
17
+
18
+
19
+ @dataclass
20
+ class ModelUtilization:
21
+ """Context utilization for one model."""
22
+
23
+ model: str
24
+ context_window: int
25
+ always_loaded_pct: float
26
+ peak_load_pct: float
27
+ remaining_pct: float
28
+ warning: bool
29
+
30
+
31
+ @dataclass
32
+ class ContextUtilizationReport:
33
+ """Context utilization across all target models."""
34
+
35
+ always_loaded_tokens: int = 0
36
+ peak_tokens: int = 0
37
+ models: list[ModelUtilization] = field(default_factory=list)
38
+
39
+
40
+ PEAK_WARNING_THRESHOLD = 0.20
41
+ PEAK_CRITICAL_THRESHOLD = 0.50
42
+ ALWAYS_LOADED_WARNING_THRESHOLD = 0.10
43
+
44
+ DEFAULT_MODELS: list[ModelSpec] = [
45
+ ModelSpec("claude-haiku-4.5", 200_000),
46
+ ModelSpec("claude-sonnet-4.6", 200_000),
47
+ ModelSpec("claude-opus-4.6", 200_000),
48
+ ModelSpec("claude-opus-4.7", 200_000),
49
+ ModelSpec("claude-opus-4.8", 200_000),
50
+ ModelSpec("claude-opus-4.6-1m", 1_000_000),
51
+ ModelSpec("claude-opus-4.7-1m", 1_000_000),
52
+ ModelSpec("claude-opus-4.8-1m", 1_000_000),
53
+ ]
54
+
55
+
56
+ def analyze_context_utilization(
57
+ setup: Setup,
58
+ budget: BudgetReport,
59
+ models: list[ModelSpec] | None = None,
60
+ ) -> ContextUtilizationReport:
61
+ """Compute context window utilization per model."""
62
+ target_models = models if models is not None else DEFAULT_MODELS
63
+ always_loaded = budget.always_loaded_tokens
64
+ peak = budget.total_tokens
65
+
66
+ utilizations: list[ModelUtilization] = []
67
+ for spec in target_models:
68
+ if spec.context_window <= 0:
69
+ continue
70
+ al_pct = always_loaded / spec.context_window
71
+ peak_pct = peak / spec.context_window
72
+ remaining = max(0.0, 1.0 - peak_pct)
73
+ utilizations.append(
74
+ ModelUtilization(
75
+ model=spec.name,
76
+ context_window=spec.context_window,
77
+ always_loaded_pct=al_pct,
78
+ peak_load_pct=peak_pct,
79
+ remaining_pct=remaining,
80
+ warning=peak_pct > PEAK_WARNING_THRESHOLD,
81
+ )
82
+ )
83
+
84
+ utilizations.sort(key=lambda m: m.peak_load_pct, reverse=True)
85
+
86
+ return ContextUtilizationReport(
87
+ always_loaded_tokens=always_loaded,
88
+ peak_tokens=peak,
89
+ models=utilizations,
90
+ )
@@ -0,0 +1,111 @@
1
+ """Dependency mapping: which components reference which, and are any broken?"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass, field
7
+
8
+ from harness_eval.core.types import ComponentType, Setup
9
+
10
+
11
+ @dataclass
12
+ class DependencyEdge:
13
+ """A reference from one component to another."""
14
+
15
+ source_type: str
16
+ source_name: str
17
+ target_type: str
18
+ target_name: str
19
+ exists: bool
20
+
21
+
22
+ @dataclass
23
+ class DependencyReport:
24
+ """Dependency analysis results."""
25
+
26
+ edges: list[DependencyEdge] = field(default_factory=list)
27
+ broken_refs: list[DependencyEdge] = field(default_factory=list)
28
+ orphan_components: list[str] = field(default_factory=list)
29
+
30
+
31
+ _SKILL_REF_PATTERNS = [
32
+ re.compile(r"skills?[/\\](\w[\w-]*)"),
33
+ re.compile(r"/(\w[\w-]*)\s+skill"),
34
+ re.compile(r"skill\s+['\"](\w[\w-]*)['\"]"),
35
+ ]
36
+
37
+
38
+ def analyze_dependencies(setup: Setup) -> DependencyReport:
39
+ """Map dependencies between components and find broken references."""
40
+ known_skills = {c.name for c in setup.by_type(ComponentType.SKILL)}
41
+ known_commands = {c.name for c in setup.by_type(ComponentType.COMMAND)}
42
+ all_known = known_skills | known_commands
43
+
44
+ edges: list[DependencyEdge] = []
45
+
46
+ for comp in setup.by_type(ComponentType.AGENT):
47
+ if comp.frontmatter:
48
+ referenced = comp.frontmatter.get("skills", []) or []
49
+ if isinstance(referenced, str):
50
+ referenced = [s.strip() for s in referenced.split(",")]
51
+ for skill_name in referenced:
52
+ if not skill_name:
53
+ continue
54
+ exists = skill_name in known_skills
55
+ edges.append(
56
+ DependencyEdge(
57
+ source_type="agent",
58
+ source_name=comp.name,
59
+ target_type="skill",
60
+ target_name=skill_name,
61
+ exists=exists,
62
+ )
63
+ )
64
+
65
+ for comp in setup.components:
66
+ if comp.component_type in (ComponentType.MCP_CONFIG, ComponentType.HOOKS):
67
+ continue
68
+ for pattern in _SKILL_REF_PATTERNS:
69
+ for match in pattern.finditer(comp.content):
70
+ ref_name = match.group(1)
71
+ if ref_name in all_known and ref_name != comp.name:
72
+ edges.append(
73
+ DependencyEdge(
74
+ source_type=comp.component_type.value,
75
+ source_name=comp.name,
76
+ target_type="skill" if ref_name in known_skills else "command",
77
+ target_name=ref_name,
78
+ exists=True,
79
+ )
80
+ )
81
+
82
+ seen = set()
83
+ deduped: list[DependencyEdge] = []
84
+ for e in edges:
85
+ key = (e.source_type, e.source_name, e.target_type, e.target_name)
86
+ if key not in seen:
87
+ seen.add(key)
88
+ deduped.append(e)
89
+
90
+ broken = [e for e in deduped if not e.exists]
91
+
92
+ referenced_names = set()
93
+ for e in deduped:
94
+ referenced_names.add(e.target_name)
95
+ referenced_names.add(e.source_name)
96
+
97
+ orphans = []
98
+ # Skills are activated by description matching, not by explicit references,
99
+ # so an unreferenced skill is not orphaned. Only flag commands and agents.
100
+ orphan_types = (ComponentType.COMMAND, ComponentType.AGENT)
101
+ for comp in setup.components:
102
+ if comp.component_type not in orphan_types:
103
+ continue
104
+ if comp.name not in referenced_names and len(setup.components) > 3:
105
+ orphans.append(f"{comp.component_type.value}/{comp.name}")
106
+
107
+ return DependencyReport(
108
+ edges=deduped,
109
+ broken_refs=broken,
110
+ orphan_components=orphans,
111
+ )
@@ -0,0 +1,95 @@
1
+ """System-level analysis: evaluate the setup as a whole, not component-by-component."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from harness_eval.analysis.budget import BudgetReport, analyze_budget
8
+ from harness_eval.analysis.context_utilization import (
9
+ ContextUtilizationReport,
10
+ analyze_context_utilization,
11
+ )
12
+ from harness_eval.analysis.dependencies import DependencyReport, analyze_dependencies
13
+ from harness_eval.analysis.triggers import TriggerReport, analyze_triggers
14
+ from harness_eval.core.types import Setup
15
+
16
+
17
+ @dataclass
18
+ class SystemReport:
19
+ """Complete system-level analysis of a setup."""
20
+
21
+ setup_name: str
22
+ component_count: int
23
+ budget: BudgetReport
24
+ triggers: TriggerReport
25
+ dependencies: DependencyReport
26
+ context_utilization: ContextUtilizationReport
27
+ findings: list[str] = field(default_factory=list)
28
+ uncategorized_files: list[str] = field(default_factory=list)
29
+ detected_tools: tuple[str, ...] = ()
30
+
31
+
32
+ def analyze_system(setup: Setup) -> SystemReport:
33
+ """Run all system-level analyses on a setup."""
34
+ budget = analyze_budget(setup)
35
+ triggers = analyze_triggers(setup)
36
+ dependencies = analyze_dependencies(setup)
37
+ context_utilization = analyze_context_utilization(setup, budget)
38
+
39
+ findings: list[str] = []
40
+
41
+ tool_set = set(setup.detected_tools)
42
+ tool_set.discard("Third-party modules")
43
+ if not tool_set:
44
+ always_label = "system instructions"
45
+ elif tool_set == {"Cursor"}:
46
+ always_label = "cursor rules"
47
+ elif len(tool_set) > 1:
48
+ always_label = "system instructions"
49
+ elif "Gemini CLI" in tool_set:
50
+ always_label = "GEMINI.md"
51
+ elif "OpenCode" in tool_set:
52
+ always_label = "AGENTS.md"
53
+ elif "Copilot" in tool_set:
54
+ always_label = "system instructions"
55
+ else:
56
+ always_label = "CLAUDE.md"
57
+
58
+ if budget.always_loaded_ratio > 0.7:
59
+ findings.append(
60
+ f"Inverted budget: {budget.always_loaded_ratio:.0%} of tokens are "
61
+ f"always-loaded ({always_label}). Most content should be in on-demand skills."
62
+ )
63
+ elif budget.always_loaded_ratio > 0.5:
64
+ findings.append(
65
+ f"Heavy always-loaded ratio: {budget.always_loaded_ratio:.0%} of tokens "
66
+ f"are always-loaded. Consider moving content to on-demand skills."
67
+ )
68
+
69
+ if budget.heaviest_component_ratio > 0.5:
70
+ findings.append(
71
+ f"One component ({budget.heaviest_component_name}) uses "
72
+ f"{budget.heaviest_component_ratio:.0%} of the total token budget."
73
+ )
74
+
75
+ for pair in triggers.overlap_pairs:
76
+ findings.append(
77
+ f"Trigger overlap: '{pair[0]}' and '{pair[1]}' have "
78
+ f"{pair[2]:.0%} description similarity, may load together."
79
+ )
80
+
81
+ from harness_eval.core.types import ComponentType as CT
82
+
83
+ uncategorized = [c.name for c in setup.by_type(CT.UNCATEGORIZED)]
84
+
85
+ return SystemReport(
86
+ setup_name=setup.name,
87
+ component_count=len([c for c in setup.components if c.component_type != CT.UNCATEGORIZED]),
88
+ budget=budget,
89
+ triggers=triggers,
90
+ dependencies=dependencies,
91
+ context_utilization=context_utilization,
92
+ findings=findings,
93
+ uncategorized_files=uncategorized,
94
+ detected_tools=setup.detected_tools,
95
+ )
@@ -0,0 +1,63 @@
1
+ """Trigger overlap analysis: detect skills that would load simultaneously."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from harness_eval.core.types import ComponentType, Setup
8
+ from harness_eval.utils.similarity import tfidf_similarity
9
+
10
+ TRIGGER_OVERLAP_THRESHOLD = 0.50
11
+
12
+
13
+ @dataclass
14
+ class TriggerReport:
15
+ """Trigger overlap analysis results."""
16
+
17
+ skill_count: int = 0
18
+ skills_with_description: int = 0
19
+ skills_without_description: int = 0
20
+ overlap_pairs: list[tuple[str, str, float]] = field(default_factory=list)
21
+ missing_use_when: list[str] = field(default_factory=list)
22
+
23
+
24
+ def analyze_triggers(setup: Setup) -> TriggerReport:
25
+ """Analyze skill trigger descriptions for overlap and quality."""
26
+ skills = setup.by_type(ComponentType.SKILL)
27
+ report = TriggerReport(skill_count=len(skills))
28
+
29
+ descriptions: dict[str, str] = {}
30
+
31
+ for comp in skills:
32
+ desc = ""
33
+ if comp.frontmatter and isinstance(comp.frontmatter.get("description"), str):
34
+ desc = comp.frontmatter["description"]
35
+
36
+ if desc:
37
+ report.skills_with_description += 1
38
+ descriptions[comp.name] = desc
39
+
40
+ desc_lower = desc.lower()
41
+ use_when_phrases = [
42
+ "use when",
43
+ "use for",
44
+ "applies to",
45
+ "relevant for",
46
+ "triggered by",
47
+ "invoke when",
48
+ ]
49
+ if not any(phrase in desc_lower for phrase in use_when_phrases):
50
+ report.missing_use_when.append(comp.name)
51
+ else:
52
+ report.skills_without_description += 1
53
+
54
+ names = list(descriptions.keys())
55
+ for i in range(len(names)):
56
+ for j in range(i + 1, len(names)):
57
+ sim = tfidf_similarity(descriptions[names[i]], descriptions[names[j]])
58
+ if sim >= TRIGGER_OVERLAP_THRESHOLD:
59
+ report.overlap_pairs.append((names[i], names[j], sim))
60
+
61
+ report.overlap_pairs.sort(key=lambda x: x[2], reverse=True)
62
+
63
+ return report
@@ -0,0 +1,40 @@
1
+ """Types for cross-analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class SetupComparison:
10
+ """Comparison of inspection results between two setups."""
11
+
12
+ setup_a: str
13
+ setup_b: str
14
+ shared_components: list[str] = field(default_factory=list)
15
+ only_in_a: list[str] = field(default_factory=list)
16
+ only_in_b: list[str] = field(default_factory=list)
17
+ score_deltas: dict[str, int] = field(default_factory=dict)
18
+
19
+
20
+ @dataclass
21
+ class Correlation:
22
+ """A correlation between a component difference and an inspection score difference."""
23
+
24
+ component_name: str
25
+ component_type: str
26
+ setup_name: str
27
+ error_count: int
28
+ warning_count: int
29
+ impact: str # "positive", "negative", "neutral"
30
+ explanation: str
31
+
32
+
33
+ @dataclass
34
+ class CrossAnalysisResult:
35
+ """Complete cross-analysis result."""
36
+
37
+ comparison: SetupComparison
38
+ correlations: list[Correlation] = field(default_factory=list)
39
+ summary: str = ""
40
+ setup_scores: dict[str, dict[str, int]] = field(default_factory=dict)
@@ -0,0 +1,13 @@
1
+ """CLI entry point for harness-eval."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ cli = click.Group(name="harness-eval", help="Evaluate AI agent setups.")
8
+ click.version_option(package_name="harness-eval")(cli)
9
+
10
+ from harness_eval.cli import lint as _lint # noqa: E402, F401
11
+ from harness_eval.cli import review as _review # noqa: E402, F401
12
+ from harness_eval.cli import security as _security # noqa: E402, F401
13
+ from harness_eval.cli import skill as _skill # noqa: E402, F401
@@ -0,0 +1,14 @@
1
+ """Shared CLI helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+
10
+ def emit_output(text: str, output_path: str | None) -> None:
11
+ if output_path:
12
+ Path(output_path).write_text(text)
13
+ else:
14
+ click.echo(text)