hud-python 0.4.54__py3-none-any.whl → 0.4.56__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.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

hud/cli/dev.py CHANGED
@@ -25,6 +25,7 @@ def show_dev_server_info(
25
25
  inspector: bool,
26
26
  interactive: bool,
27
27
  env_dir: Path | None = None,
28
+ new: bool = False,
28
29
  ) -> str:
29
30
  """Show consistent server info for both Python and Docker modes.
30
31
 
@@ -125,6 +126,7 @@ async def run_mcp_module(
125
126
  verbose: bool,
126
127
  inspector: bool,
127
128
  interactive: bool,
129
+ new: bool = False,
128
130
  ) -> None:
129
131
  """Run an MCP module directly."""
130
132
  # Check if this is a reload (not first run)
@@ -222,14 +224,53 @@ async def run_mcp_module(
222
224
 
223
225
  # Show server info only on first run
224
226
  if not is_reload:
225
- show_dev_server_info(
226
- server_name=mcp_server.name or "mcp-server",
227
- port=port,
228
- transport=transport,
229
- inspector=inspector,
230
- interactive=interactive,
231
- env_dir=Path.cwd().parent if (Path.cwd().parent / "environment").exists() else None,
232
- )
227
+ # Try dynamic trace first for HTTP mode (only if --new)
228
+ live_trace_url: str | None = None
229
+ if transport == "http" and new:
230
+ try:
231
+ local_mcp_config: dict[str, dict[str, Any]] = {
232
+ "hud": {
233
+ "url": f"http://localhost:{port}/mcp",
234
+ "headers": {},
235
+ }
236
+ }
237
+
238
+ from hud.cli.flows.dev import create_dynamic_trace
239
+
240
+ live_trace_url = await create_dynamic_trace(
241
+ mcp_config=local_mcp_config,
242
+ build_status=False,
243
+ environment_name=mcp_server.name or "mcp-server",
244
+ )
245
+ except Exception: # noqa: S110
246
+ pass
247
+
248
+ # Show UI using shared flow logic
249
+ if transport == "http" and live_trace_url and new:
250
+ # Minimal UI with live trace
251
+ from hud.cli.flows.dev import generate_cursor_deeplink, show_dev_ui
252
+
253
+ server_name = mcp_server.name or "mcp-server"
254
+ cursor_deeplink = generate_cursor_deeplink(server_name, port)
255
+
256
+ show_dev_ui(
257
+ live_trace_url=live_trace_url,
258
+ server_name=server_name,
259
+ port=port,
260
+ cursor_deeplink=cursor_deeplink,
261
+ is_docker=False,
262
+ )
263
+ else:
264
+ # Full UI for HTTP without trace, or stdio mode
265
+ show_dev_server_info(
266
+ server_name=mcp_server.name or "mcp-server",
267
+ port=port,
268
+ transport=transport,
269
+ inspector=inspector,
270
+ interactive=interactive,
271
+ env_dir=Path.cwd().parent if (Path.cwd().parent / "environment").exists() else None,
272
+ new=new,
273
+ )
233
274
 
234
275
  # Check if there's an environment backend and remind user to start it (first run only)
235
276
  if not is_reload:
@@ -238,7 +279,8 @@ async def run_mcp_module(
238
279
  if env_dir.exists() and (env_dir / "server.py").exists():
239
280
  hud_console.info("")
240
281
  hud_console.info(
241
- f"{hud_console.sym.FLOW} Don't forget to start the environment backend in another terminal:"
282
+ f"{hud_console.sym.FLOW} Don't forget to start the environment backend in another "
283
+ "terminal:"
242
284
  )
243
285
  hud_console.info(" cd environment && uv run python uvicorn server:app --reload")
244
286
 
@@ -347,6 +389,7 @@ def run_with_reload(
347
389
  verbose: bool,
348
390
  inspector: bool,
349
391
  interactive: bool,
392
+ new: bool = False,
350
393
  ) -> None:
351
394
  """Run module with file watching and auto-reload."""
352
395
  try:
@@ -389,6 +432,11 @@ def run_with_reload(
389
432
 
390
433
  if verbose:
391
434
  cmd.append("--verbose")
435
+
436
+ if new:
437
+ cmd.append("--new")
438
+
439
+ if verbose:
392
440
  hud_console.info(f"Starting: {' '.join(cmd)}")
393
441
 
394
442
  # Mark as reload after first run to suppress logs
@@ -454,7 +502,12 @@ def run_with_reload(
454
502
 
455
503
 
456
504
  def run_docker_dev_server(
457
- port: int, verbose: bool, inspector: bool, interactive: bool, docker_args: list[str]
505
+ port: int,
506
+ verbose: bool,
507
+ inspector: bool,
508
+ interactive: bool,
509
+ docker_args: list[str],
510
+ new: bool = False,
458
511
  ) -> None:
459
512
  """Run MCP server in Docker with volume mounts, expose via local HTTP proxy."""
460
513
  import typer
@@ -462,6 +515,11 @@ def run_docker_dev_server(
462
515
 
463
516
  from hud.server import MCPServer
464
517
 
518
+ # Ensure Docker CLI and daemon are available before proceeding
519
+ from .utils.docker import require_docker_running
520
+
521
+ require_docker_running()
522
+
465
523
  cwd = Path.cwd()
466
524
 
467
525
  # Find environment directory (current or parent with hud.lock.yaml)
@@ -528,15 +586,6 @@ def run_docker_dev_server(
528
586
  env_dir=env_dir,
529
587
  )
530
588
 
531
- # Env flags already injected by create_docker_run_command
532
-
533
- # Print startup info
534
- hud_console.header("HUD Development Mode (Docker)")
535
-
536
- if verbose:
537
- hud_console.section_title("Docker Command")
538
- hud_console.info(" ".join(docker_cmd))
539
-
540
589
  # Create MCP config pointing to the Docker container's stdio
541
590
  mcp_config = {
542
591
  "docker": {
@@ -545,15 +594,62 @@ def run_docker_dev_server(
545
594
  }
546
595
  }
547
596
 
548
- # Show consistent server info
549
- show_dev_server_info(
550
- server_name=image_name,
551
- port=port,
552
- transport="http", # Docker mode always uses HTTP proxy
553
- inspector=inspector,
554
- interactive=interactive,
555
- env_dir=env_dir,
556
- )
597
+ # Attempt to create dynamic trace early (before any UI)
598
+ import asyncio as _asy
599
+
600
+ from hud.cli.flows.dev import create_dynamic_trace, generate_cursor_deeplink, show_dev_ui
601
+
602
+ live_trace_url: str | None = None
603
+ if new:
604
+ try:
605
+ local_mcp_config: dict[str, dict[str, Any]] = {
606
+ "hud": {
607
+ "url": f"http://localhost:{port}/mcp",
608
+ "headers": {},
609
+ }
610
+ }
611
+ live_trace_url = _asy.run(
612
+ create_dynamic_trace(
613
+ mcp_config=local_mcp_config,
614
+ build_status=True,
615
+ environment_name=image_name,
616
+ )
617
+ )
618
+ except Exception: # noqa: S110
619
+ pass
620
+
621
+ # Show appropriate UI
622
+ if live_trace_url and new:
623
+ # Minimal UI with live trace
624
+ cursor_deeplink = generate_cursor_deeplink(image_name, port)
625
+ show_dev_ui(
626
+ live_trace_url=live_trace_url,
627
+ server_name=image_name,
628
+ port=port,
629
+ cursor_deeplink=cursor_deeplink,
630
+ is_docker=True,
631
+ )
632
+ else:
633
+ # Full UI
634
+ hud_console.header("HUD Development Mode (Docker)")
635
+ if verbose:
636
+ hud_console.section_title("Docker Command")
637
+ hud_console.info(" ".join(docker_cmd))
638
+ show_dev_server_info(
639
+ server_name=image_name,
640
+ port=port,
641
+ transport="http",
642
+ inspector=inspector,
643
+ interactive=interactive,
644
+ env_dir=env_dir,
645
+ new=new,
646
+ )
647
+ hud_console.dim_info(
648
+ "",
649
+ "Container restarts on file changes (mounted volumes), "
650
+ "if changing tools run hud dev again",
651
+ )
652
+ hud_console.info("")
557
653
 
558
654
  # Suppress logs unless verbose
559
655
  if not verbose:
@@ -562,13 +658,6 @@ def run_docker_dev_server(
562
658
  logging.getLogger("uvicorn").setLevel(logging.ERROR)
563
659
  os.environ["FASTMCP_DISABLE_BANNER"] = "1"
564
660
 
565
- # Note about hot-reload behavior
566
- hud_console.dim_info(
567
- "",
568
- "Container restarts on file changes (mounted volumes), if changing tools run hud dev again",
569
- )
570
- hud_console.info("")
571
-
572
661
  # Create and run proxy with HUD helpers
573
662
  async def run_proxy() -> None:
574
663
  from fastmcp import FastMCP
@@ -617,6 +706,7 @@ def run_mcp_dev_server(
617
706
  watch: list[str] | None,
618
707
  docker: bool = False,
619
708
  docker_args: list[str] | None = None,
709
+ new: bool = False,
620
710
  ) -> None:
621
711
  """Run MCP development server with hot-reload."""
622
712
  docker_args = docker_args or []
@@ -627,12 +717,12 @@ def run_mcp_dev_server(
627
717
  hud_console.note("Detected Dockerfile - using Docker mode with volume mounts")
628
718
  hud_console.dim_info("Tip", "Use 'hud dev --help' to see all options")
629
719
  hud_console.info("")
630
- run_docker_dev_server(port, verbose, inspector, interactive, docker_args)
720
+ run_docker_dev_server(port, verbose, inspector, interactive, docker_args, new)
631
721
  return
632
722
 
633
723
  # Route to Docker mode if explicitly requested
634
724
  if docker:
635
- run_docker_dev_server(port, verbose, inspector, interactive, docker_args)
725
+ run_docker_dev_server(port, verbose, inspector, interactive, docker_args, new)
636
726
  return
637
727
 
638
728
  transport = "stdio" if stdio else "http"
@@ -676,6 +766,6 @@ def run_mcp_dev_server(
676
766
  is_child = os.environ.get("_HUD_DEV_CHILD") == "1"
677
767
 
678
768
  if is_child:
679
- asyncio.run(run_mcp_module(module, transport, port, verbose, False, False))
769
+ asyncio.run(run_mcp_module(module, transport, port, verbose, False, False, new))
680
770
  else:
681
- run_with_reload(module, watch_paths, transport, port, verbose, inspector, interactive)
771
+ run_with_reload(module, watch_paths, transport, port, verbose, inspector, interactive, new)
hud/cli/eval.py CHANGED
@@ -22,6 +22,28 @@ logger = logging.getLogger(__name__)
22
22
  hud_console = HUDConsole()
23
23
 
24
24
 
25
+ def _tasks_use_local_mcp(tasks: list[Task]) -> bool:
26
+ """Return True if any task's MCP config uses a local command instead of a URL.
27
+
28
+ A config is considered local when a server entry contains a 'command' key and
29
+ does not provide a 'url'.
30
+ """
31
+ try:
32
+ for t in tasks:
33
+ cfg = getattr(t, "mcp_config", {}) or {}
34
+ if not isinstance(cfg, dict):
35
+ continue
36
+ for server_cfg in cfg.values():
37
+ if isinstance(server_cfg, dict) and (
38
+ "command" in server_cfg and not server_cfg.get("url")
39
+ ):
40
+ return True
41
+ return False
42
+ except Exception:
43
+ # Be conservative: if detection fails, do not block
44
+ return False
45
+
46
+
25
47
  def get_available_models() -> list[dict[str, str | None]]:
26
48
  """Fetch available models from the HUD API (only ready models).
27
49
 
@@ -265,7 +287,33 @@ async def run_single_task(
265
287
  "Using first task from dataset (run with --full to run the entire dataset)..."
266
288
  )
267
289
 
268
- task_prompt = task.prompt[:50] + "..." if len(task.prompt) > 50 else task.prompt
290
+ # Warn/confirm if the task uses local MCP config
291
+ try:
292
+ if group_size > 1 and _tasks_use_local_mcp([task]):
293
+ hud_console.warning(
294
+ "Detected a local MCP configuration (uses 'command' instead of a 'url')."
295
+ )
296
+ hud_console.info(
297
+ "Ensure there are no exposed port conflicts during Docker runs/builds in eval."
298
+ )
299
+ proceed = hud_console.confirm(
300
+ "Proceed with running local MCP servers for this evaluation?",
301
+ default=True,
302
+ )
303
+ if not proceed:
304
+ # Provide a helpful next step
305
+ hud_console.hint("You can convert tasks to remote with: hud convert <tasks_file>")
306
+ raise typer.Exit(1)
307
+ # Always show the convert hint for awareness
308
+ hud_console.hint(
309
+ "Avoid local port conflicts by converting to remote: hud convert <tasks_file>"
310
+ )
311
+ except typer.Exit:
312
+ raise
313
+ except Exception as e:
314
+ hud_console.debug(f"Local MCP confirmation skipped due to error: {e}")
315
+
316
+ task_prompt = task.prompt
269
317
 
