codeframe-ai 0.9.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 (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """CLI command modules for CodeFRAME v2.
2
+
3
+ Each module provides a Typer sub-application for a specific domain.
4
+ Commands follow the pattern: codeframe <domain> <verb> [args] [--options]
5
+ """
@@ -0,0 +1,79 @@
1
+ """`cf config` — machine-wide CodeFRAME configuration (issue #616)."""
2
+
3
+ import os
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from codeframe.core import telemetry as telemetry_core
9
+
10
+ config_app = typer.Typer(
11
+ name="config",
12
+ help="Manage machine-wide CodeFRAME configuration",
13
+ no_args_is_help=True,
14
+ )
15
+
16
+ console = Console()
17
+
18
+
19
+ @config_app.command()
20
+ def telemetry(
21
+ action: str = typer.Argument(..., help="on | off | status"),
22
+ ) -> None:
23
+ """Enable, disable, or inspect anonymous telemetry (default: off).
24
+
25
+ See PRIVACY.md for exactly what is collected. Env overrides:
26
+ CODEFRAME_TELEMETRY=on|off always wins; DO_NOT_TRACK disables.
27
+ """
28
+ action = action.strip().lower()
29
+ if action not in ("on", "off", "status"):
30
+ console.print(f"[red]Error:[/red] unknown action '{action}' (expected on|off|status)")
31
+ raise typer.Exit(1)
32
+
33
+ if action == "status":
34
+ _print_status()
35
+ return
36
+
37
+ config = telemetry_core.load_config()
38
+ config.enabled = action == "on"
39
+ config.prompted = True
40
+ telemetry_core.save_config(config)
41
+
42
+ if config.enabled:
43
+ console.print(
44
+ "[green]Telemetry enabled.[/green] Anonymous usage events and crash "
45
+ "reports will be sent — see PRIVACY.md for exactly what is collected."
46
+ )
47
+ console.print(f"Anonymous id: [dim]{config.anonymous_id}[/dim]")
48
+ else:
49
+ console.print("[yellow]Telemetry disabled.[/yellow] Nothing will be sent.")
50
+
51
+
52
+ def _print_status() -> None:
53
+ # Resolve the effective state from this single config read (rather than
54
+ # is_enabled(), which re-reads) so the displayed state and anonymous id
55
+ # can't disagree if the file changes between two loads.
56
+ config = telemetry_core.load_config()
57
+ override = telemetry_core.env_override()
58
+ dnt_active = os.environ.get("DO_NOT_TRACK", "").strip().lower() not in ("", "0", "false")
59
+ if override is not None:
60
+ effective = override
61
+ elif dnt_active:
62
+ effective = False
63
+ else:
64
+ effective = config.enabled
65
+ state = "[green]enabled[/green]" if effective else "[yellow]disabled[/yellow]"
66
+ console.print(f"Telemetry: {state}")
67
+
68
+ if override is not None:
69
+ console.print(
70
+ f" (overridden by CODEFRAME_TELEMETRY={os.environ['CODEFRAME_TELEMETRY']})"
71
+ )
72
+ elif dnt_active:
73
+ console.print(" (disabled by DO_NOT_TRACK)")
74
+
75
+ console.print(f" Config file: {telemetry_core.config_path()}")
76
+ console.print(f" Endpoint: {telemetry_core.resolve_endpoint()}")
77
+ if telemetry_core.config_path().exists():
78
+ console.print(f" Anonymous id: {config.anonymous_id}")
79
+ console.print(" Details: PRIVACY.md | Change: cf config telemetry on|off")
@@ -0,0 +1,67 @@
1
+ """CLI command for launching the TUI dashboard.
2
+
3
+ Provides `cf dashboard` to launch the Textual-based terminal dashboard.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ console = Console()
13
+
14
+ dashboard_app = typer.Typer(
15
+ name="dashboard",
16
+ help="Terminal dashboard for monitoring workspace state",
17
+ invoke_without_command=True,
18
+ )
19
+
20
+
21
+ @dashboard_app.callback(invoke_without_command=True)
22
+ def dashboard(
23
+ repo_path: Optional[Path] = typer.Option(
24
+ None,
25
+ "--workspace", "-w",
26
+ help="Workspace path (defaults to current directory)",
27
+ ),
28
+ refresh_interval: int = typer.Option(
29
+ 2,
30
+ "--refresh-interval",
31
+ min=1,
32
+ max=60,
33
+ help="Seconds between data refreshes (default: 2)",
34
+ ),
35
+ ) -> None:
36
+ """Launch the TUI dashboard.
37
+
38
+ Shows a live terminal dashboard with task board, event log,
39
+ and blocker notifications. Updates automatically.
40
+
41
+ Keyboard shortcuts:
42
+ q Quit
43
+ r Force refresh
44
+ Tab Switch panels
45
+ Up/Down Navigate rows
46
+
47
+ Example:
48
+ codeframe dashboard
49
+ codeframe dashboard --refresh-interval 5
50
+ """
51
+ from codeframe.core.workspace import get_workspace
52
+ from codeframe.tui.app import DashboardApp
53
+
54
+ workspace_path = repo_path or Path.cwd()
55
+
56
+ try:
57
+ workspace = get_workspace(workspace_path)
58
+ except Exception as e:
59
+ console.print(f"[red]Error:[/red] {e}")
60
+ console.print("[dim]Run 'codeframe init' to create a workspace first.[/dim]")
61
+ raise typer.Exit(1)
62
+
63
+ app = DashboardApp(
64
+ workspace=workspace,
65
+ refresh_interval=refresh_interval,
66
+ )
67
+ app.run()
@@ -0,0 +1,205 @@
1
+ """CLI engine management commands.
2
+
3
+ Usage:
4
+ codeframe engines list # Show available engines
5
+ codeframe engines check <name> # Check engine requirements
6
+ codeframe engines stats # Show engine performance stats
7
+ codeframe engines compare # Compare engine performance
8
+ """
9
+
10
+ import json as _json
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ import typer
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+
19
+ from codeframe.core import engine_stats
20
+ from codeframe.core.workspace import get_workspace
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ console = Console()
25
+
26
+ engines_app = typer.Typer(
27
+ name="engines",
28
+ help="Engine management commands",
29
+ no_args_is_help=True,
30
+ )
31
+
32
+
33
+ @engines_app.command("list")
34
+ def engines_list() -> None:
35
+ """List all available execution engines and their requirement status."""
36
+ from codeframe.core.engine_registry import VALID_ENGINES, EXTERNAL_ENGINES, check_requirements
37
+
38
+ table = Table(title="Available Engines")
39
+ table.add_column("Engine", style="cyan")
40
+ table.add_column("Type", style="dim")
41
+ table.add_column("Requirements", style="green")
42
+
43
+ for engine in sorted(VALID_ENGINES):
44
+ engine_type = "external" if engine in EXTERNAL_ENGINES else "builtin"
45
+ if engine == "built-in":
46
+ engine_type = "alias → react"
47
+
48
+ try:
49
+ reqs = check_requirements(engine)
50
+ except ValueError:
51
+ reqs = {}
52
+
53
+ if not reqs:
54
+ req_display = "[green]Ready[/green]"
55
+ else:
56
+ parts = []
57
+ for key, satisfied in reqs.items():
58
+ mark = "[green]✓[/green]" if satisfied else "[red]✗[/red]"
59
+ parts.append(f"{mark} {key}")
60
+ req_display = ", ".join(parts)
61
+
62
+ table.add_row(engine, engine_type, req_display)
63
+
64
+ console.print(table)
65
+
66
+
67
+ @engines_app.command("check")
68
+ def engines_check(
69
+ name: str = typer.Argument(..., help="Engine name to check"),
70
+ ) -> None:
71
+ """Check if an engine's requirements are satisfied."""
72
+ from codeframe.core.engine_registry import check_requirements
73
+
74
+ try:
75
+ reqs = check_requirements(name)
76
+ except ValueError as e:
77
+ console.print(f"[red]Error:[/red] {e}")
78
+ raise typer.Exit(1)
79
+
80
+ if not reqs:
81
+ console.print(f"[green]Engine '{name}' has no additional requirements.[/green]")
82
+ return
83
+
84
+ all_satisfied = True
85
+ for key, satisfied in reqs.items():
86
+ if satisfied:
87
+ console.print(f" [green]✓[/green] {key}")
88
+ else:
89
+ console.print(f" [red]✗[/red] {key} — not set")
90
+ all_satisfied = False
91
+
92
+ if all_satisfied:
93
+ console.print(f"\n[green]Engine '{name}' is ready.[/green]")
94
+ else:
95
+ console.print(f"\n[red]Engine '{name}' has unmet requirements.[/red]")
96
+ raise typer.Exit(1)
97
+
98
+
99
+ def _get_current_workspace():
100
+ """Get workspace from current working directory.
101
+
102
+ Returns:
103
+ Workspace object.
104
+
105
+ Raises:
106
+ typer.Exit: If no workspace is found.
107
+ """
108
+ try:
109
+ return get_workspace(Path.cwd())
110
+ except (FileNotFoundError, ValueError):
111
+ console.print("[red]Error:[/red] No workspace found. Run 'cf init' first.")
112
+ raise typer.Exit(1)
113
+
114
+
115
+ def _compute_success_rate(metrics: dict[str, float]) -> float:
116
+ """Compute success rate from engine metrics."""
117
+ attempted = metrics.get("tasks_attempted", 0)
118
+ completed = metrics.get("tasks_completed", 0)
119
+ if attempted == 0:
120
+ return 0.0
121
+ return round(100.0 * completed / attempted, 1)
122
+
123
+
124
+ def _format_duration(ms: float) -> str:
125
+ """Format duration in human-readable form."""
126
+ if ms < 1000:
127
+ return f"{ms:.0f}ms"
128
+ return f"{ms / 1000:.1f}s"
129
+
130
+
131
+ def _build_stats_table(stats: dict[str, dict[str, float]], title: str = "Engine Stats") -> Table:
132
+ """Build a Rich table from engine stats."""
133
+ table = Table(title=title)
134
+ table.add_column("Engine", style="cyan")
135
+ table.add_column("Tasks", justify="right")
136
+ table.add_column("Success %", justify="right", style="green")
137
+ table.add_column("Gate Pass %", justify="right")
138
+ table.add_column("Avg Duration", justify="right")
139
+ table.add_column("Total Tokens", justify="right")
140
+ table.add_column("Avg Tokens/Task", justify="right")
141
+
142
+ # Sort by success rate descending
143
+ sorted_engines = sorted(
144
+ stats.items(),
145
+ key=lambda item: _compute_success_rate(item[1]),
146
+ reverse=True,
147
+ )
148
+
149
+ for eng, metrics in sorted_engines:
150
+ success_rate = _compute_success_rate(metrics)
151
+ gate_rate = metrics.get("gate_pass_rate", 0.0)
152
+ avg_dur = metrics.get("avg_duration_ms", 0.0)
153
+ total_tok = metrics.get("total_tokens", 0.0)
154
+ avg_tok = metrics.get("avg_tokens_per_task", 0.0)
155
+ attempted = int(metrics.get("tasks_attempted", 0))
156
+
157
+ table.add_row(
158
+ eng,
159
+ str(attempted),
160
+ f"{success_rate}%",
161
+ f"{gate_rate}%",
162
+ _format_duration(avg_dur),
163
+ f"{int(total_tok):,}",
164
+ f"{int(avg_tok):,}",
165
+ )
166
+
167
+ return table
168
+
169
+
170
+ @engines_app.command("stats")
171
+ def stats(
172
+ engine: Optional[str] = typer.Option(None, "--engine", "-e", help="Filter by engine name"),
173
+ output_format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
174
+ ) -> None:
175
+ """Show engine performance statistics."""
176
+ workspace = _get_current_workspace()
177
+ data = engine_stats.get_engine_stats(workspace, engine=engine)
178
+
179
+ if not data:
180
+ console.print("[yellow]No engine stats recorded yet.[/yellow]")
181
+ return
182
+
183
+ if output_format == "json":
184
+ console.print(_json.dumps(data, indent=2))
185
+ return
186
+
187
+ table = _build_stats_table(data, title="Engine Performance Stats")
188
+ console.print(table)
189
+
190
+
191
+ @engines_app.command("compare")
192
+ def compare() -> None:
193
+ """Compare performance across all engines."""
194
+ workspace = _get_current_workspace()
195
+ data = engine_stats.get_engine_stats(workspace)
196
+
197
+ if not data:
198
+ console.print(
199
+ "[yellow]No engine stats recorded yet. "
200
+ "Run tasks with different engines to see comparison.[/yellow]"
201
+ )
202
+ return
203
+
204
+ table = _build_stats_table(data, title="Engine Comparison (sorted by success rate)")
205
+ console.print(table)