diff --git a/CHANGELOG.md b/CHANGELOG.md index edb0b86e..f03ebc38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,33 @@ PyPI version — not the changelog header. --- +## [2026-07-02] + +### Fixed + +- **CI green — four regressions from the DPLAN-0226 / FPLAN-0289 / TDPLAN-0010 + batch (PR #646).** The dev branch had gone red across `seedgo-audit` and the + `test` matrix; root-caused and fixed at source: + - **seedgo** — the new `template_check` advisory checker was gating CI. + `branch_audit.py` averaged *all* checker scores into the branch total, so + `template_check`'s `ADVISORY=True` was never honored and it dragged 7 + branches below the 100% floor on legitimate README brace-examples. Added a + `gating_scores` filter that excludes `ADVISORY is True` checkers before + computing the average (strict `is True` to avoid MagicMock false-positives) + and exposed `advisory_standards` in the audit output. Also refreshed the + provider hooks snapshot fixture to include the `presence_gate` + `UserPromptSubmit` hook (FPLAN-0289), fixing 4 `test_hooks_snapshot` tests. + - **hooks** — `cc_sessions.py` (added by the bridge, `f6cbe34`) was missing + its README entry and a seedgo `modules` bypass (it reads external + `~/.claude/sessions/*.json`, not branch data, so `json_handler` is the wrong + tool — same precedent as `presence.py`). Added both. + - **spawn** — retired the `passport(disabled).py` / `passport_ops(disabled).py` + pair to `.archive/`; the `(disabled)` suffix kept them visible to the type + checker, which flagged a broken cross-import between them. + - **ai_mail** — `test_child_inherits_broker_fd` gave its throwaway test branch + a real `.trinity/passport.json` so the broker's new `.trinity`-marker + resolution (`f914ab6`) can resolve it and permit the delete. + ## [2026-07-01] ### Added diff --git a/src/aipass/ai_mail/tests/test_dispatch_monitor.py b/src/aipass/ai_mail/tests/test_dispatch_monitor.py index a1fae84f..ff5f2f24 100644 --- a/src/aipass/ai_mail/tests/test_dispatch_monitor.py +++ b/src/aipass/ai_mail/tests/test_dispatch_monitor.py @@ -1731,10 +1731,13 @@ class TestBrokerRealE2E: from aipass.drone.apps.handlers.broker.daemon import BrokerDaemon from aipass.drone.apps.handlers.broker.client import create_identified_connection - # Set up repo root with branch dir + # Set up repo root with branch dir + .trinity marker (broker marker-walk requires it) repo_root = tmp_path / "repo" branch_dir = repo_root / "src" / "aipass" / "testbranch" branch_dir.mkdir(parents=True) + trinity_dir = branch_dir / ".trinity" + trinity_dir.mkdir() + (trinity_dir / "passport.json").write_text('{"branch_info": {"branch_name": "testbranch"}}', encoding="utf-8") target_file = branch_dir / "deleteme.txt" target_file.write_text("delete me", encoding="utf-8") diff --git a/src/aipass/hooks/.seedgo/bypass.json b/src/aipass/hooks/.seedgo/bypass.json index d9e3868e..c60683c9 100644 --- a/src/aipass/hooks/.seedgo/bypass.json +++ b/src/aipass/hooks/.seedgo/bypass.json @@ -1004,6 +1004,11 @@ "standard": "json_structure", "reason": "Reads CC-native ~/.claude/sessions/*.json (external files, not branch data) with stdlib json — no JSON file ops needing json_handler." }, + { + "file": "apps/modules/cc_sessions.py", + "standard": "modules", + "reason": "Direct file ops on ~/.claude/sessions/*.json — external CC-native session files, not branch data. The module IS the CC session reader service; json_handler is for branch-managed JSON, not external platform files." + }, { "file": "apps/handlers/lifecycle/session_boot.py", "standard": "json_structure", diff --git a/src/aipass/hooks/README.md b/src/aipass/hooks/README.md index e46bda2c..c3bf993e 100644 --- a/src/aipass/hooks/README.md +++ b/src/aipass/hooks/README.md @@ -49,6 +49,7 @@ src/aipass/hooks/ │ ├── sound.py # Shared sound utilities (speak, play, mute) │ ├── modules/ │ │ ├── cadence.py # Prompt injection cadence (every-Nth-turn gating) +│ │ ├── cc_sessions.py # CC-native session file reader (~/.claude/sessions/.json) │ │ ├── engine.py # Core dispatch — routes events to handlers │ │ ├── hooksound.py # Sound control (drone @hooks hooksound on/off) │ │ ├── hookstatus.py # Config viewer (drone @hooks status) diff --git a/src/aipass/seedgo/apps/handlers/audit/branch_audit.py b/src/aipass/seedgo/apps/handlers/audit/branch_audit.py index 2d192953..562580ac 100644 --- a/src/aipass/seedgo/apps/handlers/audit/branch_audit.py +++ b/src/aipass/seedgo/apps/handlers/audit/branch_audit.py @@ -187,7 +187,9 @@ def audit_branch(branch: Dict[str, str], bypass_rules: list, pack_path: Path | N logger.info("Post-check %s failed for branch %s", name, branch["name"]) json_handler.log_operation("branch_audit_completed", {"branch": branch["name"], "checkers": len(checkers)}) - avg = int(sum(scores.values()) / len(scores)) if scores else 0 + advisory_standards = [name for name, mod in checkers.items() if getattr(mod, "ADVISORY", False) is True] + gating_scores = {k: v for k, v in scores.items() if k not in advisory_standards} + avg = int(sum(gating_scores.values()) / len(gating_scores)) if gating_scores else 0 # Deprecated DOCUMENTS/ directory check deprecated = [] @@ -214,6 +216,7 @@ def audit_branch(branch: Dict[str, str], bypass_rules: list, pack_path: Path | N "branch": branch, "results": results, "scores": scores, + "advisory_standards": advisory_standards, "average": avg, "deprecated_patterns": deprecated, "files_checked": len(all_files), diff --git a/src/aipass/seedgo/tests/fixtures/provider_hooks_snapshot.json b/src/aipass/seedgo/tests/fixtures/provider_hooks_snapshot.json index fb62f90f..f46db711 100644 --- a/src/aipass/seedgo/tests/fixtures/provider_hooks_snapshot.json +++ b/src/aipass/seedgo/tests/fixtures/provider_hooks_snapshot.json @@ -1,5 +1,13 @@ { "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:presence_gate" + } + ] + }, { "hooks": [ { diff --git a/src/aipass/spawn/apps/handlers/passport_ops(disabled).py b/src/aipass/spawn/apps/handlers/passport_ops(disabled).py deleted file mode 100644 index 7ce7c0d5..00000000 --- a/src/aipass/spawn/apps/handlers/passport_ops(disabled).py +++ /dev/null @@ -1,161 +0,0 @@ -# =================== AIPass ==================== -# Name: passport_ops.py -# Description: Passport command — grants birthright citizenship to a directory -# Version: 1.0.0 -# Created: 2026-03-07 -# Modified: 2026-03-10 -# ============================================= - -"""Passport command implementation for granting birthright citizenship. - -Creates minimal citizen identity (.trinity/, .aipass/, README.md) and -registers in AIPASS_REGISTRY.json without creating the full 3-layer scaffold. -""" - -from pathlib import Path - -from aipass.prax import logger -from aipass.spawn.apps.handlers.metadata import ( - get_branch_name, - normalize_branch_name, - detect_profile, -) -from aipass.spawn.apps.handlers.placeholders import ( - build_replacements_dict, - validate_no_placeholders, -) -from aipass.spawn.apps.handlers.file_ops import ( - copy_template, - rename_placeholder_paths, - regenerate_template_registry, - ensure_directory, -) -from aipass.spawn.apps.handlers.registry import ( - find_registry, - add_to_registry, - get_next_citizen_number, - ensure_project_has_owner, -) -from aipass.spawn.apps.handlers.class_registry import get_template_dir -from aipass.spawn.apps.handlers.json import json_handler - - -def grant_passport( - target_path: str, - role: str = "", - purpose: str = "", -) -> dict: - """Grant birthright citizenship to a directory. - - Creates .trinity/, .aipass/, README.md and registers in AIPASS_REGISTRY.json. - Does NOT create apps/ scaffold -- that's for builder class. - - Args: - target_path: Path to the target directory (created if doesn't exist). - role: Optional role description for the passport. - purpose: Optional purpose description. - - Returns: - Dict with results (success, branch_name, path, etc.) - """ - target = Path(target_path).resolve() - - # Check if already a citizen - trinity_dir = target / ".trinity" - if trinity_dir.exists(): - return _error(f"Already a citizen -- .trinity/ exists at {target}") - - # Get birthright template - template = get_template_dir("birthright") - if not template.exists(): - return _error(f"Birthright template not found: {template}") - - # Extract names - folder_name = get_branch_name(target) - branch_upper = normalize_branch_name(folder_name, "upper") - branch_lower = normalize_branch_name(folder_name, "lower") - detected_profile = detect_profile(target) - - # Registry setup - reg_path = find_registry(target.parent) - citizen_number = get_next_citizen_number(reg_path) - - # Build placeholder replacements - replacements = build_replacements_dict( - target, - folder_name, - role=role, - purpose=purpose or "Birthright citizen - purpose TBD", - profile=detected_profile, - citizen_number=citizen_number, - ) - - # Create directory if needed - ensure_directory(target) - - # Copy birthright template with placeholder replacement - copied, _ = copy_template(template, target, replacements) - - # Rename any placeholder paths - _ = rename_placeholder_paths(target, folder_name) - - # Set owner field — first agent in the project is the owner - passport_path = target / ".trinity" / "passport.json" - if passport_path.exists(): - passport_data = json_handler.read_json(passport_path) - if passport_data: - passport_data.setdefault("citizenship", {})["owner"] = citizen_number == 1 - json_handler.write_json(passport_path, passport_data) - - # Regenerate template registry - regenerate_template_registry(target) - - # Register in AIPASS_REGISTRY.json (store relative path for portability) - try: - registry_branch_path = target.relative_to(reg_path.parent).as_posix() - except ValueError: - logger.warning("[passport] Cannot relativize path %s to registry %s, storing absolute", target, reg_path.parent) - registry_branch_path = target.as_posix() - registry_updated = add_to_registry( - reg_path, - branch_upper, - registry_branch_path, - detected_profile, - f"@{branch_lower}", - purpose or "Birthright citizen - purpose TBD", - ) - - # Ensure at least one agent in the project is the owner - ensure_project_has_owner(reg_path) - - # Validate - issues = validate_no_placeholders(target) - - logger.info(f"[passport] Granted birthright to {branch_upper} at {target}") - json_handler.log_operation("passport_created", data={"target": str(target)}) - - return { - "success": True, - "branch_name": branch_upper, - "path": str(target), - "citizen_class": "birthright", - "files_copied": len([c for c in copied if "(dir)" not in c]), - "registry_updated": registry_updated, - "registry_path": str(reg_path), - "validation_issues": issues, - } - - -def _error(message: str) -> dict: - """Return error result dict.""" - return { - "success": False, - "error": message, - "branch_name": "", - "path": "", - "citizen_class": "", - "files_copied": 0, - "registry_updated": False, - "registry_path": "", - "validation_issues": [], - } diff --git a/src/aipass/spawn/apps/modules/passport(disabled).py b/src/aipass/spawn/apps/modules/passport(disabled).py deleted file mode 100644 index a1bc16e6..00000000 --- a/src/aipass/spawn/apps/modules/passport(disabled).py +++ /dev/null @@ -1,171 +0,0 @@ -# =================== AIPass ==================== -# Name: passport.py -# Description: Passport command — thin CLI layer for granting birthright citizenship -# Version: 1.0.0 -# Created: 2026-03-07 -# Modified: 2026-03-14 -# ============================================= - -"""Passport command orchestrator for branch lifecycle management. - -Thin CLI module that parses arguments and delegates to the passport handler. -All implementation logic lives in apps/handlers/passport_ops.py. -""" - -import argparse - -from pathlib import Path - -from aipass.cli.apps.modules import console, error, warning -from aipass.prax import logger - -from aipass.spawn.apps.handlers.passport_ops import grant_passport -from aipass.spawn.apps.handlers.registry import find_registry -from aipass.spawn.apps.handlers.json import json_handler - - -def print_introspection(): - """Display module introspection info.""" - console.print() - console.print("[bold cyan]passport Module[/bold cyan]") - console.print("Grant birthright citizenship — minimal identity (.trinity/, .aipass/, README.md)") - console.print() - console.print("[yellow]Connected Handlers:[/yellow]") - console.print(" [cyan]handlers/[/cyan]") - console.print(" [dim]- passport_ops.py (grant_passport — create minimal citizen identity and register)[/dim]") - console.print() - - -def handle_command(command: str, args: list) -> bool: - """Handle commands routed by the entry point. - - Args: - command: The command string (e.g. "passport") - args: List of arguments for the command - - Returns: - True if command was handled, False otherwise. - """ - if command != "passport": - return False - - # No args → introspection - if not args: - print_introspection() - return True - - if "--help" in args: - print_introspection() - return True - - return handle_passport(args) == 0 - - -def _search_src_subdirs(src_dir: Path, dirname: str) -> Path | None: - """Search src/*/dirname for an existing directory.""" - if not src_dir.is_dir(): - return None - for sub in src_dir.iterdir(): - if not sub.is_dir(): - continue - candidate = sub / dirname - if candidate.exists(): - return candidate - return None - - -def _resolve_target(dirname: str) -> Path: - """Resolve a @dirname to a filesystem path. - - Searches CWD, project root (via registry), and src/ subdirectories. - For new directories, defaults to CWD-relative. - """ - # Absolute or home-relative path — use directly - if dirname.startswith("/") or dirname.startswith("~"): - return Path(dirname).expanduser() - - # 1. CWD-relative - cwd_path = Path.cwd() / dirname - if cwd_path.exists(): - return cwd_path - - # 2. Project-relative via registry - try: - reg_path = find_registry() - project_root = reg_path.parent - for candidate in [ - project_root / dirname, - project_root / "src" / dirname, - ]: - if candidate.exists(): - return candidate - # Search src/*/dirname (e.g., src/aipass/target) - found = _search_src_subdirs(project_root / "src", dirname) - if found: - return found - except Exception as exc: - logger.warning("[passport] Registry lookup failed during target resolution: %s", exc) - - # 3. Default: CWD-relative (passport will create it if needed) - return cwd_path - - -def handle_passport(args: list[str]) -> int: - """Parse args and execute passport grant. - - Args patterns: - ["@dirname"] -> grant birthright to dirname - ["@dirname", "--role", "X"] -> with role - ["@dirname", "--purpose", "X"] -> with purpose - - Returns exit code (0=success, 1=failure). - """ - if not args: - warning("Usage: drone @spawn passport <@dirname> [--role ...] [--purpose ...]") - console.print() - console.print(" [dim]Grant birthright citizenship to a directory[/dim]") - console.print() - console.print(" [green]@dirname[/green] Directory to grant citizenship (created if needed)") - warning("--role", details="Role description for the passport") - warning("--purpose", details="Purpose description") - return 1 - - # Intercept --help before argparse (argparse has add_help=False) - if "--help" in args or "-h" in args: - print_introspection() - return 0 - - parser = argparse.ArgumentParser(prog="spawn passport", add_help=False) - parser.add_argument("target") - parser.add_argument("--role", default="") - parser.add_argument("--purpose", default="") - - parsed = parser.parse_args(args) - - # Strip @ prefix if present - target = parsed.target.lstrip("@") - - # Resolve target path — CWD-aware for external project support - target_path = _resolve_target(target) - - result = grant_passport( - target_path=str(target_path), - role=parsed.role, - purpose=parsed.purpose, - ) - - if result["success"]: - json_handler.log_operation("passport_granted", data={"branch": result["branch_name"]}) - console.print() - console.print(f"[green]Passport granted: {result['branch_name']}[/green]") - console.print(" Class: birthright") - console.print(f" Path: {result['path']}") - console.print(f" Files: {result['files_copied']}") - console.print(f" Registry: {'updated' if result['registry_updated'] else 'not updated'}") - if result["validation_issues"]: - warning(f"{len(result['validation_issues'])} unreplaced placeholders") - console.print() - return 0 - else: - error(result["error"]) - return 1