270
318
  # Use grouped evaluation if group_size > 1
271
319
  agent_config: dict[str, Any] = {}
@@ -387,6 +435,56 @@ async def run_full_dataset(
387
435
  hud_console.error(f"No tasks found in: {source}")
388
436
  raise typer.Exit(1)
389
437
 
438
+ # Warn/confirm once if any task uses local MCP config
439
+ try:
440
+ if _tasks_use_local_mcp(tasks):
441
+ hud_console.warning(
442
+ "Detected local MCP configurations (use 'command' instead of a 'url')."
443
+ )
444
+ hud_console.info(
445
+ "When running many tasks concurrently, exposed host ports from Docker may conflict."
446
+ )
447
+ proceed = hud_console.confirm(
448
+ "Proceed with running local MCP servers for this evaluation?",
449
+ default=True,
450
+ )
451
+ if not proceed:
452
+ # Helpful hint when source is a file path
453
+ try:
454
+ path = Path(source)
455
+ if path.exists():
456
+ hud_console.hint(
457
+ f"You can convert tasks to remote with: hud convert {path.name}"
458
+ )
459
+ else:
460
+ hud_console.hint(
461
+ "You can convert tasks to remote with: hud convert <tasks_file>"
462
+ )
463
+ except Exception:
464
+ hud_console.hint(
465
+ "You can convert tasks to remote with: hud convert <tasks_file>"
466
+ )
467
+ raise typer.Exit(1)
468
+ # Always show the convert hint for awareness
469
+ try:
470
+ path = Path(source)
471
+ if path.exists():
472
+ hud_console.hint(
473
+ f"Convert to remote to avoid port conflicts: hud convert {path.name}"
474
+ )
475
+ else:
476
+ hud_console.hint(
477
+ "Convert to remote to avoid port conflicts: hud convert <tasks_file>"
478
+ )
479
+ except Exception:
480
+ hud_console.hint(
481
+ "Convert to remote to avoid port conflicts: hud convert <tasks_file>"
482
+ )
483
+ except typer.Exit:
484
+ raise
485
+ except Exception as e:
486
+ hud_console.debug(f"Local MCP confirmation skipped due to error: {e}")
487
+
390
488
  # Convert Task objects to dicts for dataset runners
391
489
  dataset_or_tasks = [task.model_dump() for task in tasks]
392
490
 
hud/cli/flows/dev.py ADDED
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import contextlib
5
+ import json
6
+ import logging
7
+ from typing import Any
8
+
9
+ from hud.settings import settings
10
+ from hud.shared.requests import make_request
11
+ from hud.utils.hud_console import hud_console
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def create_dynamic_trace(
17
+ *,
18
+ mcp_config: dict[str, dict[str, Any]],
19
+ build_status: bool,
20
+ environment_name: str,
21
+ ) -> str | None:
22
+ """
23
+ Create a dynamic trace for HUD dev sessions when running in HTTP mode.
24
+
25
+ Sends a POST to the HUD API with:
26
+ - mcp_config: points to the local MCP config (same as Cursor)
27
+ - build_status: True if Docker mode (built image), False if basic Python mode
28
+ - environment_name: Name of the environment/server/image
29
+
30
+ Returns the full URL to the live trace when successful, otherwise None.
31
+ """
32
+ api_base = settings.hud_api_url.rstrip("/")
33
+ # Endpoint TBD; use a sensible default path that the backend can wire up
34
+ url = f"{api_base}/dev/dynamic-traces"
35
+
36
+ payload = {
37
+ "mcp_config": mcp_config,
38
+ "build_status": bool(build_status),
39
+ "environment_name": environment_name,
40
+ }
41
+
42
+ # Best-effort; if missing API key, log and continue
43
+ api_key = settings.api_key
44
+ if not api_key:
45
+ logger.warning("Skipping dynamic trace creation; missing HUD_API_KEY")
46
+ return None
47
+
48
+ try:
49
+ resp = await make_request("POST", url=url, json=payload, api_key=api_key)
50
+ # New API returns an id; construct the URL as https://hud.so/trace/{id}
51
+ trace_id = None
52
+ if isinstance(resp, dict):
53
+ trace_id = resp.get("id")
54
+ if trace_id is None:
55
+ data = resp.get("data", {}) or {}
56
+ if isinstance(data, dict):
57
+ trace_id = data.get("id")
58
+ # Backcompat: if url is provided directly
59
+ if not trace_id:
60
+ direct_url = resp.get("url") or (resp.get("data", {}) or {}).get("url")
61
+ if isinstance(direct_url, str) and direct_url:
62
+ return direct_url
63
+
64
+ if isinstance(trace_id, str) and trace_id:
65
+ return f"https://hud.so/trace/{trace_id}"
66
+ return None
67
+ except Exception as e:
68
+ # Do not interrupt dev flow
69
+ try:
70
+ preview = json.dumps(payload)[:500]
71
+ logger.warning("Failed to create dynamic dev trace: %s | payload=%s", e, preview)
72
+ except Exception:
73
+ logger.warning("Failed to create dynamic dev trace: %s", e)
74
+ return None
75
+
76
+
77
+ def show_dev_ui(
78
+ *,
79
+ live_trace_url: str,
80
+ server_name: str,
81
+ port: int,
82
+ cursor_deeplink: str,
83
+ is_docker: bool = False,
84
+ ) -> None:
85
+ """
86
+ Show the minimal dev UI with live trace link.
87
+
88
+ This is called only when we have a successful trace URL.
89
+ For full UI mode, the caller should use show_dev_server_info() directly.
90
+
91
+ Args:
92
+ live_trace_url: URL to the live trace
93
+ server_name: Name of the server/image
94
+ port: Port the server is running on
95
+ cursor_deeplink: Pre-generated Cursor deeplink URL
96
+ is_docker: Whether this is Docker mode (affects hot-reload message)
97
+ """
98
+ import webbrowser
99
+
100
+ from rich.panel import Panel
101
+
102
+ # Show header first
103
+ hud_console.header("HUD Development Server", icon="🚀")
104
+
105
+ # Try to open the live trace in the default browser
106
+ with contextlib.suppress(Exception):
107
+ # new=2 -> open in a new tab, if possible
108
+ webbrowser.open(live_trace_url, new=2)
109
+
110
+ # Show panel with just the link
111
+ # Center the link and style it: blue, bold, underlined
112
+ link_markup = f"[bold underline rgb(108,113,196)][link={live_trace_url}]{live_trace_url}[/link][/bold underline rgb(108,113,196)]" # noqa: E501
113
+ # Use center alignment by surrounding with spaces via justify
114
+ from rich.align import Align
115
+
116
+ panel = Panel(
117
+ Align.center(link_markup),
118
+ title="🔗 Live Dev Trace",
119
+ border_style="rgb(192,150,12)", # HUD gold
120
+ padding=(1, 2),
121
+ )
122
+ hud_console.console.print(panel)
123
+
124
+ # Show other info below
125
+ label = "Base image" if is_docker else "Server"
126
+ hud_console.info("")
127
+ hud_console.info(f"{hud_console.sym.ITEM} {label}: {server_name}")
128
+ hud_console.info(f"{hud_console.sym.ITEM} Cursor: {cursor_deeplink}")
129
+ hud_console.info("")
130
+ hud_console.info(f"{hud_console.sym.SUCCESS} Hot-reload enabled")
131
+ if is_docker:
132
+ hud_console.dim_info(
133
+ "",
134
+ "Container restarts on file changes (mounted volumes), "
135
+ "if changing tools run hud dev again",
136
+ )
137
+ hud_console.info("")
138
+
139
+
140
+ def generate_cursor_deeplink(server_name: str, port: int) -> str:
141
+ """Generate a Cursor deeplink for the MCP server.
142
+
143
+ Args:
144
+ server_name: Name of the server
145
+ port: Port the server is running on
146
+
147
+ Returns:
148
+ Cursor deeplink URL
149
+ """
150
+ server_config = {"url": f"http://localhost:{port}/mcp"}
151
+ config_json = json.dumps(server_config, indent=2)
152
+ config_base64 = base64.b64encode(config_json.encode()).decode()
153
+ return (
154
+ f"cursor://anysphere.cursor-deeplink/mcp/install?name={server_name}&config={config_base64}"
155
+ )
hud/cli/flows/tasks.py CHANGED
@@ -11,7 +11,7 @@ import yaml
11
11
 
12
12
  from hud.cli.push import push_environment
13
13
  from hud.cli.utils.docker import require_docker_running
14
- from hud.cli.utils.env_check import ensure_built, find_environment_dir
14
+ from hud.cli.utils.env_check import find_environment_dir
15
15
  from hud.cli.utils.registry import extract_name_and_tag
16
16
  from hud.utils.hud_console import hud_console
17
17
  from hud.utils.tasks import load_tasks
@@ -56,7 +56,9 @@ def _validate_tasks(tasks: list[Task]) -> bool:
56
56
  return True
57
57
 
58
58
 
59
- def _ensure_pushed(env_dir: Path, lock_data: dict[str, Any]) -> dict[str, Any]:
59
+ def _ensure_pushed(
60
+ env_dir: Path, lock_data: dict[str, Any], check_docker: bool = True
61
+ ) -> dict[str, Any]:
60
62
  """Ensure the environment is pushed to a registry; return updated lock data."""
61
63
  pushed = bool(lock_data.get("push"))
62
64
  if not pushed:
@@ -64,7 +66,8 @@ def _ensure_pushed(env_dir: Path, lock_data: dict[str, Any]) -> dict[str, Any]:
64
66
  if not hud_console.confirm("Push to a registry now (runs 'hud push')?", default=True):
65
67
  raise typer.Exit(1)
66
68
  # Check Docker availability before attempting a push
67
- require_docker_running()
69
+ if check_docker:
70
+ require_docker_running()
68
71
 
69
72
  # If Docker or login is not configured, the push function will fail and halt.
70
73
  push_environment(str(env_dir), yes=True)
@@ -293,9 +296,24 @@ def convert_tasks_to_remote(tasks_file: str) -> str:
293
296
  hud_console.hint("Ensure you're in or near your environment folder before running 'hud rl'")
294
297
  raise typer.Exit(1)
295
298
 
296
- # Ensure built and pushed
297
- lock_data = ensure_built(env_dir, interactive=True)
298
- lock_data = _ensure_pushed(env_dir, lock_data)
299
+ # For convert command, we don't need Docker running - just check for lock file
300
+ # This avoids showing Docker-related messages during conversion
301
+ lock_path = env_dir / "hud.lock.yaml"
302
+ if not lock_path.exists():
303
+ hud_console.error("No hud.lock.yaml found. The environment needs to be built first.")
304
+ hud_console.info("Run 'hud build' in the environment directory to build it.")
305
+ raise typer.Exit(1)
306
+
307
+ # Load lock data directly
308
+ try:
309
+ with open(lock_path) as f:
310
+ lock_data: dict[str, Any] = yaml.safe_load(f) or {}
311
+ except Exception as e:
312
+ hud_console.error(f"Failed to read hud.lock.yaml: {e}")
313
+ raise typer.Exit(1) from e
314
+
315
+ # Check if pushed - don't check Docker for convert command
316
+ lock_data = _ensure_pushed(env_dir, lock_data, check_docker=False)
299
317
 
300
318
  # Derive remote image name org/name:tag
301
319
  remote_image = _derive_remote_image(lock_data)
@@ -387,8 +405,11 @@ def convert_tasks_to_remote(tasks_file: str) -> str:
387
405
  f"Detected env vars in .env that look like API keys: {names_preview}.\n"
388
406
  "Include them as remote headers (values will be ${VAR} placeholders)?"
389
407
  )
