Skip to content

feat(cli): generate section-1 man SYNOPSIS and OPTIONS from Typer - #1524

Open
FBISiri wants to merge 2 commits into
basicmachines-co:mainfrom
FBISiri:feat/610-section1-typer
Open

feat(cli): generate section-1 man SYNOPSIS and OPTIONS from Typer#1524
FBISiri wants to merge 2 commits into
basicmachines-co:mainfrom
FBISiri:feat/610-section1-typer

Conversation

@FBISiri

@FBISiri FBISiri commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What

Implements the section-1 slice of #610: the eight bundled section-1 man pages
(apropos, cat, find, grep, head, ls, tail, tree) now have their
SYNOPSIS and OPTIONS blocks generated from the Typer command tree and
locked by a byte-equality drift test — the CLI counterpart of what #1478 did for
section-3 PARAMETERS from the MCP registry.

Why — the drift being fixed

These pages declared generated: hand, and their SYNOPSIS/OPTIONS were manual
restatements of the POSIX verb definitions in cli/commands/posix.py. They had
already drifted: grep(1)'s SYNOPSIS listed --json, --plain, --project,
--local, --cloud, while its OPTIONS block documented only -F and the
pagination flags. Generating both blocks from the single source of truth (the
command tree) makes that class of drift impossible and CI-visible.

How

  • src/basic_memory/man/__init__.pyrender_cli_synopsis() /
    render_options() render from a resolved Click command (introspected via its
    public attributes; no Typer/Click import at module load). Added
    _SYNOPSIS_BODY_RE / _OPTIONS_RE plus extract_* / replace_* helpers that
    mirror the existing PARAMETERS machinery. declare_registry_ownership() is now
    a thin wrapper over a parameterized declare_ownership(text, owner=...), so
    section-3 call sites are untouched.
  • scripts/update_man_pages.py — a section-1 branch inside the existing
    just man-regen pipeline; resolve_cli_command() walks the command tree
    (aproposbm man apropos, a verb on the man subgroup).
  • tests/test_man_pages.py — drift test (byte-equal to the live rendering,
    failure message points at just man-regen), generated: cli ownership,
    shared/global flags present in OPTIONS, alias + paired-boolean rendering,
    block-scoped replacement (curated sections byte-identical).
  • docs/manual-pages.md — ownership token note + roadmap updated.

Maintainer-ratified decisions (from #610)

  • generated: cli is the ownership token (not registry/typer); section-3
    stays registry.
  • OPTIONS renders the complete public option list, including the shared
    output/routing flags earlier hand-written blocks left out — grep(1) grows
    from four bullets to the full set. That growth is the point, not a regression.
  • CLI-specific syntax preserved: flag aliases render short-first as
    -F, --literal, and paired booleans as --frontmatter / --no-frontmatter
    (verified against Click 8.x Option.secondary_opts).

Scope / boundaries

  • Curated sections (NAME, DESCRIPTION, EXAMPLES, SEE ALSO, PREDICATE GRAMMAR)
    come out byte-identical — verified programmatically across all eight pages.
  • The hand-owned groff sources (man/bm.1, man/basic-memory.1) are out of
    scope
    and untouched.
  • cli/commands/posix.py needed no changes: introspection confirmed every
    option already carries help= text.
  • find(1) shipped two shell forms in two fenced blocks; the generator collapses
    them to the one Typer advertises, so the whole SYNOPSIS body (not just the
    first fence) is replaced.

Follow-up (out of this slice's write scope)

plugins/claude-code/schemas/manpage.md still lists the generated enum as
[registry, typer, hand]. Per the maintainer's cli decision it should gain
cli (its validation is warn and it is not applied to the bundled pages, so
nothing breaks meanwhile). The section-1 page → bm command mapping lives in the
generator (SECTION1_COMMAND_PATHS) rather than a command: frontmatter field,
because man1/*.md is only ever rewritten by just man-regen.

Verification

  • uv run pytest tests/test_man_pages.py -q → 48 passed (man/__init__.py 100% cov)
  • uv run pytest tests/cli/test_man_command.py tests/cli/test_cli_man_lookup.py -q → 35 passed
  • just man-regen && git diff --exit-code → idempotent, no diff on second run
  • ruff check/ruff format --check clean on changed files; ty check clean

Refs #610

The eight bundled section-1 pages (apropos, cat, find, grep, head, ls,
tail, tree) declared `generated: hand`, and their SYNOPSIS and OPTIONS
blocks were hand restatements of the POSIX verb definitions in
cli/commands/posix.py. They had already drifted: grep(1)'s SYNOPSIS listed
--json/--plain/--project/--local/--cloud while its OPTIONS documented only
-F and the pagination flags.

Render both blocks from the Typer command tree and lock them with a
byte-equality drift test, mirroring what basicmachines-co#1478 did for section-3
PARAMETERS from the MCP registry:

- man/__init__.py: render_cli_synopsis()/render_options() from a resolved
  Click command; _SYNOPSIS_BODY_RE/_OPTIONS_RE + extract/replace helpers;
  declare_ownership(text, owner=...) with declare_registry_ownership kept
  as a thin wrapper.
- scripts/update_man_pages.py: a section-1 branch in the existing
  `just man-regen` pipeline; resolve_cli_command() walks the command tree
  (apropos maps to `bm man apropos`).
- tests/test_man_pages.py: drift test, ownership, shared/global flags in
  OPTIONS, alias + paired-boolean rendering, block-scoped replacement.

Per the maintainer's call (basicmachines-co#610), the ownership token is `generated: cli`.
OPTIONS renders the complete public option list including the shared
output/routing flags (grep(1) grows from four bullets to the full set),
and preserves CLI syntax PARAMETERS has no concept of: flag aliases
(`-F, --literal`) and paired booleans (`--frontmatter / --no-frontmatter`).
Curated sections (NAME, DESCRIPTION, EXAMPLES, SEE ALSO) stay byte-identical
and hand-owned; the groff sources are out of scope.

Refs basicmachines-co#610

Signed-off-by: FBISiri <masteragentsiri@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e49576a209

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/basic_memory/man/__init__.py Outdated
metavar = param.name.upper()
tokens.append(metavar if param.required else f"[{metavar}]")
for param in command.params:
if param.param_type_name != "option" or getattr(param, "hidden", False):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace the speculative visibility fallback with a typed parameter

Because command and its parameters are typed as Any, this fallback silently treats an unsupported parameter shape without hidden as public and emits it into the manual instead of failing regeneration. Define a narrow protocol for the Click attributes consumed here, or use Typer's vendored Click types and access param.hidden explicitly; the same fallback also appears in render_options.

AGENTS.md reference: AGENTS.md:L132-L133

Useful? React with 👍 / 👎.

Comment thread src/basic_memory/man/man1/apropos(1).md Outdated
Comment on lines +19 to +20
bm man apropos QUERY [--project PROJECT] [--json] [--plain] [--local]
[--cloud]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve mutually exclusive option groups in generated synopses

The regenerated synopsis presents --json/--plain and --local/--cloud as independently combinable options, but the CLI explicitly rejects both pairs: tests/cli/test_cli_man_lookup.py:124-139 verifies that these invocations exit 1. The previous synopsis used | groups, so the generator needs explicit knowledge of these cross-parameter constraints rather than flattening every standalone Click option.

Useful? React with 👍 / 👎.

Comment thread src/basic_memory/man/__init__.py Outdated
Comment on lines +366 to +369
elif param.is_flag:
tokens.append(f"[{opt}]")
else:
tokens.append(f"[{opt} {param.name.upper()}]")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Render repeatable Click options as repeatable

When a Click option has multiple=True, this branch still emits exactly one occurrence. The current find --meta option is an Optional[list[str]] explicitly documented as repeatable, yet its generated synopsis regresses from [--meta PREDICATE ...] to [--meta META]; inspect the option's multiplicity and retain repetition notation so multi-predicate usage remains accurately discoverable.

Useful? React with 👍 / 👎.

Comment thread docs/manual-pages.md
name: write-note # page name without section suffix
summary: create or overwrite a markdown note in the knowledge base
generated: hand # hand | registry | typer (regeneration ownership)
generated: hand # hand | registry | cli (regeneration ownership)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add the cli owner to the shipped Manpage schema

The documentation now declares cli as a valid ownership token and all eight regenerated section-1 pages use it, but plugins/claude-code/schemas/manpage.md:18 still permits only [registry, typer, hand]. A user following this document and copying the shipped opt-in schema will therefore get an enum-mismatch warning when validating these pages; update both that enum and its remaining typer prose in the same change.

Useful? React with 👍 / 👎.

Address the chatgpt-codex review on basicmachines-co#1524:

- Replace the speculative `getattr(param, "hidden", False)` fallback in
  render_cli_synopsis/render_options with a structural ClickParam/ClickCommand
  Protocol and read `param.hidden` directly, so an unexpected parameter shape
  fails fast instead of being silently treated as public (AGENTS.md: no
  speculative getattr). The Protocol stays structural, so no Click import is
  pulled into the lightweight `basic_memory.man` module.

- Render mutually exclusive option pairs (--json/--plain, --local/--cloud) as a
  single `[--a | --b]` alternative in the shell SYNOPSIS, matching the CLI's own
  rejection of both flags together, instead of flattening them into freely
  combinable tokens.

- Keep Click `multiple=True` options repeatable: `find --meta` renders
  `[--meta META ...]` again rather than degrading to `[--meta META]`.

- Add `cli` to the shipped Manpage schema ownership enum and fix its residual
  `typer` prose, so a user copying the opt-in schema validates the regenerated
  section-1 pages without an enum-mismatch warning.

Regenerated all 8 section-1 pages via scripts/update_man_pages.py; regeneration
is idempotent. Added regression tests for the grouped mutex pairs, the repeatable
`...` notation, and hidden-option exclusion.

Refs basicmachines-co#610

Signed-off-by: FBISiri <masteragentsiri@gmail.com>
@FBISiri

FBISiri commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Status check-in, plus the bits that are actually mine to flag.

This is the slice (B) implementation following your 09-07 direction: generated: cli on all eight section-1 pages, complete public options list including shared flags, DESCRIPTION/EXAMPLES left hand-owned, groff sources untouched.

Rather than make you reverse-engineer +706/-98, the short review map:

  • src/basic_memory/man/__init__.py (+279) — Typer-tree walker that renders SYNOPSIS/OPTIONS
  • scripts/update_man_pages.py (+81) — hooks it into the just man-regen pipeline
  • tests/test_man_pages.py (+230) — drift test extended to cover section 1

One judgment call worth your eye, since it's the only spot where I picked semantics over mechanics: mutually-exclusive groups are declared explicitly, not inferred. --json|--plain and --local|--cloud render grouped; --project/--project-id deliberately do not, because --project-id takes precedence rather than conflicting, and grouping them would describe the CLI incorrectly. The cost is that a future mutex pair has to be added to that declaration or it silently renders flat.

So: would you rather the generator infer groups from Click metadata instead of relying on a declared list? That's a real design change and I'd much rather do it now than after merge.

CI is 10/10 green on d793482d and the branch is still clean against main. Happy to rebase, or to split the eight regenerated man1 pages into their own commit if that makes the diff easier to read — say which you prefer and I'll turn it around.

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.

1 participant