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:
@@ -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")
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user