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:
@@ -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
|
||||
|
||||
@@ -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}]},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
# ===========================================================================
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user