From ef3e76f0f3e9cc7df448b4457b00a10bdbcc7b38 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:03:36 +0800 Subject: [PATCH 1/4] refactor(todos): route production consumers to concrete owners Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/capabilities/explore/result_writeback.py | 3 ++- loopx/capabilities/explore/turn_context.py | 2 +- loopx/capabilities/issue_fix/cli.py | 2 +- loopx/capabilities/issue_fix/explore_projection.py | 3 ++- loopx/capabilities/issue_fix/pr_gate_reconcile.py | 3 ++- loopx/capabilities/issue_fix/pr_monitor_materialization.py | 3 ++- loopx/capabilities/issue_fix/pr_review_ack.py | 2 +- loopx/capabilities/issue_fix/pr_wait_reconcile.py | 2 +- loopx/capabilities/manager_context/goal_attention.py | 2 +- loopx/capabilities/periodic_report/pending_intent.py | 2 +- loopx/cli_commands/explore.py | 2 +- loopx/cli_commands/goal_amendment_proposal.py | 2 +- loopx/cli_commands/shared_goal_alignment.py | 2 +- loopx/cli_commands/todo.py | 6 +++--- loopx/cli_commands/todo_registration.py | 2 +- loopx/cli_commands/turn_run_once.py | 2 +- loopx/cli_commands/turn_todo_writeback.py | 2 +- loopx/control_plane/goals/task_planning.py | 2 +- .../testing/terminal_settlement_tool_behavior.py | 2 +- loopx/control_plane/todos/durable_completion.py | 4 ++-- loopx/control_plane/todos/goal_todo_projection.py | 4 ++-- .../control_plane/work_items/task_lease_acquire_adapter.py | 2 +- loopx/demo.py | 2 +- loopx/dreaming.py | 2 +- loopx/extensions/lark/presentation/kanban.py | 3 ++- loopx/quota.py | 2 +- loopx/semantics/project_registry_io_manifest_v1.json | 4 ++-- 27 files changed, 37 insertions(+), 32 deletions(-) diff --git a/loopx/capabilities/explore/result_writeback.py b/loopx/capabilities/explore/result_writeback.py index 65fba8a3c6..3e542c55f6 100644 --- a/loopx/capabilities/explore/result_writeback.py +++ b/loopx/capabilities/explore/result_writeback.py @@ -12,7 +12,8 @@ read_canonical_todos_if_promoted, ) from ...cli_commands.post_writeback import dispatch_committed_cli_post_writeback_hooks -from ...todos import list_goal_todos, update_goal_todo +from ...control_plane.todos.list_readback import list_goal_todos +from ...control_plane.todos.mutation_api import update_goal_todo from .turn_context import _policy from .result_log import ( append_explore_result_events, diff --git a/loopx/capabilities/explore/turn_context.py b/loopx/capabilities/explore/turn_context.py index 08ec01fd19..9708f5d4ce 100644 --- a/loopx/capabilities/explore/turn_context.py +++ b/loopx/capabilities/explore/turn_context.py @@ -15,7 +15,7 @@ ) from ...control_plane.effect_runtime import effect_runtime_result from ...explore_graph import compact_explore_graph_policy -from ...todos import list_goal_todos +from ...control_plane.todos.list_readback import list_goal_todos from .result_log import ( build_explore_result_projection, explore_result_log_path, diff --git a/loopx/capabilities/issue_fix/cli.py b/loopx/capabilities/issue_fix/cli.py index add8d5c5c9..65d8ad868a 100644 --- a/loopx/capabilities/issue_fix/cli.py +++ b/loopx/capabilities/issue_fix/cli.py @@ -18,7 +18,7 @@ upsert_issue_fix_feasibility_ledger_jsonl, upsert_issue_fix_pr_lifecycle_ledger_jsonl, ) -from ...todos import add_goal_todo +from ...control_plane.todos.mutation_api import add_goal_todo from .acceptance_loop import ( build_issue_fix_acceptance_fixture_packet, build_issue_fix_caller_repo_branch_packet, diff --git a/loopx/capabilities/issue_fix/explore_projection.py b/loopx/capabilities/issue_fix/explore_projection.py index 610ad4de96..86d3725a01 100644 --- a/loopx/capabilities/issue_fix/explore_projection.py +++ b/loopx/capabilities/issue_fix/explore_projection.py @@ -18,7 +18,8 @@ from ...history import load_registry from ...paths import resolve_runtime_root from ...rollout_event_log import load_rollout_events, rollout_event_log_path -from ...todos import resolve_todo_state_path, section_bounds, todo_blocks +from ...control_plane.todos.path_resolution import resolve_todo_state_path +from ...control_plane.todos.active_state_editing import section_bounds, todo_blocks from ..explore.result_log import ( EVENT_KIND_NODE, FINDING_STATUS_CONFIRMED, diff --git a/loopx/capabilities/issue_fix/pr_gate_reconcile.py b/loopx/capabilities/issue_fix/pr_gate_reconcile.py index d8101f857a..fd37b4ee93 100644 --- a/loopx/capabilities/issue_fix/pr_gate_reconcile.py +++ b/loopx/capabilities/issue_fix/pr_gate_reconcile.py @@ -9,7 +9,8 @@ normalize_todo_decision_scope, ) from ...control_plane.todos.todo_semantics import todo_item_task_class -from ...todos import complete_goal_todo, list_goal_todos +from ...control_plane.todos.mutation_api import complete_goal_todo +from ...control_plane.todos.list_readback import list_goal_todos from .pr_lifecycle import ( TERMINAL_PR_STATES, build_issue_fix_pr_lifecycle_monitor_packet, diff --git a/loopx/capabilities/issue_fix/pr_monitor_materialization.py b/loopx/capabilities/issue_fix/pr_monitor_materialization.py index 508e8d3e31..dca347e05c 100644 --- a/loopx/capabilities/issue_fix/pr_monitor_materialization.py +++ b/loopx/capabilities/issue_fix/pr_monitor_materialization.py @@ -19,7 +19,8 @@ acquire_task_lease, inspect_task_lease, release_task_lease, runtime_root_from_registry, TaskLeaseError, ) -from ...todos import add_goal_todo, complete_goal_todo, list_goal_todos, update_goal_todo +from ...control_plane.todos.mutation_api import add_goal_todo, complete_goal_todo, update_goal_todo +from ...control_plane.todos.list_readback import list_goal_todos ISSUE_FIX_GROUPED_MONITOR_WRITEBACK_SCHEMA_VERSION = "issue_fix_grouped_monitor_writeback_v0" DEFAULT_ISSUE_FIX_MONITOR_CADENCE = "30m" diff --git a/loopx/capabilities/issue_fix/pr_review_ack.py b/loopx/capabilities/issue_fix/pr_review_ack.py index 549891ae0a..35f504bf2d 100644 --- a/loopx/capabilities/issue_fix/pr_review_ack.py +++ b/loopx/capabilities/issue_fix/pr_review_ack.py @@ -19,7 +19,7 @@ load_rollout_events, rollout_event_log_path, ) -from ...todos import list_goal_todos +from ...control_plane.todos.list_readback import list_goal_todos from .metadata_preview import normalise_github_issue_reference diff --git a/loopx/capabilities/issue_fix/pr_wait_reconcile.py b/loopx/capabilities/issue_fix/pr_wait_reconcile.py index 7dd900ad9a..09a0aa9502 100644 --- a/loopx/capabilities/issue_fix/pr_wait_reconcile.py +++ b/loopx/capabilities/issue_fix/pr_wait_reconcile.py @@ -19,7 +19,7 @@ from ...rollout_event_log import ( append_rollout_event, build_rollout_event, rollout_event_log_path, ) -from ...todos import list_goal_todos +from ...control_plane.todos.list_readback import list_goal_todos from . import pr_lifecycle from .pr_lifecycle_rollout import append_pr_merge_rollout_event diff --git a/loopx/capabilities/manager_context/goal_attention.py b/loopx/capabilities/manager_context/goal_attention.py index 40649d1848..2c8034b573 100644 --- a/loopx/capabilities/manager_context/goal_attention.py +++ b/loopx/capabilities/manager_context/goal_attention.py @@ -55,7 +55,7 @@ def read_goal_attention( registry_path: Path, runtime_root: Path, goal_id: str, quota_packet: Mapping[str, Any], ) -> dict[str, Any]: """A current canonical Todo read is independent of stale/absent run history.""" - from ...todos import list_goal_todos + from ...control_plane.todos.list_readback import list_goal_todos try: listed = list_goal_todos(registry_path=registry_path, diff --git a/loopx/capabilities/periodic_report/pending_intent.py b/loopx/capabilities/periodic_report/pending_intent.py index 27ddae2bd6..5a63c19179 100644 --- a/loopx/capabilities/periodic_report/pending_intent.py +++ b/loopx/capabilities/periodic_report/pending_intent.py @@ -1352,7 +1352,7 @@ def _consume_pending_periodic_report_intent( path=publication_candidate_path, candidate=publication_candidate, ) - from ...todos import add_goal_todo + from ...control_plane.todos.mutation_api import add_goal_todo delivery = add_goal_todo( registry_path=registry_path, diff --git a/loopx/cli_commands/explore.py b/loopx/cli_commands/explore.py index f834ed87e6..3c9c1aac2c 100644 --- a/loopx/cli_commands/explore.py +++ b/loopx/cli_commands/explore.py @@ -54,7 +54,7 @@ ) from ..history import load_registry from ..paths import resolve_runtime_root -from ..todos import list_goal_todos +from ..control_plane.todos.list_readback import list_goal_todos from .explore_feishu_commands import ( FEISHU_SINK_EXPLORE_COMMANDS, handle_explore_feishu_command, diff --git a/loopx/cli_commands/goal_amendment_proposal.py b/loopx/cli_commands/goal_amendment_proposal.py index 7621c7e0e4..640d8f240d 100644 --- a/loopx/cli_commands/goal_amendment_proposal.py +++ b/loopx/cli_commands/goal_amendment_proposal.py @@ -37,7 +37,7 @@ ) from ..history import load_registry from ..runtime import validate_goal_id_path_segment -from ..todos import resolve_todo_state_path +from ..control_plane.todos.path_resolution import resolve_todo_state_path PrintPayload = Callable[ [dict[str, object], str, Callable[[dict[str, object]], str]], diff --git a/loopx/cli_commands/shared_goal_alignment.py b/loopx/cli_commands/shared_goal_alignment.py index 7cbb506ee6..c2bf98e064 100644 --- a/loopx/cli_commands/shared_goal_alignment.py +++ b/loopx/cli_commands/shared_goal_alignment.py @@ -15,7 +15,7 @@ from ..control_plane.goals.shared_goal_alignment import ( project_shared_goal_alignment, ) -from ..todos import resolve_todo_state_path +from ..control_plane.todos.path_resolution import resolve_todo_state_path PrintPayload = Callable[ [dict[str, object], str, Callable[[dict[str, object]], str]], diff --git a/loopx/cli_commands/todo.py b/loopx/cli_commands/todo.py index aeec7a91d4..9c76fa4f04 100644 --- a/loopx/cli_commands/todo.py +++ b/loopx/cli_commands/todo.py @@ -46,15 +46,15 @@ capture_first_party_host_goal_ref, ) from ..control_plane.goals.source_session_registry_state import exact_goal_ref -from ..todos import ( +from ..control_plane.todos.mutation_api import ( add_goal_todo, archive_completed_todos, complete_goal_todo, - list_goal_todos, - resolve_todo_state, supersede_goal_todo, update_goal_todo, ) +from ..control_plane.todos.list_readback import list_goal_todos +from ..control_plane.todos.path_resolution import resolve_todo_state from .todo_argument_validation import ( validate_capability_gap_options, validate_shared_todo_options, diff --git a/loopx/cli_commands/todo_registration.py b/loopx/cli_commands/todo_registration.py index 239c65b37e..01a48e2a73 100644 --- a/loopx/cli_commands/todo_registration.py +++ b/loopx/cli_commands/todo_registration.py @@ -4,7 +4,7 @@ from collections.abc import Callable from ..control_plane.todos.contract import TODO_CONTINUATION_POLICY_VALUES -from ..todos import ARCHIVE_COMPLETED_DEFAULT_MAX_ACTIVE_DONE +from ..control_plane.todos.mutation_api import ARCHIVE_COMPLETED_DEFAULT_MAX_ACTIVE_DONE from .todo_argument_validation import ( TODO_ACTION_FIELDS, register_todo_linkage_arguments, diff --git a/loopx/cli_commands/turn_run_once.py b/loopx/cli_commands/turn_run_once.py index 591231be45..0d4892984d 100644 --- a/loopx/cli_commands/turn_run_once.py +++ b/loopx/cli_commands/turn_run_once.py @@ -48,7 +48,7 @@ ) from ..quota import spend_quota_slot from ..state_refresh import refresh_state_run -from ..todos import resolve_todo_state_path +from ..control_plane.todos.path_resolution import resolve_todo_state_path from .turn_cadence import managed_cadence_start from .turn_decision import collect_turn_status_payload from .turn_dsh_host import build_dsh_host_runner diff --git a/loopx/cli_commands/turn_todo_writeback.py b/loopx/cli_commands/turn_todo_writeback.py index 975fff2544..303255334d 100644 --- a/loopx/cli_commands/turn_todo_writeback.py +++ b/loopx/cli_commands/turn_todo_writeback.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Any, Mapping -from ..todos import complete_goal_todo, update_goal_todo +from ..control_plane.todos.mutation_api import complete_goal_todo, update_goal_todo def write_turn_repair_update( diff --git a/loopx/control_plane/goals/task_planning.py b/loopx/control_plane/goals/task_planning.py index 6a354b133e..f12076289e 100644 --- a/loopx/control_plane/goals/task_planning.py +++ b/loopx/control_plane/goals/task_planning.py @@ -12,7 +12,7 @@ from ...execution_profile import execution_profile_is_fine_grained from ...history import load_registry from ...registry import find_registry_goal -from ...todos import list_goal_todos +from ...control_plane.todos.list_readback import list_goal_todos from ..todos.todo_semantics import todo_item_is_actionable_open from ..todos.contract import ( TODO_STATUS_BLOCKED, diff --git a/loopx/control_plane/testing/terminal_settlement_tool_behavior.py b/loopx/control_plane/testing/terminal_settlement_tool_behavior.py index 153cb401d1..7bb0f570e5 100644 --- a/loopx/control_plane/testing/terminal_settlement_tool_behavior.py +++ b/loopx/control_plane/testing/terminal_settlement_tool_behavior.py @@ -11,7 +11,7 @@ from typing import Any, cast from ...heartbeat_prompt import build_heartbeat_prompt -from ...todos import add_goal_todo, complete_goal_todo +from ...control_plane.todos.mutation_api import add_goal_todo, complete_goal_todo from ..quota.turn_envelope import quota_action_signature_document from .doubao_model_behavior_actor import ( ARK_API_KEY_ENV, diff --git a/loopx/control_plane/todos/durable_completion.py b/loopx/control_plane/todos/durable_completion.py index 125dc5e2ba..77c32b73fe 100644 --- a/loopx/control_plane/todos/durable_completion.py +++ b/loopx/control_plane/todos/durable_completion.py @@ -1,8 +1,8 @@ """Project the durable completion continuation for the built-in Turn CLI provider. ``loopx turn run-once`` completes the selected Todo through -:func:`loopx.todos.complete_goal_todo` and must report the continuation the Todo -lifecycle durably recorded (``successor``, ``no_followup``, or ``active_goal``) +:func:`loopx.control_plane.todos.mutation_api.complete_goal_todo` and must report +the continuation the Todo lifecycle durably recorded (``successor``, ``no_followup``, or ``active_goal``) instead of normalizing every completion to ``active_goal``. Projection reads the persisted Todo lifecycle record only; it never trusts host diff --git a/loopx/control_plane/todos/goal_todo_projection.py b/loopx/control_plane/todos/goal_todo_projection.py index 25fa7bcc3c..5e4fa34f8d 100644 --- a/loopx/control_plane/todos/goal_todo_projection.py +++ b/loopx/control_plane/todos/goal_todo_projection.py @@ -2,8 +2,8 @@ The text is a parameter rather than a file read so a writer that still holds the state-file lock can project the exact bytes it is about to commit; -``loopx.todos.list_goal_todos`` passes the on-disk text. Everything here is a -projection of those bytes and rollout metadata. Retired event sources are +``loopx.control_plane.todos.list_readback.list_goal_todos`` passes the on-disk +text. Everything here is a projection of those bytes and rollout metadata. Retired event sources are rejected before any Markdown substitution. """ diff --git a/loopx/control_plane/work_items/task_lease_acquire_adapter.py b/loopx/control_plane/work_items/task_lease_acquire_adapter.py index 098dac43bc..85678d2e33 100644 --- a/loopx/control_plane/work_items/task_lease_acquire_adapter.py +++ b/loopx/control_plane/work_items/task_lease_acquire_adapter.py @@ -165,7 +165,7 @@ def _task_lease_authority_projection( state_file: Path | None, project_todos: bool = True, ) -> tuple[Any, list[Any], list[dict[str, Any]], dict[str, Any] | None]: - from ...todos import list_goal_todos + from ...control_plane.todos.list_readback import list_goal_todos from ..goals.active_state_metadata import parse_state_frontmatter if goal is None: diff --git a/loopx/demo.py b/loopx/demo.py index 9af7dcee64..ca58e50765 100644 --- a/loopx/demo.py +++ b/loopx/demo.py @@ -11,7 +11,7 @@ from .quota import build_quota_should_run from .state_refresh import refresh_state_run from .status import collect_status -from .todos import add_goal_todo +from .control_plane.todos.mutation_api import add_goal_todo DEFAULT_DEMO_PROJECT = Path("/tmp/loopx-demo") diff --git a/loopx/dreaming.py b/loopx/dreaming.py index ad27b1a382..c93468c5b0 100644 --- a/loopx/dreaming.py +++ b/loopx/dreaming.py @@ -25,7 +25,7 @@ from .registry import registry_goals from .runtime import validate_goal_id_path_segment from .state_refresh import now_local -from .todos import add_goal_todo, update_goal_todo +from .control_plane.todos.mutation_api import add_goal_todo, update_goal_todo DREAMING_DRY_RUN_SCHEMA_VERSION = "dreaming_dry_run_proposal_v0" DREAMING_PROPOSAL_SCHEMA_VERSION = "dreaming_proposal_v0" diff --git a/loopx/extensions/lark/presentation/kanban.py b/loopx/extensions/lark/presentation/kanban.py index 5cab97bfec..d6c28dd850 100644 --- a/loopx/extensions/lark/presentation/kanban.py +++ b/loopx/extensions/lark/presentation/kanban.py @@ -2195,7 +2195,8 @@ def sync_loopx_todos_to_lark_kanban( build_issue_fix_outcome_collection_from_domain_state, ) from ....control_plane.todos.todo_semantics import todo_priority_label - from ....todos import resolve_todo_state_path, section_bounds, todo_blocks + from ....control_plane.todos.path_resolution import resolve_todo_state_path + from ....control_plane.todos.active_state_editing import section_bounds, todo_blocks resolved_project, resolved_state_file = resolve_todo_state_path( registry_path=registry_path, diff --git a/loopx/quota.py b/loopx/quota.py index 439600b243..6e8776e0ee 100644 --- a/loopx/quota.py +++ b/loopx/quota.py @@ -1229,7 +1229,7 @@ def should_run(current_status: dict[str, Any]) -> dict[str, Any]: # Discovery/selection is an open-work projection, not the committed # Turn's lifecycle authority. Read the exact bound record, including # completed history; TS monitor admission owns its interpretation. - from .todos import list_goal_todos + from .control_plane.todos.list_readback import list_goal_todos if registry_path is not None: bound_records = list_goal_todos( diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index e076e23f56..4d6a826884 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -199,7 +199,7 @@ }, { "site": "loopx/capabilities/issue_fix/explore_projection.py::.project_issue_fix_explore_graph::codec_read:load_registry#1", - "line": 666, + "line": 667, "column": 16, "kind": "codec_read", "api": "load_registry", @@ -215,7 +215,7 @@ }, { "site": "loopx/capabilities/issue_fix/pr_gate_reconcile.py::.reconcile_acknowledged_issue_fix_pr_reviews::codec_read:load_registry#1", - "line": 372, + "line": 373, "column": 35, "kind": "codec_read", "api": "load_registry", From f4fa7d616876931cccb82c30535c561fc9976250 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:03:36 +0800 Subject: [PATCH 2/4] test(todos): qualify consumer retirement across CLI and App Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_canonical_todo_writer_isolation.py | 16 +-- .../test_todo_consumer_host_retirement.py | 99 +++++++++++++++++++ .../test_todo_consumer_retirement.py | 80 +++++++++++++++ 3 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 tests/control_plane/test_todo_consumer_host_retirement.py create mode 100644 tests/control_plane/test_todo_consumer_retirement.py diff --git a/tests/control_plane/test_canonical_todo_writer_isolation.py b/tests/control_plane/test_canonical_todo_writer_isolation.py index 765d7e5a3a..5c79e17cf8 100644 --- a/tests/control_plane/test_canonical_todo_writer_isolation.py +++ b/tests/control_plane/test_canonical_todo_writer_isolation.py @@ -9,6 +9,7 @@ import shutil import subprocess import sys +import sysconfig import pytest @@ -46,7 +47,7 @@ def isolated_todo_distribution(tmp_path, monkeypatch, request): isolate_sqlite_runtime(tmp_path, monkeypatch) package_root = tmp_path / "package" package = package_root / "loopx" - command = [sys.executable, "-m", "loopx.cli"] + command = [sys.executable, "-S", "-m", "loopx.cli"] if request.param == "source": shutil.copytree(Path(__file__).resolve().parents[2] / "loopx", package, ignore=shutil.ignore_patterns("__pycache__")) @@ -59,10 +60,13 @@ def isolated_todo_distribution(tmp_path, monkeypatch, request): assert installed.returncode == 0, installed.stdout + installed.stderr entry = package_root / "bin/loopx" assert entry.is_file() - command = [sys.executable, str(entry)] - monkeypatch.setenv("PYTHONPATH", str(package_root)) + command = [sys.executable, "-S", str(entry)] + # Keep dependencies available without processing editable-install .pth files. + # Their import finder can silently borrow a deleted module from the checkout. + dependencies = dict.fromkeys(sysconfig.get_paths()[key] for key in ("purelib", "platlib")) + monkeypatch.setenv("PYTHONPATH", os.pathsep.join([str(package_root), *dependencies])) provenance = json.loads(subprocess.check_output( - [sys.executable, "-c", "import json,loopx; from importlib import metadata; " + [sys.executable, "-S", "-c", "import json,loopx; from importlib import metadata; " "print(json.dumps({'module':loopx.__file__, " "'distribution':str(metadata.distribution('loopx').locate_file(''))}))"], cwd=tmp_path, text=True, @@ -122,7 +126,7 @@ def test_pending_source_outbox_survives_producer_retirement( def history(): result = subprocess.run( - [sys.executable, "-c", + [sys.executable, "-S", "-c", "import json,sys;from pathlib import Path;" "from loopx.control_plane.coordination.local_authority_shadow_adapter " "import read_local_authority_shadow;" @@ -253,7 +257,7 @@ def test_handoff_mode_without_source_todo_writers(tmp_path, provider, without_so def cli(*args, succeeds=True, override=False): result = subprocess.run( - [sys.executable, "-m", "loopx.cli", "--format", "json", "--registry", str(registry), + [*without_source_todo_writers, "--format", "json", "--registry", str(registry), *(["--runtime-root", str(runtime)] if override else []), *args, "--goal-id", "goal-a"], cwd=caller, capture_output=True, text=True, timeout=45, ) diff --git a/tests/control_plane/test_todo_consumer_host_retirement.py b/tests/control_plane/test_todo_consumer_host_retirement.py new file mode 100644 index 0000000000..45140f3f4e --- /dev/null +++ b/tests/control_plane/test_todo_consumer_host_retirement.py @@ -0,0 +1,99 @@ +"""Real App HTTP and bounded extension calls in the same isolated distribution. + +Only the Lark command runner is synthetic; no remote message or Base write is +performed. App import, actions, providers, backup and restart are real. +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +from types import SimpleNamespace + +import pytest + +import test_chat_cold_source_import as cold +from test_cold_source_import_cli import workspace +from test_todo_consumer_retirement import ( + consumer_distribution, isolated_todo_distribution, retirement_wheel, # noqa: F401 -- shared fixtures +) + + +@pytest.fixture +def cold_api(tmp_path, monkeypatch, consumer_distribution): # noqa: F811 + package, _command = consumer_distribution + monkeypatch.setenv("LOOPX_COLD_IMPORT_PACKAGE", str(package)) + + def isolated_workspace(*args): + result = list(workspace(*args)) + # The copied receiver is first, with only the selected dependencies; + # -S prevents an editable finder restoring the removed modules. + env = result[-1] + env["PYTHONPATH"] = os.pathsep.join([env["PYTHONPATH"], os.environ["PYTHONPATH"]]) + return tuple(result) + + def selected_process(command, **kwargs): + assert command[0] == sys.executable + return subprocess.Popen([command[0], "-S", *command[1:]], **kwargs) + + def selected_run(command, **kwargs): + assert command[0] == sys.executable + return subprocess.run([command[0], "-S", *command[1:]], **kwargs) + + # Reuse the existing independent cold-import HTTP oracle and process + # lifetime, adapting only distribution/interpreter provenance in its harness. + monkeypatch.setattr(cold, "workspace", isolated_workspace) + monkeypatch.setattr(cold, "subprocess", SimpleNamespace( + Popen=selected_process, run=selected_run, DEVNULL=subprocess.DEVNULL)) + yield from cold.cold_api.__wrapped__( + tmp_path, monkeypatch, SimpleNamespace(param=False), True) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_app_import_actions_and_restart_without_legacy_consumers(cold_api, provider): + cold.test_cold_http_preview_reload_confirm_original_recovery(cold_api, provider) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_app_rejects_missing_lease_and_provider_without_legacy_consumers(cold_api, provider): + cold.test_app_enforces_lease_and_missing_provider_without_legacy_fallback(cold_api, provider) + + +def test_app_rejects_changed_source_and_backup_without_legacy_consumers(cold_api): + cold.test_cold_http_changed_source_backup_and_untrusted_inputs(cold_api) + + +def test_lark_and_explore_source_readers_without_historic_facade( + tmp_path, monkeypatch, consumer_distribution, # noqa: F811 +): + monkeypatch.setenv("LOOPX_COLD_IMPORT_PACKAGE", str(consumer_distribution[0])) + _, state, _, _, _, receiver, env = workspace(tmp_path, monkeypatch) + env["PYTHONPATH"] = os.pathsep.join([str(receiver), os.environ["PYTHONPATH"]]) + original = state.read_bytes() + registry = tmp_path / "project/.loopx/registry.json" + script = """ +import json,sys +from pathlib import Path +from loopx.extensions.lark.presentation.kanban import LarkKanbanConfig,sync_loopx_todos_to_lark_kanban +from loopx.capabilities.issue_fix.explore_projection import project_issue_fix_explore_graph +registry = Path(sys.argv[1]) +def forbidden_runner(*args, **kwargs): + raise AssertionError('preview must not execute remote commands') +config = LarkKanbanConfig(base_token='base_public_fixture',table_id='table_public_fixture') +result = sync_loopx_todos_to_lark_kanban(config,registry_path=registry,goal_id='cold', + execute=False,runner=forbidden_runner) +assert result['ok'] and result['todo_count'] == 1, result +assert not result['external_write_performed'] and result['successful_write_count'] == 0 +assert result['records'][0]['todo_id'] == 'todo_current' +first = project_issue_fix_explore_graph(registry_path=registry,goal_id='cold',execute=True) +second = project_issue_fix_explore_graph(registry_path=registry,goal_id='cold',execute=True) +assert second['appended_event_count'] == 0 +assert first['semantic_digest'] == second['semantic_digest'] +print(json.dumps({'todo_count': result['todo_count'], 'replayed': True})) +""" + result = subprocess.run([sys.executable, "-S", "-c", script, str(registry)], + cwd=tmp_path, env=env, capture_output=True, text=True, timeout=90) + assert result.returncode == 0, result.stdout + result.stderr + assert json.loads(result.stdout) == {"todo_count": 1, "replayed": True} + assert state.read_bytes() == original diff --git a/tests/control_plane/test_todo_consumer_retirement.py b/tests/control_plane/test_todo_consumer_retirement.py new file mode 100644 index 0000000000..77bbbec22e --- /dev/null +++ b/tests/control_plane/test_todo_consumer_retirement.py @@ -0,0 +1,80 @@ +"""Production Todo consumers use retained owners, without the historic facade. + +The public import remains supported; physical removal here is a disposable +qualification probe, not a supported package format or a migration grant. +""" +from __future__ import annotations + +import json +import subprocess +import sys + +import pytest + +import test_canonical_todo_writer_isolation as journeys +from test_canonical_todo_writer_isolation import ( + isolated_todo_distribution, retirement_wheel, # noqa: F401 -- distribution fixtures +) + + +@pytest.fixture(params=[True, False], ids=["facade-present", "facade-absent"]) +def consumer_distribution(request, isolated_todo_distribution): # noqa: F811 + package, command = isolated_todo_distribution + journeys._remove_source_todo_writers(package) + journeys._remove_monitor_source_modules(package) + if not request.param: + (package / "todos.py").unlink() + probe = subprocess.run( + [sys.executable, "-S", "-c", "import importlib.util,json,loopx;" + "print(json.dumps({'package':loopx.__file__," + "'facade':importlib.util.find_spec('loopx.todos') is not None}))"], + cwd=package.parent, capture_output=True, text=True, check=True, + ) + provenance = json.loads(probe.stdout) + assert provenance["package"] == str(package / "__init__.py") + assert provenance["facade"] is request.param + return package, command + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_cli_lifecycle_uses_retained_owners(tmp_path, provider, consumer_distribution): + journeys.test_canonical_cli_lifecycle_without_source_todo_writers( + tmp_path, provider, consumer_distribution[1]) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_creation_recovery_uses_retained_owners(tmp_path, provider, consumer_distribution): + journeys.test_new_goal_and_original_creation_recovery_without_source_todo_writers( + tmp_path, provider, consumer_distribution[1]) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_once_only_settlement_uses_retained_owners(tmp_path, provider, consumer_distribution): + journeys.test_leased_delivery_settlement_and_recovery_without_source_todo_writers( + tmp_path, provider, consumer_distribution[1]) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("material_change", [False, True]) +def test_monitor_replay_uses_retained_owners( + tmp_path, request, provider, material_change, consumer_distribution, +): + journeys.test_monitor_recovery_with_source_writer_isolation( + tmp_path, request, provider, material_change, True, consumer_distribution) + + +def test_public_imports_keep_same_owners(): + # Historic external callers keep function identity and the normal lazy IO + # arm; removing internal imports does not revoke source-format support. + import loopx.todos as historic + from loopx.control_plane.todos import active_state_editing, list_readback, mutation_api, path_resolution + + for owner, names in ( + (mutation_api, ("add_goal_todo", "update_goal_todo", "complete_goal_todo", + "supersede_goal_todo", "archive_completed_todos")), + (list_readback, ("list_goal_todos",)), + (path_resolution, ("resolve_todo_state", "resolve_todo_state_path")), + (active_state_editing, ("section_bounds", "todo_blocks")), + ): + for name in names: + assert getattr(historic, name) is getattr(owner, name) From dedf0950591f46ebdda1d8a8f532e64e4271fc7c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:03:36 +0800 Subject: [PATCH 3/4] docs(todos): record production consumer retirement boundary Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 13 ++++++++++ ...script-control-plane-migration-v0.zh-CN.md | 9 +++++++ .../canonical-todo-completion-update.md | 24 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 1c474e89b3..60f14b6fff 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -64,6 +64,19 @@ without claiming code retirement: bootstrap, handoff, team-plan and Monitor still have supported source callers; keep capture/outbox and historical recovery until their own exits. [Caller and rollback inventory](../../reference/canonical-todo-completion-update.md#canonical-mutation-entrypoint-isolation). +Production Todo consumers now bypass the historic `loopx.todos` re-export: +CLI registration/actions, quota and planning, capability writeback, source lease +reads and the Lark source projection import the same concrete mutation, full-read, +path or Markdown display owners. No new wrapper, decision or RPC is introduced. +The historic public imports and their supported unpromoted IO remain available. +Disposable source/normal-wheel probes remove the facade, the whole legacy Todo +writer and four shadow producers together; real File/SQLite CLI lifecycle, +once-only settlement, Monitor replay and App cold-import/action/restart journeys +must still work, with missing-provider/lease and changed-source/backup refusals. +Lark dry-run and Explore replay qualify local consumers only, not live external +adoption. This closes the production re-export dependency, while last supported +source writers, historical recovery, installed cohort and release exits remain. + The retained import-compatible field writer now composes resume normalization and contradictory-clear rejection in its existing typed field plan. Keep blank Python input omission and locked IO; retire the duplicate normalization crossing, diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md index c297b78b16..28594d3da1 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md @@ -55,6 +55,15 @@ canonical Todo 入口隔离(T4):五种 mutation facade 统一进入既有 revision 保持。这为最后调用方删除建立边界,不宣称代码已退役:bootstrap、 handoff、team-plan 和 Monitor 仍有受支持的旧源调用;capture/outbox 及历史恢复 各等自己的出口。见[调用方及回滚清单](../../reference/canonical-todo-completion-update.md#canonical-mutation-entrypoint-isolation)。 +生产 Todo 消费者不再经历史 `loopx.todos` 重导出:CLI 注册/操作、quota 与规划、 +能力写回、旧源租约读取及 Lark 源投影直接导入相同的 mutation、完整读取、路径或 +Markdown 显示 owner,不新增 wrapper、决策或 RPC。历史公开导入及其受支持的旧源 +IO 继续保留。隔离源码/正常 wheel 同时移除 facade、整个旧 Todo writer 和四个 +shadow producer,验证真实 File/SQLite CLI 生命周期、恰好一次结算、Monitor 重放 +及 App 冷导入/操作/重启;缺 provider/租约及源/备份变化须拒绝。Lark dry-run +与 Explore 重放只验证本地消费者,不计 live 外部采用。此出口关闭生产重导出依赖, +最后受支持的旧源 writer、历史恢复、安装 cohort 与发布验收继续保留。 + 保留的 import-compatible field writer 在既有 typed field plan 中统一等待条件 归一化及矛盾 clear 拒绝;保持 Python 空白输入省略和锁内 IO,只退役重复的 normalization crossing,不删除仍受支持的 writer 或迁移 reader。 diff --git a/docs/reference/canonical-todo-completion-update.md b/docs/reference/canonical-todo-completion-update.md index a9c7ee113d..d933c5c108 100644 --- a/docs/reference/canonical-todo-completion-update.md +++ b/docs/reference/canonical-todo-completion-update.md @@ -551,6 +551,30 @@ Whole-writer retirement still needs the declared support cutoff, last callers and pending outbox disposition; this separation does not force an upgrade or change a provider default. +Production callers import those same owners directly: `mutation_api` for the +five operations and archive constant, `list_readback` for complete Todo reads, +`path_resolution` for configured source routing, and `active_state_editing` for +supported source-display parsing. This includes CLI registration, quota/planning, +capability consumers and Lark. The historic `loopx.todos` public surface remains +an alias to the same function objects; no source-format support is withdrawn. +The consumer oracle adds physical facade removal to whole-writer/four-producer +removal in source and normal-wheel copies. It reuses the lifecycle, creation, +lease/once-only settlement and Monitor negative/replay oracles, plus real App +HTTP cold import, later mutations and server restart. The Lark dry-run refuses +remote execution; Explore projection replays without duplicate events. These +local extension checks do not qualify live Lark adoption. Restoring imports is +a code rollback with no new schema, provider selection or receipt conversion. + +生产调用方直接导入相同 owner:`mutation_api` 负责五种操作及归档常量, +`list_readback` 负责完整 Todo 读取,`path_resolution` 负责已配置的源路径, +`active_state_editing` 负责受支持的源显示解析;覆盖 CLI 注册、quota/规划、能力 +消费者与 Lark。历史 `loopx.todos` 仍重导出相同函数对象,旧格式支持未撤销。 +消费者检查在源码及正常 wheel 中同时物理移除 facade、整个 writer 和四个 producer, +复用生命周期、创建、租约/恰好一次结算及 Monitor 负例/重放,另验真实 App HTTP +冷导入、后续写入与服务重启。Lark dry-run 不执行远端命令,Explore 重放不重复事件, +这些本地扩展检查不认证 live Lark 采用。回滚只恢复代码导入,不转换 schema、provider +选择或回执;有真实调用的旧源 IO 和历史恢复仍保留。 + The retained field adapter preserves blank Python resume values as omission, timezone normalization, explicit clears and rejection before source mutation. Invalid supplied conditions use the typed owner's actionable diagnostic. This From 91b3cdfcab6118821b84ad2a2eaa7d9979ba5ae4 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:19:00 +0800 Subject: [PATCH 4/4] test(todos): cover full CLI status without historic consumers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_todo_consumer_retirement.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/control_plane/test_todo_consumer_retirement.py b/tests/control_plane/test_todo_consumer_retirement.py index 77bbbec22e..b58a6f5b0b 100644 --- a/tests/control_plane/test_todo_consumer_retirement.py +++ b/tests/control_plane/test_todo_consumer_retirement.py @@ -78,3 +78,22 @@ def test_public_imports_keep_same_owners(): ): for name in names: assert getattr(historic, name) is getattr(owner, name) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_full_cli_status_loads_without_historic_consumers(tmp_path, provider, consumer_distribution): + from canonical_authority_fixture import promoted_create_fixture + + registry, runtime, state = promoted_create_fixture(tmp_path, provider=provider) + before = {str(p.relative_to(tmp_path)): p.read_bytes() + for root in (runtime, state.parent) for p in root.rglob("*") if p.is_file()} + result = subprocess.run( + [*consumer_distribution[1], "--registry", str(registry), "--runtime-root", str(runtime), + "--format", "json", "authority-shadow", "status", "--goal-id", "goal-a"], + cwd=tmp_path, capture_output=True, text=True, timeout=60, + ) + assert result.returncode == 0, result.stdout + result.stderr + payload = json.loads(result.stdout) + assert payload["ok"] and payload["candidate"]["status"] == "missing" + assert {str(p.relative_to(tmp_path)): p.read_bytes() + for root in (runtime, state.parent) for p in root.rglob("*") if p.is_file()} == before