fix(seedgo,ai_mail): close CLI help-checker loophole + render ai_mail --help in Rich

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEQZXCtgnF3NQtcttTErpq
This commit is contained in:
AIOSAI
2026-06-24 11:58:53 -07:00
co-authored by Claude Opus 4.8
parent 4af5b8bf63
commit 4d4106505f
6 changed files with 116 additions and 49 deletions
+12
View File
@@ -13,6 +13,18 @@ PyPI version — not the changelog header.
### Fixed ### 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 - **seedgo `readme_check` ignored the `(disabled)` marker in self-scans** — its
module-list and test-count scans now skip `foo(disabled).py`, matching the 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 central audit collector. An in-place disabled module no longer trips a false
+37 -43
View File
@@ -16,7 +16,6 @@ Main handles routing, modules implement functionality.
# Standard library imports # Standard library imports
import sys import sys
import importlib import importlib
import argparse
import signal import signal
from pathlib import Path from pathlib import Path
from typing import Any, List from typing import Any, List
@@ -51,52 +50,47 @@ MODULES_DIR = MODULE_ROOT / "modules"
def print_help(): def print_help():
"""Print drone-compliant help output""" """Print drone-compliant help output with Rich markup"""
parser = argparse.ArgumentParser( console.print()
description="AI_MAIL Branch Operations - Email system for branch communication", console.print("[bold cyan]AI_MAIL — Email system for branch communication[/bold cyan]")
formatter_class=argparse.RawDescriptionHelpFormatter, console.print()
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
USAGE: console.print("[yellow]COMMANDS:[/yellow]")
drone @ai_mail <command> [args] console.print(" [cyan]dispatch[/cyan] [dim]Send dispatch email + wake target (one step)[/dim]")
drone @ai_mail --help 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: console.print("[yellow]EMAIL LIFECYCLE (v2):[/yellow]")
# Dispatch (send + wake in one command) console.print(" new → opened → closed")
drone @ai_mail dispatch @branch "Subject" "Body" console.print(" [dim]new: Just arrived, never viewed[/dim]")
drone @ai_mail dispatch @branch "Subject" "Body" --fresh 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) console.print("[yellow]USAGE:[/yellow]")
drone @ai_mail email @seedgo "Subject" "Msg" # Send to branch console.print(" [cyan]drone @ai_mail[/cyan] <command> [args]")
drone @ai_mail email @all "Subject" "Msg" # Broadcast to all console.print(" [cyan]drone @ai_mail --help[/cyan]")
console.print()
# Check mail console.print("[yellow]EXAMPLES:[/yellow]")
drone @ai_mail inbox # List all emails console.print(' [cyan]drone @ai_mail dispatch @branch "Subject" "Body"[/cyan]')
drone @ai_mail view abc123 # View email (marks as opened) 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]')
# Resolve emails console.print(' [cyan]drone @ai_mail email @all "Subject" "Msg"[/cyan] [dim]Broadcast to all[/dim]')
drone @ai_mail reply abc123 "Thanks!" # Reply + close + archive console.print(" [cyan]drone @ai_mail inbox[/cyan] [dim]List all emails[/dim]")
drone @ai_mail close abc123 # Close single email console.print(" [cyan]drone @ai_mail view abc123[/cyan] [dim]View email[/dim]")
drone @ai_mail close abc123 def456 ghi789 # Close multiple emails console.print(' [cyan]drone @ai_mail reply abc123 "Thanks!"[/cyan] [dim]Reply + close + archive[/dim]')
drone @ai_mail close all # Close ALL emails 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(parser.format_help()) console.print()
# ============================================================================= # =============================================================================
@@ -135,7 +135,9 @@ console.print("[cyan]This is the ONLY approved way to output text[/cyan]")
### Help Output: Manual Rich Formatting ### 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.** **Argparse is for PARSING arguments only, NOT for help output.**
@@ -161,6 +163,9 @@ def print_help():
# DON'T DO THIS - outputs plain text # DON'T DO THIS - outputs plain text
parser = argparse.ArgumentParser(...) parser = argparse.ArgumentParser(...)
parser.print_help() parser.print_help()
# DON'T DO THIS EITHER - format_help() returns plain text
console.print(parser.format_help())
``` ```
--- ---
@@ -333,6 +333,7 @@ def check_print_usage(
# Find print() statements and raw stdout/stderr writes # Find print() statements and raw stdout/stderr writes
print_lines = [] print_lines = []
parser_print_help_lines = [] parser_print_help_lines = []
format_help_lines = []
raw_write_lines = [] raw_write_lines = []
# Track if we're inside an if __name__ == '__main__': block # Track if we're inside an if __name__ == '__main__': block
@@ -376,6 +377,15 @@ def check_print_usage(
else: else:
parser_print_help_lines.append(i) 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) # Check for raw sys.stdout.write() / sys.stderr.write() (bypasses Rich)
if "sys.stdout.write(" in stripped or "sys.stderr.write(" in stripped: if "sys.stdout.write(" in stripped or "sys.stderr.write(" in stripped:
if "#" in line: 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: if raw_write_lines:
return { return {
"name": "print() usage", "name": "print() usage",
@@ -35,10 +35,11 @@ def get_cli_standards() -> str:
" • Only in test/temp code", " • Only in test/temp code",
" • Remove before commit", " • Remove before commit",
"", "",
"[red]✗ Never use:[/red] parser.print_help()", "[red]✗ Never use:[/red] parser.print_help() or parser.format_help()",
" • Outputs plain text (violates standard)", " • Both produce plain argparse text (violates standard)",
" • console.print(parser.format_help()) still renders plain text",
" • Argparse is for PARSING only, not help output", " • 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, "─" * 70,
"", "",
@@ -84,7 +85,8 @@ def get_cli_standards() -> str:
"[bold cyan]RICH FORMATTING QUICK REFERENCE:[/bold cyan]", "[bold cyan]RICH FORMATTING QUICK REFERENCE:[/bold cyan]",
"", "",
"[bold]Colors:[/bold]", "[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]Styles:[/bold]",
" [bold]bold[/bold], [italic]italic[/italic], [dim]dim[/dim], [underline]underline[/underline]", " [bold]bold[/bold], [italic]italic[/italic], [dim]dim[/dim], [underline]underline[/underline]",
@@ -50,17 +50,27 @@ def _mock_infrastructure(monkeypatch):
json_mod, json_mod,
) )
# -- bypass handler (used by architecture_check) ------------------------ # -- bypass handler (used by architecture_check + cli_check) -------------
bypass_pkg = MagicMock() bypass_pkg = MagicMock()
bypass_ignore = MagicMock() bypass_ignore = MagicMock()
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[]) bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
bypass_pkg.ignore_handler = bypass_ignore 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", bypass_pkg)
monkeypatch.setitem( monkeypatch.setitem(
sys.modules, sys.modules,
"aipass.seedgo.apps.handlers.bypass.ignore_handler", "aipass.seedgo.apps.handlers.bypass.ignore_handler",
bypass_ignore, bypass_ignore,
) )
monkeypatch.setitem(
sys.modules,
"aipass.seedgo.apps.handlers.bypass.utils",
bypass_utils,
)
# Force re-imports so checkers pick up fresh mocks # Force re-imports so checkers pick up fresh mocks
for mod_name in [ for mod_name in [
@@ -545,6 +555,30 @@ class TestCheckPrintUsage:
assert result["passed"] is False assert result["passed"] is False
assert "parser.print_help()" in result["message"] 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): def test_sys_stdout_write_fails(self):
"""sys.stdout.write() usage fails.""" """sys.stdout.write() usage fails."""
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import ( from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (