Skip to content

Fix group help display and refactor command resolution#33

Merged
lbliii merged 3 commits intomainfrom
lbliii/group-help-fix
Apr 10, 2026
Merged

Fix group help display and refactor command resolution#33
lbliii merged 3 commits intomainfrom
lbliii/group-help-fix

Conversation

@lbliii
Copy link
Copy Markdown
Owner

@lbliii lbliii commented Apr 10, 2026

Summary

  • Bug 1: Bare group invocation (myapp config) showed "Unknown command: 'config'. Did you mean 'config'?" instead of group help — now displays the group's subcommands and description
  • Bug 2: Help output showed raw _command positional arg instead of listing subcommands by name — root and group help now render from CLI/Group registries instead of extracting from argparse internals
  • Introduces ResolvedCommand/ResolvedGroup/ResolvedNothing discriminated union so command resolution distinguishes all three outcomes cleanly
  • Adds Group.format_help() and CLI._format_root_help() that build help state directly from command/group registries, eliminating dependence on argparse private APIs
  • Suppresses noisy defaults ((default: False), (default: ), (default: 0)) in help template and tightens section whitespace

Test plan

  • 14 new tests across TestResolveResult, TestGroupBareInvocation, TestHelpSubcommandListing
  • All 162 existing tests pass with no regressions
  • Verify myapp, myapp config, myapp site config all show correct help
  • Verify myapp cofnig still shows did-you-mean suggestion

🤖 Generated with Claude Code

Two bugs: (1) bare group invocation (`myapp config`) showed "Unknown
command" instead of group help, (2) help output showed raw `_command`
positional arg instead of listing subcommands by name.

Introduces ResolvedCommand/ResolvedGroup/ResolvedNothing union type so
`_resolve_command_from_args` distinguishes commands, groups, and misses.
Group and root help now render from CLI/Group registries instead of
extracting from argparse internals. Suppresses noisy defaults
(False, 0, empty string) in help template and tightens whitespace.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 10, 2026 20:03
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 10, 2026

Coverage Report

87.5% overall coverage

File Coverage
src/milo/\_\_init\_\_.py 100.0%
src/milo/\_child.py 92.7%
src/milo/\_cli\_help.py 90.5%
src/milo/\_command\_defs.py 96.2%
src/milo/\_compat.py 63.3%
src/milo/\_errors.py 95.4%
src/milo/\_jsonrpc.py 100.0%
src/milo/\_mcp\_router.py 100.0%
src/milo/\_protocols.py 100.0%
src/milo/\_types.py 100.0%
src/milo/app.py 57.8%
src/milo/cli.py 98.6%
src/milo/commands.py 88.5%
src/milo/completions.py 96.0%
src/milo/config.py 86.8%
src/milo/context.py 87.1%
src/milo/dev.py 91.9%
src/milo/doctor.py 89.9%
src/milo/flow.py 96.2%
src/milo/form.py 87.0%
src/milo/gateway.py 74.0%
src/milo/groups.py 96.6%
src/milo/help.py 100.0%
src/milo/input/\_\_init\_\_.py 100.0%
src/milo/input/\_platform.py 77.8%
src/milo/input/\_reader.py 96.2%
src/milo/input/\_sequences.py 100.0%
src/milo/llms.py 78.7%
src/milo/mcp.py 81.9%
src/milo/middleware.py 100.0%
src/milo/observability.py 100.0%
src/milo/output.py 70.2%
src/milo/pipeline.py 85.6%
src/milo/plugins.py 100.0%
src/milo/reducers.py 100.0%
src/milo/registry.py 78.6%
src/milo/schema.py 95.0%
src/milo/state.py 91.9%
src/milo/streaming.py 100.0%
src/milo/templates/\_\_init\_\_.py 93.3%
src/milo/testing/\_\_init\_\_.py 100.0%
src/milo/testing/\_mcp.py 100.0%
src/milo/testing/\_record.py 85.5%
src/milo/testing/\_replay.py 87.1%
src/milo/testing/\_snapshot.py 100.0%
src/milo/theme.py 100.0%
src/milo/version\_check.py 62.7%

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes CLI help rendering for command groups and refactors command resolution to distinguish between resolved commands, resolved groups (bare group invocation), and no match.

Changes:

  • Introduces ResolvedCommand / ResolvedGroup / ResolvedNothing and updates dispatch logic to use these outcomes.
  • Refactors root and group help output to render from CLI/Group registries instead of argparse positional internals.
  • Updates help template formatting and adds targeted tests for resolution and help output.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_groups.py Adds tests covering resolve-result types, bare group invocation behavior, and help subcommand listing.
src/milo/templates/help.kida Extends template to render registry-driven commands and options sections and suppress noisy defaults.
src/milo/help.py Extends HelpState with commands and options to support registry-driven help rendering.
src/milo/groups.py Adds Group.format_help() to render group help from registries.
src/milo/commands.py Adds resolve-result dataclasses, refactors command/group resolution + dispatch, and adds _format_root_help().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +785 to +787
if isinstance(result, ResolvedGroup):
result.group.format_help(self.name)
return None
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For nested groups, group.format_help(self.name) only prefixes the CLI name, so bengal site config will render as bengal config in the help header. To show the correct invocation path, carry the resolved group path/prefix through _resolve_group_command (e.g., accumulate prog_prefix during recursion or store a full prog string on ResolvedGroup) and pass that into format_help.

