refactor(gateway): consolidate commit + notify role gates into verb_gates

Replaces hardcoded role-string-constants in content_actions.py
(_COMMIT_ALLOWED_ROLES, _NOTIFY_ALLOWED_ROLES) with calls into
verb_gates.is_verb_allowed against a synthetic in-progress task probe.
Pre-fix the same role lists lived in both content_actions and
verb_gates; if one drifted the other would mask it. Now there's one
table.

Adds `notify` to verb_gates._ALWAYS_AVAILABLE for cell_pm, main_pm,
product_owner, head_marketing.

Note: i_will_plan / delegate role checks INTENTIONALLY stay as
explicit `role not in (cell_pm, main_pm)` checks, not is_verb_allowed.
Their state checks must surface as `invalid_state` (a different
agent-side error code) — conflating them with role-state combo
checks breaks the rejection-code semantics agents rely on.

Tests: 3127 passing, 100% coverage, ruff clean.
This commit is contained in:
Renn F
2026-05-08 11:42:04 +02:00
parent ebdbd7fc47
commit 2eeefb2ee1
4 changed files with 79 additions and 23 deletions
+34
View File
@@ -171,3 +171,37 @@ def test_is_verb_allowed_false_for_blocked_verb() -> None:
def test_is_verb_allowed_false_for_unknown_role() -> None:
assert is_verb_allowed("nope", "i_am_idle", _task("pending")) is False
# ---------------------------------------------------------------------
# Task 4: verb_gates is the single source of truth for content tools too
# ---------------------------------------------------------------------
def test_commit_allowed_for_developer_and_documenter_only() -> None:
"""Was a hardcoded set in content_actions; now lives in verb_gates."""
task = _task("in_progress", task_type="code")
assert is_verb_allowed("developer", "commit", task) is True
assert is_verb_allowed("documenter", "commit", task) is True
assert is_verb_allowed("qa", "commit", task) is False
assert is_verb_allowed("cell_pm", "commit", task) is False
assert is_verb_allowed("main_pm", "commit", task) is False
def test_commit_not_offered_when_task_is_not_in_progress() -> None:
"""commit is a per-state verb — not offered when state forbids it."""
task = _task("completed", task_type="code")
assert is_verb_allowed("developer", "commit", task) is False
def test_notify_allowed_for_pm_and_board_only() -> None:
"""Was a hardcoded set in content_actions; now lives in verb_gates."""
task = _task("in_progress", task_type="code")
assert is_verb_allowed("cell_pm", "notify", task) is True
assert is_verb_allowed("main_pm", "notify", task) is True
assert is_verb_allowed("product_owner", "notify", task) is True
assert is_verb_allowed("head_marketing", "notify", task) is True
assert is_verb_allowed("developer", "notify", task) is False
assert is_verb_allowed("qa", "notify", task) is False
assert is_verb_allowed("documenter", "notify", task) is False
assert is_verb_allowed("auditor", "notify", task) is False