diff --git a/CHANGELOG.md b/CHANGELOG.md index 0247c1ec..b10136d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ and this project uses [Calendar Versioning](https://calver.org/) in the format ### Changed +- **All 13 branches at seedgo 100% under the new introspection standard.** + Wrapped `print_introspection()` output in Rich markup across ai_mail, drone, + spawn, trigger, prax and devpulse (the rest were already compliant) — + presentation only, no logic change — so `drone @branch` with no args renders + consistent styled output everywhere. +- **CLI polish for human-facing output.** `drone @hooks --help` rewritten (Rich, + with `hooksound on/off/status` now surfaced); `drone @spawn` repair help + clarified as distinct from `update` and showing the preview/`--apply` flow; + drone restores Rich colour on human-facing routed output (`--help`, + introspection, `status`) via the inherit path. - **Spawn backups land in one namespace `.spawn/.recovery/` (TDPLAN-0006 P4).** Spawn's pre-merge JSON backups previously dropped a `.recovery/` directory at each branch root (which had accumulated 242 stale auto-generated `DASHBOARD` @@ -48,6 +58,13 @@ and this project uses [Calendar Versioning](https://calver.org/) in the format ### Fixed +- **Flow plan-type self-serve UX — register override, help, orphan cleanup.** + Explicit `drone @flow register ` now overrides an auto-derived + prefix instead of silently failing (guarded — refuses if the auto-registered + type already holds plans), so custom prefixes are settable when adding a new + plan type. `create`/`templates --help` rewritten to dynamically list registered + types + templates and document the add-a-new-type workflow. Stale orphan plan + registries removed; dead `prefix_exists()` dropped. (728 tests, seedgo 100%.) - **`drone @spawn update` no longer scrambles branches (#636, critical — TDPLAN-0006 P0+P1).** The update engine compared a freshly-created branch against the class template by *content hash* with rename-detection, and because the CREATE path @@ -70,6 +87,17 @@ and this project uses [Calendar Versioning](https://calver.org/) in the format ### Added +- **Introspection Rich-formatting standard (seedgo).** New + `check_introspection_rich_formatting` checker enforces that each branch's + `print_introspection()` output uses Rich markup (delegation-aware — it walks + `_`-prefixed helper functions), keeping no-arg `drone @branch` output styled and + consistent. Documented in `introspection.md`; all 13 branches brought into + compliance (see Changed). +- **Playbook plan type (`PBPLAN`) — reusable SOP checklists (flow).** A new + `playbook_plans` template family for throwaway, vectorize-on-close operational + runbooks (first SOP: the Sunday merge). Drop a `.md` under + `templates/playbook_plans/`, register once, then + `drone @flow create . "subject" ` stamps a run to tick through and close. - **Memory-pool auto-processing (TDPLAN-0005)** — dropped files in `memory/memory_pool/` are now vectorized and archived automatically on session-start and pre-compact, instead of requiring a manual diff --git a/setup.sh b/setup.sh index abc55d35..1bdecbde 100755 --- a/setup.sh +++ b/setup.sh @@ -540,15 +540,16 @@ else: settings = {} # Build hooks config — bridge pattern -# UserPromptSubmit: 4 separate entries (EventType:hook_name) to avoid output merging +# UserPromptSubmit: 5 separate entries (EventType:hook_name) to avoid output merging # PreToolUse, PostToolUse, SubagentStop, Stop, Notification: single aggregate entries -# PreCompact: 2 hooks x 2 matchers (manual + auto) = 4 entries +# PreCompact: 3 hooks x 2 matchers (manual + auto) = 6 entries settings["hooks"] = { "UserPromptSubmit": [ {"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:global_prompt"}]}, {"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:branch_prompt"}]}, {"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:identity_injector"}]}, {"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:email_notification"}]}, + {"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:auto_process", "timeout": 120}]}, ], "PreToolUse": [ {"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task", @@ -572,6 +573,8 @@ settings["hooks"] = { {"matcher": "auto", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:pre_compact", "timeout": 60}]}, {"matcher": "manual", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:pre_compact_rollover", "timeout": 120}]}, {"matcher": "auto", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:pre_compact_rollover", "timeout": 120}]}, + {"matcher": "manual", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:auto_process", "timeout": 120}]}, + {"matcher": "auto", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:auto_process", "timeout": 120}]}, ], } diff --git a/src/aipass/ai_mail/apps/modules/dispatch.py b/src/aipass/ai_mail/apps/modules/dispatch.py index 6d588196..5b4e0302 100644 --- a/src/aipass/ai_mail/apps/modules/dispatch.py +++ b/src/aipass/ai_mail/apps/modules/dispatch.py @@ -408,23 +408,26 @@ def _orchestrate_daemon() -> bool: def print_introspection(): """Display module introspection info.""" console.print() - console.print("dispatch Module") + console.print("[bold cyan]dispatch Module[/bold cyan]") console.print( - "Orchestrates dispatch commands: combined send+wake, status tracking, daemon management, and manual wake." + "[dim]Orchestrates dispatch commands: combined send+wake," + " status tracking, daemon management, and manual wake.[/dim]" ) console.print() - console.print("Connected Handlers:") - console.print(" handlers/dispatch/") - console.print(" - status.py (load_dispatch_log — load dispatch log entries)") - console.print(" - status.py (check_pid_status — check if a spawned process is still running)") - console.print(" - status.py (calculate_age — calculate age string from timestamp)") - console.print(" - wake.py (wake_branch — manually wake a branch by spawning an agent)") - console.print(" - daemon.py (run_daemon — start the continuous dispatch daemon)") - console.print(" handlers/email/ (used by combined dispatch)") - console.print(" - send.py (resolve_sender_info, send_to_single — send email pipeline)") - console.print(" - create.py (create_email_file, load_email_file — email file creation)") - console.print(" - delivery.py (deliver_email_to_branch — inbox delivery)") - console.print(" - header.py (prepend_dispatch_header — dispatch header injection)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/dispatch/[/cyan]") + console.print(" - [cyan]status.py[/cyan] [dim](load_dispatch_log — load dispatch log entries)[/dim]") + console.print( + " - [cyan]status.py[/cyan] [dim](check_pid_status — check if a spawned process is still running)[/dim]" + ) + console.print(" - [cyan]status.py[/cyan] [dim](calculate_age — calculate age string from timestamp)[/dim]") + console.print(" - [cyan]wake.py[/cyan] [dim](wake_branch — manually wake a branch by spawning an agent)[/dim]") + console.print(" - [cyan]daemon.py[/cyan] [dim](run_daemon — start the continuous dispatch daemon)[/dim]") + console.print(" [cyan]handlers/email/[/cyan] [dim](used by combined dispatch)[/dim]") + console.print(" - [cyan]send.py[/cyan] [dim](resolve_sender_info, send_to_single — send email pipeline)[/dim]") + console.print(" - [cyan]create.py[/cyan] [dim](create_email_file, load_email_file — email file creation)[/dim]") + console.print(" - [cyan]delivery.py[/cyan] [dim](deliver_email_to_branch — inbox delivery)[/dim]") + console.print(" - [cyan]header.py[/cyan] [dim](prepend_dispatch_header — dispatch header injection)[/dim]") console.print() diff --git a/src/aipass/ai_mail/apps/modules/email_send.py b/src/aipass/ai_mail/apps/modules/email_send.py index 24249db6..7e30a1af 100644 --- a/src/aipass/ai_mail/apps/modules/email_send.py +++ b/src/aipass/ai_mail/apps/modules/email_send.py @@ -250,18 +250,18 @@ def _send_broadcast(subject, message, user_info, auto_execute, no_memory_save, r def print_introspection(): """Print module introspection for seedgo compliance.""" - console.print("\n" + "=" * 70) - console.print("EMAIL SEND ORCHESTRATION") - console.print("=" * 70) - console.print("\nFunctions provided:") - console.print(" - handle_send(args) -> bool") - console.print(" - _send_direct(...) -> bool") - console.print(" - _send_interactive() -> bool") - console.print(" - _send_broadcast(...) -> bool") - console.print(" - _fire_dispatch_trigger(to_branch, subject) -> None") - console.print(" - _delivery_callback(branch_path, new_count, opened_count, total)") console.print() - console.print("=" * 70 + "\n") + console.print("[bold cyan]email_send Module[/bold cyan]") + console.print("[dim]Send orchestration — direct, interactive, and broadcast email delivery.[/dim]") + console.print() + console.print("[yellow]Functions provided:[/yellow]") + console.print(" - [cyan]handle_send[/cyan][dim](args) -> bool[/dim]") + console.print(" - [cyan]_send_direct[/cyan][dim](...) -> bool[/dim]") + console.print(" - [cyan]_send_interactive[/cyan][dim]() -> bool[/dim]") + console.print(" - [cyan]_send_broadcast[/cyan][dim](...) -> bool[/dim]") + console.print(" - [cyan]_fire_dispatch_trigger[/cyan][dim](to_branch, subject) -> None[/dim]") + console.print(" - [cyan]_delivery_callback[/cyan][dim](branch_path, new_count, opened_count, total)[/dim]") + console.print() if __name__ == "__main__": diff --git a/src/aipass/ai_mail/tests/test_email_module.py b/src/aipass/ai_mail/tests/test_email_module.py index ca19d05d..fd719161 100644 --- a/src/aipass/ai_mail/tests/test_email_module.py +++ b/src/aipass/ai_mail/tests/test_email_module.py @@ -1708,7 +1708,7 @@ class TestEmailSendIntrospection: print_introspection() combined = "\n".join(printed) - assert "EMAIL SEND ORCHESTRATION" in combined + assert "email_send Module" in combined assert "handle_send" in combined assert "_send_direct" in combined assert "_send_broadcast" in combined diff --git a/src/aipass/devpulse/SETUP.md b/src/aipass/devpulse/SETUP.md deleted file mode 100644 index 37594576..00000000 --- a/src/aipass/devpulse/SETUP.md +++ /dev/null @@ -1,225 +0,0 @@ -[← Back to DevPulse](README.md) - -# DevPulse Setup, Uninstall, Troubleshooting - -Everything you need to install, run, maintain, or remove DevPulse (and AIPass as a whole). Kept here so the DevPulse README can stay lean and loads quickly on every session startup. - ---- - -## Platform support at a glance - -| Platform | Install status | Notes | -|---|---|---| -| **Linux** (Ubuntu, Debian, Fedora, Arch) | Supported | Primary development target. `setup.sh` works out of the box. | -| **macOS** (Intel and Apple Silicon) | Supported | `setup.sh` works with minor caveats (see macOS section). | -| **Windows 10 / 11** | **In progress** | Native Windows support is actively being built. Track progress in [issue #261](https://github.com/AIOSAI/AIPass/issues/261). For now: use WSL2 (Ubuntu), or wait for the cross-platform `setup.py` landing in a PR soon. | - ---- - -## Linux install - -### Requirements - -- Python 3.10 or newer (`python3 --version`) -- `git`, `bash`, `sudo` -- Claude Code CLI installed and authenticated (`claude --version`) -- ~500 MB disk for the venv and dependencies - -### Install - -```bash -git clone https://github.com/AIOSAI/AIPass.git ~/Projects/AIPass -cd ~/Projects/AIPass -bash setup.sh -``` - -`setup.sh` will: -1. Create a Python venv at `.venv/` -2. Install AIPass in editable mode (`pip install -e .`) -3. Verify the `drone` and `aipass` CLI entry points -4. Create `~/.secrets/aipass/` with `chmod 700` and seed an `.env.example` -5. Generate the AIPass branch registry -6. Bootstrap branch identity files (`.trinity/passport.json` per branch) -7. Wire Claude Code hooks into `~/.claude/settings.json` -8. Create a global symlink at `/usr/local/bin/drone` (asks for `sudo`) - -### Post-install - -```bash -# Verify -drone systems - -# Enter the DevPulse branch -cd ~/Projects/AIPass/src/aipass/devpulse -claude -``` - -You should see DevPulse greet you, read its memory, and be ready. - -### Optional - -- Add API keys to `~/.secrets/aipass/.env` if you want LLM routing beyond Claude Code -- Set `AIPASS_HOME=~/Projects/AIPass` in your shell rc if you plan to use AIPass from other projects -- Add `export AIPASS_HOME=~/Projects/AIPass` to `~/.bashrc` **and** `~/.claude/settings.json` (the `env` section) — both are needed for full cross-project access - ---- - -## macOS install - -Same as Linux. `setup.sh` uses bash and runs on macOS out of the box. - -**Caveats**: -- `chmod 700` and `chown` work correctly on macOS's HFS+ and APFS -- `sudo ln -sf /usr/local/bin/drone` works but may prompt for your admin password -- Homebrew users: if you have multiple Python installs, make sure `python3` points to Python 3.10+ before running `setup.sh` - ---- - -## Windows install - -**Short version**: use [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) (Ubuntu) and follow the Linux instructions. Full native Windows support is landing in a PR soon — follow [issue #261](https://github.com/AIOSAI/AIPass/issues/261) for status. - -**Why it's in progress**: the current `setup.sh` uses bash, `sudo`, and `ln -sf /usr/local/bin/drone`, none of which translate to Windows. The `aipass init` command also writes a shell loop into `.claude/settings.json` that assumes Unix root `/`. Fixes are in flight: -- A cross-platform `setup.py` that replaces `setup.sh` on Windows -- A Python-based directory traversal replacing the bash loop in `aipass init` -- OS detection in `setup.sh` to skip the symlink step on Windows and print PATH instructions instead - -**Interim workaround**: install WSL2 with an Ubuntu distribution, then clone and run `setup.sh` inside WSL. Claude Code also runs well inside WSL. - ---- - -## Uninstall - -### Full removal (Linux / macOS) - -```bash -# 1. Remove the venv and repo -rm -rf ~/Projects/AIPass - -# 2. Remove the global drone symlink -sudo rm /usr/local/bin/drone - -# 3. Remove secrets (if you won't reinstall) -rm -rf ~/.secrets/aipass - -# 4. Clean Claude Code hooks -# Edit ~/.claude/settings.json and remove any "hooks" sections that reference AIPass paths. -# Safer: back up the file first. -cp ~/.claude/settings.json ~/.claude/settings.json.bak -nano ~/.claude/settings.json # or your editor of choice - -# 5. Clean your shell rc -# Remove any AIPASS_HOME export from ~/.bashrc, ~/.zshrc, etc. -``` - -### Partial removal (keeping secrets for reinstall) - -Skip step 3 above. Your `~/.secrets/aipass/.env` will persist and be reused on next install. - -### Windows (WSL2) - -Same as Linux, inside the WSL distribution. To also remove the WSL distribution itself: `wsl --unregister Ubuntu` from PowerShell. - ---- - -## Troubleshooting - -### `drone: command not found` - -Your venv is not activated or the `/usr/local/bin/drone` symlink is missing. - -```bash -# Option A: activate the venv -source ~/Projects/AIPass/.venv/bin/activate -drone systems - -# Option B: run via full path -~/Projects/AIPass/.venv/bin/drone systems - -# Option C: reinstall the symlink -sudo ln -sf ~/Projects/AIPass/.venv/bin/drone /usr/local/bin/drone -``` - -### `AIPASS_HOME not set` warnings - -```bash -# In your shell rc (~/.bashrc or ~/.zshrc) -export AIPASS_HOME=~/Projects/AIPass - -# Then restart the shell or: -source ~/.bashrc -``` - -Also add it to `~/.claude/settings.json` under the `env` block for Claude Code sessions to pick it up. - -### DevPulse greets you but doesn't read its memory - -Check that `.trinity/passport.json`, `.trinity/local.json`, and `.trinity/observations.json` exist in `src/aipass/devpulse/`. If they don't, run `bash setup.sh` again to re-bootstrap the identity files. - -### `drone @git system-pr` fails with a lock error - -```bash -drone @git lock # check the lock state -drone @git fix # attempt to fix broken git state -``` - -Do NOT use raw `git reset --hard` — merge conflicts are easier to resolve than lost work. - -### Branch mail not arriving - -```bash -drone @ai_mail inbox # check your inbox -drone @prax watch # watch the monitoring dashboard -``` - -A known issue at the end of S90 affected wake delivery; see the wake investigation in DPLAN-0125 Track E if you're running a recent build. - -### Tests fail on a fresh clone - -```bash -cd ~/Projects/AIPass -source .venv/bin/activate -python -m pytest src/aipass//tests/ -``` - -If tests fail because `AIPASS_HOME` leaks the real registry into test results, that's a known pattern — the tests need `monkeypatch.delenv("AIPASS_HOME")`. See S90 notes for the fixture pattern. - -### `.claude/settings.json` has hardcoded absolute paths - -You pulled an old clone. The hardcoded paths were removed in commit `867dad0` (April 5, 2026). Pull the latest main and re-run `setup.sh`, which generates the settings dynamically from your local repo root. - ---- - -## Environment variables - -| Variable | Purpose | Set where | -|---|---|---| -| `AIPASS_HOME` | Lets external projects find the AIPass registry | `~/.bashrc` + `~/.claude/settings.json` env block | -| `AIPASS_CALLER_BRANCH` | Auto-set by dispatch; identifies the sending branch for feedback/mail | Runtime only, do not set manually | -| `AIPASS_CALLER_CWD` | Auto-set by dispatch; identifies the caller's project directory | Runtime only, do not set manually | - -Sensitive values (API keys, tokens, recovery codes) belong in `~/.secrets/aipass/.env`, not in shell rc or repo files. - ---- - -## Reporting bugs - -File issues at https://github.com/AIOSAI/AIPass/issues. - -Helpful info to include: -- OS and version -- Python version (`python3 --version`) -- Claude Code version (`claude --version`) -- The exact command you ran and the full error output -- Whether you cloned recently or have been on the same checkout for a while (clone age helps us distinguish current bugs from fixed-but-stale-clone issues) - -The first external bug report was [#261 by Gavin Rooney](https://github.com/AIOSAI/AIPass/issues/261) — that template is a good example of a useful report. - ---- - -## See also - -- [DevPulse README](README.md) — the lean entry point -- [AIPass root README](../../../README.md) — the whole framework -- [STATUS.local.md](STATUS.local.md) — current work and loose ends -- [issue #261](https://github.com/AIOSAI/AIPass/issues/261) — Windows compat tracking diff --git a/src/aipass/devpulse/apps/modules/feedback.py b/src/aipass/devpulse/apps/modules/feedback.py index b451b966..6dc7e297 100644 --- a/src/aipass/devpulse/apps/modules/feedback.py +++ b/src/aipass/devpulse/apps/modules/feedback.py @@ -50,11 +50,11 @@ HELP_TEXT = """\ def print_introspection() -> None: """Display module introspection info.""" console.print() - console.print("feedback Module") - console.print("DevPulse personal feedback mailbox. Receives cross-project") - console.print("feedback messages from any agent via drone routing.") + console.print("[bold cyan]feedback Module[/bold cyan]") + console.print("[dim]DevPulse personal feedback mailbox. Receives cross-project[/dim]") + console.print("[dim]feedback messages from any agent via drone routing.[/dim]") console.print() - console.print("Subcommands: inbox, view, reply, send, clear") + console.print("[yellow]Subcommands:[/yellow] [cyan]inbox, view, reply, send, clear[/cyan]") console.print() diff --git a/src/aipass/devpulse/apps/modules/watchdog.py b/src/aipass/devpulse/apps/modules/watchdog.py index 19a06096..5e44e856 100644 --- a/src/aipass/devpulse/apps/modules/watchdog.py +++ b/src/aipass/devpulse/apps/modules/watchdog.py @@ -97,18 +97,18 @@ Examples: def print_introspection() -> None: """Display module introspection info.""" console.print() - console.print("watchdog Module") - console.print("Devpulse-local directed wake system. Wakes devpulse when a") - console.print("watched condition fires (agent exit, timer, schedule).") + console.print("[bold cyan]watchdog Module[/bold cyan]") + console.print("[dim]Devpulse-local directed wake system. Wakes devpulse when a[/dim]") + console.print("[dim]watched condition fires (agent exit, timer, schedule).[/dim]") console.print() - console.print("Subcommands:") + console.print("[yellow]Subcommands:[/yellow]") for sub in _VALID_SUBCOMMANDS: marker = "active" if sub in ("agent", "status") else f"phase {_PHASE_BY_SUB.get(sub, '?')}" - console.print(f" {sub:<10} ({marker})") + console.print(f" [cyan]{sub:<10}[/cyan] [dim]({marker})[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/watchdog/") - console.print(" - agent.py (watch_agent — block until dispatched agent exits)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/watchdog/[/cyan]") + console.print(" [dim]- agent.py (watch_agent — block until dispatched agent exits)[/dim]") console.print() @@ -360,7 +360,8 @@ def _handle_agent(sub_args: List[str]) -> bool: if state == "completed_silent": console.print( f"watchdog: {agent_id} stopped (state={state}) -- CHECK DELIVERABLES. " - f'Next: drone @ai_mail dispatch {agent_id} "check in" "You finished your last task but did not send a reply. ' + f'Next: drone @ai_mail dispatch {agent_id} "check in" ' + '"You finished your last task but did not send a reply. ' f'Please reply with your results now via drone @ai_mail email @devpulse."' ) elif state == "completed_replied": diff --git a/src/aipass/drone/.seedgo/bypass.json b/src/aipass/drone/.seedgo/bypass.json index d3103bfa..bdf0705c 100644 --- a/src/aipass/drone/.seedgo/bypass.json +++ b/src/aipass/drone/.seedgo/bypass.json @@ -199,6 +199,12 @@ "standard": "unused_function", "lines": [108], "reason": "create_pr() is deprecated per FPLAN-0210 — pr command blocked at auth tier. Handler kept for backwards compatibility; still tested in test_git_module.py." + }, + { + "file": "apps/modules/git_module.py", + "standard": "unused_function", + "lines": [655], + "reason": "get_introspective() called dynamically via getattr() by module_registry_handler.py:219 for internal module introspection. Also tested in test_git_module, test_system_pr, test_devpulse_plugins, test_git_access." } ], "notes": { diff --git a/src/aipass/drone/README.md b/src/aipass/drone/README.md index b4d04f47..c7907051 100644 --- a/src/aipass/drone/README.md +++ b/src/aipass/drone/README.md @@ -243,6 +243,14 @@ By default, drone captures subprocess output (`capture_output=True`) with a 30s Commands in the interactive tuple bypass capture and inherit the terminal directly — enabling live Rich output, colors, and no timeout. +**Always interactive** — these presentational commands always inherit the terminal for Rich color on a TTY, plain when piped: + +| Pattern | Reason | +|----------------|---------------------------------------------| +| `@branch` | No-args introspection (branch overview) | +| `@branch --help` | Help output with Rich formatting | +| `@branch -h` | Short help flag (same as --help) | + **Per-command allowlist** (in `apps/drone.py`): | Command | Reason | @@ -250,6 +258,7 @@ Commands in the interactive tuple bypass capture and inherit the terminal direct | `monitor` | Prax real-time monitoring (live TUI) | | `audit` | Seedgo audit (Rich progress bars) | | `watchdog` | Devpulse watchdog (live monitoring) | +| `status` | Branch status with Rich formatted output | **Per-branch allowlist** — all commands from these branches get interactive mode: @@ -339,7 +348,7 @@ Run tests: `cd src/aipass/drone && python -m pytest tests/ -q` --- -**Seedgo:** 100% | **Tests:** 772 pass, 4 skip | **Last Updated:** 2026-06-05 +**Seedgo:** 100% | **Tests:** 775 pass, 4 skip | **Last Updated:** 2026-06-07 --- [← Back to AIPass](../../../README.md) diff --git a/src/aipass/drone/apps/drone.py b/src/aipass/drone/apps/drone.py index 765e7603..43d2469e 100644 --- a/src/aipass/drone/apps/drone.py +++ b/src/aipass/drone/apps/drone.py @@ -25,7 +25,6 @@ from rich.text import Text from aipass.prax import logger from aipass.cli.apps.modules import console, err_console from aipass.drone.apps.modules import BranchNotFoundError, CommandExecutionError, RegistryError -from aipass.drone.apps.modules.discovery import get_help from aipass.drone.apps.modules.resolver import get_all_branches from aipass.drone.apps.modules.router import route_command from aipass.drone.apps.modules.module_registry import ( @@ -41,7 +40,7 @@ VERSION = "1.1.0" MODULES_DIR = Path(__file__).parent / "modules" # Interactive mode — commands/branches that bypass capture + timeout for live terminal output. -INTERACTIVE_COMMANDS = ("monitor", "audit", "watchdog") +INTERACTIVE_COMMANDS = ("monitor", "audit", "watchdog", "status") INTERACTIVE_BRANCHES = ("cli",) @@ -408,8 +407,9 @@ def _handle_target(args: List[str]) -> int: rest = args[1:] module_name = target.lstrip("@").lower() - first_cmd = rest[0] if rest and rest[0] != "--help" else None - needs_interactive = first_cmd in INTERACTIVE_COMMANDS or module_name in INTERACTIVE_BRANCHES + first_cmd = rest[0] if rest and rest[0] not in ("--help", "-h") else None + is_presentational = not rest or first_cmd is None + needs_interactive = is_presentational or first_cmd in INTERACTIVE_COMMANDS or module_name in INTERACTIVE_BRANCHES # Route to internal module — unless command needs interactive terminal, # in which case fall through to branch (subprocess) routing so Rich @@ -417,10 +417,10 @@ def _handle_target(args: List[str]) -> int: if is_module(module_name) and not needs_interactive: return _handle_module(module_name, rest) - # No args = pass through to branch (introspection) + # No args = pass through to branch (introspection — inherit terminal for color) if not rest: try: - result = route_command(target) + result = route_command(target, interactive=True) except (BranchNotFoundError, CommandExecutionError, RegistryError) as exc: if isinstance(exc, BranchNotFoundError) and is_module(module_name): logger.info("Falling back to module routing for @%s (not in local registry)", module_name) @@ -430,30 +430,22 @@ def _handle_target(args: List[str]) -> int: if isinstance(exc, BranchNotFoundError) and not os.environ.get("AIPASS_HOME"): err_console.print(" Tip: set AIPASS_HOME=/path/to/AIPass to access core branches.") return 1 - if result.stdout: - console.print(result.stdout, end="", highlight=False) - if result.stderr: - err_console.print(result.stderr, end="", highlight=False) return result.exit_code - # --help = show help - if rest == ["--help"]: + # --help / -h = help (inherit terminal for color) + if rest in (["--help"], ["-h"]): try: - result = get_help(target) - if result.text: - console.print(result.text, end="", highlight=False) - else: - console.print(f"No help available for {target}.") + result = route_command(target, rest[0], interactive=True) except (BranchNotFoundError, CommandExecutionError, RegistryError) as exc: if isinstance(exc, BranchNotFoundError) and is_module(module_name): - logger.info("Falling back to module routing for @%s --help (not in local registry)", module_name) + logger.info("Falling back to module routing for @%s %s (not in local registry)", module_name, rest[0]) return _handle_module(module_name, rest) logger.warning("Help lookup failed for %s: %s", target, exc) err_console.print(f"drone: {exc}") if isinstance(exc, BranchNotFoundError) and not os.environ.get("AIPASS_HOME"): err_console.print(" Tip: set AIPASS_HOME=/path/to/AIPass to access core branches.") return 1 - return 0 + return result.exit_code # drone @branch command [args...] command = rest[0] diff --git a/src/aipass/drone/apps/modules/commands.py b/src/aipass/drone/apps/modules/commands.py index bd9c5160..3411972d 100644 --- a/src/aipass/drone/apps/modules/commands.py +++ b/src/aipass/drone/apps/modules/commands.py @@ -155,13 +155,15 @@ def print_introspection() -> None: console = Console() console.print() - console.print("commands Module") - console.print("Custom command shortcuts — map short names to full drone commands.") + console.print("[bold cyan]commands Module[/bold cyan]") + console.print("[dim]Custom command shortcuts — map short names to full drone commands.[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/command_registry/") - console.print(" - ops.py (add_command, remove_command, update_command, command_exists)") - console.print(" - lookup.py (lookup_command, match_command, list_commands, list_commands_by_branch)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/command_registry/[/cyan]") + console.print(" - [cyan]ops.py[/cyan] [dim](add_command, remove_command, update_command, command_exists)[/dim]") + console.print( + " - [cyan]lookup.py[/cyan] [dim](lookup_command, match_command, list_commands, list_commands_by_branch)[/dim]" + ) console.print() diff --git a/src/aipass/drone/apps/modules/config.py b/src/aipass/drone/apps/modules/config.py index ad0bb99a..d5e68bdd 100644 --- a/src/aipass/drone/apps/modules/config.py +++ b/src/aipass/drone/apps/modules/config.py @@ -37,14 +37,20 @@ def print_introspection(): console = Console() console.print() - console.print("config Module") - console.print("Registry configuration management — path resolution and overrides.") + console.print("[bold cyan]config Module[/bold cyan]") + console.print("[dim]Registry configuration management — path resolution and overrides.[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - registry_handler.py (get_registry_path — return current registry file path)") - console.print(" - registry_handler.py (set_registry_path — override registry file location)") - console.print(" - registry_handler.py (reset_registry_path — restore default registry path)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print( + " - [cyan]registry_handler.py[/cyan] [dim](get_registry_path — return current registry file path)[/dim]" + ) + console.print( + " - [cyan]registry_handler.py[/cyan] [dim](set_registry_path — override registry file location)[/dim]" + ) + console.print( + " - [cyan]registry_handler.py[/cyan] [dim](reset_registry_path — restore default registry path)[/dim]" + ) console.print() diff --git a/src/aipass/drone/apps/modules/discovery.py b/src/aipass/drone/apps/modules/discovery.py index 7fc7330d..24445ae2 100644 --- a/src/aipass/drone/apps/modules/discovery.py +++ b/src/aipass/drone/apps/modules/discovery.py @@ -93,19 +93,25 @@ def print_introspection(): console = Console() console.print() - console.print("discovery Module") - console.print("Module and command discovery for AIPass branch introspection.") + console.print("[bold cyan]discovery Module[/bold cyan]") + console.print("[dim]Module and command discovery for AIPass branch introspection.[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - discovery_handler.py (HelpResult — structured help query result)") - console.print(" - discovery_handler.py (discover_modules — list available commands for a branch)") - console.print(" - discovery_handler.py (get_help — get structured help for a branch/command)") - console.print(" - discovery_handler.py (get_system_help — aggregate help across all branches)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print(" - [cyan]discovery_handler.py[/cyan] [dim](HelpResult — structured help query result)[/dim]") + console.print( + " - [cyan]discovery_handler.py[/cyan] [dim](discover_modules — list available commands for a branch)[/dim]" + ) + console.print( + " - [cyan]discovery_handler.py[/cyan] [dim](get_help — get structured help for a branch/command)[/dim]" + ) + console.print( + " - [cyan]discovery_handler.py[/cyan] [dim](get_system_help — aggregate help across all branches)[/dim]" + ) console.print() - console.print("Connected Modules:") - console.print(" modules/") - console.print(" - resolver.py (resolve_branch, list_branches — branch name resolution)") + console.print("[yellow]Connected Modules:[/yellow]") + console.print(" [cyan]modules/[/cyan]") + console.print(" - [cyan]resolver.py[/cyan] [dim](resolve_branch, list_branches — branch name resolution)[/dim]") console.print() diff --git a/src/aipass/drone/apps/modules/git_module.py b/src/aipass/drone/apps/modules/git_module.py index e8cfe599..4acb82c3 100644 --- a/src/aipass/drone/apps/modules/git_module.py +++ b/src/aipass/drone/apps/modules/git_module.py @@ -683,7 +683,33 @@ def _get_console(): def print_introspection() -> None: """Print introspection (seedgo compliance).""" - _get_console().print(get_introspective()) + c = _get_console() + c.print() + c.print("[bold cyan]@git[/bold cyan] [dim]— Tier-based git workflow, dev branch model (v3.0.0)[/dim]") + c.print("[yellow]Connected Handlers:[/yellow]") + c.print(" [cyan]handlers/git/[/cyan]") + c.print( + " - [cyan]lock_handler.py[/cyan], [cyan]status_handler.py[/cyan]," + " [cyan]diff_handler.py[/cyan], [cyan]log_handler.py[/cyan]" + ) + c.print(" - [cyan]commit_handler.py[/cyan], [cyan]checkout_handler.py[/cyan], [cyan]sync_handler.py[/cyan]") + c.print( + " - [cyan]dev_pr_handler.py[/cyan], [cyan]branches_handler.py[/cyan]," + " [cyan]delete_branch_handler.py[/cyan], [cyan]close_pr_handler.py[/cyan]" + ) + c.print(" [cyan]plugins/devpulse_ops/[/cyan]") + c.print( + " - [cyan]auth.py[/cyan], [cyan]merge_plugin.py[/cyan]," + " [cyan]sync_plugin.py[/cyan], [cyan]fix_plugin.py[/cyan]" + ) + c.print(" [dim]gh passthrough: issue, run, workflow[/dim]") + c.print( + "[yellow]Tiers:[/yellow] [dim]global[/dim]" + " [dim](status,diff,log,lock,branches,prune-temp,issue,run,workflow)[/dim]" + " | [dim]owner[/dim]" + " [dim](pr,commit,checkout,dev-pr,delete-branch,close-pr,sync,unlock,merge,smart-sync,fix)[/dim]" + ) + c.print() def print_help() -> None: diff --git a/src/aipass/drone/apps/modules/module_registry.py b/src/aipass/drone/apps/modules/module_registry.py index f78e1329..db6ff17b 100644 --- a/src/aipass/drone/apps/modules/module_registry.py +++ b/src/aipass/drone/apps/modules/module_registry.py @@ -54,19 +54,34 @@ def print_introspection(): console = Console() console.print() - console.print("module_registry Module") - console.print("Internal module registry for drone — dynamic module loading and command delegation.") + console.print("[bold cyan]module_registry Module[/bold cyan]") + console.print("[dim]Internal module registry for drone — dynamic module loading and command delegation.[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - module_registry_handler.py (ModuleInfo — module metadata dataclass)") - console.print(" - module_registry_handler.py (list_modules — list registered module names)") - console.print(" - module_registry_handler.py (is_module — check if a module is registered)") - console.print(" - module_registry_handler.py (get_module_info — retrieve module metadata)") - console.print(" - module_registry_handler.py (route_module_command — delegate command to module)") - console.print(" - module_registry_handler.py (get_module_help — get help text for a module)") - console.print(" - module_registry_handler.py (get_module_introspective — introspect module adapter)") - console.print(" - module_registry_handler.py (register_module — register a new module adapter)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print(" - [cyan]module_registry_handler.py[/cyan] [dim](ModuleInfo — module metadata dataclass)[/dim]") + console.print( + " - [cyan]module_registry_handler.py[/cyan] [dim](list_modules — list registered module names)[/dim]" + ) + console.print( + " - [cyan]module_registry_handler.py[/cyan] [dim](is_module — check if a module is registered)[/dim]" + ) + console.print( + " - [cyan]module_registry_handler.py[/cyan] [dim](get_module_info — retrieve module metadata)[/dim]" + ) + console.print( + " - [cyan]module_registry_handler.py[/cyan] [dim](route_module_command — delegate command to module)[/dim]" + ) + console.print( + " - [cyan]module_registry_handler.py[/cyan] [dim](get_module_help — get help text for a module)[/dim]" + ) + console.print( + " - [cyan]module_registry_handler.py[/cyan]" + " [dim](get_module_introspective — introspect module adapter)[/dim]" + ) + console.print( + " - [cyan]module_registry_handler.py[/cyan] [dim](register_module — register a new module adapter)[/dim]" + ) console.print() diff --git a/src/aipass/drone/apps/modules/registry.py b/src/aipass/drone/apps/modules/registry.py index 3827e847..635cec93 100644 --- a/src/aipass/drone/apps/modules/registry.py +++ b/src/aipass/drone/apps/modules/registry.py @@ -37,14 +37,20 @@ def print_introspection(): console = Console() console.print() - console.print("registry Module") - console.print("Registry operations for branch management — loading and querying AIPASS_REGISTRY.json.") + console.print("[bold cyan]registry Module[/bold cyan]") + console.print("[dim]Registry operations for branch management — loading and querying AIPASS_REGISTRY.json.[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - registry_handler.py (load_registry — load and parse the registry file)") - console.print(" - registry_handler.py (get_all_branches — list branches with type/status filters)") - console.print(" - registry_handler.py (get_branch_by_name — look up a single branch by name)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print( + " - [cyan]registry_handler.py[/cyan] [dim](load_registry — load and parse the registry file)[/dim]" + ) + console.print( + " - [cyan]registry_handler.py[/cyan] [dim](get_all_branches — list branches with type/status filters)[/dim]" + ) + console.print( + " - [cyan]registry_handler.py[/cyan] [dim](get_branch_by_name — look up a single branch by name)[/dim]" + ) console.print() diff --git a/src/aipass/drone/apps/modules/resolver.py b/src/aipass/drone/apps/modules/resolver.py index 6b161526..b32a117d 100644 --- a/src/aipass/drone/apps/modules/resolver.py +++ b/src/aipass/drone/apps/modules/resolver.py @@ -112,15 +112,21 @@ def print_introspection(): console = Console() console.print() - console.print("resolver Module") - console.print("Branch resolution logic — resolves symbolic @branch names to paths and metadata.") + console.print("[bold cyan]resolver Module[/bold cyan]") + console.print("[dim]Branch resolution logic — resolves symbolic @branch names to paths and metadata.[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - registry_handler.py (load_registry — load and parse AIPASS_REGISTRY.json)") - console.print(" - registry_handler.py (get_all_branches — list branches with optional filters)") - console.print(" - registry_handler.py (get_branch_by_name — look up a single branch)") - console.print(" - exceptions.py (BranchNotFoundError — raised when branch not in registry)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print( + " - [cyan]registry_handler.py[/cyan] [dim](load_registry — load and parse AIPASS_REGISTRY.json)[/dim]" + ) + console.print( + " - [cyan]registry_handler.py[/cyan] [dim](get_all_branches — list branches with optional filters)[/dim]" + ) + console.print(" - [cyan]registry_handler.py[/cyan] [dim](get_branch_by_name — look up a single branch)[/dim]") + console.print( + " - [cyan]exceptions.py[/cyan] [dim](BranchNotFoundError — raised when branch not in registry)[/dim]" + ) console.print() diff --git a/src/aipass/drone/apps/modules/router.py b/src/aipass/drone/apps/modules/router.py index 66894278..9c994d40 100644 --- a/src/aipass/drone/apps/modules/router.py +++ b/src/aipass/drone/apps/modules/router.py @@ -126,17 +126,19 @@ def print_introspection(): console = Console() console.print() - console.print("router Module") - console.print("Command routing logic for the AIPass drone module.") + console.print("[bold cyan]router Module[/bold cyan]") + console.print("[dim]Command routing logic for the AIPass drone module.[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - router_handler.py (execute_branch_command — resolves and executes branch commands)") - console.print(" - executor.py (CommandResult — subprocess execution result dataclass)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print( + " - [cyan]router_handler.py[/cyan] [dim](execute_branch_command — resolves and executes branch commands)[/dim]" + ) + console.print(" - [cyan]executor.py[/cyan] [dim](CommandResult — subprocess execution result dataclass)[/dim]") console.print() - console.print("Connected Modules:") - console.print(" modules/") - console.print(" - resolver.py (resolve_branch, list_branches — branch name resolution)") + console.print("[yellow]Connected Modules:[/yellow]") + console.print(" [cyan]modules/[/cyan]") + console.print(" - [cyan]resolver.py[/cyan] [dim](resolve_branch, list_branches — branch name resolution)[/dim]") console.print() diff --git a/src/aipass/drone/apps/modules/scan.py b/src/aipass/drone/apps/modules/scan.py index a7377444..a3bd1fca 100644 --- a/src/aipass/drone/apps/modules/scan.py +++ b/src/aipass/drone/apps/modules/scan.py @@ -76,17 +76,17 @@ def print_introspection() -> None: console = Console() console.print() - console.print("scan Module") - console.print("Branch command scanning -- discover available commands in a branch.") + console.print("[bold cyan]scan Module[/bold cyan]") + console.print("[dim]Branch command scanning -- discover available commands in a branch.[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/scanning/") - console.print(" - scanner.py (scan_branch, scan_help_output, scan_module_files)") - console.print(" - formatters.py (format_scan_results, format_no_commands)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/scanning/[/cyan]") + console.print(" - [cyan]scanner.py[/cyan] [dim](scan_branch, scan_help_output, scan_module_files)[/dim]") + console.print(" - [cyan]formatters.py[/cyan] [dim](format_scan_results, format_no_commands)[/dim]") console.print() - console.print("Connected Modules:") - console.print(" modules/") - console.print(" - resolver.py (resolve_branch -- branch name resolution)") + console.print("[yellow]Connected Modules:[/yellow]") + console.print(" [cyan]modules/[/cyan]") + console.print(" - [cyan]resolver.py[/cyan] [dim](resolve_branch -- branch name resolution)[/dim]") console.print() diff --git a/src/aipass/drone/tests/test_cli_routing.py b/src/aipass/drone/tests/test_cli_routing.py index 83b9d861..1f95aaea 100644 --- a/src/aipass/drone/tests/test_cli_routing.py +++ b/src/aipass/drone/tests/test_cli_routing.py @@ -597,17 +597,17 @@ class TestHandleTarget: patch(f"{_DRONE}.is_module", return_value=True), patch(f"{_DRONE}._handle_module", return_value=0) as mock_hm, ): - result = _handle_target(["@git", "status"]) + result = _handle_target(["@git", "diff"]) assert result == 0 - mock_hm.assert_called_once_with("git", ["status"]) + mock_hm.assert_called_once_with("git", ["diff"]) def test_no_args_introspection(self) -> None: - """@target with no args routes via route_command for introspection.""" + """@target with no args routes via route_command with interactive=True.""" from aipass.drone.apps.drone import _handle_target from aipass.drone.apps.handlers.executor import CommandResult mock_result = CommandResult( - stdout="introspection", + stdout="", stderr="", exit_code=0, branch="seedgo", @@ -615,22 +615,31 @@ class TestHandleTarget: ) with ( patch(f"{_DRONE}.is_module", return_value=False), - patch(f"{_DRONE}.route_command", return_value=mock_result), + patch(f"{_DRONE}.route_command", return_value=mock_result) as mock_route, ): result = _handle_target(["@seedgo"]) assert result == 0 + mock_route.assert_called_once_with("@seedgo", interactive=True) def test_help_flag(self) -> None: - """@target --help routes via get_help.""" + """@target --help routes via route_command with interactive=True.""" from aipass.drone.apps.drone import _handle_target + from aipass.drone.apps.handlers.executor import CommandResult - mock_help = type("H", (), {"text": "Help text"})() + mock_result = CommandResult( + stdout="", + stderr="", + exit_code=0, + branch="seedgo", + command="--help", + ) with ( patch(f"{_DRONE}.is_module", return_value=False), - patch(f"{_DRONE}.get_help", return_value=mock_help), + patch(f"{_DRONE}.route_command", return_value=mock_result) as mock_route, ): result = _handle_target(["@seedgo", "--help"]) assert result == 0 + mock_route.assert_called_once_with("@seedgo", "--help", interactive=True) def test_command_routing(self) -> None: """@target command routes via route_command.""" @@ -651,6 +660,49 @@ class TestHandleTarget: result = _handle_target(["@seedgo", "audit", "aipass"]) assert result == 0 + def test_short_help_flag(self) -> None: + """@target -h routes via route_command with interactive=True.""" + from aipass.drone.apps.drone import _handle_target + from aipass.drone.apps.handlers.executor import CommandResult + + mock_result = CommandResult(stdout="", stderr="", exit_code=0, branch="seedgo", command="-h") + with ( + patch(f"{_DRONE}.is_module", return_value=False), + patch(f"{_DRONE}.route_command", return_value=mock_result) as mock_route, + ): + result = _handle_target(["@seedgo", "-h"]) + assert result == 0 + mock_route.assert_called_once_with("@seedgo", "-h", interactive=True) + + def test_status_routes_interactive(self) -> None: + """status command routes with interactive=True for Rich color output.""" + from aipass.drone.apps.drone import _handle_target + from aipass.drone.apps.handlers.executor import CommandResult + + mock_result = CommandResult(stdout="", stderr="", exit_code=0, branch="hooks", command="status") + with ( + patch(f"{_DRONE}.is_module", return_value=False), + patch(f"{_DRONE}.route_command", return_value=mock_result) as mock_route, + ): + result = _handle_target(["@hooks", "status"]) + assert result == 0 + call_kwargs = mock_route.call_args.kwargs + assert call_kwargs["interactive"] is True + + def test_help_flag_module_fallback(self) -> None: + """--help BranchNotFoundError for a module falls back to _handle_module.""" + from aipass.drone.apps.drone import _handle_target + from aipass.drone.apps.modules import BranchNotFoundError + + with ( + patch(f"{_DRONE}.is_module", side_effect=[False, True]), + patch(f"{_DRONE}.route_command", side_effect=BranchNotFoundError("not found")), + patch(f"{_DRONE}._handle_module", return_value=0) as mock_hm, + ): + result = _handle_target(["@seedgo", "--help"]) + assert result == 0 + mock_hm.assert_called_once_with("seedgo", ["--help"]) + def test_branch_not_found_module_fallback(self) -> None: """BranchNotFoundError for a module falls back to _handle_module.""" from aipass.drone.apps.drone import _handle_target diff --git a/src/aipass/flow/apps/handlers/template/registry_ops.py b/src/aipass/flow/apps/handlers/template/registry_ops.py index 89763672..26e44f0e 100644 --- a/src/aipass/flow/apps/handlers/template/registry_ops.py +++ b/src/aipass/flow/apps/handlers/template/registry_ops.py @@ -278,6 +278,45 @@ def save_registry(data: Dict[str, Any]) -> bool: return False +def _try_override_auto_entry(registry: Dict[str, Any], dir_name: str, new_prefix: str) -> str | None: + """Remove an auto-registered entry so add_type() can re-add with explicit prefix. + + Returns an error message on failure, or None on success. + """ + existing = registry["types"][dir_name] + old_shorthand = existing.get("shorthand", existing.get("prefix", "").lower()) + old_prefix = existing.get("prefix", "") + + if old_prefix.upper() != new_prefix.upper(): + old_reg = FLOW_ROOT / "flow_json" / f"{old_shorthand}_registry.json" + if old_reg.exists(): + has_plans = _auto_reg_has_plans(old_reg) + if has_plans: + return f"Cannot override auto-registered '{dir_name}' — {old_reg.name} has existing plans" + old_reg.unlink() + + del registry["types"][dir_name] + logger.info( + "[%s] Overriding auto-registered type '%s' (%s -> %s)", + MODULE_NAME, + dir_name, + old_prefix, + new_prefix, + ) + return None + + +def _auto_reg_has_plans(reg_path: Path) -> bool: + """Check whether a plan registry file contains any plans.""" + try: + with open(reg_path, "r", encoding="utf-8") as fh: + data = json.load(fh) + return bool(data.get("plans")) + except (json.JSONDecodeError, OSError) as exc: + logger.warning("[%s] Could not read plan registry %s: %s", MODULE_NAME, reg_path.name, exc) + return False + + def add_type( dir_name: str, prefix: str, @@ -304,17 +343,26 @@ def add_type( """ registry = load_registry() - # Validate: dir_name not already registered - if dir_name in registry["types"]: + # Allow override of auto-registered entries with explicit prefix + existing = registry["types"].get(dir_name) + if existing and existing.get("registered_by") != "auto": logger.error( - "[%s] Type '%s' is already registered", + "[%s] Type '%s' is already registered (by %s)", MODULE_NAME, dir_name, + existing.get("registered_by", "unknown"), ) return False - # Validate: prefix not already taken (case-insensitive) - if prefix_exists(prefix): + if existing and existing.get("registered_by") == "auto": + override_err = _try_override_auto_entry(registry, dir_name, prefix) + if override_err: + logger.error("[%s] %s", MODULE_NAME, override_err) + return False + + # Validate: prefix not already taken by another type (case-insensitive) + upper = prefix.upper() + if any(entry.get("prefix", "").upper() == upper for d, entry in registry["types"].items() if d != dir_name): logger.error( "[%s] Prefix '%s' is already in use by another type", MODULE_NAME, @@ -427,20 +475,6 @@ def remove_type(dir_name: str) -> bool: # --------------------------------------------------------------------------- -def prefix_exists(prefix: str) -> bool: - """Check whether any registered type uses *prefix* (case-insensitive). - - Args: - prefix: The prefix to look for. - - Returns: - True if the prefix is already in use. - """ - registry = load_registry() - upper = prefix.upper() - return any(entry.get("prefix", "").upper() == upper for entry in registry["types"].values()) - - def get_prefix_map() -> Dict[str, str]: """Return ``{dir_name: prefix}`` for all registered types. diff --git a/src/aipass/flow/apps/modules/create_plan.py b/src/aipass/flow/apps/modules/create_plan.py index 64d81198..c4be5617 100755 --- a/src/aipass/flow/apps/modules/create_plan.py +++ b/src/aipass/flow/apps/modules/create_plan.py @@ -33,6 +33,7 @@ import sys from pathlib import Path from typing import Any, Dict, Tuple, List +# ruff: noqa: E402 # INFRASTRUCTURE IMPORT PATTERN _PKG_ROOT = Path(__file__).resolve().parents[3] # file.py -> modules/ -> apps/ -> flow/ -> aipass/ FLOW_ROOT = _PKG_ROOT / "flow" @@ -124,26 +125,51 @@ def print_introspection(): def print_help(): """Print help information for create_plan module""" + from aipass.flow.apps.handlers.template.registry_ops import load_registry + console.print() console.print("[bold cyan]create_plan[/bold cyan] — Create new PLAN file") console.print() console.print("[yellow]USAGE:[/yellow]") + console.print(' drone @flow create "Subject" [type]') console.print(' drone @flow create "Subject" [template] [type]') console.print() - console.print("[yellow]TEMPLATES:[/yellow]") - console.print(" default Single task [dim](default)[/dim]") - console.print(" master Multi-phase project") - console.print() + + registry = load_registry() + types = registry.get("types", {}) console.print("[yellow]TYPES:[/yellow]") console.print(" (none) FPLAN [dim](default)[/dim]") - console.print(" dplan DPLAN") + for dir_name, entry in sorted(types.items()): + prefix = entry.get("prefix", "???") + shorthand = entry.get("shorthand", prefix.lower()) + if dir_name == "flow_plans": + continue + templates_dir = FLOW_ROOT / "templates" / dir_name + templates = sorted(p.stem for p in templates_dir.glob("*.md")) if templates_dir.is_dir() else [] + tmpl_hint = f" [dim]templates: {', '.join(templates)}[/dim]" if len(templates) > 1 else "" + console.print(f" {shorthand:<12} {prefix}{tmpl_hint}") console.print() + + console.print("[yellow]TEMPLATE SELECTION:[/yellow]") + console.print(" The 4th arg selects a non-default template within a type.") + console.print(" Any .md file stem in the type's templates/ dir works.") + console.print(' [dim]drone @flow create . "Subject" sunday_merge pplan[/dim]') + console.print(' [dim]drone @flow create . "Subject" master[/dim] # FPLAN master') + console.print() + console.print("[yellow]EXAMPLES:[/yellow]") console.print(' [dim]drone @flow create . "Implementation task"[/dim] # FPLAN default') console.print(' [dim]drone @flow create . "Multi-phase project" master[/dim] # FPLAN master') console.print(' [dim]drone @flow create . "Design investigation" dplan[/dim] # DPLAN') console.print() + console.print("[bold]ADD A NEW PLAN TYPE:[/bold]") + console.print(" 1. Create templates// with .md template files") + console.print(" 2. drone @flow register ") + console.print(' 3. drone @flow create . "Subject" ') + console.print(" [dim]See: drone @flow templates --help[/dim]") + console.print() + # ============================================= # ORCHESTRATION WORKFLOWS (thin wrappers) diff --git a/src/aipass/flow/apps/modules/template_manager.py b/src/aipass/flow/apps/modules/template_manager.py index 2780a8e8..a5eac074 100644 --- a/src/aipass/flow/apps/modules/template_manager.py +++ b/src/aipass/flow/apps/modules/template_manager.py @@ -26,6 +26,7 @@ import sys from pathlib import Path from typing import List +# ruff: noqa: E402 # INFRASTRUCTURE IMPORT PATTERN _PKG_ROOT = Path(__file__).resolve().parents[3] # file.py -> modules/ -> apps/ -> flow/ -> aipass/ FLOW_ROOT = _PKG_ROOT / "flow" @@ -85,6 +86,19 @@ def print_help(): console.print(" drone @flow unregister Remove plan type registration") console.print(" drone @flow scan Find unregistered template directories") console.print() + console.print("[bold]HOW TO ADD A NEW PLAN TYPE / SOP TEMPLATE:[/bold]") + console.print(" 1. Create a directory under templates/ (e.g. templates/playbook_plans/)") + console.print(" 2. Add one or more .md template files (e.g. default.md, sunday_merge.md)") + console.print(" 3. Register with your chosen prefix:") + console.print(" drone @flow register playbook_plans PBPLAN") + console.print(" 4. Create plans:") + console.print(' drone @flow create . "Subject" pbplan') + console.print(' drone @flow create . "Subject" sunday_merge pbplan') + console.print() + console.print(" [dim]Auto-registration runs on any flow command if you skip step 3,[/dim]") + console.print(" [dim]but derives the prefix automatically. Use register to choose your own.[/dim]") + console.print(" [dim]Register overrides an auto-derived prefix if no plans exist yet.[/dim]") + console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Register testing/ as TPLAN[/dim]") console.print(" drone @flow register testing TPLAN") diff --git a/src/aipass/flow/templates/playbook_plans/default.md b/src/aipass/flow/templates/playbook_plans/default.md new file mode 100644 index 00000000..0ef8f107 --- /dev/null +++ b/src/aipass/flow/templates/playbook_plans/default.md @@ -0,0 +1,67 @@ +# {plan_number} - {subject} (PLAYBOOK) + +**Created**: {today} +**Branch**: {location} +**Status**: Active +**Type**: Playbook (SOP run) + +--- + +## What Are Playbooks? + +Playbooks (PBPLANs) are **throwaway SOP runs** — a checklist stamped from a reusable +template for a recurring operation (Sunday merge, release cut, branch onboarding, +incident response). You tick steps off as you go, log what happened, then close. + +- **The template = the SOP.** Stable. Refine it over time as the process improves. +- **The instance (this file) = one run.** Disposable. Close when the run is done. + +Closing vectorizes the run to @memory — so the **Run Summary** below (with PR numbers, +tags, anything that broke) becomes a searchable trail. Costs nothing, gives history. + +**This is NOT for:** building features (FPLAN), design/investigation (DPLAN), +research (RPLAN), or multi-branch builds (TDPLAN). Playbooks are for *operating the +system*, not changing it. + +**Add a new SOP:** drop `templates/playbook_plans/.md`, then +`drone @flow create . "Subject" `. No registration needed (the type is +already registered; the file stem is the shorthand). + +--- + +## Steps + +Replace with the actual checklist for this SOP. + +- [ ] Step 1 +- [ ] Step 2 +- [ ] Step 3 + +--- + +## Run Summary + +Fill as you go — this is the vectorized trail. Be specific: PR numbers, tags, SHAs, +anything that broke and how it was handled. + +- **Date:** {today} +- **Outcome:** +- **PRs / tags / commits:** +- **Issues hit:** +- **Notes for next run:** + +--- + +## Listen (TTS-friendly summary) + +Write a plain English summary of this run here. No markdown, no symbols, no tables, +no code blocks, no asterisks, no bullet points. Just natural sentences for text to speech. + +--- + +## Close Command + +When all steps are ticked and the Run Summary is filled: +```bash +drone @flow close {plan_number} +``` diff --git a/src/aipass/flow/templates/playbook_plans/sunday_merge.md b/src/aipass/flow/templates/playbook_plans/sunday_merge.md new file mode 100644 index 00000000..799890cf --- /dev/null +++ b/src/aipass/flow/templates/playbook_plans/sunday_merge.md @@ -0,0 +1,123 @@ +# {plan_number} - {subject} (SUNDAY MERGE) + +**Created**: {today} +**Branch**: {location} +**Status**: Active +**Type**: Playbook — Sunday Merge SOP + +--- + +## Purpose + +The weekly `dev → main` merge + release tag. Run by **devpulse** (only branch with git +write). Tick each step as you go; fill the **Run Summary** with PR numbers and tags for +the vectorized trail. Close when done. + +> All git writes go through `drone @git` — **run drone from a branch dir** (it needs +> `.trinity/passport.json` in the cwd; running from the repo root fails with "No +> passport found"). Read git (`status`, `log`, `diff`, `rev-parse`) is allowed raw. +> ⚠️ `drone @git` has **no `tag` verb** — pushing the release tag is a MANUAL step +> (Patrick, or raw `git tag`/`push` via `!`). All other writes go through drone. + +--- + +## 1. Pre-flight + +- [ ] On `dev`, working tree understood: `drone @git status --all` +- [ ] Confirm what's shipping this week — scan uncommitted changes + already-pushed dev commits ahead of main: `git rev-list --count main..dev` (read git, raw ok) +- [ ] No surprise files (stray `/tmp` artifacts, test pollution, `.recovery`/`.archive` churn). Clean = archive, never delete. +- [ ] Decide: **release tag this week?** (tag = PyPI publish + GitHub Release). If yes, note target version. + +## 2. Verify, commit, CHANGELOG + +- [ ] **Run the CI audit gate LOCALLY before pushing** (local == CI, S199 parity — catches red before the PR): `cd && .venv/bin/python .github/scripts/seedgo_audit.py` → expect all 13 branches `>=100%`, exit 0. Uses a relative `src/aipass` path, so run from the repo **root**, not a branch dir. +- [ ] Update `CHANGELOG.md` — add entries under the current week's `[YYYY.WNN]` section (don't batch; mostly done as work landed). Sort into Added / Changed / Fixed. +- [ ] Commit: `drone @git commit "msg" --all` (from a branch dir, e.g. devpulse). New/untracked files (e.g. new templates) — confirm they got staged: `git ls-files ` after; `--all` may not pick up untracked. +- [ ] Every commit pushed — local-only commits are invisible + +## 3. Open / update the PR + +- [ ] `drone @git dev-pr "Week summary: what's shipping"` +- [ ] "PR already open" in output = push succeeded onto the existing PR (expected on re-runs) +- [ ] Record the PR number → Run Summary + +## 4. Wait for CI green (ALL required checks) + +The PR gate (verified against `.github/workflows/`): +- [ ] `ci.yml` → **lint**, **test**, **standards** (= seedgo-audit / the README + 100%-floor check, runs `.github/scripts/seedgo_audit.py`), **coverage** +- [ ] `security.yml` → Security Scan / dependency-scan +- [ ] `e2e-wheel.yml` → 3-OS wheel smoke (path-filtered: fires on `src/**`, `tests/e2e/**`, `pyproject.toml`) +- [ ] `windows-test.yml` / `macos-test.yml` → required checks, run on every PR (must NEVER be path-filtered or they park as "Expected/waiting" forever and block merge) +- [ ] If "all green but can't merge": it's usually post-push mergeability **lag**. Confirm ground truth via the public API (no gh, no gate): + - `curl -s https://api.github.com/repos/AIOSAI/AIPass/commits//check-runs` → all check-runs success (incl. app checks: codecov, CodeQL) + - `curl -s https://api.github.com/repos/AIOSAI/AIPass/pulls/` → `mergeable_state: clean` + +## 5. Merge to main + +- [ ] **User's call to merge** — confirm GO +- [ ] `drone @git merge ` (squash-merge) +- [ ] ⚠️ **Verify `dev` SURVIVES the merge** (the #625 scar — empirical, every time): `drone @git branches` → `dev` still present; `git rev-parse dev` resolves + +## 6. Post-merge realign + +- [ ] Pull main locally: `drone @git sync` +- [ ] If merged via GitHub UI (bypassing `drone @git merge`), fast-forward dev to main so dev doesn't fall behind / revert main-only commits (e.g. Dependabot): dev is an ancestor → `git merge --ff-only main` is clean (via `drone @git`) +- [ ] Dependabot / other PRs targeting main: they go green once main has the fix + bots rebase — check after the push + +## 7. Release tag (only if cutting a release) + +**Versioning rule — bump by SIGNIFICANCE, not cadence** (keeps the version from inflating weekly): +- **PATCH** (`x.y.Z+1`) = fix / internal / standards / UX only → the default, most weeks +- **MINOR** (`x.Y+1.0`) = a new backward-compatible user-facing feature shipped +- **MAJOR** (`X+1.0.0`) = breaking public-API change + +(aipass is a 2.x library others pin → keep SemVer; the CHANGELOG keeps its `YYYY.WNN` header as a date index.) + +How the release fires (verified `publish.yml`): a `v*` **git tag push** runs build → PyPI publish → GitHub Release. Key facts: +- PyPI version = `pyproject.toml [project] version` at the tagged commit — **NOT** the tag string (the tag only *triggers* the build). +- Tag and `pyproject` version **must match** (`v2.5.2` ⇄ `version = "2.5.2"`), or PyPI publishes the wrong number while the Release is named the tag. +- PyPI **rejects a duplicate version** → if shipping, you MUST bump. +- GitHub Release notes = the **topmost `## [...]` CHANGELOG block** (awk-extracted). + +Steps: +- [ ] Bump `pyproject.toml` version per the rule above, **on dev so it rides into the PR** (then main's merge commit carries the right version) +- [ ] Confirm the CHANGELOG top section is the release notes you want +- [ ] **Push the tag — MANUAL (drone has no `tag` verb):** Patrick, or raw `git tag v ` + `git push origin v` via `!`, on the merged main commit +- [ ] Verify PyPI shows the new version + the GitHub Release appeared +- [ ] Record the tag → Run Summary + +## 8. Wrap + +- [ ] Update `.trinity/` memories (session log: what merged, PR#, tag) +- [ ] Update `STATUS.local.md` +- [ ] Fill **Run Summary** below (PR numbers, tag, anything that broke) +- [ ] Close this playbook → vectorizes the run + +--- + +## Run Summary + +- **Date:** {today} +- **Outcome:** (merged clean / issues / no-merge) +- **PR(s) merged:** # +- **Release tag:** v +- **CI notes:** (any flaky/red checks + how cleared) +- **dev survived merge:** yes / no +- **Issues hit:** +- **Notes for next run:** (refine this SOP — what was missing or wrong?) + +--- + +## Listen (TTS-friendly summary) + +Write a plain English summary of this Sunday merge here when done. No markdown, no symbols, +no tables, no code blocks, no asterisks, no bullet points. Just natural sentences for text to speech. + +--- + +## Close Command + +When all steps are ticked and the Run Summary is filled: +```bash +drone @flow close {plan_number} +``` diff --git a/src/aipass/flow/tests/test_mbank_template.py b/src/aipass/flow/tests/test_mbank_template.py index 7718ae4c..039df2fc 100644 --- a/src/aipass/flow/tests/test_mbank_template.py +++ b/src/aipass/flow/tests/test_mbank_template.py @@ -939,89 +939,6 @@ class TestDiscoverPlanTypes: assert result == {} -# =================================================================== -# 9. template/registry_ops.py — prefix_exists -# =================================================================== - - -class TestPrefixExists: - def test_existing_prefix_returns_true(self, tmp_path): - """prefix_exists returns True for a registered prefix.""" - registry = { - "types": { - "flow_plans": {"prefix": "FPLAN", "shorthand": "fplan"}, - }, - "metadata": {"version": "1.0.0", "last_updated": "2026-03-18", "type_count": 1}, - } - reg_path = tmp_path / "template_registry.json" - reg_path.write_text(json.dumps(registry), encoding="utf-8") - - # Also create the templates dir so auto-heal does not prune - templates_dir = tmp_path / "templates" / "flow_plans" - templates_dir.mkdir(parents=True) - - with ( - patch( - "aipass.flow.apps.handlers.template.registry_ops.REGISTRY_PATH", - reg_path, - ), - patch("aipass.flow.apps.handlers.template.registry_ops.FLOW_ROOT", tmp_path), - ): - from aipass.flow.apps.handlers.template.registry_ops import prefix_exists - - assert prefix_exists("FPLAN") is True - - def test_case_insensitive_match(self, tmp_path): - """prefix_exists is case-insensitive.""" - registry = { - "types": { - "flow_plans": {"prefix": "FPLAN", "shorthand": "fplan"}, - }, - "metadata": {"version": "1.0.0", "last_updated": "2026-03-18", "type_count": 1}, - } - reg_path = tmp_path / "template_registry.json" - reg_path.write_text(json.dumps(registry), encoding="utf-8") - - templates_dir = tmp_path / "templates" / "flow_plans" - templates_dir.mkdir(parents=True) - - with ( - patch( - "aipass.flow.apps.handlers.template.registry_ops.REGISTRY_PATH", - reg_path, - ), - patch("aipass.flow.apps.handlers.template.registry_ops.FLOW_ROOT", tmp_path), - ): - from aipass.flow.apps.handlers.template.registry_ops import prefix_exists - - assert prefix_exists("fplan") is True - - def test_nonexistent_prefix_returns_false(self, tmp_path): - """prefix_exists returns False for an unregistered prefix.""" - registry = { - "types": { - "flow_plans": {"prefix": "FPLAN", "shorthand": "fplan"}, - }, - "metadata": {"version": "1.0.0", "last_updated": "2026-03-18", "type_count": 1}, - } - reg_path = tmp_path / "template_registry.json" - reg_path.write_text(json.dumps(registry), encoding="utf-8") - - templates_dir = tmp_path / "templates" / "flow_plans" - templates_dir.mkdir(parents=True) - - with ( - patch( - "aipass.flow.apps.handlers.template.registry_ops.REGISTRY_PATH", - reg_path, - ), - patch("aipass.flow.apps.handlers.template.registry_ops.FLOW_ROOT", tmp_path), - ): - from aipass.flow.apps.handlers.template.registry_ops import prefix_exists - - assert prefix_exists("ZPLAN") is False - - # =================================================================== # 10. template/registry_ops.py — get_prefix_map # =================================================================== diff --git a/src/aipass/flow/tests/test_registry_ops.py b/src/aipass/flow/tests/test_registry_ops.py index 15e9f2c9..d9f24437 100644 --- a/src/aipass/flow/tests/test_registry_ops.py +++ b/src/aipass/flow/tests/test_registry_ops.py @@ -441,46 +441,6 @@ class TestRemoveType: assert result is False -# ============================================================================= -# prefix_exists -# ============================================================================= - - -class TestPrefixExists: - """Tests for prefix_exists() — case-insensitive prefix lookup.""" - - def test_finds_existing_prefix_exact_case(self, setup_flow_root): - """Finds prefix with exact case match.""" - mod = _import_mod() - _create_template_dir(setup_flow_root, "flow_plans", ["default.md"]) - _create_template_dir(setup_flow_root, "dev_plans", ["default.md"]) - data = _valid_registry() - _write_registry(setup_flow_root, data) - - assert mod.prefix_exists("FPLAN") is True - - def test_finds_existing_prefix_case_insensitive(self, setup_flow_root): - """Finds prefix regardless of case.""" - mod = _import_mod() - _create_template_dir(setup_flow_root, "flow_plans", ["default.md"]) - _create_template_dir(setup_flow_root, "dev_plans", ["default.md"]) - data = _valid_registry() - _write_registry(setup_flow_root, data) - - assert mod.prefix_exists("fplan") is True - assert mod.prefix_exists("Fplan") is True - - def test_returns_false_for_unknown_prefix(self, setup_flow_root): - """Returns False for a prefix not in the registry.""" - mod = _import_mod() - _create_template_dir(setup_flow_root, "flow_plans", ["default.md"]) - _create_template_dir(setup_flow_root, "dev_plans", ["default.md"]) - data = _valid_registry() - _write_registry(setup_flow_root, data) - - assert mod.prefix_exists("ZPLAN") is False - - # ============================================================================= # get_prefix_map # ============================================================================= diff --git a/src/aipass/hooks/apps/hooks.py b/src/aipass/hooks/apps/hooks.py index 8132c611..1210af18 100644 --- a/src/aipass/hooks/apps/hooks.py +++ b/src/aipass/hooks/apps/hooks.py @@ -5,7 +5,7 @@ # Branch: hooks # Layer: apps # Created: 2026-05-18 -# Modified: 2026-05-19 +# Modified: 2026-06-07 # ============================================= """ @@ -78,33 +78,86 @@ def discover_modules() -> list[Any]: def print_introspection(): """Print branch introspection — discovered modules and capabilities.""" modules = discover_modules() - CONSOLE.print("[bold cyan]HOOKS[/bold cyan] — Hook Infrastructure for AIPass") - CONSOLE.print(f" Modules discovered: {len(modules)}") + CONSOLE.print() + CONSOLE.print("[bold cyan]HOOKS — Hook Infrastructure for AIPass[/bold cyan]") + CONSOLE.print() + CONSOLE.print("[dim]Dispatches hooks across platforms with per-project config, logging, and crash isolation.[/dim]") + CONSOLE.print() + + CONSOLE.print(f"[yellow]Discovered Modules:[/yellow] {len(modules)}") + CONSOLE.print() for module in modules: name = module.__name__.split(".")[-1] desc = (module.__doc__ or "").strip().split("\n")[0] if module.__doc__ else "No description" - CONSOLE.print(f" {name:20} {desc}") + CONSOLE.print(f" [cyan]•[/cyan] {name:20} [dim]{desc}[/dim]") + + CONSOLE.print() + CONSOLE.print("Run [green]'drone @hooks --help'[/green] for usage information") + CONSOLE.print() def print_help(): """Print CLI help — usage instructions and available commands.""" modules = discover_modules() - CONSOLE.print("[bold cyan]HOOKS[/bold cyan] — Usage") CONSOLE.print() - CONSOLE.print(" drone @hooks [args...]") + CONSOLE.print("[bold cyan]HOOKS[/bold cyan] [dim]v1.1.0[/dim] — Hook Infrastructure for AIPass") + CONSOLE.print() + CONSOLE.print("[dim]Dispatches hooks across platforms with per-project config, logging, and crash isolation.[/dim]") + CONSOLE.print() + CONSOLE.print("─" * 70) + CONSOLE.print() + + CONSOLE.print("[bold cyan]USAGE:[/bold cyan]") + CONSOLE.print() + CONSOLE.print(" [dim]drone @hooks [args...][/dim]") + CONSOLE.print(" [dim]drone @hooks --help[/dim]") + CONSOLE.print() + CONSOLE.print("─" * 70) + CONSOLE.print() + + CONSOLE.print("[bold cyan]COMMANDS:[/bold cyan]") CONSOLE.print() - CONSOLE.print("[bold]COMMANDS:[/bold]") for module in modules: - name = module.__name__.split(".")[-1] - desc = (module.__doc__ or "").strip().split("\n")[0] if module.__doc__ else "No description" - CONSOLE.print(f" {name:20} {desc}") + commands = getattr(module, "HELP_COMMANDS", None) + if commands: + for cmd, desc in commands: + CONSOLE.print(f" [green]{cmd:26}[/green] [dim]{desc}[/dim]") + else: + name = module.__name__.split(".")[-1] + desc = (module.__doc__ or "").strip().split("\n")[0] if module.__doc__ else "No description" + CONSOLE.print(f" [green]{name:26}[/green] [dim]{desc}[/dim]") + CONSOLE.print() - CONSOLE.print("[bold]BRIDGES:[/bold]") - CONSOLE.print(" claude Claude Code bridge (provider settings entry point)") + CONSOLE.print("─" * 70) + CONSOLE.print() + + CONSOLE.print("[bold cyan]BRIDGES:[/bold cyan]") + CONSOLE.print() + CONSOLE.print( + " [green]claude[/green] [dim]Claude Code bridge (provider settings entry point)[/dim]" + ) + CONSOLE.print() + CONSOLE.print("─" * 70) + CONSOLE.print() + + CONSOLE.print("[bold cyan]EXAMPLES:[/bold cyan]") + CONSOLE.print() + CONSOLE.print(" [dim]drone @hooks status[/dim] [dim]# Show hook config for current project[/dim]") + CONSOLE.print(" [dim]drone @hooks log[/dim] [dim]# Tail recent hook activity[/dim]") + CONSOLE.print(" [dim]drone @hooks hooksound off[/dim] [dim]# Mute all hook sounds[/dim]") + CONSOLE.print(" [dim]drone @hooks hooksound on[/dim] [dim]# Unmute all hook sounds[/dim]") + CONSOLE.print() + CONSOLE.print("─" * 70) + CONSOLE.print() + + CONSOLE.print("[bold cyan]FLAGS:[/bold cyan]") + CONSOLE.print() + CONSOLE.print(" [green]--help, -h[/green] [dim]Show this help message[/dim]") + CONSOLE.print(" [green]--version, -V[/green] [dim]Show version[/dim]") + CONSOLE.print() + CONSOLE.print("[bold]TIP:[/bold] For command-specific help:") + CONSOLE.print(" [dim]drone @hooks --help[/dim]") CONSOLE.print() - CONSOLE.print("[bold]FLAGS:[/bold]") - CONSOLE.print(" --help, -h Show this help message") - CONSOLE.print(" --version, -V Show version") def route_command(command: str, args: list[str], modules: list[Any]) -> bool: diff --git a/src/aipass/hooks/apps/modules/engine.py b/src/aipass/hooks/apps/modules/engine.py index 7f656ae9..c57a2a95 100644 --- a/src/aipass/hooks/apps/modules/engine.py +++ b/src/aipass/hooks/apps/modules/engine.py @@ -24,6 +24,10 @@ from aipass.hooks.apps.handlers.config.diagnostics import log_entry as _log, tai CONSOLE = err_console BRANCH_ROOT = Path(__file__).resolve().parent.parent.parent +HELP_COMMANDS = [ + ("log", "Tail recent hook activity (last 20 entries)"), +] + def _run_hook(hook_cmd: str, stdin_data: str, timeout_s: int = 30) -> dict: """Run a single hook subprocess, capture output and timing.""" diff --git a/src/aipass/hooks/apps/modules/hooksound.py b/src/aipass/hooks/apps/modules/hooksound.py index 5261d43d..541b752e 100644 --- a/src/aipass/hooks/apps/modules/hooksound.py +++ b/src/aipass/hooks/apps/modules/hooksound.py @@ -16,6 +16,12 @@ from aipass.prax.apps.modules.logger import system_logger as logger # noqa: F40 CONSOLE = err_console +HELP_COMMANDS = [ + ("hooksound on", "Unmute all hook sounds"), + ("hooksound off", "Mute all hook sounds"), + ("hooksound", "Show current sound status"), +] + def print_introspection(): """Print module structure for drone routing.""" diff --git a/src/aipass/hooks/apps/modules/hookstatus.py b/src/aipass/hooks/apps/modules/hookstatus.py index 49c07f17..9b084ab9 100644 --- a/src/aipass/hooks/apps/modules/hookstatus.py +++ b/src/aipass/hooks/apps/modules/hookstatus.py @@ -16,6 +16,10 @@ from aipass.prax.apps.modules.logger import system_logger as logger # noqa: F40 CONSOLE = err_console +HELP_COMMANDS = [ + ("status", "Show current project hook config"), +] + EVENT_TYPES = [ "UserPromptSubmit", "PreToolUse", diff --git a/src/aipass/hooks/tests/test_engine.py b/src/aipass/hooks/tests/test_engine.py index f12f9b12..9e0bbf6e 100644 --- a/src/aipass/hooks/tests/test_engine.py +++ b/src/aipass/hooks/tests/test_engine.py @@ -290,6 +290,7 @@ class TestFindProjectConfig: with patch("aipass.hooks.apps.modules.engine.Path.cwd", return_value=temp_test_dir): with patch("aipass.hooks.apps.handlers.config.loader.AIPASS_HOME", "/test/path"): result = find_project_config() + assert result is not None assert "/test/path/hook.py" in result["Stop"]["sound"]["command"] @@ -354,7 +355,7 @@ class TestHooksEntryPoint: print_introspection() captured = capsys.readouterr() assert "HOOKS" in captured.err - assert "Modules discovered" in captured.err + assert "Discovered Modules" in captured.err def test_handle_command_returns_bool(self): from aipass.hooks.apps.hooks import handle_command @@ -564,6 +565,44 @@ class TestCliRouting: assert "HOOKS" in captured.err assert "drone @hooks" in captured.err + def test_print_help_surfaces_subcommands(self, capsys): + from aipass.hooks.apps.hooks import print_help + + print_help() + captured = capsys.readouterr() + assert "hooksound on" in captured.err + assert "hooksound off" in captured.err + assert "status" in captured.err + assert "log" in captured.err + + def test_print_help_has_examples_section(self, capsys): + from aipass.hooks.apps.hooks import print_help + + print_help() + captured = capsys.readouterr() + assert "EXAMPLES" in captured.err + assert "drone @hooks status" in captured.err + assert "drone @hooks hooksound off" in captured.err + + def test_print_help_has_usage_section(self, capsys): + from aipass.hooks.apps.hooks import print_help + + print_help() + captured = capsys.readouterr() + assert "USAGE" in captured.err + assert "drone @hooks " in captured.err + + def test_help_commands_auto_discovered(self, capsys): + from aipass.hooks.apps.hooks import print_help + from aipass.hooks.apps.modules.hooksound import HELP_COMMANDS as hs_cmds + from aipass.hooks.apps.modules.hookstatus import HELP_COMMANDS as hst_cmds + from aipass.hooks.apps.modules.engine import HELP_COMMANDS as eng_cmds + + print_help() + captured = capsys.readouterr() + for cmd, _ in hs_cmds + hst_cmds + eng_cmds: + assert cmd in captured.err + def test_output_capture_status(self, capsys): from aipass.hooks.apps.hooks import handle_command diff --git a/src/aipass/prax/apps/modules/logger.py b/src/aipass/prax/apps/modules/logger.py index d6700fde..062d8bca 100755 --- a/src/aipass/prax/apps/modules/logger.py +++ b/src/aipass/prax/apps/modules/logger.py @@ -236,30 +236,34 @@ def print_introspection(): console = Console() console.print() - console.print("logger Module") + console.print("[bold cyan]logger Module[/bold cyan]") console.print("Public API for PRAX system-wide logging with auto-routing and lifecycle management") console.print() - console.print("Connected Handlers:") - console.print(" handlers/logging/") - console.print(" - setup.py (setup_individual_logger — creates per-module log files)") - console.print(" - setup.py (get_captured_loggers_count — returns active logger count)") - console.print(" - setup.py (enable_terminal_output — enables live terminal log output)") - console.print(" - setup.py (disable_terminal_output — disables terminal log output)") - console.print(" - introspection.py (get_calling_module — resolves caller module name)") - console.print(" - override.py (is_override_active — checks logger override status)") - console.print(" - direct.py (get_direct_logger — bypasses event pipeline for infrastructure logging)") - console.print(" - direct.py (direct_log — shorthand for direct logging calls)") - console.print(" - direct.py (DirectLogger — direct logger class)") - console.print(" handlers/discovery/") - console.print(" - watcher.py (start_file_watcher — starts filesystem watcher for module discovery)") - console.print(" - watcher.py (stop_file_watcher — stops the filesystem watcher)") - console.print(" - watcher.py (is_file_watcher_active — checks watcher status)") - console.print(" handlers/registry/") - console.print(" - load.py (load_module_registry — loads discovered module registry)") - console.print(" handlers/config/") - console.print(" - load.py (get_system_logs_dir — returns system logs directory path)") - console.print(" - load.py (get_module_logs_dir — returns per-module logs directory path)") - console.print(" - load.py (PRAX_JSON_DIR — base path for prax JSON data files)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print() + console.print(" [cyan]handlers/logging/[/cyan]") + console.print(" [dim]→ setup.py (setup_individual_logger — creates per-module log files)[/dim]") + console.print(" [dim]→ setup.py (get_captured_loggers_count — returns active logger count)[/dim]") + console.print(" [dim]→ setup.py (enable_terminal_output — enables live terminal log output)[/dim]") + console.print(" [dim]→ setup.py (disable_terminal_output — disables terminal log output)[/dim]") + console.print(" [dim]→ introspection.py (get_calling_module — resolves caller module name)[/dim]") + console.print(" [dim]→ override.py (is_override_active — checks logger override status)[/dim]") + console.print(" [dim]→ direct.py (get_direct_logger — bypasses event pipeline for infrastructure logging)[/dim]") + console.print(" [dim]→ direct.py (direct_log — shorthand for direct logging calls)[/dim]") + console.print(" [dim]→ direct.py (DirectLogger — direct logger class)[/dim]") + console.print() + console.print(" [cyan]handlers/discovery/[/cyan]") + console.print(" [dim]→ watcher.py (start_file_watcher — starts filesystem watcher for module discovery)[/dim]") + console.print(" [dim]→ watcher.py (stop_file_watcher — stops the filesystem watcher)[/dim]") + console.print(" [dim]→ watcher.py (is_file_watcher_active — checks watcher status)[/dim]") + console.print() + console.print(" [cyan]handlers/registry/[/cyan]") + console.print(" [dim]→ load.py (load_module_registry — loads discovered module registry)[/dim]") + console.print() + console.print(" [cyan]handlers/config/[/cyan]") + console.print(" [dim]→ load.py (get_system_logs_dir — returns system logs directory path)[/dim]") + console.print(" [dim]→ load.py (get_module_logs_dir — returns per-module logs directory path)[/dim]") + console.print(" [dim]→ load.py (PRAX_JSON_DIR — base path for prax JSON data files)[/dim]") console.print() diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/introspection.md b/src/aipass/seedgo/apps/handlers/aipass_standards/introspection.md index 73d25f39..203788ca 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/introspection.md +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/introspection.md @@ -236,6 +236,28 @@ Both levels use `print_introspection()` as the function name. Same name, differe --- +## Rich Formatting Requirement + +Introspection output MUST use Rich markup tags for consistent presentation across all branches. Plain `console.print("flat text")` without any formatting tags fails the standard. + +**Required:** At least one Rich tag in introspection output strings (e.g. `[bold cyan]`, `[dim]`, `[yellow]`, `[green]`, `[/dim]`). + +```python +# ✅ CORRECT - Rich formatting +console.print("[bold cyan]Flow[/bold cyan] - PLAN Management System") +console.print(f"[yellow]Discovered Modules:[/yellow] {count}") +console.print(f" [cyan]{name}[/cyan]") + +# ❌ WRONG - Flat plain strings +console.print("Flow - PLAN Management System") +console.print(f"Discovered Modules: {count}") +console.print(f" {name}") +``` + +**Delegation:** If `print_introspection()` delegates to a `_`-prefixed helper function (e.g. `_show_branch_introspection()`), the helper must contain Rich markup. The check walks into same-file helpers to verify formatting. + +--- + ## Compliance Checklist For entry points (`apps/{name}.py`): @@ -247,6 +269,7 @@ For entry points (`apps/{name}.py`): - [ ] Filters modules by `handle_command()` presence - [ ] Lists discovered module names - [ ] Points to `--help` for usage +- [ ] Uses Rich markup tags (not flat plain strings) For modules (`apps/modules/*.py`): @@ -255,6 +278,7 @@ For modules (`apps/modules/*.py`): - [ ] Shows module name - [ ] Lists connected handlers grouped by domain - [ ] Points to `--help` for usage +- [ ] Uses Rich markup tags (not flat plain strings) For execution order: diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/introspection_check.py b/src/aipass/seedgo/apps/handlers/aipass_standards/introspection_check.py index e92b7eb4..edc86316 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/introspection_check.py +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/introspection_check.py @@ -19,6 +19,7 @@ Checks: """ import ast +import re from pathlib import Path from typing import Dict, Optional from aipass.prax import logger @@ -153,6 +154,12 @@ def check_module(module_path: str, bypass_rules: list | None = None) -> Dict: if discovery_check: checks.append(discovery_check) + # Check 1d: print_introspection uses Rich formatting (not flat plain strings) + if introspection_check["passed"]: + richness_check = check_introspection_rich_formatting(tree, path.name) + if richness_check: + checks.append(richness_check) + # Check 2: Execution order (entry points only) if is_entry_point: order_check = check_execution_order(tree, content, path.name) @@ -347,6 +354,90 @@ def _collect_hardcoded_string_lists(node: ast.AST, result: list) -> None: result.extend(string_vals) +def check_introspection_rich_formatting(tree: ast.Module, filename: str) -> Optional[Dict]: + """Verify print_introspection() uses Rich markup tags, not flat plain strings. + + AIPass design pattern requires consistent Rich formatting in introspection output. + Checks for square-bracket tags like [bold cyan], [dim], [yellow], [green], [/...]. + + Delegation: if print_introspection delegates to _-prefixed helpers in the same file, + walks into those helpers to find markup (treats delegation as valid if helper is styled). + """ + func = _find_print_introspection(tree) + if func is None: + return None + + if not _has_output_calls(func): + return None + + if _has_rich_markup(func): + return { + "name": "Rich formatting", + "passed": True, + "message": "print_introspection() uses Rich markup tags", + } + + delegated_helpers = _get_delegated_helpers(func) + if delegated_helpers: + helpers_map = _build_helpers_map(tree) + for helper_name in delegated_helpers: + helper_func = helpers_map.get(helper_name) + if helper_func and _has_rich_markup(helper_func): + return { + "name": "Rich formatting", + "passed": True, + "message": f"print_introspection() delegates to {helper_name}() which uses Rich markup", + } + return { + "name": "Rich formatting", + "passed": False, + "message": ( + f"print_introspection() in {filename} delegates to " + f"{', '.join(delegated_helpers)} but no Rich markup found — " + f"introspection output must use Rich formatting tags " + f"(e.g. [bold cyan], [dim], [yellow])" + ), + } + + return { + "name": "Rich formatting", + "passed": False, + "message": ( + f"print_introspection() in {filename} produces output but has no Rich " + f"markup tags — introspection must use Rich formatting " + f"(e.g. [bold cyan], [dim], [yellow]) for consistent design" + ), + } + + +_RICH_TAG_RE = re.compile(r"\[/?(?:bold|dim|italic|cyan|yellow|green|red|blue|magenta|white|underline)[^\]]*\]") + + +def _has_rich_markup(func_node: ast.FunctionDef) -> bool: + for node in ast.walk(func_node): + if isinstance(node, ast.Constant) and isinstance(node.value, str): + if _RICH_TAG_RE.search(node.value): + return True + return False + + +def _get_delegated_helpers(func_node: ast.FunctionDef) -> list: + helpers = [] + for node in ast.walk(func_node): + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): + if node.func.id.startswith("_"): + helpers.append(node.func.id) + return helpers + + +def _build_helpers_map(tree: ast.Module) -> dict: + result = {} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name.startswith("_"): + result[node.name] = node + return result + + def check_execution_order(tree: ast.Module, content: str, filename: str) -> Optional[Dict]: """ In the main() function (or if __name__ block), verify that no-args check @@ -402,7 +493,10 @@ def check_execution_order(tree: ast.Module, content: str, filename: str) -> Opti return { "name": "Execution order", "passed": False, - "message": f"--help check (line {help_check_line}) before no-args check (line {no_args_line}) — no-args should come first", + "message": ( + f"--help check (line {help_check_line}) before no-args check " + f"(line {no_args_line}) — no-args should come first" + ), } # If only help check found (no no-args check) @@ -503,7 +597,11 @@ def check_module_handle_command_gate(tree: ast.Module, filename: str) -> Optiona return { "name": "handle_command no-args gate", "passed": False, - "message": f"handle_command() in {filename} has no no-args gate calling print_introspection() — module will not show introspection when called with no arguments", + "message": ( + f"handle_command() in {filename} has no no-args gate calling " + f"print_introspection() — module will not show introspection " + f"when called with no arguments" + ), } @@ -638,7 +736,11 @@ def check_correct_dispatch(tree: ast.Module, filename: str) -> Optional[Dict]: return { "name": "Correct dispatch", "passed": False, - "message": f"No-args block calls print_help() in {filename} (line {node.lineno}) — should call print_introspection() (introspection != help)", + "message": ( + f"No-args block calls print_help() in {filename} " + f"(line {node.lineno}) — should call " + f"print_introspection() (introspection != help)" + ), } # Check --help block — look for help strings in the condition @@ -650,7 +752,11 @@ def check_correct_dispatch(tree: ast.Module, filename: str) -> Optional[Dict]: return { "name": "Correct dispatch", "passed": False, - "message": f"--help block calls {introspection_funcs.pop()}() in {filename} (line {node.lineno}) — should call print_help() (help != introspection)", + "message": ( + f"--help block calls {introspection_funcs.pop()}() in " + f"{filename} (line {node.lineno}) — should call " + f"print_help() (help != introspection)" + ), } # Also check: condition contains --help string AND body calls introspection @@ -662,7 +768,12 @@ def check_correct_dispatch(tree: ast.Module, filename: str) -> Optional[Dict]: return { "name": "Correct dispatch", "passed": False, - "message": f"Block with --help condition calls {introspection_funcs.pop()}() in {filename} (line {node.lineno}) — --help should show help, not introspection", + "message": ( + f"Block with --help condition calls " + f"{introspection_funcs.pop()}() in {filename} " + f"(line {node.lineno}) — --help should show help, " + f"not introspection" + ), } return { @@ -717,7 +828,11 @@ def check_content_references(tree: ast.Module, filename: str) -> Optional[Dict]: return { "name": "Content references", "passed": False, - "message": f'Help/introspection text references python3 instead of drone commands: {refs_str} in {filename} — use "drone @branch command" instead', + "message": ( + f"Help/introspection text references python3 instead of " + f"drone commands: {refs_str} in {filename} — " + f'use "drone @branch command" instead' + ), } return { @@ -751,5 +866,7 @@ def check_module_help_interception(tree: ast.Module, filename: str) -> Optional[ return { "name": "Module help interception", "passed": False, - "message": f"handle_command() in {filename} does not intercept --help — flag may fall through to business logic", + "message": ( + f"handle_command() in {filename} does not intercept --help — flag may fall through to business logic" + ), } diff --git a/src/aipass/seedgo/tests/fixtures/provider_hooks_snapshot.json b/src/aipass/seedgo/tests/fixtures/provider_hooks_snapshot.json index 05f4c055..c2ce16d8 100644 --- a/src/aipass/seedgo/tests/fixtures/provider_hooks_snapshot.json +++ b/src/aipass/seedgo/tests/fixtures/provider_hooks_snapshot.json @@ -31,6 +31,15 @@ "command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:email_notification" } ] + }, + { + "hooks": [ + { + "type": "command", + "command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:auto_process", + "timeout": 120 + } + ] } ], "PreToolUse": [ @@ -125,6 +134,26 @@ "timeout": 120 } ] + }, + { + "matcher": "manual", + "hooks": [ + { + "type": "command", + "command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreCompact:auto_process", + "timeout": 120 + } + ] + }, + { + "matcher": "auto", + "hooks": [ + { + "type": "command", + "command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreCompact:auto_process", + "timeout": 120 + } + ] } ] } diff --git a/src/aipass/seedgo/tests/test_checkers_batch7.py b/src/aipass/seedgo/tests/test_checkers_batch7.py index 30d81e37..4f2a36f9 100644 --- a/src/aipass/seedgo/tests/test_checkers_batch7.py +++ b/src/aipass/seedgo/tests/test_checkers_batch7.py @@ -56,12 +56,20 @@ def _mock_infrastructure(monkeypatch): bypass_ignore = MagicMock() bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[]) bypass_pkg.ignore_handler = bypass_ignore + bypass_utils = MagicMock() + bypass_utils.is_bypassed = MagicMock(return_value=False) + 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 [ @@ -1019,6 +1027,110 @@ class TestCheckModuleHelpInterception: assert result is None +# -- check_introspection_rich_formatting ------------------------------------ + + +class TestCheckIntrospectionRichFormatting: + """Tests for check_introspection_rich_formatting.""" + + def test_styled_introspection_passes(self): + """print_introspection with Rich markup tags passes.""" + from aipass.seedgo.apps.handlers.aipass_standards.introspection_check import ( + check_introspection_rich_formatting, + ) + + content = ( + "def print_introspection():\n" + " console.print('[bold cyan]Flow[/bold cyan] - PLAN Management')\n" + " console.print(f'[yellow]Modules:[/yellow] {count}')\n" + ) + tree = ast.parse(content) + result = check_introspection_rich_formatting(tree, "test.py") + assert result is not None + assert result["passed"] is True + assert "Rich markup" in result["message"] + + def test_flat_introspection_fails(self): + """print_introspection with only plain strings fails.""" + from aipass.seedgo.apps.handlers.aipass_standards.introspection_check import ( + check_introspection_rich_formatting, + ) + + content = ( + "def print_introspection():\n" + " console.print('spawn Entry Point')\n" + " console.print('Branch lifecycle manager')\n" + " console.print('Connected Modules:')\n" + ) + tree = ast.parse(content) + result = check_introspection_rich_formatting(tree, "test.py") + assert result is not None + assert result["passed"] is False + assert "no Rich markup" in result["message"] + + def test_delegation_to_styled_helper_passes(self): + """print_introspection delegating to a styled _helper passes.""" + from aipass.seedgo.apps.handlers.aipass_standards.introspection_check import ( + check_introspection_rich_formatting, + ) + + content = ( + "def _show_branch_introspection():\n" + " console.print('[bold cyan]Branch[/bold cyan]')\n" + " console.print(f'[yellow]Modules:[/yellow]')\n" + "\n" + "def print_introspection():\n" + " _show_branch_introspection()\n" + ) + tree = ast.parse(content) + result = check_introspection_rich_formatting(tree, "test.py") + assert result is not None + assert result["passed"] is True + assert "delegates" in result["message"] + + def test_delegation_to_flat_helper_fails(self): + """print_introspection delegating to a flat _helper fails.""" + from aipass.seedgo.apps.handlers.aipass_standards.introspection_check import ( + check_introspection_rich_formatting, + ) + + content = ( + "def _show_info():\n" + " console.print('Plain text only')\n" + " console.print('No formatting here')\n" + "\n" + "def print_introspection():\n" + " _show_info()\n" + ) + tree = ast.parse(content) + result = check_introspection_rich_formatting(tree, "test.py") + assert result is not None + assert result["passed"] is False + assert "no Rich markup" in result["message"] + + def test_no_print_introspection_returns_none(self): + """File without print_introspection returns None.""" + from aipass.seedgo.apps.handlers.aipass_standards.introspection_check import ( + check_introspection_rich_formatting, + ) + + content = "def compute():\n return 42\n" + tree = ast.parse(content) + result = check_introspection_rich_formatting(tree, "test.py") + assert result is None + + def test_no_output_returns_none(self): + """print_introspection that produces no output returns None.""" + from aipass.seedgo.apps.handlers.aipass_standards.introspection_check import ( + check_introspection_rich_formatting, + ) + + content = "def print_introspection():\n return {'name': 'test'}\n" + tree = ast.parse(content) + result = check_introspection_rich_formatting(tree, "test.py") + assert result is None + + # =========================================================================== # 4. modules_check sub-functions # =========================================================================== diff --git a/src/aipass/spawn/apps/modules/core.py b/src/aipass/spawn/apps/modules/core.py index e2020c18..07e456a3 100644 --- a/src/aipass/spawn/apps/modules/core.py +++ b/src/aipass/spawn/apps/modules/core.py @@ -63,26 +63,32 @@ DEFAULT_TEMPLATE = Path(__file__).parents[2] / "templates" / "builder" def print_introspection(): """Display module introspection info.""" console.print() - console.print("core Module") + console.print("[bold cyan]core Module[/bold cyan]") console.print("Agent creation orchestrator — full spawn workflow from template to registry") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - metadata.py (get_branch_name, normalize_branch_name, detect_profile — branch identity)") - console.print(" - placeholders.py (build_replacements_dict, validate_no_placeholders — template substitution)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") console.print( - " - file_ops.py (copy_template, rename_placeholder_paths," - " regenerate_template_registry, ensure_directory — filesystem ops)" + " [dim]- metadata.py (get_branch_name, normalize_branch_name, detect_profile — branch identity)[/dim]" ) console.print( - " - meta_ops.py (load_template_registry, generate_branch_meta, save_branch_meta — branch metadata)" + " [dim]- placeholders.py (build_replacements_dict, validate_no_placeholders — template substitution)[/dim]" ) console.print( - " - registry.py (find_registry, add_to_registry, get_next_citizen_number — AIPASS_REGISTRY management)" + " [dim]- file_ops.py (copy_template, rename_placeholder_paths," + " regenerate_template_registry, ensure_directory — filesystem ops)[/dim]" ) console.print( - " - class_registry.py (validate_class, get_default_class," - " get_available_classes, get_template_dir — citizen class lookup)" + " [dim]- meta_ops.py" + " (load_template_registry, generate_branch_meta, save_branch_meta — branch metadata)[/dim]" + ) + console.print( + " [dim]- registry.py" + " (find_registry, add_to_registry, get_next_citizen_number — AIPASS_REGISTRY management)[/dim]" + ) + console.print( + " [dim]- class_registry.py (validate_class, get_default_class," + " get_available_classes, get_template_dir — citizen class lookup)[/dim]" ) console.print() diff --git a/src/aipass/spawn/apps/modules/delete.py b/src/aipass/spawn/apps/modules/delete.py index 96dc8fd4..24894531 100644 --- a/src/aipass/spawn/apps/modules/delete.py +++ b/src/aipass/spawn/apps/modules/delete.py @@ -24,12 +24,12 @@ from aipass.spawn.apps.handlers.json import json_handler def print_introspection(): """Display module introspection info.""" console.print() - console.print("delete Module") + console.print("[bold cyan]delete Module[/bold cyan]") console.print("Branch deletion — archive directory and deregister from AIPASS_REGISTRY") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - delete_ops.py (delete_branch — resolve path, archive, remove from registry)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print(" [dim]- delete_ops.py (delete_branch — resolve path, archive, remove from registry)[/dim]") console.print() diff --git a/src/aipass/spawn/apps/modules/passport.py b/src/aipass/spawn/apps/modules/passport.py index c1288cbf..a1bc16e6 100644 --- a/src/aipass/spawn/apps/modules/passport.py +++ b/src/aipass/spawn/apps/modules/passport.py @@ -27,12 +27,12 @@ from aipass.spawn.apps.handlers.json import json_handler def print_introspection(): """Display module introspection info.""" console.print() - console.print("passport Module") + console.print("[bold cyan]passport Module[/bold cyan]") console.print("Grant birthright citizenship — minimal identity (.trinity/, .aipass/, README.md)") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - passport_ops.py (grant_passport — create minimal citizen identity and register)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print(" [dim]- passport_ops.py (grant_passport — create minimal citizen identity and register)[/dim]") console.print() diff --git a/src/aipass/spawn/apps/modules/regenerate_registry.py b/src/aipass/spawn/apps/modules/regenerate_registry.py index e9e31013..91553ca7 100644 --- a/src/aipass/spawn/apps/modules/regenerate_registry.py +++ b/src/aipass/spawn/apps/modules/regenerate_registry.py @@ -23,13 +23,14 @@ from aipass.spawn.apps.handlers.json import json_handler def print_introspection(): """Display module introspection info.""" console.print() - console.print("regenerate_registry Module") + console.print("[bold cyan]regenerate_registry Module[/bold cyan]") console.print("Regenerate .template_registry.json for spawn template directories") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") console.print( - " - regenerate_registry_ops.py (regenerate_template_registry — walk template, hash files, build registry)" + " [dim]- regenerate_registry_ops.py" + " (regenerate_template_registry — walk template, hash files, build registry)[/dim]" ) console.print() diff --git a/src/aipass/spawn/apps/modules/repair.py b/src/aipass/spawn/apps/modules/repair.py index daf4d050..2e776a50 100644 --- a/src/aipass/spawn/apps/modules/repair.py +++ b/src/aipass/spawn/apps/modules/repair.py @@ -29,12 +29,12 @@ from aipass.spawn.apps.handlers.json import json_handler def print_introspection(): """Display module introspection info.""" console.print() - console.print("repair Module") + console.print("[bold cyan]repair Module[/bold cyan]") console.print("Project structure repair — move branches, clean pollution, fix registries") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - repair_ops.py (move_branch, cleanup_pollution, repair_project)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print(" [dim]- repair_ops.py (move_branch, cleanup_pollution, repair_project)[/dim]") console.print() diff --git a/src/aipass/spawn/apps/modules/sync_registry.py b/src/aipass/spawn/apps/modules/sync_registry.py index 0386f3d8..f270863c 100644 --- a/src/aipass/spawn/apps/modules/sync_registry.py +++ b/src/aipass/spawn/apps/modules/sync_registry.py @@ -24,13 +24,14 @@ from aipass.spawn.apps.handlers.json import json_handler def print_introspection(): """Display module introspection info.""" console.print() - console.print("sync_registry Module") + console.print("[bold cyan]sync_registry Module[/bold cyan]") console.print("Registry repair — detect and fix mismatches between AIPASS_REGISTRY and filesystem") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") console.print( - " - sync_registry_ops.py (sync_registry — scan filesystem, detect stale/unregistered, auto-repair)" + " [dim]- sync_registry_ops.py" + " (sync_registry — scan filesystem, detect stale/unregistered, auto-repair)[/dim]" ) console.print() diff --git a/src/aipass/spawn/apps/modules/sync_templates.py b/src/aipass/spawn/apps/modules/sync_templates.py index c60493a4..6b0dd5d4 100644 --- a/src/aipass/spawn/apps/modules/sync_templates.py +++ b/src/aipass/spawn/apps/modules/sync_templates.py @@ -24,12 +24,14 @@ from aipass.spawn.apps.handlers.json import json_handler def print_introspection(): """Display module introspection info.""" console.print() - console.print("sync_templates Module") + console.print("[bold cyan]sync_templates Module[/bold cyan]") console.print("Template synchronization — pull managed files from source branches into templates") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - sync_templates_ops.py (sync_templates — compare hashes and pull updates from source branches)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print( + " [dim]- sync_templates_ops.py (sync_templates — compare hashes and pull updates from source branches)[/dim]" + ) console.print() diff --git a/src/aipass/spawn/apps/modules/update.py b/src/aipass/spawn/apps/modules/update.py index 7e481516..3d72b37e 100644 --- a/src/aipass/spawn/apps/modules/update.py +++ b/src/aipass/spawn/apps/modules/update.py @@ -27,13 +27,14 @@ from aipass.spawn.apps.handlers.json import json_handler def print_introspection(): """Display module introspection info.""" console.print() - console.print("update Module") + console.print("[bold cyan]update Module[/bold cyan]") console.print("Branch updates — sync single or all branches against their class templates") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") console.print( - " - update_ops.py (update_branch, update_all — renames, additions, JSON merges, pruned file archival)" + " [dim]- update_ops.py (update_branch, update_all" + " — renames, additions, JSON merges, pruned file archival)[/dim]" ) console.print() diff --git a/src/aipass/spawn/apps/spawn.py b/src/aipass/spawn/apps/spawn.py index 8b20bd92..741dee7b 100644 --- a/src/aipass/spawn/apps/spawn.py +++ b/src/aipass/spawn/apps/spawn.py @@ -31,21 +31,34 @@ def print_help(): console.print() console.print(" [dim]drone @spawn create [class] [options][/dim]") console.print(" [dim]drone @spawn passport <@dirname> [--role ...] [--purpose ...][/dim]") - console.print(" [dim]drone @spawn update <@branch | class --all> [--dry-run][/dim]") + console.print(" [dim]drone @spawn update <@branch | class --all> [--apply][/dim]") + console.print( + " [dim]drone @spawn repair [--clean-pollution | --relocate @branch ] [--apply][/dim]" + ) console.print(" [dim]drone @spawn --help[/dim]") console.print() console.print("[bold cyan]COMMANDS:[/bold cyan]") console.print() console.print(" [green]create[/green] [class] Create a new branch from template") console.print(" [green]passport[/green] <@dirname> Grant birthright citizenship (minimal)") - console.print(" [green]update[/green] <@branch> Update single branch (preview-only by default)") + console.print( + " [green]update[/green] <@branch> Update single branch from templates (preview-only by default)" + ) console.print(" [green]update[/green] <@branch> --apply Update single branch (execute changes)") console.print(" [green]update[/green] --all --apply Update all branches of a class") console.print(" [green]delete[/green] <@branch> Archive and deregister branch") console.print(" [green]sync-registry[/green] Repair registry against filesystem") console.print(" [green]sync-templates[/green] Pull managed files from source") console.print(" [green]regenerate-registry[/green] Regenerate template registry hashes") - console.print(" [green]repair[/green] Scan and fix project structure") + console.print( + " [green]repair[/green] Scan project structure — paths/registry/pollution (read-only)" + ) + console.print( + " [green]repair[/green] --clean-pollution Archive+remove duplicate dirs (preview; add --apply)" + ) + console.print( + " [green]repair[/green] --relocate @branch Move branch + update registry (preview; add --apply)" + ) console.print() console.print("[bold cyan]CITIZEN CLASSES:[/bold cyan]") console.print() @@ -188,19 +201,19 @@ def _dry_run_create(target_path, citizen_class, parsed): def print_introspection(): """Display module introspection info.""" console.print() - console.print("spawn Entry Point") + console.print("[bold cyan]spawn Entry Point[/bold cyan]") console.print("Branch lifecycle manager — create, update, delete, and sync AIPass branches") console.print() - console.print("Connected Modules:") - console.print(" modules/") - console.print(" - core.py (handle_command, _spawn_agent — agent creation orchestrator)") - console.print(" - update.py (handle_update — single/all branch updates)") - console.print(" - delete.py (handle_delete — archive and deregister branch)") - console.print(" - sync_registry.py (handle_sync_registry — registry repair)") - console.print(" - sync_templates.py (handle_sync_templates — template synchronization)") - console.print(" - regenerate_registry.py (handle_regenerate_registry — regenerate template registry)") - console.print(" - passport.py (handle_passport — grant birthright citizenship)") - console.print(" - repair.py (handle_repair — project structure repair)") + console.print("[yellow]Connected Modules:[/yellow]") + console.print(" [cyan]modules/[/cyan]") + console.print(" [dim]- core.py (handle_command, _spawn_agent — agent creation orchestrator)[/dim]") + console.print(" [dim]- update.py (handle_update — single/all branch updates)[/dim]") + console.print(" [dim]- delete.py (handle_delete — archive and deregister branch)[/dim]") + console.print(" [dim]- sync_registry.py (handle_sync_registry — registry repair)[/dim]") + console.print(" [dim]- sync_templates.py (handle_sync_templates — template synchronization)[/dim]") + console.print(" [dim]- regenerate_registry.py (handle_regenerate_registry — regenerate template registry)[/dim]") + console.print(" [dim]- passport.py (handle_passport — grant birthright citizenship)[/dim]") + console.print(" [dim]- repair.py (handle_repair — project structure repair)[/dim]") console.print() diff --git a/src/aipass/trigger/apps/config.py b/src/aipass/trigger/apps/config.py index c6c6e46d..239e5a3f 100644 --- a/src/aipass/trigger/apps/config.py +++ b/src/aipass/trigger/apps/config.py @@ -120,6 +120,6 @@ def print_introspection(): console = Console() console.print() - console.print("config Module") - console.print("Path constants — TRIGGER_ROOT and AIPASS_PKG_ROOT used by all trigger modules") + console.print("[bold cyan]config Module[/bold cyan]") + console.print("[dim]Path constants — TRIGGER_ROOT and AIPASS_PKG_ROOT used by all trigger modules[/dim]") console.print() diff --git a/src/aipass/trigger/apps/log_watcher_service.py b/src/aipass/trigger/apps/log_watcher_service.py index 6dfc6c9e..9f019f8c 100644 --- a/src/aipass/trigger/apps/log_watcher_service.py +++ b/src/aipass/trigger/apps/log_watcher_service.py @@ -22,6 +22,7 @@ import signal import sys import threading +from aipass.prax.apps.modules.logger import system_logger as logger from aipass.trigger.apps.modules.branch_log_events import ( start as start_branch_watcher, stop as stop_branch_watcher, @@ -34,10 +35,18 @@ from aipass.trigger.apps.modules.log_events import ( def print_introspection(): """Display module introspection info.""" - print() - print("log_watcher_service Module") - print("Persistent log watcher process — starts branch and system watchers as systemd service") - print() + try: + from aipass.cli.apps.modules.display import console + except ImportError: + logger.info("CLI console not available, using rich fallback") + from rich.console import Console + + console = Console() + + console.print() + console.print("[bold cyan]log_watcher_service Module[/bold cyan]") + console.print("[dim]Persistent log watcher process — starts branch and system watchers as systemd service[/dim]") + console.print() def main() -> None: @@ -77,4 +86,7 @@ def main() -> None: if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-V"): + print("log_watcher_service 1.0.0") + sys.exit(0) main() diff --git a/src/aipass/trigger/apps/modules/branch_log_events.py b/src/aipass/trigger/apps/modules/branch_log_events.py index 86527824..6953bb78 100644 --- a/src/aipass/trigger/apps/modules/branch_log_events.py +++ b/src/aipass/trigger/apps/modules/branch_log_events.py @@ -45,17 +45,19 @@ def print_introspection(): console = Console() console.print() - console.print("branch_log_events Module") - console.print("Branch log watcher — watches branch logs for ERROR entries and fires events") + console.print("[bold cyan]branch_log_events Module[/bold cyan]") + console.print("[dim]Branch log watcher — watches branch logs for ERROR entries and fires events[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - log_watcher.py (set_event_callback — set callback for detected events)") - console.print(" - log_watcher.py (start_branch_log_watcher — start filesystem watcher)") - console.print(" - log_watcher.py (stop_branch_log_watcher — stop filesystem watcher)") - console.print(" - log_watcher.py (is_branch_log_watcher_active — check watcher state)") - console.print(" - log_watcher.py (get_watcher_status — get full status dict)") - console.print(" - log_watcher.py (clear_seen_hashes — reset error deduplication)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print( + " [cyan]•[/cyan] log_watcher.py [dim](set_event_callback — set callback for detected events)[/dim]" + ) + console.print(" [cyan]•[/cyan] log_watcher.py [dim](start_branch_log_watcher — start filesystem watcher)[/dim]") + console.print(" [cyan]•[/cyan] log_watcher.py [dim](stop_branch_log_watcher — stop filesystem watcher)[/dim]") + console.print(" [cyan]•[/cyan] log_watcher.py [dim](is_branch_log_watcher_active — check watcher state)[/dim]") + console.print(" [cyan]•[/cyan] log_watcher.py [dim](get_watcher_status — get full status dict)[/dim]") + console.print(" [cyan]•[/cyan] log_watcher.py [dim](clear_seen_hashes — reset error deduplication)[/dim]") console.print() diff --git a/src/aipass/trigger/apps/modules/core.py b/src/aipass/trigger/apps/modules/core.py index e79a7267..9c98fd0d 100644 --- a/src/aipass/trigger/apps/modules/core.py +++ b/src/aipass/trigger/apps/modules/core.py @@ -29,12 +29,14 @@ def print_introspection(): console = Console() console.print() - console.print("core Module") - console.print("Trigger event bus — fire events, register handlers, deferred queue processing") + console.print("[bold cyan]core Module[/bold cyan]") + console.print("[dim]Trigger event bus — fire events, register handlers, deferred queue processing[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/events/") - console.print(" - registry.py (setup_handlers — auto-register all event handlers on first use)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/events/[/cyan]") + console.print( + " [cyan]•[/cyan] registry.py [dim](setup_handlers — auto-register all event handlers on first use)[/dim]" + ) console.print() diff --git a/src/aipass/trigger/apps/modules/errors.py b/src/aipass/trigger/apps/modules/errors.py index de74dc75..fdcb46ad 100644 --- a/src/aipass/trigger/apps/modules/errors.py +++ b/src/aipass/trigger/apps/modules/errors.py @@ -55,22 +55,26 @@ def print_introspection(): console = Console() console.print() - console.print("errors Module") - console.print("Error registry management — view, filter, suppress, and resolve tracked errors") + console.print("[bold cyan]errors Module[/bold cyan]") + console.print("[dim]Error registry management — view, filter, suppress, and resolve tracked errors[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - error_registry.py (query — search/filter error entries)") - console.print(" - error_registry.py (get_entry — get single error by fingerprint)") - console.print(" - error_registry.py (update_status — change error status)") - console.print(" - error_registry.py (clear_resolved — purge old resolved entries)") - console.print(" - error_registry.py (get_stats — summary statistics)") - console.print(" - error_registry.py (get_circuit_breaker_status — circuit breaker state)") - console.print(" - error_registry.py (circuit_breaker_reset — reset circuit breaker)") - console.print(" - error_registry.py (update_source_fix_status — update fix tracking)") - console.print(" - error_registry.py (purge_stale — remove entries older than N days)") - console.print(" - error_reporter.py (report_error — cross-branch push error reporting)") - console.print(" - error_reporter.py (send_source_fix_email — notify branch to fix error)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print(" [cyan]•[/cyan] error_registry.py [dim](query — search/filter error entries)[/dim]") + console.print(" [cyan]•[/cyan] error_registry.py [dim](get_entry — get single error by fingerprint)[/dim]") + console.print(" [cyan]•[/cyan] error_registry.py [dim](update_status — change error status)[/dim]") + console.print(" [cyan]•[/cyan] error_registry.py [dim](clear_resolved — purge old resolved entries)[/dim]") + console.print(" [cyan]•[/cyan] error_registry.py [dim](get_stats — summary statistics)[/dim]") + console.print( + " [cyan]•[/cyan] error_registry.py [dim](get_circuit_breaker_status — circuit breaker state)[/dim]" + ) + console.print(" [cyan]•[/cyan] error_registry.py [dim](circuit_breaker_reset — reset circuit breaker)[/dim]") + console.print(" [cyan]•[/cyan] error_registry.py [dim](update_source_fix_status — update fix tracking)[/dim]") + console.print(" [cyan]•[/cyan] error_registry.py [dim](purge_stale — remove entries older than N days)[/dim]") + console.print(" [cyan]•[/cyan] error_reporter.py [dim](report_error — cross-branch push error reporting)[/dim]") + console.print( + " [cyan]•[/cyan] error_reporter.py [dim](send_source_fix_email — notify branch to fix error)[/dim]" + ) console.print() diff --git a/src/aipass/trigger/apps/modules/log_events.py b/src/aipass/trigger/apps/modules/log_events.py index 25bff445..1a884f00 100644 --- a/src/aipass/trigger/apps/modules/log_events.py +++ b/src/aipass/trigger/apps/modules/log_events.py @@ -42,15 +42,15 @@ def print_introspection(): console = Console() console.print() - console.print("log_events Module") - console.print("Centralized log watcher — watches system_logs/ for error and warning events") + console.print("[bold cyan]log_events Module[/bold cyan]") + console.print("[dim]Centralized log watcher — watches system_logs/ for error and warning events[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/watchers/") - console.print(" - log_watcher.py (start_log_watcher — start centralized log watcher)") - console.print(" - log_watcher.py (stop_log_watcher — stop centralized log watcher)") - console.print(" - log_watcher.py (is_log_watcher_active — check if watcher is running)") - console.print(" - log_watcher.py (SYSTEM_LOGS_DIR — monitored log directory path)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/watchers/[/cyan]") + console.print(" [cyan]•[/cyan] log_watcher.py [dim](start_log_watcher — start centralized log watcher)[/dim]") + console.print(" [cyan]•[/cyan] log_watcher.py [dim](stop_log_watcher — stop centralized log watcher)[/dim]") + console.print(" [cyan]•[/cyan] log_watcher.py [dim](is_log_watcher_active — check if watcher is running)[/dim]") + console.print(" [cyan]•[/cyan] log_watcher.py [dim](SYSTEM_LOGS_DIR — monitored log directory path)[/dim]") console.print() diff --git a/src/aipass/trigger/apps/modules/medic.py b/src/aipass/trigger/apps/modules/medic.py index 15a81b16..34adfa08 100644 --- a/src/aipass/trigger/apps/modules/medic.py +++ b/src/aipass/trigger/apps/modules/medic.py @@ -101,18 +101,18 @@ def print_introspection(): console = Console() console.print() - console.print("medic Module") - console.print("Medic toggle — control auto-healing error dispatch on/off/mute/unmute") + console.print("[bold cyan]medic Module[/bold cyan]") + console.print("[dim]Medic toggle — control auto-healing error dispatch on/off/mute/unmute[/dim]") console.print() - console.print("Connected Handlers:") - console.print(" handlers/") - console.print(" - medic_state.py (is_enabled — check if medic is enabled)") - console.print(" - medic_state.py (set_enabled — toggle medic on/off)") - console.print(" - medic_state.py (get_muted_branches — list muted branches)") - console.print(" - medic_state.py (mute_branch — suppress dispatch for a branch)") - console.print(" - medic_state.py (unmute_branch — resume dispatch for a branch)") - console.print(" - medic_state.py (get_suppression_stats — suppression statistics)") - console.print(" - medic_state.py (get_rate_limit_stats — rate limit statistics)") + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/[/cyan]") + console.print(" [cyan]•[/cyan] medic_state.py [dim](is_enabled — check if medic is enabled)[/dim]") + console.print(" [cyan]•[/cyan] medic_state.py [dim](set_enabled — toggle medic on/off)[/dim]") + console.print(" [cyan]•[/cyan] medic_state.py [dim](get_muted_branches — list muted branches)[/dim]") + console.print(" [cyan]•[/cyan] medic_state.py [dim](mute_branch — suppress dispatch for a branch)[/dim]") + console.print(" [cyan]•[/cyan] medic_state.py [dim](unmute_branch — resume dispatch for a branch)[/dim]") + console.print(" [cyan]•[/cyan] medic_state.py [dim](get_suppression_stats — suppression statistics)[/dim]") + console.print(" [cyan]•[/cyan] medic_state.py [dim](get_rate_limit_stats — rate limit statistics)[/dim]") console.print() diff --git a/src/aipass/trigger/tests/test_log_watcher_service.py b/src/aipass/trigger/tests/test_log_watcher_service.py index 6c79bd99..c4d12f08 100644 --- a/src/aipass/trigger/tests/test_log_watcher_service.py +++ b/src/aipass/trigger/tests/test_log_watcher_service.py @@ -80,7 +80,7 @@ class TestPrintIntrospection: mod = _import_module() mod.print_introspection() captured = capsys.readouterr() - assert "systemd service" in captured.out + assert "systemd" in captured.out and "service" in captured.out # ---------------------------------------------------------------------------