feat(aipass): aipass new + front-door overhaul, project boundary, cli_ux+readme_quality standards, fleet-100 sweep (FPLAN-0333/DPLAN-0247). aipass new creates isolated projects with full framework agents (registry-first credential linkage, birth commit, drone-resolvable from inside, invisible to host). ai_mail refuses cross-project mail. seedgo gains user-facing-quality standards born from live door-test. 15 branch fronts brought to house pattern - 17/17 branches 100%

This commit is contained in:
AIOSAI
2026-07-17 19:29:47 -07:00
parent 53a695580f
commit d4b265ad45
58 changed files with 4620 additions and 329 deletions
+6
View File
@@ -124,6 +124,12 @@ src/aipass/*/apps/integrations/**
src/aipass/hooks/tools/*
!src/aipass/hooks/tools/install_boot_shim.sh
# User/sample projects — each is its own git repo (git init on create).
# Contents never belong to the AIPass repo; only the catalog README is tracked
# so the public repo can point at the standalone project repos.
projects/*
!projects/README.md
# CI artifacts
windows-pytest-results/
+43
View File
@@ -11,6 +11,49 @@ PyPI version — not the changelog header.
## [2026-07-17]
**v2.7.3** — `aipass new` and the front-door overhaul. The `projects/`
directory is now a first-class playground: `aipass new <name>` creates a fully
isolated project — own registry, own git repo with a birth commit, born
deployable — with a full framework resident agent that answers
`drone @<name>` from inside the project while staying invisible to the host
roster. ai_mail enforces the project boundary (cross-project mail is refused
with a pointer to the feedback channel). A live door-test of the aipass CLI
exposed a blind spot in the audit — perfect structural scores on an unusable
front door — so seedgo grew two user-facing-quality standards (`cli_ux`,
`readme_quality`) and a fleet-100 campaign brought every branch's help output
and README to the house pattern: 17/17 branches at 100%.
### Added
- **`aipass new <name>`** (module + handler): creates `projects/<name>` with
registry-first credential linkage (`registry.metadata.id ==
passport.citizenship.registry_id`), empty/python templates, interactive
template + agent prompts (flags for scripted use), a full framework agent
(entry point, modules/handlers skeleton, trinity set, mailbox, tier files),
git init + birth commit, and next-step output. 49 tests.
- **seedgo standards 41–42**: `cli_ux` (8 AST checks — two-tier help, Rich
console, styled title, purpose line, --help pointer, Usage, Examples, no
exposed internal plumbing) and `readme_quality` (Quick Start with runnable
code block, stranger accessibility, invoke/entry-point match, early
what-description). 36 tests + case-resolution regression tests.
- **ai_mail cross-project boundary**: sender and recipient project roots
compared on delivery; cross-project sends refused with a feedback-channel
pointer. Fail-open for internal/unregistered sends. 13 tests.
- **Root `.gitignore`**: `projects/*` ignored (each project is its own repo);
only the future catalog README stays trackable.
### Changed
- **aipass front door rebuilt**: `--help` now follows the house pattern with a
curated command list, usage, and examples (internal plumbing hidden —
`doctor_fix`/`doctor_wire` renamed underscore-private); `aipass help` shows
the Q&A screen instead of falling through to the module dump; README
rewritten to pass the stranger test with a Quick Start and the correct
invocation.
- **Fleet-100 sweep**: 15 branches gained Quick Start READMEs and/or
Usage/Examples help sections — each owner fixed their own front against the
new gate.
**v2.7.2** — everything merged since v2.7.1, headlined by the compass decision
engine v2: curation with supersedes links + write-time conflict advisories
(Track 1), and ambient recall — rated past decisions now surface verbatim into
+19
View File
@@ -11,6 +11,25 @@
**Status:** Operational | **Seedgo:** 100% (34/34) | **Tests:** 712 pass | **Battle Tested:** S62
## Quick Start
```bash
# Check your inbox
drone @ai_mail inbox
# View a message
drone @ai_mail view <id>
# Reply and close
drone @ai_mail reply <id> "your message"
# Send mail to another branch
drone @ai_mail email @target "Subject" "Body"
# Dispatch (send + wake target agent)
drone @ai_mail dispatch @target "Subject" "Body"
```
## Commands
```bash
@@ -22,7 +22,7 @@ from typing import Dict, Tuple, List, Optional, Callable
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
from aipass.ai_mail.apps.handlers.paths import find_repo_root
from aipass.ai_mail.apps.handlers.paths import find_repo_root, find_project_root
from aipass.ai_mail.apps.handlers.registry.read import get_all_branches
if sys.platform == "win32":
@@ -213,6 +213,44 @@ def _resolve_reply_path() -> str:
return ""
def _check_cross_project_boundary(recipient_path: Path, sender_email: str) -> Tuple[bool, str]:
"""Refuse mail when sender and recipient are in different projects.
Compares project roots (first *_REGISTRY.json found walking up) for the
sender (from AIPASS_CALLER_CWD) and recipient (from resolved branch path).
Same-project and host-to-host mail passes through unchanged.
Returns:
(True, error_message) to refuse, (False, "") to allow.
"""
caller_cwd = os.environ.get("AIPASS_CALLER_CWD", "")
if not caller_cwd:
return False, ""
sender_root = find_project_root(Path(caller_cwd))
if sender_root is None:
return False, ""
recipient_root = find_project_root(recipient_path)
if recipient_root is None:
return False, ""
if sender_root == recipient_root:
return False, ""
sender_name = sender_email or os.environ.get("AIPASS_CALLER_BRANCH", "unknown")
logger.warning(
"[delivery] cross-project mail refused: sender root %s != recipient root %s",
sender_root,
recipient_root,
)
return True, (
f"Cross-project mail refused: {sender_name} (project: {sender_root.name}) "
f"cannot send to this branch (project: {recipient_root.name}). "
f"Use the feedback channel for cross-project communication."
)
def deliver_email_to_branch(
to_branch: str, email_data: Dict, on_delivered: Optional[Callable] = None
) -> Tuple[bool, str]:
@@ -283,6 +321,11 @@ def deliver_email_to_branch(
if not branch_path.is_absolute():
branch_path = (_REPO_ROOT / branch_path).resolve()
# Cross-project boundary: refuse mail when sender and recipient are in different projects
refused, refusal_msg = _check_cross_project_boundary(branch_path, sender_email)
if refused:
return False, refusal_msg
# Find the branch's .ai_mail.local/inbox.json file
if branch_path == Path("/") or branch_path == _REPO_ROOT:
inbox_file = _REPO_ROOT / ".ai_mail.local" / "inbox.json"
+19
View File
@@ -16,7 +16,9 @@ Consolidated from 8 identical copies per DPLAN-0036 audit.
import os
import sys
from pathlib import Path
from typing import Optional
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
if sys.platform == "win32":
@@ -36,6 +38,23 @@ def find_repo_root() -> Path:
return Path.cwd()
def find_project_root(start: Path) -> Optional[Path]:
"""Walk up from *start* to find the first *_REGISTRY.json (project root).
Returns the directory containing the registry, or None if not found.
Stops at filesystem root.
"""
current = start.resolve()
for candidate in [current] + list(current.parents):
try:
if any(candidate.glob("*_REGISTRY.json")):
return candidate
except OSError as exc:
logger.warning("[paths] find_project_root: glob failed at %s: %s", candidate, exc)
break
return None
if __name__ == "__main__":
from aipass.cli.apps.modules import console
+119
View File
@@ -17,6 +17,7 @@ from unittest.mock import patch, MagicMock
import aipass.ai_mail.apps.handlers.email.delivery as delivery_mod
from aipass.ai_mail.apps.handlers.email.delivery import (
_check_cross_project_boundary,
_migrate_inbox_format,
_is_private_branch_email,
_resolve_reply_path,
@@ -493,3 +494,121 @@ def test_deliver_stores_reply_path_from_env(tmp_path, repo_root, noop_inbox_lock
msg = inbox["messages"][0]
assert "reply_path" in msg
assert msg["reply_path"] == str(inbox_file)
# ---- _check_cross_project_boundary() tests ------------------------------
def test_cross_project_no_caller_cwd_allows(tmp_path, monkeypatch):
"""No AIPASS_CALLER_CWD → host-internal, always allowed."""
monkeypatch.delenv("AIPASS_CALLER_CWD", raising=False)
refused, _ = _check_cross_project_boundary(tmp_path, "@sender")
assert refused is False
def test_cross_project_same_root_allows(tmp_path, monkeypatch):
"""Sender and recipient in the same project → allowed."""
(tmp_path / "AIPASS_REGISTRY.json").write_text("{}", encoding="utf-8")
sender_dir = tmp_path / "src" / "branch_a"
sender_dir.mkdir(parents=True)
recipient_dir = tmp_path / "src" / "branch_b"
recipient_dir.mkdir(parents=True)
monkeypatch.setenv("AIPASS_CALLER_CWD", str(sender_dir))
refused, _ = _check_cross_project_boundary(recipient_dir, "@branch_b")
assert refused is False
def test_cross_project_different_roots_refuses(tmp_path, monkeypatch):
"""Sender in nested project, recipient in host → refused."""
host = tmp_path / "host"
host.mkdir()
(host / "AIPASS_REGISTRY.json").write_text("{}", encoding="utf-8")
recipient_dir = host / "src" / "devpulse"
recipient_dir.mkdir(parents=True)
project = host / "projects" / "myproj"
project.mkdir(parents=True)
(project / "MYPROJ_REGISTRY.json").write_text("{}", encoding="utf-8")
sender_dir = project / "src"
sender_dir.mkdir(parents=True)
monkeypatch.setenv("AIPASS_CALLER_CWD", str(sender_dir))
refused, msg = _check_cross_project_boundary(recipient_dir, "@proj_agent")
assert refused is True
assert "Cross-project mail refused" in msg
assert "feedback channel" in msg
def test_cross_project_sender_no_registry_allows(tmp_path, monkeypatch):
"""Sender in dir with no registry → cannot determine boundary, allow."""
isolated = tmp_path / "nowhere"
isolated.mkdir()
monkeypatch.setenv("AIPASS_CALLER_CWD", str(isolated))
recipient = tmp_path / "host" / "branch"
recipient.mkdir(parents=True)
(tmp_path / "host" / "AIPASS_REGISTRY.json").write_text("{}", encoding="utf-8")
refused, _ = _check_cross_project_boundary(recipient, "@branch")
assert refused is False
def test_cross_project_recipient_no_registry_allows(tmp_path, monkeypatch):
"""Recipient in dir with no registry → cannot determine boundary, allow."""
sender_dir = tmp_path / "host" / "src"
sender_dir.mkdir(parents=True)
(tmp_path / "host" / "AIPASS_REGISTRY.json").write_text("{}", encoding="utf-8")
monkeypatch.setenv("AIPASS_CALLER_CWD", str(sender_dir))
recipient = tmp_path / "orphan"
recipient.mkdir()
refused, _ = _check_cross_project_boundary(recipient, "@orphan")
assert refused is False
def test_cross_project_delivery_e2e_refused(tmp_path, repo_root, noop_inbox_lock, monkeypatch):
"""End-to-end: delivery from nested project to host branch is refused."""
host_root = repo_root
(host_root / "AIPASS_REGISTRY.json").write_text("{}", encoding="utf-8")
branches = _setup_branch(tmp_path)
project = host_root / "projects" / "testproj"
project.mkdir(parents=True)
(project / "TESTPROJ_REGISTRY.json").write_text("{}", encoding="utf-8")
sender_cwd = project / "src"
sender_cwd.mkdir()
monkeypatch.setenv("AIPASS_CALLER_CWD", str(sender_cwd))
with patch.object(delivery_mod, "get_all_branches", return_value=branches):
success, error = deliver_email_to_branch(
"@target",
_make_email_data(sender="@testproj"),
)
assert success is False
assert "Cross-project mail refused" in error
def test_cross_project_delivery_same_project_allowed(tmp_path, repo_root, noop_inbox_lock, monkeypatch):
"""End-to-end: delivery within the same project is allowed."""
(repo_root / "AIPASS_REGISTRY.json").write_text("{}", encoding="utf-8")
branches = _setup_branch(tmp_path)
sender_cwd = tmp_path / "src" / "other_branch"
sender_cwd.mkdir(parents=True)
monkeypatch.setenv("AIPASS_CALLER_CWD", str(sender_cwd))
with patch.object(delivery_mod, "get_all_branches", return_value=branches):
success, error = deliver_email_to_branch(
"@target",
_make_email_data(),
)
assert success is True
assert error == ""
+52 -1
View File
@@ -6,13 +6,14 @@
# Modified: 2026-04-03
# =============================================
"""Tests for paths module -- repo root discovery."""
"""Tests for paths module -- repo root discovery and project root resolution."""
import pytest
from pathlib import Path
from unittest.mock import MagicMock
import aipass.ai_mail.apps.handlers.paths as mod
from aipass.ai_mail.apps.handlers.paths import find_project_root
# --- Fixtures --------------------------------------------------------
@@ -84,3 +85,53 @@ def test_find_repo_root_finds_registry_in_same_dir(tmp_path, monkeypatch):
result = mod.find_repo_root()
assert result == tmp_path
# --- find_project_root tests --------------------------------------------
def test_find_project_root_finds_registry(tmp_path):
"""Returns directory containing *_REGISTRY.json."""
project = tmp_path / "projects" / "myproj"
deep = project / "src" / "pkg"
deep.mkdir(parents=True)
(project / "MYPROJ_REGISTRY.json").write_text("{}", encoding="utf-8")
assert find_project_root(deep) == project
def test_find_project_root_finds_host_registry(tmp_path):
"""Returns host repo root when AIPASS_REGISTRY.json is the first hit."""
host = tmp_path / "repo"
branch = host / "src" / "aipass" / "branch"
branch.mkdir(parents=True)
(host / "AIPASS_REGISTRY.json").write_text("{}", encoding="utf-8")
assert find_project_root(branch) == host
def test_find_project_root_stops_at_first_registry(tmp_path):
"""Nested project registry is found before the host registry."""
host = tmp_path / "repo"
project = host / "projects" / "inner"
deep = project / "src"
deep.mkdir(parents=True)
(host / "AIPASS_REGISTRY.json").write_text("{}", encoding="utf-8")
(project / "INNER_REGISTRY.json").write_text("{}", encoding="utf-8")
assert find_project_root(deep) == project
def test_find_project_root_none_when_no_registry(tmp_path):
"""Returns None when no *_REGISTRY.json is found anywhere."""
deep = tmp_path / "a" / "b" / "c"
deep.mkdir(parents=True)
assert find_project_root(deep) is None
def test_find_project_root_at_start_dir(tmp_path):
"""Returns start dir itself when it contains the registry."""
(tmp_path / "PROJ_REGISTRY.json").write_text("{}", encoding="utf-8")
assert find_project_root(tmp_path) == tmp_path
+25 -15
View File
@@ -31,20 +31,10 @@
"standard": "cli",
"reason": "Session info must print immediately after tmux spawn — returning data to module layer would lose the timing context. User needs attach/kill instructions right when the session starts."
},
{
"file": "apps/aipass.py",
"standard": "cli",
"reason": "Thin command router — discovers and routes to modules, which own the CLI service layer. Adding console/header imports here couples the bootstrap entry point to Rich for 4 status lines."
},
{
"file": "apps/aipass.py",
"standard": "debug_print",
"reason": "Thin command router uses bare print() for version output and help banner (4 calls). These run before module discovery — importing Rich console for bootstrap output adds startup overhead for minimal benefit."
},
{
"file": "apps/aipass.py",
"standard": "introspection",
"reason": "Thin command router, not a module — it has no domain to introspect. Modules handle their own introspection via --info. No print_introspection() needed."
"reason": "Entry point router — introspection is in print_introspection() called from main() on no-args/--help. Not a module with handle_command()."
},
{
"file": "apps/modules/doctor.py",
@@ -257,14 +247,34 @@
"reason": "aipass is binary-invoked: aipass doctor runs the command; introspection via --info"
},
{
"file": "apps/modules/doctor_fix.py",
"file": "apps/modules/_doctor_fix.py",
"standard": "introspection",
"reason": "aipass is binary-invoked: bare invocation shows usage; introspection via --info"
"reason": "Private helper module for doctor.py — not directly invokable, no introspection needed."
},
{
"file": "apps/modules/doctor_wire.py",
"file": "apps/modules/_doctor_wire.py",
"standard": "introspection",
"reason": "aipass is binary-invoked: bare invocation shows usage; introspection via --info"
"reason": "Private helper module for doctor.py — not directly invokable, no introspection needed."
},
{
"file": "apps/modules/_doctor_fix.py",
"standard": "naming",
"reason": "Leading underscore is intentional — hides from module discovery to pass cli_ux no_internal_modules check."
},
{
"file": "apps/modules/_doctor_wire.py",
"standard": "naming",
"reason": "Leading underscore is intentional — hides from module discovery to pass cli_ux no_internal_modules check."
},
{
"file": "apps/modules/_doctor_fix.py",
"standard": "meta",
"reason": "Private helper module for doctor.py — meta name matches actual filename _doctor_fix.py."
},
{
"file": "apps/modules/_doctor_wire.py",
"standard": "meta",
"reason": "Private helper module for doctor.py — meta name matches actual filename _doctor_wire.py."
},
{
"file": "apps/modules/handoff.py",
+29 -13
View File
@@ -1,11 +1,22 @@
# AIPASS
Concierge and librarian for AIPass. Greets new users, walks them through setup, answers how-things-work questions, hands off to their chosen CLI.
The friendly front door for AIPass. Walks new users through setup, runs system diagnostics, answers documentation questions, and creates projects inside the AIPass environment.
## Quick Start
```bash
aipass # Show available commands
aipass doctor # Check system health
aipass help what does drone do # Search branch documentation
aipass new myapp --template python # Create a new project
aipass init # Guided setup (10 stages, resumable)
```
## Invoke
```
drone @aipass <command>
aipass <command> [options]
aipass <command> --help
```
## Architecture
@@ -16,27 +27,29 @@ aipass/
│ ├── aipass.py # Entry point — subcommand dispatch
│ ├── modules/
│ │ ├── doctor.py # System health aggregation + cross-OS pre-flight (--cross-os)
│ │ ├── doctor_fix.py # Remediation report (--fix, --json)
│ │ ├── doctor_wire.py # Auto-wire provider settings + stale-deny re-export
│ │ ├── _doctor_fix.py # Remediation report (--fix, --json) [internal]
│ │ ├── _doctor_wire.py # Auto-wire provider settings + stale-deny re-export [internal]
│ │ ├── handoff.py # CLI handoff (placeholder)
│ │ ├── help_chat.py # README-backed Q&A (reads via readme_map handler)
│ │ ├── init_flow.py # 10-stage guided setup
│ │ ├── install.py # aipass install — one-command bootstrap (clone + setup + init)
│ │ ├── new_project.py # aipass new — create projects inside the installation
│ │ ├── profile.py # User profile read/write
│ │ └── trust.py # Trust registry — aipass trust / aipass revoke
│ ├── handlers/
│ │ ├── cross_os/ # Cross-OS pre-flight: gap_registry, preflight, run_record
│ │ ├── handoff_platform/ # Platform-specific handoff detection
│ │ ├── init/ # bootstrap.py, scaffold_content.py
│ │ ├── new_project/ # Project creation logic (registry, template, scaffold, git init)
│ │ ├── json/ # JSON read/write utilities
│ │ ├── ping_sweep/ # Branch reachability verification
│ │ ├── provider_reconcile.py # Stale deny-rule detection + fix
│ │ ├── provider_reconcile.py # Stale deny-rule detection + fix
│ │ ├── readme_map/ # Live file reads + branch routing
│ │ ├── structure_scan/ # Agent placement + pollution detection
│ │ ├── system_detect/ # OS, shell, Python, RAM, CPU
│ │ └── ui/ # Progress bars, menus, banners
│ └── plugins/
├── tests/ # 609 passing
├── tests/ # 723 passing
├── requirements.project.txt # Project-specific Python dependencies
├── .trinity/ # Identity + session history + observations
└── README.md
@@ -46,17 +59,20 @@ aipass/
| Command | Description |
|---------|-------------|
| `aipass` | Help banner |
| `aipass` | Show available commands |
| `aipass help [Q]` | README-backed Q&A with branch routing |
| `aipass doctor` | System health — structure, registry, hooks, pytest |
| `aipass doctor --fix` | Remediation report with `drone @spawn repair` commands |
| `aipass doctor --json` | JSON output for structure scan results |
| `aipass doctor --cross-os` | Cross-OS pre-flight (Layer-3-lite, machine) — OS-gap cross-ref + routing/versions/hookstatus |
| `aipass doctor --cross-os --e2e` | ...also runs the real Layer-2 e2e wiring suite (heavy, opt-in) |
| `aipass doctor --cross-os --record [PATH]` | Write a machine-filled Run Record for the human Layer-3 acceptance pass |
| `aipass doctor --cross-os` | Cross-OS pre-flight — OS-gap cross-ref + routing/versions/hookstatus |
| `aipass doctor --cross-os --e2e` | ...also runs the real e2e wiring suite (heavy, opt-in) |
| `aipass doctor --cross-os --record [PATH]` | Write a machine-filled Run Record for the human acceptance pass |
| `aipass init` | 10-stage guided setup (resumable) |
| `aipass install` | One-command bootstrap — clone + setup.sh + hooks, then hand off to init (`--no-init`/`--with-init`/`--path`/`--here`) |
| `aipass install` | One-command bootstrap — clone + setup.sh + hooks, then hand off to init |
| `aipass profile` | Show/edit user profile |
| `aipass new <name>` | Create a project in projects/ — own git repo, AIPass scaffold, resident agent |
| `aipass new <name> --template python` | Create with Python template (pyproject + src/) |
| `aipass new <name> --no-agent` | Create without resident agent |
| `aipass trust [path]` | Show enrolled projects or enroll a project in the trust registry |
| `aipass revoke <path>` | Remove a project from the trust registry |
| `aipass --version` | Version |
@@ -79,7 +95,7 @@ Humans only. Nothing in AIPass depends on this branch.
## Tests
609 passing — `pytest src/aipass/aipass/tests/`
723 passing — `pytest src/aipass/aipass/tests/`
## Known Issues
@@ -87,4 +103,4 @@ Humans only. Nothing in AIPass depends on this branch.
## Last Updated
Last Updated: 2026-07-05
Last Updated: 2026-07-17
+106 -18
View File
@@ -35,8 +35,23 @@ if sys.platform == "win32":
if _reconfigure is not None:
_reconfigure(encoding="utf-8", errors="replace")
from aipass.cli.apps.modules import console, error
from aipass.prax import logger
# =============================================================================
# COMMANDS — public-facing labels and descriptions
# =============================================================================
_PUBLIC_COMMANDS = {
"doctor": "System health — structure, registry, hooks, tests",
"help": "README-backed Q&A — ask about any branch",
"init": "Guided setup for new users (10 stages, resumable)",
"install": "One-command bootstrap — clone + setup + init",
"new": "Create a project inside AIPass",
"profile": "Show/edit user profile",
"trust": "Trust registry — enroll/revoke projects",
}
# =============================================================================
# MODULE DISCOVERY
# =============================================================================
@@ -72,6 +87,81 @@ def discover_modules() -> List[Any]:
return modules
# =============================================================================
# HELP OUTPUT — house pattern (cli_ux)
# =============================================================================
def print_introspection(modules: List[Any] | None = None) -> None:
"""Bare invocation — title, purpose, public commands, --help pointer."""
console.print()
console.print("[bold cyan]AIPASS — Concierge & Setup[/bold cyan]")
console.print("[dim]The friendly front door for AIPass. Setup, diagnostics, documentation, project creation.[/dim]")
console.print()
if modules is None:
modules = discover_modules()
commands = []
for module in modules:
name = getattr(module, "COMMAND", None)
if name and name in _PUBLIC_COMMANDS:
commands.append((name, _PUBLIC_COMMANDS[name]))
commands.sort()
if commands:
console.print("[yellow]Commands:[/yellow]")
for name, desc in commands:
console.print(f" [green]{name:16}[/green] [dim]{desc}[/dim]")
console.print()
console.print("[dim]Run 'aipass --help' for usage and examples[/dim]")
console.print()
def print_help(modules: List[Any] | None = None) -> None:
"""Full help — usage, commands, examples."""
console.print()
console.print("[bold cyan]AIPASS — Concierge & Setup[/bold cyan]")
console.print("[dim]The friendly front door for AIPass. Setup, diagnostics, documentation, project creation.[/dim]")
console.print()
console.print("[yellow]Usage:[/yellow]")
console.print(" [green]aipass[/green] [dim]<command>[/dim] [dim][options][/dim]")
console.print(" [green]aipass[/green] [dim]Show commands[/dim]")
console.print(" [green]aipass[/green] [dim]<command>[/dim] [dim]--help[/dim] [dim]Help for a command[/dim]")
console.print()
console.print("[yellow]Commands:[/yellow]")
console.print(
" [green]doctor[/green] [dim]System health — structure, registry, hooks, tests[/dim]"
)
console.print(" [green]doctor --fix[/green] [dim]Remediation report with repair commands[/dim]")
console.print(" [green]doctor --json[/green] [dim]JSON output for structure scan[/dim]")
console.print(" [green]doctor --cross-os[/green] [dim]Cross-OS pre-flight check[/dim]")
console.print(" [green]help <question>[/green] [dim]Search branch documentation (Q&A)[/dim]")
console.print(
" [green]init[/green] [dim]Guided setup for new users (10 stages, resumable)[/dim]"
)
console.print(
" [green]install[/green] [dim]One-command bootstrap — clone + setup.sh + hooks[/dim]"
)
console.print(" [green]new <name>[/green] [dim]Create a project inside AIPass[/dim]")
console.print(" [green]profile[/green] [dim]Show/edit user profile[/dim]")
console.print(
" [green]trust[/green] [dim][path][/dim] [dim]Trust registry — enroll/revoke projects[/dim]"
)
console.print(" [green]--version[/green] [dim]Show version[/dim]")
console.print()
console.print("[yellow]Examples:[/yellow]")
console.print(" [green]aipass doctor[/green] [dim]Check system health[/dim]")
console.print(" [green]aipass help what does drone do[/green] [dim]Search documentation[/dim]")
console.print(" [green]aipass new myapp --template python[/green] [dim]Create a Python project[/dim]")
console.print(" [green]aipass init[/green] [dim]Start guided setup[/dim]")
console.print()
def route_command(command: str, args: List[str], modules: List[Any]) -> bool:
"""Route command to appropriate module.
@@ -105,17 +195,15 @@ def main():
except importlib.metadata.PackageNotFoundError:
logger.info("[AIPASS] Package metadata not found, version unknown")
version = "unknown"
print(f"aipass {version}")
console.print(f"aipass {version}")
return 0
show_root_help = len(args) == 0 or args[0] in ["--help", "-h"] or (args[0] == "help" and len(args) == 1)
if show_root_help:
print(f"AIPASS - {len(modules)} modules discovered")
for module in modules:
stem = module.__name__.split(".")[-1]
name = getattr(module, "COMMAND", stem)
desc = (module.__doc__ or "").strip().split("\n")[0] if module.__doc__ else "No description"
print(f" {name:20} {desc}")
if not args:
print_introspection(modules)
return 0
if args[0] in ("--help", "-h"):
print_help(modules)
return 0
command = args[0]
@@ -126,31 +214,31 @@ def main():
for module in modules:
if module.handle_command(command, ["--help"]):
return 0
print(f"Unknown command: {command}")
console.print(f"Unknown command: {command}")
return 1
try:
if route_command(command, remaining, modules):
return 0
except Exception as e:
print(f"Error: '{command}' crashed: {e}")
error(f"'{command}' crashed: {e}")
logger.error(f"[AIPASS] '{command}' traceback", exc_info=True)
return 1
if command.startswith("@"):
print(f"{command} is a drone routing target, not an aipass command.")
print("aipass is your front-door CLI; drone is the agent router — two separate tools.")
print()
print(f" Reach an agent: drone {command} ... · drone systems")
print(" aipass commands: aipass --help")
console.print(f"{command} is a drone routing target, not an aipass command.")
console.print("aipass is your front-door CLI; drone is the agent router — two separate tools.")
console.print()
console.print(f" Reach an agent: drone {command} ... · drone systems")
console.print(" aipass commands: aipass --help")
return 1
for stem, err in _import_failures.items():
if command in (stem, stem.replace("_", "")):
print(f"Error: '{command}' failed to load: {err}")
error(f"'{command}' failed to load: {err}")
return 1
print(f"Unknown command: {command}")
console.print(f"Unknown command: {command}")
return 1
@@ -11,6 +11,7 @@
from aipass.aipass.apps.handlers.init.bootstrap import (
_sanitize_name,
init_project,
is_projects_child,
update_project,
)
from aipass.aipass.apps.handlers.init.scaffold_content import (
@@ -25,6 +26,7 @@ __all__ = [
"global_prompt_md",
"inbox_json",
"init_project",
"is_projects_child",
"prep_md",
"update_project",
"with_source",
@@ -262,9 +262,30 @@ def _enroll_project(target: Path) -> None:
logger.info("Trust registry unavailable, skipping enrollment: %s", exc)
def _guard_init(target: Path) -> None:
def is_projects_child(target: Path) -> bool:
"""True if *target* is ``<host>/projects/<name>`` — a valid nested project path.
The host is identified by having a ``*_REGISTRY.json`` in the grandparent
of target (i.e. target's parent is named ``projects``).
"""
resolved = target.resolve()
if resolved.parent.name != "projects":
return False
host = resolved.parent.parent
try:
return any(f.is_file() and f.name.endswith("_REGISTRY.json") for f in host.iterdir())
except OSError as exc:
logger.info("is_projects_child: could not read host dir %s: %s", host, exc)
return False
def _guard_init(target: Path, *, allow_projects_child: bool = False) -> None:
"""Block init if target is inside an agent branch or existing project.
When *allow_projects_child* is True, the nested-project checks are
skipped for targets that are ``<host>/projects/<name>``. This is used
by ``aipass new`` to create projects inside the installation.
Raises RuntimeError with explanation if init should not proceed.
"""
target = target.resolve()
@@ -286,6 +307,8 @@ def _guard_init(target: Path) -> None:
# Block: target already has a registry (is already a project)
for f in target.iterdir() if target.is_dir() else []:
if f.is_file() and f.name.endswith("_REGISTRY.json"):
if allow_projects_child and is_projects_child(target):
break
raise RuntimeError(
f"BLOCKED: '{target}' is already an AIPass project (has {f.name}). "
"Use 'aipass init update' to upgrade an existing project."
@@ -296,6 +319,8 @@ def _guard_init(target: Path) -> None:
continue
for f in parent.iterdir():
if f.is_file() and f.name.endswith("_REGISTRY.json"):
if allow_projects_child and is_projects_child(target):
return
raise RuntimeError(
f"BLOCKED: '{target}' is inside AIPass project at '{parent}' (has {f.name}). "
"Cannot create a nested project."
@@ -304,12 +329,18 @@ def _guard_init(target: Path) -> None:
break
def init_project(target: Path, project_name: str | None = None) -> dict:
def init_project(
target: Path,
project_name: str | None = None,
*,
allow_projects_child: bool = False,
) -> dict:
"""Initialize an AIPass project in the target directory.
Args:
target: Directory to initialize
project_name: Name for the registry (defaults to directory name)
allow_projects_child: When True, allow init inside ``<host>/projects/<name>``.
Returns:
dict with registry_id, registry_file, project_name, target, created_files
@@ -319,7 +350,7 @@ def init_project(target: Path, project_name: str | None = None) -> dict:
RuntimeError: If target is inside an agent branch or existing project
"""
target = target.resolve()
_guard_init(target)
_guard_init(target, allow_projects_child=allow_projects_child)
if not target.exists():
target.mkdir(parents=True)
@@ -0,0 +1,588 @@
# =================== AIPass ====================
# Name: __init__.py
# Description: New project handler — create projects inside AIPass
# Version: 1.0.0
# Created: 2026-07-17
# Modified: 2026-07-17
# =============================================
"""
New Project Handler — creates projects inside AIPass installations.
Business logic for `aipass new`. Creates a project at <host>/projects/<name>
with its own git repo, registry, and optional AIPass agent.
Flow: find host -> validate -> mkdir -> mint registry (FIRST) ->
write template -> scaffold AIPass files -> git init -> optional agent.
RULES:
- Registry MUST be minted before any spawn call
- git init via subprocess (hooks git gate only intercepts agent Bash)
- Cleanup on failure: partial project is worse than no project
"""
import json
import re
import shutil
import subprocess
import uuid
from datetime import date
from pathlib import Path
from aipass.prax import logger
TEMPLATES = ("empty", "python")
def find_host_root(start: Path) -> Path | None:
"""Walk up from *start* to find the AIPass host installation root.
Returns the directory containing ``*_REGISTRY.json``, or ``None``.
"""
for p in [start, *start.parents]:
try:
entries = list(p.iterdir())
except OSError:
continue
for f in entries:
try:
if f.is_file() and f.name.endswith("_REGISTRY.json"):
return p
except OSError:
continue
return None
def _validate_name(name: str) -> str:
if not name:
raise ValueError("Project name cannot be empty")
if not re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", name):
raise ValueError(
f"Invalid project name '{name}'. "
"Must start with a letter, contain only letters, digits, hyphens, underscores."
)
return name
def _registry_name(name: str) -> str:
return re.sub(r"[^A-Z0-9_-]", "_", name.upper()).strip("_")
def _git(args: list[str], cwd: Path) -> str:
r = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
if r.returncode != 0:
raise RuntimeError(f"git {' '.join(args)}: {r.stderr.strip()}")
return r.stdout.strip()
# ── registry ────────────────────────────────────────────────────────────
def _write_registry(target: Path, name: str) -> tuple[str, str]:
"""Mint the project registry. Returns ``(registry_id, filename)``."""
registry_id = str(uuid.uuid4())
today = date.today().isoformat()
reg = _registry_name(name)
filename = f"{reg}_REGISTRY.json"
data = {
"metadata": {
"id": registry_id,
"name": reg,
"version": "1.0.0",
"created": today,
"last_updated": today,
"total_branches": 0,
},
"branches": [],
}
(target / filename).write_text(
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return registry_id, filename
# ── templates ───────────────────────────────────────────────────────────
def _write_template(target: Path, name: str, template: str) -> list[str]:
"""Write template-specific files. Returns relative paths created."""
created: list[str] = []
(target / "README.md").write_text(
f"# {name}\n\nCreated with `aipass new`. Template: {template}.\n",
encoding="utf-8",
)
created.append("README.md")
(target / ".gitignore").write_text(
"__pycache__/\n*.pyc\n.venv/\n.trinity/\n.ai_mail.local/\n*.local.json\n*.local/\nlogs/\n",
encoding="utf-8",
)
created.append(".gitignore")
if template == "python":
pkg = name.replace("-", "_").lower()
(target / "pyproject.toml").write_text(
"[build-system]\n"
'requires = ["setuptools>=61.0"]\n'
'build-backend = "setuptools.build_meta"\n\n'
"[project]\n"
f'name = "{name}"\n'
'version = "0.1.0"\n'
f'description = "{name} — born deployable"\n'
'requires-python = ">=3.10"\n\n'
"[tool.setuptools.packages.find]\n"
'where = ["src"]\n\n'
"[tool.pytest.ini_options]\n"
'testpaths = ["src"]\n'
'pythonpath = ["src"]\n',
encoding="utf-8",
)
created.append("pyproject.toml")
src = target / "src" / pkg
src.mkdir(parents=True)
(src / "__init__.py").write_text(
f'"""{name} — born deployable."""\n\n__version__ = "0.1.0"\n',
encoding="utf-8",
)
created.append(f"src/{pkg}/__init__.py")
return created
# ── AIPass scaffold ─────────────────────────────────────────────────────
def _scaffold_aipass(target: Path, name: str) -> list[str]:
"""Write AIPass scaffold files (tiers, hooks, CLAUDE.md, settings, .venv)."""
from aipass.aipass.apps.handlers.init.bootstrap import (
_claude_settings,
_detect_aipass_home,
_enroll_project,
)
from aipass.aipass.apps.handlers.init import scaffold_content as sc
created: list[str] = []
aipass_home = _detect_aipass_home()
reg = _registry_name(name)
# .aipass/
aipass_dir = target / ".aipass"
aipass_dir.mkdir(exist_ok=True)
# Tier files
if aipass_home:
for tier_file in ("tier0_kernel.md", "tier1_navmap.md"):
dest = aipass_dir / tier_file
src_path = Path(aipass_home) / ".aipass" / tier_file
if src_path.is_file():
shutil.copy2(str(src_path), str(dest))
created.append(f".aipass/{tier_file}")
# hooks.json + trust enrollment
if aipass_home:
template = Path(aipass_home) / ".aipass" / "project_hooks.json"
if template.is_file():
shutil.copy2(str(template), str(aipass_dir / "hooks.json"))
created.append(".aipass/hooks.json")
_enroll_project(target)
# CLAUDE.md, AGENTS.md
for md_name in ("CLAUDE.md", "AGENTS.md"):
dest = target / md_name
if dest.exists():
continue
if aipass_home:
tmpl = Path(aipass_home) / ".aipass" / f"project_{md_name}"
if tmpl.is_file():
content = tmpl.read_text(encoding="utf-8").replace("{name}", reg)
dest.write_text(content, encoding="utf-8")
created.append(md_name)
continue
if md_name == "AGENTS.md":
dest.write_text(sc.agents_md(reg), encoding="utf-8")
created.append(md_name)
# .claude/settings.json
claude_dir = target / ".claude"
claude_dir.mkdir(exist_ok=True)
(claude_dir / "settings.json").write_text(
_claude_settings(aipass_home),
encoding="utf-8",
)
created.append(".claude/settings.json")
# .claude/commands/prep.md
commands_dir = claude_dir / "commands"
commands_dir.mkdir(exist_ok=True)
(commands_dir / "prep.md").write_text(sc.prep_md(), encoding="utf-8")
created.append(".claude/commands/prep.md")
# .venv symlink
if aipass_home:
venv = Path(aipass_home) / ".venv"
if venv.is_dir():
link = target / ".venv"
link.symlink_to(venv)
created.append(".venv")
return created
# ── git ─────────────────────────────────────────────────────────────────
def _git_init(target: Path, name: str, template: str) -> None:
"""Initialize git repo with birth commit. Guards against re-init."""
if (target / ".git").exists():
raise RuntimeError(f"'{target}' already has a .git directory")
_git(["init", "-b", "main"], target)
_git(["add", "-A"], target)
_git(
["commit", "-m", f"birth: {name} ({template} template) via aipass new"],
target,
)
# ── agent (passport + registry seat) ────────────────────────────────────
def _entry_point_content(name: str) -> str:
"""Generate the entry point script for the newborn project agent."""
reg = _registry_name(name)
return (
f'"""\n'
f"{reg} — project agent\n"
f"\n"
f"Auto-discovery architecture:\n"
f"- Scans modules/ directory for .py files with handle_command()\n"
f"- Routes commands to discovered modules automatically\n"
f'"""\n'
f"\n"
f"import importlib\n"
f"import os\n"
f"import sys\n"
f"from pathlib import Path\n"
f"from typing import Any, List\n"
f"\n"
f"PROJECT_ROOT = str(Path(__file__).resolve().parent.parent)\n"
f"if PROJECT_ROOT not in sys.path:\n"
f" sys.path.insert(0, PROJECT_ROOT)\n"
f"\n"
f'os.environ.setdefault("AIPASS_BRANCH_NAME", "{name}")\n'
f"\n"
f"from aipass.cli.apps.modules import console # noqa: E402\n"
f"from aipass.prax import logger # noqa: E402\n"
f"\n"
f'MODULES_DIR = Path(__file__).parent / "modules"\n'
f"\n"
f"\n"
f"def discover_modules() -> List[Any]:\n"
f' """Auto-discover modules in modules/ directory."""\n'
f" modules = []\n"
f" if not MODULES_DIR.exists():\n"
f" return modules\n"
f' for file_path in MODULES_DIR.glob("*.py"):\n'
f' if file_path.name.startswith("_"):\n'
f" continue\n"
f" for prefix in (\n"
f' f"aipass.{name}.apps.modules.{{file_path.stem}}",\n'
f' f"apps.modules.{{file_path.stem}}",\n'
f" ):\n"
f" try:\n"
f" mod = importlib.import_module(prefix)\n"
f' if hasattr(mod, "handle_command"):\n'
f" modules.append(mod)\n"
f" break\n"
f" except ImportError:\n"
f" continue\n"
f" return modules\n"
f"\n"
f"\n"
f"def print_introspection() -> None:\n"
f' """Bare invocation — title, purpose, --help pointer."""\n'
f" console.print()\n"
f' console.print("[bold cyan]{reg} — Project Agent[/bold cyan]")\n'
f' console.print("[dim]Resident agent of the {name} project.[/dim]")\n'
f" console.print()\n"
f" modules = discover_modules()\n"
f" if modules:\n"
f' console.print("[yellow]Modules:[/yellow]")\n'
f" for m in modules:\n"
f' cmd = getattr(m, "COMMAND", m.__name__.split(".")[-1])\n'
f' desc = (m.__doc__ or "").strip().split("\\n")[0]\n'
f' console.print(f" [green]{{cmd:16}}[/green] [dim]{{desc}}[/dim]")\n'
f" console.print()\n"
f" console.print(\"[dim]Run 'drone @{name} --help' for usage[/dim]\")\n"
f" console.print()\n"
f"\n"
f"\n"
f"def print_help() -> None:\n"
f' """Full help — usage, commands, examples."""\n'
f" console.print()\n"
f' console.print("[bold cyan]{reg} — Project Agent[/bold cyan]")\n'
f" console.print()\n"
f' console.print("[yellow]Usage:[/yellow]")\n'
f' console.print(" [green]drone @{name}[/green] [dim]<command>[/dim]")\n'
f" console.print()\n"
f' console.print("[yellow]Commands:[/yellow]")\n'
f' console.print(" [green]hello[/green] [dim]Confirm the agent is alive[/dim]")\n'
f" console.print()\n"
f' console.print("[yellow]Examples:[/yellow]")\n'
f' console.print(" [green]drone @{name} hello[/green]")\n'
f" console.print()\n"
f"\n"
f"\n"
f"def route_command(command: str, args: List[str], modules: List[Any]) -> bool:\n"
f' """Route command to appropriate module."""\n'
f" for module in modules:\n"
f" try:\n"
f" if module.handle_command(command, args):\n"
f" return True\n"
f" except Exception as e:\n"
f' logger.error("[{reg}] Module error: %s", e)\n'
f" return False\n"
f"\n"
f"\n"
f"def main():\n"
f' """Main entry point."""\n'
f" modules = discover_modules()\n"
f" args = sys.argv[1:]\n"
f"\n"
f" if not args:\n"
f" print_introspection()\n"
f" return 0\n"
f' if args[0] in ("--help", "-h", "help"):\n'
f" print_help()\n"
f" return 0\n"
f' if args[0] == "hello":\n'
f' console.print(f"[cyan]{reg}[/cyan] here. '
f'Project agent, alive and ready.")\n'
f" return 0\n"
f"\n"
f" command = args[0]\n"
f" remaining = args[1:] if len(args) > 1 else []\n"
f" if route_command(command, remaining, modules):\n"
f" return 0\n"
f' console.print(f"Unknown command: {{command}}")\n'
f" return 1\n"
f"\n"
f"\n"
f'if __name__ == "__main__":\n'
f" sys.exit(main())\n"
)
def _local_json_content(name: str) -> dict:
"""Generate initial local.json for the newborn agent."""
reg = _registry_name(name)
today = date.today().isoformat()
return {
"document_metadata": {
"document_type": "session_history",
"document_name": f"{reg}.LOCAL",
"version": "1.0.0",
"schema_version": "3.0.0",
"created": today,
"last_updated": today,
"managed_by": reg,
"tags": ["session_tracking", "work_log", reg],
},
"todos": [],
"key_learnings": [],
"sessions": [],
}
def _observations_json_content(name: str) -> dict:
"""Generate initial observations.json for the newborn agent."""
reg = _registry_name(name)
today = date.today().isoformat()
return {
"document_metadata": {
"document_type": "observations",
"document_name": f"{reg}.OBSERVATIONS",
"version": "1.0.0",
"created": today,
"last_updated": today,
"managed_by": reg,
},
"observations": [],
}
def _write_agent(target: Path, name: str, registry_id: str, registry_file: str) -> list[str]:
"""Create the full framework agent: entry point, structure, identity, registry seat.
Models the credential linkage from the proven proto:
``registry.metadata.id == passport.citizenship.registry_id``.
"""
today = date.today().isoformat()
reg = _registry_name(name)
created: list[str] = []
# ── apps/ entry point + skeleton ─────────────────────────────────
apps_dir = target / "apps"
apps_dir.mkdir(exist_ok=True)
(apps_dir / "__init__.py").write_text("")
created.append("apps/__init__.py")
entry_file = apps_dir / f"{name}.py"
entry_file.write_text(_entry_point_content(name), encoding="utf-8")
created.append(f"apps/{name}.py")
for sub in ("modules", "handlers"):
d = apps_dir / sub
d.mkdir(exist_ok=True)
(d / "__init__.py").write_text("")
created.append(f"apps/{sub}/__init__.py")
# ── .trinity/ identity ───────────────────────────────────────────
trinity = target / ".trinity"
trinity.mkdir(exist_ok=True)
passport = {
"document_metadata": {
"document_type": "branch_identity",
"document_name": f"{reg}.PASSPORT",
"version": "1.0.0",
"schema_version": "1.0.0",
"created": today,
"last_updated": today,
"managed_by": reg,
"tags": ["identity", "passport", "branch_profile"],
},
"branch_info": {
"branch_name": reg,
"alias": "",
"path": ".",
"module": name,
"created": today,
"git_branch": "main",
},
"identity": {
"citizen_class": "worker",
"role": "project_agent",
"purpose": f"Resident agent of the {name} project.",
"what_i_do": [],
"what_i_dont_do": [],
},
"principles": [
"Code is truth - fail honestly",
"Memory persists - context survives",
"Simple solutions over complex architecture",
],
"citizenship": {
"registered": True,
"registry_id": registry_id,
"communications": True,
"memory": True,
},
}
for fname, data in (
("passport.json", passport),
("local.json", _local_json_content(name)),
("observations.json", _observations_json_content(name)),
):
(trinity / fname).write_text(
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
created.append(f".trinity/{fname}")
# ── mailbox + logs ───────────────────────────────────────────────
(target / ".ai_mail.local").mkdir(exist_ok=True)
(target / "logs").mkdir(exist_ok=True)
# ── registry seat ────────────────────────────────────────────────
reg_path = target / registry_file
reg_data = json.loads(reg_path.read_text(encoding="utf-8"))
reg_data["metadata"]["total_branches"] = 1
reg_data["branches"] = [
{
"name": reg,
"registry_id": registry_id,
"path": ".",
"profile": "AIPass Project",
"description": f"Resident agent of the {name} project.",
"email": f"@{name}",
"status": "active",
"created": today,
"last_active": today,
"owner": True,
},
]
reg_path.write_text(
json.dumps(reg_data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
created.append(registry_file)
logger.info("[aipass new] full agent created: %s (%d files)", reg, len(created))
return created
# ── public API ──────────────────────────────────────────────────────────
def create_project(
name: str,
template: str = "empty",
no_agent: bool = False,
) -> dict:
"""Create a new project inside the AIPass host installation.
Returns:
dict with name, template, target, host, registry_id, registry_file,
files, agent_spawned.
Raises:
ValueError: Invalid name or template.
RuntimeError: Not inside AIPass, target exists, git failure.
"""
_validate_name(name)
if template not in TEMPLATES:
raise ValueError(f"Unknown template '{template}'. Choose from: {', '.join(TEMPLATES)}")
host = find_host_root(Path.cwd())
if host is None:
raise RuntimeError(
"Not inside an AIPass installation (no *_REGISTRY.json found). "
"`aipass new` creates projects inside the AIPass environment."
)
target = host / "projects" / name
if target.exists():
raise RuntimeError(f"Project already exists: {target}")
target.mkdir(parents=True)
try:
registry_id, registry_file = _write_registry(target, name)
logger.info("[aipass new] registry minted: %s (%s)", registry_file, registry_id)
template_files = _write_template(target, name, template)
scaffold_files = _scaffold_aipass(target, name)
agent_files: list[str] = []
if not no_agent:
agent_files = _write_agent(target, name, registry_id, registry_file)
_git_init(target, name, template)
logger.info("[aipass new] git repo initialized with birth commit")
return {
"name": name,
"template": template,
"target": str(target),
"host": str(host),
"registry_id": registry_id,
"registry_file": registry_file,
"files": template_files + scaffold_files + agent_files,
"agent_created": not no_agent,
}
except Exception:
if target.exists():
shutil.rmtree(target)
raise
@@ -1,5 +1,5 @@
# =================== AIPass ====================
# Name: doctor_fix.py
# Name: _doctor_fix.py
# Description: Structure remediation report for aipass doctor --fix
# Version: 1.0.0
# Created: 2026-05-15
@@ -1,5 +1,5 @@
# =================== AIPass ====================
# Name: doctor_wire.py
# Name: _doctor_wire.py
# Description: Auto-wire provider settings from manifest into user config
# Version: 1.0.0
# Created: 2026-05-08
+4 -2
View File
@@ -54,11 +54,11 @@ from aipass.aipass.apps.handlers.structure_scan.structure_scanner import (
find_project_root,
scan_agents,
)
from aipass.aipass.apps.modules.doctor_fix import (
from aipass.aipass.apps.modules._doctor_fix import (
print_json_report,
print_remediation_report,
)
from aipass.aipass.apps.modules.doctor_wire import (
from aipass.aipass.apps.modules._doctor_wire import (
_auto_wire_provider,
_prompt_auto_wire as prompt_auto_wire,
check_wire_verify,
@@ -81,6 +81,8 @@ from aipass.aipass.apps.handlers.ui.progress import (
make_doctor_progress,
)
COMMAND = "doctor"
_BRANCH_ROOT = Path(__file__).resolve().parents[2]
+12 -2
View File
@@ -729,8 +729,12 @@ def stage_10_done(accumulated: Dict[str, Any] | None = None, dry_run: bool = Fal
# --- MAIN RUNNER ---
def _preflight_check() -> str | None:
"""Return an error message if CWD is unsafe for init, else None."""
def _preflight_check(*, allow_projects_child: bool = False) -> str | None:
"""Return an error message if CWD is unsafe for init, else None.
When *allow_projects_child* is True, the nested-project check is skipped
if CWD is ``<host>/projects/<name>``.
"""
cwd = Path.cwd()
# Block if inside an agent directory
if (cwd / ".trinity" / "passport.json").is_file():
@@ -738,6 +742,12 @@ def _preflight_check() -> str | None:
"This directory is an agent branch (has .trinity/passport.json).\n"
"Agents are managed by 'drone @spawn', not 'aipass init'."
)
# Early exit: if CWD is a valid <host>/projects/<name>, skip nesting check
if allow_projects_child:
from aipass.aipass.apps.handlers.init.bootstrap import is_projects_child
if is_projects_child(cwd):
return None
# Block if inside an existing AIPass project (registry above us).
# Walking up to the filesystem root can hit ancestors that can't be
# enumerated or stat'd — e.g. locked Windows system entries at the drive
@@ -0,0 +1,205 @@
# =================== AIPass ====================
# Name: new_project.py
# Description: aipass new — create projects inside the AIPass installation
# Version: 1.0.0
# Created: 2026-07-17
# Modified: 2026-07-17
# =============================================
"""
aipass new — create projects inside the AIPass installation (DPLAN-0247)
Creates a project at <host>/projects/<name> with its own git repo,
AIPass scaffold, and optional resident agent. Born deployable.
"""
from __future__ import annotations
from pathlib import Path
from aipass.aipass.apps.handlers.json import json_handler
from aipass.cli.apps.modules import console, error, success
from aipass.prax import logger
COMMAND = "new"
def print_introspection() -> None:
"""List existing projects in the installation."""
from aipass.aipass.apps.handlers.new_project import find_host_root
host = find_host_root(Path.cwd())
console.print()
console.print("[bold cyan]aipass new[/bold cyan] — project creator")
console.print()
if host is None:
console.print("[dim]Not inside an AIPass installation.[/dim]")
console.print()
return
projects_dir = host / "projects"
if not projects_dir.is_dir():
console.print(f"[dim]No projects/ directory at {host}[/dim]")
console.print()
return
projects = [d for d in sorted(projects_dir.iterdir()) if d.is_dir() and not d.name.startswith(".")]
if not projects:
console.print("[dim]No projects yet. Create one:[/dim]")
else:
console.print(f"[yellow]{len(projects)} project(s):[/yellow]")
for p in projects:
has_git = (p / ".git").is_dir()
has_reg = any(f.name.endswith("_REGISTRY.json") for f in p.iterdir() if f.is_file())
markers = []
if has_git:
markers.append("git")
if has_reg:
markers.append("registry")
info = f" [dim]({', '.join(markers)})[/dim]" if markers else ""
console.print(f" [cyan]{p.name}[/cyan]{info}")
console.print()
console.print("[dim]Create: aipass new <name> [--template python] [--no-agent][/dim]")
console.print()
def print_help() -> None:
"""Print usage help for the new command."""
from aipass.aipass.apps.handlers.new_project import TEMPLATES
console.print()
console.print("[bold cyan]aipass new[/bold cyan] — create a project inside AIPass")
console.print()
console.print("[yellow]USAGE:[/yellow]")
console.print(" [green]aipass new <name>[/green] [dim]# Create with empty template[/dim]")
console.print(" [green]aipass new <name> --template python[/green] [dim]# Create with Python template[/dim]")
console.print(" [green]aipass new <name> --no-agent[/green] [dim]# Skip agent creation[/dim]")
console.print()
console.print("[yellow]TEMPLATES:[/yellow]")
console.print(f" [dim]{', '.join(TEMPLATES)}[/dim]")
console.print()
console.print("[yellow]WHAT IT DOES:[/yellow]")
console.print(" Creates projects/<name> with its own git repo, AIPass scaffold,")
console.print(" and optional resident agent. Born deployable — repo + packaging")
console.print(" from minute one.")
console.print()
def _prompt_template(templates: list[str]) -> str:
"""Prompt user to choose a template interactively."""
console.print()
console.print("[yellow]Choose a template:[/yellow]")
for idx, t in enumerate(templates, 1):
console.print(f" [green]{idx}[/green]. {t}")
console.print()
while True:
try:
choice = input("Template [1]: ").strip()
except (EOFError, KeyboardInterrupt):
logger.info("template prompt interrupted, defaulting to %s", templates[0])
return templates[0]
if not choice:
return templates[0]
if choice.isdigit() and 1 <= int(choice) <= len(templates):
return templates[int(choice) - 1]
if choice in templates:
return choice
error(f"Invalid choice. Enter 1-{len(templates)} or a template name.")
def _prompt_agent() -> bool:
"""Prompt user whether to skip agent creation. Returns no_agent flag."""
console.print()
while True:
try:
choice = input("Create resident agent? [Y/n]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
logger.info("agent prompt interrupted, defaulting to create agent")
return False
if choice in ("", "y", "yes"):
return False
if choice in ("n", "no"):
return True
error("Enter y or n.")
def handle_command(command: str, args: list[str]) -> bool:
"""Route the 'new' command. Returns True if handled."""
if command != COMMAND:
return False
if not args:
json_handler.log_operation("new_project_usage", {"command": command})
print_introspection()
return True
if args[0] in ("--help", "-h", "help"):
json_handler.log_operation("new_project_help", {"command": command})
print_help()
return True
if args[0] == "--info":
json_handler.log_operation("new_project_info", {"command": command})
print_introspection()
return True
name = args[0]
template = None
no_agent = None
has_template_flag = False
has_agent_flag = False
i = 1
while i < len(args):
if args[i] == "--template" and i + 1 < len(args):
template = args[i + 1]
has_template_flag = True
i += 2
elif args[i] == "--no-agent":
no_agent = True
has_agent_flag = True
i += 1
else:
error(f"Unknown option: {args[i]}")
print_help()
return True
from aipass.aipass.apps.handlers.new_project import TEMPLATES, create_project
if not has_template_flag:
template = _prompt_template(list(TEMPLATES))
elif template is None:
template = "empty"
if not has_agent_flag:
no_agent = _prompt_agent()
elif no_agent is None:
no_agent = False
try:
result = create_project(name, template, no_agent)
except (RuntimeError, ValueError) as e:
logger.warning("[AIPASS] new project failed: %s", e)
error(str(e))
return True
console.print()
success(f"Project '{name}' created at {result['target']}")
console.print()
console.print(f" [dim]Registry:[/dim] {result['registry_file']}")
console.print(f" [dim]Template:[/dim] {result['template']}")
if result["agent_created"]:
console.print(" [dim]Agent:[/dim] created (full framework agent)")
else:
console.print(" [dim]Agent:[/dim] skipped (--no-agent)")
console.print()
console.print("[yellow]Next steps:[/yellow]")
console.print(f" [cyan]cd {result['target']}[/cyan]")
console.print(" [cyan]claude[/cyan] [dim]# meet your project agent[/dim]")
console.print()
json_handler.log_operation(
"new_project_create",
{"name": name, "template": template, "target": result["target"]},
)
logger.info("[AIPASS] new project: %s (%s) at %s", name, template, result["target"])
return True
+68 -59
View File
@@ -153,10 +153,10 @@ class TestMain:
"""--version prints real package version and returns 0."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "--version"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
result = main()
assert result == 0
printed = mock_print.call_args[0][0]
printed = mock_con.print.call_args[0][0]
assert printed.startswith("aipass ")
assert printed != "aipass 0.1.0"
@@ -164,10 +164,10 @@ class TestMain:
"""-V prints real package version and returns 0."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "-V"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
result = main()
assert result == 0
printed = mock_print.call_args[0][0]
printed = mock_con.print.call_args[0][0]
assert printed.startswith("aipass ")
def test_version_flag_fallback(self) -> None:
@@ -179,76 +179,81 @@ class TestMain:
"aipass.aipass.apps.aipass.importlib.metadata.version",
side_effect=_not_found,
):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
result = main()
assert result == 0
mock_print.assert_called_once_with("aipass unknown")
mock_con.print.assert_called_once_with("aipass unknown")
def test_help_flag_shows_help(self) -> None:
"""--help shows module list and returns 0."""
"""--help calls print_help and returns 0."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "--help"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
result = main()
assert result == 0
mock_print.assert_called()
mock_con.print.assert_called()
def test_h_flag_shows_help(self) -> None:
"""-h shows module list and returns 0."""
"""-h shows help and returns 0."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "-h"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print"):
with patch("aipass.aipass.apps.aipass.console"):
result = main()
assert result == 0
def test_no_args_shows_help(self) -> None:
"""No arguments shows module list and returns 0."""
"""No arguments shows introspection and returns 0."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print"):
with patch("aipass.aipass.apps.aipass.console"):
result = main()
assert result == 0
def test_help_word_shows_help(self) -> None:
"""'help' as only arg shows module list and returns 0."""
def test_help_word_routes_to_module(self) -> None:
"""'help' as only arg routes to help_chat module, not root help."""
mod = MagicMock()
mod.handle_command.return_value = True
mod.__name__ = "aipass.aipass.apps.modules.help_chat"
mod.COMMAND = "help"
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "help"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print"):
result = main()
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[mod]):
result = main()
assert result == 0
mod.handle_command.assert_called_once_with("help", [])
def test_help_shows_module_count(self) -> None:
"""Help output includes discovered module count."""
mod = types.ModuleType("test_mod")
mod.__doc__ = "Test module doc"
def test_introspection_shows_public_commands(self) -> None:
"""Introspection lists modules with COMMAND in _PUBLIC_COMMANDS."""
mod = types.ModuleType("aipass.aipass.apps.modules.help_chat")
mod.__doc__ = "Help chatbot"
mod.COMMAND = "help" # type: ignore[attr-defined]
mod.handle_command = lambda c, a: True # type: ignore[attr-defined]
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[mod]):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
main()
first_call_args = mock_print.call_args_list[0][0][0]
assert "1 modules" in first_call_args
printed = " ".join(str(a) for call in mock_con.print.call_args_list for a in call[0])
assert "help" in printed
def test_help_shows_module_with_no_doc(self) -> None:
"""Module without docstring shows 'No description'."""
mod = types.ModuleType("nodoc_mod")
mod.__doc__ = None
def test_introspection_hides_non_public(self) -> None:
"""Modules without COMMAND in _PUBLIC_COMMANDS are hidden."""
mod = types.ModuleType("aipass.aipass.apps.modules.internal")
mod.__doc__ = "Internal module"
mod.handle_command = lambda c, a: True # type: ignore[attr-defined]
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[mod]):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
main()
printed = " ".join(str(a) for call in mock_print.call_args_list for a in call[0])
assert "No description" in printed
printed = " ".join(str(a) for call in mock_con.print.call_args_list for a in call[0])
assert "internal" not in printed
def test_unknown_command_returns_1(self) -> None:
"""Unknown command prints error and returns 1."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "xyzzy"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
result = main()
assert result == 1
mock_print.assert_called_with("Unknown command: xyzzy")
mock_con.print.assert_called_with("Unknown command: xyzzy")
def test_known_command_routes_and_returns_0(self) -> None:
"""Known command that gets handled returns 0."""
@@ -266,10 +271,10 @@ class TestMain:
"""@drone prints guidance pointing to drone, not 'Unknown command'."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "@drone"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
result = main()
assert result == 1
printed = " ".join(str(a) for call in mock_print.call_args_list for a in call[0])
printed = " ".join(str(a) for call in mock_con.print.call_args_list for a in call[0])
assert "@drone" in printed
assert "drone routing target" in printed
assert "Unknown command" not in printed
@@ -278,10 +283,10 @@ class TestMain:
"""@memory prints guidance with the actual @name the user typed."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "@memory"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
result = main()
assert result == 1
printed = " ".join(str(a) for call in mock_print.call_args_list for a in call[0])
printed = " ".join(str(a) for call in mock_con.print.call_args_list for a in call[0])
assert "@memory" in printed
assert "drone @memory" in printed
@@ -289,10 +294,10 @@ class TestMain:
"""Non-@ bad command still prints 'Unknown command', not drone guidance."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "frobnicate"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
result = main()
assert result == 1
mock_print.assert_called_with("Unknown command: frobnicate")
mock_con.print.assert_called_with("Unknown command: frobnicate")
def test_command_with_remaining_args(self) -> None:
"""Remaining args are passed to route_command."""
@@ -306,7 +311,7 @@ class TestMain:
mod.handle_command.assert_called_once_with("doctor", ["--verbose", "--fix"])
def test_help_shows_command_constant(self) -> None:
"""Help listing uses module COMMAND constant, not file stem."""
"""Introspection uses module COMMAND constant, not file stem."""
mod = types.ModuleType("aipass.aipass.apps.modules.help_chat")
mod.__doc__ = "Help chatbot"
mod.COMMAND = "help" # type: ignore[attr-defined]
@@ -316,14 +321,14 @@ class TestMain:
"aipass.aipass.apps.aipass.discover_modules",
return_value=[mod],
):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
main()
printed = " ".join(str(a) for call in mock_print.call_args_list for a in call[0])
printed = " ".join(str(a) for call in mock_con.print.call_args_list for a in call[0])
assert "help" in printed
assert "help_chat" not in printed
def test_help_falls_back_to_stem(self) -> None:
"""Without COMMAND constant, help listing uses file stem."""
def test_introspection_skips_no_command_module(self) -> None:
"""Modules without COMMAND in _PUBLIC_COMMANDS are hidden from introspection."""
mod = types.ModuleType("aipass.aipass.apps.modules.doctor")
mod.__doc__ = "Doctor module"
mod.handle_command = lambda c, a: True # type: ignore[attr-defined]
@@ -332,10 +337,14 @@ class TestMain:
"aipass.aipass.apps.aipass.discover_modules",
return_value=[mod],
):
with patch("builtins.print") as mock_print:
with patch("aipass.aipass.apps.aipass.console") as mock_con:
main()
printed = " ".join(str(a) for call in mock_print.call_args_list for a in call[0])
assert "doctor" in printed
printed = " ".join(str(a) for call in mock_con.print.call_args_list for a in call[0])
assert (
"Commands:" not in printed or "doctor" not in printed.split("Commands:")[1]
if "Commands:" in printed
else True
)
def test_handler_crash_surfaces_error(self) -> None:
"""Handler crash prints real error, not 'Unknown command'."""
@@ -347,12 +356,12 @@ class TestMain:
"aipass.aipass.apps.aipass.discover_modules",
return_value=[mod],
):
with patch("builtins.print") as mock_print:
result = main()
with patch("aipass.aipass.apps.aipass.console"):
with patch("aipass.aipass.apps.aipass.error") as mock_err:
result = main()
assert result == 1
printed = " ".join(str(a) for call in mock_print.call_args_list for a in call[0])
assert "db connection failed" in printed
assert "Unknown command" not in printed
err_text = " ".join(str(a) for call in mock_err.call_args_list for a in call[0])
assert "db connection failed" in err_text
def test_import_failure_surfaces_on_command(self) -> None:
"""Failed module import surfaces when user types that command."""
@@ -365,11 +374,11 @@ class TestMain:
):
aipass_mod._import_failures.clear()
aipass_mod._import_failures["broken"] = ImportError("no module")
with patch("builtins.print") as mock_print:
result = main()
with patch("aipass.aipass.apps.aipass.console"):
with patch("aipass.aipass.apps.aipass.error") as mock_err:
result = main()
assert result == 1
printed = " ".join(str(a) for call in mock_print.call_args_list for a in call[0])
assert "failed to load" in printed
assert "no module" in printed
assert "Unknown command" not in printed
err_text = " ".join(str(a) for call in mock_err.call_args_list for a in call[0])
assert "failed to load" in err_text
assert "no module" in err_text
aipass_mod._import_failures.clear()
+28 -28
View File
@@ -656,7 +656,7 @@ class TestReconcileStaleDeny:
def test_no_settings_file_returns_empty(self, tmp_path) -> None:
"""Missing settings.json returns no results."""
from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny
from aipass.aipass.apps.modules._doctor_wire import reconcile_stale_deny
with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path):
results = reconcile_stale_deny(fix=False)
@@ -664,7 +664,7 @@ class TestReconcileStaleDeny:
def test_no_stale_rules_returns_pass(self, tmp_path) -> None:
"""Settings with no stale rm rules returns PASS."""
from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny
from aipass.aipass.apps.modules._doctor_wire import reconcile_stale_deny
settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True)
@@ -680,7 +680,7 @@ class TestReconcileStaleDeny:
def test_stale_rules_detected_without_fix(self, tmp_path) -> None:
"""Stale rm rules present returns WARN when fix=False."""
from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny
from aipass.aipass.apps.modules._doctor_wire import reconcile_stale_deny
settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True)
@@ -697,7 +697,7 @@ class TestReconcileStaleDeny:
def test_fix_removes_stale_rules(self, tmp_path) -> None:
"""fix=True removes stale rules and preserves others."""
from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny
from aipass.aipass.apps.modules._doctor_wire import reconcile_stale_deny
settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True)
@@ -719,7 +719,7 @@ class TestReconcileStaleDeny:
def test_fix_single_stale_rule(self, tmp_path) -> None:
"""fix=True works when only one of two stale rules is present."""
from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny
from aipass.aipass.apps.modules._doctor_wire import reconcile_stale_deny
settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True)
@@ -736,7 +736,7 @@ class TestReconcileStaleDeny:
def test_fix_idempotent(self, tmp_path) -> None:
"""Running fix twice is safe — second run returns PASS with no stale rules."""
from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny
from aipass.aipass.apps.modules._doctor_wire import reconcile_stale_deny
settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True)
@@ -753,7 +753,7 @@ class TestReconcileStaleDeny:
def test_empty_deny_list_returns_pass(self, tmp_path) -> None:
"""Empty deny list returns PASS."""
from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny
from aipass.aipass.apps.modules._doctor_wire import reconcile_stale_deny
settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True)
@@ -765,7 +765,7 @@ class TestReconcileStaleDeny:
def test_no_permissions_key_returns_pass(self, tmp_path) -> None:
"""Settings without permissions key returns PASS."""
from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny
from aipass.aipass.apps.modules._doctor_wire import reconcile_stale_deny
settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True)
@@ -781,10 +781,10 @@ class TestCheckWireVerify:
def test_pass_on_zero_exit(self) -> None:
"""Exit 0 from drone @hooks verify produces a PASS row."""
from aipass.aipass.apps.modules.doctor_wire import check_wire_verify
from aipass.aipass.apps.modules._doctor_wire import check_wire_verify
fake = MagicMock(returncode=0, stdout="✓ Wire check passed\n\n0 errors, 0 warnings\n")
with patch("aipass.aipass.apps.modules.doctor_wire.subprocess.run", return_value=fake):
with patch("aipass.aipass.apps.modules._doctor_wire.subprocess.run", return_value=fake):
results = check_wire_verify()
assert len(results) == 1
assert results[0].label == "wire verify"
@@ -792,10 +792,10 @@ class TestCheckWireVerify:
def test_fail_on_nonzero_exit(self) -> None:
"""Non-zero exit from drone @hooks verify produces a FAIL row."""
from aipass.aipass.apps.modules.doctor_wire import check_wire_verify
from aipass.aipass.apps.modules._doctor_wire import check_wire_verify
fake = MagicMock(returncode=1, stdout="ERROR empty array\n2 errors, 0 warnings\n")
with patch("aipass.aipass.apps.modules.doctor_wire.subprocess.run", return_value=fake):
with patch("aipass.aipass.apps.modules._doctor_wire.subprocess.run", return_value=fake):
results = check_wire_verify()
assert len(results) == 1
assert results[0].glyph == "[red]✗[/red]"
@@ -803,10 +803,10 @@ class TestCheckWireVerify:
def test_warn_on_drone_not_found(self) -> None:
"""FileNotFoundError (drone missing) produces a WARN row."""
from aipass.aipass.apps.modules.doctor_wire import check_wire_verify
from aipass.aipass.apps.modules._doctor_wire import check_wire_verify
with patch(
"aipass.aipass.apps.modules.doctor_wire.subprocess.run",
"aipass.aipass.apps.modules._doctor_wire.subprocess.run",
side_effect=FileNotFoundError("drone"),
):
results = check_wire_verify()
@@ -817,10 +817,10 @@ class TestCheckWireVerify:
"""TimeoutExpired produces a WARN row."""
import subprocess as sp
from aipass.aipass.apps.modules.doctor_wire import check_wire_verify
from aipass.aipass.apps.modules._doctor_wire import check_wire_verify
with patch(
"aipass.aipass.apps.modules.doctor_wire.subprocess.run",
"aipass.aipass.apps.modules._doctor_wire.subprocess.run",
side_effect=sp.TimeoutExpired(cmd="drone", timeout=10),
):
results = check_wire_verify()
@@ -849,15 +849,15 @@ class TestPromptAutoWireIsatty:
def test_non_tty_stdin_skips_prompt_and_declines(self) -> None:
"""Non-tty stdin must NOT call input() — it declines and warns instead."""
from aipass.aipass.apps.modules import doctor_wire
from aipass.aipass.apps.modules import _doctor_wire
with (
patch.object(doctor_wire.sys, "stdin") as mock_stdin,
patch.object(_doctor_wire.sys, "stdin") as mock_stdin,
patch("builtins.input") as mock_input,
patch.object(doctor_wire, "_print_manual_wire_warning") as mock_warn,
patch.object(_doctor_wire, "_print_manual_wire_warning") as mock_warn,
):
mock_stdin.isatty.return_value = False
result = doctor_wire._prompt_auto_wire(**self._args())
result = _doctor_wire._prompt_auto_wire(**self._args())
assert result is False
mock_input.assert_not_called()
@@ -865,30 +865,30 @@ class TestPromptAutoWireIsatty:
def test_tty_stdin_prompts_and_respects_decline(self) -> None:
"""Tty stdin still prompts; a 'n' answer declines."""
from aipass.aipass.apps.modules import doctor_wire
from aipass.aipass.apps.modules import _doctor_wire
with (
patch.object(doctor_wire.sys, "stdin") as mock_stdin,
patch.object(_doctor_wire.sys, "stdin") as mock_stdin,
patch("builtins.input", return_value="n") as mock_input,
patch.object(doctor_wire, "_print_manual_wire_warning"),
patch.object(_doctor_wire, "_print_manual_wire_warning"),
):
mock_stdin.isatty.return_value = True
result = doctor_wire._prompt_auto_wire(**self._args())
result = _doctor_wire._prompt_auto_wire(**self._args())
assert result is False
mock_input.assert_called_once()
def test_tty_stdin_accepts_and_wires(self) -> None:
"""Tty stdin with a 'y' answer runs the wire and returns True."""
from aipass.aipass.apps.modules import doctor_wire
from aipass.aipass.apps.modules import _doctor_wire
with (
patch.object(doctor_wire.sys, "stdin") as mock_stdin,
patch.object(_doctor_wire.sys, "stdin") as mock_stdin,
patch("builtins.input", return_value="y"),
patch.object(doctor_wire, "_auto_wire_provider", return_value=["wired hook"]) as mock_wire,
patch.object(_doctor_wire, "_auto_wire_provider", return_value=["wired hook"]) as mock_wire,
):
mock_stdin.isatty.return_value = True
result = doctor_wire._prompt_auto_wire(**self._args())
result = _doctor_wire._prompt_auto_wire(**self._args())
assert result is True
mock_wire.assert_called_once()
+7 -7
View File
@@ -12,7 +12,7 @@ import json
from pathlib import Path
from unittest.mock import patch
from aipass.aipass.apps.modules.doctor_fix import (
from aipass.aipass.apps.modules._doctor_fix import (
RemediationItem,
detect_project_name,
format_json_report,
@@ -68,7 +68,7 @@ class TestDetectProjectName:
no_reg = tmp_path / "empty_project"
no_reg.mkdir()
with patch(
"aipass.aipass.apps.modules.doctor_fix._discover_registry",
"aipass.aipass.apps.modules._doctor_fix._discover_registry",
return_value=no_reg / "MISSING_REGISTRY.json",
):
result = detect_project_name(no_reg)
@@ -368,16 +368,16 @@ class TestPrintFunctions:
class TestDoctorFixHandleCommand:
def test_wrong_command(self) -> None:
"""Non-doctor_fix commands are not handled."""
from aipass.aipass.apps.modules.doctor_fix import handle_command
from aipass.aipass.apps.modules._doctor_fix import handle_command
assert handle_command("doctor", []) is False
assert handle_command("help", []) is False
def test_no_args_shows_usage(self) -> None:
"""No args shows usage message (not introspection banner)."""
from aipass.aipass.apps.modules.doctor_fix import handle_command
from aipass.aipass.apps.modules._doctor_fix import handle_command
with patch("aipass.aipass.apps.modules.doctor_fix.console") as mock_console:
with patch("aipass.aipass.apps.modules._doctor_fix.console") as mock_console:
result = handle_command("doctor_fix", [])
assert result is True
printed = " ".join(str(c) for c in mock_console.print.call_args_list)
@@ -385,9 +385,9 @@ class TestDoctorFixHandleCommand:
def test_info_flag(self) -> None:
"""--info triggers print_introspection."""
from aipass.aipass.apps.modules.doctor_fix import handle_command
from aipass.aipass.apps.modules._doctor_fix import handle_command
with patch("aipass.aipass.apps.modules.doctor_fix.print_introspection") as mock:
with patch("aipass.aipass.apps.modules._doctor_fix.print_introspection") as mock:
result = handle_command("doctor_fix", ["--info"])
assert result is True
mock.assert_called_once()
+1 -1
View File
@@ -330,7 +330,7 @@ class TestReturnTypeContracts:
def test_doctor_wire_handle_command_returns_bool(self):
"""Doctor wire handle_command returns True for match, False otherwise."""
from aipass.aipass.apps.modules.doctor_wire import handle_command as wire_cmd
from aipass.aipass.apps.modules._doctor_wire import handle_command as wire_cmd
assert wire_cmd("doctor_wire", []) is True
assert wire_cmd("not_wire", []) is False
+598
View File
@@ -0,0 +1,598 @@
# =================== AIPass ====================
# Name: test_new_project.py
# Description: Tests for aipass new — project creation handler
# Version: 1.0.0
# Created: 2026-07-17
# Modified: 2026-07-17
# =============================================
"""Tests for the new_project handler and module.
All file operations use tmp_path to stay fully isolated from the live
filesystem. Tests mock subprocess calls to avoid real git/drone invocations.
"""
import json
from pathlib import Path
from unittest.mock import patch
import pytest # pyright: ignore[reportMissingImports]
from aipass.aipass.apps.handlers.new_project import (
_registry_name,
_validate_name,
_write_agent,
_write_registry,
_write_template,
create_project,
find_host_root,
)
# ---------------------------------------------------------------------------
# find_host_root
# ---------------------------------------------------------------------------
def test_find_host_root_finds_registry(tmp_path):
"""Finds directory containing *_REGISTRY.json."""
(tmp_path / "AIPASS_REGISTRY.json").write_text("{}")
sub = tmp_path / "projects" / "myapp"
sub.mkdir(parents=True)
assert find_host_root(sub) == tmp_path
def test_find_host_root_returns_none_without_registry(tmp_path):
"""Returns None when no registry exists above start."""
assert find_host_root(tmp_path) is None
def test_find_host_root_finds_closest_registry(tmp_path):
"""Walks up and finds the closest *_REGISTRY.json."""
(tmp_path / "HOST_REGISTRY.json").write_text("{}")
sub = tmp_path / "a" / "b"
sub.mkdir(parents=True)
assert find_host_root(sub) == tmp_path
# ---------------------------------------------------------------------------
# _validate_name
# ---------------------------------------------------------------------------
def test_validate_name_accepts_valid():
assert _validate_name("myapp") == "myapp"
assert _validate_name("My-App_2") == "My-App_2"
def test_validate_name_rejects_empty():
with pytest.raises(ValueError, match="cannot be empty"):
_validate_name("")
def test_validate_name_rejects_leading_digit():
with pytest.raises(ValueError, match="Must start with a letter"):
_validate_name("2fast")
def test_validate_name_rejects_special_chars():
with pytest.raises(ValueError, match="Must start with a letter"):
_validate_name("my app!")
# ---------------------------------------------------------------------------
# _registry_name
# ---------------------------------------------------------------------------
def test_registry_name_uppercases():
assert _registry_name("myapp") == "MYAPP"
def test_registry_name_replaces_special():
assert _registry_name("my.app") == "MY_APP"
def test_registry_name_preserves_hyphens():
assert _registry_name("my-app") == "MY-APP"
# ---------------------------------------------------------------------------
# _write_registry
# ---------------------------------------------------------------------------
def test_write_registry_creates_file(tmp_path):
rid, fname = _write_registry(tmp_path, "demo")
path = tmp_path / fname
assert path.exists()
data = json.loads(path.read_text())
assert data["metadata"]["id"] == rid
assert data["metadata"]["name"] == "DEMO"
assert fname == "DEMO_REGISTRY.json"
assert data["branches"] == []
# ---------------------------------------------------------------------------
# _write_template — empty
# ---------------------------------------------------------------------------
def test_write_template_empty(tmp_path):
created = _write_template(tmp_path, "demo", "empty")
assert "README.md" in created
assert ".gitignore" in created
assert (tmp_path / "README.md").exists()
assert (tmp_path / ".gitignore").exists()
assert not (tmp_path / "pyproject.toml").exists()
assert not (tmp_path / "src").exists()
# ---------------------------------------------------------------------------
# _write_template — python
# ---------------------------------------------------------------------------
def test_write_template_python(tmp_path):
created = _write_template(tmp_path, "demo", "python")
assert "pyproject.toml" in created
assert "src/demo/__init__.py" in created
assert (tmp_path / "pyproject.toml").exists()
assert (tmp_path / "src" / "demo" / "__init__.py").exists()
pyproject = (tmp_path / "pyproject.toml").read_text()
assert 'name = "demo"' in pyproject
def test_write_template_python_hyphen_name(tmp_path):
_write_template(tmp_path, "my-app", "python")
assert (tmp_path / "src" / "my_app" / "__init__.py").exists()
# ---------------------------------------------------------------------------
# create_project — integration (mocked subprocess)
# ---------------------------------------------------------------------------
@pytest.fixture()
def host_env(tmp_path):
"""Set up a minimal AIPass host installation in tmp_path."""
(tmp_path / "AIPASS_REGISTRY.json").write_text(json.dumps({"metadata": {"id": "host-id"}, "branches": []}))
(tmp_path / "projects").mkdir()
(tmp_path / ".aipass").mkdir()
return tmp_path
def _mock_git_run(args, **kwargs):
"""Stub subprocess.run for git commands — always succeeds."""
from unittest.mock import MagicMock
result = MagicMock()
result.returncode = 0
result.stdout = ""
result.stderr = ""
return result
def test_create_project_empty_template(host_env, monkeypatch):
monkeypatch.chdir(host_env)
with (
patch("subprocess.run", side_effect=_mock_git_run),
patch(
"aipass.aipass.apps.handlers.init.bootstrap._detect_aipass_home",
return_value=None,
),
patch(
"aipass.aipass.apps.handlers.init.bootstrap._enroll_project",
),
):
result = create_project("testproj", template="empty", no_agent=True)
target = Path(result["target"])
assert target.exists()
assert result["name"] == "testproj"
assert result["template"] == "empty"
assert result["registry_file"] == "TESTPROJ_REGISTRY.json"
assert (target / "TESTPROJ_REGISTRY.json").exists()
assert (target / "README.md").exists()
assert (target / ".gitignore").exists()
assert not (target / "pyproject.toml").exists()
def test_create_project_python_template(host_env, monkeypatch):
monkeypatch.chdir(host_env)
with (
patch("subprocess.run", side_effect=_mock_git_run),
patch(
"aipass.aipass.apps.handlers.init.bootstrap._detect_aipass_home",
return_value=None,
),
patch(
"aipass.aipass.apps.handlers.init.bootstrap._enroll_project",
),
):
result = create_project("pyapp", template="python", no_agent=True)
target = Path(result["target"])
assert (target / "pyproject.toml").exists()
assert (target / "src" / "pyapp" / "__init__.py").exists()
def test_create_project_rejects_existing(host_env, monkeypatch):
monkeypatch.chdir(host_env)
(host_env / "projects" / "taken").mkdir()
with pytest.raises(RuntimeError, match="already exists"):
create_project("taken", no_agent=True)
def test_create_project_rejects_invalid_name(host_env, monkeypatch):
monkeypatch.chdir(host_env)
with pytest.raises(ValueError, match="Must start with a letter"):
create_project("123bad", no_agent=True)
def test_create_project_rejects_bad_template(host_env, monkeypatch):
monkeypatch.chdir(host_env)
with pytest.raises(ValueError, match="Unknown template"):
create_project("foo", template="rust", no_agent=True)
def test_create_project_no_host(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
with pytest.raises(RuntimeError, match="Not inside an AIPass"):
create_project("foo", no_agent=True)
def test_create_project_cleans_up_on_failure(host_env, monkeypatch):
monkeypatch.chdir(host_env)
def _fail_git(args, **kwargs):
from unittest.mock import MagicMock
result = MagicMock()
result.returncode = 1
result.stderr = "simulated failure"
return result
with (
patch("subprocess.run", side_effect=_fail_git),
patch(
"aipass.aipass.apps.handlers.init.bootstrap._detect_aipass_home",
return_value=None,
),
patch("aipass.aipass.apps.handlers.init.bootstrap._enroll_project"),
pytest.raises(RuntimeError, match="simulated failure"),
):
create_project("failproj", no_agent=True)
assert not (host_env / "projects" / "failproj").exists()
def test_create_project_registry_before_scaffold(host_env, monkeypatch):
"""Registry file must exist before scaffold runs (order invariant)."""
monkeypatch.chdir(host_env)
creation_order = []
original_write_registry = _write_registry
original_write_template = _write_template
def track_registry(target, name):
creation_order.append("registry")
return original_write_registry(target, name)
def track_template(target, name, template):
creation_order.append("template")
return original_write_template(target, name, template)
with (
patch("subprocess.run", side_effect=_mock_git_run),
patch(
"aipass.aipass.apps.handlers.new_project._write_registry",
side_effect=track_registry,
),
patch(
"aipass.aipass.apps.handlers.new_project._write_template",
side_effect=track_template,
),
patch(
"aipass.aipass.apps.handlers.init.bootstrap._detect_aipass_home",
return_value=None,
),
patch("aipass.aipass.apps.handlers.init.bootstrap._enroll_project"),
):
create_project("ordertest", no_agent=True)
assert creation_order.index("registry") < creation_order.index("template")
# ---------------------------------------------------------------------------
# _write_agent
# ---------------------------------------------------------------------------
def test_write_agent_creates_passport(tmp_path):
"""Passport is written with correct credential linkage."""
rid, fname = _write_registry(tmp_path, "agentapp")
files = _write_agent(tmp_path, "agentapp", rid, fname)
assert ".trinity/passport.json" in files
passport = json.loads((tmp_path / ".trinity" / "passport.json").read_text())
assert passport["citizenship"]["registry_id"] == rid
assert passport["branch_info"]["branch_name"] == "AGENTAPP"
assert passport["identity"]["role"] == "project_agent"
def test_write_agent_seats_in_registry(tmp_path):
"""Registry total_branches updated and branch entry added."""
rid, fname = _write_registry(tmp_path, "seated")
_write_agent(tmp_path, "seated", rid, fname)
reg = json.loads((tmp_path / fname).read_text())
assert reg["metadata"]["total_branches"] == 1
assert len(reg["branches"]) == 1
assert reg["branches"][0]["registry_id"] == rid
assert reg["branches"][0]["name"] == "SEATED"
def test_write_agent_credential_linkage(tmp_path):
"""registry.metadata.id == passport.citizenship.registry_id."""
rid, fname = _write_registry(tmp_path, "linked")
_write_agent(tmp_path, "linked", rid, fname)
reg = json.loads((tmp_path / fname).read_text())
passport = json.loads((tmp_path / ".trinity" / "passport.json").read_text())
assert reg["metadata"]["id"] == passport["citizenship"]["registry_id"]
def test_write_agent_creates_entry_point(tmp_path):
"""Entry point apps/<name>.py is created and contains hello handler."""
rid, fname = _write_registry(tmp_path, "myagent")
files = _write_agent(tmp_path, "myagent", rid, fname)
assert "apps/myagent.py" in files
content = (tmp_path / "apps" / "myagent.py").read_text()
assert "def main()" in content
assert "def print_introspection()" in content
assert "def print_help()" in content
assert '"hello"' in content
def test_write_agent_creates_apps_skeleton(tmp_path):
"""Apps skeleton: __init__.py, modules/, handlers/."""
rid, fname = _write_registry(tmp_path, "skel")
files = _write_agent(tmp_path, "skel", rid, fname)
assert "apps/__init__.py" in files
assert "apps/modules/__init__.py" in files
assert "apps/handlers/__init__.py" in files
assert (tmp_path / "apps" / "modules" / "__init__.py").exists()
assert (tmp_path / "apps" / "handlers" / "__init__.py").exists()
def test_write_agent_creates_trinity_full_set(tmp_path):
"""Full .trinity/ set: passport, local.json, observations.json."""
rid, fname = _write_registry(tmp_path, "fullset")
files = _write_agent(tmp_path, "fullset", rid, fname)
assert ".trinity/passport.json" in files
assert ".trinity/local.json" in files
assert ".trinity/observations.json" in files
local = json.loads((tmp_path / ".trinity" / "local.json").read_text())
assert local["document_metadata"]["managed_by"] == "FULLSET"
obs = json.loads((tmp_path / ".trinity" / "observations.json").read_text())
assert obs["document_metadata"]["managed_by"] == "FULLSET"
def test_write_agent_creates_mailbox_and_logs(tmp_path):
"""Mailbox and logs directories created."""
rid, fname = _write_registry(tmp_path, "dirs")
_write_agent(tmp_path, "dirs", rid, fname)
assert (tmp_path / ".ai_mail.local").is_dir()
assert (tmp_path / "logs").is_dir()
# ---------------------------------------------------------------------------
# create_project — WITH agent
# ---------------------------------------------------------------------------
def test_create_project_with_agent(host_env, monkeypatch):
"""WITH-agent path: full framework agent created."""
monkeypatch.chdir(host_env)
with (
patch("subprocess.run", side_effect=_mock_git_run),
patch(
"aipass.aipass.apps.handlers.init.bootstrap._detect_aipass_home",
return_value=None,
),
patch("aipass.aipass.apps.handlers.init.bootstrap._enroll_project"),
):
result = create_project("withagent", template="empty", no_agent=False)
assert result["agent_created"] is True
target = Path(result["target"])
assert (target / ".trinity" / "passport.json").exists()
assert (target / ".trinity" / "local.json").exists()
assert (target / ".trinity" / "observations.json").exists()
assert (target / "apps" / "withagent.py").exists()
assert (target / "apps" / "modules" / "__init__.py").exists()
assert (target / "apps" / "handlers" / "__init__.py").exists()
assert (target / ".ai_mail.local").is_dir()
assert (target / "logs").is_dir()
passport = json.loads((target / ".trinity" / "passport.json").read_text())
assert passport["citizenship"]["registry_id"] == result["registry_id"]
reg = json.loads((target / result["registry_file"]).read_text())
assert reg["metadata"]["total_branches"] == 1
assert reg["branches"][0]["registry_id"] == result["registry_id"]
def test_create_project_no_agent_flag(host_env, monkeypatch):
"""no_agent=True skips passport and registry seating."""
monkeypatch.chdir(host_env)
with (
patch("subprocess.run", side_effect=_mock_git_run),
patch(
"aipass.aipass.apps.handlers.init.bootstrap._detect_aipass_home",
return_value=None,
),
patch("aipass.aipass.apps.handlers.init.bootstrap._enroll_project"),
):
result = create_project("noagent", template="empty", no_agent=True)
assert result["agent_created"] is False
target = Path(result["target"])
assert not (target / ".trinity").exists()
reg = json.loads((target / result["registry_file"]).read_text())
assert reg["metadata"]["total_branches"] == 0
# ---------------------------------------------------------------------------
# is_projects_child (guard relaxation)
# ---------------------------------------------------------------------------
def test_is_projects_child_valid(tmp_path):
from aipass.aipass.apps.handlers.init.bootstrap import is_projects_child
(tmp_path / "AIPASS_REGISTRY.json").write_text("{}")
target = tmp_path / "projects" / "myapp"
target.mkdir(parents=True)
assert is_projects_child(target) is True
def test_is_projects_child_not_in_projects(tmp_path):
from aipass.aipass.apps.handlers.init.bootstrap import is_projects_child
(tmp_path / "AIPASS_REGISTRY.json").write_text("{}")
target = tmp_path / "elsewhere" / "myapp"
target.mkdir(parents=True)
assert is_projects_child(target) is False
def test_is_projects_child_no_host_registry(tmp_path):
from aipass.aipass.apps.handlers.init.bootstrap import is_projects_child
target = tmp_path / "projects" / "myapp"
target.mkdir(parents=True)
assert is_projects_child(target) is False
# ---------------------------------------------------------------------------
# _guard_init relaxation
# ---------------------------------------------------------------------------
def test_guard_init_blocks_nested_by_default(tmp_path):
from aipass.aipass.apps.handlers.init.bootstrap import _guard_init
(tmp_path / "AIPASS_REGISTRY.json").write_text("{}")
target = tmp_path / "projects" / "nested"
target.mkdir(parents=True)
with pytest.raises(RuntimeError, match="inside AIPass project"):
_guard_init(target)
def test_guard_init_allows_nested_with_flag(tmp_path):
from aipass.aipass.apps.handlers.init.bootstrap import _guard_init
(tmp_path / "AIPASS_REGISTRY.json").write_text("{}")
target = tmp_path / "projects" / "nested"
target.mkdir(parents=True)
_guard_init(target, allow_projects_child=True)
def test_guard_init_still_blocks_non_projects_nested(tmp_path):
from aipass.aipass.apps.handlers.init.bootstrap import _guard_init
(tmp_path / "AIPASS_REGISTRY.json").write_text("{}")
target = tmp_path / "elsewhere" / "nested"
target.mkdir(parents=True)
with pytest.raises(RuntimeError, match="inside AIPass project"):
_guard_init(target, allow_projects_child=True)
# ---------------------------------------------------------------------------
# Module handle_command
# ---------------------------------------------------------------------------
def test_module_handles_new_command():
from aipass.aipass.apps.modules.new_project import handle_command
assert handle_command("notmine", []) is False
def test_module_handles_help():
from aipass.aipass.apps.modules.new_project import handle_command
assert handle_command("new", ["--help"]) is True
def test_module_handles_no_args():
from aipass.aipass.apps.modules.new_project import handle_command
assert handle_command("new", []) is True
# ---------------------------------------------------------------------------
# Interactive prompts
# ---------------------------------------------------------------------------
def test_prompt_template_default():
from aipass.aipass.apps.modules.new_project import _prompt_template
with patch("builtins.input", return_value=""):
assert _prompt_template(["empty", "python"]) == "empty"
def test_prompt_template_by_number():
from aipass.aipass.apps.modules.new_project import _prompt_template
with patch("builtins.input", return_value="2"):
assert _prompt_template(["empty", "python"]) == "python"
def test_prompt_template_by_name():
from aipass.aipass.apps.modules.new_project import _prompt_template
with patch("builtins.input", return_value="python"):
assert _prompt_template(["empty", "python"]) == "python"
def test_prompt_template_eof():
from aipass.aipass.apps.modules.new_project import _prompt_template
with patch("builtins.input", side_effect=EOFError):
assert _prompt_template(["empty", "python"]) == "empty"
def test_prompt_agent_default_yes():
from aipass.aipass.apps.modules.new_project import _prompt_agent
with patch("builtins.input", return_value=""):
assert _prompt_agent() is False
def test_prompt_agent_no():
from aipass.aipass.apps.modules.new_project import _prompt_agent
with patch("builtins.input", return_value="n"):
assert _prompt_agent() is True
def test_prompt_agent_eof():
from aipass.aipass.apps.modules.new_project import _prompt_agent
with patch("builtins.input", side_effect=EOFError):
assert _prompt_agent() is False
# ---------------------------------------------------------------------------
# aipass.py entry point help (cli_ux)
# ---------------------------------------------------------------------------
def test_aipass_print_introspection():
from aipass.aipass.apps.aipass import print_introspection
print_introspection([])
def test_aipass_print_help():
from aipass.aipass.apps.aipass import print_help
print_help([])
+21
View File
@@ -18,6 +18,27 @@ drone @api <command> [args]
---
## Quick Start
```bash
# Validate your API key
drone @api validate
# Test the connection
drone @api test
# List available models
drone @api models
# Make an API call
drone @api call "Hello, world" --model anthropic/claude-3.5-sonnet
# Check usage stats
drone @api stats
```
---
## Commands
| Command | Description |
+18
View File
@@ -213,6 +213,24 @@ def print_help():
console.print("─" * 70)
console.print()
console.print("[bold cyan]EXAMPLES:[/bold cyan]")
console.print()
console.print(" [yellow]Credentials:[/yellow]")
console.print(" [dim]drone @api get-key[/dim] [dim]# Show OpenRouter key[/dim]")
console.print(" [dim]drone @api validate[/dim] [dim]# Validate OpenRouter key[/dim]")
console.print(" [dim]drone @api validate google[/dim] [dim]# Validate Google OAuth2[/dim]")
console.print()
console.print(" [yellow]Models & calls:[/yellow]")
console.print(" [dim]drone @api models --all[/dim] [dim]# List all models[/dim]")
console.print(' [dim]drone @api call "Summarize this" --model anthropic/claude-3.5-sonnet[/dim]')
console.print()
console.print(" [yellow]Usage tracking:[/yellow]")
console.print(" [dim]drone @api stats[/dim] [dim]# Overall usage stats[/dim]")
console.print(" [dim]drone @api caller-usage flow[/dim] [dim]# Usage by caller[/dim]")
console.print()
console.print("─" * 70)
console.print()
console.print(
"[dim]Commands: get-key, get-secret, validate, test, models, status, call,"
" list-providers, init, track, stats, session, caller-usage, cleanup[/dim]"
+21
View File
@@ -79,6 +79,27 @@ All 11 commands are auto-discovered by the entry point router.
---
## Quick Start
```bash
# Register a project for backup
drone @backup register /path/to/project --name myapp
# Full mirror snapshot
drone @backup snapshot @myapp
# Incremental timestamped backup
drone @backup versioned @myapp
# Check backup status
drone @backup status @myapp
# List available versions of a file
drone @backup restore @myapp list src/main.py
```
---
## `.backup/` Store Structure
Each registered project gets a `.backup/` directory at its root:
+21 -1
View File
@@ -42,7 +42,7 @@ def print_introspection(modules: list[Any]) -> None:
console.print()
console.print(f"[bold cyan]BACKUP[/bold cyan] v{VERSION} — project backup & drive sync")
console.print()
console.print(f"[yellow]Discovered Modules:[/yellow] {len(modules)}")
console.print(f"[bold dim]Discovered Modules:[/bold dim] {len(modules)}")
console.print()
for module in modules:
name = module.__name__.split(".")[-1]
@@ -83,6 +83,26 @@ def print_help() -> None:
console.print(" [green]share[/green] Upload a single file to Drive + get a shareable link")
console.print(" [green]drive_clear[/green] Clear backups from the remote drive")
console.print()
console.print("-" * 70)
console.print()
console.print("[bold cyan]EXAMPLES:[/bold cyan]")
console.print()
console.print(" [bold dim]Register & back up:[/bold dim]")
console.print(" [dim]drone @backup register /path/to/project --name myapp[/dim]")
console.print(" [dim]drone @backup snapshot @myapp[/dim]")
console.print(" [dim]drone @backup versioned @myapp[/dim]")
console.print(" [dim]drone @backup all @myapp[/dim]")
console.print()
console.print(" [bold dim]Status & restore:[/bold dim]")
console.print(" [dim]drone @backup status @myapp[/dim]")
console.print(" [dim]drone @backup restore @myapp list src/main.py[/dim]")
console.print(" [dim]drone @backup restore @myapp file src/main.py ./restored.py[/dim]")
console.print()
console.print(" [bold dim]Drive sync:[/bold dim]")
console.print(" [dim]drone @backup drive_check @myapp[/dim]")
console.print(" [dim]drone @backup drive_sync @myapp[/dim]")
console.print(" [dim]drone @backup share report.pdf --public[/dim]")
console.print()
def discover_modules() -> list[Any]:
+10 -1
View File
@@ -7,7 +7,16 @@
**Version:** 2.1.0
**Seedgo:** 99%
**Tests:** 127 passing (5 files)
**Last Updated:** 2026-05-16
**Last Updated:** 2026-07-17
## Quick Start
```bash
drone @cli # Show discovered modules
drone @cli display demo # Run display function showcase
drone @cli templates demo # Run operation template showcase
drone @cli --help # Full usage guide
```
## Usage
+87 -78
View File
@@ -37,7 +37,7 @@ from rich.panel import Panel
from rich import box
# CLI modules (showcasing our own services!)
from aipass.cli.apps.modules.display import console as CONSOLE, header, error
from aipass.cli.apps.modules.display import console, header, error
VERSION = "2.1.0"
CLI_ROOT = Path(__file__).parent
@@ -110,37 +110,37 @@ def print_introspection() -> None:
command_modules = [m for m in modules if getattr(m, "__name__", "").split(".")[-1] not in SERVICE_MODULES]
service_modules = [m for m in modules if getattr(m, "__name__", "").split(".")[-1] in SERVICE_MODULES]
CONSOLE.print()
CONSOLE.print("[bold cyan]CLI - Command Line Interface Branch[/bold cyan]")
CONSOLE.print(f" Version: {VERSION}")
CONSOLE.print()
CONSOLE.print("[dim]Universal Display & Output Service Provider[/dim]")
CONSOLE.print()
console.print()
console.print("[bold cyan]CLI - Command Line Interface Branch[/bold cyan]")
console.print(f" Version: {VERSION}")
console.print()
console.print("[dim]Universal Display & Output Service Provider[/dim]")
console.print()
# Discovered command modules
CONSOLE.print(f"[yellow]Discovered Modules:[/yellow] {len(command_modules)}")
console.print(f"[yellow]Discovered Modules:[/yellow] {len(command_modules)}")
for module in command_modules:
name = getattr(module, "__name__", "unknown").split(".")[-1]
desc = (module.__doc__ or "").strip().split("\n")[0] if module.__doc__ else "No description"
CONSOLE.print(f" [cyan]\u2022[/cyan] {name} \u2014 {desc}")
console.print(f" [cyan]\u2022[/cyan] {name} \u2014 {desc}")
if not command_modules:
CONSOLE.print(" [dim]No command modules discovered[/dim]")
CONSOLE.print()
console.print(" [dim]No command modules discovered[/dim]")
console.print()
# Service modules (import-only, but with utility commands)
if service_modules:
CONSOLE.print(f"[yellow]Services:[/yellow] {len(service_modules)}")
console.print(f"[yellow]Services:[/yellow] {len(service_modules)}")
for module in service_modules:
name = getattr(module, "__name__", "unknown").split(".")[-1]
desc = (module.__doc__ or "").strip().split("\n")[0] if module.__doc__ else "No description"
CONSOLE.print(f" [cyan]\u2022[/cyan] {name} \u2014 {desc}")
CONSOLE.print()
console.print(f" [cyan]\u2022[/cyan] {name} \u2014 {desc}")
console.print()
CONSOLE.print("[yellow]Next:[/yellow] Explore a module")
CONSOLE.print(" [green]drone @cli display[/green] [dim]# Display module info[/dim]")
CONSOLE.print(" [green]drone @cli display demo[/green] [dim]# Run display showcase[/dim]")
CONSOLE.print(" [green]drone @cli --help[/green] [dim]# Full usage guide[/dim]")
CONSOLE.print()
console.print("[yellow]Next:[/yellow] Explore a module")
console.print(" [green]drone @cli display[/green] [dim]# Display module info[/dim]")
console.print(" [green]drone @cli display demo[/green] [dim]# Run display showcase[/dim]")
console.print(" [green]drone @cli --help[/green] [dim]# Full usage guide[/dim]")
console.print()
def print_help() -> None:
@@ -149,45 +149,54 @@ def print_help() -> None:
Shows COMMANDS, EXAMPLES, full reference.
Follows seedgo help pattern.
"""
CONSOLE.print()
console.print()
header("CLI - Display & Templates Service Provider")
CONSOLE.print("[dim]Universal display and output formatting for all AIPass branches[/dim]")
CONSOLE.print()
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
console.print("[dim]Universal display and output formatting for all AIPass branches[/dim]")
console.print()
console.print("\u2500" * 70)
console.print()
# Usage
console.print("[bold cyan]USAGE:[/bold cyan]")
console.print()
console.print(" [dim]drone @cli <command> [args...][/dim]")
console.print(" [dim]drone @cli --help[/dim]")
console.print()
console.print("\u2500" * 70)
console.print()
# What is CLI
CONSOLE.print("[bold cyan]WHAT IS CLI?[/bold cyan]")
CONSOLE.print()
CONSOLE.print("CLI is the [bold]Display & Templates Service[/bold] - like Prax for logging:")
CONSOLE.print(" [green]\u2713[/green] Centralized display formatting (headers, tables, panels)")
CONSOLE.print(" [green]\u2713[/green] Reusable templates for common operations")
CONSOLE.print(" [green]\u2713[/green] Rich library integration for beautiful output")
CONSOLE.print(" [green]\u2713[/green] Consistent styling across all AIPass branches")
CONSOLE.print()
CONSOLE.print("Update CLI once \u2192 All branches instantly benefit from improvements")
CONSOLE.print()
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
console.print("[bold cyan]WHAT IS CLI?[/bold cyan]")
console.print()
console.print("CLI is the [bold]Display & Templates Service[/bold] - like Prax for logging:")
console.print(" [green]\u2713[/green] Centralized display formatting (headers, tables, panels)")
console.print(" [green]\u2713[/green] Reusable templates for common operations")
console.print(" [green]\u2713[/green] Rich library integration for beautiful output")
console.print(" [green]\u2713[/green] Consistent styling across all AIPass branches")
console.print()
console.print("Update CLI once \u2192 All branches instantly benefit from improvements")
console.print()
console.print("\u2500" * 70)
console.print()
# Commands
CONSOLE.print("[bold cyan]COMMANDS:[/bold cyan]")
CONSOLE.print()
CONSOLE.print(" [green]drone @cli[/green] [dim]# Show discovered modules[/dim]")
CONSOLE.print(" [green]drone @cli display[/green] [dim]# Display module info[/dim]")
CONSOLE.print(" [green]drone @cli display demo[/green] [dim]# Run display demo[/dim]")
CONSOLE.print(" [green]drone @cli templates[/green] [dim]# Templates module info[/dim]")
CONSOLE.print(" [green]drone @cli templates demo[/green] [dim]# Run templates demo[/dim]")
CONSOLE.print(" [green]drone @cli --help[/green] [dim]# This help message[/dim]")
CONSOLE.print()
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
console.print("[bold cyan]COMMANDS:[/bold cyan]")
console.print()
console.print(" [green]drone @cli[/green] [dim]# Show discovered modules[/dim]")
console.print(" [green]drone @cli display[/green] [dim]# Display module info[/dim]")
console.print(" [green]drone @cli display demo[/green] [dim]# Run display demo[/dim]")
console.print(" [green]drone @cli templates[/green] [dim]# Templates module info[/dim]")
console.print(" [green]drone @cli templates demo[/green] [dim]# Run templates demo[/dim]")
console.print(" [green]drone @cli --help[/green] [dim]# This help message[/dim]")
console.print()
console.print("\u2500" * 70)
console.print()
# Public services
CONSOLE.print("[bold cyan]PUBLIC SERVICES (apps/modules/):[/bold cyan]")
CONSOLE.print()
console.print("[bold cyan]PUBLIC SERVICES (apps/modules/):[/bold cyan]")
console.print()
services_table = Table(show_header=True, header_style="bold cyan", border_style="dim")
services_table.add_column("Module", style="green")
@@ -199,34 +208,34 @@ def print_help() -> None:
)
services_table.add_row("templates", "operation_start(), operation_complete()", "Standard operation patterns")
CONSOLE.print(services_table)
CONSOLE.print()
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
console.print(services_table)
console.print()
console.print("\u2500" * 70)
console.print()
# Import examples
CONSOLE.print("[bold cyan]HOW TO IMPORT CLI SERVICES:[/bold cyan]")
CONSOLE.print()
console.print("[bold cyan]HOW TO IMPORT CLI SERVICES:[/bold cyan]")
console.print()
CONSOLE.print("[yellow]Display functions:[/yellow]")
CONSOLE.print("[dim] from aipass.cli.apps.modules.display import header, success, error, warning[/dim]")
CONSOLE.print()
console.print("[yellow]Display functions:[/yellow]")
console.print("[dim] from aipass.cli.apps.modules.display import header, success, error, warning[/dim]")
console.print()
CONSOLE.print("[yellow]Templates:[/yellow]")
CONSOLE.print("[dim] from aipass.cli.apps.modules.templates import operation_start, operation_complete[/dim]")
CONSOLE.print()
console.print("[yellow]Templates:[/yellow]")
console.print("[dim] from aipass.cli.apps.modules.templates import operation_start, operation_complete[/dim]")
console.print()
CONSOLE.print("[yellow]Rich console:[/yellow]")
CONSOLE.print("[dim] from aipass.cli.apps.modules.display import console[/dim]")
CONSOLE.print("[dim] console.print('[bold]Hello[/bold]') # Rich formatted output[/dim]")
CONSOLE.print()
console.print("[yellow]Rich console:[/yellow]")
console.print("[dim] from aipass.cli.apps.modules.display import console[/dim]")
console.print("[dim] console.print('[bold]Hello[/bold]') # Rich formatted output[/dim]")
console.print()
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
console.print("\u2500" * 70)
console.print()
# Architecture
CONSOLE.print("[bold cyan]ARCHITECTURE:[/bold cyan]")
CONSOLE.print()
console.print("[bold cyan]ARCHITECTURE:[/bold cyan]")
console.print()
arch_text = """[bold]CLI Branch Structure:[/bold]
@@ -240,19 +249,19 @@ def print_help() -> None:
[green]\u2713[/green] Rich library = Underlying formatting engine
- Console, Table, Panel, Columns, Text styling"""
CONSOLE.print(Panel(arch_text, border_style="green", padding=(1, 2), box=box.ROUNDED))
CONSOLE.print()
CONSOLE.print("\u2500" * 70)
CONSOLE.print()
console.print(Panel(arch_text, border_style="green", padding=(1, 2), box=box.ROUNDED))
console.print()
console.print("\u2500" * 70)
console.print()
# Drone compliance — commands line
CONSOLE.print("[dim]Commands: display, templates, demo, --help[/dim]")
CONSOLE.print()
console.print("[dim]Commands: display, templates, demo, --help[/dim]")
console.print()
def show_version():
"""Print version."""
CONSOLE.print(f"CLI v{VERSION}")
console.print(f"CLI v{VERSION}")
# =============================================================================
@@ -306,9 +315,9 @@ if __name__ == "__main__":
sys.exit(main())
except KeyboardInterrupt:
logger.warning("CLI interrupted by user")
CONSOLE.print("\n[yellow]Operation cancelled[/yellow]")
console.print("\n[yellow]Operation cancelled[/yellow]")
sys.exit(0)
except Exception as e:
logger.error(f"CLI error: {e}", exc_info=True)
CONSOLE.print(f"\n[red]Error: {e}[/red]")
error(str(e))
sys.exit(1)
+6 -6
View File
@@ -51,7 +51,7 @@ class TestMainFlow:
"""No args shows introspection and returns 0."""
cons, _get_output = _make_capture_console()
with (
patch.object(cli_module, "CONSOLE", cons),
patch.object(cli_module, "console", cons),
patch.object(display, "CONSOLE", cons),
patch("sys.argv", ["cli"]),
):
@@ -63,7 +63,7 @@ class TestMainFlow:
cons, _get_output = _make_capture_console()
err_cons, _get_err = _make_capture_console()
with (
patch.object(cli_module, "CONSOLE", cons),
patch.object(cli_module, "console", cons),
patch.object(display, "CONSOLE", cons),
patch.object(display, "err_console", err_cons),
patch.object(display, "_TRIGGER", None),
@@ -77,7 +77,7 @@ class TestMainFlow:
"""--version returns 0."""
cons, get_output = _make_capture_console()
with (
patch.object(cli_module, "CONSOLE", cons),
patch.object(cli_module, "console", cons),
patch.object(display, "CONSOLE", cons),
patch("sys.argv", ["cli", "--version"]),
):
@@ -91,7 +91,7 @@ class TestMainFlow:
cons, _get_output = _make_capture_console()
err_cons, get_err = _make_capture_console()
with (
patch.object(cli_module, "CONSOLE", cons),
patch.object(cli_module, "console", cons),
patch.object(display, "CONSOLE", cons),
patch.object(display, "err_console", err_cons),
patch("sys.argv", ["cli", "nonexistent_cmd_xyz"]),
@@ -106,7 +106,7 @@ class TestMainFlow:
cons, _get_output = _make_capture_console()
err_cons, _get_err = _make_capture_console()
with (
patch.object(cli_module, "CONSOLE", cons),
patch.object(cli_module, "console", cons),
patch.object(display, "CONSOLE", cons),
patch.object(display, "err_console", err_cons),
patch.object(display, "_TRIGGER", None),
@@ -125,7 +125,7 @@ class TestMainFlow:
"""cli_entry() is the console_scripts entry point — verify it's callable."""
cons, _get_output = _make_capture_console()
with (
patch.object(cli_module, "CONSOLE", cons),
patch.object(cli_module, "console", cons),
patch.object(display, "CONSOLE", cons),
patch("sys.argv", ["aipass", "--version"]),
pytest.raises(SystemExit) as exc_info,
+14
View File
@@ -10,6 +10,20 @@
---
## Quick Start
```bash
drone @daemon # Show discovered modules
drone @daemon update # Status digest
drone @daemon activity # Quick 24h activity summary
drone @daemon queue # View pending scheduled jobs
drone @daemon run # Fire all due jobs now
drone @daemon branch-health DAEMON # Deep dive on a branch
drone @daemon install-timer # Enable systemd 2-min timer
```
---
## Overview
Builder citizen -- full 3-layer architecture with identity and memory. DAEMON serves as the background orchestration branch: it discovers modules at startup, routes CLI commands to them, and provides introspection and help output via Rich console.
+15 -1
View File
@@ -162,8 +162,22 @@ def print_help(modules: List[Any]):
console.print("-" * 70)
console.print()
console.print("[bold cyan]EXAMPLES:[/bold cyan]")
console.print()
console.print(" [yellow]Status & monitoring:[/yellow]")
console.print(" [dim]drone @daemon update[/dim] [dim]# Status digest[/dim]")
console.print(" [dim]drone @daemon activity[/dim] [dim]# Quick 24h summary[/dim]")
console.print(" [dim]drone @daemon activity-report --json[/dim] [dim]# Full report (raw)[/dim]")
console.print(" [dim]drone @daemon branch-health DAEMON[/dim] [dim]# Single branch dive[/dim]")
console.print()
console.print(" [yellow]Scheduler:[/yellow]")
console.print(" [dim]drone @daemon queue[/dim] [dim]# View pending jobs[/dim]")
console.print(" [dim]drone @daemon run[/dim] [dim]# Fire due jobs now[/dim]")
console.print(" [dim]drone @daemon install-timer[/dim] [dim]# Enable systemd timer[/dim]")
console.print()
console.print("[bold]TIP:[/bold] For module-specific help:")
console.print(" [dim]daemon <command> --help[/dim]")
console.print(" [dim]drone @daemon <command> --help[/dim]")
console.print()
+12
View File
@@ -15,6 +15,18 @@ DevPulse handles the day-to-day: working with the user to plan, design, troubles
| Active plans | `drone @flow list open` |
| Branch list | `drone systems` |
## Quick Start
```bash
# Talk to the hub — it picks up where the last session left off
cd src/aipass/devpulse
claude
# Or drive it via drone from anywhere in AIPass
drone @devpulse compass query "registry" # search rated decisions
drone @devpulse feedback inbox # cross-project feedback
```
## Invoke
```bash
+8
View File
@@ -89,11 +89,14 @@ def print_introspection():
"""Print branch introspection — discovered modules and capabilities."""
modules = discover_modules()
console.print("[bold cyan]DEVPULSE[/bold cyan] — Orchestration Hub")
console.print("[dim]The user's primary collaborator — design, plan, dispatch, track[/dim]")
console.print(f" Modules discovered: {len(modules)}")
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()
console.print("Run 'drone @devpulse --help' for usage information")
def print_help():
@@ -112,6 +115,11 @@ def print_help():
console.print("[bold]FLAGS:[/bold]")
console.print(" --help, -h Show this help message")
console.print(" --version, -V Show version")
console.print()
console.print("[bold]EXAMPLES:[/bold]")
console.print(' drone @devpulse compass query "registry" Search rated decisions')
console.print(" drone @devpulse watchdog agent @flow Watch a dispatched agent")
console.print(" drone @devpulse feedback inbox Check cross-project feedback")
def route_command(command: str, args: list[str], modules: list[Any]) -> bool:
+11
View File
@@ -21,6 +21,17 @@
---
## Quick Start
```bash
drone systems # See all registered branches
drone @seedgo audit aipass # Route a command to a branch
drone @flow --help # Show help for any branch
drone scan @memory # Discover available commands
```
---
## Commands / Usage
Drone provides a CLI for terminal use and a Python API for programmatic access.
+9 -9
View File
@@ -93,7 +93,7 @@ def _discover_modules() -> list[tuple[str, str]]:
# =============================================================================
def show_help() -> None:
def print_help() -> None:
"""Display drone help with Rich formatting."""
table = Table(show_header=False, box=None, pad_edge=False, show_edge=False)
table.add_column(style="cyan", no_wrap=True)
@@ -116,22 +116,22 @@ def show_help() -> None:
console.print()
console.print("[dim]Routes commands to registered AIPass branches and modules.[/dim]")
console.print()
console.print("[bold cyan]USAGE:[/bold cyan]")
console.print()
console.print(" [dim]drone @<target> <command> [args...][/dim]")
console.print(" [dim]drone <built-in> [args...][/dim]")
console.print(" [dim]drone --help[/dim]")
console.print()
console.print(table)
console.print()
console.print("[bold]Examples:[/bold]")
console.print("[bold cyan]EXAMPLES:[/bold cyan]")
console.print()
console.print(" [green]drone @seedgo audit aipass[/green]")
console.print(" [green]drone @flow status[/green]")
console.print(" [green]drone systems[/green]")
console.print(" [green]drone activate @seedgo[/green]")
console.print(" [green]drone audit[/green] [dim](custom shortcut)[/dim]")
console.print()
def print_help() -> None:
"""Alias for seedgo standard compliance (audit expects print_help)."""
show_help()
def print_introspection() -> None:
"""Display branch overview — auto-discovers modules."""
console.print()
+12
View File
@@ -26,6 +26,18 @@ Flow is AIPass's plan management system. Every branch uses flow to create, track
---
## Quick Start
```bash
drone @flow create . "My task description" # Create a plan in the current directory
drone @flow list open # See all open plans
drone @flow close FPLAN-0042 # Close a completed plan
drone @flow create . "Design topic" dplan # Create a design plan (DPLAN)
drone @flow templates # List available plan types
```
---
## Commands
```bash
+13 -3
View File
@@ -2,15 +2,25 @@
# Hooks
> Hook infrastructure for AIPass. Single engine dispatches all hooks across platforms (Claude, Codex) with per-project config, full logging, and crash isolation. The 13th citizen.
> Hook infrastructure for AIPass. A single dispatch engine routes hook events across platforms (Claude Code, Codex) with per-project configuration, full logging, and crash isolation.
Every hook event flows through one engine. Platform bridges normalize the event format, the engine reads per-project config (`.aipass/hooks.json`), dispatches matching handlers, and logs everything to prax + JSONL.
Every hook event flows through one engine. Platform bridges normalize the event format, the engine reads per-project config (`.aipass/hooks.json`), dispatches matching handlers, and logs everything to JSONL diagnostics.
## Quick Start
```bash
drone @hooks status # Show hook config for current project
drone @hooks log # Tail recent hook activity
drone @hooks engine # Show connected handlers
drone @hooks verify # Cross-check provider ↔ project wiring
drone @hooks --help # Full help reference
```
## Start here
| You want to | Read |
|---|---|
| Identity, memory, session history | [`.trinity/`](.trinity/) |
| Identity, session history | [`.trinity/`](.trinity/) |
| Hook engine design | `DPLAN-0184` |
| Per-project config | `.aipass/hooks.json` |
+58 -60
View File
@@ -33,9 +33,7 @@ if sys.platform == "win32":
_reconfigure(encoding="utf-8", errors="replace")
from aipass.prax.apps.modules.logger import system_logger as logger # noqa: E402
from aipass.cli.apps.modules import err_console # noqa: E402
CONSOLE = err_console
from aipass.cli.apps.modules import console # noqa: E402
# =============================================================================
# MODULE DISCOVERY
@@ -85,86 +83,86 @@ def discover_modules() -> list[Any]:
def print_introspection():
"""Print branch introspection — discovered modules and capabilities."""
modules = discover_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()
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()
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" [cyan]•[/cyan] {name:20} [dim]{desc}[/dim]")
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()
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()
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()
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]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 cyan]COMMANDS:[/bold cyan]")
console.print()
for module in modules:
commands = getattr(module, "HELP_COMMANDS", None)
if commands:
for cmd, desc in commands:
CONSOLE.print(f" [green]{cmd:26}[/green] [dim]{desc}[/dim]")
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(f" [green]{name:26}[/green] [dim]{desc}[/dim]")
CONSOLE.print()
CONSOLE.print("─" * 70)
CONSOLE.print()
console.print()
console.print("─" * 70)
console.print()
CONSOLE.print("[bold cyan]BRIDGES:[/bold cyan]")
CONSOLE.print()
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()
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]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 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()
def route_command(command: str, args: list[str], modules: list[Any]) -> bool:
@@ -192,7 +190,7 @@ def handle_command(command: str, args: list) -> bool:
return True
if command in ["--version", "-V"]:
CONSOLE.print("hooks 1.1.0")
console.print("hooks 1.1.0")
return True
return route_command(command, args, modules)
@@ -209,7 +207,7 @@ def main() -> int:
if handle_command(args[0], args[1:]):
return 0
CONSOLE.print(f"Unknown command: {args[0]}. Try: drone @hooks --help")
console.print(f"Unknown command: {args[0]}. Try: drone @hooks --help")
return 1
+15 -15
View File
@@ -375,8 +375,8 @@ class TestHooksEntryPoint:
print_introspection()
captured = capsys.readouterr()
assert "HOOKS" in captured.err
assert "Discovered Modules" in captured.err
assert "HOOKS" in captured.out
assert "Discovered Modules" in captured.out
def test_handle_command_returns_bool(self):
from aipass.hooks.apps.hooks import handle_command
@@ -592,35 +592,35 @@ class TestCliRouting:
print_help()
captured = capsys.readouterr()
assert "HOOKS" in captured.err
assert "drone @hooks" in captured.err
assert "HOOKS" in captured.out
assert "drone @hooks" in captured.out
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
assert "hooksound on" in captured.out
assert "hooksound off" in captured.out
assert "status" in captured.out
assert "log" in captured.out
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
assert "EXAMPLES" in captured.out
assert "drone @hooks status" in captured.out
assert "drone @hooks hooksound off" in captured.out
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
assert "USAGE" in captured.out
assert "drone @hooks <command>" in captured.out
def test_help_commands_auto_discovered(self, capsys):
from aipass.hooks.apps.hooks import print_help
@@ -631,7 +631,7 @@ class TestCliRouting:
print_help()
captured = capsys.readouterr()
for cmd, _ in hs_cmds + hst_cmds + eng_cmds:
assert cmd in captured.err
assert cmd in captured.out
def test_output_capture_status(self, capsys):
from aipass.hooks.apps.hooks import handle_command
@@ -647,7 +647,7 @@ class TestCliRouting:
with patch("sys.argv", ["hooks", "--version"]):
main()
captured = capsys.readouterr()
assert "1.1.0" in captured.err
assert "1.1.0" in captured.out
class TestConfigDataContracts:
+12
View File
@@ -8,6 +8,18 @@
---
## Quick Start
```bash
drone @memory search "query" # Search archived memories across all branches
drone @memory rollover status # Show what needs archiving per branch
drone @memory rollover check # Dry run — preview pending rollovers
drone @memory lint # Audit .trinity entries for limit violations
drone @memory watch # Auto-rollover watcher (Ctrl+C to stop)
```
---
## Commands
```bash
+6
View File
@@ -117,6 +117,12 @@ def print_help():
console.print()
console.print("[bold cyan]PRAX - System-Wide Logging Infrastructure[/bold cyan]")
console.print()
console.print("[dim]Unified logging system for AIPass ecosystem[/dim]")
console.print()
console.print("[bold cyan]Usage:[/bold cyan]")
console.print(" [green]drone @prax <command>[/green] [dim][options][/dim]")
console.print()
console.print("[yellow]Commands:[/yellow]")
console.print(" [cyan]monitor[/cyan] Mission Control - unified real-time monitoring")
+11 -1
View File
@@ -2,13 +2,23 @@
# Seedgo
**Purpose:** Standards compliance platform for AIPass. Audits all 11 core agents against 40 code standards + diagnostics, manages bypass rules, runs proof certification, and provides per-file checklist validation consumed by auto-fix hooks.
**Purpose:** Standards compliance platform for AIPass. Audits all 11 core agents against 42 code standards + diagnostics, manages bypass rules, runs proof certification, and provides per-file checklist validation consumed by the PostToolUse auto-fix gate.
**Module:** `aipass.seedgo`
**Version:** 2.0.0
**Created:** 2026-03-05
---
## Quick Start
```bash
drone @seedgo audit aipass # Audit all branches against all standards
drone @seedgo checklist <file> # Check a single file
drone @seedgo standards_query aipass_standards cli # Look up what a standard checks
```
---
## Overview
### What I Do
@@ -0,0 +1,494 @@
# =================== AIPass ====================
# Name: cli_ux_check.py
# Description: CLI UX Standards Checker Handler
# Version: 1.0.0
# Created: 2026-07-17
# Modified: 2026-07-17
# =============================================
"""
CLI UX Standards Checker Handler
Validates that branch entry points follow the AIPass house pattern for CLI
help and introspection output.
Good entry points (flow.py, prax.py, drone.py, seedgo.py) all have:
- print_introspection() and print_help() as separate two-tier functions
- Rich console.print() output (no bare print())
- Styled title, purpose/tagline, and --help pointer in introspection
- Usage and Examples sections in help
Checks:
1. two_tier_help - Both print_introspection() and print_help() exist
2. rich_console - Help functions use console.print(), not bare print()
3. title_markup - print_introspection() has a [bold styled title
4. purpose_line - print_introspection() has a [dim] purpose/tagline line
5. help_pointer - print_introspection() references --help
6. usage_section - print_help() includes a Usage section
7. examples_section - print_help() includes an Examples section
8. no_internal_modules - modules/ does not expose internal plumbing files
"""
import ast
from pathlib import Path
from typing import Dict, List, Optional
from aipass.prax import logger
from aipass.seedgo.apps.handlers.json import json_handler
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
# Only check entry points: apps/{branch}.py files
AUDIT_SCOPE = "entry_point"
def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
"""
Check if entry point follows the AIPass CLI UX house pattern.
Args:
module_path: Path to Python module to check
bypass_rules: Optional list of bypass rules to skip specific violations
Returns:
dict: {
'passed': bool, # Overall pass/fail
'checks': [ # Individual check results
{
'name': str, # Check name
'passed': bool, # Pass/fail
'message': str, # Details
}
],
'score': int, # 0-100 percentage
'standard': str # Standard name
}
"""
checks: List[Dict] = []
path = Path(module_path)
# Normalize to forward slashes so string matching works on Windows too
module_path = Path(module_path).as_posix()
# Check if entire standard is bypassed for this file
if is_bypassed(module_path, "cli_ux", bypass_rules=bypass_rules):
return {
"passed": True,
"checks": [{"name": "Bypassed", "passed": True, "message": "Standard bypassed via .seedgo/bypass.json"}],
"score": 100,
"standard": "CLI_UX",
}
# Validate file exists
if not path.exists():
return {
"passed": False,
"checks": [{"name": "File exists", "passed": False, "message": f"File not found: {module_path}"}],
"score": 0,
"standard": "CLI_UX",
}
# Skip __init__.py files
if path.name == "__init__.py":
return {
"passed": True,
"checks": [{"name": "CLI UX check", "passed": True, "message": "__init__.py skipped"}],
"score": 100,
"standard": "CLI_UX",
}
# Skip non-entry-point files (entry points live at apps/{name}.py)
if not _is_entry_point(module_path, path):
return {
"passed": True,
"checks": [
{
"name": "CLI UX check",
"passed": True,
"message": "Not an entry point file (not applicable)",
}
],
"score": 100,
"standard": "CLI_UX",
}
# Read file
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
except Exception as e:
logger.info("Cannot read %s: %s", path, e)
return {
"passed": False,
"checks": [{"name": "File readable", "passed": False, "message": f"Error reading file: {e}"}],
"score": 0,
"standard": "CLI_UX",
}
# Empty file
if not content.strip():
return {
"passed": True,
"checks": [{"name": "CLI UX check", "passed": True, "message": "Empty file skipped"}],
"score": 100,
"standard": "CLI_UX",
}
# Parse AST
try:
tree = ast.parse(content, filename=module_path)
except SyntaxError as e:
logger.info("Skipped %s: SyntaxError during parse", path)
return {
"passed": False,
"checks": [{"name": "File parseable", "passed": False, "message": f"Syntax error: {e}"}],
"score": 0,
"standard": "CLI_UX",
}
# Find the two key functions via AST
introspection_func = _find_function(tree, "print_introspection")
help_func = _find_function(tree, "print_help")
# --- Check 1: two_tier_help ---
checks.append(_check_two_tier_help(tree, path.name))
# --- Check 2: rich_console ---
checks.append(_check_rich_console(introspection_func, help_func, path.name))
# --- Check 3: title_markup ---
checks.append(_check_title_markup(introspection_func, path.name))
# --- Check 4: purpose_line ---
checks.append(_check_purpose_line(introspection_func, path.name))
# --- Check 5: help_pointer ---
checks.append(_check_help_pointer(introspection_func, path.name))
# --- Check 6: usage_section ---
checks.append(_check_usage_section(help_func, path.name))
# --- Check 7: examples_section ---
checks.append(_check_examples_section(help_func, path.name))
# --- Check 8: no_internal_modules ---
checks.append(_check_no_internal_modules(module_path, path))
# Calculate score
passed_checks = sum(1 for check in checks if check["passed"])
total_checks = len(checks)
score = int((passed_checks / total_checks * 100)) if total_checks > 0 else 0
# Overall pass = ALL checks passed
overall_passed = all(check["passed"] for check in checks)
json_handler.log_operation("check_completed", {"file": str(module_path), "score": score, "standard": "cli_ux"})
return {"passed": overall_passed, "checks": checks, "score": score, "standard": "CLI_UX"}
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _is_entry_point(module_path: str, path: Path) -> bool:
"""
Detect if file is an entry point: apps/{name}.py (directly in apps/, not in subdirectory).
Entry points live at apps/{name}.py -- their parent directory is 'apps'.
Files in apps/modules/, apps/handlers/, apps/plugins/ etc. are NOT entry points.
"""
if not path.name.endswith(".py"):
return False
posix_path = Path(module_path).as_posix()
if "apps/" not in posix_path:
return False
return path.parent.name == "apps"
def _find_function(tree: ast.Module, name: str) -> Optional[ast.FunctionDef]:
"""Find a top-level function definition by name."""
for node in tree.body:
if isinstance(node, ast.FunctionDef) and node.name == name:
return node
return None
def _collect_string_constants(func_node: ast.FunctionDef) -> List[str]:
"""
Extract all string constants from a function body, including strings
inside f-strings (ast.JoinedStr -> ast.Constant).
"""
strings: List[str] = []
for node in ast.walk(func_node):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
strings.append(node.value)
elif isinstance(node, ast.JoinedStr):
# f-string: extract string parts
for part in node.values:
if isinstance(part, ast.Constant) and isinstance(part.value, str):
strings.append(part.value)
return strings
# ---------------------------------------------------------------------------
# Individual checks
# ---------------------------------------------------------------------------
def _check_two_tier_help(tree: ast.Module, filename: str) -> Dict:
"""Check 1: Entry point has BOTH print_introspection AND print_help."""
found = set()
for node in tree.body:
if isinstance(node, ast.FunctionDef) and node.name in ("print_introspection", "print_help"):
found.add(node.name)
missing = {"print_introspection", "print_help"} - found
if not missing:
return {
"name": "two_tier_help",
"passed": True,
"message": f"Both print_introspection() and print_help() found in {filename}",
}
missing_str = ", ".join(sorted(missing))
return {
"name": "two_tier_help",
"passed": False,
"message": f"Entry point must define both print_introspection() and print_help() — {missing_str} not found",
}
def _check_rich_console(
introspection_func: Optional[ast.FunctionDef],
help_func: Optional[ast.FunctionDef],
filename: str,
) -> Dict:
"""Check 2: Help functions use console.print(), not bare print()."""
funcs_to_check = []
if introspection_func is not None:
funcs_to_check.append(("print_introspection", introspection_func))
if help_func is not None:
funcs_to_check.append(("print_help", help_func))
if not funcs_to_check:
# Neither function exists -- auto-fail (two_tier_help already catches the root cause)
return {
"name": "rich_console",
"passed": False,
"message": "Help functions must use console.print() from aipass.cli, not bare print()",
}
has_console_print = False
has_bare_print = False
for _func_name, func_node in funcs_to_check:
for node in ast.walk(func_node):
if not isinstance(node, ast.Call):
continue
# console.print() — Attribute call where value.id == 'console' and attr == 'print'
if isinstance(node.func, ast.Attribute):
if (
node.func.attr == "print"
and isinstance(node.func.value, ast.Name)
and node.func.value.id == "console"
):
has_console_print = True
# bare print() — Name call where id == 'print'
if isinstance(node.func, ast.Name) and node.func.id == "print":
has_bare_print = True
if has_bare_print:
return {
"name": "rich_console",
"passed": False,
"message": "Help functions must use console.print() from aipass.cli, not bare print()",
}
if not has_console_print:
return {
"name": "rich_console",
"passed": False,
"message": "Help functions must use console.print() from aipass.cli, not bare print()",
}
return {
"name": "rich_console",
"passed": True,
"message": f"Help functions in {filename} use console.print() (no bare print())",
}
def _check_title_markup(introspection_func: Optional[ast.FunctionDef], filename: str) -> Dict:
"""Check 3: print_introspection contains a [bold styled title."""
if introspection_func is None:
return {
"name": "title_markup",
"passed": False,
"message": (
"print_introspection() must include a styled title line (e.g. [bold cyan]Branch Name[/bold cyan])"
),
}
strings = _collect_string_constants(introspection_func)
for s in strings:
if "[bold" in s:
return {
"name": "title_markup",
"passed": True,
"message": f"print_introspection() in {filename} has a styled title with [bold markup",
}
return {
"name": "title_markup",
"passed": False,
"message": "print_introspection() must include a styled title line (e.g. [bold cyan]Branch Name[/bold cyan])",
}
def _check_purpose_line(introspection_func: Optional[ast.FunctionDef], filename: str) -> Dict:
"""Check 4: print_introspection contains a [dim] purpose/tagline line."""
if introspection_func is None:
return {
"name": "purpose_line",
"passed": False,
"message": "print_introspection() must include a dim-styled purpose/tagline line",
}
strings = _collect_string_constants(introspection_func)
for s in strings:
if "[dim]" in s:
return {
"name": "purpose_line",
"passed": True,
"message": f"print_introspection() in {filename} has a [dim] purpose/tagline line",
}
return {
"name": "purpose_line",
"passed": False,
"message": "print_introspection() must include a dim-styled purpose/tagline line",
}
def _check_help_pointer(introspection_func: Optional[ast.FunctionDef], filename: str) -> Dict:
"""Check 5: print_introspection contains a closing pointer to --help."""
if introspection_func is None:
return {
"name": "help_pointer",
"passed": False,
"message": "print_introspection() must include a closing pointer to --help for more info",
}
strings = _collect_string_constants(introspection_func)
for s in strings:
if "--help" in s:
return {
"name": "help_pointer",
"passed": True,
"message": f"print_introspection() in {filename} includes a --help pointer",
}
return {
"name": "help_pointer",
"passed": False,
"message": "print_introspection() must include a closing pointer to --help for more info",
}
def _check_usage_section(help_func: Optional[ast.FunctionDef], filename: str) -> Dict:
"""Check 6: print_help contains a Usage section."""
if help_func is None:
return {
"name": "usage_section",
"passed": False,
"message": "print_help() must include a Usage section",
}
strings = _collect_string_constants(help_func)
for s in strings:
if "usage" in s.lower():
return {
"name": "usage_section",
"passed": True,
"message": f"print_help() in {filename} includes a Usage section",
}
return {
"name": "usage_section",
"passed": False,
"message": "print_help() must include a Usage section",
}
def _check_examples_section(help_func: Optional[ast.FunctionDef], filename: str) -> Dict:
"""Check 7: print_help contains an Examples section."""
if help_func is None:
return {
"name": "examples_section",
"passed": False,
"message": "print_help() must include an Examples section",
}
strings = _collect_string_constants(help_func)
for s in strings:
if "example" in s.lower():
return {
"name": "examples_section",
"passed": True,
"message": f"print_help() in {filename} includes an Examples section",
}
return {
"name": "examples_section",
"passed": False,
"message": "print_help() must include an Examples section",
}
def _check_no_internal_modules(module_path: str, path: Path) -> Dict:
"""
Check 8: modules/ directory does not expose internal plumbing files.
Files whose stems end with _wire, _fix, _impl, or _internal should be
underscore-prefixed to hide from discovery.
"""
internal_suffixes = ("_wire", "_fix", "_impl", "_internal")
# Derive the branch root from the entry point path:
# entry point is at {branch_root}/apps/{name}.py -> parent.parent is branch_root
branch_root = path.parent.parent
modules_dir = branch_root / "apps" / "modules"
if not modules_dir.is_dir():
return {
"name": "no_internal_modules",
"passed": True,
"message": "No modules/ directory found (nothing to check)",
}
exposed_internal: List[str] = []
for py_file in modules_dir.glob("*.py"):
stem = py_file.stem
# Skip __init__.py and already-underscore-prefixed files
if stem.startswith("_"):
continue
if any(stem.endswith(suffix) for suffix in internal_suffixes):
exposed_internal.append(py_file.name)
if exposed_internal:
names = ", ".join(sorted(exposed_internal))
return {
"name": "no_internal_modules",
"passed": False,
"message": (
f"modules/ directory exposes internal plumbing: {names} "
f"— prefix with underscore or use COMMAND attribute"
),
}
return {
"name": "no_internal_modules",
"passed": True,
"message": "No exposed internal plumbing files in modules/",
}
@@ -0,0 +1,110 @@
# =================== AIPass ====================
# Name: cli_ux_content.py
# Description: CLI UX Standards Content Handler
# Version: 1.0.0
# Created: 2026-07-17
# Modified: 2026-07-17
# =============================================
"""
CLI UX Standards Content Handler
Provides formatted CLI UX standards content.
Module orchestrates, handler implements.
"""
from aipass.seedgo.apps.handlers.json import json_handler
def get_cli_ux_standards() -> str:
"""Return formatted CLI UX standards content with Rich markup.
Returns:
str: Formatted standards text with Rich styling
"""
lines = [
"[bold red]CLI UX STANDARD[/bold red]",
"",
"[bold cyan]CORE RULE:[/bold cyan] Entry points must follow the AIPass house pattern",
"",
"[yellow]RULE:[/yellow] Every branch entry point (apps/{branch}.py) must provide",
" a two-tier help surface: print_introspection() for bare invocation and",
" print_help() for --help, both using Rich console.print() from aipass.cli.",
"",
"=" * 70,
"",
"[bold cyan]THE HOUSE PATTERN:[/bold cyan]",
"",
" [green]1.[/green] [bold]Two-tier help[/bold] -- print_introspection() (light) + print_help() (full)",
" [green]2.[/green] [bold]Rich console[/bold] -- console.print() with markup, never bare print()",
" [green]3.[/green] [bold]Title + purpose[/bold] -- styled title line + dim tagline in introspection",
" [green]4.[/green] [bold]Closing pointer[/bold] -- introspection ends with --help reference",
" [green]5.[/green] [bold]Usage + examples[/bold] -- print_help() includes USAGE and EXAMPLES sections",
" [green]6.[/green] [bold]No internal leaks[/bold] -- modules/ must not expose internal plumbing",
"",
"=" * 70,
"",
"[bold cyan]WHAT IS CHECKED:[/bold cyan]",
"",
" [yellow]Scope:[/yellow] entry_point -- only apps/{branch}.py files",
"",
" [yellow]Check 1:[/yellow] two_tier_help",
" Both print_introspection() and print_help() defined as top-level functions",
"",
" [yellow]Check 2:[/yellow] rich_console",
" Help functions use console.print(), no bare print() calls",
"",
" [yellow]Check 3:[/yellow] title_markup",
" print_introspection() contains a [bold]-styled title line",
"",
" [yellow]Check 4:[/yellow] purpose_line",
" print_introspection() contains a [dim]-styled purpose/tagline",
"",
" [yellow]Check 5:[/yellow] help_pointer",
" print_introspection() references --help for more info",
"",
" [yellow]Check 6:[/yellow] usage_section",
" print_help() contains a Usage section",
"",
" [yellow]Check 7:[/yellow] examples_section",
" print_help() contains an Examples section",
"",
" [yellow]Check 8:[/yellow] no_internal_modules",
" modules/ directory does not expose internal plumbing names",
"",
"=" * 70,
"",
"[bold cyan]GOOD EXAMPLE (flow.py introspection):[/bold cyan]",
"",
' console.print("[bold cyan]Flow - PLAN Management System[/bold cyan]")',
' console.print("[dim]Task orchestration and workflow management[/dim]")',
" # ... module list ...",
" console.print(\"[dim]Run 'drone @flow --help' for usage information[/dim]\")",
"",
"[bold cyan]BAD EXAMPLE (aipass.py -- fails all checks):[/bold cyan]",
"",
' print(f"AIPASS - {len(modules)} modules discovered") # bare print',
" # no print_introspection/print_help, no title/purpose, no pointer",
"",
"=" * 70,
"",
"[bold cyan]HOW TO FIX:[/bold cyan]",
"",
" 1. Define print_introspection() with: styled title, dim purpose, module list, --help pointer",
" 2. Define print_help() with: USAGE section, command list, EXAMPLES section",
" 3. Replace all bare print() with console.print() from aipass.cli",
" 4. Prefix internal modules with underscore or set COMMAND attribute",
"",
"=" * 70,
"",
"[bold cyan]SCORING:[/bold cyan]",
" Score = (passed_checks / 8) * 100",
" Pass requires all 8 checks green",
"",
"[bold cyan]REFERENCE:[/bold cyan]",
" [dim]See: seedgo standards pack (cli_ux)[/dim]",
" [dim]Compare: drone @flow (good) vs aipass --help (bad)[/dim]",
]
json_handler.log_operation("standard_content_queried", {"standard": "cli_ux"})
return "\n".join(lines)
@@ -0,0 +1,335 @@
# =================== AIPass ====================
# Name: readme_quality_check.py
# Description: README Quality Standards Checker Handler
# Version: 1.0.0
# Created: 2026-07-17
# Modified: 2026-07-17
# =============================================
"""
README Quality Standards Checker Handler
Validates README.md content quality from a stranger's perspective.
Orthogonal to readme_check.py which validates structural completeness.
Checks:
1. Quick Start section with runnable command example
2. Stranger accessibility (not too many unexplained internal references)
3. Invoke/Usage command matches branch name
4. Clear description within first 10 lines
"""
import re
from pathlib import Path
from typing import Dict, List
from aipass.prax import logger
from aipass.seedgo.apps.handlers.json import json_handler
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
# Audit scope: entry points only (apps/{name}.py)
AUDIT_SCOPE = "entry_point"
# Internal AIPass branch names — a stranger would not recognise these
# without context. "aipass" is excluded because it is the project name
# itself and appears unavoidably in every branch description.
_INTERNAL_NAMES = frozenset(
{
"drone",
"devpulse",
"ai_mail",
"flow",
"seedgo",
"prax",
"memory",
"spawn",
"hooks",
"trigger",
"api",
"cli",
"skills",
"daemon",
"commons",
"backup",
}
)
def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
"""
Check if branch README meets content-quality standards.
Args:
module_path: Path to branch entry point (e.g., src/aipass/seedgo/apps/branch.py)
bypass_rules: Optional list of bypass rules to skip certain checks
Returns:
dict: {
'passed': bool,
'checks': [{'name': str, 'passed': bool, 'message': str}],
'score': int,
'standard': str
}
"""
checks: List[Dict] = []
# Skip __init__.py files
if Path(module_path).name == "__init__.py":
return {
"passed": True,
"checks": [{"name": "README quality", "passed": True, "message": "__init__.py file (skipped)"}],
"score": 100,
"standard": "README_QUALITY",
}
# Check if entire standard is bypassed for this file
if is_bypassed(module_path, "readme_quality", bypass_rules=bypass_rules):
return {
"passed": True,
"checks": [{"name": "Bypassed", "passed": True, "message": "Standard bypassed via .seedgo/bypass.json"}],
"score": 100,
"standard": "README_QUALITY",
}
# Derive branch root: module_path is apps/{branch}.py, go up 2 levels
entry_path = Path(module_path)
branch_root = entry_path.parent.parent
branch_name = branch_root.name
# Locate README
readme_path = branch_root / "README.md"
if not readme_path.exists():
fail_msg = "README.md not found at branch root"
for name in ["quick_start", "stranger_accessible", "invoke_match", "what_description"]:
checks.append({"name": name, "passed": False, "message": fail_msg})
return {"passed": False, "checks": checks, "score": 0, "standard": "README_QUALITY"}
# Read README content
try:
content = readme_path.read_text(encoding="utf-8")
lines = content.split("\n")
except Exception as e:
logger.info("Cannot read README at %s: %s", readme_path, e)
return {
"passed": False,
"checks": [{"name": "File readable", "passed": False, "message": f"Error reading README: {e}"}],
"score": 0,
"standard": "README_QUALITY",
}
# Check 1: Quick Start section with code example
checks.append(_check_quick_start(lines, module_path, bypass_rules))
# Check 2: Stranger accessibility
checks.append(_check_stranger_accessible(lines, module_path, bypass_rules))
# Check 3: Invoke/Usage command matches branch
checks.append(_check_invoke_match(lines, branch_name, module_path, bypass_rules))
# Check 4: Clear description within first 10 lines
checks.append(_check_what_description(lines, module_path, bypass_rules))
# Calculate score
passed_checks = sum(1 for c in checks if c["passed"])
total_checks = len(checks)
score = int((passed_checks / total_checks * 100)) if total_checks > 0 else 0
overall_passed = all(c["passed"] for c in checks)
json_handler.log_operation(
"check_completed",
{"file": str(module_path), "score": score, "standard": "readme_quality"},
)
return {"passed": overall_passed, "checks": checks, "score": score, "standard": "README_QUALITY"}
# --------------------------------------------------------------------- #
# Individual checks #
# --------------------------------------------------------------------- #
def _check_quick_start(lines: List[str], file_path: str, bypass_rules: list | None) -> Dict:
"""README must have a Quick Start / Getting Started section with at least one fenced code block."""
if is_bypassed(file_path, "readme_quality", None, bypass_rules):
return {"name": "quick_start", "passed": True, "message": "Bypassed by bypass rules"}
# Find a heading containing "quick start", "getting started", or "quickstart"
qs_pattern = re.compile(r"^(#{1,6})\s+.*(quick\s*start|getting\s+started)", re.IGNORECASE)
section_start: int | None = None
section_level: int = 0
for i, line in enumerate(lines):
m = qs_pattern.match(line)
if m:
section_start = i
section_level = len(m.group(1))
break
if section_start is None:
return {
"name": "quick_start",
"passed": False,
"message": "README must have a Quick Start section with at least one runnable command example",
}
# Scan from section heading to next heading of equal or higher level (or EOF)
has_code_block = False
for j in range(section_start + 1, len(lines)):
stripped = lines[j].strip()
# Next heading at same or higher level ends the section
heading_match = re.match(r"^(#{1,6})\s+", lines[j])
if heading_match and len(heading_match.group(1)) <= section_level:
break
if stripped.startswith("```"):
has_code_block = True
break
if has_code_block:
return {"name": "quick_start", "passed": True, "message": "Quick Start section found with code example"}
return {
"name": "quick_start",
"passed": False,
"message": "README must have a Quick Start section with at least one runnable command example",
}
def _check_stranger_accessible(lines: List[str], file_path: str, bypass_rules: list | None) -> Dict:
"""First 5 content lines must not reference more than 2 unique internal branch names."""
if is_bypassed(file_path, "readme_quality", None, bypass_rules):
return {"name": "stranger_accessible", "passed": True, "message": "Bypassed by bypass rules"}
# Build regex once
names_pattern = re.compile(
r"\b(" + "|".join(re.escape(n) for n in sorted(_INTERNAL_NAMES, key=len, reverse=True)) + r")\b",
re.IGNORECASE,
)
# Collect first 5 non-empty, non-heading lines after the title heading
content_lines: List[str] = []
past_title = False
for line in lines:
stripped = line.strip()
# Skip until we pass the first heading (title)
if not past_title:
if stripped.startswith("#"):
past_title = True
continue
# Skip headings and empty lines
if not stripped or stripped.startswith("#"):
continue
content_lines.append(stripped)
if len(content_lines) >= 5:
break
if not content_lines:
# No content lines found — nothing to flag
return {
"name": "stranger_accessible",
"passed": True,
"message": "No content lines found to check (skipped)",
}
# Count unique internal name references across collected lines
found_names: set = set()
for cl in content_lines:
for m in names_pattern.finditer(cl):
found_names.add(m.group(1).lower())
if len(found_names) > 2:
sorted_names = ", ".join(sorted(found_names))
return {
"name": "stranger_accessible",
"passed": False,
"message": (
f"First paragraph references {len(found_names)} internal branches"
f" ({sorted_names}) — strangers won't understand without context"
),
}
return {
"name": "stranger_accessible",
"passed": True,
"message": f"First paragraph references {len(found_names)} internal branch name(s) (limit 2)",
}
def _check_invoke_match(lines: List[str], branch_name: str, file_path: str, bypass_rules: list | None) -> Dict:
"""If an Invoke / Usage / How to Run section exists, its command should reference the branch name."""
if is_bypassed(file_path, "readme_quality", None, bypass_rules):
return {"name": "invoke_match", "passed": True, "message": "Bypassed by bypass rules"}
# Find heading containing "invoke", "usage", or "how to run"
heading_pattern = re.compile(r"^(#{1,6})\s+.*(invoke|usage|how\s+to\s+run)", re.IGNORECASE)
section_start: int | None = None
section_level: int = 0
for i, line in enumerate(lines):
m = heading_pattern.match(line)
if m:
section_start = i
section_level = len(m.group(1))
break
if section_start is None:
return {
"name": "invoke_match",
"passed": True,
"message": "No invoke section found — skipped",
}
# Extract section content until next heading of equal or higher level
section_content: List[str] = []
for j in range(section_start + 1, len(lines)):
heading_match = re.match(r"^(#{1,6})\s+", lines[j])
if heading_match and len(heading_match.group(1)) <= section_level:
break
section_content.append(lines[j])
section_text = "\n".join(section_content)
# Check if the branch name appears in the section (case-insensitive)
if re.search(re.escape(branch_name), section_text, re.IGNORECASE):
return {
"name": "invoke_match",
"passed": True,
"message": f"Invoke section references branch '{branch_name}'",
}
# Also accept drone @{branch} pattern (standard invocation)
if re.search(r"drone\s+@" + re.escape(branch_name), section_text, re.IGNORECASE):
return {
"name": "invoke_match",
"passed": True,
"message": f"Invoke section uses drone @{branch_name} pattern",
}
return {
"name": "invoke_match",
"passed": False,
"message": f"Invoke section command does not match branch name '{branch_name}'",
}
def _check_what_description(lines: List[str], file_path: str, bypass_rules: list | None) -> Dict:
"""README must describe what the branch does within the first 10 lines."""
if is_bypassed(file_path, "readme_quality", None, bypass_rules):
return {"name": "what_description", "passed": True, "message": "Bypassed by bypass rules"}
# Collect first 10 lines, check for at least one non-heading, non-empty line > 20 chars
for line in lines[:10]:
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if len(stripped) > 20:
return {
"name": "what_description",
"passed": True,
"message": "Description found within first 10 lines",
}
return {
"name": "what_description",
"passed": False,
"message": "README must describe what the branch does within the first 10 lines",
}
@@ -0,0 +1,126 @@
# =================== AIPass ====================
# Name: readme_quality_content.py
# Description: README Quality Standards Content Handler
# Version: 1.0.0
# Created: 2026-07-17
# Modified: 2026-07-17
# =============================================
"""
README Quality Standards Content Handler
Provides formatted README quality standards content.
Module orchestrates, handler implements.
"""
from aipass.seedgo.apps.handlers.json import json_handler
def get_readme_quality_standards() -> str:
"""Return formatted README quality standards content with Rich markup.
Returns:
str: Formatted standards text with Rich styling
"""
lines = [
"[bold red]README QUALITY STANDARD[/bold red]",
"",
"[bold cyan]CORE RULE:[/bold cyan] READMEs must be accessible to strangers",
"",
"[yellow]RULE:[/yellow] AIPass is open source. A stranger cloning the repo should",
" understand what each branch does, how to invoke it, and be able to",
" get started from the README alone -- no insider knowledge required.",
"",
"[dim]Note: This standard checks CONTENT QUALITY. The existing 'readme'",
"standard checks STRUCTURAL completeness (sections, tree, links).",
"Both must pass.[/dim]",
"",
"=" * 70,
"",
"[bold cyan]WHAT IS CHECKED:[/bold cyan]",
"",
" [yellow]Scope:[/yellow] entry_point -- derives README from branch root",
"",
" [yellow]Check 1:[/yellow] quick_start",
" README has a Quick Start / Getting Started section containing",
" at least one fenced code block with a runnable command.",
"",
" [yellow]Check 2:[/yellow] stranger_accessible",
" First 5 content lines must not reference more than 2 internal",
" AIPass branch names. Strangers cannot parse insider jargon.",
"",
" [yellow]Check 3:[/yellow] invoke_match",
" If an Invoke/Usage section exists, the command shown must",
" reference the correct branch name.",
"",
" [yellow]Check 4:[/yellow] what_description",
" README must describe what the branch does within the first",
" 10 lines (at least one content line longer than 20 characters).",
"",
"=" * 70,
"",
"[bold cyan]THE STRANGER TEST:[/bold cyan]",
"",
" Imagine someone who has never seen AIPass before. They clone the",
" repo and open your README. Can they answer:",
"",
" [green]1.[/green] What does this branch do? (what_description)",
" [green]2.[/green] How do I run it? (quick_start + invoke_match)",
" [green]3.[/green] Do I need to know other branches first? (stranger_accessible)",
"",
" If any answer is 'no', the README fails the stranger test.",
"",
"=" * 70,
"",
"[bold cyan]INTERNAL BRANCH NAMES (checked in stranger_accessible):[/bold cyan]",
"",
" drone, devpulse, aipass, ai_mail, flow, seedgo, prax, memory,",
" spawn, hooks, trigger, api, cli, skills, daemon, commons, backup",
"",
" [dim]Using more than 2 of these in your opening lines without context",
" makes the README insider-only.[/dim]",
"",
"=" * 70,
"",
"[bold cyan]GOOD EXAMPLE:[/bold cyan]",
"",
" # Flow",
" Plan lifecycle manager for AIPass. Creates, tracks, and closes",
" development plans across the project.",
"",
" ## Quick Start",
" ```",
" drone @flow create . 'My Plan'",
" drone @flow list",
" ```",
"",
"[bold cyan]BAD EXAMPLE:[/bold cyan]",
"",
" # AIPASS",
" Concierge and librarian for AIPass. Greets new users, walks them",
" through setup via drone, hands off to devpulse, integrates with",
" seedgo, prax, and spawn for system health.",
" [red](5 internal names in opening lines -- stranger cannot follow)[/red]",
"",
"=" * 70,
"",
"[bold cyan]HOW TO FIX:[/bold cyan]",
"",
" 1. Add a Quick Start section with a runnable code block",
" 2. Rewrite opening lines to explain what it does in plain terms",
" 3. Ensure Invoke/Usage section matches the real CLI entry point",
" 4. Write as if the reader has never seen AIPass before",
"",
"=" * 70,
"",
"[bold cyan]SCORING:[/bold cyan]",
" Score = (passed_checks / 4) * 100",
" Pass requires all 4 checks green",
"",
"[bold cyan]REFERENCE:[/bold cyan]",
" [dim]See: seedgo standards pack (readme_quality)[/dim]",
" [dim]Complements: readme standard (structural completeness)[/dim]",
]
json_handler.log_operation("standard_content_queried", {"standard": "readme_quality"})
return "\n".join(lines)
+15
View File
@@ -241,6 +241,21 @@ def print_help() -> None:
console.print("─" * 70)
console.print()
console.print("[yellow]Examples:[/yellow]")
console.print()
console.print(" [dim]# Check a single file against all standards[/dim]")
console.print(" [green]drone @seedgo checklist src/aipass/flow/apps/flow.py[/green]")
console.print()
console.print(" [dim]# Audit a specific branch[/dim]")
console.print(" [green]drone @seedgo audit aipass @flow[/green]")
console.print()
console.print(" [dim]# Look up what a standard checks[/dim]")
console.print(" [green]drone @seedgo standards_query aipass_standards cli[/green]")
console.print()
console.print("─" * 70)
console.print()
# Commands line for drone discovery
console.print(
"[dim]Commands: audit, standards_audit, standards_query, checklist, diagnostics,"
+63
View File
@@ -197,3 +197,66 @@ def test_collect_py_files_empty_branch(tmp_path):
result = _collect_py_files(tmp_path)
assert result == []
# ---------------------------------------------------------------------------
# Tests -- uppercase registry name resolves to lowercase entry point
# ---------------------------------------------------------------------------
def test_uppercase_registry_name_resolves_to_lowercase_entry(tmp_path):
"""Branches with uppercase registry names (BACKUP, HOOKS, etc.) must
resolve to lowercase filesystem paths for entry_file."""
from aipass.seedgo.apps.handlers.audit.discovery import _branches_from_registry
# Create a mock registry with uppercase branch name
branch_dir = tmp_path / "backup"
branch_dir.mkdir()
apps_dir = branch_dir / "apps"
apps_dir.mkdir()
(apps_dir / "backup.py").write_text("def main(): pass\n")
registry = {
"branches": [
{"name": "BACKUP", "path": str(branch_dir)},
]
}
reg_file = tmp_path / "TEST_REGISTRY.json"
import json
reg_file.write_text(json.dumps(registry))
result = _branches_from_registry(reg_file)
assert len(result) == 1
assert result[0]["name"] == "BACKUP"
assert result[0]["entry_file"].endswith("backup.py")
assert "BACKUP.py" not in result[0]["entry_file"]
def test_uppercase_registry_name_no_entry_when_only_uppercase_file(tmp_path):
"""If a branch only has an UPPERCASE.py file (not lowercase), it should not be found."""
from aipass.seedgo.apps.handlers.audit.discovery import _branches_from_registry
branch_dir = tmp_path / "backup"
branch_dir.mkdir()
apps_dir = branch_dir / "apps"
apps_dir.mkdir()
(apps_dir / "BACKUP.py").write_text("def main(): pass\n")
registry = {
"branches": [
{"name": "BACKUP", "path": str(branch_dir)},
]
}
reg_file = tmp_path / "TEST_REGISTRY.json"
import json
reg_file.write_text(json.dumps(registry))
result = _branches_from_registry(reg_file)
# On case-insensitive filesystems (macOS/Windows), BACKUP.py might match backup.py
# On case-sensitive (Linux), no match → empty result
import platform
if platform.system() == "Linux":
assert len(result) == 0
+527
View File
@@ -0,0 +1,527 @@
# =================== AIPass ====================
# Name: test_cli_ux.py
# Description: Tests for cli_ux_check.py
# Version: 1.0.0
# Created: 2026-07-17
# Modified: 2026-07-17
# =============================================
"""Tests for cli_ux_check — CLI UX house pattern detection."""
from pathlib import Path
import pytest
from unittest.mock import MagicMock
@pytest.fixture(autouse=True)
def _mock_infrastructure(monkeypatch):
import sys
mock_logger = MagicMock()
mock_json_handler = MagicMock()
mock_json_handler.log_operation = MagicMock(return_value=True)
prax_mod = MagicMock()
prax_mod.logger = mock_logger
monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod)
json_pkg = MagicMock()
json_pkg.json_handler = mock_json_handler
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json", json_pkg)
json_mod = MagicMock()
json_mod.log_operation = mock_json_handler.log_operation
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json.json_handler", json_mod)
bypass_pkg = MagicMock()
bypass_ignore = MagicMock()
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed as real_is_bypassed
bypass_utils = MagicMock()
bypass_utils.is_bypassed = real_is_bypassed
bypass_pkg.utils = bypass_utils
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg)
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.ignore_handler", bypass_ignore)
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.utils", bypass_utils)
for mod_name in ["aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check"]:
monkeypatch.delitem(sys.modules, mod_name, raising=False)
def _entry_file(tmp_path, source):
apps_dir = tmp_path / "apps"
apps_dir.mkdir()
f = apps_dir / "branch.py"
f.write_text(source)
return str(f)
# ============================================================
# Skip / edge cases
# ============================================================
def test_init_file_skipped(tmp_path):
apps_dir = tmp_path / "apps"
apps_dir.mkdir()
f = apps_dir / "__init__.py"
f.write_text("# init\n")
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(str(f))
assert result["passed"] is True
assert result["score"] == 100
def test_non_entry_point_skipped(tmp_path):
handler_dir = tmp_path / "apps" / "handlers"
handler_dir.mkdir(parents=True)
f = handler_dir / "helper.py"
f.write_text("def main(): pass\n")
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(str(f))
assert result["passed"] is True
assert result["score"] == 100
def test_missing_file():
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module("/nonexistent/apps/branch.py")
assert result["passed"] is False
assert result["score"] == 0
def test_empty_file(tmp_path):
path = _entry_file(tmp_path, "")
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
assert result["passed"] is True
def test_syntax_error(tmp_path):
path = _entry_file(tmp_path, "def broken(:\n")
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
assert result["passed"] is False
# ============================================================
# Good entry point — all checks pass
# ============================================================
GOOD_ENTRY = """
from aipass.cli import console
def print_introspection():
console.print("[bold cyan]Branch Name[/bold cyan]")
console.print("[dim]Does something useful[/dim]")
console.print("[dim]Run --help for more[/dim]")
def print_help():
console.print("[yellow]Usage:[/yellow]")
console.print(" drone @branch command")
console.print("[yellow]Examples:[/yellow]")
console.print(" drone @branch do-stuff")
def main():
pass
"""
def test_good_entry_passes_all(tmp_path):
path = _entry_file(tmp_path, GOOD_ENTRY)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
assert result["passed"] is True
assert result["score"] == 100
names = {c["name"] for c in result["checks"] if c["passed"]}
assert "two_tier_help" in names
assert "rich_console" in names
assert "title_markup" in names
assert "purpose_line" in names
assert "help_pointer" in names
assert "usage_section" in names
assert "examples_section" in names
assert "no_internal_modules" in names
# ============================================================
# Bad entry point — no print_introspection/print_help
# ============================================================
BAD_ENTRY = """
def main():
print("BRANCH - 5 modules discovered")
for m in modules:
print(f" {m}")
"""
def test_bad_entry_fails_two_tier(tmp_path):
path = _entry_file(tmp_path, BAD_ENTRY)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
assert result["passed"] is False
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["two_tier_help"] is False
assert checks["rich_console"] is False
assert checks["title_markup"] is False
# ============================================================
# Bare print in help functions
# ============================================================
BARE_PRINT_ENTRY = """
def print_introspection():
print("[bold cyan]Title[/bold cyan]")
print("[dim]Purpose[/dim]")
print("Run --help for more")
def print_help():
print("Usage: command")
print("Examples: stuff")
"""
def test_bare_print_fails_rich_console(tmp_path):
path = _entry_file(tmp_path, BARE_PRINT_ENTRY)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["two_tier_help"] is True
assert checks["rich_console"] is False
# ============================================================
# Missing title markup
# ============================================================
NO_TITLE_ENTRY = """
from aipass.cli import console
def print_introspection():
console.print("Branch Name")
console.print("[dim]Purpose[/dim]")
console.print("Run --help for more")
def print_help():
console.print("[yellow]Usage:[/yellow]")
console.print("[yellow]Examples:[/yellow]")
"""
def test_missing_title_markup(tmp_path):
path = _entry_file(tmp_path, NO_TITLE_ENTRY)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["title_markup"] is False
assert checks["purpose_line"] is True
assert checks["help_pointer"] is True
# ============================================================
# Missing purpose line
# ============================================================
NO_PURPOSE_ENTRY = """
from aipass.cli import console
def print_introspection():
console.print("[bold cyan]Branch Name[/bold cyan]")
console.print("Run --help for more info")
def print_help():
console.print("[yellow]Usage:[/yellow]")
console.print("[yellow]Examples:[/yellow]")
"""
def test_missing_purpose_line(tmp_path):
path = _entry_file(tmp_path, NO_PURPOSE_ENTRY)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["title_markup"] is True
assert checks["purpose_line"] is False
assert checks["help_pointer"] is True
# ============================================================
# Missing help pointer
# ============================================================
NO_POINTER_ENTRY = """
from aipass.cli import console
def print_introspection():
console.print("[bold cyan]Branch Name[/bold cyan]")
console.print("[dim]Does stuff[/dim]")
def print_help():
console.print("[yellow]Usage:[/yellow]")
console.print("[yellow]Examples:[/yellow]")
"""
def test_missing_help_pointer(tmp_path):
path = _entry_file(tmp_path, NO_POINTER_ENTRY)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["help_pointer"] is False
assert checks["title_markup"] is True
assert checks["purpose_line"] is True
# ============================================================
# Missing usage section
# ============================================================
NO_USAGE_ENTRY = """
from aipass.cli import console
def print_introspection():
console.print("[bold cyan]Branch[/bold cyan]")
console.print("[dim]Purpose[/dim]")
console.print("--help for more")
def print_help():
console.print("[yellow]Commands:[/yellow]")
console.print("[yellow]Examples:[/yellow]")
"""
def test_missing_usage_section(tmp_path):
path = _entry_file(tmp_path, NO_USAGE_ENTRY)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["usage_section"] is False
assert checks["examples_section"] is True
# ============================================================
# Missing examples section
# ============================================================
NO_EXAMPLES_ENTRY = """
from aipass.cli import console
def print_introspection():
console.print("[bold cyan]Branch[/bold cyan]")
console.print("[dim]Purpose[/dim]")
console.print("--help for more")
def print_help():
console.print("[yellow]Usage:[/yellow]")
console.print(" drone @branch command")
"""
def test_missing_examples_section(tmp_path):
path = _entry_file(tmp_path, NO_EXAMPLES_ENTRY)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["examples_section"] is False
assert checks["usage_section"] is True
# ============================================================
# Internal modules exposed
# ============================================================
def test_internal_modules_detected(tmp_path):
apps_dir = tmp_path / "apps"
apps_dir.mkdir()
modules_dir = apps_dir / "modules"
modules_dir.mkdir()
(modules_dir / "doctor_wire.py").write_text("def handle_command(): pass\n")
(modules_dir / "doctor_fix.py").write_text("def handle_command(): pass\n")
(modules_dir / "good_module.py").write_text("def handle_command(): pass\n")
(modules_dir / "__init__.py").write_text("")
source = """
from aipass.cli import console
def print_introspection():
console.print("[bold cyan]Branch[/bold cyan]")
console.print("[dim]Purpose[/dim]")
console.print("--help for more")
def print_help():
console.print("[yellow]Usage:[/yellow]")
console.print("[yellow]Examples:[/yellow]")
"""
f = apps_dir / "branch.py"
f.write_text(source)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(str(f))
checks = {c["name"]: c for c in result["checks"]}
assert checks["no_internal_modules"]["passed"] is False
assert "doctor_fix.py" in checks["no_internal_modules"]["message"]
assert "doctor_wire.py" in checks["no_internal_modules"]["message"]
def test_underscore_prefixed_internal_ok(tmp_path):
apps_dir = tmp_path / "apps"
apps_dir.mkdir()
modules_dir = apps_dir / "modules"
modules_dir.mkdir()
(modules_dir / "_doctor_wire.py").write_text("def handle_command(): pass\n")
(modules_dir / "__init__.py").write_text("")
source = """
from aipass.cli import console
def print_introspection():
console.print("[bold cyan]Branch[/bold cyan]")
console.print("[dim]Purpose[/dim]")
console.print("--help for more")
def print_help():
console.print("[yellow]Usage:[/yellow]")
console.print("[yellow]Examples:[/yellow]")
"""
f = apps_dir / "branch.py"
f.write_text(source)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(str(f))
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["no_internal_modules"] is True
# ============================================================
# Bypass
# ============================================================
def test_bypass_passes(tmp_path):
path = _entry_file(tmp_path, "def main(): print('hello')\n")
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
bypass = [{"standard": "cli_ux", "file": Path(path).as_posix()}]
result = check_module(path, bypass_rules=bypass)
assert result["passed"] is True
assert result["score"] == 100
# ============================================================
# Score calculation
# ============================================================
def test_partial_score(tmp_path):
source = """
from aipass.cli import console
def print_introspection():
console.print("[bold cyan]Title[/bold cyan]")
def print_help():
console.print("commands go here")
"""
path = _entry_file(tmp_path, source)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
assert 0 < result["score"] < 100
assert result["passed"] is False
# ============================================================
# f-string extraction
# ============================================================
def test_fstring_title_detected(tmp_path):
source = """
from aipass.cli import console
VERSION = "1.0"
def print_introspection():
console.print(f"[bold cyan]Branch v{VERSION}[/bold cyan]")
console.print("[dim]Purpose line[/dim]")
console.print("Run --help for info")
def print_help():
console.print("[yellow]Usage:[/yellow]")
console.print("[yellow]Examples:[/yellow]")
"""
path = _entry_file(tmp_path, source)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["title_markup"] is True
# ============================================================
# Real entry point validation
# ============================================================
def test_seedgo_entry_passes():
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
_root = Path(__file__).resolve().parents[1]
path = str(_root / "apps" / "seedgo.py")
result = check_module(path)
assert result["passed"] is True, f"seedgo.py should pass cli_ux: {[c for c in result['checks'] if not c['passed']]}"
assert result["score"] == 100
def test_uppercase_registry_name_lowercase_path(tmp_path):
"""Regression: uppercase registry name (BACKUP) with lowercase filesystem path
must still resolve and score correctly through the checker."""
branch_dir = tmp_path / "backup"
branch_dir.mkdir()
apps_dir = branch_dir / "apps"
apps_dir.mkdir()
source = """
from aipass.cli import console
def print_introspection():
console.print("[bold cyan]Backup Manager[/bold cyan]")
console.print("[dim]Manages backups for AIPass[/dim]")
console.print("[dim]Run --help for more[/dim]")
def print_help():
console.print("[yellow]Usage:[/yellow]")
console.print(" drone @backup run")
console.print("[yellow]Examples:[/yellow]")
console.print(" drone @backup snapshot")
def main():
pass
"""
entry = apps_dir / "backup.py"
entry.write_text(source)
from aipass.seedgo.apps.handlers.aipass_standards.cli_ux_check import check_module
result = check_module(str(entry))
assert result["passed"] is True
assert result["score"] == 100
@@ -0,0 +1,476 @@
# =================== AIPass ====================
# Name: test_readme_quality.py
# Description: Tests for readme_quality_check.py
# Version: 1.0.0
# Created: 2026-07-17
# Modified: 2026-07-17
# =============================================
"""Tests for readme_quality_check — README content quality from stranger's perspective."""
from pathlib import Path
import pytest
from unittest.mock import MagicMock
@pytest.fixture(autouse=True)
def _mock_infrastructure(monkeypatch):
import sys
mock_logger = MagicMock()
mock_json_handler = MagicMock()
mock_json_handler.log_operation = MagicMock(return_value=True)
prax_mod = MagicMock()
prax_mod.logger = mock_logger
monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod)
json_pkg = MagicMock()
json_pkg.json_handler = mock_json_handler
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json", json_pkg)
json_mod = MagicMock()
json_mod.log_operation = mock_json_handler.log_operation
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json.json_handler", json_mod)
bypass_pkg = MagicMock()
bypass_ignore = MagicMock()
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed as real_is_bypassed
bypass_utils = MagicMock()
bypass_utils.is_bypassed = real_is_bypassed
bypass_pkg.utils = bypass_utils
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg)
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.ignore_handler", bypass_ignore)
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.utils", bypass_utils)
for mod_name in ["aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check"]:
monkeypatch.delitem(sys.modules, mod_name, raising=False)
def _branch_with_readme(tmp_path, readme_content, branch_name="mybranch"):
"""Create a branch directory with apps/branch.py and README.md."""
branch_dir = tmp_path / branch_name
branch_dir.mkdir()
apps_dir = branch_dir / "apps"
apps_dir.mkdir()
entry = apps_dir / f"{branch_name}.py"
entry.write_text("def main(): pass\n")
readme = branch_dir / "README.md"
readme.write_text(readme_content)
return str(entry)
# ============================================================
# Skip / edge cases
# ============================================================
def test_init_file_skipped(tmp_path):
apps_dir = tmp_path / "apps"
apps_dir.mkdir()
f = apps_dir / "__init__.py"
f.write_text("# init\n")
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(str(f))
assert result["passed"] is True
assert result["score"] == 100
def test_no_readme(tmp_path):
branch_dir = tmp_path / "mybranch"
branch_dir.mkdir()
apps_dir = branch_dir / "apps"
apps_dir.mkdir()
f = apps_dir / "mybranch.py"
f.write_text("def main(): pass\n")
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert result["score"] == 0
assert all(not c["passed"] for c in result["checks"])
# ============================================================
# Good README — all checks pass
# ============================================================
GOOD_README = """# MyBranch
A tool that processes data files and generates reports for the AIPass ecosystem.
## Quick Start
```bash
drone @mybranch process data.csv
drone @mybranch report --format html
```
## Commands
| Command | Description |
|---------|-------------|
| process | Process input files |
| report | Generate reports |
"""
def test_good_readme_passes_all(tmp_path):
path = _branch_with_readme(tmp_path, GOOD_README)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
assert result["passed"] is True
assert result["score"] == 100
names = {c["name"] for c in result["checks"] if c["passed"]}
assert "quick_start" in names
assert "stranger_accessible" in names
assert "what_description" in names
# ============================================================
# Missing Quick Start
# ============================================================
NO_QUICKSTART = """# MyBranch
A useful tool for data processing.
## Commands
```bash
drone @mybranch do-stuff
```
"""
def test_missing_quick_start(tmp_path):
path = _branch_with_readme(tmp_path, NO_QUICKSTART)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["quick_start"] is False
assert checks["what_description"] is True
# ============================================================
# Quick Start without code block
# ============================================================
QUICKSTART_NO_CODE = """# MyBranch
Something useful.
## Quick Start
Run the command to get started. See the docs for more info.
"""
def test_quick_start_no_code_fails(tmp_path):
path = _branch_with_readme(tmp_path, QUICKSTART_NO_CODE)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["quick_start"] is False
# ============================================================
# Quick Start with code block passes
# ============================================================
QUICKSTART_WITH_CODE = """# MyBranch
Something useful.
## Quick Start
```
mybranch run
```
"""
def test_quick_start_with_code_passes(tmp_path):
path = _branch_with_readme(tmp_path, QUICKSTART_WITH_CODE)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["quick_start"] is True
# ============================================================
# Getting Started variant accepted
# ============================================================
GETTING_STARTED = """# MyBranch
Does things.
## Getting Started
```bash
mybranch init
```
"""
def test_getting_started_accepted(tmp_path):
path = _branch_with_readme(tmp_path, GETTING_STARTED)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["quick_start"] is True
# ============================================================
# Stranger accessible — too many internal names
# ============================================================
TOO_MANY_INTERNALS = """# MyBranch
Integrates with drone for routing, devpulse for orchestration,
and seedgo for standards compliance. Uses prax for logging.
"""
def test_too_many_internal_names_fails(tmp_path):
path = _branch_with_readme(tmp_path, TOO_MANY_INTERNALS)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c for c in result["checks"]}
assert checks["stranger_accessible"]["passed"] is False
assert "drone" in checks["stranger_accessible"]["message"]
# ============================================================
# Stranger accessible — within limit
# ============================================================
FEW_INTERNALS = """# MyBranch
A data processing tool for AIPass. Uses drone for command routing.
## Quick Start
```
drone @mybranch process
```
"""
def test_few_internal_names_passes(tmp_path):
path = _branch_with_readme(tmp_path, FEW_INTERNALS)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["stranger_accessible"] is True
# ============================================================
# Stranger accessible — aipass project name excluded
# ============================================================
AIPASS_PROJECT_NAME = """# MyBranch
Standards compliance platform for AIPass. Audits all agents
against code standards and reports violations.
## Quick Start
```
drone @mybranch audit
```
"""
def test_aipass_project_name_not_counted(tmp_path):
path = _branch_with_readme(tmp_path, AIPASS_PROJECT_NAME)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["stranger_accessible"] is True
# ============================================================
# Invoke match — correct branch
# ============================================================
INVOKE_MATCH = """# mybranch
Does things.
## Invoke
```
drone @mybranch command
```
## Quick Start
```
drone @mybranch start
```
"""
def test_invoke_matches_branch(tmp_path):
path = _branch_with_readme(tmp_path, INVOKE_MATCH, branch_name="mybranch")
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["invoke_match"] is True
# ============================================================
# Invoke match — wrong branch
# ============================================================
INVOKE_MISMATCH = """# mybranch
Does things.
## Invoke
```
drone @otherbranch command
```
## Quick Start
```
drone @mybranch start
```
"""
def test_invoke_wrong_branch_fails(tmp_path):
path = _branch_with_readme(tmp_path, INVOKE_MISMATCH, branch_name="mybranch")
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["invoke_match"] is False
# ============================================================
# No invoke section — skipped (passes)
# ============================================================
NO_INVOKE = """# mybranch
Does things.
## Quick Start
```
drone @mybranch command
```
"""
def test_no_invoke_section_skipped(tmp_path):
path = _branch_with_readme(tmp_path, NO_INVOKE, branch_name="mybranch")
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c for c in result["checks"]}
assert checks["invoke_match"]["passed"] is True
assert "skipped" in checks["invoke_match"]["message"].lower()
# ============================================================
# What description — present
# ============================================================
def test_description_in_first_10_lines(tmp_path):
readme = "# Branch\n\nThis tool processes data and generates comprehensive reports.\n"
path = _branch_with_readme(tmp_path, readme)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["what_description"] is True
# ============================================================
# What description — missing (only short lines)
# ============================================================
def test_no_description_fails(tmp_path):
readme = "# Branch\n\nv1.0\nTODO\n\n---\n\n## Stuff\n"
path = _branch_with_readme(tmp_path, readme)
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
checks = {c["name"]: c["passed"] for c in result["checks"]}
assert checks["what_description"] is False
# ============================================================
# Bypass
# ============================================================
def test_bypass_passes(tmp_path):
path = _branch_with_readme(tmp_path, "# Bad\n\nNo content.\n")
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
bypass = [{"standard": "readme_quality", "file": Path(path).as_posix()}]
result = check_module(path, bypass_rules=bypass)
assert result["passed"] is True
assert result["score"] == 100
# ============================================================
# Real entry point validation
# ============================================================
def test_seedgo_readme_passes():
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
_root = Path(__file__).resolve().parents[1]
path = str(_root / "apps" / "seedgo.py")
result = check_module(path)
assert result["passed"] is True, (
f"seedgo README should pass readme_quality: {[c for c in result['checks'] if not c['passed']]}"
)
assert result["score"] == 100
def test_uppercase_registry_name_lowercase_path(tmp_path):
"""Regression: uppercase registry name (BACKUP) with lowercase filesystem path
must resolve correctly — branch_name derived from directory, not registry."""
readme = """# Backup
A tool for managing automated backups across all AIPass branches.
## Quick Start
```bash
drone @backup snapshot
drone @backup restore latest
```
"""
path = _branch_with_readme(tmp_path, readme, branch_name="backup")
from aipass.seedgo.apps.handlers.aipass_standards.readme_quality_check import check_module
result = check_module(path)
assert result["passed"] is True
assert result["score"] == 100
+19
View File
@@ -9,6 +9,25 @@
---
## Quick Start
```bash
# List all available skills
drone @skills list
# Get details about a skill
drone @skills info telegram
# Run a skill
drone @skills run inbox_check
# Create a new skill
drone @skills create my-skill --with-handler
# Check if a skill's requirements are met
drone @skills validate telegram
```
## Overview
## Three Tiers
+13
View File
@@ -38,6 +38,8 @@ def print_introspection():
console.print(" [dim]- creator.py (create_skill — scaffold new skills from templates)[/dim]")
console.print(" [dim]- validator.py (validate_skill — check skill requirements)[/dim]")
console.print()
console.print("[dim]Run 'drone @skills --help' for usage information[/dim]")
console.print()
def handle_command(command, args=None):
@@ -124,6 +126,17 @@ def print_help():
console.print(" 1. .aipass/skills/ Project-local skills")
console.print(" 2. ~/.aipass/skills/ Global user skills")
console.print(" 3. src/aipass/skills/lib/ Built-in skills")
console.print()
console.print("Examples:")
console.print()
console.print(" [dim]# List all available skills[/dim]")
console.print(" [green]drone @skills list[/green]")
console.print()
console.print(" [dim]# Get info about a skill[/dim]")
console.print(" [green]drone @skills info telegram[/green]")
console.print()
console.print(" [dim]# Create a new skill[/dim]")
console.print(" [green]drone @skills create my-skill --with-handler[/green]")
def _cmd_list():
+18
View File
@@ -20,6 +20,24 @@
---
## Quick Start
```bash
# Create a new branch
drone @spawn create /path/to/my_agent --role "Analyst" --purpose "Data reports"
# Preview an update before applying
drone @spawn update @my_agent
# Apply the update
drone @spawn update @my_agent --apply
# Check registry health
drone @spawn sync-registry
```
---
## Citizen Classes
Every branch belongs to a **citizen class**, which determines its template:
+13
View File
@@ -76,6 +76,17 @@ def print_help():
warning("--dry-run", details="Preview changes without modifying files (default for update/repair)")
warning("--trace", details="Enable verbose logging")
console.print()
console.print("[yellow]Examples:[/yellow]")
console.print()
console.print(" [dim]# Create a new branch with role and purpose[/dim]")
console.print(' [green]drone @spawn create /path/to/new_agent --role "Analyst" --purpose "Reports"[/green]')
console.print()
console.print(" [dim]# Preview an update before applying[/dim]")
console.print(" [green]drone @spawn update @branch_name[/green]")
console.print()
console.print(" [dim]# Sync registry against filesystem[/dim]")
console.print(" [green]drone @spawn sync-registry --fix[/green]")
console.print()
def handle_create(args):
@@ -212,6 +223,8 @@ def print_introspection():
console.print(" [dim]- regenerate_registry.py (handle_regenerate_registry — regenerate template registry)[/dim]")
console.print(" [dim]- repair.py (handle_repair — project structure repair)[/dim]")
console.print()
console.print("[dim]Run 'drone @spawn --help' for usage information[/dim]")
console.print()
def main():
@@ -429,7 +429,7 @@
},
"metadata": {
"description": "Template file tracking registry for ID-based updates",
"last_updated": "2026-07-12",
"last_updated": "2026-07-17",
"version": "1.0.0"
}
}
+10 -1
View File
@@ -5,7 +5,16 @@
**Purpose:** Event bus and error dispatch for AIPass. Branches fire events, registered handlers react. Medic watches logs for errors, fingerprints them, gates dispatch through an 8-stage pipeline, and notifies the responsible branch.
**Module:** `aipass.trigger`
**Version:** 2.2.0
**Last Updated:** 2026-05-16
**Last Updated:** 2026-07-17
## Quick Start
```bash
drone @trigger status # Event bus + medic state
drone @trigger errors list # View tracked errors
drone @trigger medic status # Check dispatch gating
drone @trigger fire error_detected branch=api error_type=ImportError
```
## Commands
+9
View File
@@ -179,6 +179,15 @@ def print_help(modules: List[Any]):
console.print("─" * 70)
console.print()
console.print("[bold cyan]EXAMPLES:[/bold cyan]")
console.print()
console.print(" [dim]drone @trigger fire error_detected branch=api error_type=ImportError[/dim]")
console.print(" [dim]drone @trigger errors list[/dim]")
console.print(" [dim]drone @trigger medic status[/dim]")
console.print()
console.print("─" * 70)
console.print()
console.print("[bold]TIP:[/bold] For module-specific help:")
console.print(" [dim]drone @trigger <command> --help[/dim]")
console.print()