Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions loopx/control_plane/quota/should_run_packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,9 @@ def _planning_projections(
route.workspace_repair_allowed
and prepared.workspace_guard
and prepared.normal_delivery_allowed
) or bool(
route.effective_action == "boundary_projection_repair"
and prepared.boundary_projection_repair
)
projection_enabled = bool(
route.should_run
Expand Down
1 change: 1 addition & 0 deletions skills/loopx-self-repair/references/repair-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ teaches a reusable control-plane lesson.
| `shared_adapter_local_path_leak` | A shared adapter projection, such as Lark Kanban sync, would write an absolute local checkout, runtime, active-state, raw-log, or artifact path into a remote/shared row. | adapter dry-run payload, generated record values, docs boundary section, public/private scan. | Adapter reused local executor context as shared control-plane metadata. | Strip or replace the local path at the adapter projection layer, add a focused smoke that rejects the path, and only store explicit worker launch context when the row was authored for that purpose. |
| `peer_workspace_boundary_gap` | A peer edits or prepares repository changes from a non-git directory, unrelated worktree, or checkout that violates the selected task/repository isolation policy; a cross-repository repair may also be misclassified against the goal repo. | `agent_identity`, selected todo `task_repository` and write scopes, goal `repo`, current git origin/worktree/root, quota payload, heartbeat prompt commands, AGENTS worktree policy. | Workspace safety was inferred from identity labels, prompt text, or the goal repo alone instead of the selected task and `quota should-run`. | Add or repair `agent_workspace_guard_v1`; use a credential-free first-class `task_repository` only for genuine cross-repository tasks, and require a matching linked worktree. Block delivery and spend until the peer moves to a compliant worktree and reruns the guard with the same `--agent-id`. Keep the goal repo fallback and do not treat the field as scope or write authority. Read-only and monitor-only work should not be blocked merely by identity. |
| `workspace_guard_action_selection_projection_gap` | A turn starts in a worktree that matches one eligible Todo but not the default Todo; quota emits `workspace_guard`, commits an identity-less receipt, omits the action portfolio, and rejects same-turn selection of the matching Todo as unprojected. | Initial and selected `quota should-run` packets, selected Todo repositories, workspace guard, action portfolio, interaction selection command, and heartbeat receipt identity. | The workspace repair route correctly disabled default delivery, but action-portfolio projection incorrectly treated that route as if no eligible alternative could be selected. | Keep the workspace guard fail-closed for the default Todo, but preserve an identity-less action portfolio when ordinary delivery was otherwise eligible. Revalidate the explicitly selected Todo and its exact workspace before binding the receipt. Cover guard-first selection and same-turn upgrade without weakening repository isolation. |
| `boundary_repair_action_selection_projection_gap` | Quota selects a Todo whose required write scopes are absent from the current Goal boundary, emits `boundary_projection_repair`, and binds that blocked Todo even though another eligible Todo is runnable; same-turn selection of the alternative then fails with a receipt identity conflict. | Initial and explicitly selected `quota should-run` packets, Goal write scope, selected Todo write scopes, action portfolio, interaction selection command, and append-only heartbeat receipt events. | Boundary repair correctly blocked the default delivery, but the planning gate treated self-repair as if no eligible alternative could be selected, so recommendation order became hidden settlement authority. | Keep boundary repair fail-closed for the mismatched Todo while projecting the typed action portfolio when alternatives exist. Commit the first guard identity-less, revalidate an explicit choice against the live boundary, and append one deterministic identity upgrade without rewriting the original receipt. Retain hard conflicts for genuinely bound settlements and cover the full CLI round trip. |
| `monitor_successor_routing_gap` | A material monitor poll creates a natural-language `advance` successor for another repository, but the child has no repository, action, continuation, capabilities, stable target, or creation receipt; quota then routes plausible work without a safe workspace contract. | Parent monitor metadata, `quota monitor-poll` argv and artifact, child Todo metadata, rollout event successor ids, selected Todo workspace guard. | The agent extended a bounded observation into standing work, while the monitor-poll successor API accepted only text and an optional claimer and discarded typed routing. | Retire the unintended monitor/vision. Require explicit action semantics for every monitor-generated agent successor and explicit successor repository when the source monitor is repository-bound; carry optional execution capabilities, default continuation to `independent_handoff`, and derive a stable transition target when none is supplied. Record compact successor ids and routing receipts in the monitor artifact and rollout event. External-repository work must re-enter through that typed Todo rather than prior observation prose. |
| `delivery_workspace_causality_projection_gap` | A validated delivery is written in one worktree, but `refresh-state` or `spend-slot` is attempted from the goal repo, a canonical checkout, or another repository; the workspace guard correctly rejects accounting and the agent must discover the right invocation by trial and error. | Latest unspent accountable run `delivery_workspace`, selected Todo `task_repository`, refresh command cwd or `--delivery-workspace-path`, spend command cwd, `workspace_guard`, and the generated `interaction_contract.cli_channel`. | The fail-closed accounting guard owns the invariant, but the machine interaction contract exposed only generic refresh/spend commands and omitted the causal workspace requirements. | Preserve the guard. For spend-capable selected Todos with an explicit `task_repository`, project compact `delivery_workspace_causality_v0`: refresh from the producing workspace or pass its explicit delivery path, then spend from the recorded accountable workspace. Omit it from default and no-spend hot paths. |
| `material_monitor_workspace_settlement_gap` | A repository-bound material monitor poll records validated progress and a durable writeback receipt, but `quota spend-slot` rejects settlement because the monitor event has no delivery workspace and the matching `refresh-state --delivery-workspace-path` is treated as an ordinary idempotent replay. | Turn-scoped settlement identity and receipts, selected Todo workspace causality, material monitor run, matching refresh-state result, and spend failure. | Material monitor writeback became accountable before refresh-state could attach the required independent-worktree snapshot, while replay handling made the first durable receipt terminal even when its workspace causality remained incomplete. | Keep ordinary receipt replay idempotent. When causality says a workspace is required, the prior writeback has none, and the caller supplies an explicit validated workspace path, let refresh-state append exactly one supplemental causal record; replay that supplement idempotently and require spend to validate the recorded repository/worktree. Cover the no-path no-op, one supplement, supplement replay, and final spend. |
Expand Down
10 changes: 10 additions & 0 deletions tests/control_plane/test_quota_boundary_projection_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest

from loopx.control_plane.quota.should_run import build_quota_should_run
from loopx.control_plane.quota.turn_envelope import build_turn_envelope
from loopx.control_plane.testing.quota_fixtures import (
quota_status_payload,
quota_todo_item,
Expand Down Expand Up @@ -215,6 +216,15 @@ def test_boundary_projection_preserves_a_guarded_continuation_selection() -> Non
assert packet["effective_action"] == "boundary_projection_repair"
assert packet["normal_delivery_allowed"] is False
assert packet["self_repair_allowed"] is True
assert packet["action_portfolio"]["selection_policy"][
"requires_explicit_turn_binding"
] is True
assert packet["interaction_contract"]["agent_channel"][
"selection_required"
] is True
envelope = build_turn_envelope(packet)
assert envelope["action"]["action_portfolio"] == packet["action_portfolio"]
assert envelope["writeback"]["selection_required"] is True
assert (
packet["boundary_projection_gap"]["selected_todo"]["todo_id"]
== (in_flight["todo_id"])
Expand Down
63 changes: 63 additions & 0 deletions tests/control_plane/test_quota_settlement_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,19 @@ def _configure_repository_write_todo(project: Path) -> Path:
return state_path


def _configure_boundary_blocked_primary(project: Path) -> Path:
state_path = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md"
state_text = state_path.read_text(encoding="utf-8")
state_path.write_text(
state_text.replace(
"action_kind=validate -->",
"action_kind=validate required_write_scopes=private/** -->",
),
encoding="utf-8",
)
return state_path


def _configure_selectable_alternative(
project: Path,
*,
Expand Down Expand Up @@ -2937,6 +2950,56 @@ def test_pending_selection_preserves_workspace_repair_then_reenters_same_turn(
assert _heartbeat_receipt_count(runtime, turn_instance_id) == 2


def test_boundary_projection_repair_keeps_same_turn_alternative_selectable(
tmp_path: Path,
) -> None:
project, runtime, registry_path = _write_fixture(tmp_path)
_configure_boundary_blocked_primary(project)
_configure_selectable_alternative(project)
turn_instance_id = "turn-boundary-repair-alternative-selection"
guard_args = (
"quota",
"should-run",
"--codex-app",
"--goal-id",
GOAL_ID,
"--agent-id",
AGENT_ID,
"--turn-instance-id",
turn_instance_id,
"--scan-path",
str(project),
)

first_rc, first = _run_cli(registry_path, runtime, *guard_args)
selected_rc, selected = _run_cli(
registry_path,
runtime,
*guard_args,
"--todo-id",
ALTERNATIVE_TODO_ID,
)

assert first_rc == 0, first
assert first["effective_action"] == "boundary_projection_repair"
assert first["selected_todo"]["todo_id"] == TODO_ID
assert first["action_portfolio"]["selection_policy"][
"requires_explicit_turn_binding"
] is True
assert "settlement_identity" not in first["heartbeat_receipt"]
assert selected_rc == 0, selected
assert selected.get("error_code") != "heartbeat_receipt_identity_conflict"
assert selected["effective_action"] == "normal_run"
assert selected["normal_delivery_allowed"] is True
assert selected["selected_todo"]["todo_id"] == ALTERNATIVE_TODO_ID
assert selected["selected_todo"]["selection_binding"] == "heartbeat_receipt"
assert selected["heartbeat_receipt"]["status"] == "upgraded"
assert selected["heartbeat_receipt"]["settlement_identity"]["todo_id"] == (
ALTERNATIVE_TODO_ID
)
assert _heartbeat_receipt_count(runtime, turn_instance_id) == 2


def test_selection_added_after_pending_guard_reports_final_boundary(
tmp_path: Path,
) -> None:
Expand Down