Copilot uses AI. Check for mistakes.
Comment on lines +974 to +1005
# Built-in options
builtin_opts = [
{"flags": "-h, --help", "help": "Show this help message and exit"},
{"flags": "-v, --verbose", "help": "Increase verbosity (-v verbose, -vv debug)"},
{"flags": "-q, --quiet", "help": "Suppress non-error output"},
{"flags": "--no-color", "help": "Disable color output"},
{"flags": "-n, --dry-run", "help": "Show what would happen without making changes"},
{
"flags": "-o, --output-file",
"metavar": "FILE",
"help": "Write output to FILE instead of stdout",
},
]

if self.version:
builtin_opts.insert(0, {"flags": "--version", "help": f"Show version ({self.version})"})

# User-defined global options
for opt in self._global_options:
flags = f"--{opt.name.replace('_', '-')}"
if opt.short:
flags = f"{opt.short}, {flags}"
entry: dict[str, Any] = {"flags": flags, "help": opt.description}
if opt.default and not opt.is_flag:
entry["default"] = str(opt.default)
builtin_opts.append(entry)

state = HelpState(
prog=self.name,
description=self.description,
commands=commands,
options=tuple(builtin_opts),
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_format_root_help() hardcodes a small set of built-in options, but build_parser() also registers --llms-txt, --mcp, --mcp-install, --mcp-uninstall, and --completions (and potentially others). With the new registry-based root help, those flags will no longer appear in myapp --help. Consider generating this list from the actual parser (or maintaining a single shared built-in option registry) so root help stays in sync with the real CLI flags.

Suggested change
# Built-in options
builtin_opts = [
{"flags": "-h, --help", "help": "Show this help message and exit"},
{"flags": "-v, --verbose", "help": "Increase verbosity (-v verbose, -vv debug)"},
{"flags": "-q, --quiet", "help": "Suppress non-error output"},
{"flags": "--no-color", "help": "Disable color output"},
{"flags": "-n, --dry-run", "help": "Show what would happen without making changes"},
{
"flags": "-o, --output-file",
"metavar": "FILE",
"help": "Write output to FILE instead of stdout",
},
]
if self.version:
builtin_opts.insert(0, {"flags": "--version", "help": f"Show version ({self.version})"})
# User-defined global options
for opt in self._global_options:
flags = f"--{opt.name.replace('_', '-')}"
if opt.short:
flags = f"{opt.short}, {flags}"
entry: dict[str, Any] = {"flags": flags, "help": opt.description}
if opt.default and not opt.is_flag:
entry["default"] = str(opt.default)
builtin_opts.append(entry)
state = HelpState(
prog=self.name,
description=self.description,
commands=commands,
options=tuple(builtin_opts),
def _root_options_from_parser() -> tuple[dict[str, Any], ...]:
parser = self.build_parser()
options: list[dict[str, Any]] = []
for action in parser._actions:
if not getattr(action, "option_strings", None):
continue
if action.help == argparse.SUPPRESS:
continue
entry: dict[str, Any] = {
"flags": ", ".join(action.option_strings),
"help": action.help or "",
}
metavar = getattr(action, "metavar", None)
if metavar not in (None, argparse.SUPPRESS) and action.nargs != 0:
if isinstance(metavar, tuple):
entry["metavar"] = " ".join(str(part) for part in metavar)
else:
entry["metavar"] = str(metavar)
default = getattr(action, "default", argparse.SUPPRESS)
if (
default not in (None, argparse.SUPPRESS)
and default is not False
and action.nargs != 0
):
entry["default"] = str(default)
options.append(entry)
return tuple(options)
state = HelpState(
prog=self.name,
description=self.description,
commands=commands,
options=_root_options_from_parser(),

Copilot uses AI. Check for mistakes.
Comment on lines +184 to +214
def format_help(self, prog_prefix: str = "") -> str:
"""Render help from this group's command/group registries.

Returns the rendered help string. Does not print.
"""
import sys

from milo.help import HelpState
from milo.templates import get_env

prog = f"{prog_prefix} {self.name}".strip() if prog_prefix else self.name
commands = tuple(
[
{"name": cmd.name, "help": getattr(cmd, "description", "")}
for cmd in self._commands.values()
if not getattr(cmd, "hidden", False)
]
+ [
{"name": g.name, "help": g.description}
for g in self._groups.values()
if not g.hidden
]
)

state = HelpState(prog=prog, description=self.description, commands=commands)
env = get_env()
template = env.get_template("help.kida")
output = template.render(state=state)
sys.stdout.write(output + "\n")
sys.stdout.flush()
return output
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says format_help() "Does not print", but the implementation writes to sys.stdout and flushes. Either update the docstring to reflect the side effect, or change the method to only return the rendered string and let the caller decide where/how to output it (which also improves testability).

Copilot uses AI. Check for mistakes.
…ing fix

- Add prog field to ResolvedGroup and accumulate full path through group
  resolution so nested groups render correct prog prefix (e.g. "bengal site config")
- Replace hardcoded built-in options in _format_root_help() with parser-derived
  options so new flags like --llms-txt, --mcp are never missed
- Fix Group.format_help() docstring to accurately reflect stdout writing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lbliii lbliii merged commit 1187e67 into main Apr 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants