Skip to content

Keep lazy-command CLIs fast without full parser construction #70

@lbliii

Description

@lbliii

Problem

Bengal's CLI became noticeably slow for very cheap interactions because Milo currently builds the full argparse tree before it can answer root help, group help, --version, or execute one selected command.

In a large lazy-command app, full parser construction defeats the main benefit of lazy_command(...): every leaf command schema is resolved, which imports every command module even when the user only asked for metadata or one command.

Bengal evidence

In Bengal, profiling showed three separate framework-level costs:

  • bengal --help, bengal new, and bengal --version called build_parser(), resolving every lazy command schema before producing metadata output.
  • Branded help/error rendering imported and compiled the Kida output template stack for cheap metadata paths.
  • Ordinary single-command execution also built the full parser and resolved all registered lazy commands before running the selected command.

Before the local Bengal mitigation, representative local timings were:

  • bengal --version: ~1.08s
  • bengal --help: ~2.45s
  • bengal new --help: ~2.46s
  • bengal build --help: ~2.47s

After a local workaround that bypasses full parser construction for cheap metadata paths and builds a parser only for the selected command path:

  • bengal --version: ~0.11s
  • bengal --help: ~0.11s
  • bengal new --help: ~0.10s
  • bengal cache inputs --source site --output-format json: ~0.58s before real command work dominates

The workaround lives in Bengal's BengalCLI subclass, but it duplicates enough Milo behavior that it should move upstream.

Requested Milo changes

1. Shallow root/group help

Root and group help should render from registered command metadata without resolving leaf schemas.

Expected behavior:

  • root help lists top-level commands/groups from CommandDef/LazyCommandDef metadata
  • group help lists immediate child commands/groups
  • leaf command help resolves only that leaf command schema
  • lazy sibling commands are not imported

2. Selected-command parser construction

Milo should parse global options and the command path first, then build/resolve only the selected leaf command parser.

Expected behavior:

  • cli.run(["group", "leaf", ...]) resolves only group.leaf
  • sibling commands are not imported
  • command aliases and group aliases continue to work
  • --format, output file, global options, context injection, confirmation prompts, middleware, before/after hooks, generator progress consumption, and result output semantics remain unchanged

3. First-class lazy/precomputed schema contract

Milo already allows schema= on lazy_command(...). It would help to make that a first-class workflow:

  • document how apps should precompute schemas
  • provide a helper or cache format for persisted schemas
  • allow tools/list, completions, llms.txt, and parser generation to use schemas without importing handlers where possible
  • make missing schema fallback explicit and measurable

4. Public root option metadata API

Custom help renderers need access to Milo's built-in root options without duplicating them.

Requested API shape could be something like:

cli.root_option_specs()

or exported data used by both build_parser() and help renderers.

This should include flags, metavar, help text, defaults/choices when relevant, and user-defined global options.

5. Renderer separation for metadata paths

Help/error formatting should be able to use a text-only fast path or app-provided renderer without importing template engines for metadata-only commands.

This does not mean removing Kida support; it means the framework should not force template imports before cheap root/group metadata can be shown.

Acceptance criteria

  • Add a Milo test where a CLI has multiple lazy commands, one sibling command raises if imported, and:
    • root help does not import the sibling
    • group help does not import the sibling
    • leaf help imports only that leaf
    • selected command execution imports only that command
  • Existing full-tree modes still work:
    • --llms-txt
    • completions
    • MCP server/tool listing
  • Existing parser-conflict coverage remains, but can run as an explicit full-parser test rather than on every metadata path.
  • Custom CLI subclasses can render help from metadata without copying Milo built-in option definitions.

Bengal workaround to retire later

Bengal currently carries local fast paths for this in bengal/cli/milo_app.py:

  • registry-only root/group/leaf help
  • fast --version
  • selected-command parser path
  • root option parity test against Milo's full parser

Once Milo supports the above, Bengal should delete that local bridge and return to framework-owned dispatch/help semantics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions