fix(ci): restore green — advisory template checker no longer gates audit + 3 test/module regressions

Root-cause fixes for PR#646 red (dev broke after DPLAN-0226/FPLAN-0289/TDPLAN-0010 batch):
- seedgo: branch_audit honors ADVISORY (template_check no longer averaged into gate) + presence_gate added to hooks-snapshot fixture (4 tests)
- hooks: cc_sessions README entry + seedgo modules bypass (reads external ~/.claude, not branch data)
- spawn: retire passport(disabled).py/passport_ops(disabled).py to .archive/ (disabled suffix kept broken cross-import visible to type checker)
- ai_mail: broker-fd test gives testbranch a real .trinity/passport.json for the new marker-walk resolution (f914ab6)
This commit is contained in:
AIOSAI
2026-07-02 15:47:53 -07:00
parent e1ac4e365f
commit e92dcaff12
8 changed files with 49 additions and 334 deletions
+27
View File
@@ -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
@@ -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")
+5
View File
@@ -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",
+1
View File
@@ -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/<pid>.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)
@@ -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),
@@ -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": [
{
@@ -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": [],
}
@@ -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