[F099] wire pr_pass/pr_fail self_review block in the spec gate

The pr_pass/pr_fail ActionSpecs carry self_review_block=True, but
_gate_preflight never populated Context.original_developer_slug, and
actor_slug was read off agent.slug — which GatewayAgentView does not
carry, so it was always None in production. The block was structurally
dormant: a reviewer who was also the original developer of the
assembled PR could pass (or fail) their own work. The service-layer
_validate_not_self_review backstop only covers qa/documenter, not
pr_reviewer, so the spec gate is the only defense.

Set actor_slug=str(reviewer_agent_id) (GatewayAgentView has no slug,
so the UUID is the identity) and original_developer_slug from the
original_developer marker (a UUID stored as a string). Both resolve to
UUID strings, so the spec's string-equality comparison fires when the
reviewer IS the recorded original developer.

The marker is never set on assembled coordination tasks (only on
dev-leaf tasks at QA/doc claim), so the block stays dormant by design
in production — but the gate is now correctly wired to fire if the
marker were ever set to the reviewer. Zero production behavior change;
the dormant-in-production state is pinned by the no-marker test.
This commit is contained in:
Renn F
2026-06-28 21:10:52 +02:00
parent 0506176796
commit f4c67bedff
2 changed files with 172 additions and 1 deletions
@@ -22,6 +22,7 @@ import structlog
from roboco.foundation.policy import lifecycle as spec_module
from roboco.foundation.policy import tracing as _tr
from roboco.foundation.policy.content import markers
from roboco.services.gateway.envelope import Envelope
if TYPE_CHECKING:
@@ -187,9 +188,21 @@ class PRGateMixin(_Base):
)
if isinstance(role, Envelope):
return role
# The spec gate's ``self_review_block`` is the only self-review defense
# for pr_pass / pr_fail: the service-layer ``_validate_not_self_review``
# backstop covers qa/documenter but skips pr_reviewer. For the comparison
# to fire, both sides must be populated. ``GatewayAgentView`` carries no
# ``slug`` field (so ``getattr(agent, "slug", None)`` is always None in
# production), and the ``original_developer`` marker stores the dev's
# UUID — so resolve both as UUID strings and let the spec's string
# equality do the rest. The marker is never set on assembled coordination
# tasks (only on dev-leaf tasks at QA/doc claim), so the block is dormant
# by design in production — but the gate is now correctly wired to fire
# if the marker were ever set to the reviewer.
spec_ctx = spec_module.Context(
actor_id=reviewer_agent_id,
actor_slug=getattr(agent, "slug", None) if agent is not None else None,
actor_slug=str(reviewer_agent_id),
original_developer_slug=markers.get_original_developer(t),
notes=notes,
issues=issues,
)