390
- if hud_console.confirm(prompt, default=True):
391
- all_detected.update(missing)
408
+ if not hud_console.confirm(prompt, default=True):
409
+ # User cancelled - exit without creating the file
410
+ hud_console.info("Conversion cancelled by user")
411
+ raise typer.Exit(0)
412
+ all_detected.update(missing)
392
413
 
393
414
  # Final set of env vars to convert to headers
394
415
  provided_keys = all_detected
@@ -461,6 +482,5 @@ def convert_tasks_to_remote(tasks_file: str) -> str:
461
482
  f.write("\n")
462
483
 
463
484
  hud_console.success(f"Created remote tasks file: {remote_path.name}")
464
- hud_console.hint("Proceeding with RL training on the remote environment")
465
485
 
466
486
  return str(remote_path)
hud/cli/init.py CHANGED
@@ -23,6 +23,7 @@ PRESET_MAP: dict[str, str | None] = {
23
23
  "blank": "blank",
24
24
  "deep-research": "deepresearch",
25
25
  "browser": "browser",
26
+ "rubrics": "rubrics",
26
27
  }
27
28
 
28
29
  SKIP_DIR_NAMES = {"node_modules", "__pycache__", "dist", "build", ".next", ".git"}
@@ -91,6 +92,7 @@ def _prompt_for_preset() -> str:
91
92
  {"name": "blank", "message": "blank"},
