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:
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.
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, andbengal --versioncalledbuild_parser(), resolving every lazy command schema before producing metadata output.Before the local Bengal mitigation, representative local timings were:
bengal --version: ~1.08sbengal --help: ~2.45sbengal new --help: ~2.46sbengal build --help: ~2.47sAfter 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.11sbengal --help: ~0.11sbengal new --help: ~0.10sbengal cache inputs --source site --output-format json: ~0.58s before real command work dominatesThe workaround lives in Bengal's
BengalCLIsubclass, 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:
CommandDef/LazyCommandDefmetadata2. 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 onlygroup.leaf--format, output file, global options, context injection, confirmation prompts, middleware, before/after hooks, generator progress consumption, and result output semantics remain unchanged3. First-class lazy/precomputed schema contract
Milo already allows
schema=onlazy_command(...). It would help to make that a first-class workflow:tools/list, completions, llms.txt, and parser generation to use schemas without importing handlers where possible4. 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:
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
--llms-txtBengal workaround to retire later
Bengal currently carries local fast paths for this in
bengal/cli/milo_app.py:--versionOnce Milo supports the above, Bengal should delete that local bridge and return to framework-owned dispatch/help semantics.