feat(standards): enforce Rich introspection formatting + 13 branches to seedgo 100%

- seedgo: new check_introspection_rich_formatting (delegation-aware) + introspection.md richness section + tests
- 13 branches: wrap print_introspection in Rich markup -> all at seedgo 100%
- S202 CLI polish: @hooks --help rewrite (hooksound on/off/status surfaced), spawn repair help clarity, drone Rich colour restore for --help/introspection/status
- flow: playbook plan-type (PPLAN) self-serve templates (default.md, sunday_merge.md SOP) + register-overrides-auto fix + --help rewrite
- hooks: setup.sh installs auto_process bridges + seedgo snapshot fixture learns them (TDPLAN-0005 followup, clears commit-gate blocker)
- remove orphaned devpulse/SETUP.md (vectorized to @memory)
- CHANGELOG [2026.W23]
This commit is contained in:
AIOSAI
2026-06-07 18:51:21 -07:00
parent e80e524dfe
commit f4b203a74e
55 changed files with 1180 additions and 665 deletions
+28
View File
@@ -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 <dir> <PREFIX>` 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" <sop>` 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
+5 -2
View File
@@ -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}]},
],
}
+17 -14
View File
@@ -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()
+11 -11
View File
@@ -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__":
@@ -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
-225
View File
@@ -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/<branch>/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
+4 -4
View File
@@ -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()
+10 -9
View File
@@ -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":
+6
View File
@@ -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": {
+10 -1
View File
@@ -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)
+11 -19
View File
@@ -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]
+8 -6
View File
@@ -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()
+13 -7
View File
@@ -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()
+17 -11
View File
@@ -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()
+27 -1
View File
@@ -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:
@@ -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()
+13 -7
View File
@@ -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()
+14 -8
View File
@@ -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()
+11 -9
View File
@@ -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()
+9 -9
View File
@@ -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()
+60 -8
View File
@@ -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
@@ -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.
+31 -5
View File
@@ -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 <location> "Subject" [type]')
console.print(' drone @flow create <location> "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/<dirname>/ with .md template files")
console.print(" 2. drone @flow register <dirname> <PREFIX>")
console.print(' 3. drone @flow create . "Subject" <shorthand>')
console.print(" [dim]See: drone @flow templates --help[/dim]")
console.print()
# =============================================
# ORCHESTRATION WORKFLOWS (thin wrappers)
@@ -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 <dir> 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")
@@ -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/<sop_name>.md`, then
`drone @flow create . "Subject" <sop_name>`. 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}
```
@@ -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 <repo-root> && .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 <path>` 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/<sha>/check-runs` → all check-runs success (incl. app checks: codecov, CodeQL)
- `curl -s https://api.github.com/repos/AIOSAI/AIPass/pulls/<n>` → `mergeable_state: clean`
## 5. Merge to main
- [ ] **User's call to merge** — confirm GO
- [ ] `drone @git merge <PR#>` (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<version> <main-sha>` + `git push origin v<version>` 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}
```
@@ -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
# ===================================================================
@@ -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
# =============================================================================
+68 -15
View File
@@ -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 <command> [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 <command> [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 <command> --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:
+4
View File
@@ -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."""
@@ -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."""
@@ -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",
+40 -1
View File
@@ -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 <command>" 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
+26 -22
View File
@@ -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()
@@ -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:
@@ -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"
),
}
@@ -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
}
]
}
]
}
@@ -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
# ===========================================================================
+17 -11
View File
@@ -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()
+4 -4
View File
@@ -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()
+4 -4
View File
@@ -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()
@@ -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()
+4 -4
View File
@@ -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()
@@ -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()
@@ -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()
+5 -4
View File
@@ -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()
+27 -14
View File
@@ -31,21 +31,34 @@ def print_help():
console.print()
console.print(" [dim]drone @spawn create [class] <target_path> [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 <project_path> [--clean-pollution | --relocate @branch <path>] [--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] <path> 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] <class> --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] <project_path> Scan and fix project structure")
console.print(
" [green]repair[/green] <project_path> Scan project structure — paths/registry/pollution (read-only)"
)
console.print(
" [green]repair[/green] <path> --clean-pollution Archive+remove duplicate dirs (preview; add --apply)"
)
console.print(
" [green]repair[/green] --relocate @branch <path> 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()
+2 -2
View File
@@ -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()
+16 -4
View File
@@ -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()
@@ -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()
+7 -5
View File
@@ -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()
+19 -15
View File
@@ -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()
@@ -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()
+11 -11
View File
@@ -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()
@@ -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
# ---------------------------------------------------------------------------