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
# ---------------------------------------------------------------------------