From 4d4106505fecc93eed2767459bfb9e5a280ad8c0 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Wed, 24 Jun 2026 11:58:53 -0700 Subject: [PATCH] fix(seedgo,ai_mail): close CLI help-checker loophole + render ai_mail --help in Rich MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seedgo's cli/help_text/introspection standards are static source scans — they confirm a print_help function, console.print, and --help wiring exist, but never execute --help. So a module could score 100% while rendering raw argparse. ai_mail did exactly that via console.print(parser.format_help()), laundering argparse plain text through the approved console API and dodging the existing parser.print_help() ban. - seedgo: cli_check now flags .format_help(); cli.md/cli_content.py name it alongside print_help(); +2 regression tests (1095 pass, self-audit 100%) - ai_mail: rewrote print_help() to hand-rolled Rich (737 tests pass); --help now renders Rich with no raw argparse, Cli back to 100% legitimately - behavioral --help check (run it, assert not raw argparse) noted as a follow-up DPLAN-0217. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QEQZXCtgnF3NQtcttTErpq --- CHANGELOG.md | 12 +++ src/aipass/ai_mail/apps/ai_mail.py | 80 +++++++++---------- .../apps/handlers/aipass_standards/cli.md | 7 +- .../handlers/aipass_standards/cli_check.py | 20 +++++ .../handlers/aipass_standards/cli_content.py | 10 ++- .../seedgo/tests/test_checkers_batch6.py | 36 ++++++++- 6 files changed, 116 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 192693ad..20ca5fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,18 @@ PyPI version — not the changelog header. ### Fixed +- **seedgo CLI help checkers green-lit non-compliant `--help` output** — the + `cli`/`help_text`/`introspection` standards are static source scans (they + confirm a `print_help` function, `console.print`, and `--help` wiring exist) + but never execute `--help`, so a module could score 100% while rendering raw + argparse. `@ai_mail` did exactly that via `console.print(parser.format_help())`, + laundering argparse's plain text through the approved console API and dodging + the existing `parser.print_help()` ban. Closed the loophole: `cli_check` now + flags `.format_help()`, `cli.md`/`cli_content.py` name it alongside + `print_help()`, +2 regression tests. Also rewrote `@ai_mail`'s `print_help()` + to render hand-rolled Rich (the `--help` content was complete, just unstyled). + A behavioral `--help` check (run it, assert not raw argparse) is noted as a + follow-up. (DPLAN-0217) - **seedgo `readme_check` ignored the `(disabled)` marker in self-scans** — its module-list and test-count scans now skip `foo(disabled).py`, matching the central audit collector. An in-place disabled module no longer trips a false diff --git a/src/aipass/ai_mail/apps/ai_mail.py b/src/aipass/ai_mail/apps/ai_mail.py index 42294ea4..6c72ada6 100644 --- a/src/aipass/ai_mail/apps/ai_mail.py +++ b/src/aipass/ai_mail/apps/ai_mail.py @@ -16,7 +16,6 @@ Main handles routing, modules implement functionality. # Standard library imports import sys import importlib -import argparse import signal from pathlib import Path from typing import Any, List @@ -51,52 +50,47 @@ MODULES_DIR = MODULE_ROOT / "modules" def print_help(): - """Print drone-compliant help output""" - parser = argparse.ArgumentParser( - description="AI_MAIL Branch Operations - Email system for branch communication", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -COMMANDS: - dispatch - Send dispatch email + wake target (one step) - email - Send email to a branch - send - Send email (alias for email) - inbox - List emails (new + opened) - view - View email content (marks as opened) - reply - Reply to email (closes + archives) - close - Close email(s) without reply (archives) - sent - View sent messages - contacts - Manage contacts -EMAIL LIFECYCLE (v2): - new → opened → closed - - new: Just arrived, never viewed - - opened: You've viewed it, not yet resolved - - closed: Resolved (replied or dismissed), auto-archived + """Print drone-compliant help output with Rich markup""" + console.print() + console.print("[bold cyan]AI_MAIL — Email system for branch communication[/bold cyan]") + console.print() -USAGE: - drone @ai_mail [args] - drone @ai_mail --help + console.print("[yellow]COMMANDS:[/yellow]") + console.print(" [cyan]dispatch[/cyan] [dim]Send dispatch email + wake target (one step)[/dim]") + console.print(" [cyan]email[/cyan] [dim]Send email to a branch[/dim]") + console.print(" [cyan]send[/cyan] [dim]Send email (alias for email)[/dim]") + console.print(" [cyan]inbox[/cyan] [dim]List emails (new + opened)[/dim]") + console.print(" [cyan]view[/cyan] [dim]View email content (marks as opened)[/dim]") + console.print(" [cyan]reply[/cyan] [dim]Reply to email (closes + archives)[/dim]") + console.print(" [cyan]close[/cyan] [dim]Close email(s) without reply (archives)[/dim]") + console.print(" [cyan]sent[/cyan] [dim]View sent messages[/dim]") + console.print(" [cyan]contacts[/cyan] [dim]Manage contacts[/dim]") + console.print() -EXAMPLES: - # Dispatch (send + wake in one command) - drone @ai_mail dispatch @branch "Subject" "Body" - drone @ai_mail dispatch @branch "Subject" "Body" --fresh + console.print("[yellow]EMAIL LIFECYCLE (v2):[/yellow]") + console.print(" new → opened → closed") + console.print(" [dim]new: Just arrived, never viewed[/dim]") + console.print(" [dim]opened: You've viewed it, not yet resolved[/dim]") + console.print(" [dim]closed: Resolved (replied or dismissed), auto-archived[/dim]") + console.print() - # Send mail (no wake) - drone @ai_mail email @seedgo "Subject" "Msg" # Send to branch - drone @ai_mail email @all "Subject" "Msg" # Broadcast to all + console.print("[yellow]USAGE:[/yellow]") + console.print(" [cyan]drone @ai_mail[/cyan] [args]") + console.print(" [cyan]drone @ai_mail --help[/cyan]") + console.print() - # Check mail - drone @ai_mail inbox # List all emails - drone @ai_mail view abc123 # View email (marks as opened) - - # Resolve emails - drone @ai_mail reply abc123 "Thanks!" # Reply + close + archive - drone @ai_mail close abc123 # Close single email - drone @ai_mail close abc123 def456 ghi789 # Close multiple emails - drone @ai_mail close all # Close ALL emails - """, - ) - console.print(parser.format_help()) + console.print("[yellow]EXAMPLES:[/yellow]") + console.print(' [cyan]drone @ai_mail dispatch @branch "Subject" "Body"[/cyan]') + console.print(' [cyan]drone @ai_mail dispatch @branch "Subject" "Body" --fresh[/cyan]') + console.print(' [cyan]drone @ai_mail email @seedgo "Subject" "Msg"[/cyan] [dim]Send to branch[/dim]') + console.print(' [cyan]drone @ai_mail email @all "Subject" "Msg"[/cyan] [dim]Broadcast to all[/dim]') + console.print(" [cyan]drone @ai_mail inbox[/cyan] [dim]List all emails[/dim]") + console.print(" [cyan]drone @ai_mail view abc123[/cyan] [dim]View email[/dim]") + console.print(' [cyan]drone @ai_mail reply abc123 "Thanks!"[/cyan] [dim]Reply + close + archive[/dim]') + console.print(" [cyan]drone @ai_mail close abc123[/cyan] [dim]Close single email[/dim]") + console.print(" [cyan]drone @ai_mail close abc123 def456 ghi789[/cyan] [dim]Close multiple[/dim]") + console.print(" [cyan]drone @ai_mail close all[/cyan] [dim]Close ALL emails[/dim]") + console.print() # ============================================================================= diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/cli.md b/src/aipass/seedgo/apps/handlers/aipass_standards/cli.md index c3a570f7..b2c1b07d 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/cli.md +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/cli.md @@ -135,7 +135,9 @@ console.print("[cyan]This is the ONLY approved way to output text[/cyan]") ### Help Output: Manual Rich Formatting -**DO NOT use `parser.print_help()`** - it outputs plain text. +**DO NOT use `parser.print_help()` or `parser.format_help()`** - both produce plain argparse text. + +Wrapping `format_help()` in `console.print()` still renders plain text — it launders argparse output through the approved API without adding Rich formatting. **Argparse is for PARSING arguments only, NOT for help output.** @@ -161,6 +163,9 @@ def print_help(): # DON'T DO THIS - outputs plain text parser = argparse.ArgumentParser(...) parser.print_help() + +# DON'T DO THIS EITHER - format_help() returns plain text +console.print(parser.format_help()) ``` --- diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/cli_check.py b/src/aipass/seedgo/apps/handlers/aipass_standards/cli_check.py index ff2ab6de..364f3579 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/cli_check.py +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/cli_check.py @@ -333,6 +333,7 @@ def check_print_usage( # Find print() statements and raw stdout/stderr writes print_lines = [] parser_print_help_lines = [] + format_help_lines = [] raw_write_lines = [] # Track if we're inside an if __name__ == '__main__': block @@ -376,6 +377,15 @@ def check_print_usage( else: parser_print_help_lines.append(i) + # Check for .format_help() - returns plain argparse text + if ".format_help()" in stripped: + if "#" in line: + code_part = line.split("#")[0] + if ".format_help()" in code_part: + format_help_lines.append(i) + else: + format_help_lines.append(i) + # Check for raw sys.stdout.write() / sys.stderr.write() (bypasses Rich) if "sys.stdout.write(" in stripped or "sys.stderr.write(" in stripped: if "#" in line: @@ -412,6 +422,16 @@ def check_print_usage( ), } + if format_help_lines: + return { + "name": "print() usage", + "passed": False, + "message": ( + f"Found .format_help() in {filename} on lines " + f"{format_help_lines[:3]} (returns plain argparse text - write Rich help instead)" + ), + } + if raw_write_lines: return { "name": "print() usage", diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/cli_content.py b/src/aipass/seedgo/apps/handlers/aipass_standards/cli_content.py index e356db2b..1ca7c59f 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/cli_content.py +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/cli_content.py @@ -35,10 +35,11 @@ def get_cli_standards() -> str: " • Only in test/temp code", " • Remove before commit", "", - "[red]✗ Never use:[/red] parser.print_help()", - " • Outputs plain text (violates standard)", + "[red]✗ Never use:[/red] parser.print_help() or parser.format_help()", + " • Both produce plain argparse text (violates standard)", + " • console.print(parser.format_help()) still renders plain text", " • Argparse is for PARSING only, not help output", - " • Write custom print_help() with console.print()", + " • Write custom print_help() with console.print() and Rich markup", "", "─" * 70, "", @@ -84,7 +85,8 @@ def get_cli_standards() -> str: "[bold cyan]RICH FORMATTING QUICK REFERENCE:[/bold cyan]", "", "[bold]Colors:[/bold]", - " [red]red[/red], [green]green[/green], [yellow]yellow[/yellow], [blue]blue[/blue], [cyan]cyan[/cyan], [magenta]magenta[/magenta]", + " [red]red[/red], [green]green[/green], [yellow]yellow[/yellow]," + " [blue]blue[/blue], [cyan]cyan[/cyan], [magenta]magenta[/magenta]", "", "[bold]Styles:[/bold]", " [bold]bold[/bold], [italic]italic[/italic], [dim]dim[/dim], [underline]underline[/underline]", diff --git a/src/aipass/seedgo/tests/test_checkers_batch6.py b/src/aipass/seedgo/tests/test_checkers_batch6.py index 11d35485..f603329f 100644 --- a/src/aipass/seedgo/tests/test_checkers_batch6.py +++ b/src/aipass/seedgo/tests/test_checkers_batch6.py @@ -50,17 +50,27 @@ def _mock_infrastructure(monkeypatch): json_mod, ) - # -- bypass handler (used by architecture_check) ------------------------ + # -- bypass handler (used by architecture_check + cli_check) ------------- bypass_pkg = MagicMock() bypass_ignore = MagicMock() bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[]) bypass_pkg.ignore_handler = bypass_ignore + from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed as _real_is_bypassed + + bypass_utils = MagicMock() + bypass_utils.is_bypassed = _real_is_bypassed + bypass_pkg.utils = bypass_utils monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg) monkeypatch.setitem( sys.modules, "aipass.seedgo.apps.handlers.bypass.ignore_handler", bypass_ignore, ) + monkeypatch.setitem( + sys.modules, + "aipass.seedgo.apps.handlers.bypass.utils", + bypass_utils, + ) # Force re-imports so checkers pick up fresh mocks for mod_name in [ @@ -545,6 +555,30 @@ class TestCheckPrintUsage: assert result["passed"] is False assert "parser.print_help()" in result["message"] + def test_format_help_fails(self): + """console.print(parser.format_help()) fails — plain argparse text.""" + from aipass.seedgo.apps.handlers.aipass_standards.cli_check import ( + check_print_usage, + ) + + content = "console.print(parser.format_help())\n" + lines = _lines(content) + result = check_print_usage(content, lines, "/module.py") + assert result is not None + assert result["passed"] is False + assert ".format_help()" in result["message"] + + def test_format_help_in_comment_ignored(self): + """.format_help() inside a comment is not flagged.""" + from aipass.seedgo.apps.handlers.aipass_standards.cli_check import ( + check_print_usage, + ) + + content = "# parser.format_help() should not be used\n" + lines = _lines(content) + result = check_print_usage(content, lines, "/module.py") + assert result is None + def test_sys_stdout_write_fails(self): """sys.stdout.write() usage fails.""" from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (