"""Ralph Workflow CLI entry point - typer application with rich-click help styling.
This module provides the main CLI application for Ralph Workflow, using typer
for argument parsing and rich-click for enhanced help output.
"""
from __future__ import annotations
import sys
from dataclasses import dataclass
from importlib import import_module
from pathlib import Path as RuntimePath
from typing import TYPE_CHECKING, Annotated, Protocol, cast
import rich_click as click
import typer
import typer.testing
from loguru import logger
from rich.text import Text
from ralph import __version__
from ralph.api.opencode import list_providers as fetch_providers
from ralph.cli._cli_override_input import CLIOverrideInput
from ralph.cli.commands.check_policy import check_policy_command
from ralph.cli.commands.cleanup import cleanup
from ralph.cli.commands.commit import CommitPlumbingOptions, commit_plumbing
from ralph.cli.commands.diagnose import diagnose_command
from ralph.cli.commands.explain import explain_command
from ralph.cli.commands.init import init_command
from ralph.cli.commands.prompt_helper import run_prompt_helper
from ralph.cli.commands.run import RunPipelineRequest, run_pipeline
from ralph.cli.commands.smoke import smoke_interactive_claude_command
from ralph.cli.options import display_agents_table, display_providers_table
from ralph.config.bootstrap import (
ensure_global_config,
ensure_global_mcp_config,
ensure_global_policy_configs,
ensure_local_configs,
regenerate_all,
)
from ralph.config.enums import Verbosity
from ralph.config.loader import load_config
from ralph.config.welcome import emit_first_run_welcome
from ralph.display.context import DisplayContext
from ralph.display.context import make_display_context as _make_display_context
from ralph.onboarding import init_help_text, init_local_config_help_text
from ralph.pipeline import checkpoint as ckpt
from ralph.workspace.scope import resolve_workspace_scope
if TYPE_CHECKING:
from collections.abc import Callable, Mapping, Sequence
from types import ModuleType
from rich.console import Console
from ralph.agents.registry import AgentRegistry
from ralph.cli._cli_overrides import CLIOverrides
from ralph.config.models import AgentConfig, UnifiedConfig
from ralph.display.context import DisplayContext
if TYPE_CHECKING:
class _CommandMain(Protocol):
def __call__(
self,
*,
args: Sequence[str] | None = None,
prog_name: str | None = None,
complete_var: str | None = None,
standalone_mode: bool = True,
windows_expand_args: bool = True,
) -> object: ...
class _AgentRegistryFactory(Protocol):
@classmethod
def from_config(cls, config: UnifiedConfig) -> AgentRegistry: ...
class _ValidateCustomMcpServersFn(Protocol):
def __call__(self, workspace_root: RuntimePath) -> int: ...
click.rich_click.USE_RICH_MARKUP = True
click.rich_click.USE_MARKDOWN = True
app = typer.Typer(
name="ralph",
help="[bold]Ralph Workflow[/bold] - Multi-agent AI orchestration pipeline.\n\n"
"Ralph Workflow orchestrates AI coding agents to implement changes based on PROMPT.md.\n"
"It runs a developer agent for code implementation across multiple planning and\n"
"development iterations, automatically staging and committing the final result.",
add_completion=True,
rich_markup_mode="rich",
suggest_commands=True,
)
_typer_get_command = typer.main.get_command
def _get_cli_context() -> DisplayContext:
"""Resolve a fresh DisplayContext for the current terminal environment."""
return _make_display_context()
_KNOWN_SUBCOMMANDS: frozenset[str] = frozenset({"cleanup"})
_QUICK_FLAGS: frozenset[str] = frozenset({"-Q", "--quick"})
_THOROUGH_DEVELOPER_ITERS = 10
def _prepare_init_args(args: Sequence[str] | None) -> list[str] | None:
"""Normalize --init and -Q positional text before Click parsing."""
if args is None:
args = sys.argv[1:]
normalized_args: list[str] = list(args)
for index, arg in enumerate(normalized_args):
if arg == "--init":
next_arg = normalized_args[index + 1] if index + 1 < len(normalized_args) else None
if next_arg is None or next_arg.startswith("-"):
normalized_args.insert(index + 1, "")
break
normalized_args = _inject_quick_prompt(normalized_args)
return normalized_args
def _inject_quick_prompt(args: list[str]) -> list[str]:
"""Inject --prompt before bare positional text when -Q/--quick is present."""
if not any(a in _QUICK_FLAGS for a in args):
return args
if "--prompt" in args or "-P" in args:
return args
result: list[str] = []
skip_next = False
for i, arg in enumerate(args):
if skip_next:
skip_next = False
result.append(arg)
continue
# Options with values consume the next arg; skip it to avoid treating it as a prompt.
if arg in {
"--config",
"-c",
"--developer-iters",
"-D",
"--counter",
"--developer-agent",
"-a",
"--developer-model",
"--verbosity",
"-v",
"--init",
"--git-user-name",
"--git-user-email",
"--explain-policy-dir",
}:
result.append(arg)
skip_next = True
continue
if not arg.startswith("-") and arg not in _KNOWN_SUBCOMMANDS:
# Bare positional text: inject --prompt before it
result.append("--prompt")
result.append(arg)
return result + list(args[i + 1 :])
result.append(arg)
return result
def _module_attr(module: ModuleType, attribute: str) -> object:
namespace = cast("dict[str, object]", module.__dict__)
return namespace[attribute]
def _load_agent_registry_factory() -> _AgentRegistryFactory:
return cast(
"_AgentRegistryFactory",
_module_attr(import_module("ralph.agents.registry"), "AgentRegistry"),
)
def _load_validate_custom_mcp_servers() -> _ValidateCustomMcpServersFn:
return cast(
"_ValidateCustomMcpServersFn",
_module_attr(import_module("ralph.pipeline.runner"), "validate_custom_mcp_servers"),
)
def _set_command_main(command: click.Command, callback: _CommandMain) -> None:
cast("dict[str, object]", command.__dict__)["main"] = callback
def _set_typer_testing_get_command(
callback: Callable[[typer.Typer], click.Command],
) -> None:
cast("dict[str, object]", typer.testing.__dict__)["_get_command"] = callback
def _get_command_with_optional_init(typer_instance: typer.Typer) -> click.Command:
command = _typer_get_command(typer_instance)
if typer_instance is app:
original_main: _CommandMain = command.main
def patched_main(
*,
args: Sequence[str] | None = None,
prog_name: str | None = None,
complete_var: str | None = None,
standalone_mode: bool = True,
windows_expand_args: bool = True,
) -> object:
return original_main(
args=_prepare_init_args(args),
prog_name=prog_name,
complete_var=complete_var,
standalone_mode=standalone_mode,
windows_expand_args=windows_expand_args,
)
_set_command_main(command, patched_main)
return command
typer.main.get_command = _get_command_with_optional_init
_set_typer_testing_get_command(_get_command_with_optional_init)
[docs]
def version_callback(version: bool, ctx: DisplayContext | None = None) -> None:
"""Print version information."""
if version:
c = ctx.console if ctx is not None else _get_cli_context().console
version_text = Text()
version_text.append("Ralph Workflow", style="theme.banner.title")
version_text.append(" version ")
version_text.append(__version__, style="theme.banner.version")
c.print(version_text)
raise typer.Exit()
def _config_path(config: str | None) -> RuntimePath | None:
"""Convert CLI config string into a Path when provided."""
if config is None:
return None
return RuntimePath(config)
[docs]
def resolve_effective_verbosity(
verbosity: Verbosity,
*,
quiet: bool,
debug: bool,
) -> Verbosity:
"""Compute the verbosity to use for the run.
``--quiet`` and ``--debug`` take precedence. Absent those, the default
is ``Verbosity.VERBOSE`` so Ralph Workflow is visibly active by default. The
legacy ``--verbosity normal`` input is mapped to VERBOSE to preserve
wrapper scripts that passed ``normal`` explicitly.
"""
if quiet:
return Verbosity.QUIET
if debug:
return Verbosity.DEBUG
if verbosity == Verbosity.NORMAL:
return Verbosity.VERBOSE
return verbosity
def _try_load_registry() -> AgentRegistry | None:
"""Attempt to load the agent registry; returns None on failure."""
try:
cfg = load_config(None, {})
registry_type = _load_agent_registry_factory()
return registry_type.from_config(cfg)
except Exception:
return None
def _bootstrap_global_configs(*, display_context: DisplayContext) -> None:
"""Create user-global config files from bundled templates if they don't exist."""
results = [
ensure_global_config(),
ensure_global_mcp_config(),
*ensure_global_policy_configs(),
]
registry = None
if any(r.action in {"created", "regenerated"} for r in results):
registry = _try_load_registry()
emit_first_run_welcome(
display_context.console,
results,
agent_registry=registry,
display_context=display_context,
)
def _handle_regenerate_config(*, display_context: DisplayContext) -> None:
"""Regenerate global and local configs from bundled defaults, backing up existing files."""
c = display_context.console
agent_dir: RuntimePath | None
try:
scope = resolve_workspace_scope()
agent_dir = scope.local_config_path.parent
except Exception as exc:
logger.debug("Workspace scope unavailable, skipping local regenerate: {}", exc)
agent_dir = None
results = regenerate_all(agent_dir=agent_dir)
if results:
created_or_regenerated = [r for r in results if r.action in {"created", "regenerated"}]
if created_or_regenerated:
emit_first_run_welcome(c, results, is_regenerate=True, display_context=display_context)
else:
msg = "No configs needed regeneration (all files up-to-date)"
c.print(Text(msg, style="theme.text.muted"))
else:
c.print(Text("No configs found to regenerate", style="theme.text.muted"))
def _handle_generate_local_config(*, display_context: DisplayContext) -> None:
"""Create the full project-local config override set from the global config set."""
console = display_context.console
scope = resolve_workspace_scope()
results = ensure_local_configs(scope.local_config_path.parent)
if any(result.action in {"created", "regenerated"} for result in results):
emit_first_run_welcome(console, results, display_context=display_context)
return
text = Text("Local config files already exist in: ", style="theme.text.muted")
text.append(str(scope.local_config_path.parent))
console.print(text)
def _handle_prompt_helper(
config: str | None,
cli_overrides: dict[str, object],
) -> None:
"""Handle --prompt-helper early-exit before pipeline."""
config_path = _config_path(config)
workspace_scope = None if config_path is not None else resolve_workspace_scope()
workspace_root = workspace_scope.root if workspace_scope else RuntimePath.cwd()
cfg = load_config(config_path, cli_overrides, workspace_scope=workspace_scope)
run_prompt_helper(cfg, workspace_root)
def _handle_early_exit_flags(
*,
version: bool,
explain_policy: bool,
explain_policy_dir: str | None,
check_policy: bool,
counter_overrides: dict[str, int] | None = None,
) -> None:
"""Handle version and explain-policy early-exit flags before any bootstrap."""
if version:
version_callback(version)
if explain_policy:
policy_dir = RuntimePath(explain_policy_dir) if explain_policy_dir else None
raise typer.Exit(code=explain_command(policy_dir))
if check_policy:
policy_dir = RuntimePath(explain_policy_dir) if explain_policy_dir else None
raise typer.Exit(code=check_policy_command(policy_dir, counter_overrides=counter_overrides))
[docs]
def main(
ctx: typer.Context,
prompt: Annotated[
str | None,
typer.Option(
"--prompt",
"-P",
help="Inline prompt text for quick runs (use with --quick/-Q)",
),
] = None,
config: Annotated[
str | None,
typer.Option(
"--config",
"-c",
help="Path to configuration file",
),
] = None,
developer_iters: Annotated[
int | None,
typer.Option(
"--developer-iters",
"-D",
min=1,
help="Maximum developer agent iterations per run.",
),
] = None,
quick: Annotated[
bool,
typer.Option(
"--quick",
"-Q",
help="Quick mode: run a single developer iteration (equivalent to -D 1).",
),
] = False,
thorough: Annotated[
bool,
typer.Option(
"--thorough",
"-T",
help=(
"Thorough mode: run ten developer iterations "
f"(equivalent to -D {_THOROUGH_DEVELOPER_ITERS})."
),
),
] = False,
counter: Annotated[
list[str] | None,
typer.Option(
"--counter",
help="Override a policy-declared budget counter: NAME=VALUE (repeatable)",
),
] = None,
developer_agent: Annotated[
str | None,
typer.Option(
"--developer-agent",
"-a",
help="Developer agent name",
),
] = None,
developer_model: Annotated[
str | None,
typer.Option(
"--developer-model",
help="Model flag for developer agent",
),
] = None,
verbosity: Annotated[
Verbosity,
typer.Option(
"--verbosity",
"-v",
help=(
"Output verbosity (quiet, normal, verbose, full, debug). "
"Default: verbose. Use --quiet to silence non-error output."
),
),
] = Verbosity.VERBOSE,
quiet: Annotated[
bool,
typer.Option("--quiet", "-q", help="Suppress all output except errors"),
] = False,
debug: Annotated[
bool,
typer.Option("--debug", help="Enable debug output"),
] = False,
resume: Annotated[
bool,
typer.Option("--resume", "-r", help="Resume from checkpoint"),
] = False,
no_resume: Annotated[
bool,
typer.Option("--no-resume", help="Ignore existing checkpoint"),
] = False,
inspect_checkpoint: Annotated[
bool,
typer.Option("--inspect-checkpoint", help="Show checkpoint contents as raw JSON"),
] = False,
dry_run: Annotated[
bool,
typer.Option("--dry-run", help="Run without invoking agents"),
] = False,
list_agents: Annotated[
bool,
typer.Option("--list-agents", help="List configured agents"),
] = False,
list_providers: Annotated[
bool,
typer.Option("--list-providers", help="List available providers"),
] = False,
diagnose: Annotated[
bool,
typer.Option("--diagnose", "-d", help="Run diagnostics"),
] = False,
check_config: Annotated[
bool,
typer.Option("--check-config", "-C", help="Validate configuration"),
] = False,
check_mcp: Annotated[
bool,
typer.Option(
"--check-mcp",
help="Validate custom MCP servers and agent wiring then exit",
),
] = False,
init: Annotated[
str | None,
typer.Option(
"--init",
help=init_help_text(),
),
] = None,
regenerate_config: Annotated[
bool,
typer.Option(
"--regenerate-config",
help="Rewrite global and local configs from bundled defaults"
" (existing files are backed up to <name>.bak)",
),
] = False,
generate_local_config: Annotated[
bool,
typer.Option(
"--init-local-config",
"--generate-local-config",
help=init_local_config_help_text(),
),
] = False,
generate_commit_msg: Annotated[
bool,
typer.Option("--generate-commit-msg", help="Generate commit message"),
] = False,
generate_commit: Annotated[
bool,
typer.Option("--generate-commit", help="Generate and apply commit"),
] = False,
show_commit_msg: Annotated[
bool,
typer.Option(
"--show-commit-msg",
help=(
"Show commit message; may be empty after --generate-commit "
"because the artifact is deleted"
),
),
] = False,
git_user_name: Annotated[
str | None,
typer.Option("--git-user-name", help="Git user name for commits"),
] = None,
git_user_email: Annotated[
str | None,
typer.Option("--git-user-email", help="Git user email for commits"),
] = None,
version: Annotated[
bool,
typer.Option("--version", "-V", help="Show version"),
] = False,
explain_policy: Annotated[
bool,
typer.Option(
"--explain-policy",
help="Print a human-readable explanation of the active policy and exit",
),
] = False,
explain_policy_dir: Annotated[
str | None,
typer.Option(
"--explain-policy-dir",
hidden=True,
help="Policy directory to explain or check (default: bundled defaults)",
),
] = None,
parallel_worker_manifest: Annotated[
str | None,
typer.Option(
"--parallel-worker-manifest",
hidden=True,
help="Internal worker bootstrap manifest path.",
),
] = None,
check_policy: Annotated[
bool,
typer.Option(
"--check-policy",
help="Validate the active policy and print a summary, then exit",
),
] = False,
prompt_helper: Annotated[
bool,
typer.Option(
"--prompt-helper",
help=(
"Launch interactive prompt-refinement helper. "
"Starts a PM-style agent that helps turn a vague idea into PROMPT.md."
),
),
] = False,
) -> None:
"""Run the Ralph Workflow multi-agent pipeline or execute a sub-operation."""
# Parse --counter NAME=VALUE entries early so --check-policy can validate them.
raw_counter_entries: list[str] = list(counter) if counter else []
counter_overrides = _parse_counter_overrides(raw_counter_entries)
_handle_early_exit_flags(
version=version,
explain_policy=explain_policy,
explain_policy_dir=explain_policy_dir,
check_policy=check_policy,
counter_overrides=counter_overrides,
)
_validate_mode_flags(quick=quick, thorough=thorough, resume=resume, no_resume=no_resume)
verbosity = resolve_effective_verbosity(verbosity, quiet=quiet, debug=debug)
_cli_ctx = _get_cli_context()
_console = _cli_ctx.console
bootstrap_global_configs(display_context=_cli_ctx)
# Set up logging based on verbosity
configure_logging(verbosity)
_validate_prompt_flags(prompt, quick)
# Mode presets imply developer iteration counts and override explicit -D when supplied.
effective_developer_iters = _resolve_effective_developer_iters(
quick=quick,
thorough=thorough,
developer_iters=developer_iters,
)
# Load configuration
cli_overrides = _build_cli_overrides(
CLIOverrideInput(
developer_agent=developer_agent,
developer_model=developer_model,
git_user_name=git_user_name,
git_user_email=git_user_email,
developer_iters=effective_developer_iters,
),
)
# Check for early exit commands
exit_code = handle_list_agents(config, cli_overrides, list_agents, display_context=_cli_ctx)
if exit_code is not None:
raise typer.Exit(code=exit_code)
exit_code = handle_list_providers(list_providers, display_context=_cli_ctx)
if exit_code is not None:
raise typer.Exit(code=exit_code)
exit_code = handle_check_config(config, cli_overrides, check_config, console=_console)
if exit_code is not None:
raise typer.Exit(code=exit_code)
exit_code = handle_check_mcp(check_mcp, console=_console)
if exit_code is not None:
raise typer.Exit(code=exit_code)
if diagnose:
exit_code = diagnose_command(_config_path(config), cli_overrides, display_context=_cli_ctx)
raise typer.Exit(code=exit_code)
if init is not None:
init_command(init, _config_path(config), display_context=_cli_ctx)
raise typer.Exit()
if regenerate_config:
_handle_regenerate_config(display_context=_cli_ctx)
raise typer.Exit()
if generate_local_config:
_handle_generate_local_config(display_context=_cli_ctx)
raise typer.Exit()
if inspect_checkpoint:
summary = ckpt.inspect()
_console.print(summary)
raise typer.Exit()
exit_code = handle_commit_plumbing(
CommitPlumbingOptions(
generate_commit_msg=generate_commit_msg,
generate_commit=generate_commit,
show_commit_msg=show_commit_msg,
config_path=_config_path(config),
cli_overrides=cli_overrides,
),
display_context=_cli_ctx,
)
if exit_code is not None:
raise typer.Exit(code=exit_code)
# Handle --prompt-helper before pipeline
if prompt_helper:
_handle_prompt_helper(config, cli_overrides)
raise typer.Exit()
# If a subcommand was invoked, we're done
if ctx.invoked_subcommand:
return
# Run the main pipeline
exit_code = invoke_pipeline(
config,
RunPipelineOpts(
cli_overrides=cli_overrides,
dry_run=dry_run,
resume=resume,
no_resume=no_resume,
verbosity=verbosity,
counter_overrides=counter_overrides,
inline_prompt=prompt,
parallel_worker_manifest=_config_path(parallel_worker_manifest),
),
display_context=_cli_ctx,
)
raise typer.Exit(code=exit_code)
app.callback(invoke_without_command=True)(main)
app.command()(cleanup)
[docs]
def smoke_interactive_claude() -> None:
"""Run the manual PTY/TUI smoke test for interactive Claude using claude/haiku."""
raise typer.Exit(code=smoke_interactive_claude_command(display_context=_get_cli_context()))
app.command(name="smoke-interactive-claude")(smoke_interactive_claude)
def _validate_mode_flags(*, quick: bool, thorough: bool, resume: bool, no_resume: bool) -> None:
if resume and no_resume:
raise click.UsageError(
"Conflicting flags: --resume and --no-resume cannot be used together"
)
if quick and thorough:
raise click.UsageError("--quick/-Q and --thorough/-T cannot be used together")
def _validate_prompt_flags(prompt: str | None, quick: bool) -> None:
if prompt is not None and not quick:
raise click.UsageError(
"--prompt requires --quick/-Q. Usage: ralph -Q --prompt 'your prompt here'"
)
def _resolve_effective_developer_iters(
*, quick: bool, thorough: bool, developer_iters: int | None
) -> int | None:
if quick:
return 1
if thorough:
return _THOROUGH_DEVELOPER_ITERS
return developer_iters
def _handle_list_agents(
config: str | None,
cli_overrides: dict[str, object],
list_agents: bool,
*,
display_context: DisplayContext,
) -> int | None:
"""Handle --list-agents flag; returns exit code or None to continue."""
if not list_agents:
return None
try:
config_path = _config_path(config)
workspace_scope = None if config_path is not None else resolve_workspace_scope()
cfg = load_config(config_path, cli_overrides, workspace_scope=workspace_scope)
agents: Mapping[str, AgentConfig] = cfg.agents
display_agents_table(agents, display_context=display_context)
return 0
except Exception as e:
logger.error("Failed to list agents: {}", e)
return 1
def _handle_list_providers(
list_providers: bool,
*,
display_context: DisplayContext,
) -> int | None:
"""Handle --list-providers flag; returns exit code or None to continue."""
if not list_providers:
return None
try:
providers = fetch_providers()
display_providers_table(providers, display_context=display_context)
return 0
except Exception as e:
logger.error("Failed to list providers: {}", e)
return 1
def _handle_check_config(
config: str | None,
cli_overrides: dict[str, object],
check_config: bool,
*,
console: Console | None = None,
) -> int | None:
"""Handle --check-config flag; returns exit code or None to continue."""
if not check_config:
return None
c = console if console is not None else _get_cli_context().console
try:
config_path = _config_path(config)
workspace_scope = None if config_path is not None else resolve_workspace_scope()
load_config(config_path, cli_overrides, workspace_scope=workspace_scope)
c.print(Text("Configuration is valid", style="theme.status.success"))
return 0
except Exception as e:
logger.error("Configuration is invalid: {}", e)
return 1
def _handle_check_mcp(check_mcp: bool, *, console: Console | None = None) -> int | None:
"""Handle --check-mcp flag; returns exit code or None to continue."""
if not check_mcp:
return None
c = console if console is not None else _get_cli_context().console
validate_custom_mcp_servers = _load_validate_custom_mcp_servers()
try:
workspace_scope = resolve_workspace_scope()
rc = validate_custom_mcp_servers(workspace_scope.root)
except Exception as e:
logger.error("MCP validation failed: {}", e)
return 1
if rc == 0:
c.print(Text("MCP servers validated successfully", style="theme.status.success"))
else:
c.print(Text("MCP validation failed — see logs", style="theme.status.error"))
return rc
def _handle_commit_plumbing(
options: CommitPlumbingOptions,
*,
display_context: DisplayContext,
) -> int | None:
"""Handle commit plumbing commands; returns exit code or None to continue."""
if not (options.generate_commit_msg or options.generate_commit or options.show_commit_msg):
return None
commit_plumbing(options=options, display_context=display_context)
return 0
@dataclass(frozen=True)
class _RunPipelineOpts:
cli_overrides: dict[str, object]
dry_run: bool
resume: bool
no_resume: bool
verbosity: Verbosity = Verbosity.VERBOSE
counter_overrides: dict[str, int] | None = None
inline_prompt: str | None = None
parallel_worker_manifest: RuntimePath | None = None
def _run_pipeline(
config: str | None,
opts: _RunPipelineOpts,
*,
display_context: DisplayContext,
) -> int:
"""Run the main pipeline."""
c = display_context.console
try:
request = RunPipelineRequest(
config_path=_config_path(config),
cli_overrides=opts.cli_overrides,
dry_run=opts.dry_run,
resume=opts.resume and not opts.no_resume,
verbosity=opts.verbosity,
counter_overrides=opts.counter_overrides or {},
inline_prompt=opts.inline_prompt,
parallel_worker_manifest=opts.parallel_worker_manifest,
)
exit_code = run_pipeline(request, display_context=display_context)
return exit_code
except KeyboardInterrupt:
c.print(Text("\nInterrupted by user", style="theme.status.warning"))
return 130
except Exception as e:
logger.exception("Pipeline failed: {}")
err_text = Text()
err_text.append("Error:", style="theme.status.error")
err_text.append(" ")
err_text.append(str(e))
c.print(err_text)
return 1
def _configure_logging(verbosity: Verbosity) -> None:
"""Configure logging based on verbosity level."""
# Remove default handler
logger.remove()
if verbosity == Verbosity.QUIET:
logger.add(sys.stderr, level="ERROR")
elif verbosity == Verbosity.NORMAL:
logger.add(sys.stderr, level="INFO")
elif verbosity == Verbosity.VERBOSE:
logger.add(sys.stderr, level="DEBUG")
elif verbosity == Verbosity.FULL:
logger.add(sys.stderr, level="DEBUG", format="{time:HH:mm:ss} {level} {message}")
else: # DEBUG
logger.add(
sys.stderr,
level="TRACE",
format="{time:HH:mm:ss} {level} {name}:{function}:{line} {message}",
)
def _parse_counter_overrides(raw_entries: list[str]) -> dict[str, int]:
"""Parse NAME=VALUE counter override strings; raises UsageError on malformed input."""
result: dict[str, int] = {}
for entry in raw_entries:
if "=" not in entry:
raise click.UsageError(f"--counter: invalid format {entry!r} — expected NAME=VALUE")
name, _, raw_value = entry.partition("=")
name = name.strip()
if not name:
raise click.UsageError(f"--counter: blank counter name in {entry!r}")
try:
value = int(raw_value)
except ValueError:
raise click.UsageError(
f"--counter {name!r}: value {raw_value!r} is not a valid integer"
) from None
result[name] = value
return result
def _build_cli_overrides(
input: CLIOverrideInput,
) -> dict[str, object]:
"""Build CLI overrides dictionary from CLIOverrideInput."""
overrides: CLIOverrides = {
"general": {
"git_user_name": None,
"git_user_email": None,
"execution": {},
},
"developer_agent": None,
"developer_model": None,
}
if input.developer_agent is not None:
overrides["developer_agent"] = input.developer_agent
if input.developer_model is not None:
overrides["developer_model"] = input.developer_model
if input.git_user_name is not None:
overrides["general"]["git_user_name"] = input.git_user_name
if input.git_user_email is not None:
overrides["general"]["git_user_email"] = input.git_user_email
if input.developer_iters is not None:
overrides["general"]["developer_iters"] = input.developer_iters
return dict(overrides)
# Public aliases — test-accessible names and monkeypatch interception points.
bootstrap_global_configs = _bootstrap_global_configs
configure_logging = _configure_logging
handle_check_config = _handle_check_config
handle_check_mcp = _handle_check_mcp
handle_commit_plumbing = _handle_commit_plumbing
handle_list_agents = _handle_list_agents
handle_list_providers = _handle_list_providers
inject_quick_prompt = _inject_quick_prompt
parse_counter_overrides = _parse_counter_overrides
prepare_init_args = _prepare_init_args
build_cli_overrides = _build_cli_overrides
RunPipelineOpts = _RunPipelineOpts
invoke_pipeline = _run_pipeline
THOROUGH_DEVELOPER_ITERS = _THOROUGH_DEVELOPER_ITERS
if __name__ == "__main__":
app()