"""Decide whether an assigned issue is agent-targeted, and for whom. An issue is forge-targeted when BOTH hold: - it carries a `bot-bottle:` label naming the agent, and - at least one assignee is a member of the configured org. An optional `bot-bottle-bottle:` label overrides bottle selection. The forge is duck-typed: any object with `is_org_member(org, user)`. """ from __future__ import annotations from dataclasses import dataclass from typing import Protocol from .config import BOTTLE_LABEL_PREFIX, LABEL_PREFIX from .model import IssueAssigned class Membership(Protocol): def is_org_member(self, org: str, username: str) -> bool: ... @dataclass(frozen=True) class Target: agent_name: str bottle_override: str | None def parse_labels(labels: tuple[str, ...]) -> tuple[str | None, str | None]: """Return (agent_name, bottle_override) parsed from labels.""" agent: str | None = None bottle: str | None = None for label in labels: if label.startswith(BOTTLE_LABEL_PREFIX): bottle = label[len(BOTTLE_LABEL_PREFIX):] or None elif label.startswith(LABEL_PREFIX): agent = label[len(LABEL_PREFIX):] or None return agent, bottle def resolve_target( event: IssueAssigned, forge: Membership, org: str ) -> Target | None: """Return the `Target` for a forge-targeted issue, or None to ignore.""" agent, bottle = parse_labels(event.labels) if not agent: return None if not any(forge.is_org_member(org, a) for a in event.assignees): return None return Target(agent_name=agent, bottle_override=bottle)