From f0019ca2044c61e15bbbba5dd99d267642d24c35 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Wed, 22 Apr 2026 23:07:56 -0700 Subject: [PATCH] feat(system): chore: gitignore HERALD.md and remove from README Co-Authored-By: @devpulse --- .gitignore | 3 + README.md | 4 +- .../spawn/apps/handlers/passport_ops.py | 4 + src/aipass/spawn/apps/handlers/registry.py | 31 ++++++++ src/aipass/spawn/apps/modules/core.py | 6 +- .../spawn/tests/test_citizen_classes.py | 75 +++++++++++++++++++ 6 files changed, 119 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index fd2763a0..e888e0da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Virtual environment .venv/ +# Herald (session history — parked) +HERALD.md + # Python __pycache__/ *.pyc diff --git a/README.md b/README.md index ae0fb7b7..946e2855 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Status](https://img.shields.io/badge/status-beta-yellow)](HERALD.md) +[![Status](https://img.shields.io/badge/status-beta-yellow)](#project-status) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](pyproject.toml) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![PyPI](https://img.shields.io/pypi/v/aipass)](https://pypi.org/project/aipass/) @@ -232,8 +232,6 @@ setup.sh auto-detects which CLIs are installed and configures hooks for each. Each agent documents its own operational status in its branch README — what works, what doesn't, and why. -For detailed session history, see [HERALD.md](HERALD.md). - --- ## Requirements diff --git a/src/aipass/spawn/apps/handlers/passport_ops.py b/src/aipass/spawn/apps/handlers/passport_ops.py index 834fc5f5..138c43c0 100644 --- a/src/aipass/spawn/apps/handlers/passport_ops.py +++ b/src/aipass/spawn/apps/handlers/passport_ops.py @@ -34,6 +34,7 @@ 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 @@ -124,6 +125,9 @@ def grant_passport( 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) diff --git a/src/aipass/spawn/apps/handlers/registry.py b/src/aipass/spawn/apps/handlers/registry.py index 1a69cbff..720d1d94 100644 --- a/src/aipass/spawn/apps/handlers/registry.py +++ b/src/aipass/spawn/apps/handlers/registry.py @@ -265,3 +265,34 @@ def fix_passport_registry_id(branch_dir: Path, registry_path: Path) -> bool: except Exception as e: logger.warning("[registry] Failed to fix registry_id for %s: %s", branch_dir.name, e) return False + + +def ensure_project_has_owner(registry_path): + """If no agent in the project has owner:true, assign it to the earliest-created agent.""" + registry_path = Path(registry_path) + reg_data = load_registry(registry_path) + branches = _branches_as_list(reg_data.get("branches", [])) + if not branches: + return False + + registry_root = registry_path.parent + for branch in branches: + branch_path = registry_root / branch.get("path", "") + passport_path = branch_path / ".trinity" / "passport.json" + if passport_path.exists(): + passport = json_handler.read_json(passport_path) + if passport and passport.get("citizenship", {}).get("owner") is True: + return False + + by_created = sorted(branches, key=lambda b: b.get("created", "9999-99-99")) + for branch in by_created: + branch_path = registry_root / branch.get("path", "") + passport_path = branch_path / ".trinity" / "passport.json" + if passport_path.exists(): + passport = json_handler.read_json(passport_path) + if passport: + passport.setdefault("citizenship", {})["owner"] = True + json_handler.write_json(passport_path, passport) + logger.info("[registry] Retroactively set owner=true on %s", branch.get("name", "?")) + return True + return False diff --git a/src/aipass/spawn/apps/modules/core.py b/src/aipass/spawn/apps/modules/core.py index 85b2a772..462c16b8 100644 --- a/src/aipass/spawn/apps/modules/core.py +++ b/src/aipass/spawn/apps/modules/core.py @@ -46,6 +46,7 @@ from aipass.spawn.apps.handlers.registry import ( add_to_registry, get_next_citizen_number, fix_passport_registry_id, + ensure_project_has_owner, ) from aipass.spawn.apps.handlers.class_registry import ( get_template_dir as _get_template_dir, @@ -250,7 +251,10 @@ def _spawn_agent( purpose or "New agent - purpose TBD", ) - # Step 5: Validate no unreplaced placeholders + # Step 5: Ensure at least one agent in the project is the owner + ensure_project_has_owner(reg_path) + + # Step 6: Validate no unreplaced placeholders issues = validate_no_placeholders(target) json_handler.log_operation("branch_created", data={"branch": branch_upper}) diff --git a/src/aipass/spawn/tests/test_citizen_classes.py b/src/aipass/spawn/tests/test_citizen_classes.py index 3372b738..022bee7a 100644 --- a/src/aipass/spawn/tests/test_citizen_classes.py +++ b/src/aipass/spawn/tests/test_citizen_classes.py @@ -531,3 +531,78 @@ class TestPassportOwnerField: p2 = json.loads((second / ".trinity" / "passport.json").read_text()) assert p1["citizenship"]["owner"] is True assert p2["citizenship"]["owner"] is False + + +class TestRetroactiveOwner: + """Tests for retroactive owner assignment on legacy projects.""" + + def test_retroactive_owner_on_legacy_agents(self, tmp_path): + """Creating a new agent in a project where no agent has owner sets the first one.""" + from aipass.spawn.apps.modules.core import _spawn_agent + + reg = tmp_path / "TEST_REGISTRY.json" + reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}') + + # Create two agents, then remove their owner field to simulate legacy + _spawn_agent(str(tmp_path / "old_a"), registry_path=str(reg)) + _spawn_agent(str(tmp_path / "old_b"), registry_path=str(reg)) + + for name in ["old_a", "old_b"]: + pp = tmp_path / name / ".trinity" / "passport.json" + data = json.loads(pp.read_text()) + del data["citizenship"]["owner"] + pp.write_text(json.dumps(data, indent=2)) + + # Create a third agent — should trigger retroactive fix + _spawn_agent(str(tmp_path / "new_c"), registry_path=str(reg)) + + pa = json.loads((tmp_path / "old_a" / ".trinity" / "passport.json").read_text()) + pb = json.loads((tmp_path / "old_b" / ".trinity" / "passport.json").read_text()) + pc = json.loads((tmp_path / "new_c" / ".trinity" / "passport.json").read_text()) + assert pa["citizenship"]["owner"] is True + assert pb["citizenship"].get("owner") is not True + assert pc["citizenship"]["owner"] is False + + def test_no_retroactive_if_owner_exists(self, tmp_path): + """If an existing agent already has owner:true, no retroactive change.""" + from aipass.spawn.apps.modules.core import _spawn_agent + + reg = tmp_path / "TEST_REGISTRY.json" + reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}') + + _spawn_agent(str(tmp_path / "first"), registry_path=str(reg)) + _spawn_agent(str(tmp_path / "second"), registry_path=str(reg)) + _spawn_agent(str(tmp_path / "third"), registry_path=str(reg)) + + p1 = json.loads((tmp_path / "first" / ".trinity" / "passport.json").read_text()) + p2 = json.loads((tmp_path / "second" / ".trinity" / "passport.json").read_text()) + p3 = json.loads((tmp_path / "third" / ".trinity" / "passport.json").read_text()) + assert p1["citizenship"]["owner"] is True + assert p2["citizenship"]["owner"] is False + assert p3["citizenship"]["owner"] is False + + def test_ensure_project_has_owner_direct(self, tmp_path): + """Direct call to ensure_project_has_owner fixes a legacy project.""" + from aipass.spawn.apps.handlers.registry import ensure_project_has_owner + from aipass.spawn.apps.modules.core import _spawn_agent + + reg = tmp_path / "TEST_REGISTRY.json" + reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}') + + _spawn_agent(str(tmp_path / "agent_x"), registry_path=str(reg)) + _spawn_agent(str(tmp_path / "agent_y"), registry_path=str(reg)) + + # Strip owner from both + for name in ["agent_x", "agent_y"]: + pp = tmp_path / name / ".trinity" / "passport.json" + data = json.loads(pp.read_text()) + data["citizenship"].pop("owner", None) + pp.write_text(json.dumps(data, indent=2)) + + result = ensure_project_has_owner(reg) + assert result is True + + px = json.loads((tmp_path / "agent_x" / ".trinity" / "passport.json").read_text()) + py = json.loads((tmp_path / "agent_y" / ".trinity" / "passport.json").read_text()) + assert px["citizenship"]["owner"] is True + assert py["citizenship"].get("owner") is not True