92
93
  {"name": "deep-research", "message": "deep-research"},
93
94
  {"name": "browser", "message": "browser"},
95
+ {"name": "rubrics", "message": "rubrics"},
94
96
  ]
95
97
  display_choices = [c["message"] for c in choices]
96
98
  selected = questionary.select(
@@ -194,7 +196,7 @@ def create_environment(
194
196
  if preset_normalized not in PRESET_MAP:
195
197
  hud_console.warning(
196
198
  f"Unknown preset '{preset_normalized}', defaulting to 'blank' "
197
- "(available: blank, deep-research, browser)"
199
+ "(available: blank, deep-research, browser, rubrics)"
198
200
  )
199
201
  preset_normalized = "blank"
200
202
 
hud/cli/utils/docker.py CHANGED
@@ -308,7 +308,10 @@ def require_docker_running() -> None:
308
308
  "Is Docker running? Open Docker Desktop and wait until it reports 'Running'"
309
309
  )
310
310
  raise typer.Exit(1) from e
311
- except Exception as e:
312
- hud_console.error(f"Docker check failed: {e}")
311
+ except typer.Exit:
312
+ # Propagate cleanly without extra noise; hints already printed above
313
+ raise
314
+ except Exception:
315
+ # Unknown failure - keep output minimal and avoid stack traces
313
316
  hud_console.hint("Is the Docker daemon running?")
314
- raise typer.Exit(1) from e
317
+ raise typer.Exit(1) # noqa: B904
hud/clients/base.py CHANGED
@@ -146,7 +146,7 @@ class BaseHUDClient(AgentMCPClient):
146
146
  except HudException:
147
147
  raise
148
148
  except Exception as e:
149
- # Auto-converts to appropriate HUD exception type with hints
149
+ hud_console.error(f"Failed to initialize MCP client: {e}")
150
150
  raise HudException from e
151
151
 
152
152
  # Common hud behavior - fetch telemetry
@@ -333,7 +333,7 @@ class BaseHUDClient(AgentMCPClient):
333
333
  tool_info = {
334
334
  "name": tool.name,
335
335
  "description": tool.description,
336
- "input_schema": tool.inputSchema,
336
+ "inputSchema": tool.inputSchema,
337
337
  }
338
338
  analysis["tools"].append(tool_info)
339
339