diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index e19539b78a..c84b7da842 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -296,6 +296,51 @@ def _apply_requested_quota_action_selection_preflight( if deferred else "quota_action_selection_rejected" ) + # A workspace-repair obligation is the one deferral whose recovery is not a + # frontier question at all: the turn has to move off the shared checkout + # before any selection can be honored. Reported as "control_repair" it reads + # like an unrelated control-plane preemption, so the reason names the move + # and the recommendation names the worktree rerun. + workspace_repair_deferred = ( + deferred + and payload.get("workspace_repair_allowed") is True + and execution_obligation.get("kind") == "agent_workspace_repair" + ) + workspace_repair_contract = str(execution_obligation.get("contract") or "").strip() + if workspace_repair_deferred: + reason = ( + "explicit action selection was deferred because this turn has to move " + f"the workspace first: {workspace_repair_contract}" + if workspace_repair_contract + else "explicit action selection was deferred because this turn has to " + "move the workspace off the shared checkout first" + ) + recommended_action = ( + "rerun quota should-run from an independent worktree or branch for this " + "agent instead of the shared checkout, with the same " + "--turn-instance-id and --todo-id" + ) + else: + reason = ( + "explicit action selection was deferred by the current " + f"delivery frontier: {qualification_reason}" + if deferred + else "explicit action selection is not currently eligible: " + f"{qualification_reason}" + ) + recommended_action = ( + "handle the current delivery preemption, then rerun quota " + "should-run with the same --turn-instance-id; omit --todo-id " + "first when a refreshed action portfolio is needed" + if deferred + else "the due monitor is visible as auxiliary context, not an " + "independently selectable action in the current advancement lane; " + "choose a current advancement Todo, or rerun after the monitor " + "becomes the hard lane" + if auxiliary_monitor + else "rerun quota should-run with the same --turn-instance-id " + "without --todo-id, then choose a currently eligible Todo" + ) payload.update( { "ok": False, @@ -306,26 +351,8 @@ def _apply_requested_quota_action_selection_preflight( "waiting_on": "codex", "status": error_code, "error_code": error_code, - "reason": ( - "explicit action selection was deferred by the current " - f"delivery frontier: {qualification_reason}" - if deferred - else "explicit action selection is not currently eligible: " - f"{qualification_reason}" - ), - "recommended_action": ( - "handle the current delivery preemption, then rerun quota " - "should-run with the same --turn-instance-id; omit --todo-id " - "first when a refreshed action portfolio is needed" - if deferred - else "the due monitor is visible as auxiliary context, not an " - "independently selectable action in the current advancement lane; " - "choose a current advancement Todo, or rerun after the monitor " - "becomes the hard lane" - if auxiliary_monitor - else "rerun quota should-run with the same --turn-instance-id " - "without --todo-id, then choose a currently eligible Todo" - ), + "reason": reason, + "recommended_action": recommended_action, } ) return True diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index 9ec52e4c6c..d16bea2908 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -12,6 +12,9 @@ import pytest from loopx.bootstrap_command_pack import build_start_goal_guided_packet +from loopx.cli_commands.quota import ( + _apply_requested_quota_action_selection_preflight, +) from loopx.control_plane.work_items.delivery_outcome import ( PROGRESS_DELIVERY_OUTCOMES, DeliveryOutcome, @@ -5583,3 +5586,95 @@ def test_legacy_read_only_workspace_mismatch_fails_then_corrects_from_todo_contr receipt["step_kind"] for receipt in replay["settlement_result"]["receipts"] ] == ["validation", "durable_writeback", "quota_spend"] assert _spend_run_count(runtime) == 1 + + +def _deferred_selection_payload( + *, + reason: str, + preemptions: list[str], + workspace_repair: bool, +) -> dict[str, object]: + payload: dict[str, object] = { + "ok": True, + "decision": "run", + "should_run": True, + # A deferred selection is by definition one delivery may not honor yet; + # the two deferrals differ only in which repair the turn owes first. + "normal_delivery_allowed": False, + "workspace_repair_allowed": workspace_repair, + "selected_todo": { + "schema_version": "quota_selected_todo_v0", + "todo_id": ALTERNATIVE_TODO_ID, + "selection_binding": "pending_action_selection", + }, + "action_selection_qualification": { + "schema_version": "action_selection_qualification_v0", + "state": "deferred", + "requested_todo_id": ALTERNATIVE_TODO_ID, + "reason": reason, + "delivery_preemptions": preemptions, + }, + } + if workspace_repair: + payload["execution_obligation"] = { + "kind": "agent_workspace_repair", + "must_attempt_work": True, + "delivery_allowed": False, + "minimum": "one_workspace_move_then_guard_rerun", + "contract": ( + "do not edit repository files from a shared checkout; create or " + "switch to an independent worktree/branch, then rerun quota " + "should-run with the same agent id" + ), + } + return payload + + +def test_workspace_repair_deferral_names_the_workspace_move() -> None: + """The one deferral an agent can act on must not read as a frontier verdict. + + Selecting from the shared checkout returns + ``state=quota_action_selection_deferred`` with ``reason=control_repair``. + Read literally that points at an unrelated control-plane preemption, and an + agent can burn several turns re-reading the portfolio before noticing that + the payload's ``execution_obligation.kind`` is ``agent_workspace_repair`` + and the only required move is to select from a worktree. The message now + says so. + """ + + payload = _deferred_selection_payload( + reason="control_repair", + preemptions=["control_repair", "delivery_not_allowed"], + workspace_repair=True, + ) + applied = _apply_requested_quota_action_selection_preflight( + payload, + requested_todo_id=ALTERNATIVE_TODO_ID, + receipt_bound_todo_id=None, + receipt_bound_replan_obligation_id=None, + ) + + assert applied is True + assert "move the workspace first" in payload["reason"] + assert "shared checkout" in payload["reason"] + assert "independent worktree or branch" in payload["recommended_action"] + assert "--turn-instance-id" in payload["recommended_action"] + assert "delivery frontier" not in payload["reason"] + + +def test_unrelated_deferral_keeps_the_frontier_wording() -> None: + payload = _deferred_selection_payload( + reason="autonomous_replan", + preemptions=["autonomous_replan", "delivery_not_allowed"], + workspace_repair=False, + ) + applied = _apply_requested_quota_action_selection_preflight( + payload, + requested_todo_id=ALTERNATIVE_TODO_ID, + receipt_bound_todo_id=None, + receipt_bound_replan_obligation_id=None, + ) + + assert applied is True + assert "delivery frontier: autonomous_replan" in payload["reason"] + assert "handle the current delivery preemption" in payload["recommended_action"]