W7: Possibilities matrix (work-already-done fast path) (#522)

* [W7] Add possibilities_matrix_enabled feature flag (default off)

* [W7] Add _work_appears_done predicate (status+commits+PR+ACs+no-open-findings)

* [W7] Add CI-green quality proxy for the fast path (local fallback on no-CI)

* [W7] Add work-already-done fast path in i_am_done (slimmed gates, no rich plan)

* [W7] Add WORK_ALREADY_DONE prompt state

* [W7] Make fast path mypy-clean (cast to helpers for _resolve_ci_status; typed mock locals)

* [W7] Extract _all_criteria_addressed to bring _work_appears_done under xenon B

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-15 04:33:24 +02:00
committed by GitHub
co-authored by Renn F
parent 0089e95489
commit f2e5676198
9 changed files with 809 additions and 8 deletions
@@ -0,0 +1,27 @@
"""Possibilities matrix (work-already-done fast path) is gated by a default-off
config flag, registered as a panel-tunable feature flag (the #487 lesson)."""
from __future__ import annotations
import os
from unittest import mock
from roboco.config import Settings
from roboco.services.settings import FEATURE_FLAGS, validate_setting
def test_possibilities_matrix_disabled_by_default() -> None:
assert Settings().possibilities_matrix_enabled is False
def test_possibilities_matrix_reads_env_var() -> None:
with mock.patch.dict(os.environ, {"ROBOCO_POSSIBILITIES_MATRIX_ENABLED": "true"}):
assert Settings().possibilities_matrix_enabled is True
def test_possibilities_matrix_flag_registered_in_feature_flags() -> None:
assert "possibilities_matrix_enabled" in [key for key, _ in FEATURE_FLAGS]
def test_possibilities_matrix_flag_validates_as_bool() -> None:
validate_setting("possibilities_matrix_enabled", "true")
@@ -0,0 +1,367 @@
"""W7 possibilities matrix: the ``_work_appears_done`` predicate + fast path.
A task whose work is already done (commits + PR open + every acceptance
criterion addressed + no open findings) qualifies for the fast path. These
tests pin the predicate's truth table and the schema it actually reads (the
per-criterion rows the writer persists use ``artifact_ref``; the predicate
unions ``artifact_ref`` / ``referencing_artifact_id`` / ``addressed`` so it
sees real data, not the latent reader/writer key drift).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
import pytest
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
from roboco.services.gateway.envelope import Envelope
def _deps() -> ChoreographerDeps:
return ChoreographerDeps(
task=AsyncMock(),
work_session=AsyncMock(),
git=AsyncMock(),
a2a=AsyncMock(),
journal=AsyncMock(),
audit=AsyncMock(),
evidence_repo=AsyncMock(),
)
async def _no_findings(_task_id: Any) -> tuple[()]:
return ()
async def _one_open(_task_id: Any) -> tuple[str, ...]:
return ("f1abcd12",)
def _t(
*,
status: str = "claimed",
commits: tuple[int, ...] = (1,),
pr_created: bool = True,
criteria: tuple[str, ...] = ("ac1", "ac2"),
ac_status: list[dict[str, Any]] | None = None,
) -> MagicMock:
if ac_status is None:
ac_status = [
{"criterion": "ac1", "addressed": True, "artifact_ref": "sha1"},
{"criterion": "ac2", "addressed": True, "artifact_ref": "sha2"},
]
t = MagicMock()
t.id = uuid4()
t.status = status
t.commits = commits
t.pr_created = pr_created
t.pr_number = 12345 if pr_created else None
t.acceptance_criteria = list(criteria)
t.acceptance_criteria_status = ac_status
return t
@pytest.mark.asyncio
async def test_work_appears_done_true_when_all_hold(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(c, "_open_finding_ids", _no_findings)
assert await c._work_appears_done(_t()) is True
@pytest.mark.asyncio
async def test_work_appears_done_false_when_no_pr(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(c, "_open_finding_ids", _no_findings)
assert await c._work_appears_done(_t(pr_created=False)) is False
@pytest.mark.asyncio
async def test_work_appears_done_false_when_no_commits(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(c, "_open_finding_ids", _no_findings)
assert await c._work_appears_done(_t(commits=())) is False
@pytest.mark.asyncio
async def test_work_appears_done_false_when_ac_unaddressed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(c, "_open_finding_ids", _no_findings)
t = _t(
ac_status=[
{"criterion": "ac1", "addressed": True, "artifact_ref": "sha1"},
{"criterion": "ac2", "addressed": False, "artifact_ref": None},
]
)
assert await c._work_appears_done(t) is False
@pytest.mark.asyncio
async def test_work_appears_done_true_with_no_criteria(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(c, "_open_finding_ids", _no_findings)
assert await c._work_appears_done(_t(criteria=(), ac_status=[])) is True
@pytest.mark.asyncio
async def test_work_appears_done_false_when_open_finding(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(c, "_open_finding_ids", _one_open)
assert await c._work_appears_done(_t()) is False
@pytest.mark.asyncio
async def test_work_appears_done_false_when_terminal_status(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(c, "_open_finding_ids", _no_findings)
assert await c._work_appears_done(_t(status="awaiting_qa")) is False
assert await c._work_appears_done(_t(status="completed")) is False
assert await c._work_appears_done(_t(status="needs_revision")) is False
@pytest.mark.asyncio
async def test_work_appears_done_reads_referencing_artifact_id_schema(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(c, "_open_finding_ids", _no_findings)
t = _t(
ac_status=[
{"criterion": "ac1", "referencing_artifact_id": "sha1"},
{"criterion": "ac2", "referencing_artifact_id": "sha2"},
]
)
assert await c._work_appears_done(t) is True
# ---------------------------------------------------------------------------
# _fast_path_quality_verdict: CI-green proxy for the skipped local gate
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_quality_verdict_ci_success_skips_local_gate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(
c, "_resolve_ci_status", AsyncMock(return_value={"state": "success"})
)
local = AsyncMock(return_value=None)
monkeypatch.setattr(c, "_check_quality_gate", local)
rejection, ran_local = await c._fast_path_quality_verdict(
MagicMock(task_id=uuid4(), task=MagicMock(), briefing={})
)
assert rejection is None
assert ran_local is False
local.assert_not_awaited()
@pytest.mark.asyncio
async def test_quality_verdict_ci_failure_refuses(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(
c,
"_resolve_ci_status",
AsyncMock(return_value={"state": "failure", "failing_checks": ["lint"]}),
)
local = AsyncMock(return_value=None)
monkeypatch.setattr(c, "_check_quality_gate", local)
rejection, ran_local = await c._fast_path_quality_verdict(
MagicMock(task_id=uuid4(), task=MagicMock(), briefing={})
)
assert rejection is not None
assert ran_local is False
local.assert_not_awaited()
@pytest.mark.asyncio
async def test_quality_verdict_no_ci_falls_back_to_local_gate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(
c, "_resolve_ci_status", AsyncMock(return_value={"state": "no_ci_configured"})
)
local = AsyncMock(return_value=None)
monkeypatch.setattr(c, "_check_quality_gate", local)
rejection, ran_local = await c._fast_path_quality_verdict(
MagicMock(task_id=uuid4(), task=MagicMock(), briefing={})
)
assert rejection is None
assert ran_local is True
local.assert_awaited_once()
@pytest.mark.asyncio
async def test_quality_verdict_unresolvable_falls_back_to_local_gate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
monkeypatch.setattr(c, "_resolve_ci_status", AsyncMock(return_value=None))
local = AsyncMock(return_value=None)
monkeypatch.setattr(c, "_check_quality_gate", local)
rejection, ran_local = await c._fast_path_quality_verdict(
MagicMock(task_id=uuid4(), task=MagicMock(), briefing={})
)
assert rejection is None
assert ran_local is True
local.assert_awaited_once()
# ---------------------------------------------------------------------------
# _i_am_done_fast_path: gate ordering + transition chain (skips rich plan)
# ---------------------------------------------------------------------------
def _ctx(status: str = "claimed") -> Any:
ctx = MagicMock()
ctx.agent_id = uuid4()
ctx.task_id = uuid4()
ctx.task = _t(status=status)
ctx.briefing = {}
ctx.notes = "done"
ctx.resolved_findings = None
ctx.role_str = "developer"
return ctx
@dataclass
class _FastPathMocks:
"""Typed handles to the patched verb-boundary mocks. mypy keeps the
declared method types on ``c.<method>`` even after ``monkeypatch.setattr``
(the runtime override is invisible to static analysis), so assertions
must go through these locals typed as ``AsyncMock`` — not ``c.<method>``."""
ok: AsyncMock
reject: AsyncMock
conventions: AsyncMock
open_findings: AsyncMock
record_milestone: AsyncMock
def _stub_fast_path(
c: Choreographer, monkeypatch: pytest.MonkeyPatch, quality_rejection: Any = None
) -> _FastPathMocks:
conventions = AsyncMock(return_value=None)
open_findings = AsyncMock(return_value=())
ok = AsyncMock(return_value="OK")
reject = AsyncMock(return_value="REJECT")
record_milestone = AsyncMock(return_value=None)
monkeypatch.setattr(c, "_apply_resolved_findings", AsyncMock(return_value=None))
monkeypatch.setattr(c, "_check_submit_qa_field_gates", AsyncMock(return_value=None))
monkeypatch.setattr(c, "_behind_base_gate", AsyncMock(return_value=None))
monkeypatch.setattr(c, "_ensure_branch_pushed", AsyncMock(return_value=None))
monkeypatch.setattr(c, "_conventions_gate", conventions)
monkeypatch.setattr(c, "_open_finding_ids", open_findings)
monkeypatch.setattr(
c,
"_fast_path_quality_verdict",
AsyncMock(return_value=(quality_rejection, False)),
)
monkeypatch.setattr(c, "_notify_qa", AsyncMock(return_value=None))
monkeypatch.setattr(c, "_touch", AsyncMock(return_value=None))
monkeypatch.setattr(c, "_record_milestone_progress", record_milestone)
monkeypatch.setattr(c, "_build_i_am_done_ok", ok)
monkeypatch.setattr(c, "_reject_i_am_done", reject)
return _FastPathMocks(
ok=ok,
reject=reject,
conventions=conventions,
open_findings=open_findings,
record_milestone=record_milestone,
)
@pytest.mark.asyncio
async def test_fast_path_claimed_starts_without_set_plan(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
stubs = _stub_fast_path(c, monkeypatch)
await c._i_am_done_fast_path(_ctx(status="claimed"))
stubs.ok.assert_awaited_once() # OK path taken, no rejection
c.task.start.assert_awaited_once() # claimed -> in_progress
c.task.set_plan.assert_not_awaited() # rich plan SKIPPED (no set_plan)
c.task.submit_verification.assert_awaited_once()
c.task.submit_qa.assert_awaited_once()
stubs.conventions.assert_awaited_once() # conventions KEPT
stubs.record_milestone.assert_awaited_once()
@pytest.mark.asyncio
async def test_fast_path_in_progress_skips_start(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
stubs = _stub_fast_path(c, monkeypatch)
await c._i_am_done_fast_path(_ctx(status="in_progress"))
stubs.ok.assert_awaited_once()
c.task.start.assert_not_awaited() # already in_progress
c.task.submit_verification.assert_awaited_once()
c.task.submit_qa.assert_awaited_once()
@pytest.mark.asyncio
async def test_fast_path_open_findings_blocks(monkeypatch: pytest.MonkeyPatch) -> None:
c = Choreographer(_deps())
stubs = _stub_fast_path(c, monkeypatch)
monkeypatch.setattr(c, "_open_finding_ids", AsyncMock(return_value=("f1abcd12",)))
await c._i_am_done_fast_path(_ctx())
stubs.reject.assert_awaited_once()
c.task.submit_qa.assert_not_awaited()
@pytest.mark.asyncio
async def test_fast_path_conventions_block_rejects(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
stubs = _stub_fast_path(c, monkeypatch)
monkeypatch.setattr(
c,
"_conventions_gate",
AsyncMock(return_value=Envelope.invalid_state(message="x", remediate="fix")),
)
await c._i_am_done_fast_path(_ctx())
stubs.reject.assert_awaited_once()
c.task.submit_qa.assert_not_awaited()
@pytest.mark.asyncio
async def test_fast_path_ci_failure_rejects_before_transition(
monkeypatch: pytest.MonkeyPatch,
) -> None:
c = Choreographer(_deps())
stubs = _stub_fast_path(c, monkeypatch)
monkeypatch.setattr(
c,
"_fast_path_quality_verdict",
AsyncMock(
return_value=(
Envelope.invalid_state(message="ci red", remediate="fix"),
False,
)
),
)
await c._i_am_done_fast_path(_ctx())
stubs.reject.assert_awaited_once()
c.task.submit_qa.assert_not_awaited()
@@ -0,0 +1,149 @@
"""The possibilities-matrix proxy rewrites a dev prompt to WORK_ALREADY_DONE when
the flag is armed and the task already carries commits + an open PR.
The proxy is a cheap sync read — it does NOT re-check the async DB gates
(AC coverage, open findings); the server fast path ``_i_am_done_fast_path`` is
the authority. The prompt just collapses a 3-5 turn re-derivation to one
``i_am_done`` call. With the flag off (or the precondition unmet) the existing
``WORKFLOW STATE`` mapping is byte-for-byte unchanged.
"""
from __future__ import annotations
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, patch
from uuid import uuid4
import pytest
from roboco.config import settings
from roboco.runtime.orchestrator import AgentOrchestrator
if TYPE_CHECKING:
from collections.abc import AsyncIterator
def _orch() -> AgentOrchestrator:
orch = object.__new__(AgentOrchestrator)
orch._instances = {}
return orch
def _task(**over: Any) -> dict[str, Any]:
base: dict[str, Any] = {
"id": str(uuid4()),
"title": "Ship it",
"status": "in_progress",
"plan": "did the thing",
"pr_created": True,
"commits": [{"sha": "abc"}],
}
base.update(over)
return base
def _no_findings_db() -> tuple[Any, Any]:
"""Patch the findings fetch out so a needs_revision prompt builds no DB."""
@asynccontextmanager
async def _fake_ctx() -> AsyncIterator[AsyncMock]:
yield AsyncMock()
repo = AsyncMock()
repo.list_for_task = AsyncMock(return_value=[])
return (
patch("roboco.db.base.get_db_context", _fake_ctx),
patch(
"roboco.services.repositories.review_findings.ReviewFindingsRepository",
return_value=repo,
),
)
@pytest.mark.asyncio
async def test_armed_in_progress_with_pr_and_commits_is_work_already_done(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(settings, "possibilities_matrix_enabled", True)
prompt = await _orch()._build_dev_prompt(_task(status="in_progress"))
assert "WORK ALREADY DONE" in prompt
assert "i_am_done(task_id=" in prompt
@pytest.mark.asyncio
async def test_armed_claimed_with_pr_and_commits_is_work_already_done(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(settings, "possibilities_matrix_enabled", True)
prompt = await _orch()._build_dev_prompt(_task(status="claimed"))
assert "WORK ALREADY DONE" in prompt
@pytest.mark.asyncio
async def test_armed_verifying_with_pr_and_commits_is_work_already_done(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(settings, "possibilities_matrix_enabled", True)
prompt = await _orch()._build_dev_prompt(_task(status="verifying"))
assert "WORK ALREADY DONE" in prompt
@pytest.mark.asyncio
async def test_flag_off_leaves_in_progress_as_executing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(settings, "possibilities_matrix_enabled", False)
prompt = await _orch()._build_dev_prompt(_task(status="in_progress"))
assert "WORK ALREADY DONE" not in prompt
assert "WORKFLOW STATE: EXECUTING" in prompt
@pytest.mark.asyncio
async def test_armed_without_pr_stays_on_standard_path(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(settings, "possibilities_matrix_enabled", True)
prompt = await _orch()._build_dev_prompt(
_task(status="in_progress", pr_created=False)
)
assert "WORK ALREADY DONE" not in prompt
assert "WORKFLOW STATE: EXECUTING" in prompt
@pytest.mark.asyncio
async def test_armed_without_commits_stays_on_standard_path(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(settings, "possibilities_matrix_enabled", True)
prompt = await _orch()._build_dev_prompt(_task(status="in_progress", commits=[]))
assert "WORK ALREADY DONE" not in prompt
assert "WORKFLOW STATE: EXECUTING" in prompt
@pytest.mark.asyncio
async def test_armed_needs_revision_is_unaffected(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# needs_revision is not in the proxy's status set — REVISION_REQUIRED wins.
monkeypatch.setattr(settings, "possibilities_matrix_enabled", True)
db_ctx, repo_ctx = _no_findings_db()
with db_ctx, repo_ctx:
prompt = await _orch()._build_dev_prompt(_task(status="needs_revision"))
assert "WORK ALREADY DONE" not in prompt
assert "REVISION REQUESTED" in prompt
@pytest.mark.asyncio
async def test_flag_off_claimed_no_plan_is_needs_plan(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(settings, "possibilities_matrix_enabled", False)
prompt = await _orch()._build_dev_prompt(
_task(status="claimed", plan=None, pr_created=True, commits=[{"sha": "x"}])
)
assert "WORK ALREADY DONE" not in prompt
assert "WORKFLOW STATE: NEEDS_PLAN" in prompt
if __name__ == "__main__":
pytest.main([__file__, "-q"])