From c92fb7f42c07e3bfb7170ac4a59a447027ff2608 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:08:08 +0800 Subject: [PATCH 01/98] refactor(shadow): retain observations independently of source producers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/cli_commands/coordination_shadow.py | 17 +- .../local_authority_shadow_readback.py | 174 ++++++++++++++++- .../coordination/runtime_shadow.py | 183 ++---------------- .../coordination/runtime_shadow.ts | 5 + .../project_registry_io_manifest_v1.json | 4 +- .../test_coordination_shadow_command.py | 19 +- 6 files changed, 216 insertions(+), 186 deletions(-) diff --git a/loopx/cli_commands/coordination_shadow.py b/loopx/cli_commands/coordination_shadow.py index ab698ee6c8..31f1f894fb 100644 --- a/loopx/cli_commands/coordination_shadow.py +++ b/loopx/cli_commands/coordination_shadow.py @@ -10,6 +10,7 @@ from ..control_plane.coordination.authority_source_capture import build_runtime_shadow_source_snapshot from ..control_plane.coordination.configuration import resolve_coordination_runtime_shadow_config +from ..control_plane.coordination import local_authority_shadow_readback as observation from ..control_plane.coordination.promotion_review import execute_reviewed_coordination_promotion from ..control_plane.coordination.shadow_goal_scope import shadow_goal_scope from ..control_plane.projects.registry_codec import load_project_registry @@ -344,8 +345,6 @@ def handle_coordination_shadow_command( } print_payload(payload, output_format(args), _render) return 1 - # Only explicit, enabled legacy shadow actions load the producer. - from ..control_plane.coordination import runtime_shadow as shadow _, _, state_path = resolve_goal_state(registry=registry, goal_id=args.goal_id, project_override=args.project, state_file_override=args.state_file) @@ -357,7 +356,7 @@ def handle_coordination_shadow_command( projection_version = _projection_version(projection) projected_todos = projection.get("todos") projected_leases = projection.get("leases") - inspection = {"status": "not_evaluated"} if args.coordination_shadow_command == "rollback" else shadow.inspect_coordination_runtime_shadow( + inspection = {"status": "not_evaluated"} if args.coordination_shadow_command == "rollback" else observation.inspect_coordination_runtime_shadow( goal=goal, runtime_root=runtime_root, goal_id=args.goal_id, @@ -388,6 +387,8 @@ def handle_coordination_shadow_command( "decision_read_from_shadow": False, } if args.coordination_shadow_command == "bootstrap" and args.execute: + from ..control_plane.coordination import runtime_shadow as shadow + from ..control_plane.coordination.shadow_management import read_shadow_management_state management = read_shadow_management_state(runtime_root, args.goal_id) if management is not None and management["status"] in {"bootstrapping", "active"}: @@ -412,7 +413,7 @@ def handle_coordination_shadow_command( payload["executed"] = True payload["bootstrap"] = bootstrap if bootstrap.get("status") in {"applied", "replayed", "recovered"}: - payload["inspection"] = shadow.inspect_coordination_runtime_shadow( + payload["inspection"] = observation.inspect_coordination_runtime_shadow( goal=goal, runtime_root=runtime_root, goal_id=args.goal_id, @@ -426,7 +427,7 @@ def handle_coordination_shadow_command( and final_inspection.get("status") == "matched" ) if args.coordination_shadow_command == "qualify": - qualification = shadow.qualify_coordination_runtime_shadow( + qualification = observation.qualify_coordination_runtime_shadow( goal=goal, runtime_root=runtime_root, goal_id=args.goal_id, @@ -438,7 +439,7 @@ def handle_coordination_shadow_command( payload["qualification"] = qualification payload["ok"] = qualification.get("status") == "qualified" if args.coordination_shadow_command == "read-candidate": - read_candidate = shadow.read_coordination_runtime_shadow_todo_candidate( + read_candidate = observation.read_coordination_runtime_shadow_todo_candidate( goal=goal, runtime_root=runtime_root, goal_id=args.goal_id, @@ -453,6 +454,8 @@ def handle_coordination_shadow_command( and read_candidate.get("decision_read_from_shadow") is False ) if args.coordination_shadow_command == "promote": + from ..control_plane.coordination import runtime_shadow as shadow + # A saved plan owns qualification policy, so the flag defaults to # `None`; normalize it only for the freshly reviewed path. minimum_operations = args.minimum_operations if args.minimum_operations is not None else 3 @@ -499,6 +502,8 @@ def handle_coordination_shadow_command( payload["expected_provider_revision"] = provider_revision payload["expected_bootstrap_operation_id"] = pending_bootstrap if args.execute: + from ..control_plane.coordination import runtime_shadow as shadow + rollback = shadow.rollback_coordination_runtime_shadow( goal=goal, runtime_root=runtime_root, goal_id=args.goal_id, operation_id=f"shadow-rollback:{args.goal_id}:{provider_revision or pending_bootstrap}", diff --git a/loopx/control_plane/coordination/local_authority_shadow_readback.py b/loopx/control_plane/coordination/local_authority_shadow_readback.py index c5a1214ba6..702413b009 100644 --- a/loopx/control_plane/coordination/local_authority_shadow_readback.py +++ b/loopx/control_plane/coordination/local_authority_shadow_readback.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +from collections.abc import Callable, Mapping from dataclasses import asdict from pathlib import Path from typing import Any @@ -15,10 +16,18 @@ from ..effect_runtime import effect_runtime_result from . import local_authority_shadow_history as outbox from .coordination_state_contract_generated import ( + COORDINATION_RUNTIME_SHADOW_INSPECT_REQUEST_SCHEMA as RUNTIME_SHADOW_INSPECT_REQUEST_SCHEMA_VERSION, + COORDINATION_RUNTIME_SHADOW_INSPECT_RESULT_SCHEMA, + COORDINATION_RUNTIME_SHADOW_QUALIFY_REQUEST_SCHEMA as RUNTIME_SHADOW_QUALIFY_REQUEST_SCHEMA_VERSION, + COORDINATION_RUNTIME_SHADOW_QUALIFY_RESULT_SCHEMA, + COORDINATION_RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA as RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA_VERSION, + COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, LOCAL_AUTHORITY_SHADOW_READ_REQUEST_SCHEMA, LOCAL_AUTHORITY_SHADOW_READ_RESULT_SCHEMA, ) -from .local_authority_shadow_projection import head_digest +from .local_authority_shadow_projection import ( + head_digest, source_effect_runtime_result as source_effect_runtime_result, +) from .configuration import resolve_coordination_runtime_shadow_config, local_authority_shadow_summary from .shadow_management import read_shadow_capture_binding @@ -180,3 +189,166 @@ def local_authority_shadow_status( if store_bytes is not None else None, } + + +RuntimeInvoker = Callable[..., object] +RUNTIME_SHADOW_INSPECT_METHOD = "coordination.runtime_shadow.inspect" +RUNTIME_SHADOW_QUALIFY_METHOD = "coordination.runtime_shadow.qualify" +RUNTIME_SHADOW_TODO_READ_METHOD = "coordination.runtime_shadow.todo_read_candidate" + + +def inspect_coordination_runtime_shadow( + *, + goal: Mapping[str, Any] | None, + runtime_root: Path, + goal_id: str, + projection: Mapping[str, Any], + source_snapshot: Mapping[str, Any] | None = None, + runtime_invoker: RuntimeInvoker = source_effect_runtime_result, +) -> dict[str, object]: + """Read parity evidence without allowing the shadow to drive decisions.""" + + config = resolve_coordination_runtime_shadow_config(goal) + if not config.enabled: + return { + "schema_version": COORDINATION_RUNTIME_SHADOW_INSPECT_RESULT_SCHEMA, + "status": "disabled", + "reason_code": config.reason_code, + "parity_matches": False, + "bootstrap_required": False, + "decision_read_from_shadow": False, + } + request = { + "schema_version": RUNTIME_SHADOW_INSPECT_REQUEST_SCHEMA_VERSION, + "runtime_root": str(runtime_root.expanduser().absolute()), + "goal_id": goal_id, + "source_snapshot": dict(source_snapshot or {}), + "projection": dict(projection), + } + try: + result = runtime_invoker(RUNTIME_SHADOW_INSPECT_METHOD, request) + except Exception as exc: + return { + "schema_version": COORDINATION_RUNTIME_SHADOW_INSPECT_RESULT_SCHEMA, + "status": "failed", + "reason_code": "shadow_runtime_unavailable", + "reason": str(exc), + "parity_matches": False, + "bootstrap_required": False, + "decision_read_from_shadow": False, + } + if not isinstance(result, Mapping): + return { + "schema_version": COORDINATION_RUNTIME_SHADOW_INSPECT_RESULT_SCHEMA, + "status": "failed", + "reason_code": "shadow_runtime_result_invalid", + "parity_matches": False, + "bootstrap_required": False, + "decision_read_from_shadow": False, + } + return dict(result) + +def qualify_coordination_runtime_shadow( + *, + goal: Mapping[str, Any] | None, + runtime_root: Path, + goal_id: str, + projection: Mapping[str, Any], + minimum_operations: int, + required_event_kinds: list[str], + source_snapshot: Mapping[str, Any] | None = None, + runtime_invoker: RuntimeInvoker = source_effect_runtime_result, +) -> dict[str, object]: + """Qualify coverage across a shadow lineage without serving from it.""" + + config = resolve_coordination_runtime_shadow_config(goal) + if not config.enabled: + return { + "schema_version": COORDINATION_RUNTIME_SHADOW_QUALIFY_RESULT_SCHEMA, + "status": "disabled", + "reason_code": config.reason_code, + "qualified": False, + "primary_writeback_preserved": True, + "decision_read_from_shadow": False, + } + request = { + "schema_version": RUNTIME_SHADOW_QUALIFY_REQUEST_SCHEMA_VERSION, + "runtime_root": str(runtime_root.expanduser().absolute()), + "goal_id": goal_id, + "source_snapshot": dict(source_snapshot or {}), + "projection": dict(projection), + "minimum_operations": minimum_operations, + "required_event_kinds": list(required_event_kinds), + } + try: + result = runtime_invoker(RUNTIME_SHADOW_QUALIFY_METHOD, request) + except Exception as exc: + return { + "schema_version": COORDINATION_RUNTIME_SHADOW_QUALIFY_RESULT_SCHEMA, + "status": "failed", + "reason_code": "shadow_qualification_runtime_unavailable", + "reason": str(exc), + "qualified": False, + "primary_writeback_preserved": True, + "decision_read_from_shadow": False, + } + if not isinstance(result, Mapping): + return { + "schema_version": COORDINATION_RUNTIME_SHADOW_QUALIFY_RESULT_SCHEMA, + "status": "failed", + "reason_code": "shadow_qualification_runtime_result_invalid", + "qualified": False, + "primary_writeback_preserved": True, + "decision_read_from_shadow": False, + } + return dict(result) + +def read_coordination_runtime_shadow_todo_candidate( + *, + goal: Mapping[str, Any] | None, + runtime_root: Path, + goal_id: str, + todo_id: str, + projection: Mapping[str, Any], + source_snapshot: Mapping[str, Any] | None = None, + runtime_invoker: RuntimeInvoker = source_effect_runtime_result, +) -> dict[str, object]: + """Read one parity-matched file Todo as pre-promotion evidence only.""" + + config = resolve_coordination_runtime_shadow_config(goal) + if not config.enabled: + return { + "schema_version": COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, + "status": "disabled", + "reason_code": config.reason_code, + "read_candidate_qualified": False, + "decision_read_from_shadow": False, + } + request = { + "schema_version": RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA_VERSION, + "runtime_root": str(runtime_root.expanduser().absolute()), + "goal_id": goal_id, + "source_snapshot": dict(source_snapshot or {}), + "todo_id": todo_id, + "projection": dict(projection), + } + try: + result = runtime_invoker(RUNTIME_SHADOW_TODO_READ_METHOD, request) + except Exception as exc: + return { + "schema_version": COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, + "status": "failed", + "reason_code": "shadow_todo_read_runtime_unavailable", + "reason": str(exc), + "read_candidate_qualified": False, + "decision_read_from_shadow": False, + } + if not isinstance(result, Mapping): + return { + "schema_version": COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, + "status": "failed", + "reason_code": "shadow_todo_read_runtime_result_invalid", + "read_candidate_qualified": False, + "decision_read_from_shadow": False, + } + return dict(result) diff --git a/loopx/control_plane/coordination/runtime_shadow.py b/loopx/control_plane/coordination/runtime_shadow.py index 7cc83c4707..35a3e748d2 100644 --- a/loopx/control_plane/coordination/runtime_shadow.py +++ b/loopx/control_plane/coordination/runtime_shadow.py @@ -16,14 +16,8 @@ COORDINATION_RUNTIME_SHADOW_EXACT_BOOTSTRAP_REQUEST_SCHEMA, COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA as RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA_VERSION, COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_RESULT_SCHEMA, - COORDINATION_RUNTIME_SHADOW_INSPECT_REQUEST_SCHEMA as RUNTIME_SHADOW_INSPECT_REQUEST_SCHEMA_VERSION, - COORDINATION_RUNTIME_SHADOW_INSPECT_RESULT_SCHEMA, - COORDINATION_RUNTIME_SHADOW_QUALIFY_REQUEST_SCHEMA as RUNTIME_SHADOW_QUALIFY_REQUEST_SCHEMA_VERSION, - COORDINATION_RUNTIME_SHADOW_QUALIFY_RESULT_SCHEMA, COORDINATION_RUNTIME_SHADOW_ROLLBACK_REQUEST_SCHEMA as RUNTIME_SHADOW_ROLLBACK_REQUEST_SCHEMA_VERSION, COORDINATION_RUNTIME_SHADOW_ROLLBACK_RESULT_SCHEMA, - COORDINATION_RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA as RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA_VERSION, - COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, LOCAL_COORDINATION_PROMOTION_REVIEW_REQUEST_SCHEMA, LOCAL_COORDINATION_PROMOTION_REVIEW_RESULT_SCHEMA, ) @@ -52,11 +46,23 @@ ) -RUNTIME_SHADOW_INSPECT_METHOD = "coordination.runtime_shadow.inspect" +from .local_authority_shadow_readback import ( # noqa: F401 + RUNTIME_SHADOW_INSPECT_REQUEST_SCHEMA_VERSION, + COORDINATION_RUNTIME_SHADOW_INSPECT_RESULT_SCHEMA, + RUNTIME_SHADOW_QUALIFY_REQUEST_SCHEMA_VERSION, + COORDINATION_RUNTIME_SHADOW_QUALIFY_RESULT_SCHEMA, + RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA_VERSION, + COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, + inspect_coordination_runtime_shadow, + qualify_coordination_runtime_shadow, + read_coordination_runtime_shadow_todo_candidate, + RUNTIME_SHADOW_INSPECT_METHOD, + RUNTIME_SHADOW_QUALIFY_METHOD, + RUNTIME_SHADOW_TODO_READ_METHOD, +) + RUNTIME_SHADOW_BOOTSTRAP_METHOD = "coordination.runtime_shadow.bootstrap" RUNTIME_SHADOW_ROLLBACK_METHOD = "coordination.runtime_shadow.rollback" -RUNTIME_SHADOW_QUALIFY_METHOD = "coordination.runtime_shadow.qualify" -RUNTIME_SHADOW_TODO_READ_METHOD = "coordination.runtime_shadow.todo_read_candidate" LOCAL_AUTHORITY_PROMOTION_REVIEW_METHOD = "coordination.local_authority.promotion_review" @@ -193,114 +199,6 @@ def rollback_coordination_runtime_shadow( return dict(result) -def inspect_coordination_runtime_shadow( - *, - goal: Mapping[str, Any] | None, - runtime_root: Path, - goal_id: str, - projection: Mapping[str, Any], - source_snapshot: Mapping[str, Any] | None = None, - runtime_invoker: RuntimeInvoker = effect_runtime_result, -) -> dict[str, object]: - """Read parity evidence without allowing the shadow to drive decisions.""" - - config = resolve_coordination_runtime_shadow_config(goal) - if not config.enabled: - return { - "schema_version": COORDINATION_RUNTIME_SHADOW_INSPECT_RESULT_SCHEMA, - "status": "disabled", - "reason_code": config.reason_code, - "parity_matches": False, - "bootstrap_required": False, - "decision_read_from_shadow": False, - } - request = { - "schema_version": RUNTIME_SHADOW_INSPECT_REQUEST_SCHEMA_VERSION, - "runtime_root": str(runtime_root.expanduser().absolute()), - "goal_id": goal_id, - "source_snapshot": dict(source_snapshot or {}), - "projection": dict(projection), - } - try: - result = runtime_invoker(RUNTIME_SHADOW_INSPECT_METHOD, request) - except Exception as exc: - return { - "schema_version": COORDINATION_RUNTIME_SHADOW_INSPECT_RESULT_SCHEMA, - "status": "failed", - "reason_code": "shadow_runtime_unavailable", - "reason": str(exc), - "parity_matches": False, - "bootstrap_required": False, - "decision_read_from_shadow": False, - } - if not isinstance(result, Mapping): - return { - "schema_version": COORDINATION_RUNTIME_SHADOW_INSPECT_RESULT_SCHEMA, - "status": "failed", - "reason_code": "shadow_runtime_result_invalid", - "parity_matches": False, - "bootstrap_required": False, - "decision_read_from_shadow": False, - } - return dict(result) - - -def qualify_coordination_runtime_shadow( - *, - goal: Mapping[str, Any] | None, - runtime_root: Path, - goal_id: str, - projection: Mapping[str, Any], - minimum_operations: int, - required_event_kinds: list[str], - source_snapshot: Mapping[str, Any] | None = None, - runtime_invoker: RuntimeInvoker = effect_runtime_result, -) -> dict[str, object]: - """Qualify coverage across a shadow lineage without serving from it.""" - - config = resolve_coordination_runtime_shadow_config(goal) - if not config.enabled: - return { - "schema_version": COORDINATION_RUNTIME_SHADOW_QUALIFY_RESULT_SCHEMA, - "status": "disabled", - "reason_code": config.reason_code, - "qualified": False, - "primary_writeback_preserved": True, - "decision_read_from_shadow": False, - } - request = { - "schema_version": RUNTIME_SHADOW_QUALIFY_REQUEST_SCHEMA_VERSION, - "runtime_root": str(runtime_root.expanduser().absolute()), - "goal_id": goal_id, - "source_snapshot": dict(source_snapshot or {}), - "projection": dict(projection), - "minimum_operations": minimum_operations, - "required_event_kinds": list(required_event_kinds), - } - try: - result = runtime_invoker(RUNTIME_SHADOW_QUALIFY_METHOD, request) - except Exception as exc: - return { - "schema_version": COORDINATION_RUNTIME_SHADOW_QUALIFY_RESULT_SCHEMA, - "status": "failed", - "reason_code": "shadow_qualification_runtime_unavailable", - "reason": str(exc), - "qualified": False, - "primary_writeback_preserved": True, - "decision_read_from_shadow": False, - } - if not isinstance(result, Mapping): - return { - "schema_version": COORDINATION_RUNTIME_SHADOW_QUALIFY_RESULT_SCHEMA, - "status": "failed", - "reason_code": "shadow_qualification_runtime_result_invalid", - "qualified": False, - "primary_writeback_preserved": True, - "decision_read_from_shadow": False, - } - return dict(result) - - def review_local_coordination_authority_promotion( *, goal: Mapping[str, Any] | None, @@ -369,54 +267,3 @@ def review_local_coordination_authority_promotion( "legacy_fallback_used": False, } return dict(result) - - -def read_coordination_runtime_shadow_todo_candidate( - *, - goal: Mapping[str, Any] | None, - runtime_root: Path, - goal_id: str, - todo_id: str, - projection: Mapping[str, Any], - source_snapshot: Mapping[str, Any] | None = None, - runtime_invoker: RuntimeInvoker = effect_runtime_result, -) -> dict[str, object]: - """Read one parity-matched file Todo as pre-promotion evidence only.""" - - config = resolve_coordination_runtime_shadow_config(goal) - if not config.enabled: - return { - "schema_version": COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, - "status": "disabled", - "reason_code": config.reason_code, - "read_candidate_qualified": False, - "decision_read_from_shadow": False, - } - request = { - "schema_version": RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA_VERSION, - "runtime_root": str(runtime_root.expanduser().absolute()), - "goal_id": goal_id, - "source_snapshot": dict(source_snapshot or {}), - "todo_id": todo_id, - "projection": dict(projection), - } - try: - result = runtime_invoker(RUNTIME_SHADOW_TODO_READ_METHOD, request) - except Exception as exc: - return { - "schema_version": COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, - "status": "failed", - "reason_code": "shadow_todo_read_runtime_unavailable", - "reason": str(exc), - "read_candidate_qualified": False, - "decision_read_from_shadow": False, - } - if not isinstance(result, Mapping): - return { - "schema_version": COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, - "status": "failed", - "reason_code": "shadow_todo_read_runtime_result_invalid", - "read_candidate_qualified": False, - "decision_read_from_shadow": False, - } - return dict(result) diff --git a/loopx/control_plane/coordination/runtime_shadow.ts b/loopx/control_plane/coordination/runtime_shadow.ts index 1971164f72..7c2d2780ca 100644 --- a/loopx/control_plane/coordination/runtime_shadow.ts +++ b/loopx/control_plane/coordination/runtime_shadow.ts @@ -358,6 +358,11 @@ export async function qualifyCoordinationShadowLineageUnderLocks( const store = dependencies.createStore?.(join(request.runtime_root, "authority-shadow", "file-v0"), request.goal_id) ?? new FileAuthorityStore(join(request.runtime_root, "authority-shadow", "file-v0"), request.goal_id, { existingOnly: true }); const initial = await store.loadAuthority(); + // A failed read is not an absent baseline. Preserve the provider-owned + // refusal before lineage validation can mislabel it bootstrap_required. + if (initial.status === "failed" || initial.status === "unavailable") { + throw new ShadowLineageError(initial.reason_code); + } if (initial.status === "loaded" && initial.head.capture_profile !== "file_outbox_v1") throw new ShadowLineageError("legacy_lineage_ineligible"); const binding = await requireShadowCaptureBinding(request.runtime_root, request.goal_id); const lineage = await loadValidatedShadowLineage(store, request.runtime_root, request.goal_id, binding); diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index e076e23f56..a7cc284380 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -671,7 +671,7 @@ }, { "site": "loopx/cli_commands/coordination_shadow.py::.handle_coordination_shadow_command::codec_read:load_project_registry#1", - "line": 245, + "line": 246, "column": 20, "kind": "codec_read", "api": "load_project_registry", @@ -1119,7 +1119,7 @@ }, { "site": "loopx/control_plane/coordination/local_authority_shadow_readback.py::.local_authority_shadow_status::codec_read:load_registry#1", - "line": 84, + "line": 93, "column": 16, "kind": "codec_read", "api": "load_registry", diff --git a/tests/control_plane/test_coordination_shadow_command.py b/tests/control_plane/test_coordination_shadow_command.py index 5d8078dab4..e55da2dc8f 100644 --- a/tests/control_plane/test_coordination_shadow_command.py +++ b/tests/control_plane/test_coordination_shadow_command.py @@ -5,6 +5,7 @@ from pathlib import Path from loopx.control_plane.coordination import runtime_shadow as shadow +from loopx.control_plane.coordination import local_authority_shadow_readback as observation from loopx.control_plane.coordination.authority_source_capture import build_todo_runtime_shadow_projection from loopx.cli import build_parser from loopx.cli_commands import coordination_shadow as command @@ -119,7 +120,7 @@ def test_coordination_shadow_inspect_is_read_only_and_compact( tmp_path: Path, ) -> None: monkeypatch.setattr( - shadow, + observation, "inspect_coordination_runtime_shadow", lambda **_kwargs: { "status": "missing", @@ -161,7 +162,7 @@ def test_coordination_shadow_bootstrap_requires_execute_and_reads_back_parity( ] ) monkeypatch.setattr( - shadow, + observation, "inspect_coordination_runtime_shadow", lambda **_kwargs: next(inspections), ) @@ -208,7 +209,7 @@ def test_coordination_shadow_bootstrap_uses_source_session_goal_ref( } bootstrap_request: dict[str, object] = {} monkeypatch.setattr( - shadow, + observation, "inspect_coordination_runtime_shadow", lambda **_kwargs: { "status": "matched", @@ -326,7 +327,7 @@ def test_coordination_shadow_reads_parity_matched_todo_candidate( tmp_path: Path, ) -> None: monkeypatch.setattr( - shadow, + observation, "inspect_coordination_runtime_shadow", lambda **_kwargs: { "status": "matched", @@ -347,7 +348,7 @@ def read_candidate(**kwargs) -> dict[str, object]: } monkeypatch.setattr( - shadow, + observation, "read_coordination_runtime_shadow_todo_candidate", read_candidate, ) @@ -374,7 +375,7 @@ def test_coordination_shadow_qualify_applies_coverage_policy( tmp_path: Path, ) -> None: monkeypatch.setattr( - shadow, + observation, "inspect_coordination_runtime_shadow", lambda **_kwargs: { "status": "matched", @@ -392,7 +393,7 @@ def qualify(**kwargs) -> dict[str, object]: "decision_read_from_shadow": False, } - monkeypatch.setattr(shadow, "qualify_coordination_runtime_shadow", qualify) + monkeypatch.setattr(observation, "qualify_coordination_runtime_shadow", qualify) result, payload = _run( monkeypatch, tmp_path, @@ -417,7 +418,7 @@ def test_coordination_shadow_promote_previews_and_applies_only_through_review_ow tmp_path: Path, ) -> None: monkeypatch.setattr( - shadow, + observation, "inspect_coordination_runtime_shadow", lambda **_kwargs: { "status": "matched", @@ -502,7 +503,7 @@ def test_coordination_shadow_rollback_passes_exact_selector_to_management_owner( ] ) monkeypatch.setattr( - shadow, + observation, "inspect_coordination_runtime_shadow", lambda **_kwargs: next(inspections), ) From 920e194d37477b3fa0317b71e28e029566482d00 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:08:08 +0800 Subject: [PATCH 02/98] test(shadow): qualify retained observations and provider refusals Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_canonical_todo_writer_isolation.py | 13 +- .../test_shadow_observation_retirement.py | 121 ++++++++++++++++++ .../coordination_runtime_shadow.test.ts | 23 ++++ 3 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 tests/control_plane/test_shadow_observation_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..68a4942c0c 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, @@ -452,6 +456,7 @@ def test_explicit_source_writer_import_preserves_the_existing_seam(tmp_path): result = subprocess.run( [sys.executable, "-c", """ import sys +import sysconfig import loopx.todos as todos assert 'loopx.control_plane.todos.line_update' not in sys.modules assert 'loopx.control_plane.todos.legacy_mutation' not in sys.modules diff --git a/tests/control_plane/test_shadow_observation_retirement.py b/tests/control_plane/test_shadow_observation_retirement.py new file mode 100644 index 0000000000..1b748b0e39 --- /dev/null +++ b/tests/control_plane/test_shadow_observation_retirement.py @@ -0,0 +1,121 @@ +"""Retained migration observations must survive source-producer retirement.""" +from __future__ import annotations + +import json +from pathlib import Path +import subprocess + +import pytest + +from shadow_e2e_fixture import workspace +from test_canonical_todo_writer_isolation import ( + isolated_todo_distribution, retirement_wheel, # noqa: F401,F811 +) + + +def _bytes(root: Path): + return {str(path.relative_to(root)): path.read_bytes() + for path in root.rglob("*") if path.is_file() and path.suffix != ".lock"} + + +@pytest.mark.parametrize("producers_present", [True, False], ids=["present", "absent"]) +def test_complete_shadow_observation_without_producers( + tmp_path, isolated_todo_distribution, producers_present, # noqa: F811 -- imported pytest fixture +): + package, command = isolated_todo_distribution + w = workspace(tmp_path / "shadow", bootstrap=False) + w.command, w.cwd, w.package = tuple(command), tmp_path, package + assert w.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" + todo_id = w.add("Preserve the original source work")["todo_id"] + w.add("Second source mutation") + w.add("Third source mutation") + if not producers_present: + for name in ("runtime_shadow", "runtime_shadow_writer_adapter", + "local_authority_shadow_adapter", "local_authority_shadow_outbox"): + (package / f"control_plane/coordination/{name}.py").unlink() + check = subprocess.run([command[0], "-S", "-c", + "import importlib.util; assert importlib.util.find_spec(" + "'loopx.control_plane.coordination.runtime_shadow') is None"], + cwd=tmp_path, capture_output=True, text=True, timeout=45) + assert check.returncode == 0, check.stdout + check.stderr + + def observe(*args, success=True): + before = _bytes(w.registry.parent) + result = w.cli("coordination-shadow", *args, success=success) + assert _bytes(w.registry.parent) == before + assert result["executed"] is False + assert result["decision_read_from_shadow"] is False + return result + + inspected = observe("inspect") + assert inspected["inspection"]["status"] == "matched" + qualified = observe("qualify", "--require-event-kind", "todo_add") + assert qualified["qualification"]["status"] == "qualified" + insufficient = observe("qualify", "--minimum-operations", "4", success=False) + assert insufficient["ok"] is False + assert insufficient["qualification"]["qualified"] is False + candidate = observe("read-candidate", "--todo-id", todo_id) + assert candidate["read_candidate"]["read_candidate_qualified"] is True + missing = observe("read-candidate", "--todo-id", "todo_missing", success=False) + assert missing["ok"] is False + assert missing["read_candidate"]["read_candidate_qualified"] is False + + original = w.state.read_bytes() + w.state.write_text(w.state.read_text().replace("Third source mutation", "Changed source work")) + drifted = observe("inspect", success=False) + assert drifted["inspection"]["parity_matches"] is False + refused = observe("read-candidate", "--todo-id", todo_id, success=False) + assert refused["ok"] is False + assert refused["read_candidate"]["read_candidate_qualified"] is False + w.state.write_bytes(original) + assert observe("inspect")["inspection"]["status"] == "matched" + + store = next((w.runtime / "authority-shadow/file-v0").glob("authority-store-*.json")) + stored = store.read_bytes() + store.write_text("{unreadable historical store") + for args in [("inspect",), ("qualify",), ("read-candidate", "--todo-id", todo_id)]: + unavailable = observe(*args, success=False) + assert unavailable["ok"] is False + detail = unavailable[{"inspect": "inspection", "qualify": "qualification", + "read-candidate": "read_candidate"}[args[0]]] + assert detail["reason_code"] == "provider_protocol_violation" + store.write_bytes(stored) + assert observe("inspect")["inspection"]["status"] == "matched" + + store.rename(store.with_suffix(".retained")) + store.mkdir() + for args in [("inspect",), ("qualify",), ("read-candidate", "--todo-id", todo_id)]: + unavailable = observe(*args, success=False) + assert unavailable["ok"] is False + detail = unavailable[{"inspect": "inspection", "qualify": "qualification", + "read-candidate": "read_candidate"}[args[0]]] + assert detail["reason_code"] == "provider_read_unavailable" + store.rmdir() + store.with_suffix(".retained").rename(store) + assert observe("inspect")["inspection"]["status"] == "matched" + + directory = store.parent + retained = directory.with_name("retained-original") + directory.rename(retained) + assert observe("inspect", success=False)["inspection"]["parity_matches"] is False + for args in [("qualify",), ("read-candidate", "--todo-id", todo_id)]: + assert observe(*args, success=False)["ok"] is False + assert not directory.exists() + retained.rename(directory) + assert observe("inspect")["inspection"]["status"] == "matched" + + registry = json.loads(w.registry.read_text()) + registry["goals"][0]["coordination"]["runtime_shadow"]["enabled"] = False + w.registry.write_text(json.dumps(registry)) + for args in [("inspect",), ("qualify",), ("read-candidate", "--todo-id", todo_id)]: + disabled = observe(*args, success=False) + assert disabled["error_code"] == "coordination_shadow_not_enabled" + + +def test_historical_observation_imports_share_the_retained_owner(): + from loopx.control_plane.coordination import local_authority_shadow_readback as reader + from loopx.control_plane.coordination import runtime_shadow as old + + for name in ("inspect_coordination_runtime_shadow", "qualify_coordination_runtime_shadow", + "read_coordination_runtime_shadow_todo_candidate"): + assert getattr(old, name) is getattr(reader, name) diff --git a/tests/control_plane_ts/coordination_runtime_shadow.test.ts b/tests/control_plane_ts/coordination_runtime_shadow.test.ts index a73cda433f..dde01b6a34 100644 --- a/tests/control_plane_ts/coordination_runtime_shadow.test.ts +++ b/tests/control_plane_ts/coordination_runtime_shadow.test.ts @@ -28,6 +28,29 @@ async function qualify(f: ShadowFixture, head: JsonObject, extra: JsonObject = { minimum_operations: 3, required_event_kinds: ["todo_add"], ...extra }); } +test("retained observations refuse corrupt provider bytes without suggesting bootstrap", async (t) => { + const {f, head} = await qualifiedFixture(t); + const source = await sourceRequest(f, head); + const original = await readFile(f.store.path); + await writeFile(f.store.path, "{corrupt journal"); + for (const [read, schema, extra] of [ + [inspectCoordinationRuntimeShadow, schemas.COORDINATION_RUNTIME_SHADOW_INSPECT_REQUEST_SCHEMA, {}], + [qualifyCoordinationRuntimeShadow, schemas.COORDINATION_RUNTIME_SHADOW_QUALIFY_REQUEST_SCHEMA, + {minimum_operations: 3, required_event_kinds: ["todo_add"]}], + [readCoordinationRuntimeShadowTodoCandidate, schemas.COORDINATION_RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA, + {todo_id: "todo_1"}], + ] as const) { + const result = await read({...source, schema_version: schema, ...extra}); + assert.equal(result.status, "failed"); + assert.equal(result.reason_code, "provider_protocol_violation"); + assert.equal(result.qualified, false); + assert.equal(result.decision_read_from_shadow, false); + assert.equal(await readFile(f.store.path, "utf8"), "{corrupt journal"); + } + await writeFile(f.store.path, original); + assert.equal((await qualify(f, head)).status, "qualified"); +}); + test("bootstrap is exactly replayable, has no mutation receipt and cannot overwrite a different baseline", async (t) => { const f = await fixture(t); const request = { ...await sourceRequest(f, f.baseline), schema_version: schemas.COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA, From be6b0327341aaa69e0072da5732b99bb91c678d1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:08:08 +0800 Subject: [PATCH 03/98] docs(shadow): record retained observation exit and recovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 12 ++++++++++-- ...ypescript-control-plane-migration-v0.zh-CN.md | 9 ++++++++- .../authority-observation-retirement.md | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 1c474e89b3..b60e9e29b0 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -145,8 +145,16 @@ producer modules using the qualified CLI startup isolation above. Shadow diagnostics now have an independent read-only boundary: retained outbox disk codecs live in `local_authority_shadow_history.py`, and status/candidate -read transport in `local_authority_shadow_readback.py`. The public status CLI -does not load source producers; drain still uses its existing orchestration. +read transport in `local_authority_shadow_readback.py`. The public status and enabled +`coordination-shadow inspect`/`qualify`/`read-candidate` commands call that owner +without loading source producers. Full source and normal-wheel observations +retain matched/refused, default-off, source-drift and missing/corrupt-store +recovery behavior with all four producer modules absent. Historical imports +remain exact aliases; bootstrap, rollback and promotion retain their live adapter. +The existing TS qualifier now propagates provider read refusals before lineage +validation can mislabel corruption as `bootstrap_required`; valid/missing stores +retain their behavior. This is an explicit failure-diagnostic correction, with +no new RPC or migration authority; drain still uses its existing orchestration. Original entry identity, malformed evidence, cursor residue and missing-store behavior remain unchanged. Python retains historical filesystem decoding and presentation IO; TS retains history proof, delivery and cleanup authority. 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..d160d48227 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 @@ -116,7 +116,14 @@ producer module。 shadow 诊断现在有独立只读边界:历史 outbox 磁盘 codec 位于 `local_authority_shadow_history.py`,status/candidate read transport 位于 -`local_authority_shadow_readback.py`。公开 status CLI 不加载源 producer;drain +`local_authority_shadow_readback.py`。公开 status 和启用的 +`coordination-shadow inspect`/`qualify`/`read-candidate` 直接调用保留 owner, +不加载源 producer。源码和正常 wheel 物理删除四个 producer 后,完整读回保持 +匹配/拒绝、默认关闭、源漂移、store 缺失/损坏及恢复结果;历史 import 保留 +同一函数 alias,bootstrap/rollback/promotion 的活跃 adapter 继续受支持。 +既有 TS qualifier 保留 provider 读取失败,避免把损坏 journal 误称尚未 +bootstrap;正常/缺失状态保留原结果。这是明确的失败诊断修复,无新 RPC 或 +迁移权限;drain 仍走现有 orchestration。原 entry 身份、损坏证据、cursor 残留和缺失 store 行为 保持。Python 保留历史文件解码及展示 IO;TS 保留历史证明、投递与清理权限。 旧 producer 的已有导入为支持调用方保留 alias,不复制实现。源码及正常 wheel diff --git a/docs/reference/authority-observation-retirement.md b/docs/reference/authority-observation-retirement.md index 050a928055..4a09018aee 100644 --- a/docs/reference/authority-observation-retirement.md +++ b/docs/reference/authority-observation-retirement.md @@ -71,6 +71,22 @@ counts are diagnostic hints: they do not authorize cleanup or prove delivery. A missing candidate stays missing; status never creates a replacement identity, repairs bytes, drains an outbox or grants a writer permission. +The enabled `coordination-shadow inspect`, `qualify` and `read-candidate` +commands likewise call `local_authority_shadow_readback.py` directly. Their +complete source snapshot, default-off gate, bounded qualification and refusal +results retain the same TS owners. Source and normal-wheel checks remove all +four old producers and compare byte-stable observations, insufficient coverage, +missing Todo, source drift, unreadable/missing store and subsequent recovery. +The historical `runtime_shadow.py` imports remain aliases to the same functions. +The shared TS qualifier now preserves `provider_protocol_violation` and +`provider_read_unavailable` rather than mislabelling a failed provider read as +`bootstrap_required`. Consequently a corrupt journal makes inspection fail, +instead of reporting a missing baseline; qualification and candidate reads +retain the provider refusal too. Missing and valid stores retain their existing +behavior. Recovery restores the original bytes, without creating a new lineage. +Bootstrap, rollback and promotion still load their supported management adapter; +this reader exit grants no writer cutoff, import or promotion authority. + The retained Python filesystem codecs preserve the existing wire and errors; candidate proof and drain effects remain in their TypeScript owners. Supported unpromoted writes and `authority-shadow drain` still need their current producer From 0de81f64fdc3265510464145077bbf11dc014f75 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 01:25:06 +0800 Subject: [PATCH 04/98] refactor(todos): retire legacy Markdown writers Signed-off-by: Duang777 --- docs/book/chapters/core-state-machines.md | 4 +- docs/book/en/chapters/core-state-machines.md | 5 +- docs/concepts/interaction-pattern-catalog.md | 14 +- .../05-work-graph-and-peers.md | 8 +- .../authority-observation-retirement.md | 18 +- docs/reference/canonical-terminal-review.md | 6 +- .../canonical-todo-completion-update.md | 297 +-- .../local-authority-provider-selection.md | 181 +- docs/reference/monitor-configuration.md | 10 +- .../active-state-structured-projection-v0.md | 22 +- .../todo-deferred-capacity-cli-smoke.py | 11 - .../control_plane/todo_lifecycle_fixtures.py | 29 +- examples/shared-goal-authority-e2e/README.md | 284 +-- .../shared-goal-authority-e2e/correctness.md | 461 ++-- .../shared-goal-authority-e2e/installed.py | 201 +- examples/shared-goal-authority-e2e/mutants.py | 196 +- loopx/bootstrap.py | 38 +- .../machine_configuration/goal_storage.py | 28 - loopx/chat_actions.py | 4 +- loopx/cli_commands/project.py | 12 +- .../cli_commands/todo_argument_validation.py | 10 +- loopx/cli_commands/todo_registration.py | 8 +- loopx/cli_commands/turn_todo_writeback.py | 4 +- loopx/configuration_catalog.py | 4 +- .../goal_storage_initialization.py | 64 + .../coordination/handoff_mode_legacy_plan.ts | 76 - .../coordination/handoff_mode_runtime.ts | 7 +- .../coordination/legacy_writer_fence.py | 72 +- .../coordination/local_authority.py | 73 +- .../coordination/local_authority_runtime.ts | 40 +- .../local_authority_shadow_adapter.py | 80 - .../local_authority_shadow_outbox.py | 518 +---- .../local_authority_shadow_outbox.ts | 263 +-- .../coordination/local_authority_write.ts | 88 +- .../runtime_shadow_writer_adapter.py | 206 -- .../todo_acceptance_restoration.ts | 3 +- .../coordination/todo_monitor_poll.ts | 36 +- .../coordination/todo_terminal_lifecycle.ts | 17 +- .../control_plane/coordination/todo_update.ts | 80 +- .../coordination/todo_update_intent.ts | 31 +- .../control_plane/effect_runtime_handlers.ts | 3 +- loopx/control_plane/projects/registry.py | 194 +- loopx/control_plane/quota/monitor_poll.py | 7 + .../quota/monitor_poll_commit.ts | 2 +- .../scheduler/legacy_monitor_poll.py | 229 -- .../control_plane/scheduler/monitor_batch.ts | 123 +- .../scheduler/monitor_batch_receipts.py | 5 +- .../scheduler/monitor_poll_writeback.py | 58 +- .../scheduler/provider_monitor_poll.py | 35 +- ...actual_default_model_behavior_portfolio.py | 5 +- .../testing/authority_e2e_fixtures.py | 103 + .../testing/authority_e2e_ladder.py | 170 +- .../testing/authority_e2e_row_support.py | 8 +- .../testing/authority_e2e_rows_stage2c.py | 31 - .../testing/authority_e2e_rows_stage2c2.py | 1231 ---------- .../replan_semantic_action_behavior.py | 104 +- .../terminal_settlement_tool_behavior.py | 171 +- loopx/control_plane/todos/handoff_mode.py | 270 +-- .../todos/handoff_mode_source.py | 46 - loopx/control_plane/todos/legacy_mutation.py | 1520 ------------- loopx/control_plane/todos/line_update.py | 364 --- loopx/control_plane/todos/mutation_api.py | 94 +- .../control_plane/todos/mutation_authority.py | 351 +-- loopx/control_plane/todos/provider_create.py | 15 +- .../todos/provider_handoff_mode.py | 11 +- .../todos/provider_terminal_lifecycle.py | 48 +- loopx/control_plane/todos/provider_update.py | 54 +- .../todos/successor_derivation.py | 27 - .../canonical_task_lease_lifecycle.ts | 36 +- .../work_items/replan_settlement.ts | 3 +- loopx/control_plane/work_items/task_lease.py | 432 ---- .../work_items/task_lease_acquire.ts | 380 +--- .../work_items/task_lease_acquire_adapter.py | 200 +- .../work_items/task_lease_lifecycle.ts | 803 +------ loopx/control_plane/work_items/team_plan.ts | 2 +- .../work_items/team_plan_adapter.py | 179 +- .../work_items/team_plan_authority.ts | 10 +- .../goal_instance_binding_inventory_v1.json | 1 - .../project_registry_io_manifest_v1.json | 100 +- loopx/todos.py | 21 +- pyproject.toml | 2 - .../test_content_digest_single_owner.py | 1 - .../test_legacy_markdown_writer_retirement.py | 208 ++ ...sue_fix_grouped_monitor_materialization.py | 36 +- .../test_issue_fix_monitor_execution.py | 12 +- tests/cli_commands/test_project_registry.py | 120 +- .../canonical_authority_fixture.py | 27 +- tests/control_plane/shadow_e2e_fixture.py | 78 +- ...actual_default_model_behavior_portfolio.py | 3 +- .../test_archive_dependency_capture.py | 31 +- .../test_archive_retry_delivery.py | 11 +- .../test_auxiliary_scoped_gate.py | 56 +- ...test_backup_registered_source_retention.py | 137 -- .../test_canonical_authority_source.py | 19 +- .../test_canonical_claim_transfer.py | 27 +- .../test_canonical_todo_writer_isolation.py | 223 +- .../test_canonical_update_dispatch.py | 65 +- .../test_chat_cold_source_import.py | 12 +- .../control_plane/test_chat_gate_decisions.py | 31 +- tests/control_plane/test_chat_goal_storage.py | 33 +- .../test_chat_user_action_handling.py | 26 +- .../test_closed_monitor_replay.py | 15 +- .../test_cold_source_disposition_e2e.py | 436 ---- .../test_cold_source_import_cli.py | 127 +- .../test_cold_source_import_host.py | 30 +- .../test_cold_source_import_runtime.py | 5 +- .../test_cold_source_inspection.py | 381 ++-- tests/control_plane/test_completion_policy.py | 24 +- .../test_declared_terminal_settlement_cli.py | 6 +- .../test_effective_turn_replan.py | 2 + tests/control_plane/test_goal_handoff_mode.py | 1755 -------------- .../test_goal_handoff_mode_cli.py | 243 +- .../control_plane/test_handoff_mode_source.py | 80 - .../test_in_flight_turn_replay_cli.py | 17 +- ...t_issue_fix_private_evidence_settlement.py | 46 +- .../test_large_source_journey.py | 55 - .../control_plane/test_leased_monitor_poll.py | 24 +- .../test_legacy_coordination_writer_fence.py | 83 - .../test_local_authority_shadow_cli_e2e.py | 64 - .../test_local_authority_shadow_drain.py | 500 ---- .../test_local_authority_shadow_outbox.py | 546 ----- .../test_local_coordination_authority.py | 133 +- .../test_migrated_terminal_closeout_cli.py | 55 - .../test_monitor_followthrough_contract.py | 92 +- .../test_monitor_observation_admission.py | 148 +- .../test_monitor_poll_transaction_recovery.py | 791 ++----- .../test_monitor_quiet_due_recovery.py | 68 +- .../test_monitor_receipt_writer_isolation.py | 117 - .../control_plane/test_monitor_state_owner.py | 2 +- .../test_native_monitor_configuration.py | 27 +- .../control_plane/test_native_monitor_poll.py | 37 +- .../test_native_todo_ownership_update.py | 27 +- .../test_native_todo_planning_update.py | 187 +- .../test_native_user_completion_update.py | 13 +- .../test_new_goal_storage_defaults.py | 120 +- .../test_post_writeback_capability_hooks.py | 24 +- .../test_promoted_archive_retention.py | 129 -- .../test_public_todo_update_plan.py | 64 +- .../control_plane/test_quota_monitor_clock.py | 38 +- .../test_quota_settlement_cli.py | 261 +-- .../test_recovery_due_successor_identity.py | 53 +- .../test_refresh_state_replan_gate.py | 15 + .../test_replan_completion_validation.py | 11 + .../test_replan_semantic_action_behavior.py | 28 +- .../test_replan_successor_durable_ack.py | 50 +- .../test_retired_todo_event_source.py | 37 +- .../test_reviewed_canonical_edits.py | 18 +- .../test_reviewed_promotion_cli.py | 172 -- .../test_reviewed_terminal_actions.py | 15 +- .../test_runtime_shadow_bounded_e2e.py | 595 ----- .../test_runtime_shadow_writer_capture.py | 263 --- .../test_selected_work_requirements.py | 45 +- .../test_settled_monitor_user_gate.py | 14 +- .../test_settlement_input_discovery_cli.py | 72 +- .../test_shadow_cursor_recovery_e2e.py | 181 -- .../test_shadow_drain_adversarial.py | 224 -- tests/control_plane/test_shadow_drain_e2e.py | 268 --- .../test_shadow_drain_native_plan.py | 73 +- .../test_shadow_fence_caller_parity_e2e.py | 330 --- .../test_shadow_goal_instance.py | 379 ---- .../test_shadow_management_e2e.py | 312 --- .../test_shadow_management_variant_e2e.py | 132 +- .../test_shadow_observable_e2e.py | 389 ---- .../test_shadow_observable_native_e2e.py | 169 -- .../test_shadow_observation_retirement.py | 121 - .../test_shadow_readback_writer_isolation.py | 153 -- .../test_shadow_registry_source.py | 147 +- .../test_shadow_writer_boundaries.py | 666 +----- .../test_shadow_writer_variant_e2e.py | 321 --- .../test_shared_goal_authority_e2e.py | 134 +- .../test_split_root_todo_writeback_fence.py | 207 +- .../test_sqlite_authority_cli.py | 15 +- .../test_superseded_turn_settlement_cli.py | 21 - tests/control_plane/test_task_lease.py | 331 +-- .../test_task_lease_argument_recovery.py | 41 +- .../control_plane/test_team_plan_authority.py | 113 +- .../test_terminal_settlement_tool_behavior.py | 5 +- .../test_todo_authoring_scope.py | 50 +- ...test_todo_capability_gap_cli_validation.py | 20 +- .../test_todo_claim_argument_recovery.py | 61 +- .../test_todo_completion_validation.py | 430 +--- ...test_todo_decision_scope_cli_validation.py | 91 +- .../test_todo_decision_scope_lifecycle.py | 75 +- .../test_todo_document_ownership.py | 87 +- .../control_plane/test_todo_external_wait.py | 194 +- .../test_todo_field_update_runtime.py | 300 --- .../test_todo_lifecycle_authority_config.py | 22 +- .../test_todo_mutation_authority.py | 1597 +------------ .../test_todo_next_action_settlement.py | 333 +-- .../test_todo_priority_authoring.py | 29 +- .../test_unpromoted_todo_claim.py | 62 +- ..._user_completion_provider_followthrough.py | 108 +- .../canonical_task_lease_renew.test.ts | 22 +- .../handoff_mode_plan.test.ts | 63 +- .../legacy_writer_fence_caller_parity.test.ts | 101 - ...gacy_writer_fence_caller_parity_support.ts | 343 --- .../local_archive_attempt.test.ts | 12 +- .../local_authority_provider.test.ts | 36 +- .../local_authority_runtime.test.ts | 50 +- .../local_authority_shadow_outbox.test.ts | 101 +- tests/control_plane_ts/monitor_batch.test.ts | 332 ++- .../quota_monitor_poll_commit.test.ts | 1 - .../replan_settlement.test.ts | 6 +- .../shadow_entry_delivery.test.ts | 29 - .../shadow_native_writer_boundary.test.ts | 71 +- .../task_lease_acquire.test.ts | 749 +----- .../task_lease_acquire_cli.test.ts | 121 +- .../task_lease_lifecycle.test.ts | 1916 ++-------------- .../todo_continuation.test.ts | 2 +- .../todo_monitor_poll.test.ts | 8 +- tests/control_plane_ts/todo_update.test.ts | 58 + .../legacy_writer_fence_caller_parity_v0.json | 2007 ----------------- tests/test_cli_argument_diagnostics.py | 21 + tsconfig.control-plane.json | 2 - 214 files changed, 6189 insertions(+), 28326 deletions(-) create mode 100644 loopx/control_plane/coordination/goal_storage_initialization.py delete mode 100644 loopx/control_plane/coordination/handoff_mode_legacy_plan.ts delete mode 100644 loopx/control_plane/coordination/runtime_shadow_writer_adapter.py delete mode 100644 loopx/control_plane/scheduler/legacy_monitor_poll.py delete mode 100644 loopx/control_plane/testing/authority_e2e_rows_stage2c.py delete mode 100644 loopx/control_plane/testing/authority_e2e_rows_stage2c2.py delete mode 100644 loopx/control_plane/todos/handoff_mode_source.py delete mode 100644 loopx/control_plane/todos/legacy_mutation.py delete mode 100644 loopx/control_plane/todos/line_update.py create mode 100644 tests/architecture/test_legacy_markdown_writer_retirement.py delete mode 100644 tests/control_plane/test_cold_source_disposition_e2e.py delete mode 100644 tests/control_plane/test_goal_handoff_mode.py delete mode 100644 tests/control_plane/test_handoff_mode_source.py delete mode 100644 tests/control_plane/test_large_source_journey.py delete mode 100644 tests/control_plane/test_local_authority_shadow_cli_e2e.py delete mode 100644 tests/control_plane/test_local_authority_shadow_drain.py delete mode 100644 tests/control_plane/test_local_authority_shadow_outbox.py delete mode 100644 tests/control_plane/test_migrated_terminal_closeout_cli.py delete mode 100644 tests/control_plane/test_monitor_receipt_writer_isolation.py delete mode 100644 tests/control_plane/test_promoted_archive_retention.py delete mode 100644 tests/control_plane/test_reviewed_promotion_cli.py delete mode 100644 tests/control_plane/test_runtime_shadow_bounded_e2e.py delete mode 100644 tests/control_plane/test_runtime_shadow_writer_capture.py delete mode 100644 tests/control_plane/test_shadow_cursor_recovery_e2e.py delete mode 100644 tests/control_plane/test_shadow_drain_adversarial.py delete mode 100644 tests/control_plane/test_shadow_drain_e2e.py delete mode 100644 tests/control_plane/test_shadow_fence_caller_parity_e2e.py delete mode 100644 tests/control_plane/test_shadow_goal_instance.py delete mode 100644 tests/control_plane/test_shadow_management_e2e.py delete mode 100644 tests/control_plane/test_shadow_observable_e2e.py delete mode 100644 tests/control_plane/test_shadow_observable_native_e2e.py delete mode 100644 tests/control_plane/test_shadow_observation_retirement.py delete mode 100644 tests/control_plane/test_shadow_readback_writer_isolation.py delete mode 100644 tests/control_plane/test_shadow_writer_variant_e2e.py delete mode 100644 tests/control_plane/test_todo_field_update_runtime.py delete mode 100644 tests/control_plane_ts/legacy_writer_fence_caller_parity.test.ts delete mode 100644 tests/control_plane_ts/legacy_writer_fence_caller_parity_support.ts delete mode 100644 tests/fixtures/control_plane/legacy_writer_fence_caller_parity_v0.json diff --git a/docs/book/chapters/core-state-machines.md b/docs/book/chapters/core-state-machines.md index a14365f283..baadf6ceb6 100644 --- a/docs/book/chapters/core-state-machines.md +++ b/docs/book/chapters/core-state-machines.md @@ -535,8 +535,8 @@ observation 与它请求的独立 successor**。事务先校验 actor 注册、c 这里的“原子”有明确边界:它不包含外部网络 poll、quota spend 或 Markdown/dashboard delivery。 canonical commit 成功后,projection 仍可能处于 `pending`;outbox 只重试显示投影, -`retry_business_mutation=false`,不能再次执行已经提交的 observation/successor。没有提升 authority 的 -Goal 仍走 legacy writer,因此这项保证不能外推成“整个 Goal 的所有状态已统一迁移”。 +`retry_business_mutation=false`,不能再次执行已经提交的 observation/successor。2.0 中未迁移 +Goal 返回 `canonical_authority_required`,必须先备份并迁移,之后才能执行新的 Monitor 写入。 把 future `next_due_at` 当作 advancement frontier 是错误的:它只说明何时观察,不能替代“下一步 怎样推进 Goal”。 diff --git a/docs/book/en/chapters/core-state-machines.md b/docs/book/en/chapters/core-state-machines.md index c122fac9e1..7f49b0ee52 100644 --- a/docs/book/en/chapters/core-state-machines.md +++ b/docs/book/en/chapters/core-state-machines.md @@ -576,8 +576,9 @@ observation updates observation state and cadence without creating a delivery To “Atomic” has a precise boundary here: it excludes the external network poll, quota spend, and Markdown/dashboard delivery. After the canonical commit, projection delivery may still be `pending`; its outbox retries display projection only, with `retry_business_mutation=false`, and must not repeat the committed -observation/successor mutation. Goals not promoted to shared authority still use the legacy writer, so this guarantee -must not be generalized into “all Goal state has migrated to one store.” +observation/successor mutation. In 2.0 an unmigrated Goal returns +`canonical_authority_required`; it must be backed up and migrated before a new +Monitor write. Treating a future `next_due_at` as an advancement frontier is wrong. It says when to observe, not how to advance the Goal. diff --git a/docs/concepts/interaction-pattern-catalog.md b/docs/concepts/interaction-pattern-catalog.md index 6a4a967051..9f3aeca5cd 100644 --- a/docs/concepts/interaction-pattern-catalog.md +++ b/docs/concepts/interaction-pattern-catalog.md @@ -2756,15 +2756,11 @@ flowchart TD **Validation** -- `tests/control_plane/test_local_authority_shadow_cli_e2e.py` runs the real CLI - upgrade journey: `:10` proves a retained retired setting can neither enable - capture nor satisfy a bootstrap, `:20` asserts the rejection names - `local_authority_shadow_retired`, and `:25` asserts status still reads - `retired`. -- `tests/control_plane/test_local_authority_shadow_config.py` pins both halves: - `:49` a retired enable rejects without rewriting the registry (dry-run and - execute), `:58` clearing preserves runtime configuration and peer - registration. +- `tests/control_plane/test_local_authority_shadow_config.py` exercises the real + `configure-goal` CLI. Retired activation returns + `local_authority_shadow_retired`, leaves the registry unchanged and exposes + no enable action. Clearing the retired key preserves peer registration and + only enables runtime configuration when the caller requests it explicitly. - `tests/control_plane/test_state_migration_authority_shadow.py` keeps `migrate-state` reporting retirement instead of seeding it, and `tests/control_plane_ts/local_authority_shadow.test.ts` keeps the old RPC diff --git a/docs/development/control-plane-course/05-work-graph-and-peers.md b/docs/development/control-plane-course/05-work-graph-and-peers.md index c1931e0f15..d8a66f634c 100644 --- a/docs/development/control-plane-course/05-work-graph-and-peers.md +++ b/docs/development/control-plane-course/05-work-graph-and-peers.md @@ -217,11 +217,9 @@ if (rejection === "claim_owner_mismatch") { } ``` -这是同一 `authority` 决策中的片段,不是新的调用接口。未晋升源适配器 -`mutation_authority.py::authorize_todo_lifecycle_mutation` 仍在 owner mismatch 后 -读取并规范化 registry grant,再通过 `authority_core.decide` 调用同一 TS owner; -canonical 事务也复用该 owner。Python facts、grant 校验和 receipt 投影保留, -不再经由无人调用的单项 grant 查询。 +这是同一 `authority` 决策中的片段,不是新的调用接口。canonical 事务从 registry +读取并规范化 grant,将 facts 交给 TS owner 统一决策。Python 只保留 registry facts +与 grant 配置校验,不再暴露独立的 lifecycle mutation authorizer。 这份 grant 有四个边界: diff --git a/docs/reference/authority-observation-retirement.md b/docs/reference/authority-observation-retirement.md index 4a09018aee..d4fe5200fd 100644 --- a/docs/reference/authority-observation-retirement.md +++ b/docs/reference/authority-observation-retirement.md @@ -3,9 +3,11 @@ The `coordination.authority_shadow` / `file_one_way` observer is retired. Todo, handoff-mode and task-lease writes no longer re-read the Goal after commit or create a second observation store. Their primary semantics are unchanged. -The transaction-bound `coordination.runtime_shadow` outbox remains the only -writable shadow lineage; its existing bootstrap, source locks, byte witnesses, -recovery and promotion checks remain mandatory. +The retained `coordination.runtime_shadow` files are pre-cutoff migration and +recovery evidence. Normal 2.0 writes require canonical authority and no longer +feed a Markdown shadow lineage. Existing bootstrap, source locks, byte +witnesses, recovery and promotion checks remain available for reviewed import +and interrupted-operation recovery. This is an intentional behavior change for Goals that opted into the old observer. Unconfigured Goals remain default-off. A retained old setting is @@ -88,12 +90,10 @@ Bootstrap, rollback and promotion still load their supported management adapter; this reader exit grants no writer cutoff, import or promotion authority. The retained Python filesystem codecs preserve the existing wire and errors; -candidate proof and drain effects remain in their TypeScript owners. Supported -unpromoted writes and `authority-shadow drain` still need their current producer -and orchestration modules. This separation permits eventual producer retirement -without discarding historical readback, but does not itself authorize deletion -or disposition of pending originals. Code rollback restores the previous import -layout without changing data or restoring retired write authority. +candidate proof and drain effects remain in their TypeScript owners. Historical +outbox drain and disposition retain their orchestration modules without +reopening a business writer. Pending originals still require explicit +reconciliation; historical readback does not authorize deletion. ## Why two matching reads were not a capture guarantee diff --git a/docs/reference/canonical-terminal-review.md b/docs/reference/canonical-terminal-review.md index d04bae8c5d..aa9be08b7a 100644 --- a/docs/reference/canonical-terminal-review.md +++ b/docs/reference/canonical-terminal-review.md @@ -173,9 +173,9 @@ may remain a historical relation without being reported as open agent work. Original-operation replay returns the originally accepted completion policy, including receipts written by earlier releases, without rewriting it against today's head. Current Todo reads show subsequent changes independently. The -unpromoted Markdown writer retains its source-backed compatibility path; this -change does not require migration or retire that writer. Rollback restores the -earlier adapter and transaction together; no stored format migration is needed. +2.0 command requires canonical authority; an unmigrated Goal must use the +reviewed backup-and-migrate path before a new terminal write. Historical +receipts remain readable without rewriting their stored format. ## Retiring an original Turn with an existing successor diff --git a/docs/reference/canonical-todo-completion-update.md b/docs/reference/canonical-todo-completion-update.md index a9c7ee113d..22d383f0d3 100644 --- a/docs/reference/canonical-todo-completion-update.md +++ b/docs/reference/canonical-todo-completion-update.md @@ -5,6 +5,15 @@ through the TypeScript terminal transaction. File and SQLite use the same semantic owner; PostgreSQL exercises it through the service-owned provider. This does not select a default provider or promote an existing Goal. +> **2.0 cutoff:** normal Todo, task-lease, handoff-mode, Monitor, team-plan, +> bootstrap and registration writes require canonical File, SQLite or +> PostgreSQL authority. An unmigrated Goal returns +> `canonical_authority_required` with backup-and-migrate recovery. The later +> "isolation" sections record the pre-cutoff migration stages; they no longer +> describe an available Markdown write route. Projection/readback, reviewed +> cold import, backup/history, historical receipt replay, outbox disposition, +> exact legacy lease release and historical `fence_close` remain supported. + ```sh loopx todo update --goal-id example --todo-id todo_observation \ --agent-id agent-a --status done --note 'Observed outcome verified' \ @@ -91,18 +100,18 @@ With no other edit, a whitespace-only note or direct empty facade edit reports `Todo update requires a non-empty patch` on File/SQLite without importing the legacy Markdown editor, committing a receipt or changing the record. This replaces the erroneous legacy-writer/fence diagnostic. A valid -retry with the same operation id remains available. Unpromoted Goals retain -their existing empty-edit no-change behavior. No provider or ownership default -changes, and removing this Python routing predicate does not retire the source -writer, capture/recovery readers or permanent Markdown display. +retry with the same operation id remains available. An unmigrated Goal rejects +the edit with `canonical_authority_required`, including an empty edit. The +retired source writer and capture producers are not loaded. Permanent Markdown +display, reviewed cold import and historical recovery readers remain. 普通 promoted 更新 facade 均交由既有 TS decoder,包括空修改。空白 `--note` 仍表示 省略,不清除已有 note;CLI 对无字段和空字符串保留原参数检查。只有空白 note 或 直接 facade 的空修改在 File/SQLite 返回明确的非空 patch 要求,不加载旧 Markdown editor、不提交回执或修改记录。同一 operation id 仍可用于后续合法重试。 -这修正了误入旧 writer/fence 的诊断;未晋升 Goal 的空修改 no-change 行为保持。 -provider 与 ownership 默认不变,旧 writer、capture/恢复 reader 和永久 Markdown -投影不因删除该 Python 路由判断而退役。 +这修正了误入旧 writer/fence 的诊断;未迁移 Goal 的空修改也返回 +`canonical_authority_required`。旧 writer 和 capture producer 已退役,不再加载; +永久 Markdown 投影、审核冷导入和历史恢复 reader 继续保留。 The update decoder, authoring planner and record materializer are shared with ordinary edits. The terminal owner checks the **original** Todo's completion @@ -169,9 +178,9 @@ Python 只复用 OS I/O;准入、声明、期限、源/租约见证和完成 An admitted `todo complete --decision-outcome approve|reject|cancel` now commits its exact linked Agent Todo effects in the **same** provider transaction as the User completion and receipt. Ordinary User actions completed through update or -Chat use the same rule. `todos/user_completion.ts` owns this decision for both -canonical providers and the legacy Markdown adapter; Python retains locked -snapshot extraction and writeback, not a second scope/resume implementation. +Chat use the same rule. `todos/user_completion.ts` owns this decision for all +canonical providers. Python retains transport and readback adapters, not a +second scope/resume implementation. | Decision / current target | Effect | | --- | --- | @@ -190,10 +199,9 @@ User gate itself. Supersede never runs approval effects. An unrelated Todo or unlinked standing approval is outside this exact-target mutation. This fixes canonical completion previously leaving an approved target stranded. -It also intentionally tightens **both** legacy and canonical behavior: partial -approval cannot resume work with unmet requirements, and late rejection cannot -revive terminal/deferred work. Normal completion without a linked target is -unchanged. No provider selector or capability default changes. +Partial approval cannot resume work with unmet requirements, and late rejection +cannot revive terminal/deferred work. Normal canonical completion without a +linked target is unchanged. No provider selector or capability default changes. `unblock_resume` and `decision_scope_resolution` are historical business results, not fresh permission. Existing receipt identities remain valid. Replaying a @@ -244,20 +252,18 @@ Compatibility: canonical `todo complete` continues to accept a `user_gate` without `--decision-outcome` as closure only, not approval, rejection or cancellation. Its required scopes, scope outcomes and blocked dependents remain unchanged, just as for `todo update --status done`. To record a decision and its -linked effects, explicitly supply `approve|reject|cancel`. The legacy Markdown -explicit-completion adapter keeps its pre-existing requirement for a decision; -that adapter's stricter input rule is not imposed on native callers. Historical -successful receipts remain replayable as recorded. +linked effects, explicitly supply `approve|reject|cancel`. An unmigrated Goal +cannot use this compatibility path; it must migrate first. Historical successful +receipts remain replayable as recorded. The delivered cancellation entry point is CLI/managed CLI. Existing Chat completion continues through the shared terminal owner, and frontend/Lark consumers read the canonical completed status; no new cancellation button, configuration setting or chat-specific authority is introduced. Direct frontend/Lark cancellation controls are not part of this bounded slice. The -canonical transaction is qualified on File, SQLite and real PostgreSQL. The -legacy Markdown adapter shares outcome validation and cancellation effects, -but its separate hard-lease terminal fence is not changed. Historical receipts -are replayed as recorded, not reinterpreted as a new cancellation. +canonical transaction is qualified on File, SQLite and real PostgreSQL. +Historical receipts are replayed as recorded, not reinterpreted as a new +cancellation or fresh write authority. 已晋升且启用 `hard_lease` 的 Goal 中,精确绑定、已注册且未被排除的 Agent 可以 关闭普通 `user_action`,无需取得执行租约。用户事项本来不能领取执行租约;这里是 @@ -360,41 +366,19 @@ feedback、迁移和 team-plan 调用方复用同一 Host IO owner。旧编辑 三个定义,仍活跃的源编辑/读取函数保留。故障仍进入原调用方恢复契约,不切换 provider、默认值、格式或权威策略,也不强制旧 Goal 升级。 -Canonical Todo creation, update, completion, supersession and archive do not -import the Markdown line writer or source Todo capture producers during CLI -registration. Bootstrap loads capture producers only for a source-state write; -canonical creation and original-operation recovery do not need them. The source -adapter loads them only when an unpromoted operation needs them. Explicit historic imports -from `loopx.todos` still resolve to the same functions in -`control_plane.todos.line_update`; they do not create another decision owner. -Provider failure still rejects the canonical operation without a source write. - -The absence regression runs real File/SQLite new-Goal creation and Todo -lifecycle commands with the line writer and four source Todo capture producer -functions physically removed from a disposable package. Prose write guards and -lease capture evidence remain in their owning adapter and keep their live callers. -Original creation recovery preserves later work when the display is -missing and device defaults change; an unavailable selected provider requires -restoration rather than rebuilding source authority. It qualifies that caller -boundary, not deletion of the supported Markdown writer, backup readers or -receipt recovery. Those paths retain their own last-caller migration exits. -The same absence check covers admitted hard-lease work through material vision -writeback, one quota settlement and exact retry. Missing leases and provider -outages still reject; rebuilding the display does not make it fallback authority. - -canonical Todo 创建、更新、完成、替代和归档不再在 CLI 注册时导入 Markdown -行写入器或旧源 Todo capture producer;bootstrap 仅在源状态写入时加载 capture, -canonical 创建和原操作恢复不依赖它。未迁移操作实际需要旧路径时才加载。 -`loopx.todos` 的历史显式导入仍指向 -同一实现,不新增决策 owner。provider 失效仍拒绝 canonical 写入,不回退到旧源。 -缺失模块回归在一次性包中物理移除行写入器及四个旧源 Todo capture 函数,并运行真实 File/SQLite 新 Goal 创建与 -Todo 生命周期。显示源缺失且设备默认值改变后,原创建操作恢复仍保留后续工作;所选 -provider 不可用时要求恢复,不重建旧源权威。 -同一 adapter 中仍有真实调用方的 prose 写入边界和 lease capture evidence 保留。 -相同缺失模块检查覆盖 hard-lease 工作的准入、实质 vision 写回、一次额度结算与原 -Turn 重试;缺租约和 provider 失效仍拒绝,重建显示不会使它成为回退权威。 -这验证调用方隔离;仍受支持的 Markdown writer、备份读取和原回执恢复保留各自的 -迁移与最后调用方退役条件。 +Canonical Todo creation, update, completion, supersession and archive require +File, SQLite or PostgreSQL authority. The Markdown line writer, source Todo +capture producers and their compatibility imports are retired. Provider failure +rejects the operation without a source write. The absence regression runs the +real File/SQLite lifecycle and hard-lease settlement paths with those modules +physically absent. Permanent Markdown projection, backup readers, reviewed cold +import, historical receipt replay and bounded outbox disposition remain. + +canonical Todo 创建、更新、完成、替代和归档只接受 File、SQLite 或 PostgreSQL +authority。Markdown 行写入器、旧源 Todo capture producer 及兼容导入均已退役。 +provider 失效时明确拒绝,不回退写旧源。真实 File/SQLite 生命周期和 hard-lease +结算会在这些模块物理缺席时运行。永久 Markdown 投影、备份读取、审核冷导入、 +历史回执重放和有限 outbox 处置继续保留。 This closes the User completion-update caller within TS T1/T2 and local-default L2. It does not close every Monitor/event caller, executor-held effect fencing, @@ -494,101 +478,59 @@ The retired RPC rejects unsupported-method requests. Continuation selection, contradictory-intent rejection and historical terminal recovery remain covered through the production owner. Python normalization/cache, import-compatible enums, original receipt values, Markdown projection, backup readers and live -Host IO remain. This removes neither the Markdown writer nor shadow capture, -and changes no provider default. File/SQLite CLI completion and settlement -recovery are the real-path checks; installation qualification also runs those -paths from a wheel with the retired entries absent. Reverting this code restores -the internal crossing without converting data or rewriting receipts. +Host IO remain. The Markdown writer and shadow capture producers are retired. +File/SQLite CLI completion and settlement recovery are the real-path checks; +installation qualification also runs those paths from a wheel with the retired +entries absent. Reverting this code restores the internal crossing without +converting data or rewriting receipts. 完成与 terminal update 已在现有 TS 事务中组合完成状态规则。本批只退役两个无生产 调用方的 Python 入口及其不再使用的 RPC carrier;选择 continuation、拒绝矛盾意图 和历史收尾恢复仍由原 TS owner 承担。Python 编解码/cache、兼容枚举、原回执、 Markdown 投影、备份读取及活跃 Host IO 保留。真实 File/SQLite CLI 和删除旧入口后的 -wheel 路径验证完成与结算恢复;本批不删除 Markdown writer/shadow,也不改变默认 -provider。代码回滚不需要转换数据或重写回执。 +wheel 路径验证完成与结算恢复;Markdown writer 和 shadow capture producer 已 +退役。代码回滚不需要转换数据或重写回执。 -## Canonical mutation entrypoint isolation +## 2.0 canonical mutation boundary Create, update, complete, supersede and archive share `control_plane/todos/mutation_api.py` and the existing TypeScript transaction -owners. The historic `loopx.todos` imports remain available lazily. The -unpromoted writer bodies live in `control_plane/todos/legacy_mutation.py`; -canonical CLI registration and provider operations do not import that module. -This is a mechanical separation, with no Python deletion payoff or new decision -owner. Shared validation, successor intent and receipt adapters remain live. +owners. An unmigrated Goal receives `canonical_authority_required`; no mutation +entrypoint loads a Markdown writer or capture producer. Shared validation, +successor intent and historical receipt readers remain live. | Caller family | Current boundary and retained duty | | --- | --- | | CLI and App canonical Todo mutations | Existing provider-first create/update/terminal owners; missing selected authority fails without starting a Markdown writer. | -| Unpromoted create/update/complete/supersede/archive | Load the isolated writer only after the existing provider owner declines an unpromoted request; preserve writer fence, capture preparation, original receipts and projection. | -| Bootstrap, handoff, team-plan and Monitor source capture | Still have supported unpromoted callers; retain producers and outbox reconciliation until each caller exits. | -| Import-compatible `apply_todo_update_to_lines` and legacy terminal updates | The existing `todo.field_update.plan` owns supplied wait normalization and contradictory clears in the complete edit. Python retains blank-as-omission transport, locked Markdown IO and receipt formatting, with no separate resume-normalization preflight. | -| Markdown projection, source import and backup recovery | Retain qualified readers, historical bytes and Host IO; moving writer code does not grant deletion of these paths. | - -The File/SQLite lifecycle and settlement oracle removes the **whole isolated -writer module**, line editor and four capture producers from a copy of the -installed package. It covers original creation replay, update, completion, -supersede/archive, leased settlement, restart recovery and provider loss. -The same source and installed-wheel arms also recover a real legacy outbox -transaction interrupted before or after the native store commit, after those -producer definitions are physically removed. Recovery preserves source bytes -and original receipts, drains exactly once and rejects an unproved cursor -without deleting pending entries. This qualifies the retained File shadow drain; -it does not qualify cold backup import or dispose of an active Goal's outbox. -Retained Markdown interruption/capture tests guard the compatibility arm. -The cold-import CLI oracle also removes that whole writer and line editor -before preparing and applying a reviewed source backup. After import, real -File/SQLite CLI operations reject an unleased edit, acquire a fresh lease, -update imported work and create new work. Original import recovery and Todo -operation replay preserve the current complete records, archived evidence and -provider revision after the source display is removed. Losing the provider -refuses recovery without creating a replacement; restoring its original bytes -recovers both imported and later writes. This composes the migration and writer -isolation boundaries; the historic facade and still-live capture adapters remain -in the post-import mutation arm. -Reverting the code move requires no data conversion or receipt rewriting. -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. - -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 -removes a duplicate decision crossing on the compatibility entrypoint; canonical -updates already use their whole transaction and gain no new route or authority. - -创建、更新、完成、替代和归档统一进入既有 `mutation_api.py` 与 TS 事务 owner, -`loopx.todos` 保留惰性历史导入。未迁移 writer 已隔离到 `legacy_mutation.py`, -canonical CLI 注册及 provider 操作不加载它;这只是机械隔离,不计作 Python -删除收益,也不新增决策源。共享验证、successor intent 与原回执适配器仍有用途。 -安装包副本物理移除整个 writer、行编辑器及四个 capture producer 后,真实 -File/SQLite CLI 验证生命周期、租约结算、重启与 provider 失效恢复。未迁移 -Markdown outbox 的真实 native commit 前/后中断,在移除 producer 后由相同 -源码/安装包两臂恢复:源字节与原回执保持、恰好一次 drain,未证明的 cursor -被拒绝且 pending 条目不被删除。这验证保留的 File shadow drain,活跃 Goal 的 -outbox 仍须逐项处置;中断/capture 反例继续保留,代码回滚不转换数据或重写回执。 -冷导入 CLI 检查在审核备份、准备及应用前也移除整个 writer 和行编辑器,随后 -真实 File/SQLite CLI 拒绝无租约编辑、取得新租约、更新导入任务并新增任务。 -删除显示源后,原导入恢复和 Todo 原操作重放保留完整的新旧记录、归档证据及 -当前 provider revision;provider 丢失时明确拒绝,不重建空库,恢复原字节后 -新旧写入仍可读。该旅程组合迁移及 writer 隔离边界;迁移后的 mutation 检查 -仍保留历史 facade 和有真实调用的 capture 适配器。 -bootstrap、handoff、team-plan 和 Monitor 的旧源调用尚须分别退役,最终删除仍须 -支持截止及 outbox 处置;本批不强制升级或更改 provider 默认值。 -保留的 `apply_todo_update_to_lines` 兼容入口与 legacy terminal 更新由既有 -`todo.field_update.plan` 在完整编辑中统一归一化等待条件、拒绝矛盾 clear;Python -只适配空白值省略、锁内 Markdown IO 与回执格式,不再单独 preflight resume。 -时区归一化、显式清除和拒绝时源字节不变继续保持;非法条件使用 typed owner -的可操作诊断。canonical 更新本就使用完整事务,本批不新增路由或权限。 - -## Team-plan source-writer isolation +| Unpromoted create/update/complete/supersede/archive | Reject before write and return backup-and-migrate recovery. | +| Bootstrap, handoff, team-plan and Monitor | Require canonical authority for new writes; retain only historical receipt replay and outbox disposition. | +| Markdown projection, reviewed source import and backup recovery | Retain qualified readers, historical bytes and Host IO without granting write authority. | + +The File/SQLite lifecycle and settlement oracle runs from source and installed +packages with the retired writer, line editor and capture producers absent. It +covers create, update, completion, supersede/archive, leased settlement, restart +recovery and provider loss. Historical outbox tests seed persisted entries +directly. Recovery preserves source bytes and original receipts, drains exactly +once where disposition permits it, and rejects an unproved cursor without +deleting pending entries. + +The cold-import oracle prepares and applies a reviewed source backup without +calling a retired producer. After import, real File/SQLite operations acquire +canonical authority and preserve later writes across original-operation replay. +Losing the provider refuses recovery without creating a replacement. + +创建、更新、完成、替代和归档统一进入 `mutation_api.py` 与 TS 事务 owner。 +源码和安装包验收会物理移除旧 writer、行编辑器和 capture producer,再运行 +File/SQLite 生命周期、租约结算、重启及 provider 故障恢复。历史 outbox 用持久化 +样本直接构造,不调用已退役 producer。冷导入从审核备份开始,导入后只走 +canonical 写入;provider 丢失时明确拒绝,不重建空库。 + +## 2.0 team-plan mutation boundary Canonical team-plan preview, confirmed batch commit and original-operation -replay do not load `runtime_shadow_writer_adapter.py`. Source locks, reviewed -intent, the TypeScript batch transaction and canonical projection outbox keep -their existing owners. Only the supported unpromoted branch loads the capture -producer and Markdown editing dependencies. Its batch-write IO seam remains -available to existing failure/recovery consumers through a lazy adapter. +replay use the TypeScript batch transaction and canonical projection outbox. +An unpromoted request returns `canonical_authority_required`; it cannot load a +capture producer or Markdown editor. The real File/SQLite Chat oracle physically removes the capture adapter from the package before preview and confirmation. It checks provider outage without @@ -598,37 +540,28 @@ pre-promotion receipts remain historical readback: replay preserves newer work and the current provider revision. Normal wheel qualification uses the same oracle; the source checkout cannot substitute for its installed package. -This isolates another canonical caller, without deleting the still-supported -legacy branch, changing provider defaults or requiring existing Goals to migrate. Projection IO and historical receipt readers remain necessary. Assignment still grants neither receiver adoption nor execution authority. -canonical team-plan 的预览、确认后整批提交与原操作重放不再加载 capture adapter; -源锁、审核意图、TS batch 事务及 canonical 投影 outbox 继续复用原 owner。 -只有受支持的未迁移分支加载 capture producer 与 Markdown 编辑依赖,既有批量 -写入故障/恢复消费方通过惰性 IO adapter 保留其调用入口。 +canonical team-plan 的预览、确认后整批提交与原操作重放复用 TS batch 事务及 +canonical 投影 outbox。未迁移请求返回 `canonical_authority_required`,不能加载 +capture producer 或 Markdown editor。 真实 File/SQLite Chat 检查在预览及确认前物理移除该模块,验证 provider 失效无 Markdown fallback、恢复、后续写入后的原操作重放、审核依据过期及提交后投影失败 恢复;导入前的历史回执重放保留后来写入与当前 revision。正常 wheel 复用同一 -oracle,不能以源码检查代替安装包。本批只隔离 canonical 调用,不删除仍受支持的 -旧源分支、不改变默认值或强制迁移;投影 IO、历史 reader 继续保留,分派不授予 +oracle,不能以源码检查代替安装包。投影 IO、历史 reader 继续保留,分派不授予 receiver adoption 或执行权。 -## Monitor source-writer isolation +## 2.0 Monitor mutation boundary -Canonical Monitor polling does not load the live `legacy_monitor_poll.py` -writer. Immutable pre-promotion receipt codecs and replay now live in the -scheduler's `monitor_batch_receipts.py`, shared by the router and the supported -legacy writer. The TypeScript batch planner still validates the original -operation and supplies its unchanged writeback. There is no new decision owner, -receipt format, provider setting or execution authority. +Canonical Monitor polling uses the TypeScript batch planner and provider +writeback. `legacy_monitor_poll.py` is retired. Immutable pre-promotion receipt +codecs and replay live in `scheduler/monitor_batch_receipts.py`. There is no new +decision owner, receipt format, provider setting or execution authority. -On historical replay, the router checks promotion before loading shadow IO. -After cutover, even ambiguous retained source outbox bytes remain inert: replay -must not drain them into the current authority. Before cutover, an ordinary -retry still loads the existing drain owner when an outbox is present, recovering -one markerless prepared batch after the atomic Markdown replacement. That -valuable recovery path and the live legacy instrumentation seam remain. +On historical replay, ambiguous retained source outbox bytes remain inert and +cannot grant current authority. Bounded disposition reads persisted historical +entries; it does not reopen a Monitor business writer. Real File/SQLite CLI checks physically remove the entire Monitor writer from a copied source package or a normally installed wheel, together with all four @@ -636,26 +569,17 @@ shadow producer modules after CLI startup isolation. They cover no-change and material-change polling, provider outage/restoration, lease-release replay and recovery of an interrupted pre-promotion operation after newer canonical work. Recovery preserves the original successor identities, current provider revision, -state and retained outbox bytes, with one event and no quota spend. A fresh -production provider call proves these dependencies are not loaded even when -the modules are present. Existing malformed-receipt, intent-conflict, writer-fence and -unpromoted outbox recovery counterexamples remain required. - -This qualifies the Monitor caller boundary, not removal of the still-supported -source writer or all capture producers. Retain historical readers, backup/import -recovery and original outbox disposition until their own exits pass. No default, -existing Goal upgrade requirement, PostgreSQL transaction or Host lifecycle -changes. Reverting this isolation restores imports without rewriting receipts; -it does not restore legacy write authority after cutover. - -canonical Monitor poll 不再加载整个 `legacy_monitor_poll.py` 活跃 writer。 -晋升前的 immutable 回执 codec/replay 归入 scheduler 的 -`monitor_batch_receipts.py`,路由及旧 writer 共用;TS batch planner 继续校验 -原操作并返回相同 writeback,不新增决策源、回执格式、provider 配置或执行权。 -历史重放先检查晋升状态再加载 shadow IO。晋升后,含歧义的原 outbox 字节仍须 -保持静止,不导入当前 authority;晋升前的普通重试则在 outbox 存在时调用原 drain -owner,恢复一次 Markdown 原子替换后未写 commit marker 的 prepared batch。 -保留这条有效恢复路径和现有 legacy 故障注入入口。 +state and retained outbox bytes, with one event and no quota spend. Existing +malformed-receipt, intent-conflict, writer-fence and historical outbox +counterexamples remain required. + +Historical readers, backup/import recovery and original outbox disposition +remain. No provider default, PostgreSQL transaction or Host lifecycle changes. + +canonical Monitor poll 使用 TS batch planner 和 provider writeback; +`legacy_monitor_poll.py` 已退役。晋升前的 immutable 回执 codec/replay 归入 +`monitor_batch_receipts.py`。历史 outbox 字节保持静止,有限处置只读取持久化 +历史条目,不恢复 Monitor 业务 writer。 真实 File/SQLite CLI 检查在复制源码/正常安装 wheel 中物理移除整个 Monitor writer,并在 CLI 启动隔离后移除四个 shadow producer module;覆盖无变化/实质 @@ -664,10 +588,8 @@ writer,并在 CLI 启动隔离后移除四个 shadow producer module;覆盖 及原 outbox 字节保持,一次 event 且无 quota spend。冷 production provider 调用 另证明模块仍存在时也不加载旧 writer/drain。损坏回执、意图冲突、writer fence 和未晋升 outbox 恢复反例仍须通过。 -本批只验收 Monitor 调用边界,不删除受支持的源 writer/全部 capture producer; -历史 reader、备份导入恢复及原 outbox 处置各保留自己的出口。不改变默认、已有 -Goal 升级要求、PostgreSQL 事务或 Host 生命周期。回滚只恢复导入关系,不改写 -回执,也不恢复晋升后的 legacy 写权限。 +历史 reader、备份导入恢复及原 outbox 处置继续保留。不改变默认、 +PostgreSQL 事务或 Host 生命周期。 ## Frontier classification retirement @@ -686,10 +608,10 @@ cache or new public packet field. File/SQLite quota CLI checks use the same independent oracle before and after the move. The permanent Markdown narrative projection remains required by -status; canonical storage does not retire that document IO. Existing Markdown -writers, source capture, old backups and original-operation recovery keep -their live callers and separate retirement exits. Code rollback restores the -internal classifier without migrating Goal data or changing provider defaults. +status; canonical storage does not retire that document IO. Old backups and +historical original-operation recovery keep their declared readers. Code +rollback restores the internal classifier without migrating Goal data or +changing provider defaults. Goal frontier 的等待、fallback 与重规划调用方保留 Python import 和旧输入编码; summary 视图优先级、认领/排除分组及诊断计数下界交给既有 TS frontier owner, @@ -699,5 +621,4 @@ summary 视图优先级、认领/排除分组及诊断计数下界交给既有 同次 Goal context reduction 复用 fresh 计数给重规划及最终投影(含原回执恢复); 独立 helper 仍读取当前 summary,不新增跨调用缓存或公共 packet 字段。 真实 File/SQLite quota CLI 使用迁移前后同一独立断言。永久 Markdown 叙事投影、 -仍有调用方的 writer/capture、旧备份及原操作恢复继续保留。本批不转换数据或回执, -不改变 provider 默认,也不代表全部 Python 或旧 writer 已退役。 +旧备份及历史原操作恢复继续保留。本批不转换数据或回执,不改变 provider 默认。 diff --git a/docs/reference/local-authority-provider-selection.md b/docs/reference/local-authority-provider-selection.md index 054995ccf6..89286a3bbe 100644 --- a/docs/reference/local-authority-provider-selection.md +++ b/docs/reference/local-authority-provider-selection.md @@ -266,18 +266,18 @@ boolean and integer facts remain distinct even where Python object equality would equate them. This uses the same encoding boundary as capture identity, without granting a prose writer any Todo mutation or provider fallback. -## Retirement boundaries before changing the release default +## 2.0 retirement boundaries -SQLite adoption and Python retirement need separate evidence. A canonical Goal -can bypass a source writer while supported unmigrated Goals still call it. -Changing the creation default does not migrate those Goals or settle their -capture history. Use the existing [retirement cadence](../architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md) -to qualify each last caller family independently. +Normal mutation now requires canonical authority. Python still owns supported +Markdown projection, reviewed cold import and historical recovery adapters. +Changing the creation default does not migrate an existing Goal or settle its +historical obligations. The existing [retirement cadence](../architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md) +records how each former writer family reached this cutoff. -| Retained boundary | Current value and owner | Evidence needed before removal | +| Retained boundary | Current value and owner | Required evidence | | --- | --- | --- | -| `todos.py`, `bootstrap.py`, `todos/line_update.py` | Unmigrated source creation and Todo edits still use Markdown writes. Canonical lifecycle decisions belong to the typed provider transactions. | Migrate the corresponding supported callers; run creation, update and recovery with the old writer physically absent. Keep Markdown display projection and supported import/export. | -| `runtime_shadow_writer_adapter.py`, `local_authority_shadow_outbox.py` | Source writers prepare capture before the primary write and record commit afterward; outbox records preserve interrupted operations. Typed coordination owners interpret the captured mutations. | Stop the corresponding source producers, classify every pending prepared/committed entry against its source and exact receipt, and prove recovery after producer removal. Clearing configuration cannot cancel an active capture obligation. | +| `todos.py`, `bootstrap.py`, canonical Todo and lease adapters | Public mutation entrypoints admit only File, SQLite or PostgreSQL authority. Markdown remains a display and import source, not a write authority. | Run creation, update and recovery with the removed writer modules absent. Keep Markdown projection and supported import/export. | +| `local_authority_shadow_outbox.py` and its typed reader | New outbox production is removed. Historical prepared and committed entries remain available for bounded disposition and exact receipt replay. | Seed historical bytes directly, classify each entry against its source and receipt, and prove restart recovery without a business write. | | `local_authority_shadow_projection.py` | Python transports full source artifacts, rejects floats and unsafe integers before Python/JS digest comparison, and checks identity, size, digest and regular-file readback. `coordination.source.project` remains the typed projection owner. | Preserve exact-byte and malformed-input behavior in the replacement, including symlink rejection, bounded transfer and ambiguous-operation errors. A language-only rewrite is insufficient. | | `runtime_shadow.py`, `local_authority_shadow_adapter.py` | Explicit bootstrap, inspect, candidate read, drain and recovery still consume full source snapshots, cursor history and digest comparison. | Qualify the replacement management/recovery entrypoints and supported historical evidence; a canonical write passing does not establish these readers are unused. | | `authority_core.py` | Live Todo adapters delegate to typed mutation/ownership rules; scope overlap delegates to the typed lease owner. The lease-mode input compatibility contract is explicitly retained. | Trace direct, internal, dynamic and exported callers separately. Review registered historical input support before retiring its vocabulary; do not infer a fresh grant from old `legacy` inputs. | @@ -288,38 +288,34 @@ process cleanup, private validation declarations and supported backup/original receipt readers where they still serve real callers. Their I/O or recovery value is distinct from a duplicated decision owner. -For each proposed deletion, record the supported caller, replacement owner, -persisted obligations and rollback. Search references and exports, then exercise -the real CLI and installed package in a disposable runtime with the retired -path absent. Include interrupted writes, restart/replay, malformed evidence and -unavailable selected-provider cases. The provider must fail visibly without -falling back to a Markdown writer. Keep historical recovery coverage; reverse -cutover must retain writes made after the original migration. +The cutoff qualification records the supported caller, replacement owner, +persisted obligations and rollback. It exercises the real CLI and installed +package with the retired writer path absent. The provider fails visibly without +falling back to a Markdown writer. Historical recovery tests seed old evidence +directly instead of calling a retired producer. Useful existing regressions include `test_source_projection.py`, -`test_source_transfer.py`, `test_local_authority_shadow_outbox.py`, -`test_runtime_shadow_bounded_e2e.py`, `test_shadow_writer_boundaries.py` and -`test_shadow_cursor_recovery_e2e.py` under `tests/control_plane/`. Transport -fault tests use injected faults; source/recovery tests also run real CLI/native -writers against disposable File stores. These are bounded regression evidence, -not sustained SQLite, PostgreSQL, packaged App or release-default qualification. - -### Proposed compatibility cutoff and release sequence - -**Proposal, not installed behavior or a release announcement.** The current -implementation still accepts target-only v0 settings, explicit -`canonical_creation=false`, and normal writes to unpromoted Markdown Goals. -Retiring that supported write path is an incompatible change; target a **2.0** -release rather than silently removing it in a compatible 1.x update. This -follows [Semantic Versioning's compatibility distinction](https://semver.org/). -The implementation and release decision remain subject to the existing +`test_source_transfer.py`, `test_shadow_writer_boundaries.py`, +`test_cold_source_inspection.py`, `test_shadow_registry_source.py` and +`test_shadow_management_variant_e2e.py` under `tests/control_plane/`. These +tests cover cold source admission, historical management evidence and +producer-free recovery. Provider conformance and installed-package tests cover +the canonical write path separately. + +### 2.0 compatibility cutoff and release sequence + +The implementation still decodes target-only v0 settings and explicit +`canonical_creation=false` for inspection and reviewed configuration upgrade. +It rejects normal writes to unpromoted Markdown Goals. This is an incompatible +change for the 2.0 release, not a compatible 1.x update. The release decision +remains subject to the existing [shared-authority RFC](../architecture/rfcs/shared-goal-authority-state-provider-v0.md) and [retirement cadence](../architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md). -The proposed cutoff is **canonical authority before normal mutation**, not -SQLite for every Goal and not removal of every Python module: +The cutoff is **canonical authority before normal mutation**, not SQLite for +every Goal and not removal of every Python module: -| Existing state | Proposed major-release behavior | +| Existing state | 2.0 behavior | | --- | --- | | Unpromoted Markdown authority | Retain inspection and explicit upgrade/recovery; reject normal mutation before any write or new execution grant, with an actionable supported migration route. Do not silently initialize an empty canonical store. | | Canonical File or SQLite | Keep the recorded provider and either supported `soft_claim` or `hard_lease` policy. New unconfigured Goals default to SQLite; existing File Goals do not have to switch. Missing providers remain errors. | @@ -327,18 +323,12 @@ SQLite for every Goal and not removal of every Python module: | Target-only v0 or v1 creation disabled | Keep decoding for inspection and reviewed configuration upgrade. Require an explicit compatible creation choice before creating a writable Goal; do not erase the preference or reinterpret disabled as enabled. | | Historical ownership inputs, source captures and backups | Recognize them in declared migration/recovery contracts. Historical leases, receipts and a restored source do not grant new execution authority. Preserve supported original-operation recovery and permanent Markdown display. | -There is a concrete prerequisite for the unpromoted cohort: today's reviewed -promotion requires an enabled, bootstrapped shadow qualified by **real source -mutations**, with the saved operation/event policy. An empty shadow cannot pass. -If the major release first blocks the writer that produces those mutations, a -cold Goal cannot manufacture its own migration qualification. The selected -upgrade path is a separately qualified **reviewed import of the existing -source**, without requiring a compatible 1.x writer to create new captures. -Do not lower operation counts, fabricate capture history or bypass source and -receipt verification to hide this dependency. A direct importer is a remaining -acceptance item, not a capability established by canonical create or isolated -archive restore. Keep the compatible installer available for recovery until -the declared historical support matrix has been qualified. +Pre-cutoff promotion depended on an enabled shadow qualified by source +mutations. A cold Goal can no longer manufacture that capture history. The 2.0 +upgrade path uses a separately qualified **reviewed import of the existing +source**. It does not lower operation counts, fabricate capture history or ask +a retired 1.x writer to create new evidence. Canonical create and isolated +archive restore remain separate workflows. The import package must close one installed user journey through the existing typed coordination owner and Goal storage settings, shared by CLI and App: @@ -460,16 +450,13 @@ selected command dispatcher and the existing Goal path resolver; its File/SQLite import and original-receipt recovery run with `todos.py`, `bootstrap.py`, `runtime_shadow_writer_adapter.py` and `local_authority_shadow_outbox.py` physically absent in a disposable package. -This proves that command's independence, not that other commands or supported -writers can lose those files. The retained prose-write guard now belongs to the -existing source/fence boundary and shares the source partition projector with -capture. Its old import remains compatible. Configuration and the guard load -with both capture modules absent. Pure runtime-root routing now belongs to +The retired writer and producer modules are absent from normal and installed +qualification. Historical outbox inspection and disposition use independent +readers. Pure runtime-root routing belongs to `paths.effective_runtime_root`; canonical Todo, terminal lifecycle, acceptance, -Chat and CLI callers import that owner directly. The old adapter reexports the -same function for supported capture callers. Source and fresh-wheel File/SQLite -qualification runs the complete `serve_chat` -entrypoint with all four modules physically absent. The packaged Goal settings +Chat and CLI callers import that owner directly. Source and fresh-wheel +File/SQLite qualification runs the complete `serve_chat` entrypoint with the +retired modules physically absent. The packaged Goal settings journey performs backup/preview, reload and original-operation readback before explicit confirmation. Full current and archived requirements survive import; subsequent reviewed App creation, editing and completion work with the explicitly @@ -484,10 +471,8 @@ retains subsequent writes. Unreviewed v0/v1 callers keep their existing intent and receipt identity. Import and original-receipt recovery refresh current ownership independently on the same settings screen, including retryable read failures; a saved receipt does not substitute for today's policy. -Supported old APIs reexport the same functions; their real Markdown writers and bootstrap -operations remain for actual callers. This qualifies the bounded import/loading -journey, not retirement of every writer, full-state identity adoption, D2 or a -released SQLite default. +Read-only compatibility APIs retain their declared historical inputs. They do +not reexport a Markdown business writer or grant fresh execution authority. 已审核的 canonical Todo 创建使用请求 v2,将预览的 provider revision 绑定到既有 命令身份和事务 CAS;并发修改会拒绝过期请求。原操作恢复先于当前 head 校验,保留 @@ -504,36 +489,29 @@ used by the source witness, including registries outside conventional Goal folde App 备份使用与 source witness 一致的 registry 实际路径,也覆盖位于常规 Goal 目录之外的 registry。 -Deliver complete, reversible PR packages in this order: +The 2.0 cutoff was qualified in this order: -1. **Direct source import and support boundary.** Implement and qualify the - reviewed cold-source import on the installed CLI and packaged App, alongside +1. **Direct source import and support boundary.** Qualify the reviewed + cold-source import on the installed CLI and packaged App, alongside existing new-Goal creation/retry and provider migration. Cover both qualified and cold unpromoted sources, target-only settings, stopped writers, active/expired leases, pending captures, missing providers and original-plan - recovery. Publish the selected major-release support contract before removing - its writer. An additive 1.x release may ship the importer and deprecation - guidance first; using the old writer is not a prerequisite for import. - New SQLite creation is already implemented. -2. **Normal writer family cutoff.** In the major-release implementation, enforce - that boundary through the existing typed admission owner and remove the + recovery. The importer does not call the retired writer. +2. **Normal writer family cutoff.** Enforce the boundary through the existing + typed admission owner and remove the corresponding Markdown create/update/claim/complete/supersede/archive paths together with their private dispatch and writer-only tests. Trace CLI, internal, dynamic and supported import callers; keep shared rendering, validation effects and migration/recovery readers. Run the real wheel and packaged user journey with the retired paths physically absent, including - refusal and recovery for an unmigrated Goal. Do not land the breaking package - into an otherwise compatible 1.x release line. -3. **Capture producer and duplicate decision retirement.** After the writer - family exits, remove only producers and bridges without supported callers. + refusal and recovery for an unmigrated Goal. +3. **Capture producer and duplicate decision retirement.** Remove producers and + bridges without supported callers. Reconcile each prepared/committed outbox entry against source bytes and its original receipt first. Retain the codecs/readers needed by supported old backups and interrupted migrations. Prove restart/replay and forward recovery without recreating a business write or losing a later canonical write. -Publish 2.0 beta, then rc and final only as these user journeys qualify for the -declared support matrix. Do not wait for every external Goal to have migrated; -each affected user must have a tested route before its next normal write. Software installation, authority-format upgrade, provider migration and ownership-policy migration remain distinct operations. Binary updates do not automatically switch authority. Reverse File/SQLite cutover starts from the @@ -545,12 +523,11 @@ D2 capacity/soak qualification; missing or failed D2 evidence stays visible. A microbenchmark failure alone does not block an unrelated, proven internal deletion, and passing those deletions does not certify the release default. -### 切换发布默认前的退役边界 +### 2.0 退役边界 -canonical 写入绕过旧 writer,不代表未迁移 Goal 的调用方已消失。默认值修改也不 -迁移已有 Goal 或结算 capture 历史。按调用族分别验收,不把整项迁移设成所有小批 -退役的前置依赖;仍有调用的 writer/producer 在对应迁移后删,已无调用的内部桥接 -可先删,受支持的备份、格式、原回执恢复与 Markdown 展示继续保留。 +正常写入现在只接受 canonical authority;旧 writer、producer 和无调用内部桥接 +已删除。默认值修改仍不迁移已有 Goal 或自动处置历史 outbox。受支持的备份、格式、 +原回执恢复与 Markdown 展示继续保留。 上表中的精确数字与摘要校验、安全文件传输、prepare→主写入→commit 顺序、游标 恢复和 Host I/O 都有实际价值。替代实现须保留它们;Python 行数减少不能代替语义 @@ -560,31 +537,23 @@ canonical 写入绕过旧 writer,不代表未迁移 Goal 的调用方已消失 新写入。现有回归覆盖不等于长期 SQLite、真实 PostgreSQL、打包 App 或发布默认 已通过,trial 与正式默认的资格继续分别记录。 -**发布路径提案:**当前仍支持未晋升 Markdown Goal 的正常写入、v0 target-only -和显式关闭 canonical creation;删除这条支持路径属于不兼容变更,建议在 **2.0** -统一截止,不混入兼容 1.x 更新。要求是“正常写入前必须有 canonical authority”, -不是所有 Goal 强制 SQLite。已有 canonical File/SQLite 和明确的两种 ownership -policy 保持;已有 canonical store 若仍用 `legacy` policy,只需审核保留 claim 的 -policy 迁移,不重新晋升或强制换 provider。关闭/旧配置先明确审核升级,不删偏好 -或暗中启用。旧备份、格式、原回执和永久 Markdown 投影继续按其恢复契约支持。 - -一个必须解决的死结是:当前晋升需要真实源写入形成合格 capture;冷启动、未资格化 -的旧 Goal 若先被禁止写入,就无法再产生迁移资格。选定主路径是独立验收的 -**旧源审核导入**,无需先在兼容 1.x writer 上新增 capture;不能降低计数、伪造 -capture 或把空库创建/isolated restore 当作此验收。先完整盘点和备份源,停止 -writer/Host、结算 lease 并逐项对账 outbox,再预览、明确确认、fence 旧 writer、 -导入并读回。未支持或歧义内容明确拒绝,旧 lease/回执不转为新授权;导入新回执 -与历史原回执分别保存。中断、失响应、重启须恢复同一操作,不重复副作用、不丢 -后续 canonical 新写入。此旅程与既有 canonical File/SQLite 切换及新身份 archive -restore 分别验收;实现尚未完成。历史支持矩阵验收前保留兼容安装包用于恢复。 - -按上面的三个完整 PR 包推进:安装态 CLI/App 直接导入与支持边界 → 同边界 -create/update/claim/complete/supersede/archive 旧 writer 及私有 dispatch 删除 → -逐项对账 outbox 后删除无调用的 capture producer/重复决策。大版本实现须有原生 -拒绝与恢复、旧路径物理缺席的真实入口验证;不要求所有外部用户已迁移。2.0 -beta→rc→final 依据明确支持矩阵的旅程验收,不将原失败/缺失 D2 改成通过,也不把 -独立微基准当所有内部退役的前置。二进制更新不自动切 authority,反向切换须保留 -当前 head 的新增写入。版本和截止仍是提案,尚未改变安装行为或宣布发布。 +**2.0 支持矩阵:**未迁移 Markdown Goal 的正常写入返回 +`canonical_authority_required`;v0 target-only 和显式关闭 canonical creation +只用于检查与审核升级。要求是“正常写入前必须有 canonical authority”,不是所有 +Goal 强制 SQLite。已有 canonical File/SQLite 和两种 ownership policy 保持。 +旧备份、格式、原回执和永久 Markdown 投影继续按其恢复契约支持。 + +旧 Goal 通过独立验收的**旧源审核导入**迁移,无需调用退役 writer 新增 capture。 +流程先完整盘点和备份源,停止 Host、结算 lease 并逐项对账 outbox,再预览、明确 +确认、fence 旧 writer、导入并读回。未支持或歧义内容明确拒绝,旧 lease/回执不 +转为新授权;导入新回执与历史原回执分别保存。中断、失响应和重启恢复同一操作, +不重复副作用,也不丢后续 canonical 新写入。 + +实现按三个包完成:安装态 CLI/App 直接导入与支持边界;删除 +create/update/claim/complete/supersede/archive 旧 writer 及私有 dispatch;逐项 +对账 outbox 后删除无调用 capture producer 和重复决策。验收覆盖原生拒绝与恢复, +并在旧路径物理缺席时运行真实入口。二进制更新不自动切 authority,反向切换须 +保留当前 head 的新增写入。 ## Existing canonical Goal storage in the App diff --git a/docs/reference/monitor-configuration.md b/docs/reference/monitor-configuration.md index 748691b9f7..d28fa404e8 100644 --- a/docs/reference/monitor-configuration.md +++ b/docs/reference/monitor-configuration.md @@ -2,8 +2,9 @@ `todo update` changes an existing Monitor's configuration. After whole-Goal promotion it uses the same TS planning/CAS/receipt owner as other supported -Todo updates; it does not read Markdown as authority. Before promotion it -uses the legacy writer and the same typed configuration policy. +Todo updates; it does not read Markdown as authority. In 2.0 an unmigrated +Goal returns `canonical_authority_required`; back up and migrate it before +retrying the configuration write. ```sh loopx todo update --goal-id demo --todo-id todo_watch --agent-id agent-a \ @@ -84,8 +85,9 @@ route/handoff/closure behavior; existing v0/v1/v2 requests keep their input cont ## 中文 -Monitor 配置修改复用 `todo update`。晋升后由 TS 在同一个 canonical revision 上 -校验并提交状态、事件和回执;晋升前保留 legacy writer,复用相同 typed 配置规则。 +Monitor 配置修改复用 `todo update`。TS 在同一个 canonical revision 上校验并提交 +状态、事件和回执;2.0 中未迁移 Goal 返回 `canonical_authority_required`,需先备份 +并迁移后再重试。 上面的 CLI 给出了预览、带 operation ID 的执行和回读;operation ID 只适用于已 晋升 Goal,修正或撤销使用新 ID,重试同一请求使用原 ID。 diff --git a/docs/reference/protocols/active-state-structured-projection-v0.md b/docs/reference/protocols/active-state-structured-projection-v0.md index 836df035f6..c4ea0d66a6 100644 --- a/docs/reference/protocols/active-state-structured-projection-v0.md +++ b/docs/reference/protocols/active-state-structured-projection-v0.md @@ -133,10 +133,12 @@ This is the permanent Python presentation/legacy-input adapter described by the and [shared-authority RFC](../../architecture/rfcs/shared-goal-authority-state-provider-v0.md#next-delivery-and-parallel-provider-work). It adds no business rule, RPC, provider, or authority write. Post-cutover Todo consumers still read canonical state when Markdown is absent or malformed; -rendering never imports Objective examples into that state. Before cutover, -the existing legacy writer remains subject to its normal fence. Objective is -independent Goal narrative, outside the Todo store and Todo-section recovery. -Existing malformed documents are not automatically repaired. +rendering never imports Objective examples into that state. For an unmigrated +Goal, Markdown remains an inspection and reviewed-import source. Normal +mutation fails with `canonical_authority_required`; no legacy writer fallback +remains. Objective is independent Goal narrative, outside the Todo store and +Todo-section recovery. Existing malformed documents are not automatically +repaired. Markdown is not one undifferentiated database row. Agents generate and maintain both its structured sections and narrative through LoopX. The distinction is @@ -147,8 +149,10 @@ canonical source can reconstruct it. The cutover is deliberately section-sized, not document-sized: -- before promotion, Markdown remains the authority and writes use its existing - transaction boundary; +- before reviewed import, Markdown remains the source for inspection and + backup. Normal Todo mutation and new lease acquisition are rejected. The + exact retained lease cleanup route can settle an existing grant without + creating a new one; - after promotion, the versioned Todo records live in the canonical provider head and Markdown's Todo section is a compatibility/workbench projection; - other generated sections remain in Markdown until their canonical ownership @@ -156,9 +160,9 @@ The cutover is deliberately section-sized, not document-sized: - a provider outage after promotion fails closed and never makes stale Markdown authoritative again. -The first write using this boundary is Todo claim. It exercises a complete -provider-neutral TypeScript transaction while leaving the default local path -unchanged. On promoted `hard_lease` authority, the same claim command may +Todo claim was the first canonical write qualified through this boundary. It +exercises a complete provider-neutral TypeScript transaction. On canonical +`hard_lease` authority, the same claim command may supply a task-lease idempotency key and optional expected version so the claim, canonical lease, and durable receipt commit in one provider transaction; the write scopes come from the canonical Todo rather than caller input. After diff --git a/examples/control_plane/todo-deferred-capacity-cli-smoke.py b/examples/control_plane/todo-deferred-capacity-cli-smoke.py index e065bc02e5..5a54de1d64 100644 --- a/examples/control_plane/todo-deferred-capacity-cli-smoke.py +++ b/examples/control_plane/todo-deferred-capacity-cli-smoke.py @@ -29,20 +29,9 @@ ACTOR_AGENT_ID = "codex-side-bypass" -def remove_legacy_placeholder(state_file: Path) -> None: - state_file.write_text( - state_file.read_text(encoding="utf-8").replace( - "- [ ] Legacy monitor-only placeholder.\n", - "", - ), - encoding="utf-8", - ) - - def assert_deferred_add_and_capacity_resume() -> None: with tempfile.TemporaryDirectory(prefix="loopx-deferred-capacity-add-") as tmp: registry_path, state_file = write_fixture(Path(tmp)) - remove_legacy_placeholder(state_file) before = state_file.read_text(encoding="utf-8") unsupported = run_cli_error( diff --git a/examples/control_plane/todo_lifecycle_fixtures.py b/examples/control_plane/todo_lifecycle_fixtures.py index ddbfe4982b..869991fc9d 100644 --- a/examples/control_plane/todo_lifecycle_fixtures.py +++ b/examples/control_plane/todo_lifecycle_fixtures.py @@ -15,6 +15,9 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.cli import main as loopx_cli_main # noqa: E402 +from loopx.control_plane.coordination.goal_storage_initialization import ( # noqa: E402 + initialize_goal_storage_target, +) from loopx.control_plane.testing.canary_harness import ( # noqa: E402 runtime_root_from_registry, write_fixture_registry, @@ -52,8 +55,9 @@ def write_fixture(root: Path) -> tuple[Path, Path]: "# Active Goal State\n\n" "## Objective\n\n" "Exercise todo lifecycle transitions.\n\n" + "## User Todo / Owner Review Reading Queue\n\n" "## Agent Todo\n\n" - "- [ ] Legacy monitor-only placeholder.\n", + "## Completed Work Archive\n", encoding="utf-8", ) write_fixture_registry( @@ -65,7 +69,30 @@ def write_fixture(root: Path) -> tuple[Path, Path]: adapter_kind="generic_project_goal_v0", registered_agents=["codex-main-control", "codex-side-bypass"], quota_allowed_slots=None, + extra_goal_fields={ + "creation_operation_id": f"goal-create:{GOAL_ID}", + "coordination": { + "agent_model": "peer_v1", + "registered_agents": ["codex-main-control", "codex-side-bypass"], + "storage_target": { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "file", + "handoff_mode": "soft_claim", + }, + }, + }, + ) + registry = json.loads(registry_path.read_text(encoding="utf-8")) + initialized = initialize_goal_storage_target( + runtime, + registry["goals"][0], + registry_path=registry_path, ) + if not isinstance(initialized, dict) or ( + initialized.get("authority_initialized") is not True + or initialized.get("legacy_fallback_used") is not False + ): + raise AssertionError("fixture did not initialize canonical File authority") return registry_path, state_file diff --git a/examples/shared-goal-authority-e2e/README.md b/examples/shared-goal-authority-e2e/README.md index 7b16b0709a..c6465f7128 100644 --- a/examples/shared-goal-authority-e2e/README.md +++ b/examples/shared-goal-authority-e2e/README.md @@ -1,193 +1,121 @@ # Shared-goal-authority E2E stage ladder -One incremental end-to-end ladder for -[RFC: LoopX shared control-plane authority and pluggable state providers v0](../../docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md). -Every completed RFC stage claim is one row; every row drives the product -through the real `python -m loopx.cli` (`real_cli`) or runs a retained -store-level probe (`store_direct`). The ladder adds no product path, reads the -candidate only through the production TypeScript `FileAuthorityStore`, and -never reports green while a selected row is unverified. +This ladder qualifies the current shared-authority providers described by the +[shared control-plane authority RFC](../../docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md). +Each row uses either the public LoopX CLI or a provider's production TypeScript +store. A selected row must pass, report an explicit environment gate, or make +the command fail. ```bash -python examples/shared-goal-authority-e2e/ladder.py # exit 1 here: live rows unverified, the soak row pending -python examples/shared-goal-authority-e2e/ladder.py --allow-unverified --allow-pending -python examples/shared-goal-authority-e2e/ladder.py --stage 2c1 --report-json ladder-report.json -python examples/shared-goal-authority-e2e/ladder.py --stage 2c2 --allow-pending --report-json ladder-report.json +python examples/shared-goal-authority-e2e/ladder.py +python examples/shared-goal-authority-e2e/ladder.py --allow-unverified +python examples/shared-goal-authority-e2e/ladder.py --stage 1 \ + --report-json ladder-report.json python examples/shared-goal-authority-e2e/ladder.py --list ``` -The pytest projection is `tests/control_plane/test_shared_goal_authority_e2e.py`; -there, an unverified row skips as `unverified: ` and a POSIX-only row -skips on Windows. The `s2c1.retired_observation_upgrade` row is also covered by -`tests/control_plane/test_local_authority_shadow_cli_e2e.py` and is skipped in -the default pytest projection; `LOOPX_LADDER_FULL=1` runs it there too. The -standalone example always runs selected rows. The eleven `s2c2.*` rows run in -the stage2c correctness job alongside real-CLI process-death and recovery suites. +The pytest projection is +`tests/control_plane/test_shared_goal_authority_e2e.py`. Stage 0 runs the full +native provider suites, so the default pytest projection leaves those rows to +the TypeScript job. Set `LOOPX_LADDER_FULL=1` to run them through pytest too. +The standalone ladder always runs every selected row. -The seven former `s2c1.*` observation-writer rows are retired with that writer. -Historical reports retain their meaning; current runs validate explicit upgrade -and the transaction-bound outbox instead of requiring a second writable history. +## Current rows -## Rows - -| Row | Stage | Path | Gate | Asserts | +| Row | Stage | Path | Gate | Assertion | | --- | --- | --- | --- | --- | -| `s0.native_file_conformance` | 0 | store_direct | deterministic | Runs the complete `authority_store.test.ts` suite against the current TS FileAuthorityStore; every selected test must pass, with no skip or missing trailer | -| `s0.native_sqlite_conformance` | 0 | store_direct | deterministic | Runs the complete `sqlite_authority_store.test.ts` suite against real SQLite files and the same shared conformance contract | -| `s1.cli_document_decodes_through_ts_store` | 1 | real_cli | deterministic | Explicit bootstrap plus three CLI writes load at cursor `4`; paged `scanCommitted` returns four distinct transactions in source order and `readReceipt` finds the first source write | -| `s2a.nokv_live_qualification` | 2a | store_direct | env:nokv_authority | runs the merged `examples/nokv-authority-store/live-qualification.ts --execute-live` against an existing workbench with a fresh tenant/goal pair; requires `ok=true`, the single-node store-conformance scope, every check `passed`, NoKV SDK `0.11.1` / API `1`, the two stale-incarnation fence checks (`stale_incarnation_fence_rejected`, `stale_incarnation_fence_left_generation_unchanged`), and no promotion or availability claim; evidence carries check ids, counts, and config and workbench digest prefixes, never a configuration value or the workbench name | -| `s2b.postgresql_conformance_live` | 2b | store_direct | env:postgresql | `postgresql_authority_store.integration.test.ts` under node's TAP reporter: `# pass >= 9`, `# fail 0`, `# skipped 0` | -| `s2c1.retired_observation_upgrade` | 2c1 | real_cli | deterministic | Old enable rejects without writes; retained settings create no history; explicit clear/configure/bootstrap captures the next source transaction | -| `s2c2.outbox_prepared_then_committed_entries` | 2c2 | real_cli | deterministic | with the maintenance lock held, `todo add` (Python) and `task-lease acquire` (TypeScript) report `drain_deferred/drain_lock_busy`, `status` shows one `committed_pending` entry per partition with one prepared record and one committed marker on disk; one `drain` delivers both (`delivered=2`), history holds the bootstrap plus two committed receipts from both writer runtimes, and the next write delivers inline at cursor `4` | -| `s2c2.drain_idempotent` | 2c2 | real_cli | deterministic | three deferred entries: `drain --max-entries 1` delivers one (`pending_after=2`, `budget_exhausted`), the next `drain` delivers two, an idle `drain` reports `nothing_pending` with unchanged cursor, `head_digest` and `provider_revision`; receipts settle sequences 1..3; an idempotent same-key re-acquire carries no capture evidence and adds no transaction | -| `s2c2.sigkill_between_primary_write_and_drain` | 2c2 | real_cli | deterministic (POSIX) | `todo add` SIGKILLed at `before_replace`, `after_replace` and `before_marker` leaves one prepared-only entry each; `drain` settles it as `abandoned` (no-op, primary unchanged) or `committed_proven_by_readback`, the projection equals the primary, and `inspect` ends `matched` | -| `s2c2.sigkill_mid_drain` | 2c2 | real_cli | deterministic (POSIX) | `todo add` SIGKILLed at `before_commit`, `after_commit`, `after_cursor` and `between_unlinks`: the next `drain` delivers the uncommitted entry once or replays the committed one (`replayed=1, delivered=0`), history holds exactly one delivery, only the cursor remains, and a further drain is idle | -| `s2c2.rollback_with_pending_entries` | 2c2 | real_cli | deterministic (POSIX) | with one committed-pending and one prepared-only entry, `inspect` reports `outbox_pending` at the exact revision, a rollback preview writes nothing, `rollback --execute` applies and archives the outbox with both entries, the marker, the cursor and the manifest; capture then reports `bootstrap_required` while primary writes continue, a rebootstrap starts a new lineage from the current primary (three todos), and the historical rollback replays against it | -| `s2c2.parity_equal` | 2c2 | real_cli | deterministic | three cycles interleave Python Markdown writers (add, note update plus a no-change repeat, explicit exclusion set and clear plus a no-change repeat, complete, supersede, and a second add) with TypeScript lease writers (acquire, renew, transfer, and the fence close of a leased complete or supersede); after each cycle `inspect` is `matched`, `qualify` with every required write class is `qualified` with `operation_count` equal to the delivered mutations, `read-candidate` returns the anchor todo, and `sustained_parity_verdict` stays `not_evaluated` | -| `s2c2.parity_divergent_detects_foreign_edit` | 2c2 | real_cli | deterministic | a direct edit of the primary makes `inspect` report `drifted/shadow_projection_drift`, `qualify` and `read-candidate` reject, a later `todo add` commits but its capture holds on `source_partition_continuity_unproved`; restoring the bytes does not requalify (`outbox_pending`), `drain` stays `stopped`, and only `rollback --execute` plus a fresh bootstrap qualifies again | -| `s2c2.event_only_todo_source_holds` | 2c2 | real_cli | deterministic | a nonempty legacy Todo event source makes `inspect`, `qualify`, `read-candidate` and Todo writes refuse with `legacy_todo_event_source_retired`; status identifies the affected Goal while preserving healthy Goals and marks its Todo source unavailable; the log remains untouched; explicit owner export/removal restores the supported source | -| `s2c2.migration_seeds_and_drains` | 2c2 | real_cli | deterministic | `migrate-state` previews an actively captured goal without writing, refuses `--execute` with `shadow_source_replacement_requires_rebootstrap` (also when capture is merely disabled), and executes only after `rollback`; the migrated goal carries its disabled capture configuration, plans no observation seed, requires its own `bootstrap`, then captures a write to cursor `2` and qualifies on it while the legacy archive is retained | -| `s2c2.growth_measurement_gate` | 2c2 | real_cli | deterministic | ten fixed-size `todo add` writes: the cursor advances by one each time, `store_bytes` grows monotonically, the per-transaction delta accelerates by at most 2048 bytes (one live record), every retained transaction carries its complete projection, `retention_pressure` stays false; the report carries final and cumulative publication bytes and claims no capacity horizon (`capacity_verdict=not_evaluated`) | -| `s2c2.archive_after_leased_completion_parity` | 2c2 | real_cli | deterministic | a leased Todo is completed through its fence and then archived by `todo archive-completed`: the archive retires the Todo from the current graph while its released lease file stays on disk as audit history, and `inspect` still reports `matched` with `parity_matches=true`, no drift reason, a qualified bounded read of a co-resident open Todo, and a `qualify` that requires the archive event kind; the released lease file remains on disk | - -Pending rows are declared in the report as `pending`, never counted as pass, -and they block a green exit unless `--allow-pending` is passed. One -declaration remains: `s2c2.sustained_parity_soak`, the >=10-day synthetic-goal -soak of the selected local profile owned by RFC Section 7.2 (lane L). Bounded -qualification reports `sustained_parity_verdict=not_evaluated`, and no -`s2c2.*` row promotes a provider or completes the Stage 2C promotion. - -The former `s2c2.archive_after_leased_completion_parity` declaration is now an -executable row. The gap it recorded is closed at the fold: a Todo partition -carries the published Todo read records, and the candidate head now keeps a -lease edge only for Todos still in the current graph -(`archive_state === "active"`), matching the rule the source projection and the -TypeScript source verification already apply. - -The `s2c2.*` rows use two scheduling-only seams outside every product decision: -holding the stable maintenance lock, which makes a writer report -`drain_deferred/drain_lock_busy` and leave its committed entry pending, and a -POSIX crash worker that pauses one real CLI process at a named persistence -window so the row can SIGKILL it there. Neither substitutes a result or edits a -byte; every assertion still goes through `status`, `drain`, `inspect`, -`qualify`, `read-candidate`, `rollback`, `migrate-state` and the retained -TypeScript store read. - -## Native qualification and prototype retirement - -Stage 0 now exercises the providers used by current Goals. Run from a source -checkout after `npm ci` and `uv sync --extra test`: +| `s0.native_file_conformance` | 0 | `store_direct` | deterministic | The current `FileAuthorityStore` passes its complete conformance suite with no skip or incomplete TAP trailer. | +| `s0.native_sqlite_conformance` | 0 | `store_direct` | deterministic | The current `SqliteAuthorityStore` passes the same complete conformance contract against real SQLite files. | +| `s1.cli_document_decodes_through_ts_store` | 1 | `real_cli` | deterministic | Public CLI bootstrap creates canonical File authority. Todo add, lease acquire, and Todo update commit at cursors 2 through 4. An independent TypeScript process reads the four ordered transactions and the first business receipt from `runtime/authority/file-v0`. | +| `s2a.nokv_live_qualification` | 2a | `store_direct` | `env:nokv_authority` | The NoKV candidate passes the live single-node qualification with SDK `0.11.1`, API `1`, and both stale-incarnation fence checks. | +| `s2b.postgresql_conformance_live` | 2b | `store_direct` | `env:postgresql` | The PostgreSQL candidate passes the provider integration suite with at least nine TAP passes and no failures or skips. | + +Stage 1 checks the complete public path. It applies File creation defaults, +bootstraps a new Goal, configures its registered agents, performs three +business mutations, and verifies all responses report: + +- `source_authority=file_v0`; +- `decision_read_from_provider=true`; +- `legacy_fallback_used=false`; +- no retired `coordination_runtime_shadow` write evidence. + +The readback process opens the same canonical provider independently. A Python +response cannot make this row pass without the provider journal and receipt. + +## Legacy capture cutoff + +The 2.0 cutoff removed the Stage 2C rows that created new Markdown shadow +transactions. They were tests of a producer that no longer owns business +writes. Keeping those rows would require LoopX to restore the retired producer +only to satisfy its qualification suite. + +Current Todo, handoff, Monitor, team-plan, bootstrap, project-registration, and +lease operations require canonical authority. An unpromoted Goal returns +`canonical_authority_required` with `migrate_goal_authority` and +`backup_required=true`. It does not create a Markdown mutation or a shadow +outbox entry. + +Existing shadow stores, outboxes, rollback archives, and receipts remain +readable through their recovery commands. Those commands classify and settle +pre-cutoff bytes. They do not enable a new capture lineage or qualify a new +business authority. See [historical outbox recovery](correctness.md). + +## Native qualification + +Run Stage 0 from a prepared source checkout: ```bash -uv run --extra test python examples/shared-goal-authority-e2e/ladder.py --stage 0 +uv run --extra test python \ + examples/shared-goal-authority-e2e/ladder.py --stage 0 ``` -These are complete suites, so allow several minutes. The regular TypeScript test -job already runs them; the default pytest projection skips these two duplicate -runs. Set `LOOPX_LADDER_FULL=1` for pytest to execute them too. The standalone -ladder always executes selected rows. The test runner's successful -exit is necessary but insufficient: all tests must pass, the selection must be -nonempty, and failures, skips, cancellations or an incomplete TAP trailer fail -qualification. Missing Node or source suites is explicitly `unverified`. -Installing a wheel alone does not install the source qualification suite. - -The Python `CoordinationAuthorityExecutor`, head codec, `FileCoordinationProvider` -and bootstrap bridge had no production CLI callers. Their remaining callers were -the prototype examples, unit tests and the old ladder. They are removed together; -current TS executors, stores, logical archives and File v0-to-v1 migration remain. -`coordination-head-*.json` was the experimental head format, not a production -`authority-store-*.json` journal. This retirement neither rewrites those prototype -artifacts nor converts them into a full Goal. Historical probe records remain -historical; `s0.file_matrix_twelve_rows` and `s0.nokv_live_matrix` are no longer -selectable and are never aliases for current proof. - -Coverage follows the current semantic owner rather than reproducing prototype -schemas or its superseded rules: - -| Retired prototype obligation | Retained native qualification | -| --- | --- | -| Same-Todo CAS winner, independent writes, stale revision and operation identity | `authority_store_conformance.ts`: CAS, replay/fencing, atomic Todo claim and lease acquisition | -| Lost response, receipt recovery and bounded retries | `coordination_receipt_conformance.ts` and `claim_acquisition_proof_conformance.ts` | -| Renew, release, transfer, expiry and stale ownership | `lease_lifecycle_conformance.ts`, `lease_acquisition_conformance.ts`, `claim_transfer_conformance.ts` and claim acquisition proof | -| Completion plus successor, durable continuation and unchanged unrelated rows | Native terminal lifecycle and production-scale scenarios in `authority_store_conformance.ts` | -| Restore lineage, corrupt records, publication and reopen | File/SQLite provider suites, authority source and promotion recovery conformance | -| Large state and observation isolation | Existing production-scale coordination fixtures, complete snapshots and paginated authority scan conformance | - -Two intentional differences already belong to the current runtime: an expired -lease may still be released by its matching holder for cleanup, and an original -claim receipt does not authorize execution after the current lease has retired. -Retiring the prototype must not reinstate its expired-release rejection or treat -historical receipt replay as fresh execution authority. - -NoKV keeps its actual TS store and Stage 2A live qualification; removing the old -head adapter does not remove that provider. Open #4726 touches both implementations: -its native SDK routing/schema work remains relevant, while its prototype-only -changes must be dropped when rebasing across this retirement. Local conformance does not certify -SQLite D2 capacity/soak, production NoKV availability, PostgreSQL deployment or -default cutover. - -## Gates and environment variables - -| Gate | Requirement | Unverified reason when absent | +A successful process exit is necessary but insufficient. The ladder parses the +TAP trailer and requires a nonempty selection where every test passed. A +missing Node runtime or source suite reports `unverified`; it never becomes a +pass. + +The retired Python `CoordinationAuthorityExecutor`, head codec, and bootstrap +bridge had no production CLI callers. Their former prototype rows are not +aliases for current File or NoKV evidence. The current TypeScript stores, +logical archives, migrations, and provider conformance remain independently +qualified. + +## Environment gates + +| Gate | Required input | Unverified result | | --- | --- | --- | -| `deterministic` | none (needs `node` on `PATH` for the CLI's TypeScript runtime and the read-back probe) | `node_missing` when the probe cannot run | -| `env:postgresql` | `LOOPX_TEST_POSTGRES_URL` plus `node_modules/pg` (`npm ci`) | `postgres_url_missing`, `pg_dependency_missing`, `node_missing` | -| `env:nokv_authority` | `LOOPX_NOKV_AUTHORITY_LIVE=1` (the probe writes durable test data), `LOOPX_NOKV_AUTHORITY_CONFIG_JSON` (absolute path to the ignored NoKV client configuration), `LOOPX_NOKV_AUTHORITY_PYTHON` (absolute path to the Python executable that resolves NoKV SDK 0.11.1), `LOOPX_NOKV_AUTHORITY_WORKBENCH` (an existing workbench); `node` on `PATH` | `nokv_authority_env_missing`, `loopx_nokv_authority_live_not_enabled`, `nokv_authority_config_missing`, `nokv_authority_python_missing`, `node_missing` | - -POSIX-only rows report `unverified/posix_only` on Windows. - -## Report and exit policy - -The report schema is `loopx_shared_goal_authority_e2e_report_v0`: -`rows[]` (`status in {pass, fail, unverified}`, `reason_code`, public-safe -`evidence`, `duration_ms`), `pending[]`, `summary{pass, fail, unverified, -pending, executed, privacy_violations}`, `bindings{loopx_commit, loopx_tree_dirty, probe_sha256[], -nokv_client_config_sha256, nokv_sdk_version, postgres_url_sha256_prefix, -pg_package_version}` (`null` when unknown), and `exit_policy`. - -Exit code is `0` iff `fail == 0` and `privacy_violations == 0` and -(`unverified == 0` or `--allow-unverified`) and (`pending == 0` or -`--allow-pending`): a selected -row that never executed, whether gated or declared pending, is an unmet -obligation, so `--row s2c2.sustained_parity_soak` exits 1 with zero executions, and a -mixed selection exits 1 even when its executable rows pass. `--list` only -prints the registry and never claims verification. A privacy scan runs over -the finished report: any occurrence of a temporary root, the home directory, -the repository path, the PostgreSQL URL, a NoKV configuration value, or a NoKV -authority input path rewrites that row to `fail/privacy_violation`; a leak -confined to the `bindings` block nulls every binding, marks -`bindings.privacy_violation`, and still exits `1` through -`summary.privacy_violations`, which no flag relaxes. Evidence therefore -carries counters, cursors, outcome tokens, and sha256 prefixes only. - -## Bounded outbox correctness and the pending ladder - -The independently runnable [correctness suite](correctness.md) covers the -`file_outbox_v1` capture lineage, strict cursor recovery, mixed writers, -source fences, and recoverable bootstrap/rollback. It uses real CLI and native -processes, the production `FileAuthorityStore`, and process death at persistence -boundaries. [Installed-package E2E](installed.py) repeats the public lifecycle -outside the checkout for both wheel and sdist. [Negative controls](mutants.py) -deliberately remove correctness checks in disposable source copies. - -The ten `s2c2.*` ladder rows above exercise the same lifecycle through the -public interfaces and read history only through the retained TypeScript store. -Sustained (elapsed-time) parity and promotion remain separate obligations: the -soak and the archive-after-lease capture gap stay pending declarations, and a -bounded qualification result reports `sustained_parity_verdict=not_evaluated`. - -Future ladder rows must use the actual product interfaces: - -- `authority-shadow drain` for receipt-verified replay and cursor recovery; -- `coordination-shadow inspect / qualify / read-candidate` for comparison, - bounded historical qualification, and a qualified read from that same head; -- `coordination-shadow rollback` with an exact revision or unfinished bootstrap - operation selector, followed by explicit rebootstrap; -- the stable management lock outside the goal outbox for scheduling a management - boundary. Rollback moves the entire goal outbox, so a lock inside that directory - cannot coordinate it. The tests retain scheduling-only barriers around real - persistence calls rather than depending on the retired drain lock. - -There is no `verify` command or generic `reset`. The independent v0 observation -store and its runtime-root override behavior retain their original scope; its -historical evidence is not an outbox qualification receipt. +| `deterministic` | Node.js on `PATH` for native execution and readback | `node_missing` | +| `env:postgresql` | `LOOPX_TEST_POSTGRES_URL` and the installed `pg` package | `postgres_url_missing`, `pg_dependency_missing`, or `node_missing` | +| `env:nokv_authority` | `LOOPX_NOKV_AUTHORITY_LIVE=1`, absolute config and Python paths, an existing workbench, and Node.js | A specific missing-input or disabled-live reason | + +The NoKV probe creates a fresh tenant and Goal in the selected workbench. +Reports contain digests and counters, never configuration values, workbench +names, repository paths, temporary roots, or provider credentials. + +## Report policy + +The report schema is `loopx_shared_goal_authority_e2e_report_v0`. It contains +executed rows, explicit pending declarations, aggregate counts, environment +bindings, and the exit policy. + +Exit code `0` requires: + +- no failed row; +- no privacy violation; +- no unverified row unless `--allow-unverified` is set; +- no pending declaration unless `--allow-pending` is set. + +There are no pending rows in the current registry. The generic pending +mechanism remains tested so a future obligation cannot be listed as completed +work. `--list` prints the registry and makes no verification claim. + +## Distribution qualification + +[installed.py](installed.py) builds an isolated environment for a wheel or +source distribution, runs outside the checkout, verifies installed resource +provenance, and exercises canonical File creation, mutations, exact replay, +history, receipts, and legacy-bootstrap rejection. + +[mutants.py](mutants.py) runs negative controls in disposable source copies. +The controls prove that removing a required assertion or authority boundary +causes the qualification to fail. diff --git a/examples/shared-goal-authority-e2e/correctness.md b/examples/shared-goal-authority-e2e/correctness.md index dde1310522..91c3bd87b8 100644 --- a/examples/shared-goal-authority-e2e/correctness.md +++ b/examples/shared-goal-authority-e2e/correctness.md @@ -1,346 +1,167 @@ -# Bounded file outbox correctness +# Historical outbox recovery correctness -An active shadow captures each mutation of its bound primary once. A cursor is a -position hint: only the actual file provider's committed transaction and receipt -can authorize deletion of an outbox file. Qualification folds the complete -baseline and transaction history, then compares the current Todo, handoff mode, -and lease state. Equal final snapshots alone are insufficient. +LoopX 2.0 retains the reader and recovery contract for shadow artifacts created +before the Legacy Markdown authority cutoff. It does not retain a producer for +new shadow transactions. -This is a pre-promotion `file_outbox_v1` contract. Native canonical Todo keeps its -existing contract. Older file-v0 mirror or mixed histories remain unchanged, -read-only, and ineligible for this qualification; there is no automatic migration. -The separate observation handler remains available within its original scope. +Normal Todo, handoff, Monitor, team-plan, project, and lease operations commit +through canonical File, SQLite, or PostgreSQL authority. An unpromoted Goal is +held with `canonical_authority_required`; it does not change Markdown, create a +lease grant, or append to an old outbox. -## Operator lifecycle +## Retained responsibilities -Enable the existing per-goal `coordination.runtime_shadow` configuration with -`enabled: true` and `provider: file_v0`. Select the intended common runtime root -and state file in the registry before bootstrap. The new history binds that -source; conflicting `--runtime-root`, `--project`, or `--state-file` overrides -cannot establish a second authority for it. Default-off primary writes and the -independent observation path retain their own override behavior. -Registry-relative runtime paths also bind rollout-event logging to the registry's -owning project, including when capture is disabled and the CLI runs elsewhere. -This corrects a split-log defect; it does not activate capture for that Goal. +Recovery code may: -Use the same registry and Goal throughout: +- inventory an existing shadow store, outbox, management journal, and rollback + archive without changing them; +- decode prepared and committed records written by the retired producer; +- compare a retained candidate with its bound source; +- drain or disposition an existing entry from its exact source and receipt + evidence; +- replay an exact historical management result; +- archive a candidate and its complete outbox under the existing rollback + contract; +- export source bytes for backup and reviewed cold import; +- inspect and release an exact retained lease or close an exact historical + fence. + +Recovery code may not: + +- prepare a new outbox entry for a normal business operation; +- enable a new continuous Markdown capture lineage; +- treat an old candidate as current authority without reviewed migration; +- create, renew, transfer, or broaden an execution lease; +- clear an ambiguous artifact to make inspection pass; +- fall back to Markdown when canonical authority is unavailable. + +## Operator commands + +Use source inspection before changing any historical material: ```bash -loopx --registry registry.json --format json coordination-shadow bootstrap --goal-id goal-a -loopx --registry registry.json --format json coordination-shadow bootstrap --goal-id goal-a --execute -loopx --registry registry.json --format json authority-shadow status --goal-id goal-a -loopx --registry registry.json --format json authority-shadow drain --goal-id goal-a -loopx --registry registry.json --format json coordination-shadow inspect --goal-id goal-a -loopx --registry registry.json --format json coordination-shadow qualify --goal-id goal-a -loopx --registry registry.json --format json coordination-shadow read-candidate --goal-id goal-a --todo-id TODO_ID +loopx --registry registry.json --format json \ + coordination-shadow inspect-source --goal-id GOAL + +loopx --registry registry.json --format json \ + authority-shadow status --goal-id GOAL ``` -Bootstrap imports the complete current Todo/handoff and lease baseline. It does -not count as a mutation. Qualification requires three actual primary mutations -by default; replay and proven abandonment also do not count. `read-candidate` -uses that default policy and repeats the same validator, returning from its -verified head rather than trusting a previous successful qualification. - -Turning off the configuration does not cancel an already active capture -obligation. Todo/handoff/followup writers and native lease writers must still -prepare before changing their bound primary. A failed durable prepare holds -that mutation. A failure after primary replacement retains recovery evidence; -it cannot truthfully report that the primary was unchanged. No-change, preview, -idempotent retry, and prose-only changes produce no mutation receipt. Event-only -Todo sources retain `event_log_writer_not_bound` and prevent qualification. - -To retire the candidate, obtain the exact current `provider_revision` from -inspection and preview the target before execution. If an invalid cursor or -outbox manifest blocks inspection, use `authority-shadow status` and its -read-only provider proof. If the provider itself cannot be proved, preserve the -scene and hold; do not guess a revision. +When status proves that a pre-cutoff outbox has entries, run bounded recovery: ```bash -loopx --registry registry.json --format json coordination-shadow rollback --goal-id goal-a --provider-revision EXACT_REVISION -loopx --registry registry.json --format json coordination-shadow rollback --goal-id goal-a --provider-revision EXACT_REVISION --execute +loopx --registry registry.json --format json \ + authority-shadow drain --goal-id GOAL --max-entries 20 --budget-seconds 30 ``` -To terminate a bootstrap interrupted before active publication, use its exact -operation identity from the management result/journal: +Drain success depends on durable provider and receipt evidence. A cursor is +only a position hint. It cannot authorize deletion by itself. Invalid records, +missing source proof, identity disagreement, or a lost outcome remain explicit +holds. + +For an existing candidate, the retained readers remain available: ```bash -loopx --registry registry.json --format json coordination-shadow rollback --goal-id goal-a --bootstrap-operation-id EXACT_BOOTSTRAP_OPERATION --execute -``` +loopx --registry registry.json --format json \ + coordination-shadow inspect --goal-id GOAL -The selectors are mutually exclusive. Neither is an unconditional cleanup. -Rollback preserves the candidate and the complete Goal outbox, including pending -entries, markers, and malformed cursor bytes. It publishes `inactive` only after -the archive matches the immutable manifest. The shared store identity and other -Goals are unchanged. Primary writes then resume; capture reports -`bootstrap_required`. A new bootstrap imports that current state and starts a -new capture lineage, even if its data equals the old baseline. - -## Recovery and holds - -Management uses `bootstrapping / active / rolling_back / inactive`. The stable -management directory is under `authority-transition/file-v0/`, outside the -outbox being archived. Its immutable manifest and intent bind the operation, -request, source, provider revision, and file hashes. Retrying the same operation -reconciles actual source/archive existence and hashes; a phase string alone -cannot prove completion. Conflicts hold without overwriting or deleting either -copy. A delayed exact old request only returns its historical result. Reusing -its operation ID with different request contents returns an identity mismatch. -Cached results must match their immutable operation manifest, lineage and archive -references. A result copied from another Goal cannot establish successful replay. - -During an unfinished management operation, canonical and whole-state prose -writes return a maintenance hold before their first side effect. Management lock -order is M → T → S → L → K. Native lease writers check under their existing K; -ordinary writers release primary locks before drain. Legacy fence engagement -also takes the actual source S and therefore requires its absolute state path -in the internal RPC request. -For source owners declared in frontmatter or registered to that state path, -selecting another Goal cannot bypass the existing binding or fence. Unbound -legacy shared-state writes retain their existing behavior; prose-only writes -retain their separate maintenance boundary. - -### Fenced legacy write response - -A legacy Todo, handoff-mode, followup, registry-state, or native task-lease -write against a goal whose durable legacy writer fence is present is rejected -before its first primary side effect: no lease record, Markdown state, -canonical store document, outbox entry, or lifecycle-fence receipt is written. -The shared write check (`coordination.local_authority.legacy_write_check`) owns -only the stable machine reason and the fence binding facts: `reason_code` -`legacy_coordination_writer_fenced` with `authority_mode` and `fence_id` for an -engaged fence; `legacy_writer_fence_read_failed` with the technical `reason` -for a present but unreadable or invalid fence; every non-allowed result fails -closed. Each caller adapter (the TypeScript -`requireLegacyCoordinationPrimaryWriteAllowed` wrapper used by native acquire, -renew, transfer, release, every terminal or holder verify branch, and a -committed releasing fence-close; the Python -`require_legacy_coordination_write_allowed` wrapper used by every legacy Todo, -handoff, followup, registry, bootstrap, and monitor writer) maps the reason to -`error_code`, carries the complete check result unchanged under `write_check` -(never spread into its own envelope, so the envelope keeps its own -`schema_version`), and renders one provider-neutral remediation for an engaged -fence: +loopx --registry registry.json --format json \ + coordination-shadow qualify --goal-id GOAL +loopx --registry registry.json --format json \ + coordination-shadow read-candidate --goal-id GOAL --todo-id TODO_ID ``` -legacy coordination writer is fenced; use the promoted canonical authority -() for goal ; fence ; the primary record -was not changed -``` -A fence that cannot be validated reports the check's technical reason -verbatim. The profile label is opaque data from the check (`file_v0` today), -never a provider path, table, or command, so a provider-neutral binding needs -no template change. - -A native task-lease envelope keeps `schema_version: task_lease_v0` and its -`action`; its settlement is `{effect_id, receipts: [], failure: {step: -"validation", kind: "permission_denied", code: }}` at every -native guard site alike: no receipt exists because no settlement step ran, and -`permission_denied` marks the rejection terminal for this writer (RFC section -5 `rejected` and its no-fabricated-receipt rule, section 5.6, question 11, -Appendix C). `effect_id` follows each verb's existing rule: acquire reports -`null` whenever no receipt exists; a lifecycle operation reports the -settlement identity when the request carries `owner` and `idempotency_key` -and `null` otherwise. - -Previews: `archive-completed` without `--execute` and `todo add --dry-run` -write nothing. After promotion, public terminal/archive commands -route to canonical authority instead of treating the promotion fence as an -error. A hard-lease terminal preview therefore rejects with -`handoff_mode_requires_lease` when its target has no lease, succeeds for a -matching lease, and may preview the declared user-gate auto-acquire path; all -three leave the primary record and receipts unchanged. Legacy-only writers -such as `todo add` and `todo update` remain fenced. A fenced -committed releasing `fence_close` releases the caller's claimed mutation lock -in `finally` while the lease stays `active` and its `held` receipt is -untouched; the caller's fence token is spent, a retry reports -`fence_token_invalid`, and recovery is lease expiry or a canonical release. - -A promoted `archive-completed --execute` that selects no records is also a -provider no-op: it returns `no_change` without advancing the canonical cursor, -revision, or receipt history. Archive operation identity is bound to the -canonical source revision, so concurrent attempts over one snapshot share the -same logical operation. Projection recovery may create a missing machine-owned -User, Agent, or non-empty archive region next to an existing Todo region; it -still proves byte-for-byte preservation of all narrative outside those regions -and remains idempotent on replay. Empty successor intent is an identity at the -typed derivation boundary: it does not impose successor-only Agent registry or -completion-policy admission on an otherwise unchanged legacy terminal call. - -The complete observable behaviour is pinned row by row in -`tests/fixtures/control_plane/legacy_writer_fence_caller_parity_v0.json` -(21 TypeScript entry rows, 21 real-process CLI rows; whole-object legacy -envelopes, stable-field subsets for provider-first rows, exact exit status, -exclusion-free effect snapshots, and declared after-state) and -enforced by `tests/control_plane_ts/legacy_writer_fence_caller_parity.test.ts` -and `tests/control_plane/test_shadow_fence_caller_parity_e2e.py`; the -`baseline` entries of that fixture document earlier revisions and are never -executed. - -Promoted CLI acquire, renew, transfer and release rows assert provider commits and -independent lease readback; fresh acquisition C is separate from keyless A and -existing-lease B so one newly working caller does not invalidate another row's intended negative precondition. Acquisition replay verifies current proof. -The direct legacy-wire rows remain fenced. The CLI -scenario carries the validated current owner/key/version after renewal or -transfer, rejects the superseded completion proof, and previews with the current -proof before release. Release follows the active-lease/quiescence checks and -must leave completion unauthorized. Capture-enabled rows still create no second -legacy record or shadow outbox entry. When migrating an entry point, update this -caller matrix and its downstream proof flow together; focused transaction tests -or generic premerge canaries do not replace it. Run the owning matrix directly: +These commands report historical evidence. They do not grant current write +authority. New migration uses backup-bound source inspection and reviewed cold +import into an empty canonical provider. + +Rollback requires an exact retained selector: ```bash -uv run --extra test python -m pytest -q tests/control_plane/test_shadow_fence_caller_parity_e2e.py +loopx --registry registry.json --format json \ + coordination-shadow rollback --goal-id GOAL \ + --provider-revision EXACT_REVISION + +loopx --registry registry.json --format json \ + coordination-shadow rollback --goal-id GOAL \ + --provider-revision EXACT_REVISION --execute ``` -Baseline delta (0fb497af8 is the PR's diff baseline; ee1b17217 the previously -reviewed head): - -| Row | 0fb497af8 | ee1b17217 | Now | Basis | -| --- | --- | --- | --- | --- | -| native acquire settlement | committed validation receipt, `durable_writeback` / `writeback_rejected` | `validation` / `writeback_rejected`, no receipt | `validation` / `permission_denied`, no receipt | the guard ran before `commitAcquire` on every revision; only the classifier drifted; RFC section 5 forbids a fabricated receipt for `rejected` | -| native renew, transfer, release settlement | `validation` / `permission_denied` | same | same | unchanged; acquire now matches its siblings | -| native error text | `legacy task-lease writer is fenced; use the canonical file authority` | `legacy coordination writer is fenced` | one rendered template | remediation belongs to the caller adapter and must not name a provider | -| Python error text | `legacy coordination writer is fenced; use the canonical file authority` | same | one rendered template | same | -| envelope `schema_version` | check schema overwrote `task_lease_v0`; every CLI lease rejection printed `RuntimeError` | same | `task_lease_v0` plus nested `write_check` | envelope-owned keys win; check result travels under its contract name | -| Python Todo rejections | no `error_code` | flat check keys, `schema_version` injected | `error_code` plus `write_check` | same | -| terminal or holder verify under a fence | fence bypassed on the held and holder branches; auto-acquire branch not consulted | auto-acquire branch guarded; held branch previews reported `ok: true` and wrote receipts | every verify branch guarded before its first receipt | a preview must not succeed for a write the fence forbids | -| committed releasing fence-close | fence bypassed | guarded | guarded; lock released in `finally`, retry `fence_token_invalid` | declared and pinned | -| public terminal/archive after promotion | legacy fence rejection | legacy fence rejection | canonical provider result; lease admission, preview, CAS, receipt and projection semantics come from the promoted authority | the fence blocks legacy writes; it is not a rejection of the canonical replacement path | -| generic CLI branch of `task-lease` and `turn` | typed payload spread after envelope keys | same | payload first, envelope keys win | `LocalCoordinationAuthorityUnavailable` carries a `schema_version` | - -Known gap, unchanged on both revisions: `loopx quota monitor-poll --execute` -(and the scheduler monitor writeback it wraps) re-types every non-validation -exception to `quota_unexpected_collection_error` with reason `quota collection -failed`, so a fenced monitor writeback loses its `error_code` and remediation -at that boundary. The fix belongs to the quota domain's public-safety -contract (map the typed fence exception to its own code and pass `write_check` -through) and is not part of this batch. - -Canonical FileAuthorityStore Todo updates, including compatibility v0 records -already held by that authority, use the same M and maintenance boundary as -canonical Todo creation. They retain their own transaction receipts and do not -produce legacy shadow captures. A waiting update rechecks management state after -acquiring M; invalid or unfinished management state holds before any commit. - -Drain checks the real receipt first, persists the cursor second, and reclaims -each individually verified file last. Missing cursors can be reconstructed from -complete continuous history; malformed or unreadable cursors are never silently -replaced. Unknown files, mismatched bytes, wrong identities, or incomplete proof -preserve the scene. An interrupted primary without a marker is either proven -under the source lock or left `unproved`; an A → B → A history cannot establish -that the first write was abandoned. TS repeats source proof under its own locks -after Python releases its locks to call the native commit operation. -Cursor position includes settled no-ops; its digest is the verified head's last -applied partition marker. It stays null through bootstrap and an abandoned-only -prefix, even with a nonempty baseline. Recovery and qualification consume that -same marker rather than hashing the current snapshot independently. -If cursor persistence or file reclamation fails after a verified commit, drain -still reports the verified candidate revision. The preserved outbox can be -replayed after repairing filesystem access; the failure does not undo the commit. - -For unresolved evidence, retain the directory for inspection and use exact -rollback when abandoning the candidate. Do not delete the cursor to bypass a -failed history check, copy an old entry into a new lineage, or reseed implicitly. - -Qualification explicitly reports bounded scope and -`sustained_parity_verdict=not_evaluated`. The current proof boundary is 10,000 -transactions including the baseline; new commits hold at that boundary while -exact receipt replays remain valid. This is a bounded correctness limit, not a -performance result. Drain budgets bound work between operations; they do not -preempt an in-flight filesystem or RPC call. - -## Roadmap and retirement boundary - -This batch belongs to capture lane C in the [Shared Goal Authority RFC](../../docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md). -Its rollback retires a **pre-promotion candidate**. Lane F still needs a separate -fenced export/rollback after canonical writes, following lane I's exact profile, -import, consumer and recovery qualification and explicit maintainer approval. -The 10,000-transaction bound here grants no sustained parity or promotion. -Long-running local use also needs lane L: an embedded store, bounded live-state -and receipt access, accelerated capacity tests and at least ten natural days of -soak. L can proceed alongside native Todo callers without waiting for PostgreSQL P. - -Retire legacy capture only after every bound primary writer has cut over to the -qualified authority and its replay/recovery consumers have cut over too. Retire -the separate observation handler only with the owner's Q14 decision and verified -replacement of its remaining callers. Python guards stay until those writers no -longer own primary effects; remove the bridge/protocol once no production caller -needs it. The next cutover batch must report deleted product LOC, added bridge -LOC and happy/recovery runtime crossings under the [TS migration RFC](../../docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md). - -## Required validation - -Install the repository test extra and Node dependencies. Run the long tests -separately without skip or relaxation flags: +An unfinished historical bootstrap uses its exact operation ID instead of a +provider revision. The selectors are mutually exclusive. Rollback archives the +candidate and the complete Goal outbox before it publishes an inactive result. +It does not start another capture lineage. -```bash -python -m pip install -e '.[test]' 'build==1.6.0' -npm ci --ignore-scripts -python -m pytest -q -n 4 --dist loadfile -m stage2c_e2e --durations=20 --junitxml=stage2c-e2e.xml -python examples/shared-goal-authority-e2e/mutants.py --output .local/stage2c-mutants -python -m build -python examples/shared-goal-authority-e2e/installed.py --artifact dist/*.whl --report-json .local/installed-wheel.json -python examples/shared-goal-authority-e2e/installed.py --artifact dist/*.tar.gz --report-json .local/installed-sdist.json -python -m pytest -q -n 2 -npm run test:control-plane -npm run typecheck:control-plane -python -m mypy -python -m ruff check tests loopx/canary loopx/control_plane loopx/domain_packs loopx/presentation -python scripts/generate_coordination_state_contract.py --check -python examples/control_plane/cli-output-budget-regression-smoke.py -loopx --format json canary premerge --from-git-diff -``` +## Recovery invariants + +### Exact bytes + +Inspection and backup hash the original files. Recovery never rewrites a +malformed, unknown, or ambiguous artifact before the operator has exported it. +Symlinks, non-regular files, path escapes, and changed bytes fail closed. + +### Exact identity + +Every recovery action binds the Goal, runtime root, source path, store identity, +capture lineage, operation ID, and provider revision required by its historical +schema. A result copied from another Goal cannot establish completion. + +### Exact receipts + +A committed provider transaction and matching receipt can authorize outbox +disposition. A prepared-only entry needs source readback to classify it as +applied or abandoned. Missing or conflicting proof remains pending. + +### Idempotent replay + +Retrying the same operation returns the durable result. Reusing an operation ID +with different arguments fails. A replay never grants fresh execution +authority and never advances canonical state. + +### Bounded work + +Drain respects its entry and time budgets. It reports remaining work and can be +resumed. A failed or interrupted batch leaves enough durable evidence for the +next process to classify the exact operation. + +### Lock order + +Historical management keeps the established cross-runtime lock order. Recovery +must not weaken the canonical writer's maintenance lock or add a second lock +that permits recovery and a canonical commit to race. + +## Qualification matrix -The Linux/Python 3.11 CI lanes use the exact dependency versions and hashes in -`tests/requirements-stage2c-linux-py311.txt`, including pytest 9.1.1. It builds -the checked-out source wheel, verifies its hash during installation, and removes -its generated build tree before normal pytest discovery. The workflow records -the complete installation sequence and retains normal source discovery. - -E2E, deliberate mutants, and independently installed wheel/sdist qualification -run in separate jobs; the stable `stage2c-correctness-e2e` check requires all three -to succeed. No path-based skipping or reduced case selection is used. E2E uses -four file-grouped workers so a module's shared workspaces and ordered parity rows -stay together. Each lane has a distinct evidence artifact. Mutants remain serial -inside their isolated source copy: parallel edits to that copy would invalidate -the control/mutant comparison. Single-row fence probes initialize only the -workspace they use, without removing any baseline or mutant assertion. - -The full TS suite's PostgreSQL conformance requires `LOOPX_TEST_POSTGRES_URL` -pointing to a disposable test database. Source smoke success does not replace -installed-package evidence: the package runner creates an empty environment, -clears source-path injection, runs outside the checkout, verifies installed -Python/TS/JSON provenance, and reads back through an independent native process. - -| Obligation | Retained oracle | +| Obligation | Primary coverage | | --- | --- | -| Full baseline, mixed Python/native writers, handoff, todo add, monitor successor, one receipt per mutation | `test_runtime_shadow_bounded_e2e.py`, `test_shadow_drain_e2e.py` | -| Cursor attacks, complete proof before bounded cleanup, missing cursor writer-first, dual drainers | `test_shadow_cursor_safety.py`, `test_shadow_drain_adversarial.py`, `shadow_cursor_safety.test.ts` | -| Todo/lease abandoned prefixes, later mutations, all cursor consumers and forged applied digests | `test_shadow_cursor_recovery_e2e.py` | -| Full caller diagnostics, argument readback and no-effect controls with absent/disabled/enabled capture | `test_shadow_observable_e2e.py` | -| Primary and drain process death, lost ACK, prepared-only A → B → A | `test_shadow_drain_e2e.py`, `test_shadow_management_e2e.py` | -| Every bootstrap/rollback durable window, raw archive fidelity, late requests, cached-result binding and other-Goal isolation | `shadow_management.test.ts`, `test_shadow_management_e2e.py`, `test_shadow_management_variant_e2e.py` | -| Fence and maintenance boundaries, source and Goal override races, whole-file durability, paragraph injection, public refresh no-write boundary | `test_shadow_writer_boundaries.py`, `test_shadow_writer_variant_e2e.py`, `shadow_native_writer_boundary.test.ts`, `test_shadow_drain_adversarial.py` | -| Canonical native/v0 Todo updates through CLI and native RPC, real pending management, M ordering, and unchanged authority on hold | `test_shadow_native_todo_update_e2e.py` | -| History flaws despite equal snapshots, legacy mixed profile, source drift, event-only hold, qualified reads | `coordination_runtime_shadow.test.ts`, `file_outbox_qualification.test.ts`, `test_runtime_shadow_bounded_e2e.py` | -| Ladder parity half through the public CLI and management interfaces: deferred entries, bounded and idempotent drain, primary and drain SIGKILL windows, rollback with pending entries, sustained mixed-writer parity, drift, the event-only hold, migration refusal and rebootstrap, growth measurement | `test_shared_goal_authority_e2e.py` (`s2c2.*` rows), `ladder.py --stage 2c2` | -| Installed lifecycle and resource provenance in wheel and sdist | `installed.py` | -| Missing checks, lock placement, duplicate mirror, early marker, cursor regression | `mutants.py` with unchanged GREEN controls and assertion RED results | - -The mandatory repair set must have zero failures, skips, pending, or unverified -cases. Broader ladder rows retain their declared pending/environment gates: -`s2c2.archive_after_leased_completion_parity` is now an executable -deterministic row rather than a declaration, and `s2c2.sustained_parity_soak` -stays pending until the Section 7.2 soak exists; these tests grant neither -production promotion nor a completed Stage 2C claim. - -For a caller comparison, run both `test_shadow_observable*_e2e.py` files with -`LOOPX_SHADOW_COMPARISON_SOURCE` set to an immutable baseline checkout, then to -the reviewed source. Set `LOOPX_SHADOW_COMPARISON_OUTPUT` to separate private -directories to retain full diagnostics and before/after fixture bytes. The oracle -is unchanged: the baseline must fail cases for defects this batch intentionally -repairs. Inspect every difference; normalize only documented time, path and -generated-identity variation, and disclose changes to rejection or receipt -behavior. CI retains the reviewed source's synthetic observations alongside the -crash, mutant and installed-package evidence. +| Producer-free status and drain when no history exists | `test_shadow_drain_native_plan.py` | +| Existing prepared/committed record decoding and exact disposition | `local_authority_shadow_outbox.test.ts`, `shadow_entry_delivery.test.ts` | +| Cursor corruption, replay, and bounded cleanup | `test_shadow_cursor_safety.py`, `shadow_cursor_safety.test.ts` | +| Historical management replay and rollback archive fidelity | `test_shadow_management_variant_e2e.py`, `shadow_management.test.ts` | +| Cold source inventory, original-byte hashing, and import holds | `test_cold_source_inspection.py`, `test_cold_source_import_cli.py` | +| Archive and receipt recovery after producer removal | `test_archive_retry_delivery.py`, `test_archive_dependency_capture.py` | +| Canonical provider business writes and exact replay | `test_canonical_todo_writer_isolation.py`, native File/SQLite/PostgreSQL conformance | +| Installed wheel and source distribution lifecycle | `installed.py` | +| Negative controls for deleted authority paths | `mutants.py` | + +Fixtures for recovery tests create the same persisted schemas that old releases +wrote, then invoke only retained readers and disposition owners. A recovery test +must not call a normal business mutation to manufacture its input. + +## Retirement boundary + +The shared-authority ladder no longer registers Stage 2C capture rows or the +long-running capture soak declaration. Those claims described the deleted +producer, not the current provider architecture. + +Historical reports remain valid for the release that produced them. Current +qualification is: + +1. provider conformance for File, SQLite, NoKV, and PostgreSQL; +2. public canonical creation and mutation readback; +3. source-free installed-package retirement tests; +4. producer-free recovery tests over existing artifact schemas. + +Passing historical shadow inspection does not promote a Goal. Passing +canonical provider qualification does not authorize deleting old backups. diff --git a/examples/shared-goal-authority-e2e/installed.py b/examples/shared-goal-authority-e2e/installed.py index 2e7b16ee5e..c978a0f7a7 100644 --- a/examples/shared-goal-authority-e2e/installed.py +++ b/examples/shared-goal-authority-e2e/installed.py @@ -23,9 +23,9 @@ GOAL = "installed-authority-e2e" REQUIRED_STAGES = { - "installed_python_ts_json_resources", "real_baseline_bootstrap", - "three_mutations_one_receipt_each", "default_qualification_and_exact_candidate_read", - "rollback_inactive_write_new_lineage", + "installed_python_ts_json_resources", "canonical_file_creation_and_binding", + "direct_canonical_mutations_and_replay", "independent_provider_history_and_receipts", + "legacy_shadow_bootstrap_rejected", } @@ -60,38 +60,52 @@ def __init__(self, artifact: Path, workspace: Path, report: dict[str, Any]) -> N "removed_environment_keys": sorted(removed), "node": str(self.node), "checkout_added_to_import_path": False} - def process(self, stage: str, argv: list[str], *, timeout: int = 60, parse_json: bool = True) -> Any: + def process(self, stage: str, argv: list[str], *, timeout: int = 60, + parse_json: bool = True, succeeds: bool = True) -> Any: started = time.monotonic() result = subprocess.run(argv, cwd=self.cwd, env=self.environment, text=True, capture_output=True, timeout=timeout, check=False) self.report["processes"].append({"stage": stage, "argv": argv, "returncode": result.returncode, "elapsed_seconds": round(time.monotonic() - started, 3), "stdout": result.stdout, "stderr": result.stderr}) - require(result.returncode == 0, f"{stage} exited {result.returncode}: {result.stdout}\n{result.stderr}") + require((result.returncode == 0) is succeeds, + f"{stage} exited {result.returncode}: {result.stdout}\n{result.stderr}") return json.loads(result.stdout) if parse_json else result.stdout def checked(self, stage: str, **evidence: Any) -> None: self.report["checks"].append({"stage": stage, "status": "passed", **evidence}) - def cli(self, stage: str, *args: str) -> dict[str, Any]: + def cli(self, stage: str, *args: str, succeeds: bool = True) -> dict[str, Any]: result = self.process(stage, [str(self.console), "--registry", str(self.registry), - "--runtime-root", str(self.runtime), "--format", "json", *args]) - require(isinstance(result, dict) and result.get("ok") is True, f"{stage} did not return ok: {result}") + "--runtime-root", str(self.runtime), "--format", "json", *args], + succeeds=succeeds) + require(isinstance(result, dict) and (result.get("ok") is True) is succeeds, + f"{stage} returned an unexpected result: {result}") return result - def sdk_add(self, stage: str, text: str) -> dict[str, Any]: + def canonical_result(self, stage: str, value: dict[str, Any]) -> dict[str, Any]: + require(value.get("source_authority") == "file_v0", f"{stage} used another authority: {value}") + require(value.get("decision_read_from_provider") is True, f"{stage} did not read the provider") + require(value.get("legacy_fallback_used") is False, f"{stage} used a legacy fallback") + require("coordination_runtime_shadow" not in value, f"{stage} emitted retired shadow capture") + return value + + def sdk_add(self, stage: str, text: str, operation_id: str, + *, added: bool) -> dict[str, Any]: code = """ import json, sys from pathlib import Path from loopx.todos import add_goal_todo value = add_goal_todo(registry_path=Path(sys.argv[1]), runtime_root_arg=sys.argv[2], - goal_id=sys.argv[3], role='agent', text=sys.argv[4], task_class='advancement_task', action_kind='analyze') + goal_id=sys.argv[3], role='agent', text=sys.argv[4], task_class='advancement_task', + action_kind='analyze', operation_id=sys.argv[5]) print(json.dumps(value)) """ value = self.process(stage, [str(self.python), "-I", "-c", code, - str(self.registry), str(self.runtime), GOAL, text]) - require(value.get("ok") is True and value.get("added") is True, f"{stage} did not add one Todo") - return value + str(self.registry), str(self.runtime), GOAL, text, operation_id]) + require(value.get("ok") is True and value.get("added") is added, + f"{stage} returned an unexpected add result: {value}") + return self.canonical_result(stage, value) def read_store(self, stage: str, package: Path) -> dict[str, Any]: module = package / "control_plane/coordination/file_authority_store.ts" @@ -100,7 +114,7 @@ def read_store(self, stage: str, package: Path) -> dict[str, Any]: import {join} from 'node:path'; const [modulePath, root, goal] = process.argv.slice(1); const {FileAuthorityStore} = await import(pathToFileURL(modulePath).href); -const store = new FileAuthorityStore(join(root, 'authority-shadow', 'file-v0'), goal, {existingOnly: true}); +const store = new FileAuthorityStore(join(root, 'authority', 'file-v0'), goal, {existingOnly: true}); const head = await store.loadAuthority(); const history = await store.scanCommitted(null, 100); console.log(JSON.stringify({module_path: modulePath, head, history})); @@ -143,75 +157,116 @@ def run(self) -> None: self.checked("installed_python_ts_json_resources", resource_count=len(provenance["resources"])) storage_defaults = {"schema_version": "loopx_goal_storage_defaults_v1", "new_goal_provider": "file", - "canonical_creation": False, "new_goal_handoff_mode": "hard_lease"} + "canonical_creation": True, "new_goal_handoff_mode": "hard_lease"} storage_defaults_path = self.cwd / "goal-storage-defaults.json" storage_defaults_path.write_text(json.dumps(storage_defaults), encoding="utf-8") - storage_preview = self.cli("legacy_goal_storage_preview", "machine-config", "preview", "--namespace", + storage_preview = self.cli("canonical_goal_storage_preview", "machine-config", "preview", "--namespace", "goal_storage", "--config-json", str(storage_defaults_path)) - self.cli("legacy_goal_storage_apply", "machine-config", "apply", "--namespace", "goal_storage", + self.cli("canonical_goal_storage_apply", "machine-config", "apply", "--namespace", "goal_storage", "--config-json", str(storage_defaults_path), "--expected-plan-revision", str(storage_preview["plan_revision"]), "--execute") - self.cli("console_project_bootstrap", "bootstrap", "--project", str(self.project), + created = self.cli("console_project_bootstrap", "bootstrap", "--project", str(self.project), "--goal-id", GOAL, "--objective", "Qualify installed authority transactions.", "--no-global-sync") - self.cli("console_handoff_mode_hard_lease", "handoff-mode", "set", "--goal-id", GOAL, - "--mode", "hard_lease") - # Set configuration only, before shadow bootstrap creates the real binding. - registry = json.loads(self.registry.read_text()) - goal = next(item for item in registry["goals"] if item["id"] == GOAL) - goal["coordination"].update({"agent_model": "peer_v1", "registered_agents": ["agent-a", "agent-b"], - "runtime_shadow": {"schema_version": "loopx_coordination_runtime_shadow_config_v0", - "enabled": True, "provider": "file_v0"}}) - self.registry.write_text(json.dumps(registry)) - boot = self.cli("console_shadow_bootstrap", "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute")["bootstrap"] - require(boot.get("status") == "applied" and bool(boot.get("capture_lineage_id")), f"bootstrap not applied: {boot}") - self.checked("real_baseline_bootstrap", capture_lineage_id=boot["capture_lineage_id"]) - - first = self.sdk_add("installed_python_todo_add", "First installed package mutation.") - require(first["coordination_runtime_shadow"]["outcome"] == "delivered", f"first Todo capture failed: {first}") + selection = created.get("storage_selection") + require(isinstance(selection, dict), f"bootstrap omitted storage selection: {created}") + require(selection.get("provider") == "file" + and selection.get("handoff_mode") == "hard_lease" + and selection.get("authority_initialized") is True + and selection.get("legacy_fallback_used") is False, + f"bootstrap did not initialize canonical File authority: {selection}") + require((self.runtime / "authority" / "file-v0").is_dir(), + "bootstrap did not create the canonical File provider directory") + self.cli("console_configure_agents_and_shadow_rejection_probe", "configure-goal", + "--goal-id", GOAL, "--registered-agent", "agent-a", "--registered-agent", "agent-b", + "--coordination-runtime-shadow-file", "--execute") + self.checked("canonical_file_creation_and_binding", + provider=selection["provider"], provider_revision=selection["provider_revision"]) + + first_operation = "installed-todo-first" + first = self.sdk_add("installed_python_todo_add", + "First installed package mutation.", first_operation, added=True) lease = self.cli("console_native_lease_acquire", "task-lease", "acquire", "--goal-id", GOAL, - "--todo-id", first["todo_id"], "--owner", "agent-a", "--idempotency-key", "installed-lease-a", "--ttl-seconds", "600") - require(lease["coordination_runtime_shadow"]["outcome"] == "delivered", f"native lease capture failed: {lease}") - second = self.sdk_add("installed_python_second_todo_add", "Third installed package mutation.") - require(second["coordination_runtime_shadow"]["outcome"] == "delivered", f"second Todo capture failed: {second}") - drained = self.cli("console_drain", "authority-shadow", "drain", "--goal-id", GOAL) - require(drained.get("pending_after") == 0 and drained.get("prepared_only_after") == 0, - f"drain left unverified work: {drained}") - qualified = self.cli("console_qualify_default_policy", "coordination-shadow", "qualify", "--goal-id", GOAL)["qualification"] - require(qualified.get("status") == "qualified" and qualified.get("qualified") is True, - f"default policy was not qualified: {qualified}") - require(qualified["policy"]["minimum_operations"] == 3 and qualified["evidence"]["operation_count"] == 3, - "qualification did not prove exactly three real primary mutations under the default threshold") - candidate = self.cli("console_read_candidate", "coordination-shadow", "read-candidate", "--goal-id", GOAL, - "--todo-id", first["todo_id"])["read_candidate"] - require(candidate.get("status") == "matched" and candidate.get("read_candidate_qualified") is True, - f"candidate read was not qualified: {candidate}") + "--todo-id", first["todo_id"], "--owner", "agent-a", + "--idempotency-key", "installed-lease-a", "--expected-version", "0", + "--ttl-seconds", "600") + self.canonical_result("console_native_lease_acquire", lease) + require(lease.get("acquired") is True and lease.get("idempotent") is False, + f"native lease was not acquired: {lease}") + second_operation = "installed-todo-second" + second = self.sdk_add("installed_python_second_todo_add", + "Second installed package mutation.", second_operation, added=True) + before_replay = self.cli("console_canonical_todo_list_before_replay", + "todo", "list", "--goal-id", GOAL) + self.canonical_result("console_canonical_todo_list_before_replay", + before_replay["authority_read"]) + receipt_before = self.cli("console_exact_receipt_before_replay", "todo", "receipt", + "--goal-id", GOAL, "--operation-id", first_operation) + self.canonical_result("console_exact_receipt_before_replay", receipt_before) + require(receipt_before.get("status") == "found", f"Todo receipt missing: {receipt_before}") + + replayed = self.sdk_add("installed_python_todo_add_exact_replay", + "First installed package mutation.", first_operation, added=False) + require(replayed.get("status") in {"replayed", "recovered"}, + f"Todo retry did not replay its original operation: {replayed}") + lease_replay = self.cli("console_native_lease_acquire_exact_replay", + "task-lease", "acquire", "--goal-id", GOAL, "--todo-id", first["todo_id"], + "--owner", "agent-a", "--idempotency-key", "installed-lease-a", + "--expected-version", "0", "--ttl-seconds", "600") + self.canonical_result("console_native_lease_acquire_exact_replay", lease_replay) + require(lease_replay.get("idempotent") is True and lease_replay.get("acquired") is False, + f"lease retry did not replay its original operation: {lease_replay}") + after_replay = self.cli("console_canonical_todo_list_after_replay", + "todo", "list", "--goal-id", GOAL) + self.canonical_result("console_canonical_todo_list_after_replay", + after_replay["authority_read"]) + require(after_replay["todos"] == before_replay["todos"] + and after_replay["authority_read"]["provider_revision"] + == before_replay["authority_read"]["provider_revision"], + "exact retries advanced or changed canonical Todo authority") + receipt_after = self.cli("console_exact_receipt_after_replay", "todo", "receipt", + "--goal-id", GOAL, "--operation-id", first_operation) + require(receipt_after == receipt_before, "exact retry changed its historical receipt") + self.checked("direct_canonical_mutations_and_replay", + todo_ids=[first["todo_id"], second["todo_id"]], + lease_version=lease["lease"]["version"], + provider_revision=after_replay["authority_read"]["provider_revision"]) + independent = self.read_store("independent_installed_native_readback", package) - require(independent["head"]["status"] == "loaded" and independent["history"]["status"] == "page", "independent provider read failed") + require(independent["head"]["status"] == "loaded" + and independent["history"]["status"] == "page", + "independent provider read failed") transactions = independent["history"]["transactions"] - require(len(transactions) == 4 and independent["head"]["cursor"] == "4", "primary writes did not map one-to-one to receipts") - require(all(len(tx["receipts"]) == 1 for tx in transactions[1:]), "a primary mutation has missing or duplicate receipts") - old_head = independent["head"]["head"] - require({first["todo_id"], second["todo_id"]}.issubset({item["todo_id"] for item in old_head["todos"]}), "native readback lost a Todo") - require(any(item["todo_id"] == first["todo_id"] and item["owner"] == "agent-a" for item in old_head["leases"]), "native readback lost the lease") - self.checked("three_mutations_one_receipt_each", mutation_count=3, cursor="4") - self.checked("default_qualification_and_exact_candidate_read", minimum_operations=3, todo_id=first["todo_id"]) - - rolled = self.cli("console_rollback", "coordination-shadow", "rollback", "--goal-id", GOAL, - "--provider-revision", independent["head"]["provider_revision"], "--execute")["rollback"] - require(rolled.get("status") == "applied", f"rollback not applied: {rolled}") - absent = self.read_store("independent_read_after_rollback", package) - require(absent["head"]["status"] == "missing", "rollback left an active candidate") - inactive = self.sdk_add("installed_python_inactive_primary_write", "Primary remains writable after rollback.") - require(inactive["coordination_runtime_shadow"]["reason_code"] == "bootstrap_required", "inactive write fabricated capture qualification") - require(not (self.runtime / "authority-shadow" / "outbox" / GOAL).exists(), "inactive write created outbox without a lineage") - again = self.cli("console_new_bootstrap", "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute")["bootstrap"] - require(again.get("status") == "applied" and again.get("capture_lineage_id") != boot["capture_lineage_id"], "new baseline reused the retired lineage") - final = self.read_store("independent_new_baseline_readback", package) - require(final["head"]["status"] == "loaded" and final["head"]["cursor"] == "1", "new baseline has an invalid initial cursor") - require(inactive["todo_id"] in {item["todo_id"] for item in final["head"]["head"]["todos"]}, "new baseline omitted the inactive primary write") - require(len(final["history"]["transactions"]) == 1, "new baseline pretended to retain old mutation coverage") - self.checked("rollback_inactive_write_new_lineage", previous_lineage=boot["capture_lineage_id"], new_lineage=again["capture_lineage_id"]) + require(len(transactions) == 4 and independent["head"]["cursor"] == "4", + "canonical writes did not map one-to-one to provider transactions") + require(all(len(tx["receipts"]) == 1 for tx in transactions[1:]), + "a canonical business mutation has missing or duplicate receipts") + require({first_operation, second_operation}.issubset( + {tx["operation_id"] for tx in transactions}), + "provider history omitted an installed SDK operation") + head = independent["head"]["head"] + require({first["todo_id"], second["todo_id"]}.issubset( + {item["todo_id"] for item in head["todos"]}), + "native readback lost a Todo") + require(any(item["todo_id"] == first["todo_id"] and item["owner"] == "agent-a" + for item in head["leases"]), "native readback lost the lease") + self.checked("independent_provider_history_and_receipts", + transaction_count=len(transactions), cursor=independent["head"]["cursor"], + operation_ids=[tx["operation_id"] for tx in transactions]) + + rejected = self.cli("console_legacy_shadow_bootstrap_rejected", + "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute", + succeeds=False)["bootstrap"] + require(rejected.get("status") == "failed" + and rejected.get("reason_code") == "legacy_authority_already_promoted", + f"legacy shadow bootstrap did not reject canonical authority: {rejected}") + final = self.read_store("independent_read_after_legacy_rejection", package) + require(final["head"]["provider_revision"] == independent["head"]["provider_revision"] + and final["history"] == independent["history"], + "rejected legacy bootstrap changed canonical authority") + self.checked("legacy_shadow_bootstrap_rejected", + reason_code=rejected["reason_code"], + provider_revision=final["head"]["provider_revision"]) def main(argv: list[str] | None = None) -> int: diff --git a/examples/shared-goal-authority-e2e/mutants.py b/examples/shared-goal-authority-e2e/mutants.py index 5661d25221..d8c9586845 100644 --- a/examples/shared-goal-authority-e2e/mutants.py +++ b/examples/shared-goal-authority-e2e/mutants.py @@ -7,7 +7,6 @@ from __future__ import annotations import argparse -import ast import difflib import hashlib import json @@ -92,39 +91,6 @@ def command(self) -> list[str]: '["advancement_task", "user_gate", "user_action", "blocker"].includes(String(condition.target_task_class))', 'true')),), 'tests/control_plane_ts/delivery_response.test.ts', 'exact dependency identity'), - Case('rollout_cwd_root', (('loopx/cli_rollout.py', replacement( - 'resolve_runtime_root(registry, runtime_root_arg, registry_path=registry_path)', - 'resolve_runtime_root(registry, runtime_root_arg)')),), - 'tests/control_plane/test_shadow_observable_e2e.py::test_registry_relative_root_does_not_depend_on_callers_cwd[disabled]'), - Case('native_note_dropped', ((COORDINATION + 'todo_update_intent.ts', replacement( - ' const next: JsonObject = {...todo, ...input.patch};', - ' const next: JsonObject = {...todo, ...input.patch};\n if ("note" in input.patch) next.note = todo.note;')),), - 'tests/control_plane/test_shadow_observable_native_e2e.py::test_native_unclaimed_edit_and_explicit_note_clear[disabled]'), - Case('native_unclaimed_edit_rejected', ((COORDINATION + 'todo_lifecycle_decision.ts', replacement( - ' if (todo.claimed_by !== null && todo.claimed_by !== actor) return "claim_owner_mismatch";', - ' if (todo.claimed_by === null || todo.claimed_by !== actor) return "claim_owner_mismatch";')),), - 'tests/control_plane/test_shadow_observable_native_e2e.py::test_native_unclaimed_edit_and_explicit_note_clear[disabled]'), - Case('native_diagnostic_truncated', ((COORDINATION + 'todo_update_admission.ts', replacement( - '? "Todo update cannot edit another claim owner\'s work"', - '? "Update rejected"')),), - 'tests/control_plane/test_shadow_observable_native_e2e.py::test_canonical_argument_intent_and_atomic_claim[disabled]'), - Case('cursor_baseline_digest', ((COORDINATION + 'shadow_drain_plan.ts', replacement( - 'last_partition_digest: partitionDigest(last, r.partition)', - 'last_partition_digest: view.head_digest')),), - 'tests/control_plane/test_shadow_cursor_recovery_e2e.py::test_abandoned_cursor_survives_all_consumers[2-0-todos]'), - Case('qualification_baseline_digest', ((COORDINATION + 'runtime_shadow.ts', replacement( - 'const digest = marker === null ? null : (marker as JsonObject).partition_digest;', - 'const digest = marker === null ? localAuthorityShadowHeadDigest(anchor.projection) : (marker as JsonObject).partition_digest;')),), - 'tests/control_plane/test_shadow_cursor_recovery_e2e.py::test_abandoned_cursor_survives_all_consumers[2-0-leases]'), - Case('cursor_digest_unchecked', ( - (COORDINATION + 'runtime_shadow.ts', replacement( - 'if (digest !== cursor.last_partition_digest) throw new ShadowLineageError("outbox_cursor_unproved");', - '// DELIBERATE MUTANT: accept any syntactically valid cursor digest.')), - (COORDINATION + 'shadow_drain_plan.ts', replacement( - ' anchor.provider_revision === r.cursor.last_provider_revision &&\n' - ' partitionDigest(anchor, r.partition) === r.cursor.last_partition_digest,\n', - ' anchor.provider_revision === r.cursor.last_provider_revision,\n'))), - 'tests/control_plane/test_shadow_cursor_recovery_e2e.py::test_forged_applied_digest_holds_every_consumer_without_rewriting_bytes[True-todos]'), Case('lineage', ((COORDINATION + 'local_authority_shadow.ts', replacement(' requireLineage(entry.capture_lineage_id === binding.capture_lineage_id, "stale_generation");', ' // DELIBERATE MUTANT: omit active lineage validation.')),), 'tests/control_plane_ts/local_authority_shadow_outbox.test.ts', 'self-consistent foreign'), Case('previous_partition', ((COORDINATION + 'local_authority_shadow.ts', replacement(' requireLineage(request.entry.source.previous_partition_digest === digest, "source_partition_continuity_unproved");', ' // DELIBERATE MUTANT: omit previous partition proof.')),), @@ -143,59 +109,6 @@ def command(self) -> list[str]: "tests/control_plane_ts/shadow_management.test.ts", 'rollback refuses a different valid candidate lineage'), ] -PREPARED_WRITE = ''' durable_write_json( - self._directory / entry_file_name(seq, entry_id, "prepared"), - record, - )''' -CASES.extend([ - Case("receipt_bytes", ((COORDINATION + "shadow_drain_plan.ts", replacement( - "entry[key] === null || entry[key] === rc[key]", - "entry[key] === null || entry[key] === entry[key]")),), - "tests/control_plane/test_shadow_drain_adversarial.py::test_raw_residue_mismatch_preserves_every_file_before_any_cleanup"), - Case("cursor_regression", ((COORDINATION + "shadow_drain_plan.ts", replacement( - "{last_seq: history.size, last_entry_id: last.operation_id", - "{last_seq: 1, last_entry_id: last.operation_id")),), - "tests/control_plane/test_shadow_drain_e2e.py::test_public_primary_maps_one_to_one_to_receipts_and_replays_idempotently"), - Case("early_committed", ((COORDINATION + "local_authority_shadow_outbox.py", replacement( - PREPARED_WRITE, PREPARED_WRITE + ''' - durable_write_json( - self._directory / entry_file_name(seq, entry_id, "committed"), - {"schema_version": OUTBOX_COMMIT_SCHEMA, "entry_id": entry_id, - "capture_lineage_id": self._lineage_id, "committed_at": utc_now_text()}, - )''')),), - "tests/control_plane/test_shadow_drain_e2e.py::test_primary_sigkill_preserves_complete_bytes_and_proves_before_marker[before_replace]"), -]) - -# Ladder parity-half rows (s2c2.*) drive the same lifecycle through the public -# CLI; these mutants remove one operator-visible truth each and must turn the -# corresponding row red. -LADDER_ROW = "tests/control_plane/test_shared_goal_authority_e2e.py::test_ladder_row_passes_or_is_declared_unverified" -CASES.extend([ - Case("status_hides_prepared_only", ((COORDINATION + "local_authority_shadow_history.py", replacement( - ' "committed_pending": sum(1 for entry in entries if entry.is_committed),\n' - ' "prepared_only": sum(1 for entry in entries if not entry.is_committed),', - ' "committed_pending": len(entries),\n' - ' "prepared_only": 0,')),), - LADDER_ROW + "[s2c2.sigkill_between_primary_write_and_drain]"), - Case("qualification_ignores_drift", ((COORDINATION + "runtime_shadow.ts", replacement( - "const matched = localAuthorityShadowHeadDigest(request.projection) === localAuthorityShadowHeadDigest(lineage.head.head);", - "const matched = true;")),), - LADDER_ROW + "[s2c2.parity_divergent_detects_foreign_edit]"), - Case("replay_counted_as_delivery", ((COORDINATION + "shadow_drain.ts", replacement( - "result.no_op += Number(noOp === true); result.entries.push(summary); result.replayed++; consumed++;", - "result.no_op += Number(noOp === true); result.entries.push(summary); result.delivered++; consumed++;")),), - LADDER_ROW + "[s2c2.sigkill_mid_drain]"), -]) - - -def remove_fence(source: str) -> str: - function = next(node for node in ast.parse(source).body - if isinstance(node, ast.FunctionDef) - and node.name == "require_legacy_coordination_write_allowed") - lines = source.splitlines(keepends=True) - return "".join(lines[:function.body[0].lineno - 1]) + " return\n" + "".join(lines[function.end_lineno:]) - - def typescript_exported_function_span(source: str, name: str) -> tuple[int, int]: """Bound one top-level exported function without depending on its successor.""" @@ -215,47 +128,19 @@ def remove_native_update_maintenance(source: str) -> str: "return await withCanonicalWriter(root, goalId, input.dry_run === true, async () => {", "return await (async () => {", )(function) - function = replacement(" });\n } catch (error) {", " })();\n } catch (error) {")(function) + function = replacement( + ' }, {businessWrite: "todo_update"});\n } catch (error) {', + " })();\n } catch (error) {", + )(function) return source[:start] + function + source[end:] -def move_guard_outside_lock(name: str) -> Callable[[str], str]: - def apply(source: str) -> str: - function = next(node for node in ast.parse(source).body - if isinstance(node, ast.FunctionDef) - and node.name == "legacy_todo_write_transaction") - calls = [node for node in ast.walk(function) if isinstance(node, ast.Expr) - and isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) - and node.value.func.id == name] - if len(calls) != 1: - raise ValueError("guard locator drift; review lock protocol") - call = calls[0] - lines = source.splitlines(keepends=True) - segment = "".join(lines[call.lineno - 1:call.end_lineno]) - text = "".join(lines[:call.lineno - 1] + lines[call.end_lineno:]) - start = text.index(" with exclusive_cross_runtime_file_lock(\n", - text.index("def legacy_todo_write_transaction(")) - # Preserve the exact arguments while moving only the actual guard. - unindented = "".join(line[4:] for line in segment.splitlines(keepends=True)) - return text[:start] + " if not dry_run:\n" + unindented + text[start:] - return apply - - WRITER_TEST = "tests/control_plane/test_shadow_writer_boundaries.py::" -FENCE_TEST = WRITER_TEST + "test_cli_waiting_for_todo_mutex_rechecks_fence_after_engagement" CASES.extend([ Case("management_result_binding", ((COORDINATION + "shadow_management.ts", replacement( " await validateReplayResult(request, manifest, prior.result, state);", " // DELIBERATE MUTANT: trust a cached result from another operation.")),), "tests/control_plane/test_shadow_management_variant_e2e.py::test_rollback_result_cannot_borrow_another_goals_archive_evidence[historical]"), - Case("cross_goal_source_guard", ((COORDINATION + "legacy_writer_fence.py", replacement( - " resolved_source = state_file.resolve(strict=False)", - " return # DELIBERATE MUTANT: allow another goal to bypass source authority.\n resolved_source = state_file.resolve(strict=False)")),), - "tests/control_plane/test_shadow_writer_variant_e2e.py::test_other_goal_cannot_write_a_protected_goal_source_via_state_override[active_capture]"), - Case("cleanup_hides_verified_commit", ((COORDINATION + "shadow_drain.ts", replacement( - " if (plan.view !== null && typeof plan.view === \"object\") observe(plan.view as JsonObject);\n", - "")),), - "tests/control_plane/test_shadow_drain_adversarial.py::test_cleanup_permission_failure_reports_verified_commit_and_recovers[before_commit]"), Case("native_update_maintenance", ((COORDINATION + "local_authority_runtime.ts", remove_native_update_maintenance),), "tests/control_plane/test_shadow_native_todo_update_e2e.py::test_native_update_holds_before_primary_for_management[native-bootstrapping-cli]"), @@ -263,12 +148,6 @@ def apply(source: str) -> str: "movable.slice(0, moveCount)", "movable.slice(-moveCount)")),), "tests/control_plane_ts/todo_archive_selection.test.ts", "archive selection preserves imported order"), - Case("remove_fence", ((COORDINATION + "legacy_writer_fence.py", remove_fence),), FENCE_TEST), - Case("fence_outside_lock", ((COORDINATION + "legacy_writer_fence.py", - move_guard_outside_lock("require_legacy_coordination_write_allowed")),), FENCE_TEST), - Case("source_binding_outside_lock", ((COORDINATION + "legacy_writer_fence.py", - move_guard_outside_lock("require_registry_source_write_allowed")),), - WRITER_TEST + "test_waiting_override_writer_rechecks_registry_binding_inside_shared_state_lock"), Case("remove_refresh_source_recheck", (("loopx/state_refresh.py", replacement( " if normalized_next_action:", " if False and normalized_next_action: # DELIBERATE MUTANT: bypass source recheck.")),), @@ -282,65 +161,15 @@ def apply(source: str) -> str: 'withFileMutationLock(statePath + ".mutant-unshared", () =>')),), "tests/control_plane_ts/shadow_native_writer_boundary.test.ts", "^fence engagement waits for an existing state writer before publication$"), - Case("remove_bound_state_path", ((COORDINATION + "legacy_writer_fence.py", replacement( - "if bound_source.resolve(strict=False) != state_file.resolve(strict=False):", - "if False:")),), - "tests/control_plane/test_shadow_drain_adversarial.py::test_state_file_override_cannot_attribute_an_unbound_source_to_active_lineage"), - Case("bootstrap_unregistered_root", ((COORDINATION + "runtime_shadow.ts", replacement( - "if (!await sameSourceRuntimeRoot(registeredRoot, request.runtime_root))", - "if (false)")),), - "tests/control_plane/test_runtime_shadow_bounded_e2e.py::test_controller_cannot_bind_the_registered_source_to_an_override_runtime_root"), - Case("bootstrap_unregistered_state", ((COORDINATION + "runtime_shadow.ts", replacement( - "if (resolve(String(snapshot.state_path)) !== resolve(String(snapshot.registered_state_path)))", - "if (false && resolve(String(snapshot.state_path)) !== resolve(String(snapshot.registered_state_path)))")),), - "tests/control_plane/test_runtime_shadow_bounded_e2e.py::test_controller_cannot_bind_an_alternate_state_file_before_or_after_bootstrap"), - Case("locale_dependent_lease_order", ((COORDINATION + "runtime_shadow.ts", replacement( - '''.sort((left, right) => { - if (left < right) return -1; - if (left > right) return 1; - return 0; - });''', ".sort((left, right) => left.localeCompare(right));")),), - "tests/control_plane/test_runtime_shadow_bounded_e2e.py::test_source_snapshot_preserves_inventory_without_projecting_orphan_leases"), ]) -# Restore both halves of the obsolete mirror: the public CLI hook and an actual -# second FileAuthorityStore commit. A fabricated RPC result would prove nothing. -MIRROR_HOOK = ''' if payload.get("ok") and payload.get("added") and not payload.get("dry_run"): - from ..control_plane.effect_runtime import effect_runtime_result as restored_mirror - restored_mirror("coordination.runtime_shadow.commit", { - "runtime_root": str(resolve_runtime_root(load_registry(registry_path), runtime_root_arg)), - "goal_id": args.goal_id, "operation_id": "restored-snapshot:" + payload["todo_id"], - }) -''' -MIRROR_COMMIT = ''' const request = requireJsonObject(_value, "restored snapshot request"); - const root = String(request.runtime_root); const goal = String(request.goal_id); - return await withShadowMaintenanceLock(root, goal, async () => { - const store = new FileAuthorityStore(join(root, "authority-shadow", "file-v0"), goal, { existingOnly: true }); - const head = await store.loadAuthority(); - if (head.status !== "loaded") throw new Error("restored mirror needs a real existing baseline"); - const result = await store.commitAuthority({expected_provider_revision: head.provider_revision, - operation_id: String(request.operation_id), next_projection: head.head, - events: [{schema_version: "loopx_coordination_runtime_shadow_event_v0", event_kind: "todo_add"}], - receipts: [{schema_version: "loopx_coordination_runtime_shadow_receipt_v0", operation_id: request.operation_id}]}); - return {...result}; - });''' -CASES.append(Case("duplicate_mirror", ( - ("loopx/cli_commands/todo.py", replacement(" print_payload(\n payload,\n", - MIRROR_HOOK + " print_payload(\n payload,\n")), - (COORDINATION + "runtime_shadow.ts", replacement( - ''' return { schema_version: COORDINATION_RUNTIME_SHADOW_RESULT_SCHEMA, status: "failed", - reason_code: "legacy_lineage_read_only", primary_writeback_preserved: true, decision_read_from_shadow: false };''', - MIRROR_COMMIT)), -), "tests/control_plane/test_shadow_drain_e2e.py::test_public_mutation_has_no_second_snapshot_mirror")) - -# Fenced sibling-caller parity: each mutant is caught by exactly one parity row. -# Promoted CLI acquisition now succeeds through canonical authority. Probe the -# retained native legacy entrypoint for its fence diagnostic/envelope contract. +# The retained exact-release path still observes the durable promotion fence. CASES.append(Case("native_fence_remediation_truncated", ( (COORDINATION + "legacy_writer_fence.ts", replacement( 'export const LEGACY_WRITER_FENCED_REMEDIATION =\n "legacy coordination writer is fenced; use the promoted canonical authority ({authority_mode}) for goal {goal_id}; fence {fence_id}; the primary record was not changed";', 'export const LEGACY_WRITER_FENCED_REMEDIATION =\n "legacy coordination writer is fenced";')), -), "tests/control_plane_ts/legacy_writer_fence_caller_parity.test.ts", pattern="^fence parity: ts-acquire-engaged$")) +), "tests/control_plane_ts/task_lease_lifecycle.test.ts", + pattern="^exact legacy release honors an engaged promotion fence without writes$")) CASES.append(Case("python_fence_remediation_truncated", ( (COORDINATION + "legacy_writer_fence.py", replacement( 'LEGACY_WRITER_FENCED_REMEDIATION = (\n "legacy coordination writer is fenced; use the promoted canonical authority "\n "({authority_mode}) for goal {goal_id}; fence {fence_id}; "\n "the primary record was not changed"\n)', @@ -350,17 +179,14 @@ def apply(source: str) -> str: (COORDINATION + "legacy_writer_fence.ts", replacement( " this.payload = { write_check: writeCheck };", " this.payload = writeCheck;")), -), "tests/control_plane_ts/legacy_writer_fence_caller_parity.test.ts", pattern="^fence parity: ts-acquire-engaged$")) -CASES.append(Case("fence_acquire_receipt_fabricated", ( - ("loopx/control_plane/work_items/task_lease_acquire.ts", replacement( - ' return { code: error.code, message: error.message, payload: error.payload, stage: "validation", kind: "permission_denied" };', - ' return { code: error.code, message: error.message, payload: error.payload };')), -), "tests/control_plane_ts/legacy_writer_fence_caller_parity.test.ts", pattern="^fence parity: ts-acquire-engaged$")) +), "tests/control_plane_ts/task_lease_lifecycle.test.ts", + pattern="^exact legacy release honors an engaged promotion fence without writes$")) CASES.append(Case("lifecycle_fence_guard_skipped", ( ("loopx/control_plane/work_items/task_lease_lifecycle.ts", replacement( " await requireLegacyCoordinationPrimaryWriteAllowed(request!.runtime_root, request!.goal_id);\n", "")), -), "tests/control_plane_ts/legacy_writer_fence_caller_parity.test.ts", pattern="^fence parity: ts-release-engaged$")) +), "tests/control_plane_ts/task_lease_lifecycle.test.ts", + pattern="^exact legacy release honors an engaged promotion fence without writes$")) def run(case: Case, directory: Path, log: Path) -> subprocess.CompletedProcess[str]: diff --git a/loopx/bootstrap.py b/loopx/bootstrap.py index 66f25169af..c3ddc8b4d7 100644 --- a/loopx/bootstrap.py +++ b/loopx/bootstrap.py @@ -4,7 +4,10 @@ from uuid import uuid4 from pathlib import Path -from .capabilities.machine_configuration.goal_storage import new_goal_storage_target, initialize_goal_storage_target +from .capabilities.machine_configuration.goal_storage import new_goal_storage_target +from .control_plane.coordination.goal_storage_initialization import ( + initialize_goal_storage_target, +) from .control_plane.projects.identity import ( GoalCreationConflictError as GoalCreationConflictError, slugify_goal_id as slugify_goal_id, @@ -12,8 +15,11 @@ derive_goal_display_name as derive_goal_display_name, ) from .registry import find_registry_goal, resolve_state_file -from .control_plane.coordination.local_authority import read_canonical_todos_if_promoted -from .control_plane.coordination.legacy_writer_fence import legacy_todo_write_transaction, require_legacy_state_replacement_allowed +from .control_plane.coordination.local_authority import ( + read_canonical_todos_if_promoted, + require_canonical_authority, +) +from .control_plane.coordination.legacy_writer_fence import require_legacy_state_replacement_allowed from .control_plane.coordination.legacy_writer_fence import require_registry_source_write_allowed from .control_plane.projects.registry_codec import ( load_project_registry, @@ -392,6 +398,8 @@ def bootstrap_project( existing_authority = (read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id) if previous_goal is not None and not canonical_creation else None) canonical_existing = existing_authority is not None + if not canonical_creation and not canonical_existing: + require_canonical_authority(goal_id=goal_id, operation="goal_create") if canonical_creation and previous_goal is not None and force: raise ValueError("Canonical creation cannot rebuild existing state; restore or migrate through its owning operation") if canonical_existing and force: @@ -529,7 +537,6 @@ def bootstrap_project( "error": str(global_writability.get("error") or "global registry is not writable"), } storage_selection = None - shadow_capture = None shadow_evidence: dict[str, Any] = {} if not dry_run: # Reconnect enters the same typed receipt owner before compatibility @@ -544,10 +551,7 @@ def bootstrap_project( registry_path, operation="bootstrap_registry", create=dict, - ) as registry_transaction, (nullcontext() if canonical_existing else legacy_todo_write_transaction( - registry_path, goal_id, state_file, None, "bootstrap_state", canonical_creation, - runtime_root=runtime_root, - )): + ) as registry_transaction, nullcontext(): current_registry = registry_transaction.payload_copy() current_goal = find_registry_goal(current_registry, goal_id) if canonical_existing and current_goal != previous_goal: @@ -606,20 +610,7 @@ def bootstrap_project( if canonical_bootstrap_transport: require_registry_source_write_allowed(registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id, state_file=state_file) - if not canonical_creation: - from .control_plane.coordination.runtime_shadow_writer_adapter import ( - begin_todo_runtime_shadow_capture, - require_runtime_shadow_capture_prepared, - ) - - shadow_capture = begin_todo_runtime_shadow_capture(registry_path=registry_path, - runtime_root=runtime_root, goal_id=goal_id, state_path=state_file, - write_class="bootstrap_state", original_text=original) - shadow_capture.prepare(planned) - require_runtime_shadow_capture_prepared(shadow_capture, runtime_root=runtime_root, goal_id=goal_id) atomic_write_state_text(state_file, planned) - if shadow_capture is not None: - shadow_capture.committed() if todo_source_migration is not None: todo_source_migration["applied"] = True current_registry.setdefault("schema_version", "0.1") @@ -632,11 +623,6 @@ def bootstrap_project( # retarget that Goal. The TS owner refuses replacing an existing provider. if not canonical_existing: storage_selection = initialize_goal_storage_target(runtime_root, find_registry_goal(registry, goal_id) or {}, registry_path=registry_path) - if shadow_capture is not None: - from .control_plane.coordination.runtime_shadow_writer_adapter import settle_todo_runtime_shadow_capture - - shadow_evidence = settle_todo_runtime_shadow_capture({}, registry_path=registry_path, - runtime_root=runtime_root, goal_id=goal_id, capture=shadow_capture, emit_disabled=False) if sync_global: global_sync = sync_project_registry_to_global( registry_path=registry_path, diff --git a/loopx/capabilities/machine_configuration/goal_storage.py b/loopx/capabilities/machine_configuration/goal_storage.py index 62956a4110..ec9aa567b6 100644 --- a/loopx/capabilities/machine_configuration/goal_storage.py +++ b/loopx/capabilities/machine_configuration/goal_storage.py @@ -52,31 +52,3 @@ def new_goal_storage_target(runtime_root: Path) -> dict[str, Any]: # Existing Goals do not enter this creation-time resolution path. raw = registry.resolve("goal_storage").default_configuration return effect_runtime_result(NEW_GOAL_STORAGE_METHOD, {"action": "resolve", "configuration": raw}) - - -def initialize_goal_storage_target(runtime_root: Path, goal: Mapping[str, Any], *, registry_path: Path | None = None) -> dict[str, Any] | None: - target = (goal.get("coordination") or {}).get("storage_target") - if target is None: - return None - request = {"action": "initialize", "runtime_root": str(runtime_root), "goal_id": goal["id"], "target": target} - if target.get("schema_version") == "loopx_new_goal_storage_target_v1": - from ...registry import resolve_state_file - from ...control_plane.coordination.authority_source_capture import authority_registry_source - from ...agent_registry import registered_agent_ids_for_goal - if registry_path is None: - raise ValueError("Canonical creation requires its registered source") - state_path = resolve_state_file(Path(goal["repo"]), goal["state_file"]) - with authority_registry_source(registry_path) as witness: - request.update(creation_operation_id=goal.get("creation_operation_id"), source_snapshot={ - "state_path": str(state_path.resolve()), - "registry_source": {**witness, "registered_agents": registered_agent_ids_for_goal(dict(goal))}}) - result = effect_runtime_result(NEW_GOAL_STORAGE_METHOD, request) - # Native receipt readback needs no Markdown parsing. Only the typed - # owner can request a complete source capture for unfinished creation. - if result.get("source_capture_required") is not True: - return result - from ...control_plane.coordination.authority_source_capture import build_runtime_shadow_source_snapshot - projection, snapshot = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=runtime_root, - state_path=state_path, registry_path=registry_path) - request.update(creation_operation_id=goal.get("creation_operation_id"), projection=projection, source_snapshot=snapshot) - return effect_runtime_result(NEW_GOAL_STORAGE_METHOD, request) diff --git a/loopx/chat_actions.py b/loopx/chat_actions.py index d2aa3d00ef..d1fa0a1052 100644 --- a/loopx/chat_actions.py +++ b/loopx/chat_actions.py @@ -10,7 +10,6 @@ from .agent_registry import agent_profile_for_goal, registered_agent_ids_for_goal from .control_plane.projects.identity import GoalCreationConflictError -from .capabilities.machine_configuration.goal_storage import initialize_goal_storage_target from .chat import apply_todo_review_preview, build_todo_review_preview from .chat_action_normalization import ChatActionNormalizationMixin from .chat_action_store import ActionConflictError, ChatActionStore @@ -23,6 +22,9 @@ bind_goal_agent_with_global_sync, read_goal_agent_binding_with_source_route, ) +from .control_plane.coordination.goal_storage_initialization import ( + initialize_goal_storage_target, +) from .control_plane.runtime.time import now_utc, parse_timestamp from .control_plane.scheduler.monitor_todo import monitor_next_due_at from .control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable, read_canonical_todos_if_promoted diff --git a/loopx/cli_commands/project.py b/loopx/cli_commands/project.py index e9813e167f..d8b3a74e32 100644 --- a/loopx/cli_commands/project.py +++ b/loopx/cli_commands/project.py @@ -2,11 +2,13 @@ from ..control_plane.coordination.legacy_writer_fence import LegacyCoordinationWriterFenced from ..control_plane.coordination.shadow_management import ShadowManagementError +from ..control_plane.effect_runtime import EffectRuntimeRemoteError import argparse from collections.abc import Callable, Mapping from pathlib import Path +from ..capabilities.machine_configuration.goal_storage import new_goal_storage_target from ..control_plane.projects.registry import ( PROJECT_KINDS, bind_session, @@ -160,6 +162,7 @@ def handle_project_command( stop_condition=args.stop_condition, repository_bindings=args.repository, external_locator_bindings=args.external_locator, + resolve_new_goal_storage_target=new_goal_storage_target, goal_instance_profile=args.goal_instance_profile, operation_id=args.operation_id, ) @@ -197,7 +200,14 @@ def handle_project_command( operation_id=args.operation_id, execute=args.execute, ) - except (OSError, TypeError, ValueError, LegacyCoordinationWriterFenced, ShadowManagementError) as exc: + except ( + OSError, + TypeError, + ValueError, + EffectRuntimeRemoteError, + LegacyCoordinationWriterFenced, + ShadowManagementError, + ) as exc: payload = { "ok": False, "changed": False, diff --git a/loopx/cli_commands/todo_argument_validation.py b/loopx/cli_commands/todo_argument_validation.py index 2f4982ebf5..eda6ec3cb6 100644 --- a/loopx/cli_commands/todo_argument_validation.py +++ b/loopx/cli_commands/todo_argument_validation.py @@ -499,6 +499,10 @@ def validate_todo_complete_options(args: argparse.Namespace) -> None: ) if args.completion_identity_key and not args.no_follow_up: raise ValueError("--completion-identity-key is only valid with --no-follow-up") + if args.completion_identity_key and (args.note or args.evidence): + raise ValueError( + "--completion-identity-key reentry preserves the original note and evidence" + ) if any(getattr(args, field) for field in ("task_repository", "bound_agent", "goal_bound", "blocks_agent", "clear_blocks_agent", "excluded_agents", "clear_excluded_agents", "global_gate", "clear_global_gate", "unblocks_todo_id", "resume_when")): raise ValueError("todo complete does not update current todo routing metadata; use todo update first") if any(getattr(args, field) for field in ("monitor_target_key", "cadence", "next_due_at", "expires_at")): @@ -509,7 +513,11 @@ def validate_todo_complete_options(args: argparse.Namespace) -> None: raise ValueError("--no-follow-up cannot be combined with successor todos") if args.successor_todo_ids and (args.next_agent_todo or args.next_user_todo): raise ValueError("--successor-todo-id links existing work and cannot be combined with --next-agent-todo or --next-user-todo") - if args.no_follow_up and not (args.note or args.evidence): + if ( + args.no_follow_up + and not args.completion_identity_key + and not (args.note or args.evidence) + ): raise ValueError("--no-follow-up requires --note or --evidence") if args.continuation_policy: raise ValueError("todo complete does not update --continuation-policy; use todo update first") diff --git a/loopx/cli_commands/todo_registration.py b/loopx/cli_commands/todo_registration.py index 239c65b37e..738739e812 100644 --- a/loopx/cli_commands/todo_registration.py +++ b/loopx/cli_commands/todo_registration.py @@ -93,7 +93,8 @@ def register_todo_command( help=( "For todo complete --no-follow-up lifecycle reentry, reuse the " "exact completion identity projected by LoopX. This is not a quota " - "turn id and cannot be combined with --turn-instance-id." + "turn id and cannot be combined with --turn-instance-id, --note, " + "or --evidence." ), ) todo_parser.add_argument( @@ -418,8 +419,9 @@ def register_todo_command( "--no-follow-up", action="store_true", help=( - "For todo update/complete, record a structured no-follow-up rationale " - "when a completed todo intentionally has no successor." + "For ordinary todo update/complete, record a structured no-follow-up " + "rationale when a completed todo intentionally has no successor. " + "Lifecycle reentry preserves the original completion prose." ), ) register_todo_successor_creation_arguments(todo_parser) diff --git a/loopx/cli_commands/turn_todo_writeback.py b/loopx/cli_commands/turn_todo_writeback.py index 975fff2544..dbf02a57f4 100644 --- a/loopx/cli_commands/turn_todo_writeback.py +++ b/loopx/cli_commands/turn_todo_writeback.py @@ -21,8 +21,8 @@ def write_turn_repair_update( ``runtime_root_arg`` is required on purpose: the Turn settlement must hand down the same ``--runtime-root`` override the dispatch resolved, so - the legacy writer fence and the todo mutex of a promotion cannot split - from the Turn writeback path. + the canonical provider route and its maintenance guard cannot split from + the Turn writeback path. """ update_goal_todo( diff --git a/loopx/configuration_catalog.py b/loopx/configuration_catalog.py index df5801d930..a332755029 100644 --- a/loopx/configuration_catalog.py +++ b/loopx/configuration_catalog.py @@ -241,8 +241,8 @@ def build_goal_configuration_catalog( "cutover to canonical local authority." ), "effect": ( - "Captures transaction-bound Todo and task-lease mutations in " - "the file-v0 shadow used by parity qualification." + "Retains inspection and bootstrap controls for pre-cutoff " + "file-v0 shadow lineage; normal writes no longer feed it." ), "does_not": [ "promote the Goal or fence legacy writers", diff --git a/loopx/control_plane/coordination/goal_storage_initialization.py b/loopx/control_plane/coordination/goal_storage_initialization.py new file mode 100644 index 0000000000..81495b2864 --- /dev/null +++ b/loopx/control_plane/coordination/goal_storage_initialization.py @@ -0,0 +1,64 @@ +"""Initialize a new Goal from its frozen canonical storage target.""" + +from __future__ import annotations + +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +from ...agent_registry import registered_agent_ids_for_goal +from ...registry import resolve_state_file +from ..effect_runtime import effect_runtime_result +from .authority_source_capture import ( + authority_registry_source, + build_runtime_shadow_source_snapshot, +) + +NEW_GOAL_STORAGE_METHOD = "coordination.local_authority.new_goal_storage" + + +def initialize_goal_storage_target( + runtime_root: Path, + goal: Mapping[str, Any], + *, + registry_path: Path | None = None, +) -> dict[str, Any] | None: + target = (goal.get("coordination") or {}).get("storage_target") + if target is None: + return None + request = { + "action": "initialize", + "runtime_root": str(runtime_root), + "goal_id": goal["id"], + "target": target, + } + if target.get("schema_version") == "loopx_new_goal_storage_target_v1": + if registry_path is None: + raise ValueError("Canonical creation requires its registered source") + state_path = resolve_state_file(Path(goal["repo"]), goal["state_file"]) + with authority_registry_source(registry_path) as witness: + request.update( + creation_operation_id=goal.get("creation_operation_id"), + source_snapshot={ + "state_path": str(state_path.resolve()), + "registry_source": { + **witness, + "registered_agents": registered_agent_ids_for_goal(dict(goal)), + }, + }, + ) + result = effect_runtime_result(NEW_GOAL_STORAGE_METHOD, request) + if result.get("source_capture_required") is not True: + return result + projection, snapshot = build_runtime_shadow_source_snapshot( + goal=goal, + runtime_root=runtime_root, + state_path=state_path, + registry_path=registry_path, + ) + request.update( + creation_operation_id=goal.get("creation_operation_id"), + projection=projection, + source_snapshot=snapshot, + ) + return effect_runtime_result(NEW_GOAL_STORAGE_METHOD, request) diff --git a/loopx/control_plane/coordination/handoff_mode_legacy_plan.ts b/loopx/control_plane/coordination/handoff_mode_legacy_plan.ts deleted file mode 100644 index 86e95f95a6..0000000000 --- a/loopx/control_plane/coordination/handoff_mode_legacy_plan.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** Typed compatibility plan. Python owns source locks/capture; TS owns the change. */ -import type {JsonObject} from "../effect_program.ts"; -import {requireJsonObject, requireStringLiteral} from "../runtime_decode.ts"; -import {HANDOFF_MODES, decideHandoffMode} from "./handoff_mode_policy.ts"; -import {handoffQuiescence, persistedHandoffMode, previousModeFields} from "./handoff_mode_facts.ts"; - -export const LEGACY_HANDOFF_PLAN_SCHEMA = "loopx_legacy_handoff_mode_plan_request_v0"; -const RESULT_SCHEMA = "loopx_legacy_handoff_mode_plan_result_v0"; - -/** Edit only the generated scalar field. All other bytes, including CRLF and - * Unicode line separators inside quoted metadata, remain untouched. A duplicate - * key has no single writable owner, so do not silently patch only one copy. */ -export function patchHandoffMode(text: string, requested: string): string { - const opening = /^---[ \t]*\r?\n/.exec(text)?.[0]; - if (!opening) throw new Error("state_frontmatter_missing"); - const fields: {start: number; end: number; ending: string}[] = []; - let close: number | undefined; - // JavaScript multiline ^ also recognizes U+2028/U+2029. The generated - // metadata protocol uses physical LF only, including inside JSON strings. - for (let start = opening.length; start <= text.length;) { - const newline = text.indexOf("\n", start); - const end = newline < 0 ? text.length : newline + 1; - const raw = text.slice(start, end); - const line = raw.replace(/\r?\n$/, ""); - if (/^---[ \t]*$/.test(line)) { close = start; break; } - if (/^[ \t]*handoff_mode[ \t]*:/.test(line)) { - fields.push({start, end, ending: /\r?\n$/.exec(raw)?.[0] ?? ""}); - } - if (newline < 0) break; - start = end; - } - if (close === undefined) throw new Error("state_frontmatter_missing"); - if (fields.length > 1) throw new Error("handoff_mode_duplicate_field"); - const field = fields[0]; - if (field) return text.slice(0, field.start) + `handoff_mode: ${requested}${field.ending}` + text.slice(field.end); - const newline = opening.endsWith("\r\n") ? "\r\n" : "\n"; - return text.slice(0, close) + `handoff_mode: ${requested}${newline}` + text.slice(close); -} - -export function planLegacyHandoffMode(value: unknown): JsonObject { - const input = requireJsonObject(value, "legacy handoff mode plan"); - if (input.schema_version !== LEGACY_HANDOFF_PLAN_SCHEMA) throw new Error("legacy handoff plan schema mismatch"); - const requested = requireStringLiteral(input.requested_mode, HANDOFF_MODES, "requested_mode"); - if (typeof input.frontmatter_text !== "string") throw new Error("frontmatter_text must be a string"); - const previous = persistedHandoffMode(input.previous_value); - const fields = {...previousModeFields(previous), handoff_mode: requested}; - if (requested === "legacy") return {schema_version: RESULT_SCHEMA, - ...decideHandoffMode(previous, requested, 0, 0), ...fields}; - // An identical valid mode grants no new behavior. Preserve the compatibility - // no-op even when leases exist or the legacy state has no frontmatter. - if (previous.kind === "valid" && previous.value === requested) { - return {schema_version: RESULT_SCHEMA, outcome: "no_change", code: "handoff_mode_unchanged", - changed: false, ...fields}; - } - if (input.todos === null && input.leases === null) return {schema_version: RESULT_SCHEMA, - outcome: "snapshot_required", ...fields, changed: false}; - if (!Array.isArray(input.todos) || !Array.isArray(input.leases)) throw new Error("complete Todo and lease facts required"); - let facts; - try { - facts = handoffQuiescence(input.todos.map(row => requireJsonObject(row, "Todo")), - input.leases.map(row => requireJsonObject(row, "lease")), String(input.observed_at)); - } catch (error) { - return {schema_version: RESULT_SCHEMA, outcome: "rejected", code: "invalid_handoff_mode_authority", - ...fields, changed: false, reason: String(error)}; - } - const plan = decideHandoffMode(previous, requested, facts.claimed_todos.length, facts.active_leases.length); - if (plan.outcome === "rejected") return {schema_version: RESULT_SCHEMA, ...plan, ...fields, - changed: false, ...facts}; - try { - return {schema_version: RESULT_SCHEMA, ...plan, ...fields, changed: true, - next_frontmatter_text: patchHandoffMode(input.frontmatter_text, requested)}; - } catch (error) { - if (!(error instanceof Error) || !["state_frontmatter_missing", "handoff_mode_duplicate_field"].includes(error.message)) throw error; - return {schema_version: RESULT_SCHEMA, outcome: "rejected", code: error.message, ...fields, changed: false}; - } -} diff --git a/loopx/control_plane/coordination/handoff_mode_runtime.ts b/loopx/control_plane/coordination/handoff_mode_runtime.ts index 84236670e2..e63f19cf46 100644 --- a/loopx/control_plane/coordination/handoff_mode_runtime.ts +++ b/loopx/control_plane/coordination/handoff_mode_runtime.ts @@ -5,7 +5,7 @@ import {requireAuthorityStoreId} from "./authority_store_codec.ts"; import {openLocalAuthorityStore, localAuthorityOpenFailure} from "./local_authority_provider.ts"; import {requireLocalAuthorityRuntimeRoot as runtimeRoot} from "./local_authority_provider.ts"; import {authorityStoreSourceAuthority as sourceAuthorityFor} from "./authority_store.ts"; -import {withCanonicalWriter} from "./local_authority_write.ts"; +import {canonicalWriteRouteFailure, withCanonicalWriter} from "./local_authority_write.ts"; import {ShadowManagementError} from "./shadow_management.ts"; import {executeHandoffModeSet, HANDOFF_MODE_SET_SCHEMA} from "./handoff_mode_transaction.ts"; import {planHandoffModeMigration, executeHandoffModeMigration} from "./handoff_mode_migration.ts"; @@ -24,11 +24,12 @@ export async function setLocalHandoffMode(value: unknown): Promise { return {...await executeHandoffModeSet(store, {goal_id: goalId, operation_id: input.operation_id as string, requested_mode: input.requested_mode as string, observed_at: input.observed_at as string, dry_run: input.dry_run as boolean}), ...evidence}; - }); + }, {businessWrite: "handoff_mode_set"}); } catch (error) { return {schema_version: "loopx_coordination_handoff_mode_set_result_v0", status: "failed", changed: false, reason_code: error instanceof ShadowManagementError ? error.reason_code : "handoff_mode_unavailable", - reason: error instanceof Error ? error.message : String(error), ...evidence, ...localAuthorityOpenFailure(error)}; + reason: error instanceof Error ? error.message : String(error), ...evidence, + ...localAuthorityOpenFailure(error), ...canonicalWriteRouteFailure(error)}; } } diff --git a/loopx/control_plane/coordination/legacy_writer_fence.py b/loopx/control_plane/coordination/legacy_writer_fence.py index bab4cf543e..a987000e98 100644 --- a/loopx/control_plane/coordination/legacy_writer_fence.py +++ b/loopx/control_plane/coordination/legacy_writer_fence.py @@ -1,22 +1,21 @@ -"""Cheap Python entry guard for the TypeScript-owned coordination fence. +"""Python guards for retained source maintenance and historical cleanup. -Legacy Todo writes still happen in Python during Stage 2C. The durable fence -itself and its validation semantics remain owned by the TypeScript control -plane; this module only avoids starting that runtime while no fence exists and -delegates every present-fence decision to the canonical handler. +Normal Markdown Todo writes are retired. The TypeScript control plane still +owns the durable promotion fence used by exact legacy lease release, recovery, +and prose-only source maintenance. This module avoids starting that runtime +while no fence exists and delegates every present-fence decision to the +canonical handler. """ from __future__ import annotations from collections.abc import Mapping -from contextlib import contextmanager import hashlib import re from pathlib import Path -from typing import Any, Iterator +from typing import Any from ..effect_runtime import effect_runtime_result -from ...file_lock import exclusive_cross_runtime_file_lock from ..projects.registry_codec import load_registry from ...paths import resolve_runtime_root from ...registry import find_registry_goal, registry_goals, resolve_state_file @@ -274,10 +273,10 @@ def require_legacy_coordination_write_allowed( ) -> None: """Allow the default path cheaply; delegate a present fence fail-closed. - A future promotion transaction must acquire the legacy writer's existing - mutation lock before engaging the fence. Calling this function while that - lock is held then closes the check/write race without introducing a second - Python authority for fence contents. + A promotion transaction acquires the retained mutation lock before + engaging the fence. Calling this function while that lock is held closes + the check/write race without introducing a second Python authority for + fence contents. """ fence_path = legacy_coordination_writer_fence_path( @@ -337,52 +336,3 @@ def require_legacy_coordination_write_allowed( code=code, payload={"write_check": result}, ) - - -@contextmanager -def legacy_todo_write_transaction( - registry_path: Path, - goal_id: str, - state_file: Path, - agent_id: str | None, - operation: str, - dry_run: bool, - *, - runtime_root: Path | None = None, -) -> Iterator[None]: - """Serialize promotion with one complete legacy Todo mutation. - - ``runtime_root`` is the effective runtime root of the CLI call (the - ``--runtime-root`` override when given) so the mutex and the fence check - share the same root as promotion and the observation hooks. Callers that - omit it keep the registry-derived root. - """ - - resolved_runtime_root = runtime_root or resolve_runtime_root( - load_registry(registry_path), - None, - registry_path=registry_path, - ) - with exclusive_cross_runtime_file_lock( - legacy_coordination_todo_lock_path( - runtime_root=resolved_runtime_root, - goal_id=goal_id, - ), - agent_id=agent_id, - operation="legacy_coordination_todo_write", - ), exclusive_cross_runtime_file_lock( - state_file, - agent_id=agent_id, - operation=operation, - ): - if not dry_run: - require_registry_source_write_allowed( - registry_path=registry_path, runtime_root=resolved_runtime_root, goal_id=goal_id, - state_file=state_file, - ) - require_shadow_primary_write_allowed(resolved_runtime_root, goal_id) - require_legacy_coordination_write_allowed( - runtime_root=resolved_runtime_root, - goal_id=goal_id, - ) - yield diff --git a/loopx/control_plane/coordination/local_authority.py b/loopx/control_plane/coordination/local_authority.py index 689fb1aea8..513238df44 100644 --- a/loopx/control_plane/coordination/local_authority.py +++ b/loopx/control_plane/coordination/local_authority.py @@ -10,7 +10,7 @@ from collections.abc import Mapping from copy import deepcopy from pathlib import Path -from typing import Any +from typing import Any, NoReturn from uuid import uuid4 from ...agent_registry import registered_agent_ids_from_registry @@ -27,7 +27,6 @@ TODO_ITEM_SCHEMA_VERSION, ) from .canonical_snapshot import read_canonical_snapshot -from .legacy_writer_fence import legacy_coordination_writer_fence_path LOCAL_COORDINATION_TODO_LIST_TIMEOUT_SECONDS = 15.0 @@ -37,7 +36,7 @@ LOCAL_COORDINATION_TODO_CLAIM_METHOD = "coordination.local_authority.todo_claim" -LOCAL_AUTHORITY_SOURCES = ("file_v0", "sqlite_v0") +LOCAL_AUTHORITY_SOURCES = ("file_v0", "sqlite_v0", "postgresql_v0") class LocalCoordinationAuthorityUnavailable(RuntimeError): @@ -68,7 +67,63 @@ def __init__(self, message: str, *, code: str, payload: Mapping[str, Any]) -> No super().__init__(message, code=code, payload=payload) +class CanonicalAuthorityRequired(LocalCoordinationAuthorityRejection): + """A normal write targeted a Goal that has not completed authority migration.""" + + def __init__( + self, + *, + goal_id: str, + operation: str, + payload: Mapping[str, Any] | None = None, + ) -> None: + evidence = { + "goal_id": goal_id, + "operation": operation, + "write_admitted": False, + "decision_read_from_provider": False, + "legacy_fallback_used": False, + "recovery": { + "action": "migrate_goal_authority", + "backup_required": True, + }, + **dict(payload or {}), + } + super().__init__( + "Canonical authority is required; back up and migrate this Goal before retrying", + code="canonical_authority_required", + payload=evidence, + ) + + +def raise_canonical_write_failure( + payload: Mapping[str, Any], + *, + goal_id: str, + operation: str, +) -> None: + """Map native route admission once without re-reading promotion state.""" + + if ( + payload.get("reason_code") == "canonical_authority_required" + or payload.get("error_code") == "canonical_authority_required" + ): + raise CanonicalAuthorityRequired( + goal_id=goal_id, + operation=operation, + payload=payload, + ) + + +def require_canonical_authority(*, goal_id: str, operation: str) -> NoReturn: + """Fail a compatibility-only path that has no native request to dispatch.""" + + raise CanonicalAuthorityRequired(goal_id=goal_id, operation=operation) + + def local_authority_is_promoted(*, runtime_root: Path, goal_id: str) -> bool: + from .legacy_writer_fence import legacy_coordination_writer_fence_path + fence_path = legacy_coordination_writer_fence_path( runtime_root=runtime_root, goal_id=goal_id, @@ -101,11 +156,8 @@ def claim_canonical_todo_if_promoted( task_lease_expected_version: int | None = None, project: Path | None = None, state_file: Path | None = None, -) -> dict[str, Any] | None: - """Route a post-cutover claim to the TypeScript transaction owner.""" - - if not local_authority_is_promoted(runtime_root=runtime_root, goal_id=goal_id): - return None +) -> dict[str, Any]: + """Route a claim to the canonical TypeScript transaction owner.""" with authority_registry_source(registry_path) as registry_source: registered = registered_agent_ids_from_registry(registry_path, goal_id) result = effect_runtime_result( @@ -146,6 +198,11 @@ def claim_canonical_todo_if_promoted( payload={"source_authority": "file_v0"}, ) payload = dict(result) + raise_canonical_write_failure( + payload, + goal_id=goal_id, + operation="todo_claim", + ) accepted = {"applied", "recovered", "replayed", "no_change", "planned"} if ( payload.get("status") == "failed" diff --git a/loopx/control_plane/coordination/local_authority_runtime.ts b/loopx/control_plane/coordination/local_authority_runtime.ts index ff6f0b41a0..8fd371f9e8 100644 --- a/loopx/control_plane/coordination/local_authority_runtime.ts +++ b/loopx/control_plane/coordination/local_authority_runtime.ts @@ -7,14 +7,13 @@ import {decodeTaskLeaseProof} from "./task_lease_proof.ts"; import {COORDINATION_TODO_ARCHIVE_RESULT_SCHEMA} from "./todo_archive.ts"; import {readCoordinationOwnership} from "./ownership_observation.ts"; import {executeTodoContinuation} from "./todo_continuation.ts"; -import {withCanonicalWriter} from "./local_authority_write.ts"; +import {canonicalWriteRouteFailure, withCanonicalWriter} from "./local_authority_write.ts"; import { ShadowManagementError, withShadowMaintenanceLock } from "./shadow_management.ts"; import { isAbsolute, join } from "node:path"; import {readFile, realpath} from "node:fs/promises"; import {createHash} from "node:crypto"; import type { JsonObject } from "../effect_program.ts"; -import {decodeMonitorPollObservation} from "../todos/monitor_metadata.ts"; import {decodeCompletionValidationRevision} from "../todos/completion_validation_revision.ts"; import {executeCoordinationMonitorPoll, COORDINATION_MONITOR_POLL_REQUEST_SCHEMA, COORDINATION_LEASED_MONITOR_POLL_REQUEST_SCHEMA, COORDINATION_WITNESSED_MONITOR_POLL_REQUEST_SCHEMA, COORDINATION_GUARDED_MONITOR_POLL_REQUEST_SCHEMA, COORDINATION_MONITOR_POLL_RESULT_SCHEMA} from "./todo_monitor_poll.ts"; @@ -482,12 +481,12 @@ export async function pollLocalCoordinationMonitor(value: unknown, lease_proof: proof, gate_scope_guard: input.gate_scope_guard == null ? false : requireBoolean(input.gate_scope_guard, "gate_scope_guard"), now: new Date(), }, authoritySourcesCurrent), ...evidence}; - }); + }, {businessWrite: "monitor_poll"}); } catch (error) { return {schema_version: COORDINATION_MONITOR_POLL_RESULT_SCHEMA, status: "failed", changed: false, reason_code: error instanceof ShadowManagementError ? error.reason_code : "invalid_local_monitor_poll_request", reason: error instanceof Error ? error.message : String(error), ...evidence, - ...localAuthorityOpenFailure(error)}; + ...localAuthorityOpenFailure(error), ...canonicalWriteRouteFailure(error)}; } } @@ -1083,7 +1082,7 @@ export async function claimLocalCoordinationTodo( decision_read_from_provider: true, legacy_fallback_used: false, }; - }); + }, {businessWrite: "todo_claim"}); } catch (error) { return { schema_version: COORDINATION_TODO_CLAIM_RESULT_SCHEMA, @@ -1094,6 +1093,7 @@ export async function claimLocalCoordinationTodo( decision_read_from_provider: true, legacy_fallback_used: false, ...localAuthorityOpenFailure(error), + ...canonicalWriteRouteFailure(error), }; } } @@ -1154,7 +1154,7 @@ export async function createLocalCoordinationTodo( ...result, ...providerEvidence, }; - }); + }, {businessWrite: "todo_create"}); } catch (error) { return { schema_version: COORDINATION_TODO_CREATE_RESULT_SCHEMA, @@ -1163,6 +1163,7 @@ export async function createLocalCoordinationTodo( reason: error instanceof Error ? error.message : "invalid Todo create request", ...providerEvidence, ...localAuthorityOpenFailure(error), + ...canonicalWriteRouteFailure(error), }; } } @@ -1248,7 +1249,12 @@ export async function updateLocalCoordinationTodo( lease_expected_version: optionalNonNegativeSafeInteger(input.lease_expected_version, "lease_expected_version"), patch: requireJsonObject(input.patch, "Todo update patch"), planning_intent: planningIntent, - ...(observationUpdate ? {monitor_observation: decodeMonitorPollObservation(input.monitor_observation)} : {}), + ...(observationUpdate ? { + monitor_observation: requireJsonObject( + input.monitor_observation, + "Monitor observation", + ), + } : {}), ...(completionUpdate ? {completion: requireJsonObject(input.completion, "Todo completion payload")} : {}), ...(validationRevisionUpdate ? {completion_validation_revision: decodeCompletionValidationRevision(input.completion_validation_revision)} : {}), @@ -1256,13 +1262,14 @@ export async function updateLocalCoordinationTodo( dry_run: input.dry_run as boolean, now: claimObservedAt(input.observed_at), }, authoritySourcesCurrent), ...providerEvidence}; - }); + }, {businessWrite: "todo_update"}); } catch (error) { return {schema_version: COORDINATION_TODO_UPDATE_RESULT_SCHEMA, status: "failed", changed: false, reason_code: error instanceof ShadowManagementError ? error.reason_code : "invalid_local_coordination_todo_update_request", reason: error instanceof Error ? error.message : "invalid Todo update request", ...providerEvidence, ...localAuthorityOpenFailure(error), + ...canonicalWriteRouteFailure(error), }; } } @@ -1368,7 +1375,7 @@ export async function terminalLifecycleLocalCoordinationTodo( dry_run: input.dry_run as boolean, now: claimObservedAt(input.observed_at), }, authoritySourcesCurrent, request => verifyReentrySettlement(root, request)), ...providerEvidence}; - }); + }, {businessWrite: "todo_terminal"}); } catch (error) { return {schema_version: COORDINATION_TODO_TERMINAL_LIFECYCLE_RESULT_SCHEMA, status: "failed", changed: false, @@ -1377,6 +1384,7 @@ export async function terminalLifecycleLocalCoordinationTodo( reason: error instanceof Error ? error.message : "invalid local Todo terminal request", ...providerEvidence, ...localAuthorityOpenFailure(error), + ...canonicalWriteRouteFailure(error), }; } } @@ -1419,7 +1427,7 @@ export async function archiveLocalCoordinationTodos( dry_run: input.dry_run as boolean, now, }), ...providerEvidence}; - }); + }, {businessWrite: "todo_archive"}); } catch (error) { return {schema_version: COORDINATION_TODO_ARCHIVE_RESULT_SCHEMA, status: "failed", changed: false, @@ -1428,6 +1436,7 @@ export async function archiveLocalCoordinationTodos( reason: error instanceof Error ? error.message : "invalid local Todo archive request", ...providerEvidence, ...localAuthorityOpenFailure(error), + ...canonicalWriteRouteFailure(error), }; } } @@ -1449,7 +1458,7 @@ export async function acknowledgeLocalCoordinationTodoArchive( return await withCanonicalWriter(root, goalId, false, async () => { const store = await openRuntimeStore(root, goalId, dependencies); return acknowledgeLocalArchiveAttempt(store, root, goalId, role, operationId); - }); + }, {businessWrite: "todo_archive"}); } catch (error) { return { schema_version: LOCAL_TODO_ARCHIVE_ACK_RESULT_SCHEMA, @@ -1458,6 +1467,7 @@ export async function acknowledgeLocalCoordinationTodoArchive( "invalid_local_coordination_todo_archive_ack_request", reason: error instanceof Error ? error.message : "invalid archive acknowledgement", ...localAuthorityOpenFailure(error), + ...canonicalWriteRouteFailure(error), }; } } @@ -1475,17 +1485,13 @@ export async function continueLocalTodo( return await withCanonicalWriter(root, goalId, false, async () => { const store = await openRuntimeStore(root, goalId, dependencies); evidence.source_authority = sourceAuthorityFor(store); - const fence = await loadLegacyCoordinationWriterFence(root, goalId); - if (fence.status !== "loaded") return {ok: false, status: "rejected", - reason_code: "continuation_requires_canonical_authority", - reason: "Use an explicitly promoted canonical authority; this command never promotes or falls back to Markdown"}; return {...await executeTodoContinuation(store, input), ...evidence}; - }); + }, {businessWrite: "todo_update"}); } catch (error) { return {ok: false, status: "failed", reason_code: error instanceof ShadowManagementError ? error.reason_code : "invalid_continuation_request", reason: error instanceof Error ? error.message : "Invalid continuation request", ...evidence, - ...localAuthorityOpenFailure(error)}; + ...localAuthorityOpenFailure(error), ...canonicalWriteRouteFailure(error)}; } } diff --git a/loopx/control_plane/coordination/local_authority_shadow_adapter.py b/loopx/control_plane/coordination/local_authority_shadow_adapter.py index 9e304babf4..0e7e36fa6a 100644 --- a/loopx/control_plane/coordination/local_authority_shadow_adapter.py +++ b/loopx/control_plane/coordination/local_authority_shadow_adapter.py @@ -19,10 +19,8 @@ LOCAL_AUTHORITY_SHADOW_COMMIT_ENTRY_RESULT_SCHEMA, LOCAL_AUTHORITY_SHADOW_READ_REQUEST_SCHEMA, LOCAL_AUTHORITY_SHADOW_READ_RESULT_SCHEMA, - LOCAL_AUTHORITY_SHADOW_TRANSACTION_EVIDENCE_SCHEMA, ) from .configuration import resolve_coordination_runtime_shadow_config -from .authority_source_capture import todo_partition_projector as todo_partition_projector from .shadow_management import shadow_management_state_path from .shadow_goal_scope import shadow_goal_scope from .local_authority_shadow_readback import ( @@ -44,9 +42,6 @@ # behind instead of guessing. # --------------------------------------------------------------------------- -LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA_V1 = ( - LOCAL_AUTHORITY_SHADOW_TRANSACTION_EVIDENCE_SCHEMA -) INLINE_DRAIN_MAX_ENTRIES = 16 INLINE_DRAIN_BUDGET_SECONDS = 2.0 INLINE_DRAIN_LOCK_TIMEOUT_SECONDS = 0.25 @@ -190,91 +185,16 @@ def drain_local_authority_shadow_outbox( return result -def capture_evidence( - *, - goal_id: str, - capture: outbox.CaptureOutcome, - drain: DrainResult | None, -) -> dict[str, Any]: - """Evidence v1 attached to a writer payload: capture facts plus drain facts. - - Every flag here is a measured fact of this write. ``source_candidate_compared`` - and ``parity_verdict`` stay negative until the verify step exists. - """ - - if capture.failure is not None: - outcome = "capture_failed" - reason_code: str | None = str(capture.failure.get("reason_code")) - elif capture.entry_id is None: - outcome = "no_transaction" - reason_code = capture.skipped_reason - elif drain is None or drain.outcome == "drain_deferred": - outcome = "drain_deferred" if drain is not None else "pending" - reason_code = drain.reason_code if drain is not None else None - else: - settled = drain.entry_outcome(capture.entry_id) - if settled is None: - outcome = "pending" - reason_code = drain.reason_code - else: - outcome = str(settled["outcome"]) - reason_code = settled.get("reason_code") - return { - "schema_version": LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA_V1, - "outcome": outcome, - "reason_code": reason_code, - "goal_id": goal_id, - "entry": { - "entry_id": capture.entry_id, - "partition": capture.partition, - "seq": capture.seq, - "partition_digest": capture.partition_digest, - "source_bytes_digest": capture.source_bytes_digest, - }, - "drain": None - if drain is None - else { - "outcome": drain.outcome, - "delivered": drain.delivered, - "replayed": drain.replayed, - "reclaimed_residue": drain.reclaimed_residue, - "pending_after": drain.pending_after, - "prepared_only_after": drain.prepared_only_after, - "stopped_at": drain.stopped_at, - "last_cursor": drain.last_cursor, - "provider_revision": drain.provider_revision, - "candidate_readback_verified": drain.candidate_readback_verified, - }, - "capture_kind": "source_transaction_outbox", - # Both flags are measured facts of this write: they are true only when - # a prepared/committed entry was actually recorded for it. A disabled - # or unchanged capture recorded nothing and claims nothing. - "source_transaction_correlated": capture.recorded, - "durable_source_outbox": capture.recorded, - "source_candidate_compared": False, - "parity_verdict": "not_evaluated", - "primary_authority": "legacy_local", - "candidate_provider": "file", - "candidate_read_for_decision": False, - "provider_to_local_writes": False, - "primary_writeback_preserved": True, - "store_identity": drain.store_identity if drain is not None else None, - } - - __all__ = [ "effective_runtime_root", "INLINE_DRAIN_BUDGET_SECONDS", "INLINE_DRAIN_LOCK_TIMEOUT_SECONDS", "INLINE_DRAIN_MAX_ENTRIES", "LOCAL_AUTHORITY_SHADOW_COMMIT_ENTRY_RESULT_SCHEMA", - "LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA_V1", "LOCAL_AUTHORITY_SHADOW_READ_REQUEST_SCHEMA", "LOCAL_AUTHORITY_SHADOW_READ_RESULT_SCHEMA", "RETENTION_PRESSURE_BYTES", "DrainResult", - "capture_evidence", - "todo_partition_projector", "drain_local_authority_shadow_outbox", "local_authority_shadow_status", "read_local_authority_shadow", diff --git a/loopx/control_plane/coordination/local_authority_shadow_outbox.py b/loopx/control_plane/coordination/local_authority_shadow_outbox.py index b6b2297bdf..8fd656f370 100644 --- a/loopx/control_plane/coordination/local_authority_shadow_outbox.py +++ b/loopx/control_plane/coordination/local_authority_shadow_outbox.py @@ -1,51 +1,9 @@ -"""Durable, transaction-bound outbox for the local authority shadow. +"""Read-only compatibility exports for retained shadow outbox history.""" -A legacy writer that changes coordination facts records, inside the lock it -already holds, a two-phase entry for exactly the state that lock guards (one -*partition*): a ``prepared`` file computed from the bytes about to be written, -then a ``committed`` marker after the primary write returns. Nothing in the -lock talks to the TypeScript runtime. A later drain (same process after the -lock, or an operator command) turns each committed entry into exactly one -candidate-store transaction whose ``operation_id`` is the entry id. - -Prepare failures return typed evidence for the transaction owner to reject an -active-lineage write before changing primary bytes. Marker failures preserve -prepared evidence; candidate delivery happens after releasing primary locks. -The native drain owns cursor publication and receipt-proven cleanup; Python -retains source capture; historical inventory/decoding has an independent IO owner. -""" - -from __future__ import annotations - -import json -import os -import uuid -from collections.abc import Callable, Mapping -from dataclasses import dataclass -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -from .local_authority_shadow_projection import ( - TODO_PARTITION, - canonical_value, - partition_digest, - text_digest, -) -from .coordination_state_contract_generated import ( - LOCAL_AUTHORITY_SHADOW_READ_REQUEST_SCHEMA, - LOCAL_AUTHORITY_SHADOW_READ_RESULT_SCHEMA, -) -from .shadow_management import ( - read_shadow_capture_binding, - shadow_maintenance_lock_target, -) - - -# Retained imports preserve existing writer/tool callers; the definitions and -# read-only CLI no longer depend on this producer. from .local_authority_shadow_history import ( DRAIN_CURSOR_SCHEMA as DRAIN_CURSOR_SCHEMA, + ENTRY_ID_PREFIX as ENTRY_ID_PREFIX, + MAX_OUTBOX_SEQUENCE as MAX_OUTBOX_SEQUENCE, OUTBOX_COMMIT_SCHEMA as OUTBOX_COMMIT_SCHEMA, OUTBOX_ENTRY_SCHEMA as OUTBOX_ENTRY_SCHEMA, SOURCE_MARKDOWN as SOURCE_MARKDOWN, @@ -53,483 +11,42 @@ SOURCE_TASK_LEASE as SOURCE_TASK_LEASE, WRITER_RUNTIME_PYTHON as WRITER_RUNTIME_PYTHON, WRITER_RUNTIME_TYPESCRIPT as WRITER_RUNTIME_TYPESCRIPT, - ENTRY_ID_PREFIX as ENTRY_ID_PREFIX, - MAX_OUTBOX_SEQUENCE as MAX_OUTBOX_SEQUENCE, OutboxEntry as OutboxEntry, OutboxError as OutboxError, - outbox_root as outbox_root, - partition_directory as partition_directory, - lease_directory as lease_directory, - entry_identity as entry_identity, + cursor_path as cursor_path, + decode_cursor as decode_cursor, entry_file_name as entry_file_name, + entry_identity as entry_identity, + lease_directory as lease_directory, list_entries as list_entries, - cursor_path as cursor_path, + outbox_root as outbox_root, + outbox_summary as outbox_summary, + partition_directory as partition_directory, read_cursor as read_cursor, - decode_cursor as decode_cursor, - retired_residue as retired_residue, record_source_ref as record_source_ref, + retired_residue as retired_residue, runtime_root_digest as runtime_root_digest, - outbox_summary as outbox_summary, - _ENTRY_FILE, ) -TodoPartitionProjector = Callable[[str], dict[str, Any]] -"""Active-state Markdown text -> ``{"handoff_mode": str, "todos": [compact...]}``.""" - - -def utc_now_text() -> str: - return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") - - -def drain_lock_target(runtime_root: Path, goal_id: str) -> Path: - return shadow_maintenance_lock_target(runtime_root, goal_id) - - -def _fsync_directory(directory: Path) -> None: - if os.name == "nt": - return - descriptor = os.open(directory, os.O_RDONLY) - try: - os.fsync(descriptor) - finally: - os.close(descriptor) - - -def durable_write_json(path: Path, payload: Mapping[str, Any]) -> None: - """Temp file in the same directory, fsync, atomic replace, directory fsync.""" - - path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) - temporary = path.with_name(f"{path.name}.tmp-{os.getpid()}-{uuid.uuid4().hex}") - data = json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=1).encode( - "utf-8" - ) - descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) - try: - view = memoryview(data) - while view: - written = os.write(descriptor, view) - view = view[written:] - os.fsync(descriptor) - finally: - os.close(descriptor) - try: - os.replace(temporary, path) - _fsync_directory(path.parent) - finally: - if temporary.exists(): - temporary.unlink(missing_ok=True) - - -def _require_binding_goal_ref( - binding: Mapping[str, Any], - *, - goal_id: str, - goal_ref: Mapping[str, str] | None, -) -> None: - bound = binding.get("goal_ref") - if goal_ref is None: - if bound is not None: - raise OutboxError( - "goal_instance_id_missing", - "exact shadow operation requires the source GoalRef", - ) - return - if bound is None: - raise OutboxError( - "legacy_goal_binding", - "exact shadow operation cannot infer a legacy binding", - ) - if bound != dict(goal_ref) or bound.get("goal_id") != goal_id: - raise OutboxError( - "stale_goal_instance", - "shadow operation belongs to another Goal instance", - ) - - -def _proved_sequence( - runtime_root: Path, - goal_id: str, - partition: str, - goal_ref: Mapping[str, str] | None, -) -> int: - """Read settled progress while the primary lock prevents management changes. - - This exceptional missing-cursor path owns neither M nor cursor writes. The - native boundary validates the complete bounded history without initializing - a missing store. Primary exclusion keeps its lineage stable during the read. - """ - from ..effect_runtime import effect_runtime_result - - binding = read_shadow_capture_binding(runtime_root, goal_id) - if binding["status"] != "active": - raise OutboxError( - "bootstrap_required", "sequence recovery needs an active lineage" - ) - _require_binding_goal_ref( - binding["binding"], goal_id=goal_id, goal_ref=goal_ref - ) - view = effect_runtime_result( - "coordination.runtime_shadow.outbox_read", - { - "schema_version": LOCAL_AUTHORITY_SHADOW_READ_REQUEST_SCHEMA, - "runtime_root": str(runtime_root), - "goal_id": goal_id, - "scan_limit": 0, - "read_model": "proof", - }, - timeout=15.0, - ) - proof = view.get("proof") if isinstance(view, dict) else None - if ( - not isinstance(view, dict) - or view.get("schema_version") != LOCAL_AUTHORITY_SHADOW_READ_RESULT_SCHEMA - or view.get("status") != "loaded" - or not isinstance(proof, dict) - or proof.get("capture_lineage_id") != binding["binding"]["capture_lineage_id"] - or view.get("store_identity") != binding["binding"]["store_identity"] - or read_shadow_capture_binding(runtime_root, goal_id) != binding - ): - raise OutboxError( - "outbox_sequence_unproved", "missing cursor has no stable history proof" - ) - sequences = proof.get("last_sequences") - seq = sequences.get(partition) if isinstance(sequences, dict) else None - if type(seq) is not int or not 0 <= seq <= MAX_OUTBOX_SEQUENCE: - raise OutboxError( - "outbox_sequence_unproved", "history has no verified partition progress" - ) - return seq - - -def next_seq( - directory: Path, - *, - runtime_root: Path | None = None, - goal_id: str | None = None, - goal_ref: Mapping[str, str] | None = None, -) -> int: - """Allocate after visible files and the cursor hint; drain proves continuity.""" - - highest = 0 - if directory.is_dir(): - for path in directory.iterdir(): - match = _ENTRY_FILE.match(path.name) - if match is not None: - highest = max(highest, int(match.group("seq"))) - cursor = read_cursor(directory) - if cursor is not None: - highest = max(highest, int(cursor.get("last_seq") or 0)) - elif runtime_root is not None and goal_id is not None: - highest = max( - highest, - _proved_sequence( - runtime_root, - goal_id, - directory.name, - goal_ref, - ), - ) - if highest >= MAX_OUTBOX_SEQUENCE: - raise OutboxError("outbox_sequence_exhausted", "outbox sequence is exhausted") - return highest + 1 - - -def _writer( - write_class: str, *, runtime: str, operation_id: str | None -) -> dict[str, Any]: - return { - "runtime": runtime, - "write_class": write_class, - "operation_id": operation_id, - } - - -def _entry_record( - *, - goal_id: str, - partition: str, - seq: int, - entry_id: str, - writer: Mapping[str, Any], - source: Mapping[str, Any], - source_root_digest: str, - capture_lineage_id: str, - projection: Mapping[str, Any] | None, - digest: str | None, -) -> dict[str, Any]: - return { - "schema_version": OUTBOX_ENTRY_SCHEMA, - "goal_id": goal_id, - "partition": partition, - "seq": seq, - "entry_id": entry_id, - "writer": dict(writer), - "source": dict(source), - "source_root_digest": source_root_digest, - "capture_lineage_id": capture_lineage_id, - "projection": canonical_value(dict(projection)) - if projection is not None - else None, - "partition_digest": digest, - "prepared_at": utc_now_text(), - } - - -@dataclass -class CaptureOutcome: - """What the capture did for one primary write (attached to its evidence).""" - - entry_id: str | None = None - partition: str | None = None - seq: int | None = None - partition_digest: str | None = None - source_bytes_digest: str | None = None - skipped_reason: str | None = None - failure: dict[str, Any] | None = None - - @property - def recorded(self) -> bool: - return self.entry_id is not None and self.failure is None - - -class TodoPartitionCapture: - """Two-phase capture of the todos partition inside the active-state lock. - - ``begin`` returns an inert capture when the goal has no shadow binding, so - default-off writers create no directory, lock, or file. - """ - - def __init__( - self, - *, - enabled: bool, - registry_path: Path | None, - runtime_root: Path | None, - goal_id: str, - state_path: Path | None, - write_class: str, - original_text: str, - projector: TodoPartitionProjector | None, - goal_ref: Mapping[str, str] | None = None, - ) -> None: - self._enabled = enabled - self._registry_path = registry_path - self._runtime_root = runtime_root - self._goal_id = goal_id - self._state_path = state_path - self._write_class = write_class - self._original_digest = text_digest(original_text) - self._original_text = original_text - self._projector = projector - self._goal_ref = dict(goal_ref) if goal_ref is not None else None - self._directory = ( - partition_directory(runtime_root, goal_id, TODO_PARTITION) - if enabled and runtime_root is not None - else None - ) - self._seq: int | None = None - self._entry_id: str | None = None - self._lineage_id: str | None = None - self.outcome = CaptureOutcome(partition=TODO_PARTITION if enabled else None) - - @classmethod - def begin( - cls, - *, - enabled: bool, - registry_path: Path | None = None, - runtime_root: Path | None, - goal_id: str, - state_path: Path | None, - write_class: str, - original_text: str, - projector: TodoPartitionProjector | None, - goal_ref: Mapping[str, str] | None = None, - ) -> TodoPartitionCapture: - """``projector`` maps active-state text to the todos partition projection. - - It is injected so this module stays free of the Markdown parser; the - adapter supplies the production projector. - """ - - return cls( - enabled=enabled, - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - state_path=state_path, - write_class=write_class, - original_text=original_text, - projector=projector, - goal_ref=goal_ref, - ) - - @property - def enabled(self) -> bool: - return self._enabled and self._directory is not None - - @property - def goal_ref(self) -> dict[str, str] | None: - return dict(self._goal_ref) if self._goal_ref is not None else None - - @property - def registry_path(self) -> Path | None: - return self._registry_path - - def skip(self, reason: str) -> None: - """Record why this writer deliberately did not open a transaction.""" - - if self.enabled and self.outcome.entry_id is None: - self.outcome.skipped_reason = reason - - def _project(self, state_text: str) -> dict[str, Any]: - if self._projector is None: - raise OutboxError( - "outbox_prepare_failed", "a todo partition projector is required" - ) - projection = self._projector(state_text) - if set(projection) != {"handoff_mode", "todos"}: - raise OutboxError( - "outbox_prepare_failed", "projector must return {handoff_mode, todos}" - ) - return projection - - def _fail(self, reason_code: str, error: BaseException) -> None: - self.outcome.failure = { - "reason_code": reason_code, - "error_class": error.__class__.__name__, - } - - def prepare(self, new_text: str) -> None: - """Prepare capture for the exact Markdown bytes about to be written.""" - - if not self.enabled or self._directory is None or self._runtime_root is None: - self.outcome.skipped_reason = "shadow_disabled" - return - binding_view = read_shadow_capture_binding(self._runtime_root, self._goal_id) - if binding_view["status"] != "active": - self.outcome.skipped_reason = str( - binding_view.get("reason_code") or "bootstrap_required" - ) - return - binding = binding_view["binding"] - try: - _require_binding_goal_ref( - binding, - goal_id=self._goal_id, - goal_ref=self._goal_ref, - ) - except OutboxError as error: - self._fail( - error.reason_code, - error, - ) - return - self._lineage_id = str(binding["capture_lineage_id"]) - source_root_digest = str(binding["source_root_digest"]) - try: - projection = self._project(new_text) - digest = partition_digest(projection) - previous_digest = partition_digest(self._project(self._original_text)) - if digest == previous_digest: - self.outcome.skipped_reason = "partition_unchanged" - return - source_ref = text_digest(new_text) - bytes_digest = source_ref - source_kind = SOURCE_MARKDOWN - seq = next_seq( - self._directory, - runtime_root=self._runtime_root, - goal_id=self._goal_id, - goal_ref=self._goal_ref, - ) - entry_id = entry_identity( - goal_id=self._goal_id, - partition=TODO_PARTITION, - seq=seq, - source_ref=source_ref, - capture_lineage_id=self._lineage_id, - source_root_digest=source_root_digest, - ) - record = _entry_record( - goal_id=self._goal_id, - partition=TODO_PARTITION, - seq=seq, - entry_id=entry_id, - writer=_writer( - self._write_class, - runtime=WRITER_RUNTIME_PYTHON, - operation_id=None, - ), - source={ - "kind": source_kind, - "previous_bytes_digest": self._original_digest, - "previous_partition_digest": previous_digest, - "bytes_digest": bytes_digest, - "lease": None, - "event_id": None, - }, - source_root_digest=source_root_digest, - capture_lineage_id=self._lineage_id, - projection=projection, - digest=digest, - ) - durable_write_json( - self._directory / entry_file_name(seq, entry_id, "prepared"), - record, - ) - except Exception as error: # noqa: BLE001 - the transaction owner enforces active preparation - self._fail("outbox_prepare_failed", error) - return - self._seq = seq - self._entry_id = entry_id - self.outcome.entry_id = entry_id - self.outcome.seq = seq - self.outcome.partition_digest = digest - self.outcome.source_bytes_digest = bytes_digest - - def committed(self) -> None: - """Mark the prepared entry committed after the primary write returned.""" - - if self._seq is None or self._entry_id is None or self._directory is None: - return - if self.outcome.failure is not None: - return - marker: dict[str, Any] = { - "schema_version": OUTBOX_COMMIT_SCHEMA, - "entry_id": self._entry_id, - "capture_lineage_id": self._lineage_id, - "committed_at": utc_now_text(), - } - try: - durable_write_json( - self._directory - / entry_file_name(self._seq, self._entry_id, "committed"), - marker, - ) - except Exception as error: # noqa: BLE001 - the primary write already landed - self._fail("outbox_commit_marker_failed", error) - - __all__ = [ "DRAIN_CURSOR_SCHEMA", + "ENTRY_ID_PREFIX", + "MAX_OUTBOX_SEQUENCE", "OUTBOX_COMMIT_SCHEMA", "OUTBOX_ENTRY_SCHEMA", "SOURCE_MARKDOWN", "SOURCE_STATE_EVENT_LOG", "SOURCE_TASK_LEASE", - "CaptureOutcome", + "WRITER_RUNTIME_PYTHON", + "WRITER_RUNTIME_TYPESCRIPT", "OutboxEntry", "OutboxError", - "TodoPartitionCapture", - "TodoPartitionProjector", - "drain_lock_target", - "durable_write_json", + "cursor_path", + "decode_cursor", "entry_file_name", "entry_identity", "lease_directory", "list_entries", - "next_seq", "outbox_root", "outbox_summary", "partition_directory", @@ -537,5 +54,4 @@ def committed(self) -> None: "record_source_ref", "retired_residue", "runtime_root_digest", - "utc_now_text", ] diff --git a/loopx/control_plane/coordination/local_authority_shadow_outbox.ts b/loopx/control_plane/coordination/local_authority_shadow_outbox.ts index 974d2de82b..0e11e77d62 100644 --- a/loopx/control_plane/coordination/local_authority_shadow_outbox.ts +++ b/loopx/control_plane/coordination/local_authority_shadow_outbox.ts @@ -1,19 +1,10 @@ import { createHash } from "node:crypto"; -import { readdir, readFile } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; import { join } from "node:path"; import type { JsonObject } from "../effect_program.ts"; -import { durableWriteJson } from "../effect_runtime_io.ts"; -import { authorityUnicodeCompare, canonicalAuthorityBytes } from "./authority_store_codec.ts"; -import { - requireShadowBindingGoalRef, - requireShadowCaptureBinding, - shadowBindingGoalRef, - ShadowManagementError, -} from "./shadow_management.ts"; import { parseExactGoalRef } from "../goals/goal_instance_identity.ts"; -import { outboxEntryIdentity, OUTBOX_ENTRY_FILE_PATTERN } from "./local_authority_shadow_identity.ts"; -import { readProvenShadowSequence } from "./local_authority_shadow.ts"; +import { outboxEntryIdentity } from "./local_authority_shadow_identity.ts"; import { LOCAL_AUTHORITY_SHADOW_BINDING_SCHEMA, LOCAL_AUTHORITY_SHADOW_EXACT_BINDING_SCHEMA, @@ -41,9 +32,6 @@ export { LOCAL_AUTHORITY_SHADOW_OUTBOX_COMMIT_SCHEMA, LOCAL_AUTHORITY_SHADOW_OUTBOX_ENTRY_SCHEMA, }; -export const LEASE_PARTITION = "leases"; -const LEASE_FILE = /^[A-Za-z0-9_.-]+\.json$/u; -const LEASE_SOURCE_FIELDS = ["todo_id", "version", "lease_epoch", "status", "updated_at"] as const; export interface LegacyLocalAuthorityShadowBinding { schema_version: typeof LOCAL_AUTHORITY_SHADOW_BINDING_SCHEMA; @@ -95,11 +83,6 @@ export function sha256Digest(input: Uint8Array | string): string { return `sha256:${createHash("sha256").update(input).digest("hex")}`; } -/** Digest of the exact bytes `atomicWriteJson` persists for a lease record. */ -export function leaseRecordDigest(record: JsonObject): string { - return sha256Digest(`${JSON.stringify(record, null, 2)}\n`); -} - export function outboxPartitionDirectory( runtimeRoot: string, goalId: string, @@ -171,245 +154,3 @@ export function decodeOutboxCursor(value: unknown, partition: string): JsonObjec parsed.toISOString().slice(0, 19) !== timestamp.slice(0, 19)) throw invalid(); return record; } - -async function nextSeq( - directory: string, - runtimeRoot: string, - goalId: string, - lineageId: string, - goalRef: unknown, -): Promise { - let highest = 0; - try { - for (const name of await readdir(directory)) { - const match = OUTBOX_ENTRY_FILE_PATTERN.exec(name); - if (match !== null) highest = Math.max(highest, Number(match[1])); - } - } catch (error) { - if (!isMissing(error)) throw error; - } - const cursor = await readOutboxCursor(directory, LEASE_PARTITION); - const proved = cursor === null - ? await readProvenShadowSequence(runtimeRoot, goalId, LEASE_PARTITION, lineageId, goalRef) - : cursor.last_seq as number; - highest = Math.max(highest, proved); - if (highest >= MAX_OUTBOX_SEQUENCE) throw new Error("outbox sequence exhausted"); - return highest + 1; -} - -/** - * Read the lease partition for one capture. - * - * The legacy lease directory is append-retained history: archiving a Todo - * leaves its released lease file on disk. The source projection models only - * the current Todo graph, so it drops a lease whose Todo is no longer part of - * that graph (see `build_todo_runtime_shadow_projection`). Capture must apply - * the same rule, or archiving a Todo after a released lease writes an orphan - * edge into the candidate head and parity reports `shadow_projection_drift`. - * - * `activeTodoIds` is the current Todo graph supplied by the caller; `null` - * means the caller could not read it, and the capture stays strict rather - * than guessing a projection. - */ -async function readLeasePartition( - leaseDirectory: string, - plannedStem: string, - plannedLease: JsonObject | null, - activeTodoIds: ReadonlySet | null, -): Promise { - const records = new Map(); - let names: string[] = []; - try { - names = await readdir(leaseDirectory); - } catch (error) { - if (!isMissing(error)) throw error; - } - for (const name of names) { - if (!LEASE_FILE.test(name) || name.startsWith(".")) continue; - const stem = name.slice(0, -".json".length); - if (stem === plannedStem) continue; - if (activeTodoIds !== null && !activeTodoIds.has(stem)) continue; - const raw: unknown = JSON.parse(await readFile(join(leaseDirectory, name), "utf8")); - if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) { - records.set(stem, raw as JsonObject); - } - } - if (plannedLease !== null) records.set(plannedStem, plannedLease); - const stems = [...records.keys()]; - stems.sort(authorityUnicodeCompare); - return stems.map((stem) => ({ file_stem: stem, record: records.get(stem) as JsonObject })); -} - -function leaseSourceFacts(record: JsonObject | null): JsonObject | null { - if (record === null) return null; - const facts: JsonObject = {}; - for (const field of LEASE_SOURCE_FIELDS) { - const value = record[field]; - if (value === undefined) continue; - if (typeof value === "string" || typeof value === "number" || value === null) { - facts[field] = value; - } - } - return facts; -} - -export interface LeaseOutboxCaptureInput { - runtime_root: string; - goal_id: string; - lease_directory: string; - write_class: string; - operation_id: string | null; - previous_lease: JsonObject | null; - planned_lease: JsonObject; - /** - * Current Todo graph for the goal, used to drop leases orphaned by an - * archived Todo exactly as the source projection does. `null` keeps the - * strict pre-existing behavior. - */ - active_todo_ids: readonly string[] | null; - goal_ref?: unknown; -} - -export interface LeaseOutboxCapture { - entry_id: string | null; - seq: number | null; - source_bytes_digest: string | null; - failure: { reason_code: string; error_class: string } | null; - skipped_reason?: string; - /** Write the committed marker after the lease record landed. Never throws. */ - commit(): Promise; -} - -function failureOf(reasonCode: string, error: unknown): { reason_code: string; error_class: string } { - return { - reason_code: reasonCode, - error_class: error instanceof Error ? error.constructor.name : typeof error, - }; -} - -/** - * Record a prepared lease-partition entry for the record about to be written. - * - * Call inside the lease lock, right before `atomicWriteJson(leasePath, planned)`; - * call `commit()` right after it returns. The primary owner must reject an - * active mutation when preparation returns a failure. - */ -export async function beginLeaseOutboxEntry( - input: LeaseOutboxCaptureInput, -): Promise { - const inert: LeaseOutboxCapture = { - entry_id: null, - seq: null, - source_bytes_digest: null, - failure: null, - async commit(): Promise {}, - }; - const plannedStem = typeof input.planned_lease.todo_id === "string" - ? input.planned_lease.todo_id - : ""; - if (plannedStem.length === 0) { - return { ...inert, failure: { reason_code: "outbox_prepare_failed", error_class: "MissingTodoId" } }; - } - const directory = outboxPartitionDirectory(input.runtime_root, input.goal_id, LEASE_PARTITION); - try { - const binding = await requireShadowCaptureBinding(input.runtime_root, input.goal_id); - const boundGoalRef = shadowBindingGoalRef(binding); - if (boundGoalRef === null && input.goal_ref !== undefined && input.goal_ref !== null) { - throw new ShadowManagementError("legacy_goal_binding"); - } - if (boundGoalRef !== null) { - requireShadowBindingGoalRef(binding, input.goal_ref, input.goal_id); - } - if (input.previous_lease !== null && - canonicalAuthorityBytes(input.previous_lease).equals(canonicalAuthorityBytes(input.planned_lease))) { - return { ...inert, skipped_reason: "partition_unchanged" }; - } - const activeTodoIds = input.active_todo_ids === null - ? null - : new Set(input.active_todo_ids); - const projection = { leases: await readLeasePartition(input.lease_directory, plannedStem, input.planned_lease, activeTodoIds) }; - const previousRecords = await readLeasePartition(input.lease_directory, plannedStem, input.previous_lease, activeTodoIds); - for (const item of [...previousRecords, ...projection.leases]) { - const record = item.record as JsonObject; - if (record.goal_id !== input.goal_id || record.todo_id !== item.file_stem) { - throw new Error("lease source identity does not match its partition"); - } - } - const previousPartitionDigest = sha256Digest(canonicalAuthorityBytes({ - leases: previousRecords.map((item) => item.record), - })); - const bytesDigest = leaseRecordDigest(input.planned_lease); - const seq = await nextSeq( - directory, - input.runtime_root, - input.goal_id, - binding.capture_lineage_id, - input.goal_ref ?? null, - ); - const sourceRootDigest = binding.source_root_digest; - const entryId = outboxEntryIdentity(input.goal_id, LEASE_PARTITION, seq, bytesDigest, - binding.capture_lineage_id, sourceRootDigest); - const entry: JsonObject = { - schema_version: LOCAL_AUTHORITY_SHADOW_OUTBOX_ENTRY_SCHEMA, - goal_id: input.goal_id, - partition: LEASE_PARTITION, - seq, - entry_id: entryId, - capture_lineage_id: binding.capture_lineage_id, - writer: { - runtime: "typescript", - write_class: input.write_class, - operation_id: input.operation_id, - }, - source: { - kind: "task_lease_record", - previous_partition_digest: previousPartitionDigest, - previous_bytes_digest: input.previous_lease === null - ? null - : leaseRecordDigest(input.previous_lease), - bytes_digest: bytesDigest, - lease: leaseSourceFacts(input.planned_lease), - previous_lease: leaseSourceFacts(input.previous_lease), - event_id: null, - }, - source_root_digest: sourceRootDigest, - projection, - partition_digest: null, - prepared_at: new Date().toISOString(), - }; - await durableWriteJson(join(directory, outboxEntryFileName(seq, entryId, "prepared")), entry); - let committed = false; - const capture: LeaseOutboxCapture = { - entry_id: entryId, - seq, - source_bytes_digest: bytesDigest, - failure: null, - async commit(): Promise { - if (committed) return; - committed = true; - try { - await durableWriteJson( - join(directory, outboxEntryFileName(seq, entryId, "committed")), - { - schema_version: LOCAL_AUTHORITY_SHADOW_OUTBOX_COMMIT_SCHEMA, - entry_id: entryId, - capture_lineage_id: binding.capture_lineage_id, - committed_at: new Date().toISOString(), - }, - ); - } catch (error) { - capture.failure = failureOf("outbox_commit_marker_failed", error); - } - }, - }; - return capture; - } catch (error) { - if (error instanceof ShadowManagementError && error.code === "bootstrap_required") { - return { ...inert, skipped_reason: "bootstrap_required" }; - } - if (error instanceof ShadowManagementError) { - return { ...inert, failure: failureOf(error.code, error) }; - } - return { ...inert, failure: failureOf("outbox_prepare_failed", error) }; - } -} diff --git a/loopx/control_plane/coordination/local_authority_write.ts b/loopx/control_plane/coordination/local_authority_write.ts index 864da1ba9b..81333cc984 100644 --- a/loopx/control_plane/coordination/local_authority_write.ts +++ b/loopx/control_plane/coordination/local_authority_write.ts @@ -1,4 +1,6 @@ import {withFileMutationLock} from "../effect_runtime_io.ts"; +import type {JsonObject} from "../effect_program.ts"; +import {loadLegacyCoordinationWriterFence} from "./legacy_writer_fence.ts"; import {requireShadowPrimaryWriteAllowed, shadowMaintenanceLockPath} from "./shadow_management.ts"; // File-v0 still verifies and republishes its retained history on each write. @@ -6,11 +8,93 @@ import {requireShadowPrimaryWriteAllowed, shadowMaintenanceLockPath} from "./sha // section; the provider CAS and hard lease remain the decision authority. const CANONICAL_WRITER_LOCK_TIMEOUT_MS = 30_000; +export type CanonicalBusinessWrite = + | "todo_claim" + | "todo_create" + | "todo_update" + | "todo_terminal" + | "todo_archive" + | "monitor_poll" + | "team_plan" + | "handoff_mode_set" + | "task_lease_acquire" + | "task_lease_renew" + | "task_lease_transfer" + | "task_lease_release" + | "goal_create"; + +export class CanonicalWriteRouteError extends Error { + readonly code: "canonical_authority_required" | "canonical_authority_fence_invalid"; + readonly payload: JsonObject; + + constructor(code: CanonicalWriteRouteError["code"], goalId: string, operation: CanonicalBusinessWrite, reason: string) { + super(reason); + this.name = "CanonicalWriteRouteError"; + this.code = code; + this.payload = { + status: "failed", + reason_code: code, + goal_id: goalId, + operation, + write_admitted: false, + decision_read_from_provider: false, + legacy_fallback_used: false, + recovery: { + action: code === "canonical_authority_required" + ? "migrate_goal_authority" + : "repair_canonical_authority_fence", + backup_required: true, + }, + }; + } +} + +export function canonicalWriteRouteFailure(error: unknown): JsonObject { + return error instanceof CanonicalWriteRouteError ? {...error.payload, reason: error.message} : {}; +} + +interface CanonicalWriterOptions { + businessWrite?: CanonicalBusinessWrite; +} + +/** Call only while holding the Goal maintenance lock. */ +export async function requireCanonicalBusinessWrite( + root: string, + goalId: string, + operation: CanonicalBusinessWrite, +): Promise { + const fence = await loadLegacyCoordinationWriterFence(root, goalId); + if (fence.status === "missing") { + throw new CanonicalWriteRouteError( + "canonical_authority_required", + goalId, + operation, + "Canonical authority is required; back up and migrate this Goal before retrying", + ); + } + if (fence.status === "failed") { + throw new CanonicalWriteRouteError( + "canonical_authority_fence_invalid", + goalId, + operation, + fence.reason, + ); + } +} + /** Canonical command writers share the maintenance guard; provider CAS owns state. */ -export async function withCanonicalWriter(root: string, goalId: string, dryRun: boolean, write: () => Promise): Promise { - if (dryRun) return await write(); +export async function withCanonicalWriter( + root: string, + goalId: string, + _dryRun: boolean, + write: () => Promise, + options: CanonicalWriterOptions = {}, +): Promise { return await withFileMutationLock(shadowMaintenanceLockPath(root, goalId), async () => { await requireShadowPrimaryWriteAllowed(root, goalId); + if (options.businessWrite !== undefined) { + await requireCanonicalBusinessWrite(root, goalId, options.businessWrite); + } return await write(); }, CANONICAL_WRITER_LOCK_TIMEOUT_MS); } diff --git a/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py b/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py deleted file mode 100644 index 167407d60b..0000000000 --- a/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py +++ /dev/null @@ -1,206 +0,0 @@ -"""Transaction-capture orchestration for legacy Todo and lease writers.""" - -from __future__ import annotations - -from collections.abc import Mapping -from pathlib import Path -from typing import Any - -from . import local_authority_shadow_outbox as outbox -from .local_authority_shadow_projection import LEASE_PARTITION -from .legacy_writer_fence import ( - ActiveStateAuthorityMutationError as ActiveStateAuthorityMutationError, - require_prose_state_write_allowed as require_prose_state_write_allowed, -) -from .configuration import resolve_coordination_runtime_shadow_config -from .shadow_goal_scope import shadow_goal_scope -from .shadow_management import ShadowManagementError, read_shadow_capture_binding, require_shadow_primary_write_allowed - - -def begin_todo_runtime_shadow_capture( - *, - registry_path: Path, - runtime_root: Path, - goal_id: str, - state_path: Path, - write_class: str, - original_text: str, -) -> outbox.TodoPartitionCapture: - """Create the default-off transaction capture while the Todo lock is held.""" - - active_binding = read_shadow_capture_binding(runtime_root, goal_id)["status"] == "active" - goal_ref: dict[str, str] | None = None - projector = None - try: - with shadow_goal_scope(registry_path, goal_id=goal_id) as scope: - goal_ref = ( - dict(scope.goal_ref) if scope.goal_ref is not None else None - ) - enabled = ( - active_binding - or resolve_coordination_runtime_shadow_config(scope.goal).enabled - ) - if enabled: - from ...rollout_event_log import load_rollout_events, rollout_event_log_path - from ..todos.todo_index import MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL - from .local_authority_shadow_adapter import todo_partition_projector - - events = load_rollout_events( - rollout_event_log_path(runtime_root, goal_id), - limit=MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL, - ) - projector = todo_partition_projector( - scope.goal, - state_path=state_path, - rollout_events=events, - ) - except Exception: - enabled = active_binding - projector = None - return outbox.TodoPartitionCapture.begin( - enabled=enabled, - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - state_path=state_path, - write_class=write_class, - original_text=original_text, - projector=projector, - goal_ref=goal_ref, - ) - - - -def require_runtime_shadow_capture_prepared( - capture: outbox.TodoPartitionCapture, *, runtime_root: Path, goal_id: str, -) -> None: - """Active lineage cannot admit a primary transition without durable preparation.""" - - if capture.outcome.failure is not None and require_shadow_primary_write_allowed(runtime_root, goal_id) is not None: - raise ShadowManagementError( - "shadow_capture_prepare_failed", - "durable shadow preparation failed; the primary state was not changed", - ) - - -def write_captured_todo_state( - capture: outbox.TodoPartitionCapture, - *, - runtime_root: Path, - goal_id: str, - state_path: Path, - text: str, -) -> None: - """Under the primary lock, prepare before replacement and mark only after durability.""" - - from ..runtime.document_io import atomic_write_state_text - - def write() -> None: - capture.prepare(text) - require_runtime_shadow_capture_prepared( - capture, - runtime_root=runtime_root, - goal_id=goal_id, - ) - atomic_write_state_text(state_path, text) - capture.committed() - - captured_goal_ref = capture.goal_ref - if captured_goal_ref is None: - write() - return - registry_path = capture.registry_path - if registry_path is None: - raise ShadowManagementError( - "goal_instance_id_missing", - "exact Todo capture is missing its source registry", - ) - with shadow_goal_scope(registry_path, goal_id=goal_id) as scope: - if scope.goal_ref != captured_goal_ref: - raise ShadowManagementError( - "stale_goal_instance", - "Todo capture belongs to a retired Goal instance", - ) - write() - - -def settle_todo_runtime_shadow_capture( - payload: dict[str, Any], - *, - registry_path: Path, - runtime_root: Path, - goal_id: str, - capture: outbox.TodoPartitionCapture, - emit_disabled: bool = True, -) -> dict[str, Any]: - """Boundedly drain one transaction capture after releasing the Todo lock.""" - - from .local_authority_shadow_adapter import capture_evidence, drain_local_authority_shadow_outbox - - drain = ( - drain_local_authority_shadow_outbox( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - ) - if capture.outcome.entry_id is not None - else None - ) - if emit_disabled or capture.enabled: - payload["coordination_runtime_shadow"] = capture_evidence( - goal_id=goal_id, capture=capture.outcome, drain=drain, - ) - return payload - - -def settle_lease_runtime_shadow_capture( - payload: dict[str, Any], - *, - registry_path: Path | None, - runtime_root: Path, - goal_id: str, -) -> dict[str, Any]: - """Turn the TS writer capture receipt into bounded runtime-shadow evidence.""" - - raw = payload.pop("coordination_runtime_shadow_capture", None) - if not isinstance(raw, Mapping) or registry_path is None: - return payload - capture = outbox.CaptureOutcome( - entry_id=str(raw["entry_id"]) if isinstance(raw.get("entry_id"), str) else None, - partition=LEASE_PARTITION, - seq=int(raw["seq"]) if isinstance(raw.get("seq"), int) else None, - source_bytes_digest=( - str(raw["source_bytes_digest"]) - if isinstance(raw.get("source_bytes_digest"), str) - else None - ), - failure=dict(raw["failure"]) if isinstance(raw.get("failure"), Mapping) else None, - skipped_reason=str(raw["skipped_reason"]) if raw.get("skipped_reason") else None, - ) - from .local_authority_shadow_adapter import ( - capture_evidence, - drain_local_authority_shadow_outbox, - ) - - drain = ( - drain_local_authority_shadow_outbox( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - ) - if capture.entry_id is not None - else None - ) - payload["coordination_runtime_shadow"] = capture_evidence( - goal_id=goal_id, - capture=capture, - drain=drain, - ) - return payload - - -__all__ = [ - "begin_todo_runtime_shadow_capture", - "settle_lease_runtime_shadow_capture", - "settle_todo_runtime_shadow_capture", -] diff --git a/loopx/control_plane/coordination/todo_acceptance_restoration.ts b/loopx/control_plane/coordination/todo_acceptance_restoration.ts index 0d4df4ce2f..3d0777d158 100644 --- a/loopx/control_plane/coordination/todo_acceptance_restoration.ts +++ b/loopx/control_plane/coordination/todo_acceptance_restoration.ts @@ -41,7 +41,8 @@ export function acceptanceRestoration( input.completion_validation_revision !== undefined) { return unavailable("Exact restoration supports only the prior text/wait; ask the owner to review and rebind other work changes"); } - const candidate = prepareUpdatedTodo(todo, input, head).next; + const {monitor_observation: _absentObservation, ...restorationInput} = input; + const candidate = prepareUpdatedTodo(todo, restorationInput, head).next; const binding = state.bindings.find(item => item.todo_id === input.todo_id)!; if (goalAcceptanceTodoDigest(candidate) !== binding.todo_semantic_digest) { return unavailable("The supplied text/wait does not reconstruct the exact owner-confirmed declaration; ask the owner to review and rebind this Todo"); diff --git a/loopx/control_plane/coordination/todo_monitor_poll.ts b/loopx/control_plane/coordination/todo_monitor_poll.ts index 927947417c..317e119902 100644 --- a/loopx/control_plane/coordination/todo_monitor_poll.ts +++ b/loopx/control_plane/coordination/todo_monitor_poll.ts @@ -35,14 +35,32 @@ export interface CoordinationMonitorPollInput { now?: Date; } -function failure(reason_code: string, reason: string): JsonObject & {schema_version: typeof COORDINATION_MONITOR_POLL_RESULT_SCHEMA} { - return {schema_version: COORDINATION_MONITOR_POLL_RESULT_SCHEMA, status: "failed", changed: false, reason_code, reason}; +type CoordinationMonitorPollFailureKind = "decision_rejection" | "protocol_failure"; + +function failure( + reason_code: string, + reason: string, + kind: CoordinationMonitorPollFailureKind = "protocol_failure", +): JsonObject & {schema_version: typeof COORDINATION_MONITOR_POLL_RESULT_SCHEMA} { + return {schema_version: COORDINATION_MONITOR_POLL_RESULT_SCHEMA, status: "failed", + changed: false, failure_kind: kind, reason_code, reason}; +} + +function rejection( + reason_code: string, + reason: string, +): JsonObject & {schema_version: typeof COORDINATION_MONITOR_POLL_RESULT_SCHEMA} { + return failure(reason_code, reason, "decision_rejection"); } function monitorReceipt(input: CoordinationMonitorPollInput, hash: string) { return new CoordinationCommandReceipt({result_schema: COORDINATION_MONITOR_POLL_RESULT_SCHEMA, identity: {schema_version: RECEIPT_SCHEMA, goal_id: input.goal_id, - operation_id: input.operation_id, request_sha256: hash}, failure, + operation_id: input.operation_id, request_sha256: hash}, + failure: (code, reason) => failure(code, reason, + code === "coordination_operation_identity_mismatch" + ? "decision_rejection" + : "protocol_failure"), decode(original, phase) { const writeback = canonicalAuthorityObject(original.writeback, "Monitor writeback"); if (writeback.schema_version !== "monitor_poll_todo_writeback_v0" || @@ -96,13 +114,15 @@ export async function executeCoordinationMonitorPoll(store: AuthorityStore, authoritySourcesCurrent: AuthoritySourceCheck = uncheckedAuthoritySource): Promise { let input: NormalizedMonitorPollInput; try { input = normalize(raw); } - catch (error) { return failure("invalid_monitor_poll_request", String(error)); } + catch (error) { return rejection("invalid_monitor_poll_request", String(error)); } // Original wire identity, before any normalization/default route inference. const hash = monitorPollRequestHash(input); const receipt = monitorReceipt(input, hash); const previous = await receipt.read(store); if (previous) return previous; - if (!await authoritySourcesCurrent()) return failure(AUTHORITY_SOURCE_CHANGED.code, AUTHORITY_SOURCE_CHANGED.reason); + if (!await authoritySourcesCurrent()) { + return rejection(AUTHORITY_SOURCE_CHANGED.code, AUTHORITY_SOURCE_CHANGED.reason); + } const observation = await receipt.observe(store); if (observation.kind === "receipt") return observation.result; const head = observation.authority; @@ -112,11 +132,13 @@ export async function executeCoordinationMonitorPoll(store: AuthorityStore, catch (error) { // Receipt lookup succeeded and planning failed before commit. Only this // boundary can certify no effect; outages and commit failures cannot. - return {...failure("monitor_poll_rejected", error instanceof Error ? error.message : String(error)), + return {...rejection("monitor_poll_rejected", error instanceof Error ? error.message : String(error)), no_effect: {schema_version: "monitor_poll_no_effect_v0", goal_id: input.goal_id, operation_id: input.operation_id, request_sha256: hash}}; } - if (!await authoritySourcesCurrent()) return failure(AUTHORITY_SOURCE_CHANGED.code, AUTHORITY_SOURCE_CHANGED.reason); + if (!await authoritySourcesCurrent()) { + return rejection(AUTHORITY_SOURCE_CHANGED.code, AUTHORITY_SOURCE_CHANGED.reason); + } if (input.dry_run) return {schema_version: COORDINATION_MONITOR_POLL_RESULT_SCHEMA, status: "planned", changed: true, writeback: plan.writeback, provider_revision: head.provider_revision}; const commit = prepareCoordinationProjectionCommit({goal_id: input.goal_id, operation_id: input.operation_id, diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index 2ec9a34560..032d11bc41 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -1,6 +1,11 @@ import {planUserCompletion, requireCompletionDecisionOutcome} from "../todos/user_completion.ts"; import {AUTHORITY_SOURCE_CHANGED, uncheckedAuthoritySource, type AuthoritySourceCheck} from "./authority_source.ts"; -import {normalizeTodoUpdateInput, prepareUpdatedTodo, type CoordinationTodoUpdateInput, type TodoCompletionEdit} from "./todo_update_intent.ts"; +import { + normalizeTodoUpdateInput, + prepareUpdatedTodo, + type NormalizedCoordinationTodoUpdateInput, + type TodoCompletionEdit, +} from "./todo_update_intent.ts"; import {todoUpdateAdmissionRejection} from "./todo_update_admission.ts"; import { createHash } from "node:crypto"; import {acceptanceWorkGuard, acceptanceCompletionRequirements, validateAcceptanceCompletion, @@ -1087,7 +1092,7 @@ export async function executeCoordinationTodoTerminalLifecycle( } head = observation.authority; } - let update: CoordinationTodoUpdateInput | undefined; + let update: NormalizedCoordinationTodoUpdateInput | undefined; if (input.user_update !== undefined) { try { const edit = canonicalAuthorityObject(input.user_update, "User completion edit"); @@ -1459,6 +1464,14 @@ export async function executeCoordinationTodoTerminalLifecycle( cursor: head.cursor, }; } + if (completion.decision === "policy_reject") { + return terminalFailure( + "completion_policy_rejected", + completion.completion_policy_failure.summary, + {completion_policy_failure: completion.completion_policy_failure}, + "decision_rejection", + ); + } if (completion.decision === "reject") { return terminalFailure( completion.failure.kind, diff --git a/loopx/control_plane/coordination/todo_update.ts b/loopx/control_plane/coordination/todo_update.ts index 1ff28f40a7..e988d37b71 100644 --- a/loopx/control_plane/coordination/todo_update.ts +++ b/loopx/control_plane/coordination/todo_update.ts @@ -1,5 +1,6 @@ import {AUTHORITY_SOURCE_CHANGED, uncheckedAuthoritySource, type AuthoritySourceCheck} from "./authority_source.ts"; import type { JsonObject } from "../effect_program.ts"; +import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; import {acceptanceWorkGuard} from "../goals/acceptance_contract.ts"; import type { AuthorityStore, AuthorityStoreCommit } from "./authority_store.ts"; import { @@ -41,7 +42,13 @@ export const COORDINATION_TODO_OBSERVATION_UPDATE_REQUEST_SCHEMA = export const COORDINATION_TODO_VALIDATION_REVISION_REQUEST_SCHEMA = "loopx_local_coordination_todo_update_request_v5"; export type {CoordinationTodoUpdateInput} from "./todo_update_intent.ts"; -import {normalizeTodoUpdateInput, prepareUpdatedTodo, type CoordinationTodoUpdateInput} from "./todo_update_intent.ts"; +import { + normalizeTodoUpdateEnvelope, + normalizeTodoUpdateInput, + prepareUpdatedTodo, + type CoordinationTodoUpdateInput, + type NormalizedCoordinationTodoUpdateInput, +} from "./todo_update_intent.ts"; import {executeCoordinationTodoTerminalLifecycle} from "./todo_terminal_lifecycle.ts"; import {planCompletionValidationRevision} from "../todos/completion_validation_revision.ts"; @@ -49,9 +56,19 @@ export type CoordinationTodoUpdateResult = JsonObject & { readonly schema_version: typeof COORDINATION_TODO_UPDATE_RESULT_SCHEMA; }; -function failure(code: string, reason: string): CoordinationTodoUpdateResult { +type CoordinationTodoUpdateFailureKind = "decision_rejection" | "protocol_failure"; + +function failure( + code: string, + reason: string, + kind: CoordinationTodoUpdateFailureKind = "protocol_failure", +): CoordinationTodoUpdateResult { return {schema_version: COORDINATION_TODO_UPDATE_RESULT_SCHEMA, status: "failed", - changed: false, reason_code: code, reason}; + changed: false, failure_kind: kind, reason_code: code, reason}; +} + +function rejection(code: string, reason: string): CoordinationTodoUpdateResult { + return failure(code, reason, "decision_rejection"); } function isFailure(value: JsonObject): value is CoordinationTodoUpdateResult { @@ -63,7 +80,11 @@ function updateReceipt(input: CoordinationTodoUpdateInput, requestSha: string) { return new CoordinationCommandReceipt({result_schema: COORDINATION_TODO_UPDATE_RESULT_SCHEMA, identity: {schema_version: COORDINATION_TODO_UPDATE_RECEIPT_SCHEMA, operation_id: input.operation_id, goal_id: input.goal_id, todo_id: input.todo_id, - request_sha256: requestSha}, failure, + request_sha256: requestSha}, + failure: (code, reason) => failure(code, reason, + code === "coordination_operation_identity_mismatch" + ? "decision_rejection" + : "protocol_failure"), decode(original) { if (typeof original.changed !== "boolean") throw new AuthorityStoreProtocolError("update receipt changed must be boolean"); return {fields: {todo_id: input.todo_id, original_receipt: original, @@ -110,7 +131,7 @@ function loadUpdateTarget( const projection = indexCoordinationProjection(head, input.goal_id); const found = projection.todos.get(input.todo_id); return found === undefined - ? failure("todo_not_found", "canonical Todo is missing") + ? rejection("todo_not_found", "canonical Todo is missing") : {todo: found, leases: projection.leases}; } catch (error) { return failure("invalid_coordination_projection", @@ -124,8 +145,8 @@ export async function executeCoordinationTodoUpdate( authoritySourcesCurrent: AuthoritySourceCheck = uncheckedAuthoritySource, ): Promise { let input: CoordinationTodoUpdateInput; - try { input = normalizeTodoUpdateInput(rawInput); } catch (error) { - return failure("invalid_coordination_todo_update", + try { input = normalizeTodoUpdateEnvelope(rawInput); } catch (error) { + return rejection("invalid_coordination_todo_update", error instanceof Error ? error.message : "invalid Todo update"); } if (input.completion !== undefined) { @@ -161,18 +182,21 @@ export async function executeCoordinationTodoUpdate( const receipt = updateReceipt(input, requestSha); const replay = await receipt.read(store); if (replay !== null) return replay; - const sourceChanged = () => failure(AUTHORITY_SOURCE_CHANGED.code, AUTHORITY_SOURCE_CHANGED.reason); + const sourceChanged = () => rejection( + AUTHORITY_SOURCE_CHANGED.code, + AUTHORITY_SOURCE_CHANGED.reason, + ); if (!await authoritySourcesCurrent()) return sourceChanged(); // Legacy single-agent callers historically omitted actor_agent_id for an // unowned Todo. Keep that narrow compatibility path, while retaining the // registered-actor requirement for multi-agent or explicitly-owned work. if (input.actor_agent_id === null) { if (input.registered_agents.length > 1) { - return failure("actor_not_registered", "Todo update requires a registered actor"); + return rejection("actor_not_registered", "Todo update requires a registered actor"); } } else if (input.registered_agents.length === 0 || !input.registered_agents.includes(input.actor_agent_id)) { - return failure("actor_not_registered", "Todo update requires a registered actor"); + return rejection("actor_not_registered", "Todo update requires a registered actor"); } const observation = await receipt.observe(store); if (observation.kind === "receipt") return observation.result; @@ -182,18 +206,25 @@ export async function executeCoordinationTodoUpdate( } if (input.expected_provider_revision !== undefined && input.expected_provider_revision !== head.provider_revision) { - return failure("provider_revision_mismatch", "Current revision changed; inspect again before continuing"); + return rejection("provider_revision_mismatch", "Current revision changed; inspect again before continuing"); } const target = loadUpdateTarget(head.head, input); if (isFailure(target)) return target; const rejected = todoUpdateAdmissionRejection(head.head, target.todo, target.leases, input); - if (rejected !== null) return {...failure(rejected.code, rejected.reason), + if (rejected !== null) return {...rejection(rejected.code, rejected.reason), ...(rejected.handoff_mode === undefined ? {} : {handoff_mode: rejected.handoff_mode}), ...(rejected.recovery === undefined ? {} : {recovery: rejected.recovery})}; + let normalizedInput: NormalizedCoordinationTodoUpdateInput; let prepared: ReturnType; - try { prepared = prepareUpdatedTodo(target.todo, input, head.head); } - catch (error) { return failure("invalid_coordination_todo_update", + try { + normalizedInput = normalizeTodoUpdateInput(input); + prepared = prepareUpdatedTodo(target.todo, normalizedInput, head.head); + } + catch (error) { return rejection( + error instanceof EffectRuntimeRequestError + ? error.code + : "invalid_coordination_todo_update", error instanceof Error ? error.message : "invalid updated Todo"); } let {next, changed, clearFields} = prepared; let completionValidationRevisionReceipt: JsonObject | null = null; @@ -219,7 +250,7 @@ export async function executeCoordinationTodoUpdate( ); canonicalTodoRecord(next, "updated Todo"); } catch (error) { - return failure( + return rejection( "invalid_completion_validation_revision", error instanceof Error ? error.message @@ -234,19 +265,28 @@ export async function executeCoordinationTodoUpdate( cycle = planMonitorCycleTransition({goal_id: input.goal_id, before: target.todo, after: next, lease: target.leases.get(input.todo_id), handoff_mode: head.head.handoff_mode, now: input.now}); if ((head.head.handoff_mode === "hard_lease" || target.leases.has(input.todo_id)) && - (isDeferredReopen(input, target.todo) || isOwnerDeferral(input, target.todo))) { + (isDeferredReopen(normalizedInput, target.todo) || isOwnerDeferral(normalizedInput, target.todo))) { deferredCycle = planDeferredLifecycle({goal_id: input.goal_id, before: target.todo, after: next, lease: target.leases.get(input.todo_id), now: input.now}); } - if (head.head.handoff_mode === "hard_lease" && isBlockedLifecycleTransition(input, target.todo)) { + if (head.head.handoff_mode === "hard_lease" && + isBlockedLifecycleTransition(normalizedInput, target.todo)) { blockedCycle = planBlockedLifecycleTransition({goal_id: input.goal_id, before: target.todo, after: next, lease: target.leases.get(input.todo_id), now: input.now}); } } catch (error) { - return failure("invalid_coordination_projection", error instanceof Error ? error.message : "invalid retained lease"); + return failure( + "invalid_coordination_projection", + error instanceof Error ? error.message : "invalid retained lease", + error instanceof EffectRuntimeRequestError + ? "decision_rejection" + : "protocol_failure", + ); } const deferredTransition = deferredCycle?.transition == null ? {} : { - [isOwnerDeferral(input, target.todo) ? "deferred_transition" : "deferred_resume_transition"]: deferredCycle.transition, + [isOwnerDeferral(normalizedInput, target.todo) + ? "deferred_transition" + : "deferred_resume_transition"]: deferredCycle.transition, }; const commit: AuthorityStoreCommit = changed ? prepareCoordinationProjectionCommit({ goal_id: input.goal_id, operation_id: input.operation_id, @@ -261,7 +301,7 @@ export async function executeCoordinationTodoUpdate( if (input.planning_intent?.claimed_by != null) { const acceptance = acceptanceWorkGuard(commit.next_projection, input.goal_id, input.todo_id); if (acceptance !== null && !acceptance.allowed) { - return {...failure(String(acceptance.reason_code), `${String(acceptance.reason)} Inspect Goal acceptance and ask the owner to configure or rebind this Todo.`), + return {...rejection(String(acceptance.reason_code), `${String(acceptance.reason)} Inspect Goal acceptance and ask the owner to configure or rebind this Todo.`), goal_acceptance_guard: acceptance}; } } diff --git a/loopx/control_plane/coordination/todo_update_intent.ts b/loopx/control_plane/coordination/todo_update_intent.ts index 24025ea38d..38f2c3d30c 100644 --- a/loopx/control_plane/coordination/todo_update_intent.ts +++ b/loopx/control_plane/coordination/todo_update_intent.ts @@ -36,10 +36,15 @@ export interface CoordinationTodoUpdateInput { readonly lease_expected_version?: number | null; readonly planning_intent?: JsonObject; readonly completion?: JsonObject; - readonly monitor_observation?: MonitorPollObservation; + readonly monitor_observation?: JsonObject; readonly completion_validation_revision?: CompletionValidationRevision; } +export type NormalizedCoordinationTodoUpdateInput = + Omit & { + readonly monitor_observation?: MonitorPollObservation; + }; + /** Only edit intent crosses into the terminal owner; actor/lease authority is * carried once, in the enclosing terminal request. */ export type TodoCompletionEdit = Pick requireAuthorityStoreId(field, `clear_fields[${index}]`)); - const observation = raw.monitor_observation === undefined ? undefined : decodeMonitorPollObservation(raw.monitor_observation); + const observation = raw.monitor_observation === undefined + ? undefined + : canonicalAuthorityObject(raw.monitor_observation, "Monitor observation"); const validationRevision = raw.completion_validation_revision === undefined ? undefined : decodeCompletionValidationRevision(raw.completion_validation_revision); @@ -148,8 +157,22 @@ export function normalizeTodoUpdateInput(raw: CoordinationTodoUpdateInput): Coor patch, clear_fields: clearFields}; } +export function normalizeTodoUpdateInput( + raw: CoordinationTodoUpdateInput, +): NormalizedCoordinationTodoUpdateInput { + const envelope = normalizeTodoUpdateEnvelope(raw); + const observation = envelope.monitor_observation === undefined + ? undefined + : decodeMonitorPollObservation(envelope.monitor_observation); + const {monitor_observation: _rawObservation, ...normalized} = envelope; + return { + ...normalized, + ...(observation === undefined ? {} : {monitor_observation: observation}), + }; +} + export function prepareUpdatedTodo( - todo: JsonObject, input: CoordinationTodoUpdateInput, head: JsonObject, kind: "planning" | "user_completion" = "planning", + todo: JsonObject, input: NormalizedCoordinationTodoUpdateInput, head: JsonObject, kind: "planning" | "user_completion" = "planning", ): {next: JsonObject; changed: boolean; clearFields: string[]; monitorTransition?: JsonObject} { const next: JsonObject = {...todo, ...input.patch}; for (const field of input.clear_fields) delete next[field]; diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index 9558769848..d3a4f8ac2c 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -420,7 +420,6 @@ export function createEffectRuntimeHandlers( ["work_items.team_plan.commit", lazyHandler(() => import("./work_items/team_plan_authority.ts"), ({commitLocalTeamPlan}) => commitLocalTeamPlan)], ["coordination.local_authority.todo_update", lazyHandler(() => import("./coordination/local_authority_runtime.ts"), ({updateLocalCoordinationTodo}) => updateLocalCoordinationTodo)], ["coordination.local_authority.monitor_poll", lazyHandler(() => import("./coordination/local_authority_runtime.ts"), ({pollLocalCoordinationMonitor}) => pollLocalCoordinationMonitor)], - ["coordination.handoff_mode.legacy_plan", lazyHandler(() => import("./coordination/handoff_mode_legacy_plan.ts"), ({planLegacyHandoffMode}) => planLegacyHandoffMode)], ["coordination.local_authority.handoff_mode_set", lazyHandler(() => import("./coordination/handoff_mode_runtime.ts"), ({setLocalHandoffMode}) => setLocalHandoffMode)], ["coordination.local_authority.handoff_mode_migrate", lazyHandler(() => import("./coordination/handoff_mode_runtime.ts"), ({migrateLocalHandoffMode}) => migrateLocalHandoffMode)], ["coordination.local_authority.todo_terminal", lazyHandler(() => import("./coordination/local_authority_runtime.ts"), ({terminalLifecycleLocalCoordinationTodo}) => terminalLifecycleLocalCoordinationTodo)], @@ -458,7 +457,7 @@ export function createEffectRuntimeHandlers( ["operation.agent_handoff.plan", lazyHandler(() => import("./work_items/operation_agent_handoff.ts"), ({planAgentOperationHandoff}) => planAgentOperationHandoff)], ["operation.agent_handoff.inbox", lazyHandler(() => import("./work_items/operation_agent_handoff.ts"), ({projectAgentOperationInbox}) => projectAgentOperationInbox)], ["scheduler.monitor_successor.plan", lazyHandler(() => import("./scheduler/monitor_successor.ts"), ({planMonitorSuccessor}) => planMonitorSuccessor)], - ["scheduler.monitor_batch.plan", lazyHandler(() => import("./scheduler/monitor_batch.ts"), ({planLegacyMonitorBatch}) => planLegacyMonitorBatch)], + ["scheduler.monitor_batch.replay", lazyHandler(() => import("./scheduler/monitor_batch.ts"), ({replayLegacyMonitorBatch}) => replayLegacyMonitorBatch)], ["scheduler.monitor_target.select", lazyHandler(() => import("./scheduler/monitor_successor.ts"), ({selectMonitorTodoRequest}) => selectMonitorTodoRequest)], ["capabilities.issue_fix.monitor_reconciliation.plan", lazyHandler(() => import("./capabilities/issue_fix_monitor_reconciliation.ts"), ({planIssueFixMonitorReconciliation}) => planIssueFixMonitorReconciliation)], ["capabilities.change_quality.validation_gate", lazyHandler(() => import("./capabilities/change_quality_validation.ts"), ({qualifyChangeQualityValidation}) => qualifyChangeQualityValidation)], diff --git a/loopx/control_plane/projects/registry.py b/loopx/control_plane/projects/registry.py index 992b82b3d0..746e81b7f7 100644 --- a/loopx/control_plane/projects/registry.py +++ b/loopx/control_plane/projects/registry.py @@ -1,9 +1,12 @@ from __future__ import annotations +import hashlib import re +from collections.abc import Callable from pathlib import Path from typing import Any +from ...file_lock import exclusive_cross_runtime_file_lock from ...control_plane.runtime.time import now_local_iso from ...paths import ( registered_goal_state_file, @@ -12,7 +15,11 @@ select_default_runtime_root, ) from ..runtime.document_io import atomic_write_state_text as _atomic_write_text -from ..coordination.legacy_writer_fence import legacy_todo_write_transaction, require_legacy_state_replacement_allowed +from ..coordination.local_authority import ( + read_canonical_todos_if_promoted, + require_canonical_authority, +) +from ..coordination.goal_storage_initialization import initialize_goal_storage_target from ..goals.source_session_services import ( FreshSourceSessionRegistration, RecreateGoalRequest, @@ -134,7 +141,7 @@ def _resolve_exact_project_binding( } -def register_project_goal( +def _register_project_goal_unlocked( *, registry_path: Path, runtime_root: Path | None, @@ -150,6 +157,7 @@ def register_project_goal( stop_condition: str, repository_bindings: list[str], external_locator_bindings: list[str], + resolve_new_goal_storage_target: Callable[[Path], dict[str, Any]], goal_instance_profile: str | None = None, operation_id: str | None = None, ) -> dict[str, Any]: @@ -253,6 +261,74 @@ def register_project_goal( if operation_id is not None: raise ValueError("operation_id requires goal_instance_profile") + initial_registry = existing_registry or { + "schema_version": "0.1", + "registry_role": "project-local", + "common_runtime_root": str(runtime_root or select_default_runtime_root()), + } + effective_root = resolve_runtime_root( + initial_registry, + str(runtime_root) if runtime_root else None, + registry_path=registry_path, + ) + existing_goal_record = next( + ( + item + for item in initial_registry.get("goals", []) + if isinstance(item, dict) and item.get("id") == goal_id + ), + None, + ) + existing_target = ( + (existing_goal_record.get("coordination") or {}).get("storage_target") + if existing_goal_record is not None + else None + ) + resumable_creation = ( + isinstance(existing_target, dict) + and existing_target.get("schema_version") + == "loopx_new_goal_storage_target_v1" + and isinstance(existing_goal_record.get("creation_operation_id"), str) + and existing_goal_record["creation_operation_id"] != "" + ) + existing_authority = ( + read_canonical_todos_if_promoted( + runtime_root=effective_root, + goal_id=goal_id, + ) + if existing_goal_record is None or not resumable_creation + else None + ) + if existing_goal_record is None and existing_authority is not None: + raise ValueError( + f"goal_id is already bound to canonical authority through another registry: {goal_id}" + ) + if ( + existing_goal_record is not None + and existing_authority is None + and not resumable_creation + ): + require_canonical_authority(goal_id=goal_id, operation="goal_create") + storage_target = ( + existing_target + if existing_goal_record is not None + else resolve_new_goal_storage_target(effective_root) + ) + if ( + existing_goal_record is None + and (storage_target or {}).get("schema_version") + != "loopx_new_goal_storage_target_v1" + ): + require_canonical_authority(goal_id=goal_id, operation="goal_create") + if storage_target is not None: + goal_record.setdefault("coordination", {})["storage_target"] = storage_target + goal_record["creation_operation_id"] = ( + (existing_goal_record or {}).get("creation_operation_id") + or "goal-create:project-register:" + + hashlib.sha256(f"{project_id}\0{goal_id}".encode()).hexdigest()[:24] + ) + + changed = True with project_registry_transaction( registry_path, operation="project_register", @@ -291,9 +367,7 @@ def register_project_goal( f"project_id already has its first registered goal: {project_id}" ) effective_root = resolve_runtime_root(registry, str(runtime_root) if runtime_root else None, registry_path=registry_path) - with legacy_todo_write_transaction(registry_path, goal_id, state_file, None, "project_register_state", False, runtime_root=effective_root): - if not state_file.exists(): - require_legacy_state_replacement_allowed(runtime_root=effective_root, goal_id=goal_id, goal=existing_goal) + if existing_project is not None and existing_goal is not None: if state_file.exists(): existing_state = state_file.read_text(encoding="utf-8") existing_updated_at = registration_state_updated_at(existing_state) @@ -318,28 +392,11 @@ def register_project_goal( raise ValueError( f"goal state file conflicts with registration: {state_file}" ) - if existing_project is not None and existing_goal is not None: - if not state_file.exists(): - _atomic_write_text(state_file, state_text) - return { - "ok": True, - "schema_version": "loopx_project_registration_v0", - "changed": True, - "registry": str(registry_path), - "project": existing_project, - "goal": existing_goal, - "state_file": str(state_file), - } - return { - "ok": True, - "schema_version": "loopx_project_registration_v0", - "changed": False, - "registry": str(registry_path), - "project": existing_project, - "goal": existing_goal, - "state_file": str(state_file), - } - + if not state_file.exists(): + _atomic_write_text(state_file, state_text) + else: + changed = False + else: state_file.parent.mkdir(parents=True, exist_ok=True) state_created = False try: @@ -354,18 +411,99 @@ def register_project_goal( if state_created: state_file.unlink(missing_ok=True) raise + current_registry = load_project_registry(registry_path) + registered_goal = next( + item + for item in current_registry.get("goals", []) + if isinstance(item, dict) and item.get("id") == goal_id + ) + storage_selection = initialize_goal_storage_target( + effective_root, + registered_goal, + registry_path=registry_path, + ) + if storage_selection is None or storage_selection.get("authority_initialized") is not True: + raise RuntimeError("canonical project Goal initialization did not complete") return { "ok": True, "schema_version": "loopx_project_registration_v0", - "changed": True, + "changed": changed, "registry": str(registry_path), "project": project_record, "goal": goal_record, "state_file": str(state_file), + "storage_selection": storage_selection, } +def register_project_goal( + *, + registry_path: Path, + runtime_root: Path | None, + project_id: str, + project_kind: str, + knowledge_root: Path, + goal_id: str, + objective: str, + non_goals: list[str], + acceptance: list[str], + unknowns: list[str], + next_effect: str, + stop_condition: str, + repository_bindings: list[str], + external_locator_bindings: list[str], + resolve_new_goal_storage_target: Callable[[Path], dict[str, Any]], + goal_instance_profile: str | None = None, + operation_id: str | None = None, +) -> dict[str, Any]: + normalized_goal_id = _identifier(goal_id, field="goal_id") + expanded_registry = registry_path.expanduser() + existing_registry = ( + load_project_registry(expanded_registry) if expanded_registry.exists() else None + ) + registry = existing_registry or { + "schema_version": "0.1", + "registry_role": "project-local", + "common_runtime_root": str(runtime_root or select_default_runtime_root()), + } + effective_root = resolve_runtime_root( + registry, + str(runtime_root) if runtime_root else None, + registry_path=expanded_registry, + ) + digest = hashlib.sha256(normalized_goal_id.encode("utf-8")).hexdigest()[:16] + lock_target = ( + effective_root + / "authority-transition" + / "project-registration" + / f"goal-{digest}" + ) + with exclusive_cross_runtime_file_lock( + lock_target, + operation="project_register", + ): + return _register_project_goal_unlocked( + registry_path=expanded_registry, + runtime_root=runtime_root, + project_id=project_id, + project_kind=project_kind, + knowledge_root=knowledge_root, + goal_id=normalized_goal_id, + objective=objective, + non_goals=non_goals, + acceptance=acceptance, + unknowns=unknowns, + next_effect=next_effect, + stop_condition=stop_condition, + repository_bindings=repository_bindings, + external_locator_bindings=external_locator_bindings, + resolve_new_goal_storage_target=resolve_new_goal_storage_target, + goal_instance_profile=goal_instance_profile, + operation_id=operation_id, + ) + + def bind_session( *, registry_path: Path, diff --git a/loopx/control_plane/quota/monitor_poll.py b/loopx/control_plane/quota/monitor_poll.py index 72003a412d..4d25c92e2c 100644 --- a/loopx/control_plane/quota/monitor_poll.py +++ b/loopx/control_plane/quota/monitor_poll.py @@ -952,6 +952,13 @@ def transact( ) if isinstance(exc, _NativeMonitorPollRejected): payload["error_code"] = exc.diagnostic_code + else: + from ..coordination.local_authority import ( + LocalCoordinationAuthorityRejection, + ) + + if isinstance(exc, LocalCoordinationAuthorityRejection): + payload["error_code"] = exc.code return payload if native.get("status") == "conflict": diff --git a/loopx/control_plane/quota/monitor_poll_commit.ts b/loopx/control_plane/quota/monitor_poll_commit.ts index 3e12392ab7..7e1c3040bf 100644 --- a/loopx/control_plane/quota/monitor_poll_commit.ts +++ b/loopx/control_plane/quota/monitor_poll_commit.ts @@ -918,7 +918,7 @@ function requestDigest(request: MonitorRequest): string { function providerPlanFor(request: MonitorRequest, gateGuard = Boolean( request.observation.settlement_todo_id && request.observation.settlement_todo_id !== request.observation.todo_id -), legacyBatchVersion: 1 | null = 1): MonitorProviderPlan { +), legacyBatchVersion: 1 | null = null): MonitorProviderPlan { const resultHash = request.observation.result_hash; if (!resultHash) { throw new EffectRuntimeRequestError("monitor todo writeback requires --result-hash"); diff --git a/loopx/control_plane/scheduler/legacy_monitor_poll.py b/loopx/control_plane/scheduler/legacy_monitor_poll.py deleted file mode 100644 index 0b9169c456..0000000000 --- a/loopx/control_plane/scheduler/legacy_monitor_poll.py +++ /dev/null @@ -1,229 +0,0 @@ -"""Atomic Markdown storage for the shared TypeScript Monitor batch. - -The operation receipt and every business mutation share one replacement. -Parsing, locking and projection delivery stay here; retry and Todo rules stay -in the typed planner. Retained receipts are outside the archivable Todo blocks. -""" -from __future__ import annotations - -import base64 -import json -from pathlib import Path -from typing import Any - -from ...agent_registry import load_goal_from_registry, registered_agent_ids_for_goal -from ..coordination.legacy_writer_fence import legacy_todo_write_transaction -from ..runtime.local_state_write_correctness import ( - build_local_state_write_correctness_dry_run_packet, -) -from ..runtime.document_io import verify_state_text_durable -from ..todos.active_state_editing import ( - find_todo_block, insert_into_existing_section, insert_new_section, - replace_updated_at, section_bounds, -) -from ..todos.contract import ( - TODO_METADATA_FIELDS, format_todo_metadata_line, metadata_line_for_todo_block, - parse_todo_metadata_line, - todo_marker_for_status, -) -from ..todos.goal_todo_projection import goal_todo_summaries -from ..todos.handoff_mode import goal_handoff_mode -from ..todos.mutation_response import serialize_added_todo_payload, serialize_todo_update_result -from ..todos.mutation_authority import authorize_todo_lifecycle_mutation -from ..todos.next_action_runtime import apply_added_todo_next_action -from ..todos.path_resolution import resolve_todo_state_path -from ..todos.todo_semantics import todo_priority_label -from .monitor_batch_receipts import _plan, _read_receipt, _receipt_marker, _writeback - - -def _metadata(record: dict[str, Any]) -> dict[str, Any]: - """Use the public Markdown codec for the same wire types as Todo add/update.""" - line = format_todo_metadata_line(**{key: record[key] for key in TODO_METADATA_FIELDS - if key != "role" and key in record and record[key] is not None}) - return parse_todo_metadata_line(line) or {} - - -def _legacy_writeback( - planned: dict[str, Any], *, request: dict[str, Any], original: str, - todos: list[dict[str, Any]], state: Path, project: Path | None, - mutation_authority: dict[str, Any], -) -> dict[str, Any]: - """Serialize typed batch facts through the existing public Todo result shapes.""" - result = _writeback(planned) - monitor = next(todo for todo in todos if todo["todo_id"] == result["todo_id"]) - updated = planned["mutations"][0]["todo"] - original_lines = original.splitlines() - match = find_todo_block(original_lines, todo_id=result["todo_id"], role="agent") - if match is None: - raise ValueError("Monitor Todo disappeared during batch planning") - section = match[1] - status_changed = monitor.get("status") != updated.get("status") - text_changed = monitor.get("text") != updated.get("text") - metadata_updated = any(monitor.get(key) != updated.get(key) - for key in TODO_METADATA_FIELDS if key != "role") - changed = status_changed or text_changed or metadata_updated - timestamp = request["observation"]["generated_at"] - handoff = {"handoff_mode": goal_handoff_mode(original)} - update = serialize_todo_update_result( - role="agent", section=section, todo=monitor.get("text"), - todo_id=result["todo_id"], status=str(updated["status"]), - priority=todo_priority_label(monitor), status_changed=status_changed, - text_changed=text_changed, metadata_updated=metadata_updated, - metadata=_metadata(updated), - monitor_poll_transition=result["todo_update"]["monitor_poll_transition"], - ) - result["todo_update"] = { - "ok": True, "dry_run": request["dry_run"], "changed": changed, - "goal_id": request["goal_id"], "agent_id": request["actor_agent_id"], - "mutation_authority": mutation_authority, **handoff, **update, - "state_file": str(state), "project": str(project) if project else None, - "updated_at": timestamp if changed else None, - } - created_ids = {mutation["todo"]["todo_id"] for mutation in planned["mutations"][1:]} - successors = [] - for record in result["next_todos"]: - role = record["role"] - added = record["todo_id"] in created_ids - metadata = _metadata(record) - add_result = {**metadata, "todo_id": record["todo_id"], - "section": next((match[2] for match in [section_bounds(original_lines, role)] if match), - "User Todo / Owner Review Reading Queue" if role == "user" else "Agent Todo"), - "status": record["status"], "task_class": record.get("task_class"), - "action_kind": record.get("action_kind"), - "required_write_scopes": record.get("required_write_scopes") or [], - "required_capabilities": record.get("required_capabilities") or [], - "target_capabilities": record.get("target_capabilities") or [], - "explore_result_node_refs": record.get("explore_result_node_refs") or [], - "required_decision_scopes": record.get("required_decision_scopes") or [], - "excluded_agents": record.get("excluded_agents") or [], - "already_exists": not added, "metadata_updated": False, - "status_changed": False} - successors.append(serialize_added_todo_payload( - add_result=add_result, goal_id=request["goal_id"], role=role, - todo_text=record["text"], - agent_id=request["actor_agent_id"] if role == "user" else None, - state_file=state, project=project, updated_at=timestamp, - dry_run=request["dry_run"], added=added, - metadata_updated=False, changed=added, handoff_gate=handoff, - )) - result["next_todos"] = successors - result["successor_receipts"] = [ - {key: todo[key] for key in ( - "todo_id", "role", "task_class", "action_kind", "task_repository", - "continuation_policy", "required_capabilities", "claimed_by", - "unblocks_todo_id", "target_key") if todo.get(key) not in (None, "", [])} - for todo in successors - ] - return result - - -def _attach_batch_shadow(result: dict[str, Any]) -> dict[str, Any]: - """Expose one real capture through the legacy nested result locations.""" - evidence = result.get("coordination_runtime_shadow") - if evidence is not None: - result["todo_update"]["coordination_runtime_shadow"] = evidence - for todo in result["next_todos"]: - todo["coordination_runtime_shadow"] = evidence - return result - - -def _attach_batch_dry_run(result: dict[str, Any], original: str) -> None: - """One preview describes the single locked replacement and its Todos.""" - ids = [result["todo_id"], *(todo["todo_id"] for todo in result["next_todos"])] - packet = build_local_state_write_correctness_dry_run_packet( - goal_id=result["goal_id"], writer_id="loopx.todo", write_class="todo_update", - state_text=original, - target_refs={"state_file_ref": "registry.goal.state_file", "todo_ids": ids}, - patch_summary=f"preview one Monitor poll batch for {len(ids)} Todos: would change active state", - expected_write_scopes=["active_state"], - narrower_lock_allowed="not_for_multi_todo_batch", - ) - result["todo_update"]["local_state_write_correctness"] = packet - for todo in result["next_todos"]: - todo["local_state_write_correctness"] = packet - - -def _apply_mutations(lines: list[str], mutations: list[dict[str, Any]], - todos: list[dict[str, Any]]) -> None: - from ..todos.line_update import upsert_todo_metadata - - sources = {todo["todo_id"]: todo for todo in todos} - for mutation in mutations: - record = mutation["todo"] - role, todo_id = record["role"], record["todo_id"] - found = find_todo_block(lines, todo_id=todo_id, role=role) - if found is not None: - # Preserve narrative and private validation declarations in the - # original block; only fields changed by the typed owner are edited. - source = sources[todo_id] - updates = {key: record.get(key) for key in set(source) | set(record) - if key in TODO_METADATA_FIELDS and source.get(key) != record.get(key)} - block = found[-1] - upsert_todo_metadata(lines, block, metadata_line_for_todo_block(block, updates)) - else: - metadata = format_todo_metadata_line(**{key: record[key] for key in TODO_METADATA_FIELDS - if key != "role" and key in record and record[key] is not None}) - block = f"- [{todo_marker_for_status(record['status'])}] {record['text']}\n{metadata}" - bounds = section_bounds(lines, role) - if bounds: - insert_into_existing_section(lines, bounds[0], bounds[1], block) - else: - insert_new_section(lines, role, block) - - -def apply_legacy_monitor_poll( - *, registry_path: Path, runtime_root: Path, request: dict[str, Any], -) -> dict[str, Any]: - goal_id, dry_run = request["goal_id"], request["dry_run"] - project, state = resolve_todo_state_path(registry_path=registry_path, goal_id=goal_id) - with legacy_todo_write_transaction(registry_path, goal_id, state, - request["actor_agent_id"], "todo_update", dry_run, runtime_root=runtime_root): - original = state.read_text(encoding="utf-8") - previous = _read_receipt(original, request["operation_id"]) - goal = load_goal_from_registry(registry_path, goal_id) - todos = goal_todo_summaries(goal, state_text=original, state_path=state, - rollout_events=[], roles=["user", "agent"], status=None, todo_id=None, - agent_id=None, limit=None, include_retained=True).todos - planned = _plan(request, registered_agents=registered_agent_ids_for_goal(goal), - todos=todos, previous_receipt=previous) - if planned["replayed"]: - verify_state_text_durable(state, original) - return _writeback(planned) - from ..coordination.runtime_shadow_writer_adapter import ( - begin_todo_runtime_shadow_capture, settle_todo_runtime_shadow_capture, - write_captured_todo_state, - ) - - monitor = next(todo for todo in todos if todo["todo_id"] == planned["writeback"]["todo_id"]) - authority = authorize_todo_lifecycle_mutation(registry_path=registry_path, goal_id=goal_id, - command="update", todo=monitor, actor_agent_id=request["actor_agent_id"]) - result = _legacy_writeback(planned, request=request, original=original, - todos=todos, state=state, project=project, mutation_authority=authority) - planned["receipt"]["writeback"] = result - lines = original.splitlines() - _apply_mutations(lines, planned["mutations"], todos) - for todo in result["next_todos"]: - apply_added_todo_next_action(lines, role=todo["role"], - add_result={**todo, "changed": todo["added"]}) - if dry_run: - _attach_batch_dry_run(result, original) - capture = begin_todo_runtime_shadow_capture(registry_path=registry_path, - runtime_root=runtime_root, goal_id=goal_id, state_path=state, - write_class="todo_update", original_text=original) - return _attach_batch_shadow(settle_todo_runtime_shadow_capture(result, - registry_path=registry_path, runtime_root=runtime_root, - goal_id=goal_id, capture=capture)) - capture = begin_todo_runtime_shadow_capture(registry_path=registry_path, - runtime_root=runtime_root, goal_id=goal_id, state_path=state, - write_class="todo_update", original_text=original) - encoded = base64.b64encode(json.dumps(planned["receipt"], ensure_ascii=False, - separators=(",", ":")).encode()).decode() - first_heading = next((i for i, line in enumerate(lines) if line.startswith("#")), len(lines)) - lines[first_heading:first_heading] = [_receipt_marker(request["operation_id"]) + encoded + " -->", ""] - text = replace_updated_at("\n".join(lines) + "\n", request["observation"]["generated_at"]) - write_captured_todo_state(capture, runtime_root=runtime_root, goal_id=goal_id, - state_path=state, text=text) - verify_state_text_durable(state, text) - return _attach_batch_shadow(settle_todo_runtime_shadow_capture(result, - registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id, - capture=capture)) diff --git a/loopx/control_plane/scheduler/monitor_batch.ts b/loopx/control_plane/scheduler/monitor_batch.ts index 2859f0da40..341a311bc4 100644 --- a/loopx/control_plane/scheduler/monitor_batch.ts +++ b/loopx/control_plane/scheduler/monitor_batch.ts @@ -52,8 +52,7 @@ export function normalizeMonitorPollFields(observation: unknown, intent: unknown return fields; } -function planWriteback(input: MonitorBatchInput, - claimPolicy: "actor_owned" | "registered_peer_handoff" = "actor_owned") { +function planWriteback(input: MonitorBatchInput) { const todos = input.todos; const observation = input.observation; const monitor = selectMonitorTodo(todos, @@ -120,7 +119,7 @@ function planWriteback(input: MonitorBatchInput, const created = planCoordinationTodoCreate({goal_id: input.goal_id, operation_id: input.operation_id, actor_agent_id: actor, registered_agents: input.registered_agents, dry_run: input.dry_run, now: new Date(String(observation.generated_at)), todo}, plannedTodos, - input.read_model_schema ?? TODO_DOMAIN_READ_RECORD_SCHEMA, "role_text", claimPolicy); + input.read_model_schema ?? TODO_DOMAIN_READ_RECORD_SCHEMA, "role_text", "actor_owned"); if (created.status !== "planned" && created.status !== "no_change") throw new EffectRuntimeRequestError(String(created.reason)); const record = canonicalAuthorityObject(created.todo, "successor Todo"); nextTodos.push({...record, todo: record.text, ok: true, dry_run: input.dry_run}); @@ -156,18 +155,17 @@ function normalizeBatchInput(raw: MonitorBatchInput): MonitorBatchInput { }; } -/** Pure fresh batch planning. Canonical and legacy callers own their receipts. */ +/** Pure fresh batch planning for canonical authority transactions. */ export function planMonitorBatch(raw: MonitorBatchInput) { const input = normalizeBatchInput(raw); const todos = input.todos.map(todo => canonicalAuthorityObject(todo, "Monitor Todo snapshot item")); return planWriteback({...input, todos}); } -/** Public legacy adapter endpoint; the caller saves receipt and mutations in - * the same atomic state-file replacement. */ -export function planLegacyMonitorBatch(value: unknown): JsonObject { +/** Validate and replay one immutable pre-cutoff Markdown receipt. */ +export function replayLegacyMonitorBatch(value: unknown): JsonObject { try { - return planLegacyMonitorBatchRequest(value); + return replayLegacyMonitorBatchRequest(value); } catch (error) { if (error instanceof AuthorityStoreProtocolError) { throw new EffectRuntimeRequestError(error.message); @@ -176,62 +174,65 @@ export function planLegacyMonitorBatch(value: unknown): JsonObject { } } -function planLegacyMonitorBatchRequest(value: unknown): JsonObject { - const request = requireJsonObject(value, "legacy Monitor batch request"); - if (request.schema_version !== MONITOR_BATCH_PLAN_REQUEST_SCHEMA || !Array.isArray(request.todos)) { - throw new EffectRuntimeRequestError("legacy Monitor batch request schema mismatch"); +function historicalReplayInput(request: JsonObject): MonitorBatchInput { + const actor = request.actor_agent_id; + if (actor !== null && typeof actor !== "string") { + throw new EffectRuntimeRequestError("actor_agent_id must be a string or null"); } - const input = normalizeBatchInput(request as unknown as MonitorBatchInput); + return { + goal_id: requireAuthorityStoreId(request.goal_id, "goal id"), + operation_id: requireAuthorityStoreId(request.operation_id, "operation id"), + actor_agent_id: actor === null ? null : normalizeTodoAgent(actor, "actor_agent_id"), + registered_agents: [], + dry_run: requireBoolean(request.dry_run, "dry_run"), + ...normalizeMonitorPollFields(request.observation, request.intent), + todos: [], + }; +} + +function replayLegacyMonitorBatchRequest(value: unknown): JsonObject { + const request = requireJsonObject(value, "historical Monitor batch replay request"); + if (request.schema_version !== MONITOR_BATCH_PLAN_REQUEST_SCHEMA || + request.previous_receipt == null) { + throw new EffectRuntimeRequestError( + "historical Monitor batch replay requires an immutable previous receipt", + ); + } + const input = historicalReplayInput(request); const hash = monitorPollRequestHash(input); - if (request.previous_receipt != null) { - const previous = canonicalAuthorityObject(request.previous_receipt, "Monitor batch receipt"); - if (previous.schema_version !== MONITOR_BATCH_RECEIPT_SCHEMA || previous.goal_id !== input.goal_id || - previous.operation_id !== input.operation_id || previous.request_sha256 !== hash) { - throw new EffectRuntimeRequestError("monitor effect identity is already bound: Monitor batch receipt identity or original request mismatch"); - } - const writeback = canonicalAuthorityObject(previous.writeback, "Monitor batch receipt writeback"); - const expectedRoles = (["agent", "user"] as const).filter(role => { - const successor = input.intent[role === "agent" ? "next_agent_todo" : "next_user_todo"]; - return typeof successor === "string" && stripPythonWhitespace(successor).length > 0; - }); - if (writeback.schema_version !== "monitor_poll_todo_writeback_v0" || - writeback.goal_id !== input.goal_id || writeback.monitor_effect_id !== input.operation_id || - (input.observation.todo_id != null && writeback.todo_id !== input.observation.todo_id) || - writeback.result_hash !== input.observation.result_hash || - writeback.material_change !== input.observation.material_change || - writeback.last_checked_at !== input.observation.generated_at || - writeback.dry_run !== input.dry_run || - !Array.isArray(writeback.next_todos) || !Array.isArray(writeback.successor_receipts) || - writeback.next_todos.length !== writeback.successor_receipts.length || - writeback.next_todos.length !== expectedRoles.length) { - throw new EffectRuntimeRequestError("Monitor batch receipt writeback is malformed"); - } - const successorIds = new Set(); - for (const [index, todo] of writeback.next_todos.entries()) { - const item = canonicalAuthorityObject(todo, "Monitor successor receipt"); - const summary = canonicalAuthorityObject(writeback.successor_receipts[index], "Monitor successor summary"); - if (typeof item.todo_id !== "string" || !item.todo_id.startsWith("todo_") || - successorIds.has(item.todo_id) || item.todo_id !== summary.todo_id || - item.role !== expectedRoles[index] || summary.role !== expectedRoles[index]) { - throw new EffectRuntimeRequestError("Monitor batch receipt successor identity is malformed"); - } - successorIds.add(item.todo_id); - } - return {schema_version: MONITOR_BATCH_PLAN_RESULT_SCHEMA, mutations: [], - writeback: {...writeback, provider_replayed: true}, receipt: previous, replayed: true}; + const previous = canonicalAuthorityObject(request.previous_receipt, "Monitor batch receipt"); + if (previous.schema_version !== MONITOR_BATCH_RECEIPT_SCHEMA || previous.goal_id !== input.goal_id || + previous.operation_id !== input.operation_id || previous.request_sha256 !== hash) { + throw new EffectRuntimeRequestError("monitor effect identity is already bound: Monitor batch receipt identity or original request mismatch"); } - if (request.legacy_batch_version !== 1) { - throw new EffectRuntimeRequestError("legacy Monitor recovery needs reconciliation: preserve the pending receipt and inspect the original Monitor and successor Todos before retrying; frozen plan predates atomic batch receipts", "legacy_monitor_recovery_required"); + const writeback = canonicalAuthorityObject(previous.writeback, "Monitor batch receipt writeback"); + const expectedRoles = (["agent", "user"] as const).filter(role => { + const successor = input.intent[role === "agent" ? "next_agent_todo" : "next_user_todo"]; + return typeof successor === "string" && stripPythonWhitespace(successor).length > 0; + }); + if (writeback.schema_version !== "monitor_poll_todo_writeback_v0" || + writeback.goal_id !== input.goal_id || writeback.monitor_effect_id !== input.operation_id || + (input.observation.todo_id != null && writeback.todo_id !== input.observation.todo_id) || + writeback.result_hash !== input.observation.result_hash || + writeback.material_change !== input.observation.material_change || + writeback.last_checked_at !== input.observation.generated_at || + writeback.dry_run !== input.dry_run || + !Array.isArray(writeback.next_todos) || !Array.isArray(writeback.successor_receipts) || + writeback.next_todos.length !== writeback.successor_receipts.length || + writeback.next_todos.length !== expectedRoles.length) { + throw new EffectRuntimeRequestError("Monitor batch receipt writeback is malformed"); } - const todos = input.todos.map(todo => canonicalAuthorityObject(todo, "Monitor Todo snapshot item")); - if (todos.some(todo => todo.monitor_effect_id === input.operation_id)) { - throw new EffectRuntimeRequestError("Monitor effect already applied without its batch receipt; preserve the pending receipt and reconcile the original successor Todos", "legacy_monitor_recovery_required"); + const successorIds = new Set(); + for (const [index, todo] of writeback.next_todos.entries()) { + const item = canonicalAuthorityObject(todo, "Monitor successor receipt"); + const summary = canonicalAuthorityObject(writeback.successor_receipts[index], "Monitor successor summary"); + if (typeof item.todo_id !== "string" || !item.todo_id.startsWith("todo_") || + successorIds.has(item.todo_id) || item.todo_id !== summary.todo_id || + item.role !== expectedRoles[index] || summary.role !== expectedRoles[index]) { + throw new EffectRuntimeRequestError("Monitor batch receipt successor identity is malformed"); + } + successorIds.add(item.todo_id); } - // Legacy Monitor explicitly permits routing a successor to a registered peer. - // Canonical Monitor and ordinary Todo create keep their actor-owned claim rule. - const planned = planWriteback({...input, todos}, "registered_peer_handoff"); - const receipt = {schema_version: MONITOR_BATCH_RECEIPT_SCHEMA, goal_id: input.goal_id, - operation_id: input.operation_id, request_sha256: hash, writeback: planned.writeback}; - return {schema_version: MONITOR_BATCH_PLAN_RESULT_SCHEMA, mutations: planned.mutations, - writeback: planned.writeback, receipt, replayed: false}; + return {schema_version: MONITOR_BATCH_PLAN_RESULT_SCHEMA, mutations: [], + writeback: {...writeback, provider_replayed: true}, receipt: previous, replayed: true}; } diff --git a/loopx/control_plane/scheduler/monitor_batch_receipts.py b/loopx/control_plane/scheduler/monitor_batch_receipts.py index dee4757b99..323a04b905 100644 --- a/loopx/control_plane/scheduler/monitor_batch_receipts.py +++ b/loopx/control_plane/scheduler/monitor_batch_receipts.py @@ -38,7 +38,10 @@ def _read_receipt(text: str, operation_id: str) -> dict[str, Any] | None: def _plan(request: dict[str, Any], **facts: Any) -> dict[str, Any]: try: - result = effect_runtime_result("scheduler.monitor_batch.plan", {**request, **facts}) + result = effect_runtime_result( + "scheduler.monitor_batch.replay", + {**request, **facts}, + ) except EffectRuntimeRejected as error: raise ValueError(str(error)) from error if not isinstance(result, dict) or result.get("schema_version") != "loopx_monitor_batch_plan_result_v0": diff --git a/loopx/control_plane/scheduler/monitor_poll_writeback.py b/loopx/control_plane/scheduler/monitor_poll_writeback.py index a686881282..4118f88137 100644 --- a/loopx/control_plane/scheduler/monitor_poll_writeback.py +++ b/loopx/control_plane/scheduler/monitor_poll_writeback.py @@ -8,6 +8,11 @@ normalize_todo_id, ) from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result +from ..coordination.local_authority import ( + read_canonical_todos_if_promoted, + require_canonical_authority, +) +from ...paths import effective_runtime_root def resolve_monitor_todo_item( @@ -18,19 +23,24 @@ def resolve_monitor_todo_item( todo_id: str | None = None, target_key: str | None = None, ) -> dict[str, Any]: - from ...control_plane.todos.list_readback import list_goal_todos - normalized_todo_id = normalize_todo_id(todo_id) if todo_id else None safe_target_key = str(target_key or "").strip() if not normalized_todo_id and not safe_target_key: raise ValueError("monitor todo writeback requires --todo-id or --target-key") - payload = list_goal_todos( - registry_path=registry_path, + authority = read_canonical_todos_if_promoted( + runtime_root=effective_runtime_root( + registry_path, + str(runtime_root) if runtime_root is not None else None, + ), goal_id=goal_id, - role="agent", - runtime_root_arg=str(runtime_root) if runtime_root is not None else None, ) - items = payload.get("todos") if isinstance(payload.get("todos"), list) else [] + if authority is None: + require_canonical_authority(goal_id=goal_id, operation="monitor_poll") + items = [ + item + for item in authority["todos"] + if item.get("role") == "agent" and item.get("archive_state") != "archive" + ] try: result = effect_runtime_result("scheduler.monitor_target.select", { "schema_version": "loopx_monitor_target_request_v0", "items": items, @@ -71,11 +81,10 @@ def write_monitor_poll_todo_state( task_lease_idempotency_key: str | None = None, task_lease_expected_version: int | None = None, gate_scope_guard: bool = False, - legacy_batch_version: int | None = 1, + legacy_batch_version: int | None = None, ) -> dict[str, Any] | None: """Route one typed Monitor transaction to its owning storage adapter.""" from .monitor_batch_receipts import replay_legacy_monitor_poll - from .provider_monitor_poll import poll_canonical_monitor_if_promoted lease_proof = ({"idempotency_key": task_lease_idempotency_key, "expected_version": task_lease_expected_version} @@ -99,23 +108,19 @@ def write_monitor_poll_todo_state( "actor_agent_id": agent_id, "dry_run": not execute, "observation": observation, "intent": intent, "gate_scope_guard": gate_scope_guard, "legacy_batch_version": legacy_batch_version} + from .provider_monitor_poll import ( + poll_canonical_monitor_if_promoted, + require_monitor_poll_source_available, + ) + + require_monitor_poll_source_available( + runtime_root=runtime_root, + goal_id=goal_id, + ) if monitor_effect_id and lease_proof is None: # Legacy receipts are historical facts even after authority promotion. previous = replay_legacy_monitor_poll(registry_path=registry_path, request=request) if previous is not None: - from ..coordination.local_authority import local_authority_is_promoted - # A crash after the one Markdown install can leave its prepared - # shadow entry without a commit marker. The existing drain proves - # that source replacement and recovers it from durable readback. - # Historical receipts after promotion must not drain stale legacy - # state into the canonical authority. - if not local_authority_is_promoted(runtime_root=runtime_root, goal_id=goal_id): - from ..coordination.local_authority_shadow_adapter import drain_local_authority_shadow_outbox - from ..coordination.local_authority_shadow_outbox import outbox_root - - if outbox_root(runtime_root, goal_id).exists(): - drain_local_authority_shadow_outbox( - registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id) return previous canonical = poll_canonical_monitor_if_promoted( registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id, @@ -123,11 +128,4 @@ def write_monitor_poll_todo_state( lease_proof=lease_proof, gate_scope_guard=gate_scope_guard, observation=observation, intent=intent, ) - if canonical is not None: - return canonical - if lease_proof is not None: - raise ValueError("Monitor lease proof requires promoted canonical authority; no legacy write attempted") - from .legacy_monitor_poll import apply_legacy_monitor_poll - - return apply_legacy_monitor_poll( - registry_path=registry_path, runtime_root=runtime_root, request=request) + return canonical diff --git a/loopx/control_plane/scheduler/provider_monitor_poll.py b/loopx/control_plane/scheduler/provider_monitor_poll.py index 2f9222f74a..43001f161f 100644 --- a/loopx/control_plane/scheduler/provider_monitor_poll.py +++ b/loopx/control_plane/scheduler/provider_monitor_poll.py @@ -9,9 +9,11 @@ from ..coordination.authority_source_capture import authority_registry_source from ..coordination.local_authority import ( LOCAL_AUTHORITY_SOURCES, + LocalCoordinationAuthorityRejection, LocalCoordinationAuthorityUnavailable, - local_authority_is_promoted, + raise_canonical_write_failure, read_canonical_todos_if_promoted, + require_canonical_authority, ) from ..effect_runtime import ( CANONICAL_AUTHORITY_WRITE_TIMEOUT_SECONDS, @@ -24,7 +26,8 @@ def require_monitor_poll_source_available(*, runtime_root: Path, goal_id: str) -> None: """Fail closed on unavailable promoted authority before unrelated quota work.""" - read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id) + if read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id) is None: + require_canonical_authority(goal_id=goal_id, operation="monitor_poll") def poll_canonical_monitor_if_promoted( @@ -32,9 +35,7 @@ def poll_canonical_monitor_if_promoted( monitor_effect_id: str | None, agent_id: str | None, observation: dict[str, Any], intent: dict[str, Any], lease_proof: dict[str, Any] | None = None, gate_scope_guard: bool = False, -) -> dict[str, Any] | None: - if not local_authority_is_promoted(runtime_root=runtime_root, goal_id=goal_id): - return None +) -> dict[str, Any]: with authority_registry_source(registry_path) as registry_source: registered = registered_agent_ids_from_registry(registry_path, goal_id) result = effect_runtime_result("coordination.local_authority.monitor_poll", { @@ -56,6 +57,30 @@ def poll_canonical_monitor_if_promoted( or result.get("legacy_fallback_used") is not False or not isinstance(result.get("writeback"), dict)): payload = result if isinstance(result, dict) else {} + raise_canonical_write_failure( + payload, + goal_id=goal_id, + operation="monitor_poll", + ) + if ( + payload.get("status") == "failed" + and payload.get("reason_code") + == "coordination_operation_identity_mismatch" + ): + raise LocalCoordinationAuthorityRejection( + "monitor effect identity is already bound to a different observation", + code="coordination_operation_identity_mismatch", + payload=payload, + ) + if ( + payload.get("status") == "failed" + and payload.get("failure_kind") == "decision_rejection" + ): + raise LocalCoordinationAuthorityRejection( + str(payload.get("reason") or "canonical Monitor poll was rejected"), + code=str(payload.get("reason_code") or "monitor_poll_rejected"), + payload=payload, + ) raise LocalCoordinationAuthorityUnavailable( str(payload.get("reason") or "canonical Monitor transaction unavailable"), code=str(payload.get("reason_code") or "monitor_poll_unavailable"), payload=payload, diff --git a/loopx/control_plane/testing/actual_default_model_behavior_portfolio.py b/loopx/control_plane/testing/actual_default_model_behavior_portfolio.py index 8ac6dae391..b792cb3591 100644 --- a/loopx/control_plane/testing/actual_default_model_behavior_portfolio.py +++ b/loopx/control_plane/testing/actual_default_model_behavior_portfolio.py @@ -54,6 +54,7 @@ SELECTED_TODO_TOOL_FIXTURE_ACTION_TEXT, SELECTED_TODO_TOOL_FIXTURE_TODO_ID, ) +from .terminal_settlement_tool_behavior import TERMINAL_SETTLEMENT_FIXTURE_TODO_ID from .replan_vision_closeout_behavior import required_vision_scenario_contract ACTUAL_DEFAULT_MODEL_BEHAVIOR_PORTFOLIO_SCHEMA_VERSION = ( @@ -529,7 +530,7 @@ def _terminal_settlement_scenario_source() -> dict[str, Any]: payload = _turn_scenario_source(human_gate=False) payload["selected_todo"] = { **dict(payload["selected_todo"]), - "todo_id": "todo_terminal001", + "todo_id": TERMINAL_SETTLEMENT_FIXTURE_TODO_ID, "text": ( "Read `fixture/settlement-proof.json`, verify the bounded delivery, " "then settle this final Todo from the quota-projected plan. " @@ -910,7 +911,7 @@ def _validate_identity_scenario_contract( if spec.scenario_id == "turn_terminal_settlement": selected = dict(source_packet.get("selected_todo") or {}) if not ( - contract.get("selected_todo_id") == "todo_terminal001" + contract.get("selected_todo_id") == TERMINAL_SETTLEMENT_FIXTURE_TODO_ID and "settlement-proof.json" in str(selected.get("text") or "") ): raise ValueError( diff --git a/loopx/control_plane/testing/authority_e2e_fixtures.py b/loopx/control_plane/testing/authority_e2e_fixtures.py index ffe2283135..ca2aa94001 100644 --- a/loopx/control_plane/testing/authority_e2e_fixtures.py +++ b/loopx/control_plane/testing/authority_e2e_fixtures.py @@ -239,6 +239,108 @@ def build_goal_workspace( ) +def build_canonical_file_workspace(root: Path, *, goal_id: str) -> GoalWorkspace: + """Create a new Goal through the public CLI with File as its authority.""" + + repo = root / goal_id + repo.mkdir() + runtime_root = root / f"{goal_id}-runtime" + home = root / f"{goal_id}-home" + home.mkdir() + registry_path = root / f"{goal_id}-registry.json" + provisional = GoalWorkspace( + goal_id=goal_id, + root=root, + repo=repo, + registry_path=registry_path, + state_path=repo / "ACTIVE_GOAL_STATE.md", + runtime_root=runtime_root, + home=home, + runtime_root_binding="cli_override", + registry_runtime_root=runtime_root, + ) + defaults_path = root / f"{goal_id}-storage-defaults.json" + defaults_path.write_text( + json.dumps( + { + "schema_version": "loopx_goal_storage_defaults_v1", + "new_goal_provider": "file", + "canonical_creation": True, + "new_goal_handoff_mode": "hard_lease", + } + ), + encoding="utf-8", + ) + preview = run_cli( + provisional, + "machine-config", + "preview", + "--namespace", + "goal_storage", + "--config-json", + str(defaults_path), + ) + plan_revision = preview.get("plan_revision") + if not isinstance(plan_revision, str) or not plan_revision: + raise AssertionError("goal storage preview omitted its plan revision") + run_cli( + provisional, + "machine-config", + "apply", + "--namespace", + "goal_storage", + "--config-json", + str(defaults_path), + "--expected-plan-revision", + plan_revision, + "--execute", + ) + created = run_cli( + provisional, + "bootstrap", + "--project", + str(repo), + "--goal-id", + goal_id, + "--objective", + "Qualify canonical File authority through the public CLI.", + "--no-global-sync", + ) + selection = created.get("storage_selection") + if not isinstance(selection, dict) or ( + selection.get("provider") != "file" + or selection.get("authority_initialized") is not True + or selection.get("legacy_fallback_used") is not False + ): + raise AssertionError("bootstrap did not initialize canonical File authority") + state_value = created.get("state_file") + if not isinstance(state_value, str) or not state_value: + raise AssertionError("bootstrap omitted the canonical projection path") + workspace = GoalWorkspace( + goal_id=goal_id, + root=root, + repo=repo, + registry_path=registry_path, + state_path=Path(state_value), + runtime_root=runtime_root, + home=home, + runtime_root_binding="cli_override", + registry_runtime_root=runtime_root, + ) + run_cli( + workspace, + "configure-goal", + "--goal-id", + goal_id, + "--registered-agent", + "agent-a", + "--registered-agent", + "agent-b", + "--execute", + ) + return workspace + + def build_legacy_migration_source( root: Path, *, @@ -534,6 +636,7 @@ def tap_summary( "RUNTIME_ROOT_BINDINGS", "TS_READBACK_PROBE", "TapSummary", + "build_canonical_file_workspace", "build_goal_workspace", "build_legacy_migration_source", "cli_command", diff --git a/loopx/control_plane/testing/authority_e2e_ladder.py b/loopx/control_plane/testing/authority_e2e_ladder.py index ff7350c89b..ccf069a7c5 100644 --- a/loopx/control_plane/testing/authority_e2e_ladder.py +++ b/loopx/control_plane/testing/authority_e2e_ladder.py @@ -4,8 +4,8 @@ is green only when every selected row passed. A row that cannot run in this environment (no PostgreSQL, no NoKV stack, no POSIX signals) reports ``unverified`` and, unless ``--allow-unverified`` is given, the ladder exits -non-zero. Rows for stages that later PRs will land are declared as -``pending`` so the report never silently claims them. +non-zero. Future obligations may be declared as ``pending`` so the report never +silently claims them. Rows drive the product through ``python -m loopx.cli`` (``real_cli``) or run the retained store-level probes (``store_direct``); this module adds no @@ -39,27 +39,12 @@ sha256_hex, unverified, ) -from .authority_e2e_rows_stage2c import row_retired_observation_upgrade -from .authority_e2e_rows_stage2c2 import ( - capture_workspace, - delivered, - row_archive_after_leased_completion_parity, - row_drain_idempotent, - row_event_only_todo_source_holds, - row_growth_measurement_gate, - row_migration_seeds_and_drains, - row_outbox_prepared_then_committed_entries, - row_parity_divergent_detects_foreign_edit, - row_parity_equal, - row_rollback_with_pending_entries, - row_sigkill_between_primary_write_and_drain, - row_sigkill_mid_drain, -) from .authority_e2e_fixtures import ( REPO_ROOT, CliOutputError, TS_READBACK_PROBE, JsonObject, + build_canonical_file_workspace, node_executable, parse_json_object, run_cli, @@ -70,7 +55,7 @@ REPORT_SCHEMA = "loopx_shared_goal_authority_e2e_report_v0" LIST_SCHEMA = "loopx_shared_goal_authority_e2e_rows_v0" -STAGES: tuple[str, ...] = ("0", "1", "2a", "2b", "2c1", "2c2") +STAGES: tuple[str, ...] = ("0", "1", "2a", "2b") PRODUCT_PATHS: tuple[str, ...] = ("real_cli", "store_direct") GATES: tuple[str, ...] = ( "deterministic", @@ -137,8 +122,6 @@ Path("loopx") / "control_plane" / "testing" / "authority_e2e_ladder.py", Path("loopx") / "control_plane" / "testing" / "authority_e2e_fixtures.py", Path("loopx") / "control_plane" / "testing" / "authority_e2e_row_support.py", - Path("loopx") / "control_plane" / "testing" / "authority_e2e_rows_stage2c.py", - Path("loopx") / "control_plane" / "testing" / "authority_e2e_rows_stage2c2.py", ) MINIMUM_POSTGRES_TAP_PASSES = 9 @dataclass(frozen=True) @@ -249,8 +232,15 @@ def _row_local_conformance(context: RowContext, provider: str) -> RowOutcome: def _row_cli_document_decodes_through_ts_store(context: RowContext) -> RowOutcome: if node_executable() is None: return unverified("node_missing") - workspace = capture_workspace(context, "ladder-s1") - added = add_todo(workspace, "Decode this source transaction through the TypeScript store.") + workspace = build_canonical_file_workspace( + context.root, + goal_id=unique_goal_id("ladder-s1"), + ) + added = add_todo( + workspace, + "Decode this source transaction through the TypeScript store.", + claimed_by=AGENT_A, + ) todo_id = str(added["todo_id"]) acquired = acquire_lease( workspace, @@ -258,6 +248,9 @@ def _row_cli_document_decodes_through_ts_store(context: RowContext) -> RowOutcom owner=AGENT_A, idempotency_key="ladder-s1-lease", ) + lease = acquired.get("lease") + expect(isinstance(lease, dict), "task-lease acquire must return its canonical lease") + assert isinstance(lease, dict) updated = run_cli( workspace, "todo", @@ -270,10 +263,18 @@ def _row_cli_document_decodes_through_ts_store(context: RowContext) -> RowOutcom "A public-safe note recorded after the lease.", "--agent-id", AGENT_A, + "--task-lease-idempotency-key", + str(lease["idempotency_key"]), + "--task-lease-expected-version", + str(lease["version"]), ) for label, payload in (("todo add", added), ("task-lease acquire", acquired), ("todo update", updated)): - delivered(payload, label=label) - probe = ts_readback(workspace, directory=workspace.runtime_root / "authority-shadow" / "file-v0") + expect(payload.get("source_authority") == "file_v0", f"{label} must use canonical File authority") + expect(payload.get("decision_read_from_provider") is True, f"{label} must read provider authority") + expect(payload.get("legacy_fallback_used") is False, f"{label} must not use a legacy fallback") + expect("coordination_runtime_shadow" not in payload, f"{label} must not emit retired capture evidence") + directory = workspace.runtime_root / "authority" / "file-v0" + probe = ts_readback(workspace, directory=directory) expect(probe is not None, "read-back probe requires node") assert probe is not None load, scan = probe.get("load"), probe.get("scan") @@ -287,7 +288,7 @@ def _row_cli_document_decodes_through_ts_store(context: RowContext) -> RowOutcom expect(scan.get("cursors") == ["1", "2", "3", "4"], "transactions must retain source order") assert isinstance(operations, list) receipt_probe = ts_readback(workspace, receipt=str(operations[1]), - directory=workspace.runtime_root / "authority-shadow" / "file-v0") + directory=directory) expect(isinstance(receipt_probe, dict), "receipt probe must load") assert isinstance(receipt_probe, dict) receipt = receipt_probe.get("receipt") @@ -524,124 +525,9 @@ def _row_postgresql_conformance_live(context: RowContext) -> RowOutcome: posix_only=False, run=_row_postgresql_conformance_live, ), - LadderRow( - id="s2c1.retired_observation_upgrade", - stage="2c1", - title="Retired settings cannot write; explicit replacement bootstrap captures the next transaction", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_retired_observation_upgrade, - ), - LadderRow( - id="s2c2.outbox_prepared_then_committed_entries", - stage="2c2", - title="Python and TypeScript writers leave prepared records with committed markers that one drain delivers once", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_outbox_prepared_then_committed_entries, - ), - LadderRow( - id="s2c2.drain_idempotent", - stage="2c2", - title="Bounded drains are cumulative, an idle drain changes nothing, and a writer replay mints no entry", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_drain_idempotent, - ), - LadderRow( - id="s2c2.sigkill_between_primary_write_and_drain", - stage="2c2", - title="A SIGKILL around the primary replace leaves a prepared-only entry that drain settles from the primary bytes", - product_path="real_cli", - gate="deterministic", - posix_only=True, - run=row_sigkill_between_primary_write_and_drain, - ), - LadderRow( - id="s2c2.sigkill_mid_drain", - stage="2c2", - title="A SIGKILL inside the inline drain is recovered from exact receipts without a second delivery", - product_path="real_cli", - gate="deterministic", - posix_only=True, - run=row_sigkill_mid_drain, - ), - LadderRow( - id="s2c2.rollback_with_pending_entries", - stage="2c2", - title="Rollback archives pending entries, holds capture until rebootstrap, and replays its historical result", - product_path="real_cli", - gate="deterministic", - posix_only=True, - run=row_rollback_with_pending_entries, - ), - LadderRow( - id="s2c2.parity_equal", - stage="2c2", - title="Sustained interleaving of Python and TypeScript writers keeps every bounded qualification matched", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_parity_equal, - ), - LadderRow( - id="s2c2.parity_divergent_detects_foreign_edit", - stage="2c2", - title="A direct primary edit is reported as drift, holds later captures, and recovers only by rollback and rebootstrap", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_parity_divergent_detects_foreign_edit, - ), - LadderRow( - id="s2c2.event_only_todo_source_holds", - stage="2c2", - title="A retired Todo event source refuses reads and primary writes without changing either source", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_event_only_todo_source_holds, - ), - LadderRow( - id="s2c2.migration_seeds_and_drains", - stage="2c2", - title="migrate-state refuses an active capture source; after rollback the migrated goal bootstraps a fresh lineage that drains", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_migration_seeds_and_drains, - ), - LadderRow( - id="s2c2.growth_measurement_gate", - stage="2c2", - title="file-v0 history growth is measured per transaction and gated on retention integrity, claiming no capacity horizon", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_growth_measurement_gate, - ), - LadderRow( - id="s2c2.archive_after_leased_completion_parity", - stage="2c2", - title="archiving a Todo whose released lease stays on disk keeps the candidate head matched and qualifiable", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_archive_after_leased_completion_parity, - ), ) -PENDING_ROWS: tuple[PendingRow, ...] = ( - PendingRow( - "s2c2.sustained_parity_soak", - "2c2", - "a >=10-day synthetic-goal soak of the selected local profile (RFC section 7.2, lane L); " - "bounded qualification reports sustained_parity_verdict=not_evaluated", - ), -) +PENDING_ROWS: tuple[PendingRow, ...] = () def row_by_id(row_id: str) -> LadderRow: diff --git a/loopx/control_plane/testing/authority_e2e_row_support.py b/loopx/control_plane/testing/authority_e2e_row_support.py index 50fcfc7233..c2dbd8f801 100644 --- a/loopx/control_plane/testing/authority_e2e_row_support.py +++ b/loopx/control_plane/testing/authority_e2e_row_support.py @@ -59,7 +59,12 @@ def sha256_hex(value: str | bytes) -> str: return hashlib.sha256(payload).hexdigest() -def add_todo(workspace: GoalWorkspace, text: str) -> JsonObject: +def add_todo( + workspace: GoalWorkspace, + text: str, + *, + claimed_by: str | None = None, +) -> JsonObject: return run_cli( workspace, "todo", @@ -72,6 +77,7 @@ def add_todo(workspace: GoalWorkspace, text: str) -> JsonObject: text, "--task-class", "advancement_task", + *(("--claimed-by", claimed_by) if claimed_by else ()), ) diff --git a/loopx/control_plane/testing/authority_e2e_rows_stage2c.py b/loopx/control_plane/testing/authority_e2e_rows_stage2c.py deleted file mode 100644 index 4090cc8b1d..0000000000 --- a/loopx/control_plane/testing/authority_e2e_rows_stage2c.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Upgrade acceptance for the retired, post-commit observation lineage.""" -from __future__ import annotations - -from .authority_e2e_fixtures import build_goal_workspace, run_cli, unique_goal_id -from .authority_e2e_row_support import RowContext, RowOutcome, add_todo, expect, passed -from .authority_e2e_rows_stage2c2 import bootstrap_capture, delivered, goal_cli - - -def row_retired_observation_upgrade(context: RowContext) -> RowOutcome: - workspace = build_goal_workspace(context.root, goal_id=unique_goal_id("retired-observer"), - handoff_mode="hard_lease", shadow_enabled=True, - runtime_root_binding="cli_override") - original = workspace.registry_path.read_bytes() - rejected = goal_cli(workspace, "configure-goal", "--local-authority-shadow-file", "--execute", check=False) - expect(rejected.get("ok") is False and "local_authority_shadow_retired" in str(rejected.get("error")), - "retired enable must fail with actionable context") - expect(workspace.registry_path.read_bytes() == original, "rejection must not change configuration") - added = add_todo(workspace, "Continue work before explicit capture upgrade.") - expect("authority_shadow" not in added, "a retained setting cannot restore observation writes") - status = goal_cli(workspace, "authority-shadow", "status") - expect(status["config"]["status"] == "retired" and status["config"]["enabled"] is False, - "status must disclose the inactive historical configuration") - expect(not (workspace.runtime_root / "authority-shadow").exists(), "primary work must create no shadow lineage") - run_cli(workspace, "configure-goal", "--goal-id", workspace.goal_id, - "--clear-local-authority-shadow", "--coordination-runtime-shadow-file", "--execute") - expect(not (workspace.runtime_root / "authority-shadow" / "file-v0").exists(), - "configuration must not implicitly bootstrap a source lineage") - bootstrap_capture(workspace) - delivered(add_todo(workspace, "Capture a transaction after explicit bootstrap."), label="upgraded write") - expect(not workspace.shadow_directory.exists(), "replacement capture must not recreate historical observation storage") - return passed(retired_enable_rejected=True, implicit_capture=False, explicit_bootstrap=True) diff --git a/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py b/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py deleted file mode 100644 index dfd6df21a4..0000000000 --- a/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py +++ /dev/null @@ -1,1231 +0,0 @@ -"""Stage 2C parity-half rows (``s2c2.*``) of the shared-goal-authority ladder. - -Every row drives the product through the real ``python -m loopx.cli`` against -one goal whose ``coordination.runtime_shadow`` capture is explicitly enabled -and bootstrapped, and asserts only through shipped operator interfaces: -``authority-shadow status|drain``, ``coordination-shadow bootstrap|inspect| -qualify|read-candidate|rollback`` and ``migrate-state``. Candidate history is -read back through the retained TypeScript store (the adapter's read RPC). The -rows add no product path and never read the candidate for a decision. - -Two scheduling-only seams exist, both outside every product decision: - -* holding the stable maintenance lock defers a writer's post-commit drain, so - its committed outbox entry stays pending (``drain_lock_busy``); -* a POSIX crash worker pauses a real CLI process at one persistence window and - the row SIGKILLs it there. It substitutes no result and edits no byte. -""" - -from __future__ import annotations - -import json -import select -import subprocess -import sys -from collections.abc import Iterator, Mapping, Sequence -from contextlib import contextmanager -from dataclasses import dataclass, field -from pathlib import Path - -from ...file_lock import exclusive_cross_runtime_file_lock -from ..coordination import local_authority_shadow_outbox as shadow_outbox -from .authority_e2e_fixtures import ( - REPO_ROOT, - GoalWorkspace, - JsonObject, - build_goal_workspace, - cli_env, - node_executable, - parse_json_object, - run_cli, - unique_goal_id, -) -from .authority_e2e_row_support import ( - AGENT_A, - AGENT_B, - RowContext, - RowOutcome, - acquire_lease, - add_todo, - expect, - lease_version, - passed, -) - -RUNTIME_SHADOW_CONFIG_SCHEMA = "loopx_coordination_runtime_shadow_config_v0" -RUNTIME_SHADOW_PROVIDER = "file_v0" -CRASH_BARRIER_TIMEOUT_SECONDS = 30.0 -PRIMARY_CRASH_WINDOWS: tuple[tuple[str, bool], ...] = ( - ("before_replace", False), - ("after_replace", True), - ("before_marker", True), -) -DRAIN_CRASH_WINDOWS: tuple[str, ...] = ("before_commit", "after_commit", "after_cursor", "between_unlinks") -PARITY_CYCLES = 3 -# ``todo archive-completed`` stays out of this shared cross-writer parity set -# because it is a Python-only lifecycle writer with no TypeScript counterpart to -# interleave; the archive-after-lease fold is pinned by its own deterministic row -# ``s2c2.archive_after_leased_completion_parity`` instead. -PARITY_REQUIRED_WRITE_CLASSES: tuple[str, ...] = ( - "todo_add", - "todo_update", - "todo_complete", - "todo_supersede", - "task_lease_acquire", - "task_lease_renew", - "task_lease_transfer", - "task_lease_fence_close", -) -GROWTH_TRANSACTIONS = 10 -# The archive-after-lease row delivers six transactions: the add, the lease -# acquire, the fenced complete (which also closes the lease fence), the anchor -# add, and the archive that retires the completed Todo from the graph. -ARCHIVE_PARITY_OPERATIONS = 6 -# The row's own coverage: the Todo add, the lease acquire, the fenced complete -# with its lease fence close, and the archive-completed writer. -ARCHIVE_PARITY_REQUIRED_WRITE_CLASSES: tuple[str, ...] = ( - "todo_add", - "todo_complete", - "todo_archive_completed", - "task_lease_acquire", - "task_lease_fence_close", -) -GROWTH_TEXT_TEMPLATE = "Growth workload todo %02d " + "x" * 160 -# Each file-v0 transaction retains the complete projection, so the per-transaction -# byte delta may grow by about one Todo record per transaction. A larger jump -# means something beyond the live projection is being re-published. -GROWTH_DELTA_ACCELERATION_ENVELOPE_BYTES = 2048 -EVENT_ONLY_HOLD = "legacy_todo_event_source_retired" -CONTINUITY_HOLD = "source_partition_continuity_unproved" -SHADOW_READ_MODULE = Path("loopx") / "control_plane" / "coordination" / "local_authority_shadow.ts" -SHADOW_READ_REQUEST_SCHEMA = "loopx_coordination_runtime_shadow_outbox_read_v0" -SHADOW_READ_SCAN_LIMIT = 10_000 - -# A real CLI process that pauses at exactly one persistence window. The parent -# waits for the BARRIER line and SIGKILLs the child there. Every hook forwards -# to the production function; nothing decides an outcome or substitutes bytes. -CRASH_WORKER = r""" -import json, pathlib, sys, time -from loopx.cli import main -from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox -from loopx.control_plane.runtime import document_io -window, state = sys.argv[1], pathlib.Path(sys.argv[2]) -def pause(payload=None): - print('BARRIER ' + json.dumps(payload or {}), flush=True) - time.sleep(40) - raise RuntimeError('parent failed to terminate at persistence barrier') -actual_rpc = adapter.effect_runtime_result -def rpc(method, request, **kwargs): - if method == 'coordination.runtime_shadow.drain' and window in {'before_commit', 'after_commit', 'after_cursor', 'between_unlinks'}: - import subprocess - child = subprocess.Popen(['node', '--no-warnings', '--experimental-strip-types', - 'loopx/control_plane/testing/shadow_drain_fault_process.ts', window], - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - child.stdin.write(json.dumps(request)); child.stdin.close() - for line in child.stdout: - if line.startswith('BARRIER '): - print(line, end='', flush=True) - sys.stdin.readline() # Parent requests death at the observed barrier. - child.kill(); child.wait(timeout=10) - print('REAPED', flush=True) - time.sleep(40) - raise RuntimeError('parent failed to terminate crash worker') - raise RuntimeError('native barrier missing: ' + child.stderr.read()) - return actual_rpc(method, request, **kwargs) -adapter.effect_runtime_result = rpc -actual_json = outbox.durable_write_json -def write_json(path, value): - if window == 'before_marker' and path.name.endswith('.committed.json'): pause() - return actual_json(path, value) -outbox.durable_write_json = write_json -actual_replace = document_io.os.replace -def replace(source, target): - is_primary = pathlib.Path(target) == state - if is_primary and window == 'before_replace': pause() - result = actual_replace(source, target) - if is_primary and window == 'after_replace': pause() - return result -document_io.os.replace = replace -actual_unlink = pathlib.Path.unlink -def unlink(path, *args, **kwargs): - result = actual_unlink(path, *args, **kwargs) - if window == 'between_unlinks' and path.name.endswith('.prepared.json'): pause() - return result -pathlib.Path.unlink = unlink -raise SystemExit(main(sys.argv[3:])) -""" - - -# --------------------------------------------------------------------------- -# Workspace, CLI and read-back helpers -# --------------------------------------------------------------------------- - - -def _object(value: object, label: str) -> JsonObject: - expect(isinstance(value, dict), f"{label} must be an object") - assert isinstance(value, dict) - return {str(key): item for key, item in value.items()} - - -def _list(value: object, label: str) -> list[object]: - expect(isinstance(value, list), f"{label} must be a list") - assert isinstance(value, list) - return list(value) - - -def set_runtime_shadow(workspace: GoalWorkspace, *, enabled: bool) -> None: - """The operator step: enable or disable transaction capture in the registry.""" - - registry = parse_json_object(workspace.registry_path.read_text(encoding="utf-8")) - goals = _list(registry.get("goals"), "registry goals") - goal = _object(goals[0], "registry goal") - coordination = _object(goal.get("coordination"), "goal coordination") - coordination["runtime_shadow"] = { - "schema_version": RUNTIME_SHADOW_CONFIG_SCHEMA, - "enabled": enabled, - "provider": RUNTIME_SHADOW_PROVIDER, - } - goal["coordination"] = coordination - goals[0] = goal - registry["goals"] = goals - workspace.registry_path.write_text(json.dumps(registry), encoding="utf-8") - - -def goal_cli(workspace: GoalWorkspace, *args: str, check: bool = True) -> JsonObject: - return run_cli(workspace, *args, "--goal-id", workspace.goal_id, check=check) - - -def bootstrap_capture(workspace: GoalWorkspace) -> JsonObject: - """Bootstrap the capture lineage; returns the whole CLI payload.""" - - payload = goal_cli(workspace, "coordination-shadow", "bootstrap", "--execute") - bootstrap = _object(payload.get("bootstrap"), "bootstrap") - expect(bootstrap.get("status") == "applied", "bootstrap must apply a fresh lineage") - expect(bootstrap.get("cursor") == "1", "bootstrap must be the first transaction") - expect(payload.get("decision_read_from_shadow") is False, "bootstrap must not read the candidate for a decision") - return payload - - -def capture_workspace( - context: RowContext, - prefix: str, - *, - handoff_mode: str = "hard_lease", - bootstrap: bool = True, -) -> GoalWorkspace: - workspace = build_goal_workspace( - context.root, - goal_id=unique_goal_id(prefix), - handoff_mode=handoff_mode, - shadow_enabled=False, - runtime_root_binding="cli_override", - ) - set_runtime_shadow(workspace, enabled=True) - if bootstrap: - bootstrap_capture(workspace) - return workspace - - -def capture_evidence(payload: Mapping[str, object], *, label: str) -> JsonObject: - evidence = _object(payload.get("coordination_runtime_shadow"), f"{label} capture evidence") - expect(evidence.get("primary_writeback_preserved") is True, f"{label} must preserve the primary writeback") - expect(evidence.get("provider_to_local_writes") is False, f"{label} must never write from provider to local state") - expect(evidence.get("candidate_read_for_decision") is False, f"{label} must never read the candidate for a decision") - expect(evidence.get("parity_verdict") == "not_evaluated", f"{label} must not claim parity on the write path") - return evidence - - -def delivered(payload: Mapping[str, object], *, label: str) -> JsonObject: - """A primary write whose outbox entry was drained inline into the candidate.""" - - evidence = capture_evidence(payload, label=label) - expect(evidence.get("outcome") == "delivered", f"{label} must deliver its outbox entry") - expect( - evidence.get("source_transaction_correlated") is True and evidence.get("durable_source_outbox") is True, - f"{label} must correlate a durable outbox entry", - ) - drain = _object(evidence.get("drain"), f"{label} drain") - expect(drain.get("outcome") == "drained" and drain.get("candidate_readback_verified") is True, f"{label} must verify its candidate read-back") - return evidence - - -def no_transaction(payload: Mapping[str, object], *, label: str) -> JsonObject: - evidence = capture_evidence(payload, label=label) - expect(evidence.get("outcome") == "no_transaction", f"{label} must record no transaction") - expect(_object(evidence.get("entry"), f"{label} entry").get("entry_id") is None, f"{label} must mint no outbox entry") - return evidence - - -def deferred(payload: Mapping[str, object], *, label: str) -> JsonObject: - evidence = capture_evidence(payload, label=label) - expect( - evidence.get("outcome") == "drain_deferred" and evidence.get("reason_code") == "drain_lock_busy", - f"{label} must defer its drain behind the held maintenance lock", - ) - expect( - evidence.get("source_transaction_correlated") is True and evidence.get("durable_source_outbox") is True, - f"{label} must still correlate a durable outbox entry", - ) - return evidence - - -def shadow_status(workspace: GoalWorkspace) -> JsonObject: - return goal_cli(workspace, "authority-shadow", "status", check=False) - - -def backlog(status: Mapping[str, object], partition: str) -> JsonObject: - return _object(_object(status.get("outbox"), "status outbox").get(partition), f"{partition} backlog") - - -def candidate(status: Mapping[str, object]) -> JsonObject: - return _object(status.get("candidate"), "status candidate") - - -def management_status(status: Mapping[str, object]) -> str: - return str(_object(status.get("management"), "status management").get("status")) - - -def drain(workspace: GoalWorkspace, *flags: str) -> JsonObject: - return goal_cli(workspace, "authority-shadow", "drain", *flags, check=False) - - -def inspect(workspace: GoalWorkspace) -> JsonObject: - payload = goal_cli(workspace, "coordination-shadow", "inspect", check=False) - expect(payload.get("decision_read_from_shadow") is False, "inspect must not read the candidate for a decision") - return payload - - -def qualify(workspace: GoalWorkspace, *flags: str) -> JsonObject: - payload = goal_cli(workspace, "coordination-shadow", "qualify", *flags, check=False) - expect(payload.get("decision_read_from_shadow") is False, "qualify must not read the candidate for a decision") - return payload - - -def read_candidate(workspace: GoalWorkspace, todo_id: str) -> JsonObject: - payload = goal_cli(workspace, "coordination-shadow", "read-candidate", "--todo-id", todo_id, check=False) - expect(payload.get("decision_read_from_shadow") is False, "read-candidate must not read the candidate for a decision") - return payload - - -def qualified(payload: Mapping[str, object], *, label: str) -> JsonObject: - qualification = _object(payload.get("qualification"), f"{label} qualification") - expect(payload.get("ok") is True and qualification.get("status") == "qualified", f"{label} must qualify") - expect(qualification.get("parity_matches") is True and qualification.get("scope") == "bounded", f"{label} must be a bounded parity match") - expect( - qualification.get("sustained_parity_verified") is False and qualification.get("sustained_parity_verdict") == "not_evaluated", - f"{label} must not claim sustained parity", - ) - return qualification - - -def rejected(payload: Mapping[str, object], key: str, *, label: str) -> JsonObject: - result = _object(payload.get(key), f"{label} result") - expect(payload.get("ok") is False and result.get("qualified") is False, f"{label} must be rejected") - return result - - -def history(workspace: GoalWorkspace) -> list[JsonObject]: - """Complete candidate history through the TypeScript shadow read over the retained store. - - The read runs in an independent node process so the row never imports the - Python adapter; it is the same ``readLocalAuthorityShadow`` the product - uses for status and drain proof. - """ - - node = node_executable() - expect(node is not None, "node is required to read the candidate history") - assert node is not None - request = { - "schema_version": SHADOW_READ_REQUEST_SCHEMA, - "runtime_root": str(workspace.runtime_root), - "goal_id": workspace.goal_id, - "scan_limit": SHADOW_READ_SCAN_LIMIT, - } - request_path = workspace.home / "shadow-read-request.json" - request_path.write_text(json.dumps(request), encoding="utf-8") - script = ( - f"import {{ readLocalAuthorityShadow }} from {json.dumps((REPO_ROOT / SHADOW_READ_MODULE).as_uri())};" - "import { readFile } from 'node:fs/promises';" - "process.stdout.write(JSON.stringify(await readLocalAuthorityShadow(JSON.parse(await readFile(process.argv[1], 'utf8')))));" - ) - completed = subprocess.run( - [node, "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", script, str(request_path)], - cwd=REPO_ROOT, - capture_output=True, - text=True, encoding="utf-8", errors="replace", - timeout=60, - check=False, - ) - expect(completed.returncode == 0, f"the TypeScript shadow read exited {completed.returncode}") - view = parse_json_object(completed.stdout) - expect(view.get("status") == "loaded", "candidate history must load through the TypeScript store") - proof = _object(view.get("proof"), "history proof") - return [_object(item, "transaction") for item in _list(proof.get("transactions"), "history transactions")] - - -def single_receipt(transaction: Mapping[str, object], *, label: str) -> JsonObject: - receipts = _list(transaction.get("receipts"), f"{label} receipts") - expect(len(receipts) == 1, f"{label} must carry exactly one receipt") - return _object(receipts[0], f"{label} receipt") - - -def committed_receipt(transaction: Mapping[str, object], *, label: str) -> JsonObject: - receipt = single_receipt(transaction, label=label) - expect(receipt.get("resolution") == "committed" and receipt.get("no_op") is False, f"{label} must be a committed, effective receipt") - return receipt - - -def projected_todo_count(transaction: Mapping[str, object]) -> int: - projection = _object(transaction.get("projection"), "transaction projection") - todos = projection.get("todos") - return len(todos) if isinstance(todos, list) else 0 - - -def partition_files(workspace: GoalWorkspace, partition: str) -> list[str]: - directory = shadow_outbox.partition_directory(workspace.runtime_root, workspace.goal_id, partition) - return sorted(path.name for path in directory.iterdir()) if directory.is_dir() else [] - - -def phase_count(names: Sequence[str], phase: str) -> int: - return sum(1 for name in names if name.endswith(f".{phase}.json")) - - -def outbox_present(workspace: GoalWorkspace) -> bool: - return shadow_outbox.outbox_root(workspace.runtime_root, workspace.goal_id).exists() - - -def store_documents(workspace: GoalWorkspace) -> list[Path]: - return sorted((workspace.runtime_root / "authority-shadow" / "file-v0").glob("authority-store-*.json")) - - -def todo_count(workspace: GoalWorkspace) -> int: - listed = goal_cli(workspace, "todo", "list") - return len(_list(listed.get("todos"), "todo list")) - - -@contextmanager -def hold_drain_lock(workspace: GoalWorkspace) -> Iterator[None]: - """Hold the maintenance lock so writers defer their post-commit drain. - - The native batch takes the TypeScript mutation marker, so the window must - hold the same cross-runtime lock the production readers take; a kernel-only - flock would no longer exclude it. - """ - - with exclusive_cross_runtime_file_lock( - shadow_outbox.drain_lock_target(workspace.runtime_root, workspace.goal_id), - operation="e2e_window", - ): - yield - - -def crash_cli(workspace: GoalWorkspace, window: str, *args: str) -> None: - """Run one real CLI command and SIGKILL it at ``window``.""" - - command = [sys.executable, "-c", CRASH_WORKER, window, str(workspace.state_path), *workspace.cli_prefix(), *args, "--goal-id", workspace.goal_id] - process = subprocess.Popen( - command, - cwd=REPO_ROOT, - env=cli_env(workspace), - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, encoding="utf-8", errors="replace", - ) - line = "" - try: - assert process.stdout is not None - readable, _, _ = select.select([process.stdout], [], [], CRASH_BARRIER_TIMEOUT_SECONDS) - if readable: - line = process.stdout.readline() - finally: - if line.startswith("BARRIER "): - barrier = json.loads(line.removeprefix("BARRIER ")) - if barrier.get("native_pid"): - assert process.stdin is not None and process.stdout is not None - process.stdin.write("terminate_native\n") - process.stdin.flush() - readable, _, _ = select.select([process.stdout], [], [], 10) - expect(bool(readable) and process.stdout.readline().strip() == "REAPED", "native owner must be reaped") - process.kill() - _, stderr = process.communicate(timeout=10) - expect(line.startswith("BARRIER "), f"{window}: the CLI did not reach its persistence window: {stderr[-200:]}") - expect(process.returncode == -9, f"{window}: the crash worker must die by SIGKILL") - - -# --------------------------------------------------------------------------- -# Rows -# --------------------------------------------------------------------------- - - -def row_outbox_prepared_then_committed_entries(context: RowContext) -> RowOutcome: - """A Python Todo writer and a TypeScript lease writer each leave one prepared record and committed marker; drain delivers both once.""" - - workspace = capture_workspace(context, "ladder-outbox") - with hold_drain_lock(workspace): - added = add_todo(workspace, "Prepared and committed while the drain lock is held.") - expect(added.get("added") is True, "the todo must commit while its drain is deferred") - todo_id = str(added["todo_id"]) - acquired = acquire_lease(workspace, todo_id=todo_id, owner=AGENT_A, idempotency_key="ladder-outbox-lease") - expect(acquired.get("acquired") is True, "the lease must commit while its drain is deferred") - entries = [ - _object(deferred(added, label="todo add").get("entry"), "todo entry"), - _object(deferred(acquired, label="task-lease acquire").get("entry"), "lease entry"), - ] - expect([entry.get("partition") for entry in entries] == ["todos", "leases"], "the writers must target their own partitions") - expect(all(entry.get("seq") == 1 for entry in entries), "each partition must start at sequence 1") - status = shadow_status(workspace) - for partition in ("todos", "leases"): - pending = backlog(status, partition) - expect( - pending.get("committed_pending") == 1 and pending.get("prepared_only") == 0 and pending.get("cursor_last_seq") is None, - f"{partition} must hold exactly one committed, undrained entry", - ) - names = partition_files(workspace, partition) - expect( - phase_count(names, "prepared") == 1 and phase_count(names, "committed") == 1, - f"{partition} must retain its prepared record and committed marker", - ) - expect(todo_count(workspace) == 1, "the primary must show the committed todo while its entry is pending") - drained = drain(workspace) - expect( - drained.get("ok") is True and drained.get("outcome") == "drained" and drained.get("delivered") == 2 and drained.get("replayed") == 0, - "one drain must deliver both pending entries exactly once", - ) - settled = [_object(item, "drained entry") for item in _list(drained.get("entries"), "drain entries")] - expect( - all(item.get("outcome") == "delivered" and item.get("resolution") == "committed" for item in settled), - "every drained entry must be a committed delivery", - ) - status = shadow_status(workspace) - for partition in ("todos", "leases"): - pending = backlog(status, partition) - expect(pending.get("committed_pending") == 0 and pending.get("prepared_only") == 0 and pending.get("cursor_last_seq") == 1, f"{partition} must be drained to sequence 1") - expect(partition_files(workspace, partition) == ["drain-cursor.json"], f"{partition} must retain only its cursor after delivery") - transactions = history(workspace) - expect(len(transactions) == 3 and transactions[0].get("receipts") == [] and transactions[0].get("cursor") == "1", "history must be the bootstrap plus two deliveries") - receipts = [committed_receipt(transaction, label=f"transaction {index}") for index, transaction in enumerate(transactions[1:], start=2)] - runtimes = sorted({str(receipt.get("writer_runtime")) for receipt in receipts}) - classes = sorted({str(receipt.get("write_class")) for receipt in receipts}) - expect(runtimes == ["python", "typescript"], "the deliveries must come from both writer runtimes") - expect(classes == ["task_lease_acquire", "todo_add"], "the deliveries must carry their write classes") - inline = delivered(add_todo(workspace, "Delivered inline once the lock is free."), label="todo add (inline)") - expect(_object(inline.get("drain"), "inline drain").get("last_cursor") == "4", "an inline delivery must continue the same lineage") - return passed( - deferred_entries=2, - partitions=["leases", "todos"], - drained=2, - writer_runtimes=runtimes, - write_classes=classes, - inline_cursor="4", - ) - - -def row_drain_idempotent(context: RowContext) -> RowOutcome: - """Bounded drains are cumulative, an idle drain changes nothing, and a writer replay mints no entry.""" - - workspace = capture_workspace(context, "ladder-drain") - with hold_drain_lock(workspace): - for index in range(3): - deferred(add_todo(workspace, f"Deferred todo {index}."), label=f"todo add {index}") - pending = backlog(shadow_status(workspace), "todos") - expect(pending.get("committed_pending") == 3 and pending.get("next_seq") == 3, "three committed entries must be pending") - first = drain(workspace, "--max-entries", "1") - expect( - first.get("ok") is True and first.get("delivered") == 1 and first.get("replayed") == 0 and first.get("pending_after") == 2 and first.get("budget_exhausted") is True, - "a bounded drain must deliver one entry and preserve the rest", - ) - expect(first.get("cursor_after") == "2", "the bounded drain must advance the candidate by one transaction") - rest = drain(workspace) - expect(rest.get("ok") is True and rest.get("delivered") == 2 and rest.get("replayed") == 0 and rest.get("pending_after") == 0, "the next drain must deliver the remaining entries") - expect(rest.get("cursor_after") == "4", "the second drain must finish at cursor 4") - idle = drain(workspace) - expect( - idle.get("ok") is True and idle.get("outcome") == "nothing_pending" and idle.get("delivered") == 0 and idle.get("replayed") == 0, - "an idle drain must deliver and replay nothing", - ) - expect( - idle.get("cursor_before") == "4" and idle.get("cursor_after") == "4" - and idle.get("head_digest") == rest.get("head_digest") and idle.get("provider_revision") == rest.get("provider_revision"), - "an idle drain must leave the candidate head unchanged", - ) - after = backlog(shadow_status(workspace), "todos") - expect(after.get("committed_pending") == 0 and after.get("cursor_last_seq") == 3, "the backlog must be empty at sequence 3") - expect(partition_files(workspace, "todos") == ["drain-cursor.json"], "only the cursor may remain") - transactions = history(workspace) - expect(len(transactions) == 4, "history must hold the bootstrap plus three deliveries") - sequences = [committed_receipt(transaction, label=f"transaction {index}").get("seq") for index, transaction in enumerate(transactions[1:], start=2)] - expect(sequences == [1, 2, 3], "receipts must settle the outbox sequences in order") - listed = _list(goal_cli(workspace, "todo", "list").get("todos"), "todo list") - todo_id = str(_object(listed[0], "listed todo")["todo_id"]) - acquired = delivered( - acquire_lease(workspace, todo_id=todo_id, owner=AGENT_A, idempotency_key="ladder-drain-lease"), - label="task-lease acquire", - ) - replayed = acquire_lease(workspace, todo_id=todo_id, owner=AGENT_A, idempotency_key="ladder-drain-lease") - expect(replayed.get("idempotent") is True, "a same-key re-acquire must be idempotent") - expect("coordination_runtime_shadow" not in replayed, "an idempotent replay must not capture") - expect(len(history(workspace)) == 5, "a writer replay must not add a candidate transaction") - expect(backlog(shadow_status(workspace), "leases").get("committed_pending") == 0, "a writer replay must leave no pending entry") - return passed( - bounded_first_pass=1, - second_pass=2, - idle_pass_delivered=0, - head_digest_stable=True, - lease_partition_cursor=str(_object(acquired.get("drain"), "lease drain").get("last_cursor")), - transactions=5, - ) - - -def row_sigkill_between_primary_write_and_drain(context: RowContext) -> RowOutcome: - """A writer killed around its primary replace leaves a prepared-only entry that drain settles from the primary bytes.""" - - workspace = capture_workspace(context, "ladder-primary-crash") - resolutions: dict[str, str] = {} - visible = 0 - for index, (window, replaced) in enumerate(PRIMARY_CRASH_WINDOWS, start=1): - crash_cli(workspace, window, "todo", "add", "--role", "agent", "--text", f"Primary write killed at {window}.", "--task-class", "advancement_task") - visible += 1 if replaced else 0 - expect(todo_count(workspace) == visible, f"{window}: the primary must show exactly the replaced writes") - pending = backlog(shadow_status(workspace), "todos") - expect(pending.get("committed_pending") == 0 and pending.get("prepared_only") == 1, f"{window}: one prepared-only entry must remain") - expect(phase_count(partition_files(workspace, "todos"), "committed") == 0, f"{window}: no committed marker may exist") - drained = drain(workspace) - expect( - drained.get("ok") is True and drained.get("delivered") == 1 and drained.get("replayed") == 0 and drained.get("no_op") == (0 if replaced else 1), - f"{window}: drain must settle the entry from the primary bytes", - ) - transactions = history(workspace) - expect(len(transactions) == index + 1, f"{window}: every settled entry must be one transaction") - receipt = single_receipt(transactions[-1], label=window) - resolution = str(receipt.get("resolution")) - expect( - resolution == ("committed_proven_by_readback" if replaced else "abandoned") and receipt.get("no_op") is (not replaced), - f"{window}: the receipt must prove the primary outcome", - ) - expect(projected_todo_count(transactions[-1]) == visible, f"{window}: the candidate must equal the primary after recovery") - after = backlog(shadow_status(workspace), "todos") - expect(after.get("prepared_only") == 0 and after.get("cursor_last_seq") == index, f"{window}: the cursor must settle sequence {index}") - resolutions[window] = resolution - inspection = _object(inspect(workspace).get("inspection"), "inspection") - expect(inspection.get("status") == "matched" and inspection.get("parity_matches") is True, "recovered history must match the primary") - return passed(windows=resolutions, primary_todos=visible, transactions=len(PRIMARY_CRASH_WINDOWS) + 1, parity="matched") - - -def row_sigkill_mid_drain(context: RowContext) -> RowOutcome: - """A writer killed inside its inline drain is recovered by the next drain from exact receipts, never delivered twice.""" - - outcomes: dict[str, JsonObject] = {} - for window in DRAIN_CRASH_WINDOWS: - workspace = capture_workspace(context, "ladder-drain-crash") - crash_cli(workspace, window, "todo", "add", "--role", "agent", "--text", f"Drain killed at {window}.", "--task-class", "advancement_task") - expect(todo_count(workspace) == 1, f"{window}: the primary write must be committed") - before = backlog(shadow_status(workspace), "todos") - drained = drain(workspace) - expected_delivered = 1 if window == "before_commit" else 0 - expect( - drained.get("ok") is True and drained.get("outcome") == "drained" - and drained.get("delivered") == expected_delivered and drained.get("replayed") == 1 - expected_delivered, - f"{window}: recovery must deliver an uncommitted entry once and replay a committed one", - ) - transactions = history(workspace) - expect(len(transactions) == 2, f"{window}: the candidate must hold exactly one delivery") - committed_receipt(transactions[1], label=window) - expect(projected_todo_count(transactions[1]) == 1, f"{window}: the delivery must project the committed todo") - after = backlog(shadow_status(workspace), "todos") - expect(after.get("committed_pending") == 0 and after.get("prepared_only") == 0 and after.get("cursor_last_seq") == 1, f"{window}: the backlog must settle at sequence 1") - expect(partition_files(workspace, "todos") == ["drain-cursor.json"], f"{window}: only the cursor may remain") - idle = drain(workspace) - expect(idle.get("outcome") == "nothing_pending" and idle.get("delivered") == 0 and idle.get("replayed") == 0, f"{window}: a further drain must be idle") - outcomes[window] = { - "delivered": drained.get("delivered"), - "replayed": drained.get("replayed"), - "committed_pending_before": before.get("committed_pending"), - "backlog_invalid_before": before.get("invalid"), - } - return passed(windows=outcomes) - - -def row_rollback_with_pending_entries(context: RowContext) -> RowOutcome: - """Rollback archives the outbox with its pending entries, holds capture until an explicit rebootstrap, and replays.""" - - workspace = capture_workspace(context, "ladder-rollback", bootstrap=False) - first = _object(bootstrap_capture(workspace).get("bootstrap"), "first bootstrap") - delivered(add_todo(workspace, "Delivered before rollback."), label="todo add") - with hold_drain_lock(workspace): - deferred(add_todo(workspace, "Committed but not yet drained."), label="todo add (deferred)") - crash_cli(workspace, "before_replace", "todo", "add", "--role", "agent", "--text", "Prepared only.", "--task-class", "advancement_task") - status = shadow_status(workspace) - pending = backlog(status, "todos") - expect(pending.get("committed_pending") == 1 and pending.get("prepared_only") == 1 and pending.get("cursor_last_seq") == 1, "one committed and one prepared-only entry must be pending") - current = candidate(status) - revision = str(current.get("provider_revision")) - expect(current.get("status") == "loaded" and current.get("cursor") == "2", "the candidate must be at cursor 2") - inspection = _object(inspect(workspace).get("inspection"), "inspection") - expect( - inspection.get("qualified") is False and inspection.get("reason_code") == "outbox_pending" and inspection.get("provider_revision") == revision, - "pending entries must block qualification and name the exact revision", - ) - preview = goal_cli(workspace, "coordination-shadow", "rollback", "--provider-revision", revision) - expect(preview.get("ok") is True and preview.get("executed") is False and "rollback" not in preview, "a rollback preview must not execute") - expect(outbox_present(workspace), "a preview must leave the outbox in place") - executed = goal_cli(workspace, "coordination-shadow", "rollback", "--provider-revision", revision, "--execute") - rollback = _object(executed.get("rollback"), "rollback") - expect( - rollback.get("status") == "applied" and rollback.get("active_shadow_removed") is True and rollback.get("archive_retained") is True, - "rollback must retire the active candidate into a retained archive", - ) - expect(rollback.get("archived_cursor") == "2" and rollback.get("archived_provider_revision") == revision, "the archive must bind the exact revision") - archive = rollback.get("outbox_archive_path") - expect(isinstance(archive, str) and Path(archive).is_dir(), "rollback must archive the goal outbox") - assert isinstance(archive, str) - archived = sorted(path.name for path in Path(archive).rglob("*") if path.is_file()) - expect( - phase_count(archived, "prepared") == 2 and phase_count(archived, "committed") == 1 - and "drain-cursor.json" in archived and "manifest.json" in archived, - "the archive must preserve pending entries, the marker, the cursor and the manifest", - ) - expect(not outbox_present(workspace), "the active outbox must be moved, not copied") - status = shadow_status(workspace) - expect(management_status(status) == "inactive" and candidate(status).get("status") == "missing", "management must be inactive with no candidate") - expect(backlog(status, "todos").get("committed_pending") == 0 and backlog(status, "todos").get("prepared_only") == 0, "no backlog may remain") - after = add_todo(workspace, "Primary write after rollback.") - expect(after.get("added") is True, "primary writes must resume after rollback") - held = capture_evidence(after, label="todo add (after rollback)") - expect(held.get("outcome") == "no_transaction" and held.get("reason_code") == "bootstrap_required", "capture must report bootstrap_required, not a transaction") - rebootstrap = bootstrap_capture(workspace) - second = _object(rebootstrap.get("bootstrap"), "second bootstrap") - expect(second.get("capture_lineage_id") != first.get("capture_lineage_id"), "a rebootstrap must start a new lineage") - summary = _object(rebootstrap.get("projection_summary"), "projection summary") - expect(summary.get("todo_count") == 3 and summary.get("lease_count") == 0, "the new baseline must import the current primary") - replay = _object(goal_cli(workspace, "coordination-shadow", "rollback", "--provider-revision", revision, "--execute").get("rollback"), "rollback replay") - expect( - replay.get("status") == "replayed" and replay.get("current_capture_lineage_id") == second.get("capture_lineage_id"), - "a replayed rollback must return its historical result against the new lineage", - ) - resumed = delivered(add_todo(workspace, "Captured in the new lineage."), label="todo add (new lineage)") - expect(_object(resumed.get("drain"), "drain").get("last_cursor") == "2", "the new lineage must capture from cursor 2") - transactions = history(workspace) - expect(len(transactions) == 2 and transactions[0].get("receipts") == [] and projected_todo_count(transactions[0]) == 3, "the new history must start from the imported baseline") - return passed( - pending_before_rollback={"committed": 1, "prepared_only": 1}, - archived_files=len(archived), - archived_prepared=2, - archived_committed=1, - capture_after_rollback="bootstrap_required", - rebootstrap_baseline_todos=3, - new_lineage=True, - replayed_rollback=True, - ) - - -@dataclass -class _MixedWriterLedger: - workspace: GoalWorkspace - mutations: list[tuple[str, JsonObject]] = field(default_factory=list) - deliveries: int = 0 - no_change_writes: int = 0 - - def cli(self, *args: str) -> JsonObject: - return goal_cli(self.workspace, *args) - - def mutate(self, label: str, payload: JsonObject, *, flag: str) -> JsonObject: - expect(payload.get(flag) is True, f"{label} must report {flag}=true") - evidence = delivered(payload, label=label) - # A leased complete or supersede also closes its lease fence, so one - # CLI call may deliver two transactions; count what the drain reports. - count = _object(evidence.get("drain"), f"{label} drain").get("delivered") - expect(isinstance(count, int) and count >= 1, f"{label} must deliver at least one transaction") - assert isinstance(count, int) - self.deliveries += count - self.mutations.append((label, evidence)) - return payload - - def unchanged(self, label: str, payload: JsonObject) -> None: - expect(payload.get("changed") is False, f"{label} must report changed=false") - no_transaction(payload, label=label) - self.no_change_writes += 1 - - -def _mixed_writer_cycle(ledger: _MixedWriterLedger, cycle: int) -> None: - """One cycle of interleaved Python Markdown writers and TypeScript lease writers.""" - - workspace = ledger.workspace - key_a, key_b, key_c = (f"ladder-parity-{cycle}-{suffix}" for suffix in ("a", "b", "c")) - first = ledger.mutate("todo add", add_todo(workspace, f"Cycle {cycle}: deliver one bounded change."), flag="added") - todo_id = str(first["todo_id"]) - acquired = ledger.mutate("task-lease acquire", acquire_lease(workspace, todo_id=todo_id, owner=AGENT_A, idempotency_key=key_a), flag="acquired") - note = ["todo", "update", "--todo-id", todo_id, "--note", f"Cycle {cycle} note.", "--agent-id", AGENT_A] - ledger.mutate("todo update (note)", ledger.cli(*note), flag="changed") - ledger.unchanged("todo update (same note)", ledger.cli(*note)) - renewed = ledger.mutate( - "task-lease renew", - ledger.cli( - "task-lease", "renew", "--todo-id", todo_id, "--owner", AGENT_A, "--idempotency-key", key_a, - "--expected-version", lease_version(acquired, label="acquire"), "--ttl-seconds", "120", - ), - flag="renewed", - ) - transferred = ledger.mutate( - "task-lease transfer", - ledger.cli( - "task-lease", "transfer", "--todo-id", todo_id, "--owner", AGENT_A, "--idempotency-key", key_a, - "--new-owner", AGENT_B, "--new-idempotency-key", key_b, - "--expected-version", lease_version(renewed, label="renew"), "--ttl-seconds", "120", - ), - flag="transferred", - ) - ledger.mutate( - "todo complete", - ledger.cli( - "todo", "complete", "--todo-id", todo_id, "--agent-id", AGENT_B, - "--task-lease-idempotency-key", key_b, "--task-lease-expected-version", lease_version(transferred, label="transfer"), - "--evidence", "validation://ladder-parity", "--no-follow-up", - ), - flag="completed", - ) - second = ledger.mutate("todo add (second)", add_todo(workspace, f"Cycle {cycle}: replace this work with a successor."), flag="added") - second_id = str(second["todo_id"]) - ledger.mutate( - "todo update (explicit exclusion)", - ledger.cli("todo", "update", "--todo-id", second_id, "--excluded-agent", AGENT_B, "--agent-id", AGENT_A), - flag="changed", - ) - clear = ["todo", "update", "--todo-id", second_id, "--clear-excluded-agents", "--agent-id", AGENT_A] - ledger.mutate("todo update (explicit clear)", ledger.cli(*clear), flag="changed") - ledger.unchanged("todo update (clear again)", ledger.cli(*clear)) - held = ledger.mutate("task-lease acquire (second)", acquire_lease(workspace, todo_id=second_id, owner=AGENT_A, idempotency_key=key_c), flag="acquired") - ledger.mutate( - "todo supersede", - ledger.cli( - "todo", "supersede", "--todo-id", second_id, "--agent-id", AGENT_A, "--reason", "Replace obsolete work.", - "--next-agent-todo", f"Cycle {cycle}: carry the bounded work forward.", - "--task-lease-idempotency-key", key_c, "--task-lease-expected-version", lease_version(held, label="acquire (second)"), - ), - flag="superseded", - ) - ledger.mutate( - "todo add (verification)", - add_todo(workspace, f"Cycle {cycle}: verify the captured projection."), - flag="added", - ) - - -def _assert_bounded_parity(ledger: _MixedWriterLedger, cycle: int, anchor_todo_id: str) -> JsonObject: - workspace = ledger.workspace - inspection = _object(inspect(workspace).get("inspection"), f"cycle {cycle} inspection") - expect( - inspection.get("status") == "matched" and inspection.get("parity_matches") is True, - f"cycle {cycle}: the candidate head must match the primary " - f"(status={inspection.get('status')!r}, reason_code={inspection.get('reason_code')!r})", - ) - flags = ["--minimum-operations", str(ledger.deliveries)] - for write_class in PARITY_REQUIRED_WRITE_CLASSES: - flags.extend(["--require-event-kind", write_class]) - qualification = qualified(qualify(workspace, *flags), label=f"cycle {cycle}") - evidence = _object(qualification.get("evidence"), f"cycle {cycle} evidence") - expect(evidence.get("operation_count") == ledger.deliveries, f"cycle {cycle}: every delivered transaction must count as one verified operation") - expect(evidence.get("missing_required_event_kinds") == [] and evidence.get("pending_outbox") is False, f"cycle {cycle}: coverage must be complete with nothing pending") - read = read_candidate(workspace, anchor_todo_id) - result = _object(read.get("read_candidate"), f"cycle {cycle} read-candidate") - expect( - read.get("ok") is True and result.get("status") == "matched" and result.get("read_candidate_qualified") is True, - f"cycle {cycle}: a qualified read must return the anchor todo from the verified head", - ) - return qualification - - -def row_parity_equal(context: RowContext) -> RowOutcome: - """Sustained interleaving of Python and TypeScript writers keeps every bounded qualification matched.""" - - workspace = capture_workspace(context, "ladder-parity") - ledger = _MixedWriterLedger(workspace) - anchor = ledger.mutate("todo add (anchor)", add_todo(workspace, "Anchor todo that stays open for qualified reads."), flag="added") - anchor_todo_id = str(anchor["todo_id"]) - qualification: JsonObject = {} - for cycle in range(1, PARITY_CYCLES + 1): - _mixed_writer_cycle(ledger, cycle) - qualification = _assert_bounded_parity(ledger, cycle, anchor_todo_id) - transactions = history(workspace) - expect(len(transactions) == ledger.deliveries + 1, "history must hold the bootstrap plus one transaction per delivery") - receipts = [committed_receipt(transaction, label=f"transaction {index}") for index, transaction in enumerate(transactions[1:], start=2)] - runtimes = sorted({str(receipt.get("writer_runtime")) for receipt in receipts}) - classes = sorted({str(receipt.get("write_class")) for receipt in receipts}) - expect(runtimes == ["python", "typescript"], "both writer runtimes must appear in one lineage") - expect(set(PARITY_REQUIRED_WRITE_CLASSES) <= set(classes), "every required write class must be captured") - expect(len({str(receipt.get("entry_id")) for receipt in receipts}) == len(receipts), "every receipt must carry a distinct entry id") - return passed( - cycles=PARITY_CYCLES, - mutations=len(ledger.mutations), - deliveries=ledger.deliveries, - no_change_writes=ledger.no_change_writes, - writer_runtimes=runtimes, - write_classes=classes, - final_cursor=str(qualification.get("cursor")), - operation_count=_object(qualification.get("evidence"), "evidence").get("operation_count"), - sustained_parity_verdict=str(qualification.get("sustained_parity_verdict")), - ) - - -def _recover_by_rollback_and_rebootstrap(workspace: GoalWorkspace, *, label: str) -> JsonObject: - """The documented recovery for a held lineage: exact-revision rollback, then a fresh bootstrap.""" - - revision = str(candidate(shadow_status(workspace)).get("provider_revision")) - rollback = _object(goal_cli(workspace, "coordination-shadow", "rollback", "--provider-revision", revision, "--execute").get("rollback"), f"{label} rollback") - expect(rollback.get("status") == "applied" and rollback.get("archive_retained") is True, f"{label}: rollback must retire the held lineage into an archive") - rebootstrap = bootstrap_capture(workspace) - for index in range(3): - delivered(add_todo(workspace, f"{label} recovery write {index}."), label=f"{label} recovery write {index}") - qualified(qualify(workspace), label=f"{label} after recovery") - return _object(rebootstrap.get("projection_summary"), f"{label} rebootstrap summary") - - -def row_parity_divergent_detects_foreign_edit(context: RowContext) -> RowOutcome: - """A direct edit of the primary is detected as drift, holds later captures, and is recovered only by rollback and rebootstrap.""" - - workspace = capture_workspace(context, "ladder-drift") - todo_ids: list[str] = [] - for index in range(3): - added = add_todo(workspace, f"Drift baseline {index}.") - delivered(added, label=f"todo add {index}") - todo_ids.append(str(added["todo_id"])) - qualified(qualify(workspace), label="baseline") - original = workspace.state_path.read_text(encoding="utf-8") - foreign = original.replace("handoff_mode: hard_lease", "handoff_mode: soft_claim") - expect(foreign != original, "the foreign edit must change the primary") - workspace.state_path.write_text(foreign, encoding="utf-8") - inspection = _object(inspect(workspace).get("inspection"), "inspection") - expect( - inspection.get("status") == "drifted" and inspection.get("parity_matches") is False - and inspection.get("reason_code") == "shadow_projection_drift" and inspection.get("qualified") is False, - "inspect must report the drift", - ) - rejected(qualify(workspace), "qualification", label="qualify after drift") - read = read_candidate(workspace, todo_ids[0]) - expect(read.get("ok") is False and rejected(read, "read_candidate", label="read after drift").get("read_candidate_qualified") is False, "no candidate read may qualify after drift") - after = add_todo(workspace, "Public write after the foreign edit.") - expect(after.get("added") is True, "the primary write must still commit") - held = capture_evidence(after, label="todo add (after drift)") - expect(held.get("outcome") == "pending" and held.get("reason_code") == CONTINUITY_HOLD, "the capture must hold on unproven continuity") - expect(_object(held.get("drain"), "held drain").get("outcome") == "stopped", "the inline drain must stop, not deliver") - expect(backlog(shadow_status(workspace), "todos").get("committed_pending") == 1, "the held entry must stay pending") - workspace.state_path.write_text(workspace.state_path.read_text(encoding="utf-8").replace("handoff_mode: soft_claim", "handoff_mode: hard_lease"), encoding="utf-8") - restored = rejected(qualify(workspace), "qualification", label="qualify after restore") - expect(restored.get("reason_code") == "outbox_pending", "restoring the bytes must not requalify a held lineage") - stopped = drain(workspace) - expect( - stopped.get("ok") is False and stopped.get("outcome") == "stopped" and stopped.get("delivered") == 0 and stopped.get("reason_code") == CONTINUITY_HOLD, - "drain must keep holding the entry rather than guess continuity", - ) - summary = _recover_by_rollback_and_rebootstrap(workspace, label="drift") - return passed( - drift_status="drifted", - drift_reason="shadow_projection_drift", - held_write=CONTINUITY_HOLD, - restore_requalifies=False, - recovered_by="rollback_then_bootstrap", - rebootstrap_baseline_todos=summary.get("todo_count"), - recovered_qualification="qualified", - ) - - -def row_event_only_todo_source_holds(context: RowContext) -> RowOutcome: - """A retired source refuses candidate/primary operations; preserving both sources allows explicit cleanup.""" - - workspace = capture_workspace(context, "ladder-event") - todo_ids: list[str] = [] - for index in range(3): - added = add_todo(workspace, f"Markdown baseline {index}.") - delivered(added, label=f"todo add {index}") - todo_ids.append(str(added["todo_id"])) - qualified(qualify(workspace), label="baseline") - log = workspace.state_path.with_name("events.jsonl") - log.write_text(json.dumps({"schema_version": "loopx_state_event_v0", - "event_type": "todo_added", "refs": {"todo_id": "todo_event_only"}}) + "\n", encoding="utf-8") - log_bytes = log.read_bytes() - surfaces = {"inspect": inspect(workspace), "qualify": qualify(workspace), "read-candidate": read_candidate(workspace, todo_ids[0])} - for label, payload in surfaces.items(): - expect(payload.get("ok") is False and str(payload.get("error") or "").startswith(EVENT_ONLY_HOLD), f"{label} must refuse retired source: {payload.get('error')}") - status = shadow_status(workspace) - expect(status.get("ok") is True and management_status(status) == "active", "status must stay readable while the lineage is held") - before = workspace.state_path.read_bytes() - during = goal_cli(workspace, "todo", "add", "--role", "agent", - "--text", "Markdown write during the retired event hold.", check=False) - expect(during.get("ok") is False, "retired source must reject primary writes") - expect(workspace.state_path.read_bytes() == before, "refusal must preserve Markdown") - expect(log.read_bytes() == log_bytes, "refusal must preserve the event file") - log.unlink() # The fixture owns this file; product never removes it. - qualified(qualify(workspace), label="retired source removed without any write") - return passed(hold=EVENT_ONLY_HOLD, held_surfaces=sorted(surfaces), - primary_write_refused=True, event_log_untouched=True) - - -@dataclass(frozen=True) -class _MigrationTarget: - """The target registry, repository and runtime root of one migration.""" - - registry_path: Path - repo: Path - runtime_root: Path - home: Path - goal_id: str - - def cli_prefix(self) -> list[str]: - return ["--registry", str(self.registry_path), "--runtime-root", str(self.runtime_root), "--format", "json"] - - -MIGRATION_SENTINEL = b'{"schema_version":"existing","goals":[]}\n' - - -def _migration_target(context: RowContext, legacy: GoalWorkspace) -> _MigrationTarget: - root = context.root / "migration-target" - repo = root / "repo" - repo.mkdir(parents=True) - registry_path = root / "registry.json" - registry_path.write_bytes(MIGRATION_SENTINEL) - return _MigrationTarget(registry_path=registry_path, repo=repo, runtime_root=root / "runtime", home=legacy.home, goal_id=unique_goal_id("migrated")) - - -def _migration_arguments(legacy: GoalWorkspace, target: _MigrationTarget) -> list[str]: - return [ - "migrate-state", - "--legacy-registry", str(legacy.registry_path), - "--legacy-runtime-root", str(legacy.runtime_root), - "--target-runtime-root", str(target.runtime_root), - "--goal-id", legacy.goal_id, - "--goal-id-map", f"{legacy.goal_id}={target.goal_id}", - "--path-map", f"{legacy.repo}={target.repo}", - "--copy-active-state", - "--copy-runtime", - "--no-global-sync", - ] - - -def _set_target_runtime_shadow(target: _MigrationTarget, *, enabled: bool) -> JsonObject: - registry = parse_json_object(target.registry_path.read_text(encoding="utf-8")) - goals = [_object(goal, "target goal") for goal in _list(registry.get("goals"), "target goals")] - expect(len(goals) == 1 and goals[0].get("id") == target.goal_id, "the target registry must carry exactly the migrated goal") - coordination = _object(goals[0].get("coordination"), "target coordination") - carried = _object(coordination.get("runtime_shadow"), "carried runtime_shadow") - coordination["runtime_shadow"] = {**carried, "enabled": enabled} - goals[0]["coordination"] = coordination - registry["goals"] = goals - target.registry_path.write_text(json.dumps(registry), encoding="utf-8") - return carried - - -def row_migration_seeds_and_drains(context: RowContext) -> RowOutcome: - """migrate-state refuses an active capture source; after rollback the migrated goal bootstraps a fresh lineage that drains.""" - - legacy = capture_workspace(context, "ladder-legacy", bootstrap=False) - first = _object(bootstrap_capture(legacy).get("bootstrap"), "legacy bootstrap") - delivered(add_todo(legacy, "Captured before migration."), label="legacy todo add") - documents = store_documents(legacy) - expect(len(documents) == 1, "the legacy runtime must hold one candidate document") - legacy_bytes = documents[0].read_bytes() - target = _migration_target(context, legacy) - arguments = _migration_arguments(legacy, target) - preview = run_cli(target, *arguments, check=False) - expect(preview.get("ok") is True and preview.get("dry_run") is True and target.registry_path.read_bytes() == MIGRATION_SENTINEL, "a dry run must plan without writing") - refused = run_cli(target, *arguments, "--execute", check=False) - expect( - refused.get("ok") is False and refused.get("error_code") == "shadow_source_replacement_requires_rebootstrap", - "an active capture source must refuse a generic state rebuild", - ) - expect(target.registry_path.read_bytes() == MIGRATION_SENTINEL and documents[0].read_bytes() == legacy_bytes, "a refused migration must write nothing") - set_runtime_shadow(legacy, enabled=False) - disabled_only = run_cli(target, *arguments, "--execute", check=False) - expect(disabled_only.get("error_code") == "shadow_source_replacement_requires_rebootstrap", "disabling capture alone must not release an active lineage") - set_runtime_shadow(legacy, enabled=True) - revision = str(candidate(shadow_status(legacy)).get("provider_revision")) - rollback = _object(goal_cli(legacy, "coordination-shadow", "rollback", "--provider-revision", revision, "--execute").get("rollback"), "legacy rollback") - expect(rollback.get("status") == "applied" and rollback.get("archive_retained") is True, "the legacy lineage must be retired into an archive first") - set_runtime_shadow(legacy, enabled=False) - executed = run_cli(target, *arguments, "--execute") - expect(executed.get("ok") is True and executed.get("wrote_project_registry") is True, "the migration must execute after rollback") - runtime_goal = _object(_list(executed.get("runtime_goals"), "runtime goals")[0], "runtime goal") - expect(runtime_goal.get("copied") is True, "the runtime goal directory must be copied") - expect(executed.get("authority_shadow_seeds") == [], "a capture-only goal plans no observation seed") - expect((target.repo / legacy.state_path.name).exists(), "the active state must be copied to the mapped repository") - carried = _set_target_runtime_shadow(target, enabled=True) - expect(carried.get("enabled") is False and carried.get("provider") == RUNTIME_SHADOW_PROVIDER, "the migrated goal must carry its disabled capture configuration") - inspection = _object(run_cli(target, "coordination-shadow", "inspect", "--goal-id", target.goal_id, check=False).get("inspection"), "target inspection") - expect(inspection.get("status") == "missing" and inspection.get("bootstrap_required") is True, "the migrated goal must require its own bootstrap") - bootstrap_payload = run_cli(target, "coordination-shadow", "bootstrap", "--goal-id", target.goal_id, "--execute") - bootstrap = _object(bootstrap_payload.get("bootstrap"), "target bootstrap") - expect(bootstrap.get("status") == "applied" and bootstrap.get("capture_lineage_id") != first.get("capture_lineage_id"), "the target must seed a fresh lineage") - summary = _object(bootstrap_payload.get("projection_summary"), "target projection summary") - expect(summary.get("todo_count") == 1 and summary.get("lease_count") == 0, "the fresh lineage must import the migrated baseline") - added = run_cli(target, "todo", "add", "--goal-id", target.goal_id, "--role", "agent", "--text", "Captured in the migrated runtime root.", "--task-class", "advancement_task") - drained = _object(delivered(added, label="target todo add").get("drain"), "target drain") - expect(drained.get("last_cursor") == "2", "the first migrated write must drain to cursor 2") - qualification = _object(run_cli(target, "coordination-shadow", "qualify", "--goal-id", target.goal_id, "--minimum-operations", "1", check=False).get("qualification"), "target qualification") - expect(qualification.get("status") == "qualified" and _object(qualification.get("evidence"), "evidence").get("operation_count") == 1, "the migrated lineage must qualify on its own write") - archive = legacy.runtime_root / "authority-shadow" / "file-v0" / "rollback" - expect(archive.is_dir() and any(archive.iterdir()), "the legacy archive must be retained") - return passed( - preview_dry_run=True, - active_capture_refused="shadow_source_replacement_requires_rebootstrap", - disabled_config_still_refused=True, - executed_after_rollback=True, - observation_seeds=0, - migrated_baseline_todos=1, - new_lineage=True, - drained_cursor="2", - legacy_archive_retained=True, - ) - - -def row_growth_measurement_gate(context: RowContext) -> RowOutcome: - """Measure file-v0 history growth per transaction and gate its integrity; no capacity horizon is claimed.""" - - workspace = capture_workspace(context, "ladder-growth") - status = shadow_status(workspace) - sizes = [int(str(status.get("store_bytes")))] - cursors = [str(candidate(status).get("cursor"))] - for index in range(GROWTH_TRANSACTIONS): - delivered(add_todo(workspace, GROWTH_TEXT_TEMPLATE % index), label=f"growth write {index}") - status = shadow_status(workspace) - expect(status.get("retention_pressure") is False, "the bounded workload must not trip retention pressure") - sizes.append(int(str(status.get("store_bytes")))) - cursors.append(str(candidate(status).get("cursor"))) - expect(cursors == [str(index) for index in range(1, GROWTH_TRANSACTIONS + 2)], "every write must advance the cursor by exactly one") - deltas = [after - before for before, after in zip(sizes, sizes[1:])] - expect(all(delta > 0 for delta in deltas), "every transaction must grow the retained history") - accelerations = [later - earlier for earlier, later in zip(deltas, deltas[1:])] - expect( - max(abs(value) for value in accelerations) <= GROWTH_DELTA_ACCELERATION_ENVELOPE_BYTES, - "per-transaction growth may increase by at most one live record per transaction", - ) - transactions = history(workspace) - expect(len(transactions) == GROWTH_TRANSACTIONS + 1, "history must retain every transaction") - expect( - [projected_todo_count(transaction) for transaction in transactions] == list(range(GROWTH_TRANSACTIONS + 1)), - "every transaction must retain its complete projection", - ) - final = sizes[-1] - cumulative = sum(sizes) - return passed( - transactions=GROWTH_TRANSACTIONS + 1, - final_history_bytes=final, - cumulative_publication_bytes=cumulative, - publication_rewrite_ratio=round(cumulative / final, 2), - first_delta_bytes=deltas[0], - last_delta_bytes=deltas[-1], - delta_acceleration_max_bytes=max(abs(value) for value in accelerations), - delta_acceleration_envelope_bytes=GROWTH_DELTA_ACCELERATION_ENVELOPE_BYTES, - retention_pressure=False, - capacity_verdict="not_evaluated", - qualification_horizon="not_claimed", - ) - - -def row_archive_after_leased_completion_parity(context: RowContext) -> RowOutcome: - """Archiving a Todo whose released lease stays on disk keeps the candidate head matched and qualifiable.""" - - workspace = capture_workspace(context, "ladder-archive-leased") - leased = add_todo(workspace, "Leased todo archived after its completion is captured.") - leased_todo_id = str(leased["todo_id"]) - delivered(leased, label="todo add (leased)") - acquired = acquire_lease(workspace, todo_id=leased_todo_id, owner=AGENT_A, idempotency_key="ladder-archive-leased-a") - delivered(acquired, label="task-lease acquire") - completed = goal_cli( - workspace, "todo", "complete", "--todo-id", leased_todo_id, "--agent-id", AGENT_A, - "--task-lease-idempotency-key", "ladder-archive-leased-a", - "--task-lease-expected-version", lease_version(acquired, label="acquire"), - "--evidence", "validation://ladder-archive-leased", "--no-follow-up", - ) - delivered(completed, label="todo complete") - anchor = add_todo(workspace, "Anchor todo that stays open across the archive write.") - anchor_todo_id = str(anchor["todo_id"]) - delivered(anchor, label="todo add (anchor)") - qualified(qualify(workspace), label="baseline") - archived = goal_cli(workspace, "todo", "archive-completed", "--role", "agent", "--max-active-done", "0", "--execute") - expect(archived.get("changed") is True and archived.get("moved_count") == 1, "archive-completed must move exactly the completed todo") - inspection = _object(inspect(workspace).get("inspection"), "post-archive inspection") - expect( - inspection.get("status") == "matched" and inspection.get("parity_matches") is True, - "archiving a Todo whose released lease remains on disk must not orphan that lease in the candidate head", - ) - expect(inspection.get("reason_code") is None, "a matched archive must report no drift reason") - # Require exactly the event kinds this row delivers, including the archive - # that retires the Todo from the graph while its released lease file stays - # on disk as audit history. Reusing the mixed-writer parity set here would - # demand writers this row never drives. - flags = ["--minimum-operations", str(ARCHIVE_PARITY_OPERATIONS)] - for write_class in ARCHIVE_PARITY_REQUIRED_WRITE_CLASSES: - flags.extend(["--require-event-kind", write_class]) - qualification = qualified(qualify(workspace, *flags), label="post-archive") - read = read_candidate(workspace, anchor_todo_id) - expect(read.get("ok") is True, "a qualified read must remain available after the archive write") - lease_dir = workspace.runtime_root / "goals" / workspace.goal_id / "task-leases" - lease_names = sorted(path.name for path in lease_dir.glob("*.json")) - expect(f"{leased_todo_id}.json" in lease_names, "the released lease file must stay on disk as audit history") - - # The second boundary the same rule covers: a later lease write must not - # re-read the retained lease of the archived Todo into its own partition. - successor = add_todo(workspace, "Successor todo that takes a fresh lease after the archive.") - successor_todo_id = str(successor["todo_id"]) - delivered(successor, label="todo add (successor)") - successor_lease = acquire_lease( - workspace, todo_id=successor_todo_id, owner=AGENT_A, idempotency_key="ladder-archive-leased-b", - ) - delivered(successor_lease, label="task-lease acquire (successor)") - after_successor = _object(inspect(workspace).get("inspection"), "post-successor inspection") - expect( - after_successor.get("status") == "matched" and after_successor.get("parity_matches") is True, - "a lease write after the archive must not inherit the retained lease of the archived Todo", - ) - # Completion closes the lease fence in a separate native request. It must - # retain the same current-graph rule as acquire, not reintroduce audit leases. - successor_completed = goal_cli( - workspace, "todo", "complete", "--todo-id", successor_todo_id, "--agent-id", AGENT_A, - "--task-lease-idempotency-key", "ladder-archive-leased-b", - "--task-lease-expected-version", lease_version(successor_lease, label="successor acquire"), - "--evidence", "validation://ladder-successor-complete", "--no-follow-up", - ) - delivered(successor_completed, label="todo complete (successor)") - drained = drain(workspace) - expect(drained.get("ok") is True and drained.get("pending_after") == 0, - "successor fence-close must leave no unprovable lease partition") - qualified(qualify(workspace), label="post-successor completion") - expect(read_candidate(workspace, anchor_todo_id).get("ok") is True, - "candidate reads must survive successor fence-close") - final_lease_names = sorted(path.name for path in lease_dir.glob("*.json")) - expect( - {f"{leased_todo_id}.json", f"{successor_todo_id}.json"} <= set(final_lease_names), - "both the archived Todo's audit lease and the successor's live lease must remain on disk", - ) - return passed( - archived_todo=leased_todo_id, - retained_lease_files=len(lease_names), - parity_status="matched", - parity_reason=None, - qualification_cursor=str(qualification.get("cursor")), - anchor_read_qualified=True, - successor_todo=successor_todo_id, - post_successor_parity="matched", - final_lease_files=len(final_lease_names), - ) - -__all__ = [ - "CRASH_WORKER", - "DRAIN_CRASH_WINDOWS", - "GROWTH_DELTA_ACCELERATION_ENVELOPE_BYTES", - "GROWTH_TRANSACTIONS", - "PARITY_CYCLES", - "PARITY_REQUIRED_WRITE_CLASSES", - "PRIMARY_CRASH_WINDOWS", - "row_archive_after_leased_completion_parity", - "row_drain_idempotent", - "row_event_only_todo_source_holds", - "row_growth_measurement_gate", - "row_migration_seeds_and_drains", - "row_outbox_prepared_then_committed_entries", - "row_parity_divergent_detects_foreign_edit", - "row_parity_equal", - "row_rollback_with_pending_entries", - "row_sigkill_between_primary_write_and_drain", - "row_sigkill_mid_drain", -] diff --git a/loopx/control_plane/testing/replan_semantic_action_behavior.py b/loopx/control_plane/testing/replan_semantic_action_behavior.py index 6189d43a44..69b49d3e13 100644 --- a/loopx/control_plane/testing/replan_semantic_action_behavior.py +++ b/loopx/control_plane/testing/replan_semantic_action_behavior.py @@ -57,7 +57,10 @@ def __init__(self, code: str, output: str, exit_code: int) -> None: _FIXTURE_GOAL_ID = "replan-semantic-action-fixture" _FIXTURE_AGENT_ID = "codex-replan-semantic-action" -_FIXTURE_WORK_ITEM_ID = "todo-replan-semantic-action" +_FIXTURE_WORK_ITEM_OPERATION_ID = "fixture-replan-semantic-monitor" +_FIXTURE_WORK_ITEM_ID = ( + "todo_" + sha256(_FIXTURE_WORK_ITEM_OPERATION_ID.encode()).hexdigest()[:24] +) _FIXTURE_FRONTIER_FILE = "replan-frontier.json" _FIXTURE_SOURCE_FILE = "fixture/permission-config.json" _FIXTURE_NEW_SURFACE_ID = "surface-permission-config" @@ -204,26 +207,11 @@ def _build_fixture( "refresh-state." ) ) - fixture_agent_todo = ( - "- [ ] [P1-monitor] Observe the bounded fixture until its frontier " - "changes.\n" - " \n" - ) - if required_vision: - fixture_agent_todo += ( - "- [ ] [P0] Continue the peer-owned implementation.\n" - " \n" - ) state_path.write_text( "---\n" "status: active\n" "owner_mode: goal\n" + "handoff_mode: soft_claim\n" f'objective: "{fixture_objective}"\n' "updated_at: 2026-08-13T00:00:00+08:00\n" "---\n\n" @@ -232,8 +220,7 @@ def _build_fixture( f"{fixture_objective}\n\n" "## Next Action\n\n" f"- {fixture_next_action}\n\n" - "## Agent Todo\n\n" - f"{fixture_agent_todo}", + "## Agent Todo\n", encoding="utf-8", ) registry_goal: dict[str, Any] = { @@ -249,6 +236,11 @@ def _build_fixture( "coordination": { "registered_agents": [_FIXTURE_AGENT_ID, "codex-fixture-peer"] if required_vision else [_FIXTURE_AGENT_ID], "agent_model": "peer_v1", + "storage_target": { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "file", + "handoff_mode": "soft_claim", + }, "agent_profiles": { _FIXTURE_AGENT_ID: { "schema_version": "agent_profile_v1", @@ -269,6 +261,7 @@ def _build_fixture( "window_hours": 24, "allowed_slots": 5, }, + "creation_operation_id": "goal-create:replan-semantic-action-fixture", } if composition_frontier: registry_goal["spawn_policy"] = { @@ -285,6 +278,56 @@ def _build_fixture( registry_path.parent.mkdir(parents=True, exist_ok=True) registry_path.write_text(registry_text + "\n", encoding="utf-8") + from ...todos import add_goal_todo + from ..coordination.goal_storage_initialization import ( + initialize_goal_storage_target, + ) + + initialized = initialize_goal_storage_target( + runtime_root, + registry_goal, + registry_path=global_registry_path, + ) + if not isinstance(initialized, dict) or ( + initialized.get("authority_initialized") is not True + or initialized.get("legacy_fallback_used") is not False + ): + raise AssertionError("fixture did not initialize canonical File authority") + monitor = add_goal_todo( + registry_path=global_registry_path, + runtime_root_arg=str(runtime_root), + goal_id=_FIXTURE_GOAL_ID, + role="agent", + text="Observe the bounded fixture until its frontier changes.", + priority="P1", + task_class="continuous_monitor", + action_kind="observe_fixture", + claimed_by=_FIXTURE_AGENT_ID, + agent_id=_FIXTURE_AGENT_ID, + monitor_metadata={ + "target_key": "replan-semantic-fixture", + "cadence": "1d", + "next_due_at": "2999-01-01T00:00:00Z", + "watch_only": "true", + }, + operation_id=_FIXTURE_WORK_ITEM_OPERATION_ID, + ) + if monitor.get("todo_id") != _FIXTURE_WORK_ITEM_ID: + raise AssertionError("fixture monitor identity is not deterministic") + if required_vision: + add_goal_todo( + registry_path=global_registry_path, + runtime_root_arg=str(runtime_root), + goal_id=_FIXTURE_GOAL_ID, + role="agent", + text="Continue the peer-owned implementation.", + priority="P0", + task_class="advancement_task", + claimed_by="codex-fixture-peer", + agent_id="codex-fixture-peer", + operation_id="fixture-replan-peer-implementation", + ) + runs_dir = runtime_root / "goals" / _FIXTURE_GOAL_ID / "runs" runs_dir.mkdir(parents=True, exist_ok=True) prior_runs: list[dict[str, Any]] = [] @@ -678,6 +721,7 @@ def _semantic_reentry_observation( packet: Mapping[str, Any], *, obligation_id: str, + result_class: str, ) -> dict[str, Any]: obligation = packet.get("autonomous_replan_obligation") if ( @@ -710,12 +754,29 @@ def _semantic_reentry_observation( and int(normalized_progress.get("agent_monitor_open_count") or 0) > 0 and int(normalized_progress.get("agent_monitor_due_count") or 0) == 0 ) - if not future_monitor_wait: + terminal_coverage_exit = bool( + result_class in { + ProgressResultClass.EXPLORATION_EXHAUSTED.value, + ProgressResultClass.NO_FOLLOWUP.value, + } + and packet.get("effective_action") == EffectiveAction.MONITOR_QUIET_SKIP.value + and isinstance(frontier, Mapping) + and frontier.get("replan_required") is False + and isinstance(monitor_lanes, Mapping) + and monitor_lanes.get("present") is False + and isinstance(normalized_progress, Mapping) + and int(normalized_progress.get("agent_open_count") or 0) == 0 + ) + if not future_monitor_wait and not terminal_coverage_exit: raise ValueError("semantic_reentry_not_future_monitor_wait") return { "decision": packet.get("decision"), "effective_action": packet.get("effective_action"), - "replan_rule": "future_monitor_wait", + "replan_rule": ( + "future_monitor_wait" + if future_monitor_wait + else "terminal_coverage_exhausted" + ), "replan_closed": True, "exited": True, } @@ -1150,6 +1211,7 @@ def _handle_semantic_writeback( state.semantic_reentry_observation = _semantic_reentry_observation( reentry_packet, obligation_id=obligation_id, + result_class=result_class, ) state.read_only_host_commands_executed = True return output diff --git a/loopx/control_plane/testing/terminal_settlement_tool_behavior.py b/loopx/control_plane/testing/terminal_settlement_tool_behavior.py index 153cb401d1..f194623f04 100644 --- a/loopx/control_plane/testing/terminal_settlement_tool_behavior.py +++ b/loopx/control_plane/testing/terminal_settlement_tool_behavior.py @@ -10,6 +10,7 @@ from pathlib import Path from typing import Any, cast +from ..coordination.goal_storage_initialization import initialize_goal_storage_target from ...heartbeat_prompt import build_heartbeat_prompt from ...todos import add_goal_todo, complete_goal_todo from ..quota.turn_envelope import quota_action_signature_document @@ -41,7 +42,15 @@ TERMINAL_SETTLEMENT_FIXTURE_GOAL_ID = "terminal-settlement-fixture" TERMINAL_SETTLEMENT_FIXTURE_AGENT_ID = "codex-terminal-settlement" -TERMINAL_SETTLEMENT_FIXTURE_TODO_ID = "todo_terminal001" +TERMINAL_SETTLEMENT_FIXTURE_TODO_OPERATION_ID = ( + "terminal-settlement-fixture-todo" +) +TERMINAL_SETTLEMENT_FIXTURE_TODO_ID = ( + "todo_" + + sha256(TERMINAL_SETTLEMENT_FIXTURE_TODO_OPERATION_ID.encode("utf-8")).hexdigest()[ + :24 + ] +) TERMINAL_SETTLEMENT_FIXTURE_PROOF = "fixture/settlement-proof.json" TERMINAL_SETTLEMENT_FIXTURE_ACTION_TEXT = ( f"Read `{TERMINAL_SETTLEMENT_FIXTURE_PROOF}`, verify the bounded delivery, " @@ -103,6 +112,21 @@ class _QualificationState: seen_clock: bool = False +def _initialize_fixture_goal_storage( + *, + runtime_root: Path, + goal: Mapping[str, Any], + registry_path: Path, +) -> None: + result = initialize_goal_storage_target( + runtime_root, + goal, + registry_path=registry_path, + ) + if not isinstance(result, dict) or result.get("authority_initialized") is not True: + raise ValueError("fixture canonical authority initialization did not complete") + + def _build_fixture(root: Path) -> _TerminalSettlementFixture: source_root = Path(__file__).resolve().parents[3] project_root = root / "project" @@ -173,48 +197,70 @@ def _build_fixture(root: Path) -> _TerminalSettlementFixture: "Settle one final validated delivery without losing quota accounting.\n\n" "## Next Action\n\n" f"- {TERMINAL_SETTLEMENT_FIXTURE_ACTION_TEXT}\n\n" + "## User Todo / Owner Review Reading Queue\n\n" "## Agent Todo\n\n" - f"- [ ] [P0] {TERMINAL_SETTLEMENT_FIXTURE_ACTION_TEXT}\n" - " \n", + "\n", encoding="utf-8", ) + goal = { + "id": TERMINAL_SETTLEMENT_FIXTURE_GOAL_ID, + "creation_operation_id": "goal-create:terminal-settlement-fixture", + "domain": "terminal-settlement-live-fixture", + "status": "active", + "repo": str(project_root), + "state_file": str(state_relative), + "adapter": { + "kind": "fixture_connected_delivery_v0", + "status": "connected-delivery", + }, + "coordination": { + "registered_agents": [TERMINAL_SETTLEMENT_FIXTURE_AGENT_ID], + "agent_model": "peer_v1", + "storage_target": { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "file", + "handoff_mode": "soft_claim", + }, + }, + "authority_sources": [], + "quota": { + "compute": 1.0, + "window_hours": 24, + "allowed_slots": 5, + }, + } registry = { "schema_version": "0.1", "updated_at": "2026-08-16T00:00:00+08:00", "common_runtime_root": str(runtime_root), - "goals": [ - { - "id": TERMINAL_SETTLEMENT_FIXTURE_GOAL_ID, - "domain": "terminal-settlement-live-fixture", - "status": "active", - "repo": str(project_root), - "state_file": str(state_relative), - "adapter": { - "kind": "fixture_connected_delivery_v0", - "status": "connected-delivery", - }, - "coordination": { - "registered_agents": [TERMINAL_SETTLEMENT_FIXTURE_AGENT_ID], - "agent_model": "peer_v1", - }, - "authority_sources": [], - "quota": { - "compute": 1.0, - "window_hours": 24, - "allowed_slots": 5, - }, - } - ], + "goals": [goal], } registry_text = json.dumps(registry, ensure_ascii=False, indent=2, sort_keys=True) for registry_path in (local_registry_path, global_registry_path): registry_path.parent.mkdir(parents=True, exist_ok=True) registry_path.write_text(registry_text + "\n", encoding="utf-8") + _initialize_fixture_goal_storage( + runtime_root=runtime_root, + goal=goal, + registry_path=global_registry_path, + ) + created = add_goal_todo( + registry_path=global_registry_path, + goal_id=TERMINAL_SETTLEMENT_FIXTURE_GOAL_ID, + role="agent", + text=TERMINAL_SETTLEMENT_FIXTURE_ACTION_TEXT, + priority="P0", + task_class="advancement_task", + action_kind="terminal_settlement_qualification", + task_repository=( + "git:github.com/example/terminal-settlement-fixture" + ), + continuation_policy="independent_handoff", + claimed_by=TERMINAL_SETTLEMENT_FIXTURE_AGENT_ID, + operation_id=TERMINAL_SETTLEMENT_FIXTURE_TODO_OPERATION_ID, + ) + if created.get("todo_id") != TERMINAL_SETTLEMENT_FIXTURE_TODO_ID: + raise ValueError("fixture canonical Todo identity drifted") prompt = build_heartbeat_prompt( goal_id=TERMINAL_SETTLEMENT_FIXTURE_GOAL_ID, @@ -367,37 +413,40 @@ def _build_reentry_fixture(root: Path) -> _TerminalSettlementReentryFixture: "## Agent Todo\n\n", encoding="utf-8", ) + goal = { + "id": TERMINAL_SETTLEMENT_REENTRY_FIXTURE_GOAL_ID, + "creation_operation_id": "goal-create:terminal-reentry-fixture", + "domain": "terminal-reentry-live-fixture", + "status": "active", + "repo": str(project_root), + "state_file": str(state_relative), + "adapter": { + "kind": "fixture_connected_delivery_v0", + "status": "connected-delivery", + }, + "coordination": { + "registered_agents": [TERMINAL_SETTLEMENT_REENTRY_FIXTURE_AGENT_ID], + "agent_model": "peer_v1", + "storage_target": { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "file", + "handoff_mode": "soft_claim", + }, + }, + "authority_sources": [], + "quota": { + "compute": 1.0, + "window_hours": 24, + "allowed_slots": 5, + }, + } registry_path.write_text( json.dumps( { "schema_version": "0.1", "updated_at": "2026-08-23T00:00:00+08:00", "common_runtime_root": str(runtime_root), - "goals": [ - { - "id": TERMINAL_SETTLEMENT_REENTRY_FIXTURE_GOAL_ID, - "domain": "terminal-reentry-live-fixture", - "status": "active", - "repo": str(project_root), - "state_file": str(state_relative), - "adapter": { - "kind": "fixture_connected_delivery_v0", - "status": "connected-delivery", - }, - "coordination": { - "registered_agents": [ - TERMINAL_SETTLEMENT_REENTRY_FIXTURE_AGENT_ID - ], - "agent_model": "peer_v1", - }, - "authority_sources": [], - "quota": { - "compute": 1.0, - "window_hours": 24, - "allowed_slots": 5, - }, - } - ], + "goals": [goal], }, ensure_ascii=False, indent=2, @@ -406,6 +455,11 @@ def _build_reentry_fixture(root: Path) -> _TerminalSettlementReentryFixture: + "\n", encoding="utf-8", ) + _initialize_fixture_goal_storage( + runtime_root=runtime_root, + goal=goal, + registry_path=registry_path, + ) todos = [ add_goal_todo( registry_path=registry_path, @@ -414,6 +468,7 @@ def _build_reentry_fixture(root: Path) -> _TerminalSettlementReentryFixture: text=f"Validate terminal slice {index}.", task_class="advancement_task", claimed_by=TERMINAL_SETTLEMENT_REENTRY_FIXTURE_AGENT_ID, + operation_id=f"terminal-reentry-fixture-todo-{index}", ) for index in (1, 2) ] @@ -1090,7 +1145,6 @@ def receipt(*, passed: bool, failure_code: str | None) -> dict[str, Any]: ) if todo_id in state.settled_todo_ids: raise ValueError("repeated_lifecycle_settlement") - note = str(argument_value(tokens, "--note") or "").strip() if ( expected_tokens is None or argument_value(tokens, "--goal-id") @@ -1105,8 +1159,7 @@ def receipt(*, passed: bool, failure_code: str | None) -> dict[str, Any]: or "--turn-instance-id" in tokens or "--replan-obligation-id" in tokens or "--no-follow-up" not in tokens - or not note - or note == "" + or "--note" in tokens ): raise ValueError("lifecycle_settlement_identity_mismatch") payload = _execute_reentry_cli(tool_call.command, state=state) diff --git a/loopx/control_plane/todos/handoff_mode.py b/loopx/control_plane/todos/handoff_mode.py index dd92a3558d..353fa78764 100644 --- a/loopx/control_plane/todos/handoff_mode.py +++ b/loopx/control_plane/todos/handoff_mode.py @@ -29,18 +29,11 @@ from __future__ import annotations -from contextlib import ExitStack from pathlib import Path from typing import Any from ...paths import effective_runtime_root -from ..coordination.authority_core import ( - HandoffMode, - OwnershipGate, - ownership_gate_requirement, -) -from ..goals.active_state_metadata import parse_state_frontmatter, split_state_frontmatter -from .contract import normalize_todo_claimed_by +from ..goals.active_state_metadata import parse_state_frontmatter HANDOFF_MODE_SCHEMA_VERSION = "goal_handoff_mode_v0" HANDOFF_MODE_FRONTMATTER_KEY = "handoff_mode" @@ -52,7 +45,6 @@ HANDOFF_MODE_SOFT_CLAIM, HANDOFF_MODE_HARD_LEASE, ) -DELEGATED_AUTHORITY_MODE = "delegated_orchestration_override" class HandoffModeError(ValueError): @@ -104,138 +96,6 @@ def _resolve_state( ) -def goal_handoff_mode_for_goal( - *, - registry_path: Path, - goal_id: str, - project: Path | None = None, - state_file: Path | None = None, -) -> str: - return str(show_goal_handoff_mode(registry_path=registry_path, goal_id=goal_id, - project=project, state_file=state_file)["handoff_mode"]) - - -def enter_todo_ownership_handoff_gate( - stack: ExitStack, - *, - state_text: str, - registry_path: Path, - goal_id: str, - todo_id: str, - mutation_authority: dict[str, Any], - actor_agent_id: str | None, - ownership_mutation: bool, - runtime_root: Path | None = None, -) -> dict[str, Any]: - """Gate one claimed_by mutation on an existing todo behind the goal mode. - - Returns additive payload fields (always ``handoff_mode``; plus the door - marker or the held holder-gate receipt in hard_lease mode). In hard_lease - mode the per-goal lease lock is entered on ``stack`` so it stays held - through the markdown commit, matching the completion fence's - state-lock-then-lease-lock order. - """ - - mode = goal_handoff_mode(state_text) - extras: dict[str, Any] = {"handoff_mode": mode} - gate = ownership_gate_requirement( - handoff_mode=HandoffMode(mode), - ownership_mutation=ownership_mutation, - authority_mode=str(mutation_authority.get("mode") or "") or None, - ) - if gate is OwnershipGate.NOT_REQUIRED: - return extras - if gate is OwnershipGate.DELEGATED_OVERRIDE: - extras["handoff_gate_overridden"] = True - return extras - from ..work_items.task_lease import hold_handoff_lease_holder_gate - - extras["task_lease_holder_gate"] = stack.enter_context( - hold_handoff_lease_holder_gate( - registry_path=registry_path, - goal_id=goal_id, - todo_id=todo_id, - actor_agent_id=actor_agent_id, - runtime_root=runtime_root, - ) - ) - return extras - - -def enter_added_todo_ownership_handoff_gate( - stack: ExitStack, - *, - lines: list[str], - state_text: str, - registry_path: Path, - goal_id: str, - role: str, - text: str, - claimed_by: str | None, - actor_agent_id: str | None, - runtime_root: Path | None = None, -) -> dict[str, Any]: - """Gate the ``todo add`` path when it would reassign an existing todo. - - ``todo add`` matches an open todo by text and upserts its metadata, so a - ``claimed_by`` argument on that branch changes the claim of a todo that - may already hold a lease. Creating a todo stays ungated in every mode: a - todo id that does not exist yet cannot hold one. Re-adding a todo with the - claim it already carries is not an ownership change and is not gated. - - The delegated-authority door is not offered here. Reassignment under a - ``coordination.todo_lifecycle_authority`` grant goes through - ``todo update --claimed-by``, which is the verb that owns that action. - """ - - from .addition import matching_todo_block - from .active_state_editing import section_bounds - - bounds = section_bounds(lines, role) - existing = ( - matching_todo_block( - lines, bounds[0], bounds[1], text, role=role, source_section=bounds[2] - ) - if bounds - else None - ) - requested = normalize_todo_claimed_by(claimed_by) if claimed_by else None - current = normalize_todo_claimed_by(existing.get("claimed_by")) if existing else None - return enter_todo_ownership_handoff_gate( - stack, - state_text=state_text, - registry_path=registry_path, - goal_id=goal_id, - todo_id=str(existing.get("todo_id") or "") if existing else "", - mutation_authority={}, - actor_agent_id=actor_agent_id, - ownership_mutation=( - role == "agent" - and existing is not None - and requested is not None - and requested != current - ), - runtime_root=runtime_root, - ) - - -def resolve_todo_completion_handoff( - *, - state_text: str, - mutation_authority: dict[str, Any], -) -> dict[str, Any]: - """Resolve mode plus the delegated-authority door for one todo completion.""" - - mode = goal_handoff_mode(state_text) - extras: dict[str, Any] = {"handoff_mode": mode} - if ( - mode == HANDOFF_MODE_HARD_LEASE - and mutation_authority.get("mode") == DELEGATED_AUTHORITY_MODE - ): - extras["handoff_gate_overridden"] = True - return extras - - def show_goal_handoff_mode( *, registry_path: Path, @@ -273,21 +133,6 @@ def show_goal_handoff_mode( } -def _plan_legacy_mode(request: dict[str, Any]) -> dict[str, Any]: - from ..effect_runtime import effect_runtime_result - - result = effect_runtime_result("coordination.handoff_mode.legacy_plan", request) - if not isinstance(result, dict) or result.get("schema_version") != "loopx_legacy_handoff_mode_plan_result_v0": - raise HandoffModeError("invalid typed handoff plan", code="handoff_mode_plan_unavailable") - if result.get("outcome") == "rejected": - raise HandoffModeError( - "handoff mode change rejected; resolve the reported state or ownership blockers", - code=result["code"], payload={key: value for key, value in result.items() - if key not in {"schema_version", "outcome", "code", "changed"}}, - ) - return result - - def set_goal_handoff_mode( *, registry_path: Path, @@ -299,24 +144,7 @@ def set_goal_handoff_mode( operation_id: str | None = None, dry_run: bool = False, ) -> dict[str, Any]: - """Set the authoritative goal mode; changed modes require quiescence. - - Promoted Goals use one provider transaction; the remaining text below - describes the unpromoted compatibility writer. - - Changed modes require complete unclaimed Todo state and no time-active - leases. Event source locks remain held through the frontmatter replacement. - Hand-editing frontmatter bypasses this check and is outside the contract. - """ - - from ..coordination.legacy_writer_fence import ( - LegacyCoordinationWriterFenced, - require_legacy_coordination_write_allowed, - ) - from ..coordination.local_authority import ( - LocalCoordinationAuthorityRejection, - LocalCoordinationAuthorityUnavailable, - ) + """Set the provider-owned mode; unpromoted Markdown is read-only in 2.0.""" requested = normalize_handoff_mode(mode) if not str(mode or "").strip(): @@ -327,94 +155,10 @@ def set_goal_handoff_mode( from .provider_handoff_mode import set_canonical_handoff_mode runtime_root = effective_runtime_root(registry_path, runtime_root_arg) - try: - canonical = set_canonical_handoff_mode( - runtime_root=runtime_root, - goal_id=goal_id, - mode=requested, - operation_id=operation_id, - dry_run=dry_run, - ) - except LocalCoordinationAuthorityRejection: - raise - except LocalCoordinationAuthorityUnavailable: - # A present legacy fence is the admission boundary for this caller. - # Re-check it when canonical dispatch is unavailable so an outage cannot - # turn a fenced legacy writer into an attempted Markdown mutation. - try: - require_legacy_coordination_write_allowed( - runtime_root=runtime_root, - goal_id=goal_id, - ) - except LegacyCoordinationWriterFenced: - raise - raise - if canonical is not None: - return canonical - if operation_id is not None: - raise HandoffModeError("--operation-id requires canonical authority", code="handoff_mode_operation_id_unsupported") - # Compatibility source IO is needed only after canonical routing declines. - # Promoted commands and provider failures must survive producer retirement. - from .handoff_mode_source import handoff_mode_source - from ..coordination.legacy_writer_fence import legacy_todo_write_transaction - from ..coordination.runtime_shadow_writer_adapter import ( - write_captured_todo_state, - begin_todo_runtime_shadow_capture, - settle_todo_runtime_shadow_capture, - ) - - _project, resolved_state_file = _resolve_state( - registry_path=registry_path, + return set_canonical_handoff_mode( + runtime_root=runtime_root, goal_id=goal_id, - project=project, - state_file=state_file, - ) - # The already resolved root also governs the legacy lock, scan and capture. - with legacy_todo_write_transaction( - registry_path, goal_id, resolved_state_file, None, "handoff_mode_set", - dry_run, runtime_root=runtime_root, - ): - with resolved_state_file.open(encoding="utf-8", newline="") as source: - original = source.read() - metadata, body = split_state_frontmatter(original) - frontmatter = original[:len(original) - len(body)] - request = { - "schema_version": "loopx_legacy_handoff_mode_plan_request_v0", - "previous_value": metadata.get(HANDOFF_MODE_FRONTMATTER_KEY), - "requested_mode": requested, "frontmatter_text": frontmatter, - "todos": None, "leases": None, - } - plan = _plan_legacy_mode(request) - payload = { - "ok": True, "schema_version": HANDOFF_MODE_SCHEMA_VERSION, - "action": "set", "goal_id": goal_id, "state_file": str(resolved_state_file), - **{key: value for key, value in plan.items() if key.startswith("previous_mode")}, - "handoff_mode": requested, "changed": False, - **({"dry_run": True} if dry_run else {}), - } - if plan["outcome"] == "no_change": - return payload - if plan["outcome"] != "snapshot_required": - raise HandoffModeError("unexpected handoff planning phase", code="handoff_mode_plan_unavailable") - with handoff_mode_source(registry_path=registry_path, goal_id=goal_id, - state_path=resolved_state_file, state_text=original, runtime_root=runtime_root) as facts: - try: - plan = _plan_legacy_mode({**request, **facts}) - except HandoffModeError as error: - error.payload.update(goal_id=goal_id, requested_mode=requested) - raise - if plan.get("outcome") != "apply" or not isinstance(plan.get("next_frontmatter_text"), str): - raise HandoffModeError("incomplete handoff mutation plan", code="handoff_mode_plan_unavailable") - if dry_run: - return {**payload, "changed": True} - capture = begin_todo_runtime_shadow_capture( - registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id, - state_path=resolved_state_file, write_class="handoff_mode_set", original_text=original) - write_captured_todo_state(capture, runtime_root=runtime_root, goal_id=goal_id, - state_path=resolved_state_file, text=plan["next_frontmatter_text"] + body) - payload["changed"] = True - return settle_todo_runtime_shadow_capture( - payload, registry_path=registry_path, runtime_root=runtime_root, - goal_id=goal_id, capture=capture, - emit_disabled=False, + mode=requested, + operation_id=operation_id, + dry_run=dry_run, ) diff --git a/loopx/control_plane/todos/handoff_mode_source.py b/loopx/control_plane/todos/handoff_mode_source.py deleted file mode 100644 index 3ab28a85b7..0000000000 --- a/loopx/control_plane/todos/handoff_mode_source.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Locked compatibility inputs for the typed handoff transition planner. - -The caller holds the active-state mutex; local leases share their existing -mutex. Retired Todo event sources are rejected before quiescence is projected. -""" -from __future__ import annotations - -from contextlib import ExitStack, contextmanager -from collections.abc import Iterator -from pathlib import Path -from typing import Any - - -@contextmanager -def handoff_mode_source( - *, registry_path: Path, goal_id: str, state_path: Path, state_text: str, - runtime_root: Path, -) -> Iterator[dict[str, Any]]: - from ..goals.legacy_event_source import require_no_legacy_todo_events, RetiredTodoEventSourceError - from ...file_lock import exclusive_cross_runtime_file_lock - from ..runtime.time import now_local_iso - from ..work_items.task_lease import read_lease, task_lease_dir, task_lease_lock_path - from .goal_todo_projection import project_goal_todo_items - from .handoff_mode import HandoffModeError - - from ...history import load_registry - from ...registry import find_registry_goal - goal = find_registry_goal(load_registry(registry_path), goal_id) or {"id": goal_id} - with ExitStack() as stack: - stack.enter_context(exclusive_cross_runtime_file_lock( - task_lease_lock_path(runtime_root=runtime_root, goal_id=goal_id), operation="handoff_mode_set")) - try: - require_no_legacy_todo_events(goal, state_path=state_path) - todos = project_goal_todo_items(goal, state_text=state_text, - state_path=state_path, rollout_events=[]) - except (OSError, RetiredTodoEventSourceError) as error: - raise HandoffModeError("cannot establish handoff quiescence from the legacy source", - code="handoff_mode_source_unavailable") from error - fields = ("todo_id", "done", "status", "claimed_by", "archive_state") - leases = [] - for path in sorted(task_lease_dir(runtime_root=runtime_root, goal_id=goal_id).glob("todo_*.json")): - lease = read_lease(path) - if lease is not None: - leases.append({**lease, "lease_path": str(path)}) - yield {"todos": [{key: item[key] for key in fields if key in item} for item in todos], - "leases": leases, "observed_at": now_local_iso()} diff --git a/loopx/control_plane/todos/legacy_mutation.py b/loopx/control_plane/todos/legacy_mutation.py deleted file mode 100644 index 6db5f0f668..0000000000 --- a/loopx/control_plane/todos/legacy_mutation.py +++ /dev/null @@ -1,1520 +0,0 @@ -"""Retained unpromoted Markdown writer; canonical callers never import this module.""" - -from __future__ import annotations - -from collections.abc import Mapping -from contextlib import ExitStack -from json import dumps as json_dumps -from pathlib import Path -from typing import Any - -from ...agent_registry import ( - registered_agent_ids_from_registry, - require_registered_agent_id, -) -from ...state_refresh import now_local -from .contract import ( - TODO_STATUS_DONE, - TODO_STATUS_OPEN, - build_todo_id, - format_todo_metadata_line, - metadata_line_for_todo_block, - normalize_required_capabilities, - normalize_required_write_scopes, - normalize_explore_result_node_refs, - normalize_target_capabilities, - normalize_todo_blocks_agent, - normalize_todo_bound_agent, - normalize_todo_capability_binding_ref, - normalize_todo_claimed_by, - normalize_todo_continuation_policy, - normalize_todo_decision_scope, - normalize_todo_excluded_agents, - normalize_todo_global_gate, - normalize_todo_goal_bound, - normalize_todo_id, - normalize_todo_id_list, - normalize_todo_required_decision_scopes, - normalize_todo_replan_obligation_id, - normalize_todo_resume_when, - normalize_todo_status, - normalize_todo_task_domain, - normalize_todo_task_repository, - parse_todo_metadata_line, - require_todo_excluded_agents, - resolve_next_user_task_class, - require_supported_todo_resume_when, - todo_marker_for_status, -) -from .active_state_editing import ( - TODO_SECTION_HEADINGS, - find_todo_block, - insert_into_existing_section, - insert_new_section, - replace_updated_at, - section_bounds, - set_todo_marker, - todo_blocks, -) -from .addition import matching_todo_block, require_replan_successor_rebinding -from .completed_archive import ( - DEFAULT_COMPLETED_TODO_ARCHIVE_HEADROOM, - DEFAULT_MAX_ACTIVE_DONE_TODOS_BEFORE_ARCHIVE, - archive_completed_todo_lines, -) -from .completion_policy import ( - completion_policy_from_transaction, -) -from .completion_transaction import ( - locked_todo_completion_transaction, - materialized_todo_completion_replay, - require_completion_successor_todo_ids, - user_todo_completion_metadata_updates, -) -from ..todos import completion_validation as completion_validation_module -from .mutation_response import serialize_added_todo_payload -from .next_action_runtime import ( - apply_added_todo_next_action, - settle_completed_todo_next_action, -) -from ..todos import monitor_metadata as todo_monitor_metadata -from .mutation_authority import ( - authorize_todo_lifecycle_mutation, - todo_update_authority_action, -) -from .succession_warning import build_open_parent_successor_advisory -from .successor_derivation import ( - build_successor_intents, - derive_successor_proposals, - successor_add_kwargs, -) -from .text import normalize_new_todo -from .todo_semantics import todo_priority_label -from .unblock_resume import ( - apply_completed_user_todo_lifecycle, - completion_decision_target, - require_completion_decision_outcome, -) -from .write_correctness import ( - attach_todo_write_correctness_dry_run_packet as _attach_todo_write_correctness_dry_run_packet, -) -from .authoring_scope import require_user_todo_task_class -from ..coordination.legacy_writer_fence import legacy_todo_write_transaction -from .path_resolution import resolve_todo_state_path -from .handoff_mode import ( - goal_handoff_mode, - enter_added_todo_ownership_handoff_gate, - enter_todo_ownership_handoff_gate, - resolve_todo_completion_handoff, -) -from ...paths import effective_runtime_root -from ..work_items.task_lease import ( - enter_terminal_todo_lease_fence, - hold_task_lease_mutation_fence, - release_verified_task_lease_fence, -) - - -ARCHIVE_COMPLETED_DEFAULT_MAX_ACTIVE_DONE = max( - 0, DEFAULT_MAX_ACTIVE_DONE_TODOS_BEFORE_ARCHIVE - DEFAULT_COMPLETED_TODO_ARCHIVE_HEADROOM -) - - -def _validate_legacy_add_declaration( - *, - role: str, - task_class: str | None, - blocks_agent: str | None, - excluded_agents: list[str] | None, - capability_binding_ref: str | None, - global_gate: bool | None, - validation_command: str | None, - validation_command_json: str | None, - validation_timeout_seconds: int | None, -) -> list[str] | None: - """Validate the retained writer's declaration before any line mutation.""" - if validation_command and validation_command_json: - raise ValueError( - "--validation-command and --validation-command-json are mutually " - "exclusive; declare the validation command in exactly one form" - ) - validation_argv = completion_validation_module.normalize_validation_command_json( - validation_command_json - ) - if validation_timeout_seconds is not None: - if not validation_command and validation_argv is None: - raise ValueError( - "--validation-timeout-seconds requires --validation-command " - "or --validation-command-json" - ) - if not ( - 1 - <= validation_timeout_seconds - <= completion_validation_module.COMPLETION_VALIDATION_TIMEOUT_MAX_SECONDS - ): - raise ValueError( - "--validation-timeout-seconds must be between 1 and " - f"{completion_validation_module.COMPLETION_VALIDATION_TIMEOUT_MAX_SECONDS} (the outer " - "CLI/MCP subprocess budget is 30s, and a timed-out " - "validation must still produce a typed receipt)" - ) - if role == "agent" and blocks_agent: - raise ValueError( - "blocks_agent is only valid for user gates; use excluded_agents for " - "agent executor constraints" - ) - if role != "agent" and excluded_agents: - raise ValueError("excluded_agents is only valid for agent todos") - if role != "agent" and capability_binding_ref: - raise ValueError("capability_binding_ref is only valid for agent todos") - require_user_todo_task_class( - role=role, - task_class=task_class, - blocks_agent=blocks_agent, - global_gate=global_gate, - ) - return validation_argv - - -def add_todo_to_lines( - lines: list[str], - *, - role: str, - text: str, - status: str | None = None, - task_class: str | None = None, - action_kind: str | None = None, - task_domain: str | None = None, - capability_binding_ref: str | None = None, - task_repository: str | None = None, - continuation_policy: str | None = None, - required_write_scopes: list[str] | None = None, - required_capabilities: list[str] | None = None, - target_capabilities: list[str] | None = None, - explore_result_node_refs: list[str] | None = None, - decision_scope: Any = None, - required_decision_scopes: Any = None, - claimed_by: str | None = None, - bound_agent: str | None = None, - goal_bound: bool | None = None, - blocks_agent: str | None = None, - excluded_agents: list[str] | None = None, - global_gate: bool | None = None, - unblocks_todo_id: str | None = None, - replan_obligation_id: str | None = None, - resume_when: str | None = None, - validation_command: str | None = None, - validation_command_json: str | None = None, - validation_label: str | None = None, - validation_timeout_seconds: int | None = None, - monitor_metadata: dict[str, Any] | None = None, - note: str | None = None, - evidence: str | None = None, - updated_at: str | None = None, - _prepared: dict[str, Any] | None = None, -) -> dict[str, Any]: - from .line_update import upsert_todo_metadata - - # Only public add supplies its same-call typed draft. Standalone line - # codecs and successor creation still validate their own complete input. - if _prepared is not None: - validation_argv = _prepared["validation_argv"] - todo_text = _prepared["todo_text"] - normalized_status = _prepared["normalized_status"] - normalized_resume_when = _prepared["normalized_resume_when"] - normalized_monitor_metadata = _prepared["normalized_monitor_metadata"] - else: - validation_argv = _validate_legacy_add_declaration( - role=role, - task_class=task_class, - blocks_agent=blocks_agent, - excluded_agents=excluded_agents, - capability_binding_ref=capability_binding_ref, - global_gate=global_gate, - validation_command=validation_command, - validation_command_json=validation_command_json, - validation_timeout_seconds=validation_timeout_seconds, - ) - todo_text = normalize_new_todo(text) - normalized_status = normalize_todo_status(status) if status else TODO_STATUS_OPEN - if status and not normalized_status: - raise ValueError("todo status must be one of: open, done, blocked, deferred") - assert normalized_status is not None - normalized_resume_when = require_supported_todo_resume_when(resume_when) - normalized_monitor_metadata = todo_monitor_metadata.require_monitor_metadata_scope( - monitor_metadata=monitor_metadata, - role=role, - task_class=task_class, generated_at=updated_at, - ) - bounds = section_bounds(lines, role) - section = bounds[2] if bounds else TODO_SECTION_HEADINGS[role] - existing_blocks = ( - todo_blocks(lines, bounds[0], bounds[1], role=role, source_section=section) - if bounds - else [] - ) - block = matching_todo_block( - lines, - bounds[0], - bounds[1], - todo_text, - role=role, - source_section=section, - ) if bounds else None - added = block is None - metadata_updated = False - status_changed = False - - if block is None: - todo_id = build_todo_id( - role=role, - source_section=section, - index=len(existing_blocks) + 1, - text=todo_text, - ) - metadata_line = format_todo_metadata_line( - todo_id=todo_id, - status=normalized_status, - task_class=task_class, - action_kind=action_kind, - task_domain=task_domain, - capability_binding_ref=capability_binding_ref, - task_repository=task_repository, - continuation_policy=continuation_policy, - required_write_scopes=required_write_scopes, - required_capabilities=required_capabilities, - target_capabilities=target_capabilities, - explore_result_node_refs=explore_result_node_refs, - decision_scope=decision_scope, - required_decision_scopes=required_decision_scopes, - claimed_by=claimed_by, - bound_agent=bound_agent, - goal_bound=goal_bound, - blocks_agent=blocks_agent, - excluded_agents=excluded_agents, - global_gate=global_gate, - unblocks_todo_id=unblocks_todo_id, - replan_obligation_id=replan_obligation_id, - resume_when=normalized_resume_when, - validation_command=validation_command, - validation_command_argv=( - json_dumps(validation_argv) - if validation_argv is not None - else None - ), - validation_label=validation_label, - validation_timeout_seconds=( - str(validation_timeout_seconds) - if validation_timeout_seconds is not None - else None - ), - **normalized_monitor_metadata, - note=note, - evidence=evidence, - updated_at=updated_at, - ) - marker = todo_marker_for_status(normalized_status) - todo_line = "\n".join([f"- [{marker}] {todo_text}", metadata_line] if metadata_line else [f"- [{marker}] {todo_text}"]) - if bounds: - insert_into_existing_section(lines, bounds[0], bounds[1], todo_line) - else: - insert_new_section(lines, role, todo_line) - effective_metadata = parse_todo_metadata_line(metadata_line or "") or {} - else: - updates: dict[str, Any] = { - "todo_id": block.get("todo_id"), - "status": normalized_status if status else block.get("status") or TODO_STATUS_OPEN, - } - if status: - status_changed = set_todo_marker(lines, block, normalized_status) - for metadata_field, metadata_value in ( - ("task_class", task_class), - ("action_kind", action_kind), - ("task_domain", task_domain), - ("task_repository", task_repository), - ("continuation_policy", continuation_policy), - ("claimed_by", claimed_by), - ("blocks_agent", blocks_agent), - ("unblocks_todo_id", unblocks_todo_id), - ("note", note), - ("evidence", evidence), - ): - if metadata_value: - updates[metadata_field] = metadata_value - if capability_binding_ref: - requested_binding_ref = normalize_todo_capability_binding_ref( - capability_binding_ref - ) - if not requested_binding_ref: - raise ValueError( - "capability_binding_ref must be a public-safe namespaced token" - ) - existing_binding_ref = normalize_todo_capability_binding_ref( - block.get("capability_binding_ref") - ) - if ( - existing_binding_ref - and existing_binding_ref != requested_binding_ref - ): - raise ValueError( - "capability_binding_ref is immutable once set" - ) - updates["capability_binding_ref"] = requested_binding_ref - if required_write_scopes is not None: - updates["required_write_scopes"] = required_write_scopes - if required_capabilities is not None: - updates["required_capabilities"] = required_capabilities - if target_capabilities is not None: - updates["target_capabilities"] = target_capabilities - if explore_result_node_refs is not None: - updates["explore_result_node_refs"] = explore_result_node_refs - if decision_scope is not None: - updates["decision_scope"] = decision_scope - if required_decision_scopes is not None: - updates["required_decision_scopes"] = required_decision_scopes - if bound_agent: - updates["bound_agent"] = bound_agent - updates["goal_bound"] = None - elif goal_bound is not None: - updates["bound_agent"] = None - updates["goal_bound"] = goal_bound - if excluded_agents is not None: - updates["excluded_agents"] = excluded_agents - if global_gate is not None: - updates["global_gate"] = global_gate - if replan_obligation_id: - updates["replan_obligation_id"] = require_replan_successor_rebinding( - existing_obligation_id=block.get("replan_obligation_id"), - requested_obligation_id=replan_obligation_id, - ) - if normalized_resume_when: - updates["resume_when"] = normalized_resume_when - updates.update(normalized_monitor_metadata) - if updated_at and not block.get("updated_at"): - updates["updated_at"] = updated_at - metadata_line = metadata_line_for_todo_block(block, updates) - metadata_updated = upsert_todo_metadata(lines, block, metadata_line) - todo_id = str(block.get("todo_id") or "") - effective_metadata = parse_todo_metadata_line(metadata_line or "") or {} - - return { - "added": added, - "already_exists": not added, - "metadata_updated": metadata_updated, - "status_changed": status_changed, - "changed": added or metadata_updated or status_changed, - "role": role, - "section": section, - "todo": todo_text, - "priority": todo_priority_label({"text": todo_text}), - "todo_id": todo_id, - "status": normalize_todo_status(effective_metadata.get("status")) or normalized_status, - "task_class": effective_metadata.get("task_class") or task_class, - "action_kind": effective_metadata.get("action_kind") or action_kind, - "task_domain": normalize_todo_task_domain( - effective_metadata.get("task_domain") or task_domain - ), - "capability_binding_ref": effective_metadata.get("capability_binding_ref") - or capability_binding_ref, - "task_repository": normalize_todo_task_repository( - effective_metadata.get("task_repository") or task_repository - ), - "continuation_policy": normalize_todo_continuation_policy( - effective_metadata.get("continuation_policy") or continuation_policy - ), - "required_write_scopes": normalize_required_write_scopes( - effective_metadata.get("required_write_scopes") or required_write_scopes - ), - "required_capabilities": normalize_required_capabilities( - effective_metadata.get("required_capabilities") or required_capabilities - ), - "target_capabilities": normalize_target_capabilities( - effective_metadata.get("target_capabilities") or target_capabilities - ), - "explore_result_node_refs": normalize_explore_result_node_refs( - effective_metadata.get("explore_result_node_refs") or explore_result_node_refs - ), - "decision_scope": normalize_todo_decision_scope( - effective_metadata.get("decision_scope") or decision_scope - ), - "required_decision_scopes": normalize_todo_required_decision_scopes( - effective_metadata.get("required_decision_scopes") or required_decision_scopes - ), - "claimed_by": normalize_todo_claimed_by(effective_metadata.get("claimed_by")), - "bound_agent": normalize_todo_bound_agent(effective_metadata.get("bound_agent")), - "goal_bound": normalize_todo_goal_bound(effective_metadata.get("goal_bound")), - "blocks_agent": normalize_todo_blocks_agent(effective_metadata.get("blocks_agent")), - "excluded_agents": normalize_todo_excluded_agents( - effective_metadata.get("excluded_agents") - ), - "global_gate": normalize_todo_global_gate(effective_metadata.get("global_gate")), - "unblocks_todo_id": normalize_todo_id(effective_metadata.get("unblocks_todo_id")), - "replan_obligation_id": normalize_todo_replan_obligation_id( - effective_metadata.get("replan_obligation_id") - ), - "resume_when": normalize_todo_resume_when(effective_metadata.get("resume_when")), - "target_key": effective_metadata.get("target_key"), - "cadence": effective_metadata.get("cadence"), - "next_due_at": effective_metadata.get("next_due_at"), - "expires_at": effective_metadata.get("expires_at"), - "watch_only": effective_metadata.get("watch_only"), - "note": effective_metadata.get("note") or note, - "evidence": effective_metadata.get("evidence") or evidence, - "updated_at": effective_metadata.get("updated_at") or updated_at, - } - - -def _add_goal_todo_legacy( - *, - registry_path: Path, - goal_id: str, - runtime_root_arg: str | None = None, - role: str, - text: str, - priority: str | None = None, - status: str | None = None, - note: str | None = None, - task_class: str | None = None, - action_kind: str | None = None, - task_domain: str | None = None, - capability_binding_ref: str | None = None, - task_repository: str | None = None, - continuation_policy: str | None = None, - required_write_scopes: list[str] | None = None, - required_capabilities: list[str] | None = None, - target_capabilities: list[str] | None = None, - explore_result_node_refs: list[str] | None = None, - decision_scope: Any = None, - required_decision_scopes: Any = None, - claimed_by: str | None = None, - bound_agent: str | None = None, - goal_bound: bool = False, - blocks_agent: str | None = None, - excluded_agents: list[str] | None = None, - global_gate: bool = False, - agent_id: str | None = None, - unblocks_todo_id: str | None = None, - replan_obligation_id: str | None = None, - resume_when: str | None = None, - validation_command: str | None = None, - validation_command_json: str | None = None, - validation_label: str | None = None, - validation_timeout_seconds: int | None = None, - monitor_metadata: dict[str, Any] | None = None, - project: Path | None = None, - state_file: Path | None = None, - dry_run: bool = False, - operation_id: str | None = None, - _prepared: dict[str, Any], -) -> dict[str, Any]: - effective_agent_id = _prepared["effective_agent_id"] - effective_blocks_agent = _prepared["effective_blocks_agent"] - effective_bound_agent = _prepared["effective_bound_agent"] - effective_claimed_by = _prepared["effective_claimed_by"] - effective_excluded_agents = _prepared["effective_excluded_agents"] - effective_goal_bound = _prepared["effective_goal_bound"] - normalized_monitor_metadata = _prepared["normalized_monitor_metadata"] - normalized_resume_when = _prepared["normalized_resume_when"] - normalized_status = _prepared["normalized_status"] - normalized_unblocks_todo_id = _prepared["normalized_unblocks_todo_id"] - replan_obligation_id = _prepared["replan_obligation_id"] - shadow_runtime_root = _prepared["shadow_runtime_root"] - todo_text = _prepared["todo_text"] - updated_at = _prepared["updated_at"] - from ..coordination.runtime_shadow_writer_adapter import begin_todo_runtime_shadow_capture, write_captured_todo_state, settle_todo_runtime_shadow_capture - - resolved_project, resolved_state_file = resolve_todo_state_path( - registry_path=registry_path, - goal_id=goal_id, - project=project, - state_file=state_file, - ) - - with legacy_todo_write_transaction( - registry_path, goal_id, resolved_state_file, agent_id or claimed_by, "todo_add", dry_run, - runtime_root=shadow_runtime_root, - ), ExitStack() as handoff_gate_stack: - original = resolved_state_file.read_text(encoding="utf-8") - shadow_capture = begin_todo_runtime_shadow_capture( - registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, state_path=resolved_state_file, - write_class="todo_add", original_text=original, - ) - lines = original.splitlines() - handoff_gate = enter_added_todo_ownership_handoff_gate( - handoff_gate_stack, - lines=lines, - state_text=original, - registry_path=registry_path, - goal_id=goal_id, - role=role, - text=todo_text, - claimed_by=effective_claimed_by, - actor_agent_id=effective_agent_id or effective_claimed_by, - runtime_root=shadow_runtime_root, - ) - add_result = add_todo_to_lines( - lines, - role=role, - text=todo_text, - status=normalized_status, - task_class=task_class, - action_kind=action_kind, - task_domain=task_domain, - capability_binding_ref=capability_binding_ref, - task_repository=task_repository, - continuation_policy=continuation_policy, - required_write_scopes=required_write_scopes, - required_capabilities=required_capabilities, - target_capabilities=target_capabilities, - explore_result_node_refs=explore_result_node_refs, - decision_scope=decision_scope, - required_decision_scopes=required_decision_scopes, - claimed_by=effective_claimed_by, - bound_agent=effective_bound_agent, - goal_bound=( - True if role == "user" and effective_goal_bound else None - ), - blocks_agent=effective_blocks_agent, - excluded_agents=effective_excluded_agents, - global_gate=True if global_gate else None, - unblocks_todo_id=normalized_unblocks_todo_id, - replan_obligation_id=replan_obligation_id, - resume_when=normalized_resume_when, - validation_command=validation_command, - validation_command_json=validation_command_json, - validation_label=validation_label, - validation_timeout_seconds=validation_timeout_seconds, - monitor_metadata=normalized_monitor_metadata, - note=note, - updated_at=updated_at, - _prepared=_prepared, - ) - added = bool(add_result["added"]) - metadata_updated = bool(add_result["metadata_updated"]) - changed = apply_added_todo_next_action(lines, role=role, add_result=add_result) - - new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "") - if changed: - new_text = replace_updated_at(new_text, updated_at) - if changed and not dry_run: - write_captured_todo_state(shadow_capture, runtime_root=shadow_runtime_root, goal_id=goal_id, - state_path=resolved_state_file, text=new_text) - - payload = serialize_added_todo_payload( - add_result=add_result, goal_id=goal_id, role=role, todo_text=todo_text, - agent_id=effective_agent_id, state_file=resolved_state_file, - project=resolved_project, updated_at=updated_at, dry_run=dry_run, - added=added, metadata_updated=metadata_updated, changed=changed, - handoff_gate=handoff_gate, - ) - payload = _attach_todo_write_correctness_dry_run_packet( - payload, - goal_id=goal_id, - write_class="todo_add", - state_text=original, - ) - return settle_todo_runtime_shadow_capture( - payload, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, capture=shadow_capture, - ) - - -def _update_goal_todo_legacy( - *, - registry_path: Path, - goal_id: str, - runtime_root_arg: str | None = None, - todo_id: str, - text: str | None = None, - priority: str | None = None, - clear_priority: bool = False, - status: str | None = None, - role: str | None = None, - note: str | None = None, - validation_command: str | None = None, - validation_command_json: str | None = None, - validation_label: str | None = None, - validation_timeout_seconds: int | None = None, - evidence: str | None = None, - reason: str | None = None, - task_class: str | None = None, - action_kind: str | None = None, - task_domain: str | None = None, - task_repository: str | None = None, - continuation_policy: str | None = None, - required_write_scopes: list[str] | None = None, - required_capabilities: list[str] | None = None, - target_capabilities: list[str] | None = None, - explore_result_node_refs: list[str] | None = None, - append_explore_result_node_refs: list[str] | None = None, - decision_scope: Any = None, - required_decision_scopes: Any = None, - claimed_by: str | None = None, - bound_agent: str | None = None, - goal_bound: bool = False, - blocks_agent: str | None = None, - clear_blocks_agent: bool = False, - excluded_agents: list[str] | None = None, - clear_excluded_agents: bool = False, - global_gate: bool = False, clear_global_gate: bool = False, - agent_id: str | None = None, authority_reason: str | None = None, - unblocks_todo_id: str | None = None, - successor_todo_ids: list[str] | None = None, - resume_when: str | None = None, - clear_resume_when: bool = False, - no_followup: bool | None = None, - monitor_metadata: todo_monitor_metadata.MonitorMetadataInput = None, - enforce_monitor_boundedness: bool = True, - monitor_gate_scope_guard: bool = False, - clear_claim: bool = False, - claim_only: bool = False, - claim_operation_id: str | None = None, - update_operation_id: str | None = None, - update_expected_provider_revision: str | None = None, - update_expected_registry_sha256: str | None = None, - task_lease_idempotency_key: str | None = None, - task_lease_expected_version: int | None = None, - project: Path | None = None, - state_file: Path | None = None, - dry_run: bool = False, - _prepared: dict[str, Any], -) -> dict[str, Any]: - monitor_intent = _prepared["monitor_intent"] - shadow_runtime_root = _prepared["shadow_runtime_root"] - from .line_update import apply_todo_update_to_lines - from ..coordination.runtime_shadow_writer_adapter import begin_todo_runtime_shadow_capture, write_captured_todo_state, settle_todo_runtime_shadow_capture - - resolved_project, resolved_state_file = resolve_todo_state_path( - registry_path=registry_path, - goal_id=goal_id, - project=project, - state_file=state_file, - ) - requested_successor_todo_ids = require_completion_successor_todo_ids( - successor_todo_ids - ) - completion_validation_gate = ( - completion_validation_module.prepare_user_todo_update_completion( - status=status, - state_file=resolved_state_file, - todo_id=todo_id, - role=role, - registry_path=registry_path, - goal_id=goal_id, - dry_run=dry_run, - no_followup=no_followup is True, - requested_has_successor=bool(requested_successor_todo_ids), - ) - ) - if completion_validation_gate is not None: - validation_failure: dict[str, Any] | None = completion_validation_gate.get("failure") - if validation_failure is not None: - return validation_failure - write_class = "todo_claim" if claim_only else "todo_update" - with legacy_todo_write_transaction( - registry_path, goal_id, resolved_state_file, agent_id or claimed_by, write_class, dry_run, - runtime_root=shadow_runtime_root, - ), ExitStack() as handoff_gate_stack: - original = resolved_state_file.read_text(encoding="utf-8") - shadow_capture = begin_todo_runtime_shadow_capture( - registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, state_path=resolved_state_file, - write_class=write_class, original_text=original, - ) - lines = original.splitlines() - updated_at = now_local() - effective_claimed_by = ( - require_registered_agent_id( - registry_path=registry_path, - goal_id=goal_id, - agent_id=claimed_by, - ) - if claimed_by - else None - ) - effective_agent_id = ( - require_registered_agent_id( - registry_path=registry_path, - goal_id=goal_id, - agent_id=agent_id, - field="agent_id", - ) - if agent_id - else None - ) - effective_blocks_agent = ( - require_registered_agent_id( - registry_path=registry_path, - goal_id=goal_id, - agent_id=blocks_agent, - field="blocks_agent", - ) - if blocks_agent - else None - ) - effective_bound_agent = ( - require_registered_agent_id( - registry_path=registry_path, - goal_id=goal_id, - agent_id=bound_agent, - field="bound_agent", - ) - if bound_agent - else None - ) - existing_block_match = find_todo_block(lines, todo_id=todo_id, role=role) - if not existing_block_match: - normalized_todo_id = normalize_todo_id(todo_id) or todo_id - raise ValueError(f"todo_id {normalized_todo_id!r} was not found in active user or agent todos") - existing_role, _section, _start, _end, existing_block = existing_block_match - target_role = role or existing_role - authority_todo = dict(existing_block) - authority_todo["role"] = target_role - authority_action = todo_update_authority_action( - existing_role=existing_role, - role=role, - claimed_by=claimed_by, - clear_claim=clear_claim, - other_values=( - text, status, note, evidence, reason, task_class, action_kind, - task_domain, - task_repository, continuation_policy, required_write_scopes, - required_capabilities, target_capabilities, - explore_result_node_refs, append_explore_result_node_refs, decision_scope, - required_decision_scopes, blocks_agent, clear_blocks_agent, - bound_agent, goal_bound, - excluded_agents, clear_excluded_agents, global_gate, - clear_global_gate, unblocks_todo_id, successor_todo_ids, - resume_when, clear_resume_when, no_followup, - ), - monitor_metadata=monitor_intent["observation"] or monitor_intent["metadata"], - ) - mutation_authority = authorize_todo_lifecycle_mutation( - registry_path=registry_path, - goal_id=goal_id, - command="claim" if claim_only else "update", - todo=authority_todo, - actor_agent_id=effective_agent_id, - authority_action=None if claim_only else authority_action, - authority_reason=authority_reason, - requested_claimed_by=effective_claimed_by, - ) - if monitor_gate_scope_guard: - todo_monitor_metadata.require_locked_monitor_gate_scope(state_text=original, - todo=authority_todo, observation=monitor_intent["observation"], agent_id=effective_agent_id) - if monitor_intent["observation"] is not None and task_lease_idempotency_key is not None: - # Explicit observation proof uses the existing native held fence - # under the Markdown writer lock. Closing this guard does not retire - # execution; the acquiring caller owns release after observation. - handoff_gate_stack.enter_context(hold_task_lease_mutation_fence( - registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, todo_id=todo_id, todo=authority_todo, actor_agent_id=effective_agent_id, - idempotency_key=task_lease_idempotency_key, - expected_version=task_lease_expected_version, - require_active_when_key_supplied=True, - handoff={"handoff_mode": goal_handoff_mode(original)}, - )) - handoff_gate = enter_todo_ownership_handoff_gate( - handoff_gate_stack, - state_text=original, - registry_path=registry_path, - goal_id=goal_id, - todo_id=str(authority_todo.get("todo_id") or todo_id), - mutation_authority=mutation_authority, - actor_agent_id=effective_agent_id or effective_claimed_by, - ownership_mutation=(claimed_by is not None or clear_claim) and target_role == "agent", - runtime_root=shadow_runtime_root, - ) - effective_excluded_agents = ( - [] if clear_excluded_agents else require_todo_excluded_agents(excluded_agents) - if excluded_agents is not None else None - ) - completion_metadata_updates_override = None - if completion_validation_gate is not None: - locked_completion = locked_todo_completion_transaction( - validation_gate=completion_validation_gate, - todo=existing_block, - goal_id=goal_id, - todo_id=todo_id, - dry_run=dry_run, - require_source_match=True, - missing_is_drift=False, - ) - locked_failure: dict[str, Any] | None = locked_completion["failure"] - if locked_failure is not None: - return locked_failure - completion_metadata_updates_override = ( - user_todo_completion_metadata_updates( - locked_completion["transaction"], - todo_already_done=( - str(existing_block.get("status") or "") - == TODO_STATUS_DONE - ), - ) - ) - normalized_unblocks_todo_id = normalize_todo_id(unblocks_todo_id) if unblocks_todo_id else None - if unblocks_todo_id and not normalized_unblocks_todo_id: - raise ValueError("unblocks_todo_id must use the public token shape todo_") - normalized_successor_todo_ids = requested_successor_todo_ids - update_result = apply_todo_update_to_lines( - lines, - todo_id=todo_id, - text=text, - priority=priority, - clear_priority=clear_priority, - status=status, - role=role, - note=note, - evidence=evidence, - reason=reason, - task_class=task_class, - action_kind=action_kind, - task_domain=task_domain, - task_repository=task_repository, - continuation_policy=continuation_policy, - required_write_scopes=required_write_scopes, - required_capabilities=required_capabilities, - target_capabilities=target_capabilities, - explore_result_node_refs=explore_result_node_refs, - append_explore_result_node_refs=append_explore_result_node_refs, - decision_scope=decision_scope, - required_decision_scopes=required_decision_scopes, - claimed_by=effective_claimed_by, - bound_agent=effective_bound_agent, - goal_bound=goal_bound, - blocks_agent=effective_blocks_agent, - clear_blocks_agent=clear_blocks_agent, - excluded_agents=effective_excluded_agents, - global_gate=True if global_gate else None, - clear_global_gate=clear_global_gate, - unblocks_todo_id=normalized_unblocks_todo_id, - successor_todo_ids=normalized_successor_todo_ids if successor_todo_ids is not None else None, - resume_when=resume_when, - clear_resume_when=clear_resume_when, - no_followup=no_followup, - completion_metadata_updates_override=( - completion_metadata_updates_override - ), - monitor_metadata=monitor_intent["metadata"], - public_context={ - "goal_id": goal_id, "role": target_role, - "actor_agent_id": effective_agent_id, - "registered_agents": registered_agent_ids_from_registry(registry_path, goal_id), - "monitor_observation": monitor_intent["observation"], - "enforce_monitor_boundedness": enforce_monitor_boundedness, - }, - clear_claim=clear_claim, - claim_only=claim_only, - updated_at=updated_at, - ) - changed = bool(update_result["changed"]) - new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "") - if changed: - new_text = replace_updated_at(new_text, updated_at) - if changed and not dry_run: - write_captured_todo_state(shadow_capture, runtime_root=shadow_runtime_root, goal_id=goal_id, - state_path=resolved_state_file, text=new_text) - payload = { - "ok": True, - "dry_run": dry_run, - "changed": changed, - "goal_id": goal_id, - "agent_id": effective_agent_id, - "mutation_authority": mutation_authority, - **handoff_gate, - **update_result, - "state_file": str(resolved_state_file), - "project": str(resolved_project) if resolved_project else None, - "updated_at": updated_at if changed else None, - } - if successor_todo_ids is not None: - parent_successor_advisory = build_open_parent_successor_advisory( - todo_id=update_result.get("todo_id"), - status=update_result.get("status"), - successor_todo_ids=update_result.get("successor_todo_ids"), - ) - if parent_successor_advisory: - payload["parent_successor_advisory"] = parent_successor_advisory - payload = _attach_todo_write_correctness_dry_run_packet( - payload, - goal_id=goal_id, - write_class=write_class, - state_text=original, - ) - return settle_todo_runtime_shadow_capture( - payload, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, capture=shadow_capture, - ) - - -def _complete_goal_todo_legacy( - *, - registry_path: Path, - goal_id: str, - runtime_root_arg: str | None = None, - todo_id: str, - role: str | None = None, - decision_outcome: str | None = None, - evidence: str | None = None, - completion_result_file: Path | None = None, - completion_turn_key: str | None = None, - completion_identity_source: str | None = None, - terminal_review_basis: Mapping[str, Any] | None = None, - completion_delivery_workspace: Mapping[str, Any] | None = None, - completion_validation_workspace_path: Path | None = None, - task_lease_idempotency_key: str | None = None, - task_lease_expected_version: int | None = None, - note: str | None = None, - no_followup: bool = False, - successor_todo_ids: list[str] | None = None, - claimed_by: str | None = None, - clear_claim: bool = False, - next_agent_todo: str | None = None, - next_user_todo: str | None = None, - next_user_task_class: str | None = None, - next_claimed_by: str | None = None, - next_task_class: str | None = None, - next_action_kind: str | None = None, - next_task_repository: str | None = None, - next_required_capabilities: list[str] | None = None, - next_continuation_policy: str | None = None, - next_excluded_agents: list[str] | None = None, - self_merged: bool = False, - agent_id: str | None = None, authority_reason: str | None = None, - project: Path | None = None, - state_file: Path | None = None, - dry_run: bool = False, -) -> dict[str, Any]: - from .line_update import apply_todo_update_to_lines, link_generated_successor_todo_ids - from ..coordination.runtime_shadow_writer_adapter import begin_todo_runtime_shadow_capture, write_captured_todo_state, settle_todo_runtime_shadow_capture - - shadow_runtime_root = effective_runtime_root(registry_path, runtime_root_arg) - if next_task_repository and not next_agent_todo: - raise ValueError("--next-task-repository requires --next-agent-todo") - if next_required_capabilities and not next_agent_todo: - raise ValueError("--next-required-capability requires --next-agent-todo") - normalized_successor_todo_ids = require_completion_successor_todo_ids( - successor_todo_ids - ) - effective_next_user_task_class = resolve_next_user_task_class( - next_user_todo, - next_user_task_class, - ) - completion_policy_facts = { - "claimed_by": claimed_by, "next_claimed_by": next_claimed_by, - "next_agent_todo": next_agent_todo, "next_action_kind": next_action_kind, - "next_continuation_policy": next_continuation_policy, - "next_excluded_agents": next_excluded_agents or [], - "self_merged": self_merged, "evidence": evidence, - } - resolved_project, resolved_state_file = resolve_todo_state_path( - registry_path=registry_path, - goal_id=goal_id, - project=project, - state_file=state_file, - ) - # Run caller-approved validation before locking so slow commands do not - # hold the 5s mutation lock; return typed failure payloads unchanged. - validation_gate = completion_validation_module.run_completion_validation_gate_with_source( - state_file=resolved_state_file, - todo_id=todo_id, - role=role, - registry_path=registry_path, - goal_id=goal_id, - dry_run=dry_run, - no_followup=no_followup, - completion_turn_key=completion_turn_key, - completion_identity_source=completion_identity_source, - requested_has_successor=bool( - normalized_successor_todo_ids or next_agent_todo or next_user_todo - ), - completion_policy_facts=completion_policy_facts, - requested_successor_todo_ids=normalized_successor_todo_ids, - completion_delivery_workspace=completion_delivery_workspace, - completion_validation_workspace_path=completion_validation_workspace_path, - ) - validation_failure: dict[str, Any] | None = validation_gate.get("failure") - if validation_failure is not None: - return validation_failure - with legacy_todo_write_transaction( - registry_path, goal_id, resolved_state_file, agent_id or claimed_by, "todo_complete", dry_run, - runtime_root=shadow_runtime_root, - ), ExitStack() as lease_fence_stack: - original = resolved_state_file.read_text(encoding="utf-8") - shadow_capture = begin_todo_runtime_shadow_capture( - registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, state_path=resolved_state_file, - write_class="todo_complete", original_text=original, - ) - lines = original.splitlines() - updated_at = now_local() - completion_match, completion_todo = ( - completion_validation_module.locked_todo_completion_source( - lines=lines, - state_file=resolved_state_file, - project=resolved_project, - registry_path=registry_path, - goal_id=goal_id, - todo_id=todo_id, - role=role, - ) - ) - effective_decision_outcome = require_completion_decision_outcome( - completion_todo, - decision_outcome, - materialized=bool(completion_match), - ) - if completion_todo is None: - normalized_todo_id = normalize_todo_id(todo_id) or todo_id - raise ValueError( - f"todo_id {normalized_todo_id!r} was not found in active user or agent todos" - ) - locked_completion_policy_source = ( - completion_validation_module.completion_policy_source_from_state( - registry_path=registry_path, - goal_id=goal_id, - lines=lines, - successor_todo_ids=normalized_successor_todo_ids, - facts=completion_policy_facts, - ) - ) - locked_completion = locked_todo_completion_transaction( - validation_gate=validation_gate, - todo=completion_todo, - goal_id=goal_id, - todo_id=todo_id, - dry_run=dry_run, - require_source_match=bool(completion_match), - missing_is_drift=True, - current_completion_policy_source=locked_completion_policy_source, - ) - locked_failure: dict[str, Any] | None = locked_completion["failure"] - if locked_failure is not None: - return locked_failure - completion_transaction = locked_completion["transaction"] - completion_turn_key = completion_transaction.get( - "completion_identity_key" - ) - completion_identity_source = completion_transaction.get( - "completion_identity_source" - ) - decision_target = completion_decision_target(lines, completion_todo) - mutation_authority = authorize_todo_lifecycle_mutation( - registry_path=registry_path, - goal_id=goal_id, - command="complete", - todo=completion_todo, - actor_agent_id=agent_id, authority_reason=authority_reason, - requested_claimed_by=claimed_by, - decision_outcome=effective_decision_outcome, - decision_target=decision_target, - ) - completion_handoff = resolve_todo_completion_handoff(state_text=original, mutation_authority=mutation_authority) - terminal_replay = materialized_todo_completion_replay( - transaction=completion_transaction, - todo=completion_todo, - dry_run=dry_run, - goal_id=goal_id, - todo_id=todo_id, - handoff=completion_handoff, - mutation_authority=mutation_authority, - state_file=str(resolved_state_file), - project=str(resolved_project) if resolved_project else None, - ) if completion_match else None - if terminal_replay is not None: - return terminal_replay - task_lease_fence = lease_fence_stack.enter_context( - hold_task_lease_mutation_fence( - registry_path=registry_path, - goal_id=goal_id, - todo_id=todo_id, - todo=completion_todo, - actor_agent_id=agent_id or claimed_by, - idempotency_key=(task_lease_idempotency_key or completion_turn_key) - if completion_identity_source != "unscoped_completion" - else task_lease_idempotency_key, - expected_version=task_lease_expected_version, - require_active_when_key_supplied=( - task_lease_idempotency_key is not None - or task_lease_expected_version is not None - ), - handoff=completion_handoff, - runtime_root=shadow_runtime_root, - ) - ) - completion_state = completion_transaction.get("completion_state") - completion_policy = completion_policy_from_transaction(completion_transaction) - effective_claimed_by = completion_policy.effective_claimed_by - registered_agents = completion_policy.registered_agents - effective_next_claimed_by = completion_policy.effective_next_claimed_by - effective_next_excluded_agents = ( - completion_policy.effective_next_excluded_agents - ) - effective_self_merged = completion_policy.self_merged - if not isinstance(completion_state, dict): - raise RuntimeError( - "TypeScript Todo completion transaction did not authorize a commit" - ) - update_result = apply_todo_update_to_lines( - lines, - todo_id=todo_id, - status=TODO_STATUS_DONE, - role=role, - decision_outcome=effective_decision_outcome, - note=note, - evidence=evidence, - completion_turn_key=completion_turn_key, - completion_continuation=str(completion_state["continuation"]), - completion_recovery=( - str(completion_state["recovery"]) - if completion_state.get("recovery") is not None - else None - ), - completion_metadata_updates_override=dict( - completion_transaction["metadata_updates"] - ), - claimed_by=effective_claimed_by, - clear_claim=clear_claim, - no_followup=True if no_followup else None, - successor_todo_ids=normalized_successor_todo_ids if successor_todo_ids is not None else None, - updated_at=updated_at, - ) - unblock_resume, decision_scope_resolution = ( - apply_completed_user_todo_lifecycle( - lines, - completion_todo=completion_todo, - update_result=update_result, - fallback_todo_id=todo_id, - decision_outcome=effective_decision_outcome, - updated_at=updated_at, - apply_update=apply_todo_update_to_lines, - ) - ) - successor_intents = build_successor_intents( - next_agent_todo=next_agent_todo, - next_user_todo=next_user_todo, - next_user_task_class=effective_next_user_task_class, - next_claimed_by=next_claimed_by, - next_task_class=next_task_class, - next_action_kind=next_action_kind, - next_task_repository=next_task_repository, - next_required_capabilities=next_required_capabilities, - next_continuation_policy=next_continuation_policy, - next_excluded_agents=next_excluded_agents, - ) - successor_proposals = derive_successor_proposals( - command="complete", - predecessor=completion_todo, - registered_agents=registered_agents, - actor_agent_id=mutation_authority.get("actor_agent_id"), - completion_policy={ - "effective_claimed_by": effective_claimed_by, - "effective_next_claimed_by": effective_next_claimed_by, - "effective_next_excluded_agents": effective_next_excluded_agents, - }, - successor_intents=successor_intents, - ) - next_results = [ - add_todo_to_lines( - lines, - **successor_add_kwargs(proposal), - updated_at=updated_at, - ) - for proposal in successor_proposals - ] - generated_successor_todo_ids = [ - todo_id - for todo_id in normalize_todo_id_list([item.get("todo_id") for item in next_results]) - ] - successor_metadata_updated = link_generated_successor_todo_ids( - lines, - update_result=update_result, - role=role, - successor_todo_ids=generated_successor_todo_ids, - ) - next_action_changed = update_result.get("role") == "agent" and ( - settle_completed_todo_next_action( - lines, - completed_todo_id=str(update_result.get("todo_id") or todo_id), - ) - ) - next_changed = any(item.get("added") or item.get("metadata_updated") for item in next_results) - changed = bool( - update_result["changed"] - or next_changed - or successor_metadata_updated - or next_action_changed - or (unblock_resume or {}).get("changed") - or (decision_scope_resolution or {}).get("changed") - ) - new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "") - if changed: - new_text = replace_updated_at(new_text, updated_at) - if changed and not dry_run: - write_captured_todo_state(shadow_capture, runtime_root=shadow_runtime_root, goal_id=goal_id, - state_path=resolved_state_file, text=new_text) - release_verified_task_lease_fence( - task_lease_fence, - committed=changed and not dry_run, - ) - result = { - "ok": True, - "dry_run": dry_run, - "completed": True, - "goal_id": goal_id, - **update_result, - "changed": changed, - "next_todos": next_results, - "linked_successor_id": completion_policy.linked_successor_id, - "mutation_authority": mutation_authority, - "task_lease_fence": task_lease_fence, - **completion_handoff, - "state_file": str(resolved_state_file), - "project": str(resolved_project) if resolved_project else None, - "updated_at": updated_at if changed else None, - } - if unblock_resume: - result["unblock_resume"] = unblock_resume - if decision_scope_resolution: - result["decision_scope_resolution"] = decision_scope_resolution - if effective_decision_outcome: - result["decision_outcome"] = effective_decision_outcome - result["self_merged"] = effective_self_merged - return settle_todo_runtime_shadow_capture( - result, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, capture=shadow_capture, - ) - - -def _supersede_goal_todo_legacy( - *, - registry_path: Path, - goal_id: str, - runtime_root_arg: str | None = None, - todo_id: str, - role: str | None = None, - reason: str | None = None, - successor_todo_ids: list[str] | None = None, - next_agent_todo: str | None = None, - next_user_todo: str | None = None, - next_user_task_class: str | None = None, - next_claimed_by: str | None = None, - next_task_class: str | None = None, - next_action_kind: str | None = None, - next_task_repository: str | None = None, - next_required_capabilities: list[str] | None = None, - next_continuation_policy: str | None = None, - next_excluded_agents: list[str] | None = None, - agent_id: str | None = None, authority_reason: str | None = None, - task_lease_idempotency_key: str | None = None, task_lease_expected_version: int | None = None, - project: Path | None = None, - state_file: Path | None = None, - dry_run: bool = False, -) -> dict[str, Any]: - from .line_update import apply_todo_update_to_lines, link_superseding_todo_id - from ..coordination.runtime_shadow_writer_adapter import begin_todo_runtime_shadow_capture, write_captured_todo_state, settle_todo_runtime_shadow_capture - - if successor_todo_ids: - raise ValueError("Existing-successor supersede requires promoted canonical Todo authority; migrate the Goal before retrying") - shadow_runtime_root = effective_runtime_root(registry_path, runtime_root_arg) - if next_task_repository and not next_agent_todo: - raise ValueError("--next-task-repository requires --next-agent-todo") - if next_required_capabilities and not next_agent_todo: - raise ValueError("--next-required-capability requires --next-agent-todo") - effective_next_user_task_class = resolve_next_user_task_class( - next_user_todo, - next_user_task_class, - ) - resolved_project, resolved_state_file = resolve_todo_state_path( - registry_path=registry_path, - goal_id=goal_id, - project=project, - state_file=state_file, - ) - with legacy_todo_write_transaction( - registry_path, goal_id, resolved_state_file, agent_id, "todo_supersede", dry_run, - runtime_root=shadow_runtime_root, - ), ExitStack() as lease_fence_stack: - original = resolved_state_file.read_text(encoding="utf-8") - shadow_capture = begin_todo_runtime_shadow_capture( - registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, state_path=resolved_state_file, - write_class="todo_supersede", original_text=original, - ) - lines = original.splitlines() - updated_at = now_local() - current_match = find_todo_block(lines, todo_id=todo_id, role=role) - if not current_match: - normalized_todo_id = normalize_todo_id(todo_id) or todo_id - raise ValueError( - f"todo_id {normalized_todo_id!r} was not found in active user or agent todos" - ) - current_role, _section, _start, _end, current_block = current_match - authority_todo = dict(current_block) - authority_todo["role"] = current_role - mutation_authority = authorize_todo_lifecycle_mutation( - registry_path=registry_path, - goal_id=goal_id, - command="supersede", - todo=authority_todo, - actor_agent_id=agent_id, authority_reason=authority_reason, - ) - completion_handoff, task_lease_fence = enter_terminal_todo_lease_fence( - lease_fence_stack, registry_path=registry_path, goal_id=goal_id, todo_id=todo_id, - todo=authority_todo, actor_agent_id=agent_id, state_text=original, mutation_authority=mutation_authority, - idempotency_key=task_lease_idempotency_key, expected_version=task_lease_expected_version, - runtime_root=shadow_runtime_root, - ) - update_result = apply_todo_update_to_lines( - lines, - todo_id=todo_id, - status=TODO_STATUS_DONE, - role=role, - reason=reason, - note="superseded", - updated_at=updated_at, - ) - registered_agents = registered_agent_ids_from_registry(registry_path, goal_id) - successor_intents = build_successor_intents( - next_agent_todo=next_agent_todo, - next_user_todo=next_user_todo, - next_user_task_class=effective_next_user_task_class, - next_claimed_by=next_claimed_by, - next_task_class=next_task_class, - next_action_kind=next_action_kind, - next_task_repository=next_task_repository, - next_required_capabilities=next_required_capabilities, - next_continuation_policy=next_continuation_policy, - next_excluded_agents=next_excluded_agents, - ) - successor_proposals = derive_successor_proposals( - command="supersede", - predecessor=authority_todo, - registered_agents=registered_agents, - actor_agent_id=mutation_authority.get("actor_agent_id"), - completion_policy=None, - successor_intents=successor_intents, - ) - next_results = [ - add_todo_to_lines( - lines, - **successor_add_kwargs(proposal), - updated_at=updated_at, - ) - for proposal in successor_proposals - ] - generated_successor_todo_ids = [ - todo_id - for todo_id in normalize_todo_id_list([item.get("todo_id") for item in next_results]) - ] - link_superseding_todo_id( - lines, - update_result=update_result, - role=role, - successor_todo_ids=generated_successor_todo_ids, - ) - next_action_changed = current_role == "agent" and ( - settle_completed_todo_next_action( - lines, - completed_todo_id=str(update_result.get("todo_id") or todo_id), - ) - ) - next_changed = any(item.get("added") or item.get("metadata_updated") for item in next_results) - changed = bool( - update_result["changed"] - or next_changed - or next_action_changed - ) - new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "") - if changed: - new_text = replace_updated_at(new_text, updated_at) - if changed and not dry_run: - write_captured_todo_state(shadow_capture, runtime_root=shadow_runtime_root, goal_id=goal_id, - state_path=resolved_state_file, text=new_text) - release_verified_task_lease_fence(task_lease_fence, committed=changed and not dry_run) - result = { - "ok": True, - "dry_run": dry_run, - "superseded": True, - "goal_id": goal_id, - **update_result, - "changed": changed, - "mutation_authority": mutation_authority, - "task_lease_fence": task_lease_fence, **completion_handoff, - "next_todos": next_results, - "state_file": str(resolved_state_file), - "project": str(resolved_project) if resolved_project else None, - "updated_at": updated_at if changed else None, - } - return settle_todo_runtime_shadow_capture( - result, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, capture=shadow_capture, - ) - - -def _archive_completed_todos_legacy( - *, - registry_path: Path, - goal_id: str, - runtime_root_arg: str | None = None, - role: str = "agent", - max_active_done: int = ARCHIVE_COMPLETED_DEFAULT_MAX_ACTIVE_DONE, - project: Path | None = None, - state_file: Path | None = None, - dry_run: bool = True, -) -> dict[str, Any]: - from ..coordination.runtime_shadow_writer_adapter import begin_todo_runtime_shadow_capture, write_captured_todo_state, settle_todo_runtime_shadow_capture - - shadow_runtime_root = effective_runtime_root(registry_path, runtime_root_arg) - if role not in TODO_SECTION_HEADINGS: - raise ValueError("todo role must be one of: user, agent") - if max_active_done < 0: - raise ValueError("max_active_done must be non-negative") - resolved_project, resolved_state_file = resolve_todo_state_path( - registry_path=registry_path, - goal_id=goal_id, - project=project, - state_file=state_file, - ) - - with legacy_todo_write_transaction( - registry_path, goal_id, resolved_state_file, None, "todo_archive_completed", dry_run, - runtime_root=shadow_runtime_root, - ): - original = resolved_state_file.read_text(encoding="utf-8") - shadow_capture = begin_todo_runtime_shadow_capture( - registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, state_path=resolved_state_file, - write_class="todo_archive_completed", original_text=original, - ) - lines = original.splitlines() - archive_result = archive_completed_todo_lines( - lines, - role=role, - max_active_done=max_active_done, - ) - lines = archive_result.pop("lines") - - updated_at = now_local() - changed = bool(archive_result["changed"]) - new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "") - if changed: - new_text = replace_updated_at(new_text, updated_at) - if changed and not dry_run: - write_captured_todo_state(shadow_capture, runtime_root=shadow_runtime_root, goal_id=goal_id, - state_path=resolved_state_file, text=new_text) - - result = { - "ok": True, - "dry_run": dry_run, - "goal_id": goal_id, - **archive_result, - "state_file": str(resolved_state_file), - "project": str(resolved_project) if resolved_project else None, - "updated_at": updated_at if changed else None, - } - return settle_todo_runtime_shadow_capture( - result, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, capture=shadow_capture, - ) diff --git a/loopx/control_plane/todos/line_update.py b/loopx/control_plane/todos/line_update.py deleted file mode 100644 index fe4b9389dd..0000000000 --- a/loopx/control_plane/todos/line_update.py +++ /dev/null @@ -1,364 +0,0 @@ -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any - -from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result -from .mutation_response import serialize_todo_update_result -from .authoring_scope import todo_authoring_facts -from .external_wait_contract import TodoExternalWaitAuthoringError, build_monitor_advancement_authoring_contract -from .update_source import todo_update_snapshot -from .todo_semantics import todo_priority_label - -from .active_state_editing import ( - TODO_SECTION_HEADINGS, - find_todo_block, - set_todo_marker, - set_todo_text, - todo_metadata_would_change, -) -from .contract import ( - merge_todo_id_lists, - metadata_line_for_todo_block, - normalize_todo_id, - normalize_todo_id_list, - parse_todo_metadata_line, -) -from .completion_state import ( - TodoCompletionContinuation, -) -from .contract import TODO_MONITOR_METADATA_FIELDS - - -def upsert_todo_metadata( - lines: list[str], block: dict[str, Any], metadata_line: str | None -) -> bool: - if not metadata_line: - return False - start = int(block["start"]) - end = int(block["end"]) - for index in range(start + 1, end): - if parse_todo_metadata_line(lines[index]) is not None: - if lines[index] == metadata_line: - return False - lines[index] = metadata_line - return True - insert_at = end - while insert_at > start + 1 and not lines[insert_at - 1].strip(): - insert_at -= 1 - lines.insert(insert_at, metadata_line) - return True - - -def link_generated_successor_todo_ids( - lines: list[str], - *, - update_result: dict[str, Any], - role: str | None, - successor_todo_ids: list[str], -) -> bool: - merged_successor_ids = merge_todo_id_lists( - update_result.get("successor_todo_ids"), - successor_todo_ids, - ) - if merged_successor_ids == normalize_todo_id_list( - update_result.get("successor_todo_ids") - ): - return False - block_match = find_todo_block( - lines, - todo_id=str(update_result.get("todo_id") or ""), - role=role, - ) - if not block_match: - return False - _resolved_role, _section, _start, _end, block = block_match - metadata_updated = upsert_todo_metadata( - lines, - block, - metadata_line_for_todo_block( - block, - { - "successor_todo_ids": merged_successor_ids, - "completion_continuation": (TodoCompletionContinuation.SUCCESSOR.value), - }, - ), - ) - update_result["successor_todo_ids"] = merged_successor_ids - update_result["metadata_updated"] = bool( - update_result.get("metadata_updated") or metadata_updated - ) - update_result["changed"] = bool(update_result.get("changed") or metadata_updated) - return metadata_updated - - -def link_superseding_todo_id( - lines: list[str], - *, - update_result: dict[str, Any], - role: str | None, - successor_todo_ids: list[str], -) -> bool: - """Persist supersede lineage after generated successors are materialized.""" - - if not successor_todo_ids: - return False - block_match = find_todo_block( - lines, - todo_id=str(update_result.get("todo_id") or ""), - role=role, - ) - if not block_match: - return False - _resolved_role, _section, _start, _end, block = block_match - merged_successor_ids = merge_todo_id_lists( - update_result.get("successor_todo_ids"), - successor_todo_ids, - ) - metadata_updated = upsert_todo_metadata( - lines, - block, - metadata_line_for_todo_block( - block, - { - "superseded_by": successor_todo_ids[0], - "successor_todo_ids": merged_successor_ids, - }, - ), - ) - update_result["metadata_updated"] = bool( - update_result.get("metadata_updated") or metadata_updated - ) - update_result["superseded_by"] = successor_todo_ids[0] - update_result["successor_todo_ids"] = merged_successor_ids - update_result["changed"] = True - return metadata_updated - - -def _field_update_plan( - block: Mapping[str, Any], intent: dict[str, Any], updated_at: str, - monitor_context: dict[str, Any] | None = None, - public_context: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Adapt source facts only; the TS planner owns omission/clear/state rules.""" - try: - result = effect_runtime_result( - "todo.public_update.plan" if public_context is not None else "todo.field_update.plan", - { - "schema_version": "todo_public_update_request_v0" if public_context is not None else "loopx_todo_field_update_request_v0", - "todo": { - **todo_authoring_facts(dict(block)), - "role": block.get("role"), - **{key: block.get(key) - for key in ( - "text", - "todo_id", - "status", - "claimed_by", - "completed_at", - "removed_continuation_policy", - "no_followup", - "completion_continuation", - "successor_todo_ids", - "explore_result_node_refs", - "resume_monitor_generation", - "task_class", - *TODO_MONITOR_METADATA_FIELDS, - )}, - }, - "intent": intent, - "updated_at": updated_at, - "monitor_context": monitor_context, - "context": public_context, - }, - ) - except EffectRuntimeRejected as exc: - if public_context is not None and exc.diagnostic_code.startswith("external_wait_"): - condition = str(intent.get("resume_when") or block.get("resume_when") or "").strip().lower() - kind, _, target = condition.partition(":") - raise TodoExternalWaitAuthoringError(str(exc), code=exc.diagnostic_code, - monitor_todo_id=target if kind == "monitor_changed" else None, - successor_todo_ids=intent.get("successor_todo_ids") - if intent.get("successor_todo_ids") is not None else block.get("successor_todo_ids")) from None - raise ValueError(str(exc)) from None - if ( - not isinstance(result, dict) - or result.get("schema_version") != "loopx_todo_field_update_result_v0" - or result.get("target_status") not in {"open", "done", "blocked", "deferred"} - or result.get("normalized_status") - not in {None, "open", "done", "blocked", "deferred"} - or not isinstance(result.get("metadata_updates"), dict) - ): - raise RuntimeError("TypeScript Todo field update result shape mismatch") - transition = result.get("external_wait_transition") - if isinstance(transition, dict) and transition.get("resume_kind") == "monitor_changed": - transition["authoring_contract"] = build_monitor_advancement_authoring_contract( - monitor_todo_id=transition["dependency_todo_id"], - successor_todo_ids=transition["successor_todo_ids"]) - return result - - -def apply_todo_update_to_lines( - lines: list[str], - *, - todo_id: str, - text: str | None = None, - priority: str | None = None, - clear_priority: bool = False, - status: str | None = None, - role: str | None = None, - note: str | None = None, - evidence: str | None = None, - completion_turn_key: str | None = None, - reason: str | None = None, - task_class: str | None = None, - action_kind: str | None = None, - task_domain: str | None = None, - task_repository: str | None = None, - continuation_policy: str | None = None, - required_write_scopes: list[str] | None = None, - required_capabilities: list[str] | None = None, - target_capabilities: list[str] | None = None, - explore_result_node_refs: list[str] | None = None, - append_explore_result_node_refs: list[str] | None = None, - decision_scope: Any = None, - required_decision_scopes: Any = None, - decision_outcome: str | None = None, - decision_scope_outcomes: Any = None, - claimed_by: str | None = None, - bound_agent: str | None = None, - goal_bound: bool | None = None, - clear_user_binding: bool = False, - blocks_agent: str | None = None, - clear_blocks_agent: bool = False, - excluded_agents: list[str] | None = None, - global_gate: bool | None = None, - clear_global_gate: bool = False, - unblocks_todo_id: str | None = None, - successor_todo_ids: list[str] | None = None, - completion_continuation: str | None = None, - completion_recovery: str | None = None, - completion_metadata_updates_override: Mapping[str, Any] | None = None, - resume_when: str | None = None, - resume_monitor_generation: int | None = None, - clear_resume_when: bool = False, - no_followup: bool | None = None, - monitor_metadata: dict[str, Any] | None = None, - monitor_context: dict[str, Any] | None = None, - public_context: dict[str, Any] | None = None, - clear_claim: bool = False, - claim_only: bool = False, - updated_at: str, -) -> dict[str, Any]: - # Blank Python compatibility values mean omission. The whole typed field - # owner already normalizes and validates a supplied condition, including - # contradictory clear intent; do not preflight it through a second RPC. - if public_context is None and resume_when is not None and not str(resume_when).strip(): - resume_when = None - normalized_todo_id = normalize_todo_id(todo_id) - if not normalized_todo_id: - raise ValueError( - "todo_id must use the public token shape " - "todo_" - ) - if role is not None and role not in TODO_SECTION_HEADINGS: - raise ValueError("todo role must be one of: user, agent") - block_match = find_todo_block(lines, todo_id=normalized_todo_id, role=role) - if not block_match: - raise ValueError( - f"todo_id {normalized_todo_id!r} was not found in active user or agent todos" - ) - resolved_role, section, _start, _end, block = block_match - if public_context is not None: - public_context = {**public_context, "items": todo_update_snapshot(lines) - if resume_when or block.get("resume_when") else []} - raw_intent = { - "text": text, - "priority": priority, - "clear_priority": clear_priority, - "status": status, - "note": note, - "evidence": evidence, - "completion_turn_key": completion_turn_key, - "reason": reason, - "task_class": task_class, - "action_kind": action_kind, - "task_domain": task_domain, - "task_repository": task_repository, - "continuation_policy": continuation_policy, - "required_write_scopes": required_write_scopes, - "required_capabilities": required_capabilities, - "target_capabilities": target_capabilities, - "explore_result_node_refs": explore_result_node_refs, - "append_explore_result_node_refs": append_explore_result_node_refs, - "decision_scope": decision_scope, - "required_decision_scopes": required_decision_scopes, - "decision_outcome": decision_outcome, - "decision_scope_outcomes": decision_scope_outcomes, - "claimed_by": claimed_by, - "bound_agent": bound_agent, - "goal_bound": goal_bound, - "clear_user_binding": clear_user_binding, - "blocks_agent": blocks_agent, - "clear_blocks_agent": clear_blocks_agent, - "excluded_agents": excluded_agents, - "global_gate": global_gate, - "clear_global_gate": clear_global_gate, - "unblocks_todo_id": unblocks_todo_id, - "successor_todo_ids": successor_todo_ids, - "completion_continuation": completion_continuation, - "completion_recovery": completion_recovery, - "completion_metadata_updates_override": completion_metadata_updates_override, - "resume_when": resume_when, - "resume_monitor_generation": resume_monitor_generation, - "clear_resume_when": clear_resume_when, - "no_followup": no_followup, - "monitor_metadata": monitor_metadata, - "clear_claim": clear_claim, - "claim_only": claim_only, - } - # Python's compatibility API uses None (and blank note text) for - # omission. Strip those sentinels before crossing the typed planner; an - # actual empty scalar such as reason="" remains an explicit clear. - intent = { - key: value for key, value in raw_intent.items() - if value is not None and not ( - key == "note" and isinstance(value, str) and not value.strip() - ) - } - plan = _field_update_plan( - {**block, "role": resolved_role}, - intent, - updated_at, - monitor_context, - public_context, - ) - normalized_status = plan["normalized_status"] - target_status = plan["target_status"] - updates = plan["metadata_updates"] - text = updates.pop("text", text) - updated_priority = updates.pop("priority", todo_priority_label(block)) - updates.pop("title", None) - status_changed = ( - set_todo_marker(lines, block, normalized_status) if normalized_status else False - ) - text_changed = ( - set_todo_text(lines, block, text, status=target_status) - if text is not None - else False - ) - metadata_line = metadata_line_for_todo_block(block, updates) - semantic_metadata_changed = todo_metadata_would_change(lines, block, metadata_line) - if status_changed or text_changed or semantic_metadata_changed: - updates["updated_at"] = updated_at - metadata_line = metadata_line_for_todo_block(block, updates) - metadata_updated = upsert_todo_metadata(lines, block, metadata_line) - effective_metadata = parse_todo_metadata_line(metadata_line or "") or {} - return serialize_todo_update_result( - role=resolved_role, section=section, todo=block.get("text"), - todo_id=normalized_todo_id, status=target_status, priority=updated_priority, - status_changed=status_changed, text_changed=text_changed, - metadata_updated=metadata_updated, metadata=effective_metadata, - monitor_poll_transition=plan.get("monitor_poll_transition"), - external_wait_transition=plan.get("external_wait_transition"), - ) diff --git a/loopx/control_plane/todos/mutation_api.py b/loopx/control_plane/todos/mutation_api.py index 1f104f90f6..71cefe7207 100644 --- a/loopx/control_plane/todos/mutation_api.py +++ b/loopx/control_plane/todos/mutation_api.py @@ -1,7 +1,7 @@ """Todo input transport shared by App and the compatibility facade. -Canonical transactions remain owned by the existing typed provider adapters. -Only an unpromoted operation loads its original Markdown writer. +Canonical transactions remain owned by the typed provider adapters. +Unmigrated Goals fail with the common migration-required result. """ from __future__ import annotations @@ -24,14 +24,11 @@ from . import monitor_metadata as todo_monitor_metadata from .text import plan_todo_priority from .authoring_scope import plan_todo_creation_scope -from ..coordination.local_authority import ( - claim_canonical_todo_if_promoted, - local_authority_is_promoted, -) +from ..coordination.local_authority import claim_canonical_todo_if_promoted from .provider_update import update_canonical_todo_if_promoted from .update_intent import build_canonical_update_intent from .provider_create import create_canonical_todo_if_promoted -from .provider_terminal_lifecycle import provider_first_terminal_lifecycle +from .provider_terminal_lifecycle import route_terminal_call from ...paths import effective_runtime_root from .completed_archive import ( DEFAULT_COMPLETED_TODO_ARCHIVE_HEADROOM, @@ -87,8 +84,6 @@ def add_goal_todo( operation_id: str | None = None, expected_provider_revision: str | None = None, ) -> dict[str, Any]: - call = dict(locals()) - call.pop("expected_provider_revision") shadow_runtime_root = effective_runtime_root(registry_path, runtime_root_arg) if role not in TODO_SECTION_HEADINGS: raise ValueError("todo role must be one of: user, agent") @@ -159,7 +154,7 @@ def add_goal_todo( normalized_unblocks_todo_id = authoring_scope["unblocks_todo_id"] normalized_resume_when = authoring_scope["normalized_resume_when"] normalized_monitor_metadata = authoring_scope["monitor_metadata"] - canonical_create = create_canonical_todo_if_promoted( + return create_canonical_todo_if_promoted( operation_id=operation_id, expected_provider_revision=expected_provider_revision, registry_path=registry_path, @@ -205,31 +200,6 @@ def add_goal_todo( state_file=state_file, dry_run=dry_run, ) - if canonical_create is not None: - return canonical_create - if expected_provider_revision is not None: - raise ValueError("reviewed provider revision requires canonical authority") - if operation_id is not None: - raise ValueError("todo add --operation-id requires promoted canonical authority") - from .legacy_mutation import _add_goal_todo_legacy - - return _add_goal_todo_legacy(**call, _prepared={ - "effective_agent_id": effective_agent_id, - "effective_blocks_agent": effective_blocks_agent, - "effective_bound_agent": effective_bound_agent, - "effective_claimed_by": effective_claimed_by, - "effective_excluded_agents": effective_excluded_agents, - "effective_goal_bound": effective_goal_bound, - "normalized_monitor_metadata": normalized_monitor_metadata, - "normalized_resume_when": normalized_resume_when, - "normalized_status": normalized_status, - "normalized_unblocks_todo_id": normalized_unblocks_todo_id, - "replan_obligation_id": replan_obligation_id, - "shadow_runtime_root": shadow_runtime_root, - "todo_text": todo_text, - "updated_at": updated_at, - "validation_argv": validation_argv, - }) def _update_completion_validation_declaration( @@ -348,7 +318,6 @@ def update_goal_todo( state_file: Path | None = None, dry_run: bool = False, ) -> dict[str, Any]: - call = dict(locals()) if priority is not None and clear_priority: raise ValueError("provide either priority or clear_priority, not both") shadow_runtime_root = effective_runtime_root(registry_path, runtime_root_arg) @@ -374,23 +343,14 @@ def update_goal_todo( raise ValueError( "todo update accepts either resume_when or clear_resume_when, not both" ) - promoted_claim = claim_only and local_authority_is_promoted( - runtime_root=shadow_runtime_root, goal_id=goal_id - ) if claim_operation_id is not None: if not claim_only: raise ValueError("claim_operation_id is supported only by todo claim") - if not promoted_claim: - raise ValueError("--claim-operation-id requires promoted canonical authority; no legacy write attempted") if task_lease_expected_version is not None and task_lease_idempotency_key is None: raise ValueError( "--task-lease-expected-version requires --task-lease-idempotency-key" ) - if task_lease_idempotency_key is not None and claim_only and not promoted_claim: - raise ValueError( - "--task-lease-idempotency-key on todo claim requires promoted canonical authority; no legacy write attempted" - ) - if promoted_claim: + if claim_only: unsupported_claim_values = ( text, priority, clear_priority or None, status, note, validation_command, validation_command_json, validation_label, @@ -415,7 +375,7 @@ def update_goal_todo( "todo claim only accepts todo_id, claimed_by, agent_id, optional role, " "project, state_file, and dry_run" ) - canonical_claim = claim_canonical_todo_if_promoted( + return claim_canonical_todo_if_promoted( registry_path=registry_path, runtime_root=shadow_runtime_root, goal_id=goal_id, @@ -430,8 +390,6 @@ def update_goal_todo( project=project, state_file=state_file, ) - if canonical_claim is not None: - return canonical_claim # Translate the compatibility-sized CLI signature exactly once. The # canonical transaction now owns ordinary role/binding/work-declaration # edits as well as text/note corrections. User completion is dispatched to @@ -462,11 +420,10 @@ def update_goal_todo( if clear_priority: planning_intent["clear_priority"] = True monitor_intent = todo_monitor_metadata.monitor_metadata_intent(monitor_metadata) - # Promotion selects the authority, not Python's estimate of edit validity. - # The typed decoder must also reject empty/invalid edits without importing - # an unpromoted source writer. Unpromoted Goals retain their legacy route. + # The provider selects the authority and owns edit validity. The typed + # decoder also rejects empty or invalid edits without consulting Markdown. if not claim_only: - canonical_edit = update_canonical_todo_if_promoted( + return update_canonical_todo_if_promoted( registry_path=registry_path, runtime_root=shadow_runtime_root, goal_id=goal_id, todo_id=normalize_todo_id(todo_id) or todo_id, actor_agent_id=agent_id, role=role, text=text, note=note, dry_run=dry_run, @@ -483,22 +440,8 @@ def update_goal_todo( {"monitor_metadata": monitor_intent["metadata"]} if monitor_intent["metadata"] else {} )}, ) - if canonical_edit is not None: - return canonical_edit - if (update_operation_id is not None or update_expected_provider_revision is not None - or update_expected_registry_sha256 is not None) or (not claim_only and ( - task_lease_idempotency_key is not None or task_lease_expected_version is not None - ) and not (monitor_intent["observation"] is not None and status is None)): - raise ValueError("update operation id and lease proof require a supported promoted update; no legacy write attempted") - from .legacy_mutation import _update_goal_todo_legacy - return _update_goal_todo_legacy(**call, _prepared={ - "monitor_intent": monitor_intent, - "shadow_runtime_root": shadow_runtime_root, - }) - -@provider_first_terminal_lifecycle("complete") def complete_goal_todo( *, registry_path: Path, @@ -537,13 +480,9 @@ def complete_goal_todo( state_file: Path | None = None, dry_run: bool = False, ) -> dict[str, Any]: - call = dict(locals()) - from .legacy_mutation import _complete_goal_todo_legacy - - return _complete_goal_todo_legacy(**call) + return route_terminal_call("complete", locals()) -@provider_first_terminal_lifecycle("supersede") def supersede_goal_todo( *, registry_path: Path, @@ -571,13 +510,9 @@ def supersede_goal_todo( state_file: Path | None = None, dry_run: bool = False, ) -> dict[str, Any]: - call = dict(locals()) - from .legacy_mutation import _supersede_goal_todo_legacy + return route_terminal_call("supersede", locals()) - return _supersede_goal_todo_legacy(**call) - -@provider_first_terminal_lifecycle("archive") def archive_completed_todos( *, registry_path: Path, @@ -589,7 +524,4 @@ def archive_completed_todos( state_file: Path | None = None, dry_run: bool = True, ) -> dict[str, Any]: - call = dict(locals()) - from .legacy_mutation import _archive_completed_todos_legacy - - return _archive_completed_todos_legacy(**call) + return route_terminal_call("archive", locals()) diff --git a/loopx/control_plane/todos/mutation_authority.py b/loopx/control_plane/todos/mutation_authority.py index 1c78eb8fa8..908d218d67 100644 --- a/loopx/control_plane/todos/mutation_authority.py +++ b/loopx/control_plane/todos/mutation_authority.py @@ -7,33 +7,17 @@ load_goal_from_registry, registered_agent_ids_for_goal, ) -from ..coordination.authority_core import ( - CoordinationSnapshot, - DecisionOutcome, - LifecycleGrant, - TodoAction, - TodoMutationCommand, - TodoSnapshot, - decide, -) -from ..coordination.local_snapshot import todo_snapshot_from_mapping -from ..effect_runtime import effect_runtime_result -from .contract import ( - normalize_removed_todo_continuation_policy, - normalize_todo_claimed_by, - normalize_todo_decision_outcome, - normalize_todo_decision_scope, -) +from .contract import normalize_todo_claimed_by -TODO_MUTATION_AUTHORITY_SCHEMA_VERSION = "todo_mutation_authority_v0" TODO_LIFECYCLE_AUTHORITY_ACTIONS = frozenset( {"complete", "reassign", "supersede", "update"} ) def todo_lifecycle_facts( - registry_path: Path, goal_id: str, + registry_path: Path, + goal_id: str, ) -> tuple[list[str], list[dict[str, Any]]]: """Project the existing registry owner for all canonical Todo mutations.""" goal = load_goal_from_registry(registry_path, goal_id) @@ -41,7 +25,8 @@ def todo_lifecycle_facts( coordination = goal.get("coordination") if isinstance(goal, Mapping) else None grants = normalize_todo_lifecycle_authority( coordination.get("todo_lifecycle_authority") - if isinstance(coordination, Mapping) else None, + if isinstance(coordination, Mapping) + else None, registered_agents=registered, ) return registered, grants @@ -63,16 +48,22 @@ def normalize_todo_lifecycle_authority( raise ValueError("each todo lifecycle authority grant must be an object") agent_id = normalize_todo_claimed_by(raw.get("agent_id")) if not agent_id: - raise ValueError("todo lifecycle authority agent_id must be a public-safe token") + raise ValueError( + "todo lifecycle authority agent_id must be a public-safe token" + ) if agent_id not in registered_agents: raise ValueError( f"todo lifecycle authority agent_id={agent_id!r} must already be registered" ) if agent_id in seen_agents: - raise ValueError(f"duplicate todo lifecycle authority grant for {agent_id!r}") + raise ValueError( + f"duplicate todo lifecycle authority grant for {agent_id!r}" + ) raw_actions = raw.get("actions") if not isinstance(raw_actions, list) or not raw_actions: - raise ValueError("todo lifecycle authority actions must be a non-empty list") + raise ValueError( + "todo lifecycle authority actions must be a non-empty list" + ) actions: list[str] = [] for value in raw_actions: action = str(value or "").strip().lower() @@ -96,317 +87,3 @@ def normalize_todo_lifecycle_authority( ) seen_agents.add(agent_id) return normalized - - -def todo_update_authority_action( - *, - existing_role: str, - role: str | None, - claimed_by: str | None, - clear_claim: bool, - other_values: tuple[Any, ...], - monitor_metadata: Mapping[str, Any] | None, -) -> str | None: - if claimed_by is None and not clear_claim: - return None - has_other_change = bool( - (role is not None and role != existing_role) - or any(value is not None and value is not False for value in other_values) - or any(value is not None for value in (monitor_metadata or {}).values()) - ) - return "update" if has_other_change else "reassign" - - -def _raise_core_authority_rejection( - *, - code: str, - goal_id: str, - registered_agents: list[str], - command: str, - actor: str | None, - requested_owner: str | None, - todo: TodoSnapshot, - action: str, -) -> None: - if code == "actor_not_registered" and actor: - if not registered_agents: - raise ValueError( - f"agent_id={actor!r} cannot be used because goal {goal_id!r} " - "has no coordination.registered_agents list. Register this peer " - "identity first: loopx configure-goal --goal-id " - f"{goal_id} --registered-agent {actor} --execute" - ) - raise ValueError( - f"agent_id={actor!r} is not registered for goal {goal_id!r}; " - f"registered_agents={', '.join(registered_agents)}" - ) - if code == "actor_required": - raise ValueError( - f"multi-agent todo {command} requires --agent-id to attribute the " - "lifecycle actor; only completion of an exactly linked user_gate " - "decision_scope may use the typed owner/controller override" - ) - if code == "actor_excluded": - raise ValueError( - f"agent_id={actor!r} is excluded from mutating todo_id={todo.todo_id!r}" - ) - if code == "bound_agent_mismatch": - bound_agent = todo.bound_agent or todo.blocks_agent - raise ValueError( - f"agent_id={actor!r} cannot {command} user todo_id={todo.todo_id!r}; " - f"its response continuation is bound to bound_agent={bound_agent!r}" - ) - if code == "claim_owner_mismatch": - raise ValueError( - f"agent_id={actor!r} cannot {command} todo_id={todo.todo_id!r}; " - f"it is claimed_by={todo.claimed_by!r}" - ) - if code == "delegation_action_not_granted": - raise ValueError( - "coordination.todo_lifecycle_authority for " - f"agent_id={actor!r} does not grant action={action!r}" - ) - if code == "delegation_reason_required": - raise ValueError(f"delegated {action} override requires --authority-reason") - if code == "claim_actor_mismatch": - raise ValueError( - "todo claim requires --claimed-by to match the lifecycle " - "--agent-id; use todo update for an owner-attributed transfer" - ) - raise ValueError( - f"todo lifecycle authority rejected command={command!r} code={code!r} " - f"actor={actor!r} requested_owner={requested_owner!r}" - ) - - -def _raise_claim_ineligible_rejection( - *, - code: str, - todo: Mapping[str, Any], - core_todo: TodoSnapshot, -) -> None: - """Preserve legacy claim messages for ineligible-Todo TypeScript codes.""" - - if code == "todo_not_open": - raise ValueError( - f"todo claim requires status=open; todo_id {core_todo.todo_id!r} " - f"is status={core_todo.status!r}" - ) - if code == "todo_not_agent": - raise ValueError( - "claimed_by is execution ownership for agent todos, not a user-todo " - "binding; use --bound-agent or --goal-bound" - ) - if code == "removed_continuation_policy": - raw_policy = str(todo.get("removed_continuation_policy") or "").strip() - policy = normalize_removed_todo_continuation_policy(raw_policy) or raw_policy - raise ValueError( - f"todo_id {core_todo.todo_id!r} uses removed continuation_policy=" - f"{policy}; repair it before claiming" - ) - if code in {"todo_archived", "todo_role_mismatch"}: - raise ValueError( - f"todo_id {core_todo.todo_id!r} was not found in active user or agent todos" - ) - - -def _authorize_typescript_claim( - *, - goal_id: str, - todo: Mapping[str, Any], - core_todo: TodoSnapshot, - registered_agents: list[str], - actor: str | None, - requested_owner: str | None, -) -> dict[str, Any]: - payload = effect_runtime_result( - "todo.claim.decide", - { - "goal_id": goal_id, - "todo_id": core_todo.todo_id, - "todo": { - "todo_id": core_todo.todo_id, - "role": core_todo.role, - "status": core_todo.status, - "archive_state": str(todo.get("archive_state") or "active"), - "claimed_by": core_todo.claimed_by, - "excluded_agents": sorted(core_todo.excluded_agents), - "removed_continuation_policy": todo.get( - "removed_continuation_policy" - ), - }, - "claimed_by": requested_owner, - "actor_agent_id": actor, - "expected_role": core_todo.role, - "registered_agents": registered_agents, - }, - ) - if not isinstance(payload, Mapping): - raise RuntimeError("TypeScript Todo claim decision shape mismatch") - if payload.get("status") != "accepted": - code = str(payload.get("reason_code") or "claim_rejected") - if code in { - "actor_not_registered", - "actor_required", - "actor_excluded", - "claim_owner_mismatch", - "claim_actor_mismatch", - }: - _raise_core_authority_rejection( - code=code, - goal_id=goal_id, - registered_agents=registered_agents, - command="claim", - actor=actor, - requested_owner=requested_owner, - todo=core_todo, - action="claim", - ) - _raise_claim_ineligible_rejection(code=code, todo=todo, core_todo=core_todo) - raise ValueError(str(payload.get("reason") or "Todo claim was rejected")) - mutation_authority = payload.get("mutation_authority") - if not isinstance(mutation_authority, Mapping): - raise RuntimeError("TypeScript Todo claim authority shape mismatch") - return dict(mutation_authority) - - -def authorize_todo_lifecycle_mutation( - *, - registry_path: Path, - goal_id: str, - command: str, - todo: Mapping[str, Any], - actor_agent_id: str | None, - authority_action: str | None = None, - authority_reason: str | None = None, - requested_claimed_by: str | None = None, - decision_outcome: str | None = None, - decision_target: Mapping[str, Any] | None = None, -) -> dict[str, Any]: - """Authorize one existing-todo lifecycle mutation before state changes.""" - - goal = load_goal_from_registry(registry_path, goal_id) - registered_agents = registered_agent_ids_for_goal(goal) - normalized_actor = normalize_todo_claimed_by(actor_agent_id) - core_todo = todo_snapshot_from_mapping(todo, infer_status_from_done=True) - assert core_todo is not None - core_target = todo_snapshot_from_mapping( - decision_target, - infer_status_from_done=True, - ) - requested_owner = normalize_todo_claimed_by(requested_claimed_by) - # Legacy single-agent callers historically omitted ``agent_id`` while the - # Todo itself carried the sole registered owner/binding. Attribute those - # calls to that unambiguous owner before entering the shared decision - # function; genuinely unowned actorless records remain compatibility-safe. - if normalized_actor is None and len(registered_agents) == 1: - sole_agent = registered_agents[0] - bound_agent = core_todo.bound_agent or ( - core_todo.blocks_agent if core_todo.role == "user" else None - ) - if requested_owner == sole_agent or core_todo.claimed_by == sole_agent or bound_agent == sole_agent: - normalized_actor = sole_agent - # A pre-coordination goal has no declared registration list, but explicit - # legacy callers still supplied an actor. Preserve that behavior without - # weakening the shared ownership/exclusion checks: the supplied actor is - # the only admissible peer for this decision. - decision_registered_agents = registered_agents or ( - [normalized_actor] if normalized_actor is not None else [] - ) - effective_action = str(authority_action or command).strip().lower() - if command == "claim": - return _authorize_typescript_claim( - goal_id=goal_id, - todo=todo, - core_todo=core_todo, - registered_agents=decision_registered_agents, - actor=normalized_actor, - requested_owner=requested_owner, - ) - try: - core_action = TodoAction(command) - except ValueError as exc: - raise ValueError(f"unsupported todo lifecycle command={command!r}") from exc - normalized_decision_outcome = normalize_todo_decision_outcome(decision_outcome) - core_command = TodoMutationCommand( - action=core_action, - actor_agent_id=normalized_actor, - requested_claimed_by=requested_owner, - authority_action=effective_action, - authority_reason=authority_reason, - decision_outcome=normalized_decision_outcome, - ) - core_snapshot = CoordinationSnapshot( - registered_agents=tuple(decision_registered_agents), - todo=core_todo, - decision_target=core_target, - ) - raw_grants: list[dict[str, Any]] = [] - plan = decide(core_snapshot, core_command) - if plan.outcome is DecisionOutcome.REJECTED and plan.code == "claim_owner_mismatch": - coordination = goal.get("coordination") if isinstance(goal, Mapping) else None - raw_grants = normalize_todo_lifecycle_authority( - coordination.get("todo_lifecycle_authority") - if isinstance(coordination, Mapping) - else None, - registered_agents=decision_registered_agents, - ) - plan = decide( - CoordinationSnapshot( - registered_agents=core_snapshot.registered_agents, - lifecycle_grants=tuple( - LifecycleGrant( - agent_id=grant["agent_id"], - actions=frozenset(grant["actions"]), - requires_reason=grant["requires_reason"], - ) - for grant in raw_grants - ), - todo=core_todo, - decision_target=core_target, - ), - core_command, - ) - if plan.outcome is DecisionOutcome.REJECTED: - _raise_core_authority_rejection( - code=plan.code, - goal_id=goal_id, - registered_agents=registered_agents, - command=command, - actor=normalized_actor, - requested_owner=requested_owner, - todo=core_todo, - action=effective_action, - ) - base = { - "schema_version": TODO_MUTATION_AUTHORITY_SCHEMA_VERSION, - "command": command, - "mode": plan.authority_mode, - "actor_agent_id": normalized_actor, - "todo_id": core_todo.todo_id, - "registered_agent_count": len(registered_agents), - } - if plan.authority_mode == "exact_user_gate_decision_scope_override": - gate_scope = normalize_todo_decision_scope(todo.get("decision_scope")) - return { - **base, - "actor_agent_id": None, - "target_todo_id": core_todo.unblocks_todo_id, - "decision_outcome": normalized_decision_outcome, - "decision_scope": gate_scope, - "authority_source": "linked_user_gate_decision_scope", - } - if plan.authority_mode == "delegated_orchestration_override": - grant = next(item for item in raw_grants if item["agent_id"] == normalized_actor) - normalized_reason = str(authority_reason or "").strip() - return { - **base, - "claim_owner": core_todo.claimed_by, - "authority_action": effective_action, - "authority_source": "coordination.todo_lifecycle_authority", - "authority_reason": normalized_reason or None, - "requires_reason": grant["requires_reason"], - } - if plan.authority_mode == "registered_peer_actor": - base["claim_owner"] = core_todo.claimed_by - return base diff --git a/loopx/control_plane/todos/provider_create.py b/loopx/control_plane/todos/provider_create.py index b3f8e0aba4..8386b1b018 100644 --- a/loopx/control_plane/todos/provider_create.py +++ b/loopx/control_plane/todos/provider_create.py @@ -14,7 +14,7 @@ from ..coordination.local_authority import ( LOCAL_AUTHORITY_SOURCES, LocalCoordinationAuthorityUnavailable, - local_authority_is_promoted, + raise_canonical_write_failure, read_canonical_todos_if_promoted, ) from ..effect_runtime import ( @@ -47,11 +47,9 @@ def create_canonical_todo_if_promoted( project: Path | None = None, state_file: Path | None = None, operation_id: str | None = None, expected_provider_revision: str | None = None, -) -> dict[str, Any] | None: - # The durable fence selects the route; the native transaction owns the - # complete-head validation. A separate source read supplies no create facts. - if not local_authority_is_promoted(runtime_root=runtime_root, goal_id=goal_id): - return None +) -> dict[str, Any]: + # The native writer guard selects the route under the same maintenance + # lock as provider open and commit. A separate source read grants nothing. if operation_id is not None and not re.fullmatch(r"[A-Za-z0-9_.:-]+", operation_id): raise ValueError("operation_id must be a non-empty public-safe token") operation_id = operation_id if operation_id is not None else f"todo-create:{uuid4().hex}" @@ -132,6 +130,11 @@ def create_canonical_todo_if_promoted( or result.get("legacy_fallback_used") is not False ): payload = result if isinstance(result, dict) else {} + raise_canonical_write_failure( + payload, + goal_id=goal_id, + operation="todo_create", + ) if ( payload.get("status") == "missing" and payload.get("source_authority") in LOCAL_AUTHORITY_SOURCES diff --git a/loopx/control_plane/todos/provider_handoff_mode.py b/loopx/control_plane/todos/provider_handoff_mode.py index 5272dbdfc5..c79ee6b523 100644 --- a/loopx/control_plane/todos/provider_handoff_mode.py +++ b/loopx/control_plane/todos/provider_handoff_mode.py @@ -6,7 +6,7 @@ from ..coordination.local_authority import ( LOCAL_AUTHORITY_SOURCES, LocalCoordinationAuthorityRejection, LocalCoordinationAuthorityUnavailable, - local_authority_is_promoted, read_canonical_todos_if_promoted, + raise_canonical_write_failure, read_canonical_todos_if_promoted, ) from ..effect_runtime import ( CANONICAL_AUTHORITY_WRITE_TIMEOUT_SECONDS, @@ -24,9 +24,7 @@ def read_canonical_handoff_mode(*, runtime_root: Path, goal_id: str) -> dict[str def set_canonical_handoff_mode(*, runtime_root: Path, goal_id: str, mode: str, - operation_id: str | None, dry_run: bool) -> dict[str, Any] | None: - if not local_authority_is_promoted(runtime_root=runtime_root, goal_id=goal_id): - return None + operation_id: str | None, dry_run: bool) -> dict[str, Any]: result = effect_runtime_result("coordination.local_authority.handoff_mode_set", { "schema_version": "loopx_coordination_handoff_mode_set_request_v0", "runtime_root": str(runtime_root.expanduser().resolve()), "goal_id": goal_id, @@ -37,6 +35,11 @@ def set_canonical_handoff_mode(*, runtime_root: Path, goal_id: str, mode: str, or result.get("source_authority") not in LOCAL_AUTHORITY_SOURCES or result.get("decision_read_from_provider") is not True or result.get("legacy_fallback_used") is not False): payload = result if isinstance(result, dict) else {} + raise_canonical_write_failure( + payload, + goal_id=goal_id, + operation="handoff_mode_set", + ) if payload.get("status") == "failed" and payload.get("failure_kind") == "decision_rejection": raise LocalCoordinationAuthorityRejection( str(payload.get("reason") or "canonical handoff mode request was rejected"), diff --git a/loopx/control_plane/todos/provider_terminal_lifecycle.py b/loopx/control_plane/todos/provider_terminal_lifecycle.py index 9ef427c2b3..7532f09238 100644 --- a/loopx/control_plane/todos/provider_terminal_lifecycle.py +++ b/loopx/control_plane/todos/provider_terminal_lifecycle.py @@ -8,9 +8,7 @@ from __future__ import annotations import hashlib -from collections.abc import Callable, Iterable, Mapping -from functools import wraps -from inspect import signature +from collections.abc import Iterable, Mapping from pathlib import Path from typing import Any @@ -20,6 +18,7 @@ LocalCoordinationAuthorityRejection, LocalCoordinationAuthorityUnavailable, read_canonical_todos_if_promoted, + require_canonical_authority, ) from ...paths import effective_runtime_root from ..effect_runtime import ( @@ -47,9 +46,6 @@ _ACCEPTED = {"applied", "recovered", "replayed", "no_change", "planned"} -TodoMutation = Callable[..., dict[str, Any]] - - def _non_negative_integer(value: Any, label: str, *, optional: bool) -> int | None: if value is None and optional: return None @@ -60,7 +56,7 @@ def _non_negative_integer(value: Any, label: str, *, optional: bool) -> int | No return result -def _route_terminal_call(command: str, call: Mapping[str, Any]) -> dict[str, Any] | None: +def route_terminal_call(command: str, call: Mapping[str, Any]) -> dict[str, Any]: registry_path = Path(call["registry_path"]) goal_id = str(call["goal_id"]) runtime_root = effective_runtime_root(registry_path, call.get("runtime_root_arg")) @@ -78,10 +74,10 @@ def _route_terminal_call(command: str, call: Mapping[str, Any]) -> dict[str, Any project=call.get("project"), state_file=call.get("state_file"), # Canonical archive can recover its display after committing or - # replaying. The legacy fallback still requires an existing file. + # replaying, so a missing projection file is not admission. require_existing=False, ) - return archive_canonical_todos_if_promoted( + result = archive_canonical_todos_if_promoted( registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id, @@ -91,6 +87,9 @@ def _route_terminal_call(command: str, call: Mapping[str, Any]) -> dict[str, Any project=project, state_file=state_file, ) + if result is None: + require_canonical_authority(goal_id=goal_id, operation="todo_archive") + return result next_agent_todo = call.get("next_agent_todo") if call.get("next_task_repository") and not next_agent_todo: @@ -107,7 +106,7 @@ def _route_terminal_call(command: str, call: Mapping[str, Any]) -> dict[str, Any require_existing=False, ) complete = command == "complete" - return terminal_canonical_todo_if_promoted( + result = terminal_canonical_todo_if_promoted( registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id, @@ -159,30 +158,9 @@ def _route_terminal_call(command: str, call: Mapping[str, Any]) -> dict[str, Any project=project, state_file=state_file, ) - - -def provider_first_terminal_lifecycle(command: str) -> Callable[[TodoMutation], TodoMutation]: - """Route the public facade through canonical authority before legacy fallback.""" - if command not in {"complete", "supersede", "archive"}: - raise ValueError(f"unsupported terminal lifecycle command: {command}") - - def decorate(legacy: TodoMutation) -> TodoMutation: - call_signature = signature(legacy) - - @wraps(legacy) - def routed(*args: Any, **kwargs: Any) -> dict[str, Any]: - bound = call_signature.bind(*args, **kwargs) - bound.apply_defaults() - result = _route_terminal_call(command, bound.arguments) - if result is None and bound.arguments.get("terminal_review_basis") is not None: - raise ValueError("Reviewed canonical completion cannot fall back to legacy authority; regenerate preview") - if result is None and bound.arguments.get("completion_result_file") is not None: - raise ValueError("Completion results require promoted canonical Todo authority") - return result if result is not None else legacy(*args, **kwargs) - - return routed - - return decorate + if result is None: + require_canonical_authority(goal_id=goal_id, operation="todo_terminal") + return result def _todo_by_id( @@ -634,6 +612,6 @@ def archive_canonical_todos_if_promoted( __all__ = [ "archive_canonical_todos_if_promoted", - "provider_first_terminal_lifecycle", + "route_terminal_call", "terminal_canonical_todo_if_promoted", ] diff --git a/loopx/control_plane/todos/provider_update.py b/loopx/control_plane/todos/provider_update.py index 60a29ef0e3..9a259c4f0f 100644 --- a/loopx/control_plane/todos/provider_update.py +++ b/loopx/control_plane/todos/provider_update.py @@ -14,8 +14,9 @@ from ...state_refresh import now_local from ..coordination.local_authority import ( LOCAL_AUTHORITY_SOURCES, + LocalCoordinationAuthorityRejection, LocalCoordinationAuthorityUnavailable, - local_authority_is_promoted, + raise_canonical_write_failure, read_canonical_todos_if_promoted, ) from ..effect_runtime import ( @@ -40,6 +41,7 @@ from .completion_validation_projection import ( completion_validation_declaration_sha256, ) +from .external_wait_contract import TodoExternalWaitAuthoringError from .path_resolution import resolve_todo_state_path from .monitor_metadata import MonitorPollObservation @@ -158,9 +160,7 @@ def update_canonical_todo_if_promoted( expected_registry_sha256: str | None = None, monitor_observation: MonitorPollObservation | None = None, completion_validation_revision: dict[str, Any] | None = None, -) -> dict[str, Any] | None: - if not local_authority_is_promoted(runtime_root=runtime_root, goal_id=goal_id): - return None +) -> dict[str, Any]: # The witness brackets fact projection and is rechecked by the native # transaction. Reviewed edits additionally bind the preview's registry hash. with authority_registry_source(registry_path) as registry_source: @@ -246,15 +246,57 @@ def update_canonical_todo_if_promoted( timeout=CANONICAL_AUTHORITY_WRITE_TIMEOUT_SECONDS, ) if isinstance(result, dict): + raise_canonical_write_failure( + result, + goal_id=goal_id, + operation="todo_update", + ) failure = completion_validation_failure(result, goal_id=goal_id, todo_id=todo_id, dry_run=dry_run) if failure is not None: return failure # Keep the public lookup-error contract, without a second pre-transaction read. if isinstance(result, dict) and result.get("status") == "failed": - if result.get("reason_code") == "todo_not_found": + reason_code = str(result.get("reason_code") or "") + if reason_code.startswith("external_wait_"): + resume_when = str((planning_intent or {}).get("resume_when") or "") + resume_kind, separator, dependency_id = resume_when.partition(":") + raw_successors = (planning_intent or {}).get("successor_todo_ids") + raise TodoExternalWaitAuthoringError( + str(result.get("reason") or "external-wait Todo update was rejected"), + code=reason_code, + monitor_todo_id=( + dependency_id + if separator and resume_kind == "monitor_changed" + else None + ), + successor_todo_ids=( + [str(item) for item in raw_successors] + if isinstance(raw_successors, list) + else None + ), + ) + if reason_code == "todo_not_found": raise ValueError("Todo is missing from canonical authority") - if result.get("reason_code") == "todo_role_mismatch": + if reason_code == "todo_role_mismatch": raise ValueError("Todo does not have the requested role") + if reason_code == "coordination_operation_identity_mismatch": + raise LocalCoordinationAuthorityRejection( + "monitor effect identity is already bound to a different observation", + code=reason_code, + payload=result, + ) + if reason_code == "update_owner_mismatch": + raise LocalCoordinationAuthorityRejection( + "Todo claimed_by does not grant this actor update authority", + code=reason_code, + payload=result, + ) + if result.get("failure_kind") == "decision_rejection": + raise LocalCoordinationAuthorityRejection( + str(result.get("reason") or "canonical Todo update was rejected"), + code=reason_code or "todo_update_rejected", + payload=result, + ) if isinstance(result, dict) and ( result.get("status") == "missing" and result.get("source_authority") in LOCAL_AUTHORITY_SOURCES diff --git a/loopx/control_plane/todos/successor_derivation.py b/loopx/control_plane/todos/successor_derivation.py index 65352a4b29..d6abe8ad33 100644 --- a/loopx/control_plane/todos/successor_derivation.py +++ b/loopx/control_plane/todos/successor_derivation.py @@ -11,22 +11,6 @@ TODO_SUCCESSOR_DERIVATION_REQUEST_SCHEMA = "loopx_todo_successor_derivation_request_v0" TODO_SUCCESSOR_DERIVATION_RESULT_SCHEMA = "loopx_todo_successor_derivation_result_v0" -_SUCCESSOR_ADD_FIELDS = ( - "role", - "text", - "task_class", - "action_kind", - "capability_binding_ref", - "task_repository", - "continuation_policy", - "required_capabilities", - "claimed_by", - "bound_agent", - "blocks_agent", - "excluded_agents", - "unblocks_todo_id", -) - def build_successor_intents( *, @@ -118,18 +102,7 @@ def derive_successor_proposals( return proposals -def successor_add_kwargs(proposal: Mapping[str, Any]) -> dict[str, Any]: - """Adapt one typed proposal to the legacy Markdown writer arguments.""" - - return { - field: proposal[field] - for field in _SUCCESSOR_ADD_FIELDS - if field in proposal - } - - __all__ = [ "build_successor_intents", "derive_successor_proposals", - "successor_add_kwargs", ] diff --git a/loopx/control_plane/work_items/canonical_task_lease_lifecycle.ts b/loopx/control_plane/work_items/canonical_task_lease_lifecycle.ts index 6ae7f67b3f..38d8496cd4 100644 --- a/loopx/control_plane/work_items/canonical_task_lease_lifecycle.ts +++ b/loopx/control_plane/work_items/canonical_task_lease_lifecycle.ts @@ -4,7 +4,11 @@ import {canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts import {openLocalAuthorityStoreHandle, localAuthorityOpenFailure, type LocalAuthorityProviderDependencies} from "../coordination/local_authority_provider.ts"; import {loadLegacyCoordinationWriterFence} from "../coordination/legacy_writer_fence.ts"; -import {withCanonicalWriter} from "../coordination/local_authority_write.ts"; +import { + canonicalWriteRouteFailure, + withCanonicalWriter, + type CanonicalBusinessWrite, +} from "../coordination/local_authority_write.ts"; import {ShadowManagementError} from "../coordination/shadow_management.ts"; import {EffectRuntimeLockTimeoutError} from "../effect_runtime_errors.ts"; import {executeCanonicalTaskLeaseLifecycle} from "../coordination/task_lease_lifecycle.ts"; @@ -27,23 +31,40 @@ export interface LocalTaskLeaseDependencies { authorityProvider?: LocalAuthorityProviderDependencies; } +type CanonicalTaskLeaseBusinessOperation = + | "acquire" + | TaskLeaseLifecycleDecisionOperation; + +const TASK_LEASE_BUSINESS_WRITES = { + acquire: "task_lease_acquire", + renew: "task_lease_renew", + transfer: "task_lease_transfer", + release: "task_lease_release", +} satisfies Record; + /** The same local promotion/source fence surrounds acquire and maintenance. */ export async function withCanonicalTaskLeaseAuthority( - request: {runtime_root: string; goal_id: string; operation: string; + request: {runtime_root: string; goal_id: string; operation: CanonicalTaskLeaseBusinessOperation; owner: string | null; idempotency_key: string | null; authority: AuthorityFacts | null}, dependencies: LocalTaskLeaseDependencies, execute: (store: AuthorityStore, guards: { revalidate: () => Promise; beforeCommit: (lease: JsonObject | null) => Promise; }) => Promise): Promise { const root = request.runtime_root, goalId = request.goal_id; - const initialFence = await loadLegacyCoordinationWriterFence(root, goalId); const evidence: JsonObject = {source_authority: null, decision_read_from_provider: false, legacy_fallback_used: false}; const rejected = (code: string, reason: string): JsonObject => ({status: "failed", changed: false, reason_code: code, reason, failure_stage: "validation", ...evidence}); - if (initialFence.status === "missing") return rejected(`canonical_${request.operation}_fence_missing`, "canonical lease writer fence is missing; legacy fallback is forbidden"); - if (initialFence.status === "failed") return rejected(initialFence.reason_code, initialFence.reason); + const businessWrite = TASK_LEASE_BUSINESS_WRITES[request.operation]; try { return await withCanonicalWriter(root, goalId, false, async () => { + const initialFence = await loadLegacyCoordinationWriterFence(root, goalId); + if (initialFence.status !== "loaded") { + throw new TaskLeaseAcquireError( + "canonical lease writer fence changed; inspect authority before retrying", + `canonical_${request.operation}_fence_changed`, + {goal_id: goalId}, + ); + } const verifyFence = async () => { const current = await loadLegacyCoordinationWriterFence(root, goalId); if (current.status !== "loaded" || canonicalAuthoritySha256(current.fence) !== canonicalAuthoritySha256(initialFence.fence)) { @@ -69,11 +90,12 @@ export async function withCanonicalTaskLeaseAuthority( await revalidate(); }}); return {...result, ...evidence}; - }); + }, {businessWrite}); } catch (error) { if (error instanceof ShadowManagementError || error instanceof TaskLeaseAcquireError) return {...rejected(error.code, error.message), ...error.payload}; if (error instanceof EffectRuntimeLockTimeoutError) return rejected("lock_acquire_timeout", "canonical lease maintenance lock timed out"); - return {...rejected("canonical_lease_route_failed", error instanceof Error ? error.message : "canonical lease route failed"), ...localAuthorityOpenFailure(error)}; + return {...rejected("canonical_lease_route_failed", error instanceof Error ? error.message : "canonical lease route failed"), + ...localAuthorityOpenFailure(error), ...canonicalWriteRouteFailure(error)}; } } diff --git a/loopx/control_plane/work_items/replan_settlement.ts b/loopx/control_plane/work_items/replan_settlement.ts index 540ca257cf..fe8dee6334 100644 --- a/loopx/control_plane/work_items/replan_settlement.ts +++ b/loopx/control_plane/work_items/replan_settlement.ts @@ -148,13 +148,14 @@ export function projectTodoLifecycleSettlementReentry(value: unknown): JsonObjec trigger.completion_identity_source === "unscoped_completion" ? ["--completion-identity-key", completionIdentityKey] : ["--turn-instance-id", completionIdentityKey]; + // Reentry closes continuation only; it must not replace completion prose. return ( `when no real successor remains for completed Todo ${todoId}: ` + `${commandPrefix} todo complete --goal-id ${shellArgument(goalId)} ` + `--todo-id ${shellArgument(todoId)}` + argumentSuffix(completionIdentityArgs) + argumentSuffix(lifecycleActorArgs) + - " --no-follow-up --note ''" + " --no-follow-up" ); }); nextCliActions.push( diff --git a/loopx/control_plane/work_items/task_lease.py b/loopx/control_plane/work_items/task_lease.py index 23d7e03fdd..e7e1abd857 100644 --- a/loopx/control_plane/work_items/task_lease.py +++ b/loopx/control_plane/work_items/task_lease.py @@ -1,9 +1,6 @@ from __future__ import annotations -import os import re -from collections.abc import Callable, Iterator -from contextlib import ExitStack, contextmanager from datetime import datetime from pathlib import Path from typing import Any @@ -25,13 +22,6 @@ normalize_todo_excluded_agents, normalize_todo_id, ) -from ..todos.handoff_mode import ( - HANDOFF_MODE_HARD_LEASE, - HANDOFF_MODE_LEGACY, - HandoffModeError, - goal_handoff_mode_for_goal, - resolve_todo_completion_handoff, -) from .local_lease_record import ( TASK_LEASE_SCHEMA_VERSION, TaskLeaseError, @@ -44,7 +34,6 @@ require_expected_version as require_expected_version, write_lease as write_lease, ) -from ..content_digest import BARE_SHA256_PATTERN DEFAULT_TASK_LEASE_TTL_SECONDS = 45 * 60 MAX_TASK_LEASE_TTL_SECONDS = 24 * 60 * 60 @@ -119,102 +108,6 @@ def task_lease_lock_path(*, runtime_root: Path, goal_id: str) -> Path: return task_lease_dir(runtime_root=runtime_root, goal_id=goal_id) / ".task-leases" -class _VerifiedTaskLeaseFence(dict): - """Key-verified fence payload with its release hook held out-of-band. - - The hook lives on the instance attribute, never inside the mapping, so - the payload stays JSON-serializable for every consumer at all times. - """ - - release_hook: Callable[[bool, bool], dict[str, Any] | None] | None = None - abandon_hook: Callable[[], bool] | None = None - # The fence receipt id is an internal bridge detail. Keep it out of the - # historical JSON mapping while carrying it across the one-shot close RPC. - fence_operation_id: str | None = None - - -def _native_fence_payload( - result: dict[str, Any], - *, - operation: str, -) -> tuple[dict[str, Any], str | None, str | None]: - raw = result.get("fence") - if not isinstance(raw, dict): - raise TaskLeaseError( - f"native task-lease {operation} result omitted fence", - code="invalid_fence_result", - ) - token = raw.get("lock_token") - operation_id = raw.get("fence_operation_id") - if operation_id is not None and ( - not isinstance(operation_id, str) - or not BARE_SHA256_PATTERN.fullmatch(operation_id) - ): - raise TaskLeaseError( - f"native task-lease {operation} result has an invalid fence operation id", - code="invalid_fence_result", - ) - if raw.get("required") is False: - return { - key: value - for key, value in raw.items() - if key not in {"lock_token", "fence_operation_id"} - }, None, operation_id - if not isinstance(token, str) or not token: - raise TaskLeaseError( - f"native task-lease {operation} result omitted lock token", - code="invalid_fence_result", - ) - public = { - key: value - for key, value in raw.items() - if key not in {"lock_token", "fence_operation_id"} - } - return public, token, operation_id - - -def _close_native_fence( - fence: dict[str, Any] | None, - *, - committed: bool, - release_lease: bool, - suppress_errors: bool = False, -) -> dict[str, Any] | None: - hook = getattr(fence, "release_hook", None) - if hook is None or not isinstance(fence, dict): - return None - try: - result = hook(committed, release_lease) - except (OSError, TaskLeaseError, RuntimeError): - abandon = getattr(fence, "abandon_hook", None) - abandoned = False - if abandon is not None: - try: - abandoned = abandon() - except OSError: - abandoned = False - if abandoned: - # A token claim proved that no native closer was in its critical - # section, so this caller safely retired only its own held lock. - # Disarm the semantic close hook; the lease intentionally remains - # active for explicit release or TTL expiry. - fence.release_hook = None # type: ignore[attr-defined] - fence.abandon_hook = None # type: ignore[attr-defined] - if not suppress_errors: - raise - fence["released"] = False - return None - # Only disarm after the close RPC has completed. A failed close remains - # retryable by the context manager's cleanup path. - fence.release_hook = None # type: ignore[attr-defined] - fence.abandon_hook = None # type: ignore[attr-defined] - if isinstance(result, dict) and result.get("released") is True: - fence["released"] = True - elif committed and release_lease: - fence["released"] = False - return result - - def _execute_native_task_lease_lifecycle(**kwargs: Any) -> dict[str, Any]: from .task_lease_acquire_adapter import execute_native_task_lease_lifecycle @@ -226,330 +119,6 @@ def _execute_native_task_lease_lifecycle(**kwargs: Any) -> dict[str, Any]: return execute_native_task_lease_lifecycle(**kwargs) -def goal_handoff_mode_for_lease(registry_path: Path, goal_id: str) -> str: - """Resolve the goal handoff mode for lease verbs. - - An invalid ``handoff_mode`` value is re-typed as a TaskLeaseError. A goal - or state file that cannot be resolved falls back to legacy: each lease - verb's own todo-projection checks already own that failure surface. - """ - - try: - return goal_handoff_mode_for_goal(registry_path=registry_path, goal_id=goal_id) - except HandoffModeError as exc: - raise TaskLeaseError(str(exc), code=exc.code, payload=exc.payload) from exc - except (OSError, ValueError): - return HANDOFF_MODE_LEGACY - - -def _optional_handoff_mode(registry_path: Path | None, goal_id: str) -> str | None: - if registry_path is None: - return None - try: - return goal_handoff_mode_for_lease(registry_path, goal_id) - except (TaskLeaseError, OSError, ValueError): - return None - - -@contextmanager -def hold_handoff_lease_holder_gate( - *, - registry_path: Path, - goal_id: str, - todo_id: str, - actor_agent_id: str | None, - runtime_root: Path | None = None, -) -> Iterator[dict[str, Any]]: - """Hold the per-goal lease lock while proving the actor owns the lease. - - hard_lease handoff mode only: an ownership mutation (claim, claimed_by - update, clear) on an existing todo requires the acting agent to hold that - todo's time-active lease. Identity is compared with - normalize_todo_claimed_by on both sides. Callers must already hold the - markdown state lock; this guard then takes the per-goal lease lock, the - same markdown-lock-then-lease-lock order used by the completion fence, - and the caller keeps it held until the markdown write commits. - """ - - normalized_goal_id = normalize_goal_id(goal_id) - normalized_todo_id = normalize_lease_todo_id(todo_id) - actor = normalize_todo_claimed_by(actor_agent_id) - if not actor: - raise TaskLeaseError( - "hard_lease handoff mode requires an attributed actor for " - "ownership changes; provide --agent-id", - code="handoff_mode_requires_lease", - payload={ - "goal_id": normalized_goal_id, - "todo_id": normalized_todo_id, - "handoff_mode": HANDOFF_MODE_HARD_LEASE, - "reason": "missing_actor", - }, - ) - if runtime_root is None: - runtime_root = runtime_root_from_registry(registry_path, None) - result = _execute_native_task_lease_lifecycle( - runtime_root=runtime_root, - registry_path=registry_path, - goal_id=normalized_goal_id, - todo_id=normalized_todo_id, - operation="holder_verify", - owner=actor, - ) - public, token, operation_id = _native_fence_payload( - result, - operation="holder_verify", - ) - fence = _VerifiedTaskLeaseFence(public) - fence.fence_operation_id = operation_id - - def close(committed: bool, release_lease: bool) -> dict[str, Any] | None: - return _execute_native_task_lease_lifecycle( - runtime_root=runtime_root, - registry_path=registry_path, - goal_id=normalized_goal_id, - todo_id=normalized_todo_id, - operation="fence_close", - lock_token=token, - committed=committed, - release_lease=release_lease, - fence_owner=actor, - fence_idempotency_key=None, - fence_expected_version=public.get("version") - if isinstance(public.get("version"), int) - else None, - fence_expected_lease_epoch=public.get("lease_epoch") - if isinstance(public.get("lease_epoch"), int) - else None, - fence_operation_id=operation_id, - owner_pid=os.getpid(), - ) - - fence.release_hook = close - from ...file_lock import release_cross_runtime_mutation_lock - - fence.abandon_hook = lambda: release_cross_runtime_mutation_lock( - task_lease_lock_path( - runtime_root=runtime_root, - goal_id=normalized_goal_id, - ), - token=token, - ) - try: - yield fence - finally: - # A holder gate is only a lock-scoped proof; it never releases the - # lease. Always close it on exceptional caller paths. - _close_native_fence( - fence, - committed=False, - release_lease=False, - suppress_errors=True, - ) - - -@contextmanager -def hold_task_lease_mutation_fence( - *, - registry_path: Path, - goal_id: str, - todo_id: str, - todo: dict[str, Any], - actor_agent_id: str | None, - idempotency_key: str | None, - expected_version: int | None = None, - require_active_when_key_supplied: bool = True, - handoff: dict[str, Any] | None = None, - runtime_root: Path | None = None, -) -> Iterator[dict[str, Any]]: - """Hold the per-goal lease lock while one todo lifecycle write commits. - - Task leases are optional in legacy and soft_claim handoff modes. Once an - effective lease exists, however, the lifecycle writer must prove that it - is the execution instance that acquired the lease. The idempotency key is - the execution-instance fence; agent ids alone are insufficient because - multiple host processes may share one peer identity. - - ``handoff`` carries the resolved goal handoff mode plus the delegated - authority door marker. In hard_lease mode (without the door) the fence is - mandatory: no effective lease is a typed error instead of a silent - ``{"required": False}``, and a time-active lease whose owner constraint is - no longer effective (the legacy self-disarm state) fails loudly instead of - letting a keyless completion through. For a user-role ``user_gate`` - completion that supplies no explicit lease credentials, the fence mints - the key itself under the same per-goal lease lock; an existing - time-active lease is never displaced. - """ - - normalized_goal_id = normalize_goal_id(goal_id) - normalized_todo_id = normalize_lease_todo_id(todo_id) - if runtime_root is None: - runtime_root = runtime_root_from_registry(registry_path, None) - handoff = handoff or {} - result = _execute_native_task_lease_lifecycle( - runtime_root=runtime_root, - registry_path=registry_path, - goal_id=normalized_goal_id, - todo_id=normalized_todo_id, - operation="terminal_verify", - owner=normalize_todo_claimed_by(actor_agent_id), - idempotency_key=idempotency_key, - expected_version=expected_version, - todo=todo, - delegated_authority=handoff.get("handoff_gate_overridden") is True, - allow_user_gate_auto_acquire=not require_active_when_key_supplied, - require_active_when_fence_supplied=require_active_when_key_supplied, - ) - public, token, operation_id = _native_fence_payload( - result, - operation="terminal_verify", - ) - if token is None: - yield public - return - fence = _VerifiedTaskLeaseFence(public) - fence.fence_operation_id = operation_id - native_fence = result.get("fence") - if not isinstance(native_fence, dict): - raise TaskLeaseError("native terminal fence result is malformed", code="invalid_fence_result") - verified_owner = native_fence.get("owner") - verified_key = idempotency_key or ( - f"auto-{normalized_todo_id}" - if public.get("auto_acquired") is True - else None - ) - verified_version = native_fence.get("version") - verified_lease_epoch = native_fence.get("lease_epoch") - - def close(committed: bool, release_lease: bool) -> dict[str, Any] | None: - return _execute_native_task_lease_lifecycle( - runtime_root=runtime_root, - registry_path=registry_path, - goal_id=normalized_goal_id, - todo_id=normalized_todo_id, - operation="fence_close", - lock_token=token, - committed=committed, - release_lease=release_lease, - fence_owner=verified_owner if isinstance(verified_owner, str) else None, - fence_idempotency_key=verified_key if isinstance(verified_key, str) else None, - fence_expected_version=verified_version if isinstance(verified_version, int) else None, - fence_expected_lease_epoch=verified_lease_epoch - if isinstance(verified_lease_epoch, int) - else None, - fence_operation_id=operation_id, - owner_pid=os.getpid(), - ) - - fence.release_hook = close - from ...file_lock import release_cross_runtime_mutation_lock - - fence.abandon_hook = lambda: release_cross_runtime_mutation_lock( - task_lease_lock_path( - runtime_root=runtime_root, - goal_id=normalized_goal_id, - ), - token=token, - ) - try: - yield fence - finally: - _close_native_fence( - fence, - committed=False, - release_lease=False, - suppress_errors=True, - ) - - -def enter_terminal_todo_lease_fence( - stack: ExitStack, - *, - registry_path: Path, - goal_id: str, - todo_id: str, - todo: dict[str, Any], - actor_agent_id: str | None, - state_text: str, - mutation_authority: dict[str, Any], - idempotency_key: str | None = None, - expected_version: int | None = None, - runtime_root: Path | None = None, -) -> tuple[dict[str, Any], dict[str, Any]]: - """Resolve the goal handoff and hold the lease fence for one terminal write. - - Every transition that moves an existing todo to ``done`` (complete and - supersede alike) is a completion for the lease contract: it must cross - the same per-goal lease fence with the same typed outcomes, so a leased - todo cannot be retired keylessly through a sibling verb. The caller keeps - the state-file lock and ``stack`` open across its write and then calls - ``release_verified_task_lease_fence`` with the commit outcome. - """ - - handoff = resolve_todo_completion_handoff( - state_text=state_text, - mutation_authority=mutation_authority, - ) - fence = stack.enter_context( - hold_task_lease_mutation_fence( - registry_path=registry_path, - goal_id=goal_id, - todo_id=todo_id, - todo=todo, - actor_agent_id=actor_agent_id, - idempotency_key=idempotency_key, - expected_version=expected_version, - require_active_when_key_supplied=( - idempotency_key is not None or expected_version is not None - ), - handoff=handoff, - runtime_root=runtime_root, - ) - ) - return handoff, fence - - -def release_verified_task_lease_fence( - fence: dict[str, Any] | None, - *, - committed: bool, -) -> None: - """Release the lease behind a key-verified fence once its write committed. - - Must run while the hold_task_lease_mutation_fence context is still open so - the per-goal lease lock it acquired is still held; the lease therefore - cannot have been renewed, transferred, or re-acquired since the fence - verified the owner, idempotency key, and version. The release reuses the - CLI release semantics and persists the terminal record that fences the - next lease generation. - - The private release hook rides on the fence object's attribute, not inside - the payload mapping, and is disarmed here on every call. Only a committed, - key-verified fence releases the lease; non-verified fences carry no hook - and are never touched. A release failure never unwinds the committed - lifecycle write: it is surfaced additively as fence["released"] = False - and the active record is left for an explicit `loopx task-lease release` - or TTL expiry. - """ - - if not isinstance(fence, dict): - return - if fence.get("execution_instance_verified") is not True: - _close_native_fence( - fence, - committed=False, - release_lease=False, - suppress_errors=True, - ) - return - _close_native_fence( - fence, - committed=committed, - release_lease=committed, - suppress_errors=True, - ) - - def runtime_root_from_registry( registry_path: Path, runtime_root_override: str | None, @@ -756,7 +325,6 @@ def acquire_task_lease( ttl_seconds=ttl_seconds, write_scopes=write_scopes, expected_version=expected_version, - _legacy_provider_projection=True, ) if payload.get("ok") is not True: error_payload = { diff --git a/loopx/control_plane/work_items/task_lease_acquire.ts b/loopx/control_plane/work_items/task_lease_acquire.ts index 9bd184109b..5d17e9038f 100644 --- a/loopx/control_plane/work_items/task_lease_acquire.ts +++ b/loopx/control_plane/work_items/task_lease_acquire.ts @@ -1,22 +1,18 @@ import {observeLeaseWorktree, type LeaseWorkspace} from "./task_lease_workspace.ts"; import {executeCanonicalTaskLeaseAcquire} from "../coordination/task_lease_acquire.ts"; import {withCanonicalTaskLeaseAuthority} from "./canonical_task_lease_lifecycle.ts"; -import {evaluateTaskLeaseWriteScopesOverlap, evaluateTaskLeaseAcquireDecision, materializeTaskLeaseAcquire, type AcquireDecisionOtherLease} from "./task_lease_acquire_decision.ts"; -import {leaseOwnerRejection as ownerRejection} from "./task_lease_eligibility.ts"; -import { ShadowManagementError, requireShadowPrimaryWriteAllowed } from "../coordination/shadow_management.ts"; +import {evaluateTaskLeaseWriteScopesOverlap} from "./task_lease_acquire_decision.ts"; +import { ShadowManagementError } from "../coordination/shadow_management.ts"; import { parseIsoTimestamp } from "../runtime_timestamp.ts"; -import { LegacyCoordinationWriteError, requireLegacyCoordinationPrimaryWriteAllowed } from "../coordination/legacy_writer_fence.ts"; import { createHash } from "node:crypto"; -import { readdir, readFile } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { EffectRuntimeLockTimeoutError, EffectRuntimeRequestError, } from "../effect_runtime_errors.ts"; -import { atomicWriteJson, withFileMutationLock } from "../effect_runtime_io.ts"; import { - legacyCoordinationLeaseLockPath, taskLeaseLockPath, } from "../coordination/legacy_writer_lock_paths.ts"; import { @@ -25,7 +21,6 @@ import { } from "../effect_program.ts"; import { requireJsonObject } from "../runtime_decode.ts"; import { - beginLeaseOutboxEntry, decodeLocalAuthorityShadowBinding, type LocalAuthorityShadowBinding, } from "../coordination/local_authority_shadow_outbox.ts"; @@ -581,35 +576,6 @@ export function utcIsoformat(value: Date): string { return value.toISOString().replace(/\.\d{3}Z$/u, "Z"); } -function ownerFailure( - code: string, - request: AcquireRequest, - todo: TodoFact | undefined, -): TaskLeaseAcquireError { - let message: string; - const detail: JsonObject = { effective: false, reason: code }; - if (code === "todo_not_found") { - message = "todo is missing from the canonical projection"; - } else if (code === "todo_not_open") { - detail.todo_status = todo?.status || "unknown"; - message = `task lease requires an open todo; '${request.todo_id}' is '${detail.todo_status}'`; - } else if (code === "owner_excluded_from_todo") { - detail.excluded_agents = [...(todo?.excluded_agents ?? [])]; - message = `task lease owner '${request.owner}' is excluded from todo '${request.todo_id}'`; - } else if (code === "owner_conflicts_with_claim") { - detail.claimed_by = todo?.claimed_by ?? null; - message = `task lease owner '${request.owner}' conflicts with todo claim '${todo?.claimed_by}'`; - } else { - message = `task lease owner '${request.owner}' is not registered for goal '${request.goal_id}'`; - } - return new TaskLeaseAcquireError(message, code, { - goal_id: request.goal_id, - todo_id: request.todo_id, - owner: request.owner, - ...detail, - }); -} - async function currentSourceReceipt(receipt: SourceReceipt): Promise { try { const bytes = await readFile(receipt.path); @@ -646,132 +612,6 @@ export async function revalidateAuthoritySources(receipts: readonly SourceReceip } } -async function leaseFiles(directory: string): Promise { - try { - return (await readdir(directory)) - .filter((name) => /^todo_[a-z0-9_-]{3,64}\.json$/u.test(name)) - .sort((left, right) => left.localeCompare(right)) - .map((name) => join(directory, name)); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; - throw error; - } -} - -function conflictPayload( - lease: LeaseRecord, - path: string, -): JsonObject { - return { - todo_id: lease.todo_id ?? null, - owner: lease.owner ?? null, - expires_at: lease.expires_at ?? null, - version: lease.version ?? null, - lease_epoch: leaseEpoch(lease), - write_scopes: lease.write_scopes || [], - lease_path: path, - }; -} - -async function otherLeaseFacts( - request: AcquireRequest, - at: Date, -): Promise> { - const facts: Array<{ decision: AcquireDecisionOtherLease; payload: JsonObject }> = []; - for (const path of await leaseFiles(taskLeaseDirectory(request))) { - const lease = await readLease(path); - if (!leaseIsActive(lease, at) || lease === null) continue; - const leaseTodoId = normalizeTodoId(lease.todo_id, "lease.todo_id"); - if (leaseTodoId === request.todo_id) continue; - const leaseOwner = normalizeAgent(lease.owner); - const effective = ownerRejection( - request.authority.todos.get(leaseTodoId), - leaseOwner, - request.authority.registered_agents, - ) === null; - facts.push({ - decision: { - todo_id: leaseTodoId, - active: true, - effective, - write_scopes: normalizeWriteScopes(lease.write_scopes), - }, - payload: conflictPayload(lease, path), - }); - } - return facts; -} - -function transitionError( - code: string, - request: AcquireRequest, - lease: LeaseRecord | null, - leasePath: string, - conflicts: readonly JsonObject[] = [], - idempotencyReuseKind: "retired" | "acquire_parameters" = "acquire_parameters", -): TaskLeaseAcquireError { - if (code === "handoff_mode_forbids_lease") { - return new TaskLeaseAcquireError( - "goal handoff mode 'soft_claim' forbids task lease acquire; " + - "release and inspect remain available for legacy leftovers", - code, - { - goal_id: request.goal_id, - todo_id: request.todo_id, - action: "acquire", - handoff_mode: request.authority.handoff_mode, - }, - ); - } - if (code === "version_mismatch") { - const actual = leaseVersion(lease); - return new TaskLeaseAcquireError( - `lease version mismatch: expected ${request.expected_version}, got ${actual}`, - code, - { expected_version: request.expected_version, actual_version: actual }, - ); - } - if (code === "idempotency_key_reuse") { - if (idempotencyReuseKind === "retired") { - return new TaskLeaseAcquireError( - "idempotency key belongs to an expired or released lease generation; " + - "a new execution must use a new key", - code, - { lease, lease_path: leasePath }, - ); - } - return new TaskLeaseAcquireError( - "idempotency key was reused with different acquire parameters", - code, - { - lease, - lease_path: leasePath, - requested_write_scopes: [...request.write_scopes], - requested_ttl_seconds: request.ttl_seconds, - }, - ); - } - if (code === "todo_lease_conflict") { - return new TaskLeaseAcquireError( - "todo already has an active lease", - code, - { lease, lease_path: leasePath }, - ); - } - if (code === "write_scope_conflict") { - return new TaskLeaseAcquireError( - "write scope overlaps another active task lease", - code, - { conflicts: [...conflicts] }, - ); - } - return new TaskLeaseAcquireError( - `task lease acquire rejected by authority core: ${code}`, - code, - { lease, lease_path: leasePath }, - ); -} - function successEnvelope( request: AcquireRequest, lease: LeaseRecord, @@ -833,15 +673,6 @@ function failureKind(code: string): string { return "writeback_rejected"; } -/** - * A fenced legacy writer is a terminal permission decision taken by the - * promoted authority before this verb's first side effect: no settlement step - * ran, so no receipt exists and the rejection is a validation-stage denial. - */ -function fencedFailure(error: LegacyCoordinationWriteError): TaskLeaseFailure { - return { code: error.code, message: error.message, payload: error.payload, stage: "validation", kind: "permission_denied" }; -} - function failureEnvelope( failure: TaskLeaseFailure, context: ExecutionContext, @@ -870,17 +701,6 @@ function failureEnvelope( }; } -function validateAcquireAuthority(request: AcquireRequest): TodoFact | null { - if (request.authority.todo_projection_error !== null) { - throw new TaskLeaseAcquireError( - request.authority.todo_projection_error.message, - request.authority.todo_projection_error.code, - request.authority.todo_projection_error.payload, - ); - } - return request.authority.todos.get(request.todo_id) ?? null; -} - function acquireEffectId(request: AcquireRequest): string { return settlementIdentity({ goal_id: request.goal_id, @@ -890,131 +710,6 @@ function acquireEffectId(request: AcquireRequest): string { }).effect_id; } -async function commitAcquire( - request: AcquireRequest, - dependencies: TaskLeaseAcquireDependencies, -): Promise { - const at = (dependencies.now ?? (() => new Date()))(); - if (Number.isNaN(at.valueOf())) { - throw new EffectRuntimeRequestError("task-lease acquire clock returned an invalid date"); - } - await revalidateAuthoritySources(request.authority.source_receipts); - const todo = validateAcquireAuthority(request); - - const leasePath = taskLeasePath(request); - const existing = await readLease(leasePath); - const version = leaseVersion(existing); - const epoch = leaseEpoch(existing); - const active = leaseIsActive(existing, at); - const otherLeases = await otherLeaseFacts(request, at); - const decision = evaluateTaskLeaseAcquireDecision({ - handoff_mode: request.authority.handoff_mode, - registered_agents: [...request.authority.registered_agents], - todo, - lease: existing === null - ? null - : { - present: true, - active, - status: typeof existing.status === "string" ? existing.status : null, - owner: normalizeAgent(existing.owner), - idempotency_key: typeof existing.idempotency_key === "string" - ? existing.idempotency_key - : null, - version, - lease_epoch: epoch, - write_scopes: normalizeWriteScopes(existing.write_scopes), - acquire_ttl_seconds: leaseInteger(existing, "acquire_ttl_seconds"), - }, - other_leases: otherLeases.map((item) => item.decision), - command: { - owner: request.owner, - idempotency_key: request.idempotency_key, - ttl_seconds: request.ttl_seconds, - write_scopes: [...request.write_scopes], - expected_version: request.expected_version, - }, - }); - if (decision.outcome === "no_change") { - if (existing === null) { - throw new EffectRuntimeRequestError( - "task-lease acquire decision returned replay without an existing lease", - ); - } - return successEnvelope( - request, - existing, - leasePath, - acquireEffectId(request), - true, - ); - } - if (decision.outcome !== "apply") { - if (PERMISSION_DENIED_CODES.has(decision.code)) { - throw ownerFailure(decision.code, request, todo ?? undefined); - } - const conflicts = decision.conflict_indexes.map((index) => { - const fact = otherLeases[index]; - if (fact === undefined) { - throw new EffectRuntimeRequestError( - "task-lease acquire decision returned an invalid conflict index", - ); - } - return fact.payload; - }); - throw transitionError( - decision.code, - request, - existing, - leasePath, - conflicts, - active ? "acquire_parameters" : "retired", - ); - } - if (decision.next_lease === null) { - throw new EffectRuntimeRequestError( - "task-lease acquire decision returned apply without next_lease", - ); - } - - const lease = materializeTaskLeaseAcquire(request, request, decision, at); - await dependencies.beforeWrite?.(lease); - await revalidateAuthoritySources(request.authority.source_receipts); - const captureRequired = request.runtime_shadow !== null || - await requireShadowPrimaryWriteAllowed(request.runtime_root, request.goal_id) !== null; - const shadowCapture = !captureRequired - ? null - : await beginLeaseOutboxEntry({ - runtime_root: request.runtime_root, - goal_id: request.goal_id, - lease_directory: taskLeaseDirectory(request), - write_class: "task_lease_acquire", - operation_id: request.idempotency_key, - previous_lease: existing, - planned_lease: lease, - active_todo_ids: [...request.authority.todos.keys()], - goal_ref: request.runtime_shadow !== null && "goal_ref" in request.runtime_shadow - ? request.runtime_shadow.goal_ref - : undefined, - }); - if (shadowCapture?.failure && await requireShadowPrimaryWriteAllowed(request.runtime_root, request.goal_id) !== null) { - throw new ShadowManagementError("shadow_capture_prepare_failed", "durable shadow preparation failed; the primary lease was not changed"); - } - await atomicWriteJson(leasePath, lease); - await shadowCapture?.commit(); - const response = successEnvelope(request, lease, leasePath, acquireEffectId(request), false); - if (shadowCapture !== null) { - response.coordination_runtime_shadow_capture = { - entry_id: shadowCapture.entry_id, - seq: shadowCapture.seq, - source_bytes_digest: shadowCapture.source_bytes_digest, - failure: shadowCapture.failure, - skipped_reason: shadowCapture.skipped_reason, - }; - } - return response; -} - export async function executeTaskLeaseAcquire( value: unknown, dependencies: TaskLeaseAcquireDependencies = {}, @@ -1031,7 +726,7 @@ export async function executeTaskLeaseAcquire( catch { throw new TaskLeaseAcquireError("cannot verify independent Git worktree and origin for --write-worktree", "invalid_worktree_lease_request"); } } } catch (error) { - if (error instanceof TaskLeaseAcquireError || error instanceof ShadowManagementError || error instanceof LegacyCoordinationWriteError) { + if (error instanceof TaskLeaseAcquireError || error instanceof ShadowManagementError) { return failureEnvelope( { code: error.code, message: error.message, payload: error.payload }, context, @@ -1041,39 +736,46 @@ export async function executeTaskLeaseAcquire( } try { - if (request.canonical) { - const result = await withCanonicalTaskLeaseAuthority({...request, operation: "acquire"}, { - ...dependencies, now: dependencies.now ?? (() => new Date()), - }, async (store, guards) => { - await guards.revalidate(); - return await executeCanonicalTaskLeaseAcquire(store, {...request, - registered_agents: request.authority.registered_agents, now: (dependencies.now ?? (() => new Date()))()}, guards.beforeCommit); - }); - if (["applied", "no_change", "replayed", "recovered"].includes(String(result.status))) { - const idempotent = result.status !== "applied"; - const envelope = successEnvelope(request, result.lease as LeaseRecord, "", acquireEffectId(request), idempotent); - delete envelope.lease_path; - return {...result, ...envelope, acquired: !idempotent && result.changed === true}; - } - const envelope = failureEnvelope({code: String(result.reason_code ?? result.conflict_kind ?? "canonical_acquire_failed"), - message: String(result.reason ?? "canonical acquisition failed; inspect before retrying"), - kind: result.reason_code === "legacy_writer_fence_read_failed" ? "permission_denied" : undefined, - payload: result, stage: result.failure_stage === "durable_writeback" ? "durable_writeback" : "validation"}, context); + if (!request.canonical) { + return failureEnvelope({ + code: "canonical_authority_required", + message: "Canonical authority is required; back up and migrate this Goal before acquiring a task lease", + kind: "permission_denied", + stage: "validation", + payload: { + goal_id: request.goal_id, + operation: "task_lease_acquire", + write_admitted: false, + decision_read_from_provider: false, + legacy_fallback_used: false, + recovery: { + action: "migrate_goal_authority", + backup_required: true, + }, + }, + }, context); + } + const result = await withCanonicalTaskLeaseAuthority({...request, operation: "acquire"}, { + ...dependencies, now: dependencies.now ?? (() => new Date()), + }, async (store, guards) => { + await guards.revalidate(); + return await executeCanonicalTaskLeaseAcquire(store, {...request, + registered_agents: request.authority.registered_agents, now: (dependencies.now ?? (() => new Date()))()}, guards.beforeCommit); + }); + if (["applied", "no_change", "replayed", "recovered"].includes(String(result.status))) { + const idempotent = result.status !== "applied"; + const envelope = successEnvelope(request, result.lease as LeaseRecord, "", acquireEffectId(request), idempotent); delete envelope.lease_path; - // Keep the public acquire schema even when the inner command has diagnostics. - return {...envelope, schema_version: TASK_LEASE_SCHEMA_VERSION}; + return {...result, ...envelope, acquired: !idempotent && result.changed === true}; } - return await withFileMutationLock( - legacyCoordinationLeaseLockPath(request.runtime_root, request.goal_id), - () => withFileMutationLock(taskLeaseLockPath(request), async () => { - await requireLegacyCoordinationPrimaryWriteAllowed(request.runtime_root, request.goal_id); - return await commitAcquire(request, dependencies); - }), - ); + const envelope = failureEnvelope({code: String(result.reason_code ?? result.conflict_kind ?? "canonical_acquire_failed"), + message: String(result.reason ?? "canonical acquisition failed; inspect before retrying"), + kind: result.reason_code === "legacy_writer_fence_read_failed" ? "permission_denied" : undefined, + payload: result, stage: result.failure_stage === "durable_writeback" ? "durable_writeback" : "validation"}, context); + delete envelope.lease_path; + // Keep the public acquire schema even when the inner command has diagnostics. + return {...envelope, schema_version: TASK_LEASE_SCHEMA_VERSION}; } catch (error) { - if (error instanceof LegacyCoordinationWriteError) { - return failureEnvelope(fencedFailure(error), context); - } if (error instanceof TaskLeaseAcquireError || error instanceof ShadowManagementError) { return failureEnvelope( { code: error.code, message: error.message, payload: error.payload }, 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..6c8379c994 100644 --- a/loopx/control_plane/work_items/task_lease_acquire_adapter.py +++ b/loopx/control_plane/work_items/task_lease_acquire_adapter.py @@ -3,8 +3,6 @@ from __future__ import annotations import hashlib -import os -import secrets from datetime import datetime from pathlib import Path from typing import Any @@ -12,14 +10,11 @@ from ..projects.registry_codec import load_registry from ...paths import resolve_runtime_root from ..coordination.coordination_state_contract_generated import ( - LOCAL_AUTHORITY_SHADOW_BINDING_SCHEMA, - TASK_LEASE_ACQUIRE_REQUEST_SCHEMA, TASK_LEASE_CANONICAL_ACQUIRE_REQUEST_SCHEMA, TASK_LEASE_CANONICAL_LIFECYCLE_REQUEST_SCHEMA, TASK_LEASE_CANONICAL_CLAIM_TRANSFER_REQUEST_SCHEMA, TASK_LEASE_LIFECYCLE_REQUEST_SCHEMA, ) -from ..coordination.configuration import resolve_coordination_runtime_shadow_config from ..goals.legacy_event_source import ( state_event_log_candidates as _state_event_log_candidates, ) @@ -27,7 +22,6 @@ from ..todos.handoff_mode import HANDOFF_MODE_LEGACY from .local_lease_record import TASK_LEASE_SCHEMA_VERSION, TaskLeaseError -TASK_LEASE_ACQUIRE_NATIVE_SCHEMA_VERSION = TASK_LEASE_ACQUIRE_REQUEST_SCHEMA TASK_LEASE_LIFECYCLE_NATIVE_SCHEMA_VERSION = TASK_LEASE_LIFECYCLE_REQUEST_SCHEMA TASK_LEASE_AUTHORITY_SNAPSHOT_ATTEMPTS = 3 @@ -326,37 +320,6 @@ def _canonical_lease_authority_facts(registry_path: Path, goal_id: str) -> dict[ raise TaskLeaseError("canonical lease lifecycle registry changed; retry", code="authority_source_changed") -def _finalize_native_acquire_result( - payload: dict[str, Any], - *, - authority: dict[str, Any], - registry_path: Path, - runtime_root: Path, - goal_id: str, - legacy_provider_projection: bool, -) -> dict[str, Any]: - result = dict(payload) - if legacy_provider_projection and result.get("ok") is True: - result["handoff_mode"] = authority.get("handoff_mode") or HANDOFF_MODE_LEGACY - result.pop("settlement", None) - if ( - result.get("ok") is True - and result.get("acquired") is True - and "coordination_runtime_shadow_capture" in result - ): - from ..coordination.runtime_shadow_writer_adapter import ( - settle_lease_runtime_shadow_capture, - ) - - result = settle_lease_runtime_shadow_capture( - result, - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - ) - return result - - def execute_native_task_lease_acquire( *, registry_path: Path, @@ -369,7 +332,6 @@ def execute_native_task_lease_acquire( write_scopes: list[str] | None = None, expected_version: int | None = None, write_worktree: str | None = None, - _legacy_provider_projection: bool = False, ) -> dict[str, Any]: """Transport one compact acquire request to the native TypeScript owner.""" @@ -378,17 +340,12 @@ def execute_native_task_lease_acquire( effect_runtime_result, ) - from ..coordination.local_authority import local_authority_is_promoted + from ..coordination.local_authority import raise_canonical_write_failure - canonical = local_authority_is_promoted(runtime_root=runtime_root, goal_id=str(goal_id)) for attempt in range(TASK_LEASE_AUTHORITY_SNAPSHOT_ATTEMPTS): - authority = _canonical_lease_authority_facts(registry_path, str(goal_id)) if canonical else task_lease_acquire_authority_facts( - registry_path=registry_path, - goal_id=str(goal_id or ""), - todo_id=str(todo_id or ""), - ) + authority = _canonical_lease_authority_facts(registry_path, str(goal_id)) request = { - "schema_version": TASK_LEASE_CANONICAL_ACQUIRE_REQUEST_SCHEMA if canonical else TASK_LEASE_ACQUIRE_NATIVE_SCHEMA_VERSION, + "schema_version": TASK_LEASE_CANONICAL_ACQUIRE_REQUEST_SCHEMA, "runtime_root": str(runtime_root), "goal_id": goal_id, "todo_id": todo_id, @@ -403,74 +360,32 @@ def execute_native_task_lease_acquire( "expected_version": expected_version, "authority": authority, } - registry = load_registry(registry_path) - goal = _registry_goal(registry, str(goal_id)) - if not canonical and resolve_coordination_runtime_shadow_config(goal).enabled: - request["runtime_shadow"] = { - "schema_version": LOCAL_AUTHORITY_SHADOW_BINDING_SCHEMA, - "provider": "file_v0", - } payload = _require_native_acquire_shape( effect_runtime_result( "task_lease.acquire.native", request, - timeout=(CANONICAL_AUTHORITY_WRITE_TIMEOUT_SECONDS if canonical else 15.0), + timeout=CANONICAL_AUTHORITY_WRITE_TIMEOUT_SECONDS, ) ) + raise_canonical_write_failure( + payload, + goal_id=str(goal_id), + operation="task_lease_acquire", + ) if ( payload.get("error_code") == "authority_source_changed" and attempt + 1 < TASK_LEASE_AUTHORITY_SNAPSHOT_ATTEMPTS ): continue - if canonical: - if payload.get("ok") is True and ( - payload.get("source_authority") not in ("file_v0", "sqlite_v0") - or payload.get("decision_read_from_provider") is not True - or payload.get("legacy_fallback_used") is not False - ): - raise RuntimeError("canonical acquire omitted valid provider evidence") - return payload - return _finalize_native_acquire_result( - payload, - authority=authority, - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=str(goal_id), - legacy_provider_projection=_legacy_provider_projection, - ) + if payload.get("ok") is True and ( + payload.get("source_authority") not in ("file_v0", "sqlite_v0", "postgresql_v0") + or payload.get("decision_read_from_provider") is not True + or payload.get("legacy_fallback_used") is not False + ): + raise RuntimeError("canonical acquire omitted valid provider evidence") + return payload raise RuntimeError("native task-lease acquire exhausted source-CAS retries") -def _compact_lifecycle_todo( - todo: dict[str, Any] | None, - *, - todo_id: str, -) -> dict[str, Any] | None: - if todo is None: - return None - value = dict(todo) - value["todo_id"] = todo_id - # Active-state parser rows historically expose ``done`` while the compact - # authority projection exposes ``status``. Normalize that compatibility - # shape at the transport edge; the TS owner consumes one typed field. - if "status" not in value and "done" in value: - value["status"] = "done" if value.get("done") is True else "open" - value.setdefault("excluded_agents", []) - return { - key: value[key] - for key in ( - "todo_id", - "status", - "claimed_by", - "excluded_agents", - "role", - "task_class", - "bound_agent", - "blocks_agent", - ) - if key in value - } - - def _native_lifecycle_failure( payload: dict[str, Any], *, @@ -547,7 +462,7 @@ def _settle_canonical_lifecycle_result( """Validate provider provenance and deliver the committed Todo projection.""" if ( request["operation"] not in {"renew", "transfer", "release"} - or result.get("source_authority") not in ("file_v0", "sqlite_v0") + or result.get("source_authority") not in ("file_v0", "sqlite_v0", "postgresql_v0") or result.get("decision_read_from_provider") is not True or result.get("legacy_fallback_used") is not False ): @@ -596,7 +511,6 @@ def execute_native_task_lease_lifecycle( fence_expected_lease_epoch: int | None = None, fence_operation_id: str | None = None, owner_pid: int | None = None, - _legacy_provider_projection: bool = False, _now: datetime | None = None, ) -> dict[str, Any]: """Call the native local task-lease lifecycle owner. @@ -614,24 +528,18 @@ def execute_native_task_lease_lifecycle( expected_version, operation=normalized_operation or "lifecycle", ) - needs_authority = normalized_operation in { - "renew", - "transfer", - "terminal_verify", - "holder_verify", - } - # Keyless fences have no caller-supplied execution identity. Give each - # bridge invocation a stable receipt id so a transport retry can adopt the - # same held lock, while an independent invocation gets a fresh id and - # cannot be mistaken for a replay after a prior close. - if normalized_operation in {"holder_verify", "terminal_verify"} and ( - fence_operation_id is None and idempotency_key is None - ): - fence_operation_id = secrets.token_hex(32) + if normalized_operation in {"holder_verify", "terminal_verify"}: + raise TaskLeaseError( + "separate source-backed lease verification is retired; use the canonical Todo transaction", + code="task_lease_verification_retired", + ) + needs_authority = normalized_operation in {"renew", "transfer"} for attempt in range(TASK_LEASE_AUTHORITY_SNAPSHOT_ATTEMPTS): authority: dict[str, Any] | None = None canonical_lifecycle = False - if normalized_operation in {"renew", "transfer", "release"}: + if normalized_operation in {"renew", "transfer"}: + canonical_lifecycle = True + elif normalized_operation == "release": from ..coordination.local_authority import local_authority_is_promoted canonical_lifecycle = local_authority_is_promoted( @@ -642,13 +550,9 @@ def execute_native_task_lease_lifecycle( "atomic claim transfer requires canonical authority; inspect provider readiness first", code="claim_transfer_requires_canonical_authority", ) - if registry_path is not None and (needs_authority or (normalized_operation == "release" and not canonical_lifecycle)): + if registry_path is not None and needs_authority: try: - authority = _canonical_lease_authority_facts(registry_path, goal_id) if canonical_lifecycle else task_lease_acquire_authority_facts( - registry_path=registry_path, - goal_id=str(goal_id or ""), - todo_id=str(todo_id or ""), - ) + authority = _canonical_lease_authority_facts(registry_path, goal_id) except FileNotFoundError: # Release is a cleanup operation and may run without its # optional authority projection. Every operation that can @@ -693,13 +597,7 @@ def execute_native_task_lease_lifecycle( # A held fence outlives this one-shot managed-runtime request. Its # liveness owner is the Python caller, so a crashed caller can be # reclaimed even though the shared Node server remains alive. - "owner_pid": owner_pid - if owner_pid is not None - else ( - os.getpid() - if normalized_operation in {"terminal_verify", "holder_verify"} - else None - ), + "owner_pid": owner_pid, # The normal production path leaves the clock to the managed # runtime. A caller-provided instant is an internal deterministic # test seam used by Python parity tests; it is never exposed as a @@ -716,25 +614,6 @@ def execute_native_task_lease_lifecycle( if transfer_claim: request.update(schema_version=TASK_LEASE_CANONICAL_CLAIM_TRANSFER_REQUEST_SCHEMA, transfer_claim=True) - if registry_path is not None and not canonical_lifecycle: - registry = load_registry(registry_path) - goal = _registry_goal(registry, str(goal_id)) - if resolve_coordination_runtime_shadow_config(goal).enabled: - request["runtime_shadow"] = { - "schema_version": LOCAL_AUTHORITY_SHADOW_BINDING_SCHEMA, - "provider": "file_v0", - } - if normalized_operation == "fence_close" and committed and release_lease: - # The preceding Todo write may have changed the source. - # Capture the current graph, not the terminal-verify snapshot - # or the append-retained lease directory. Default-off cleanup - # still needs no authority read. - request["authority"] = task_lease_acquire_authority_facts( - registry_path=registry_path, goal_id=goal_id, todo_id=todo_id, - ) - compacted_todo = _compact_lifecycle_todo(todo, todo_id=str(todo_id)) - if compacted_todo is not None and not canonical_lifecycle: - request["todo"] = compacted_todo from ..effect_runtime import ( CANONICAL_AUTHORITY_WRITE_TIMEOUT_SECONDS, effect_runtime_result, @@ -748,6 +627,14 @@ def execute_native_task_lease_lifecycle( ) if not isinstance(payload, dict): raise RuntimeError("native task-lease lifecycle result shape mismatch") + if canonical_lifecycle: + from ..coordination.local_authority import raise_canonical_write_failure + + raise_canonical_write_failure( + payload, + goal_id=goal_id, + operation=f"task_lease_{normalized_operation}", + ) if payload.get("schema_version") != TASK_LEASE_SCHEMA_VERSION: raise RuntimeError("native task-lease lifecycle schema mismatch") if payload.get("action") != normalized_operation: @@ -768,19 +655,6 @@ def execute_native_task_lease_lifecycle( return _settle_canonical_lifecycle_result(result, request=request, registry_path=registry_path) if authority is not None and authority.get("handoff_mode") and "handoff_mode" not in result: result["handoff_mode"] = authority["handoff_mode"] - if _legacy_provider_projection: - result.pop("settlement", None) - if "coordination_runtime_shadow_capture" in result: - from ..coordination.runtime_shadow_writer_adapter import ( - settle_lease_runtime_shadow_capture, - ) - - result = settle_lease_runtime_shadow_capture( - result, - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=str(goal_id), - ) # lock_token is an internal bridge value. Callers that need a held # fence read it from the nested native payload before redacting it. return result diff --git a/loopx/control_plane/work_items/task_lease_lifecycle.ts b/loopx/control_plane/work_items/task_lease_lifecycle.ts index 86f3c48cdb..068c74b37b 100644 --- a/loopx/control_plane/work_items/task_lease_lifecycle.ts +++ b/loopx/control_plane/work_items/task_lease_lifecycle.ts @@ -1,9 +1,8 @@ -import {leaseOwnerRejection as ownerRejection} from "./task_lease_eligibility.ts"; import {mutateCanonicalTaskLease} from "./canonical_task_lease_lifecycle.ts"; import {taskLeaseStableValue as stableValue, taskLeaseDigest as digest, taskLeaseOperationIdentity as operationIdentity, taskLeaseOperationRequestDigest as operationRequestDigest} from "./task_lease_operation_identity.ts"; -import { ShadowManagementError, requireShadowPrimaryWriteAllowed } from "../coordination/shadow_management.ts"; +import { ShadowManagementError } from "../coordination/shadow_management.ts"; import { LegacyCoordinationWriteError, requireLegacyCoordinationPrimaryWriteAllowed } from "../coordination/legacy_writer_fence.ts"; import { createHash } from "node:crypto"; import { readFile, readdir } from "node:fs/promises"; @@ -23,7 +22,6 @@ import { releaseFileMutationLock, releaseFileMutationLockClaim, withFileMutationLock, - type FileMutationLock, type FileMutationLockClaim, } from "../effect_runtime_io.ts"; import { @@ -50,7 +48,6 @@ import { taskLeaseDirectory, taskLeaseLockPath, taskLeasePath, - utcIsoformat, type AuthorityFacts, type LeaseRecord, type TodoFact, @@ -63,10 +60,6 @@ import { type TaskLeaseLifecycleDecision, type TaskLeaseLifecycleDecisionInput, } from "./task_lease_lifecycle_decision.ts"; -import { - beginLeaseOutboxEntry, - type LeaseOutboxCapture, -} from "../coordination/local_authority_shadow_outbox.ts"; import {decodeTaskLeaseLifecycleRequest, TaskLeaseLifecycleError, TASK_LEASE_LIFECYCLE_OPERATIONS, type LifecycleRequest, type CanonicalLifecycleRequest, type LifecycleErrorInfo, type LifecycleStage, type TaskLeaseLifecycleOperation} from "./task_lease_lifecycle_request.ts"; @@ -87,57 +80,6 @@ interface LifecycleDependencies { } -async function captureLeaseWrite( - request: LifecycleRequest, - previous: LeaseRecord | null, - next: LeaseRecord, - writeClass: string, -): Promise> | null> { - if (request.runtime_shadow === null && - await requireShadowPrimaryWriteAllowed(request.runtime_root, request.goal_id) === null) return null; - if (request.operation === "fence_close" && request.authority !== null) { - await revalidateAuthoritySources(request.authority.source_receipts); - } - const capture = await beginLeaseOutboxEntry({ - runtime_root: request.runtime_root, - goal_id: request.goal_id, - lease_directory: taskLeaseDirectory(request), - write_class: writeClass, - operation_id: request.idempotency_key ?? request.fence_operation_id, - previous_lease: previous, - planned_lease: next, - // Lifecycle requests may omit authority facts; absent authority keeps the - // strict pre-existing capture instead of guessing a Todo graph. - active_todo_ids: request.authority === null - ? null - : [...request.authority.todos.keys()], - goal_ref: request.runtime_shadow !== null && "goal_ref" in request.runtime_shadow - ? request.runtime_shadow.goal_ref - : undefined, - }); - if (capture.failure && await requireShadowPrimaryWriteAllowed(request.runtime_root, request.goal_id) !== null) { - throw new ShadowManagementError("shadow_capture_prepare_failed", "durable shadow preparation failed; the primary lease was not changed"); - } - return capture; -} - -function attachRuntimeShadowCapture( - response: JsonObject, - capture: LeaseOutboxCapture | null, -): JsonObject { - if (capture === null) return response; - return { - ...response, - coordination_runtime_shadow_capture: { - entry_id: capture.entry_id, - seq: capture.seq, - source_bytes_digest: capture.source_bytes_digest, - failure: capture.failure, - skipped_reason: capture.skipped_reason, - }, - }; -} - function lifecycleNow( request: Pick, dependencies: LifecycleDependencies, @@ -446,31 +388,6 @@ async function persistFenceReceipt( }); } -function fenceLockHandle( - request: LifecycleRequest, - token: string, -): FileMutationLock { - const targetPath = lockPathFor(request); - return { - targetPath, - lockPath: `${targetPath}.ts-effect.lock`, - token, - }; -} - -async function adoptFenceReceiptLock( - request: LifecycleRequest, - receipt: FenceReceipt | null, -): Promise { - if (!receipt || receipt.state === "closed") return null; - const owner = await mutationLockOwner(lockPathFor(request)); - if (!owner || owner.token !== receipt.lock_token) return null; - const expectedPid = request.owner_pid ?? process.pid; - if (owner.pid !== expectedPid || receipt.owner_pid !== expectedPid) return null; - return fenceLockHandle(request, receipt.lock_token); -} - - async function findFenceReceiptByToken( request: LifecycleRequest, ): Promise<{ path: string; receipt: FenceReceipt } | null> { @@ -1059,18 +976,6 @@ function plannedLeaseMatches(actual: LeaseRecord | null, planned: LeaseRecord | return JSON.stringify(stableValue(actual)) === JSON.stringify(stableValue(planned)); } -function allowsUserGateAutoAcquire( - request: LifecycleRequest, - todo: TodoFact | null, -): boolean { - return request.operation === "terminal_verify" && - request.authority?.handoff_mode === "hard_lease" && - !request.delegated_authority && - request.allow_user_gate_auto_acquire && - todo?.role === "user" && - todo.task_class === "user_gate"; -} - async function loadLeaseForRequest( request: LifecycleRequest, leasePath: string, @@ -1271,13 +1176,9 @@ async function ordinaryOperation( const response = responseForOrdinary(request, leasePath, next, false, { released: true }); await persistOperationReceipt(request, "prepared", next, response); await dependencies.beforeWrite?.(next); - const shadowCapture = await captureLeaseWrite( - request, existing, next, "task_lease_release", - ); await atomicWriteJson(leasePath, next); - await shadowCapture?.commit(); await persistOperationReceipt(request, "committed", next, response); - return attachRuntimeShadowCapture(response, shadowCapture); + return response; } if (existing === null || decision.next_lease === null) { throw transitionError(request, "invalid_lease_snapshot", existing, leasePath); @@ -1298,663 +1199,11 @@ async function ordinaryOperation( await persistOperationReceipt(request, "prepared", next, response); await dependencies.beforeWrite?.(next); if (request.authority) await revalidateAuthoritySources(request.authority.source_receipts); - const shadowCapture = await captureLeaseWrite( - request, existing, next, `task_lease_${request.operation}`, - ); await atomicWriteJson(leasePath, next); - await shadowCapture?.commit(); await persistOperationReceipt(request, "committed", next, response); - return attachRuntimeShadowCapture(response, shadowCapture); + return response; } -function fencePayload( - request: LifecycleRequest, - lease: LeaseRecord, - token: string, - autoAcquired = false, -): JsonObject { - return { - schema_version: "task_lease_v0", - required: true, - active: true, - owner: request.owner, - version: leaseVersion(lease), - lease_epoch: leaseEpoch(lease), - execution_instance_verified: true, - lock_token: token, - fence_operation_id: fenceOperationId(request), - ...(autoAcquired ? { auto_acquired: true } : {}), - }; -} - -async function fenceVerify( - request: LifecycleRequest, - dependencies: LifecycleDependencies, -): Promise { - if (!request.authority) { - throw new TaskLeaseLifecycleError("authority is required for a fence", "authority_required"); - } - const operationId = fenceOperationId(request); - const receiptPath = fenceReceiptPath(request, operationId); - let keep = false; - let recheckingClosedNoop = false; - let receipt = await readFenceReceipt(receiptPath, request); - let lock: FileMutationLock | null = null; - let lockClaim: FileMutationLockClaim | null = null; - try { - // A closed receipt is terminal only relative to the lease generation that - // produced it. Inspect the current lease while holding the same goal - // lock before replaying it; otherwise an aborted close or a later acquire - // could turn an old required fence into a keyless success. - if (receipt?.state === "closed") { - // The receipt write is authoritative even if the original handler lost - // its response or failed during best-effort lock cleanup. Retire only - // this receipt's token before taking a fresh replay lock; an in-flight - // closer that already owns the token claim wins and keeps the lock. - await releaseFileMutationLock( - lockPathFor(request), - receipt.lock_token, - null, - true, - ); - const replayLock = await acquireFileMutationLock( - lockPathFor(request), - request.owner_pid ?? process.pid, - ); - try { - await revalidateAuthoritySources(request.authority.source_receipts); - const closedTodo = authorityTodo(request); - if (!closedTodo) { - throw new TaskLeaseLifecycleError( - "todo is missing from the canonical projection", - "todo_not_found", - { goal_id: request.goal_id, todo_id: request.todo_id }, - ); - } - // Preserve the holder gate's current mode boundary even though the - // receipt is terminal. A previous proof must not authorize a later - // soft-claim ownership mutation. - if (request.operation === "holder_verify" && - request.authority.handoff_mode !== "hard_lease") { - throw new TaskLeaseLifecycleError( - "task-lease holder verification is only available in hard_lease handoff mode", - "handoff_mode_forbids_lease", - { - goal_id: request.goal_id, - todo_id: request.todo_id, - handoff_mode: request.authority.handoff_mode, - }, - ); - } - const verifiedOwner = receipt.fence_owner; - if (verifiedOwner !== null && ( - !request.authority.registered_agents.includes(verifiedOwner) || - closedTodo.excluded_agents.includes(verifiedOwner) || - (closedTodo.claimed_by !== null && - closedTodo.claimed_by !== verifiedOwner) - )) { - throw new TaskLeaseLifecycleError( - "task lease fence receipt owner no longer matches the canonical todo authority", - "authority_todo_mismatch", - { goal_id: request.goal_id, todo_id: request.todo_id, fence_owner: verifiedOwner }, - ); - } - - const currentLease = await loadLeaseForRequest( - request, - leasePathFor(request), - ); - const nonRequired = receipt.response?.required === false && - receipt.response.active === false; - if (nonRequired) { - // A no-op receipt is replayable only while no lease record exists. - // If a lease was acquired after the no-op, run the normal fence - // rules again so hard-lease/CAS requirements cannot be bypassed. - if ( - currentLease === null && - (request.authority.handoff_mode !== "hard_lease" || - request.delegated_authority) - ) { - const { lock_token: _retiredToken, ...publicReplay } = receipt.response!; - return { - ...publicReplay, - required: false, - active: false, - closed: true, - fence_operation_id: operationId, - idempotent: true, - }; - } - if (currentLease === null && - request.authority.handoff_mode === "hard_lease" && - !request.delegated_authority) { - // Re-evaluate the stricter authority without replacing a valid - // legacy no-op receipt with a transient acquired receipt. The - // same execution may return to legacy mode after a failed hard - // lease attempt and must still be able to replay its durable ACK. - throw new TaskLeaseLifecycleError( - "hard_lease handoff mode requires an effective task lease to complete this todo; acquire one with `loopx task-lease acquire`", - "handoff_mode_requires_lease", - { - goal_id: request.goal_id, - todo_id: request.todo_id, - handoff_mode: request.authority.handoff_mode, - lease_path: leasePathFor(request), - }, - "validation", - ); - } - // Keep the replay marker so the locked re-read below can continue - // with the normal lease/CAS checks instead of recursively replaying - // the same closed no-op receipt forever. - recheckingClosedNoop = currentLease !== null; - receipt = null; - } else { - const closeReleased = receipt.response?.action === "fence_close" && - receipt.response.released === true; - const tupleMatches = currentLease !== null && - receipt.lease !== null && - fenceLeaseMatchesReceipt(currentLease, receipt); - - if (closeReleased) { - // A committed close that released the lease is replayable only - // against the exact retired generation. A missing or newer - // generation is an ABA/stale-fence conflict, never a no-op. - if (currentLease === null || - currentLease.status !== "released" || - !tupleMatches || - !plannedLeaseMatches(currentLease, receipt.lease)) { - throw new TaskLeaseLifecycleError( - "closed task lease fence receipt does not match the current retired lease generation", - "fence_state_invalid", - { - goal_id: request.goal_id, - todo_id: request.todo_id, - lease_path: leasePathFor(request), - expected_version: receipt.fence_expected_version, - expected_lease_epoch: receipt.fence_expected_lease_epoch, - actual_version: currentLease ? leaseVersion(currentLease) : null, - actual_lease_epoch: currentLease ? leaseEpoch(currentLease) : null, - }, - "validation", - ); - } - const original = receipt.verify_response ?? ( - receipt.response?.action === request.operation ? receipt.response : null - ); - if (!original) { - throw fenceReceiptError( - receiptPath, - "closed task lease fence receipt omitted its verification response", - ); - } - const { lock_token: _retiredToken, ...publicReplay } = original; - return { - ...publicReplay, - required: false, - active: false, - closed: true, - fence_operation_id: operationId, - idempotent: true, - }; - } - - // An aborted/non-releasing close leaves the lease active. It is - // safe to retry the verification under a newly acquired lock only - // when the same verified generation is still present. A different - // generation must fail closed and force a fresh fence identity. - if (currentLease !== null && - currentLease.status === "active" && - tupleMatches) { - receipt = null; - } else { - throw new TaskLeaseLifecycleError( - "closed task lease fence receipt is stale or has no retryable lease generation", - "fence_state_invalid", - { - goal_id: request.goal_id, - todo_id: request.todo_id, - lease_path: leasePathFor(request), - expected_version: receipt.fence_expected_version, - expected_lease_epoch: receipt.fence_expected_lease_epoch, - actual_version: currentLease ? leaseVersion(currentLease) : null, - actual_lease_epoch: currentLease ? leaseEpoch(currentLease) : null, - }, - "validation", - ); - } - } - } finally { - await releaseFileMutationLock( - replayLock.targetPath, - replayLock.token, - null, - true, - ); - } - } - const adoptedLock = await adoptFenceReceiptLock(request, receipt); - const acquiredFreshLock = adoptedLock === null; - lock = adoptedLock ?? await acquireFileMutationLock( - lockPathFor(request), - request.owner_pid ?? process.pid, - ); - try { - lockClaim = await claimFileMutationLock(lock.targetPath, lock.token); - } catch (error) { - // An adopted lock belongs to the execution that published its receipt; - // if claiming it fails, leave that owner's lock untouched. - if (!acquiredFreshLock) lock = null; - throw error; - } - if (!lockClaim) { - if (acquiredFreshLock) { - await releaseFileMutationLock(lock.targetPath, lock.token, null, true); - lock = null; - } else { - // Another verifier currently owns the token claim. Do not release its - // adopted lock; report the contention to the caller instead. - lock = null; - } - throw new EffectRuntimeConflictError( - "task lease fence token is already being verified", - "fence_token_invalid", - ); - } - // A token claim serializes operations that name this token, but it does - // not prove that the lock pathname still names the same token. The lock - // may have been closed and replaced between adoption and claim creation. - // Re-check ownership while holding the claim before trusting or writing - // any receipt state. - const claimedOwner = await mutationLockOwner(lock.targetPath); - if (!claimedOwner || claimedOwner.token !== lock.token) { - await releaseFileMutationLockClaim(lockClaim); - lockClaim = null; - if (acquiredFreshLock) { - await releaseFileMutationLock(lock.targetPath, lock.token, null, true); - } - lock = null; - throw new EffectRuntimeConflictError( - "task lease fence token is no longer held", - "fence_token_invalid", - ); - } - // The first receipt read happened before lock acquisition or adoption. - // Re-read it while holding both the lock and its token claim so a verifier - // that waited behind another request cannot overwrite a held or closed - // receipt. - const lockedReceipt = await readFenceReceipt(receiptPath, request); - if (lockedReceipt?.state === "closed") { - // A closed receipt can be retried only for an aborted, non-releasing - // close whose exact active generation is still present. Otherwise - // leave it for the normal closed-replay validator below, which either - // returns the retired result or fails closed on an ABA transition. - const lockedLease = await loadLeaseForRequest( - request, - leasePathFor(request), - ); - const retryableClosed = lockedReceipt.response?.action === "fence_close" && - lockedReceipt.response.released !== true && - lockedLease !== null && - lockedLease.status === "active" && - fenceLeaseMatchesReceipt(lockedLease, lockedReceipt); - const lockedNonRequired = lockedReceipt.response?.required === false && - lockedReceipt.response.active === false; - if (!retryableClosed && - !(recheckingClosedNoop && lockedNonRequired && lockedLease !== null)) { - receipt = lockedReceipt; - if (acquiredFreshLock) { - await releaseFileMutationLock(lock.targetPath, lock.token, lockClaim, true); - } else { - await releaseFileMutationLockClaim(lockClaim); - } - lockClaim = null; - lock = null; - return await fenceVerify(request, dependencies); - } - receipt = retryableClosed ? lockedReceipt : null; - } else { - receipt = lockedReceipt; - } - // Every legacy verify branch (held replay, holder verification, user-gate - // auto-acquire) persists a receipt; the promoted authority's fence is - // checked once here, before that first side effect. - await requireLegacyCoordinationPrimaryWriteAllowed(request.runtime_root, request.goal_id); - const initialTodo = authorityTodo(request); - // Publish the token before semantic validation. If the response is lost - // after acquisition, a retry can adopt this exact lock and finish the - // validation instead of waiting for a lock timeout. - if ((acquiredFreshLock && (!receipt || receipt.state === "acquired")) || - !receipt) { - await persistFenceReceipt(request, "acquired", lock.token, { - operationId, - lease: null, - response: null, - ownerPid: request.owner_pid ?? process.pid, - fenceOwner: request.owner, - // Record the auto-acquire intent before the lease write. If the - // caller loses its response in the write window, a retry can - // identify the lease it already minted instead of acquiring a - // second one. - fenceIdempotencyKey: allowsUserGateAutoAcquire(request, initialTodo) - ? request.idempotency_key ?? `auto-${request.todo_id}` - : request.idempotency_key, - fenceExpectedVersion: request.expected_version, - base: receipt, - }); - receipt = await readFenceReceipt(receiptPath, request); - } - if (!lock) { - throw new TaskLeaseLifecycleError( - "task lease fence lock could not be established", - "fence_lock_unavailable", - ); - } - // Authority is part of the replay boundary. A fence receipt is a - // durable execution record, not a bypass around the current canonical - // projection. Re-read and revalidate it before returning any replay. - await revalidateAuthoritySources(request.authority.source_receipts); - const todo = authorityTodo(request); - if (!todo) { - throw new TaskLeaseLifecycleError( - "todo is missing from the canonical projection", - "todo_not_found", - { goal_id: request.goal_id, todo_id: request.todo_id }, - ); - } - const leasePath = leasePathFor(request); - const at = lifecycleNow(request, dependencies); - const rawLease = await readLease(leasePath); - let lease = rawLease === null - ? null - : validatePersistedLease(rawLease, request, leasePath); - const active = leaseIsActive(lease, at); - const effective = active && lease !== null && ownerRejection( - todo, - persistedOwner(lease), - request.authority.registered_agents, - ) === null; - - if (receipt?.state === "held" && receipt.response) { - if ( - lease && - receipt.lease && - plannedLeaseMatches(lease, receipt.lease) && - (await mutationLockOwner(lockPathFor(request)))?.token === lock.token - ) { - const verifiedOwner = receipt.fence_owner ?? persistedOwner(lease); - if (!request.authority.registered_agents.includes(verifiedOwner) || - todo.excluded_agents.includes(verifiedOwner) || - (todo.claimed_by !== null && todo.claimed_by !== verifiedOwner)) { - throw new TaskLeaseLifecycleError( - "task lease fence receipt owner no longer matches the canonical todo authority", - "authority_todo_mismatch", - { goal_id: request.goal_id, todo_id: request.todo_id, fence_owner: verifiedOwner }, - ); - } - if (request.operation === "holder_verify") { - if (request.authority.handoff_mode !== "hard_lease" || - request.owner !== verifiedOwner || - !active || - ownerRejection( - todo, - verifiedOwner, - request.authority.registered_agents, - ) !== null) { - throw new TaskLeaseLifecycleError( - "task lease holder verification authority no longer matches the canonical lease projection", - "authority_todo_mismatch", - { goal_id: request.goal_id, todo_id: request.todo_id, fence_owner: verifiedOwner }, - ); - } - } else if (!request.delegated_authority && (!active || !effective)) { - throw new TaskLeaseLifecycleError( - "task lease fence authority no longer permits this verification", - "handoff_mode_lease_claim_divergence", - { goal_id: request.goal_id, todo_id: request.todo_id, fence_owner: verifiedOwner }, - ); - } - keep = true; - const replayResponse = { - ...receipt.response, - lock_token: lock.token, - fence_operation_id: operationId, - idempotent: true, - }; - // A held receipt can be replayed by a different caller process after - // the original owner PID is gone. The fresh lock is the new - // transferable capability, so publish its token before returning it. - if (receipt.lock_token !== lock.token || receipt.owner_pid !== (request.owner_pid ?? process.pid)) { - await persistFenceReceipt(request, "held", lock.token, { - operationId, - lease, - response: replayResponse, - verifyResponse: replayResponse, - ownerPid: request.owner_pid ?? process.pid, - fenceOwner: verifiedOwner, - fenceIdempotencyKey: receipt.fence_idempotency_key, - fenceExpectedVersion: receipt.fence_expected_version, - fenceExpectedLeaseEpoch: receipt.fence_expected_lease_epoch, - base: receipt, - }); - } - return replayResponse; - } - } - if (request.operation === "holder_verify") { - if (request.authority.handoff_mode !== "hard_lease") { - throw new TaskLeaseLifecycleError( - "task-lease holder verification is only available in hard_lease handoff mode", - "handoff_mode_forbids_lease", - { goal_id: request.goal_id, todo_id: request.todo_id, handoff_mode: request.authority.handoff_mode }, - ); - } - if (!request.owner) throw new TaskLeaseLifecycleError("hard_lease handoff mode requires an attributed actor for ownership changes; provide --agent-id", "handoff_mode_requires_lease", {}, "validation"); - if (!active || !lease) { - throw new TaskLeaseLifecycleError( - "hard_lease handoff mode requires a time-active task lease before ownership of an existing todo can change; acquire one with `loopx task-lease acquire`", - "handoff_mode_requires_lease", - { goal_id: request.goal_id, todo_id: request.todo_id, reason: "no_active_lease", lease_path: leasePath }, - ); - } - if (persistedOwner(lease) !== request.owner) { - throw new TaskLeaseLifecycleError( - `hard_lease handoff mode: actor '${request.owner}' does not own the time-active task lease held by '${lease.owner}'`, - "handoff_mode_requires_lease", - { goal_id: request.goal_id, todo_id: request.todo_id, reason: "lease_owner_mismatch", lease_owner: lease.owner, lease_version: lease.version, lease_epoch: leaseEpoch(lease), expires_at: lease.expires_at, lease_path: leasePath }, - ); - } - const response = { - schema_version: "task_lease_v0", - checked: true, - active: true, - owner: persistedOwner(lease), - version: leaseVersion(lease), - lease_epoch: leaseEpoch(lease), - lock_token: lock.token, - fence_operation_id: operationId, - }; - await persistFenceReceipt(request, "held", lock.token, { - operationId, - lease, - response, - ownerPid: request.owner_pid ?? process.pid, - fenceOwner: persistedOwner(lease), - fenceExpectedVersion: leaseVersion(lease), - fenceExpectedLeaseEpoch: leaseEpoch(lease), - base: receipt, - }); - keep = true; - return response; - } - - const explicitFence = request.idempotency_key !== null || request.expected_version !== null; - const autoAcquire = allowsUserGateAutoAcquire(request, todo); - if (autoAcquire && !effective && !active) { - if (!request.owner) { - throw new TaskLeaseLifecycleError("hard_lease handoff mode auto-acquire requires an attributed actor; provide --agent-id", "handoff_mode_requires_lease", { goal_id: request.goal_id, todo_id: request.todo_id, reason: "missing_actor", lease_path: leasePath }); - } - const rejection = ownerRejection(todo, request.owner, request.authority.registered_agents); - if (rejection) throw ownerError(request, rejection, todo); - const now = utcIsoformat(at); - lease = { - schema_version: "task_lease_v0", - goal_id: request.goal_id, - todo_id: request.todo_id, - owner: request.owner, - idempotency_key: request.idempotency_key ?? `auto-${request.todo_id}`, - write_scopes: [], - acquire_ttl_seconds: 2700, - version: leaseVersion(lease) + 1, - lease_epoch: leaseEpoch(lease) + 1, - acquired_at: now, - updated_at: now, - expires_at: utcIsoformat(new Date(at.valueOf() + 2700 * 1_000)), - status: "active", - }; - await dependencies.beforeWrite?.(lease); - await revalidateAuthoritySources(request.authority.source_receipts); - const shadowCapture = await captureLeaseWrite( - request, rawLease, lease, "task_lease_auto_acquire", - ); - await atomicWriteJson(leasePath, lease); - await shadowCapture?.commit(); - const response = fencePayload( - { ...request, owner: request.owner }, - lease, - lock.token, - true, - ); - await persistFenceReceipt(request, "held", lock.token, { - operationId, - lease, - response, - ownerPid: request.owner_pid ?? process.pid, - fenceOwner: request.owner, - fenceIdempotencyKey: lease.idempotency_key as string, - fenceExpectedVersion: leaseVersion(lease), - fenceExpectedLeaseEpoch: leaseEpoch(lease), - base: receipt, - }); - keep = true; - return attachRuntimeShadowCapture(response, shadowCapture); - } - // A response can be lost after an auto-acquire write but before the - // receipt reaches the caller. The durable fence receipt proves that the - // same execution instance already minted this lease, so recover it under - // the adopted lock instead of treating the active lease as a divergence. - const recoveredAutoAcquire = request.operation === "terminal_verify" && - receipt?.state !== "closed" && - receipt?.fence_idempotency_key === `auto-${request.todo_id}` && - receipt?.fence_owner !== null && - lease !== null && - persistedOwner(lease) === receipt.fence_owner && - persistedKey(lease) === receipt.fence_idempotency_key && - lease.status === "active"; - if (recoveredAutoAcquire) { - // Keep the narrowing explicit: `lease` is mutable in the surrounding - // validation flow, so TypeScript cannot retain the conjunction above. - if (lease === null || receipt === null) { - throw new TaskLeaseLifecycleError( - "task lease auto-acquire receipt is missing its lease snapshot", - "fence_state_invalid", - ); - } - const response = fencePayload( - { ...request, owner: persistedOwner(lease) }, - lease, - lock.token, - true, - ); - await persistFenceReceipt(request, "held", lock.token, { - operationId, - lease, - response, - ownerPid: request.owner_pid ?? process.pid, - fenceOwner: persistedOwner(lease), - fenceIdempotencyKey: persistedKey(lease), - fenceExpectedVersion: leaseVersion(lease), - fenceExpectedLeaseEpoch: leaseEpoch(lease), - base: receipt, - }); - keep = true; - return response; - } - if (!effective) { - if (request.authority.handoff_mode === "hard_lease" && !request.delegated_authority) { - throw new TaskLeaseLifecycleError( - active ? "hard_lease handoff mode found a time-active lease diverged from the todo projection; repair ownership (release or transfer the lease, or restore claimed_by) before completing." : "hard_lease handoff mode requires an effective task lease to complete this todo; acquire one with `loopx task-lease acquire`", - active ? "handoff_mode_lease_claim_divergence" : "handoff_mode_requires_lease", - { goal_id: request.goal_id, todo_id: request.todo_id, handoff_mode: request.authority.handoff_mode, lease_owner: lease?.owner, lease_version: lease?.version, lease_epoch: lease ? leaseEpoch(lease) : 0, expires_at: lease?.expires_at, lease_path: leasePath }, - ); - } - if (explicitFence && request.require_active_when_fence_supplied) { - throw new TaskLeaseLifecycleError("task lease fence was supplied but no effective lease is active", "lease_not_active", { goal_id: request.goal_id, todo_id: request.todo_id, lease_path: leasePath }); - } - const response = { - schema_version: "task_lease_v0", - required: false, - active: false, - fence_operation_id: operationId, - }; - await persistFenceReceipt(request, "closed", lock.token, { - operationId, - lease: null, - response, - ownerPid: request.owner_pid ?? process.pid, - base: receipt, - }); - return response; - } - if (!lease) throw new TaskLeaseLifecycleError("task lease fence state is invalid", "invalid_lease_snapshot"); - if (!request.idempotency_key) throw new TaskLeaseLifecycleError("todo has an active task lease; lifecycle mutation requires its idempotency key", "lease_fence_required", { goal_id: request.goal_id, todo_id: request.todo_id, lease_owner: lease.owner, lease_version: lease.version, lease_epoch: leaseEpoch(lease), lease_path: leasePath }); - if (persistedOwner(lease) !== request.owner || persistedKey(lease) !== request.idempotency_key) throw new TaskLeaseLifecycleError("task lease owner or execution-instance key mismatch", "lease_cas_mismatch", { goal_id: request.goal_id, todo_id: request.todo_id, lease_owner: lease.owner, lease_version: leaseVersion(lease), lease_epoch: leaseEpoch(lease), actor_agent_id: request.owner, lease_path: leasePath }); - if (request.expected_version === null) throw new TaskLeaseLifecycleError("task lease terminal fence requires the current lease version", "version_required", { action: "terminal_verify" }); - if (leaseVersion(lease) !== request.expected_version) throw new TaskLeaseLifecycleError(`lease version mismatch: expected ${request.expected_version}, got ${leaseVersion(lease)}`, "version_mismatch", { expected_version: request.expected_version, actual_version: leaseVersion(lease) }); - const response = fencePayload(request, lease, lock.token); - await persistFenceReceipt(request, "held", lock.token, { - operationId, - lease, - response, - ownerPid: request.owner_pid ?? process.pid, - fenceOwner: persistedOwner(lease), - fenceIdempotencyKey: persistedKey(lease), - fenceExpectedVersion: leaseVersion(lease), - fenceExpectedLeaseEpoch: leaseEpoch(lease), - base: receipt, - }); - keep = true; - return response; - } finally { - // A non-required fence is still a lock-scoped read. Release the native - // lock on every return and validation failure; only a returned held fence - // transfers ownership to the caller for a later fence_close. - if (lockClaim) { - if (!keep && lock) { - await releaseFileMutationLock( - lock.targetPath, - lock.token, - lockClaim, - true, - ); - lockClaim = null; - lock = null; - } else { - try { - await releaseFileMutationLockClaim(lockClaim); - } catch { - // Claim cleanup is best effort; the owner PID/token remain - // reclaimable if this process is interrupted during cleanup. - } - lockClaim = null; - } - } - if (!keep && lock) { - await releaseFileMutationLock(lock.targetPath, lock.token, null, true); - lock = null; - } - } -} async function replayClosedFenceClose( request: LifecycleRequest, @@ -2177,7 +1426,6 @@ async function fenceClose( if (!claimedOwner || claimedOwner.token !== request.lock_token) { throw new EffectRuntimeConflictError("task lease fence token is no longer held", "fence_token_invalid"); } - let shadowCapture: LeaseOutboxCapture | null = null; if (request.committed && request.release_lease) { await requireLegacyCoordinationPrimaryWriteAllowed(request.runtime_root, request.goal_id); const at = lifecycleNow(request, dependencies); @@ -2252,11 +1500,7 @@ async function fenceClose( if (!alreadyReleased) { const next = releasedLease(lease, at); await dependencies.beforeWrite?.(next); - shadowCapture = await captureLeaseWrite( - request, lease, next, "task_lease_fence_close", - ); await atomicWriteJson(leasePath, next); - await shadowCapture?.commit(); released = true; } } @@ -2282,7 +1526,7 @@ async function fenceClose( fenceExpectedLeaseEpoch: request.fence_expected_lease_epoch ?? receipt?.fence_expected_lease_epoch, closeRequestDigest: fenceCloseRequestDigest(request), }); - return attachRuntimeShadowCapture(response, shadowCapture); + return response; } catch (error) { // A changed authority source is a retryable precondition, not a lost // fence: the lease write never ran and the caller still holds this token. @@ -2354,19 +1598,32 @@ export async function executeTaskLeaseLifecycle( const request = decoded.request; if (request.operation === "fence_close") return await fenceClose(request, dependencies); if (request.operation === "terminal_verify" || request.operation === "holder_verify") { - const fence = await fenceVerify(request, dependencies); - const capture = fence.coordination_runtime_shadow_capture; - delete fence.coordination_runtime_shadow_capture; - return { - ok: true, - schema_version: "task_lease_v0", - action: request.operation, - fence, - settlement: lifecycleSettlement(request, "committed"), - ...(capture === undefined - ? {} - : { coordination_runtime_shadow_capture: capture }), - }; + throw new TaskLeaseLifecycleError( + "Separate source-backed lease verification is retired; use the canonical Todo transaction", + "task_lease_verification_retired", + { + action: request.operation, + write_admitted: false, + legacy_fallback_used: false, + }, + ); + } + if (request.operation !== "release") { + throw new TaskLeaseLifecycleError( + "Canonical authority is required; back up and migrate this Goal before mutating a task lease", + "canonical_authority_required", + { + goal_id: request.goal_id, + operation: `task_lease_${request.operation}`, + write_admitted: false, + decision_read_from_provider: false, + legacy_fallback_used: false, + recovery: { + action: "migrate_goal_authority", + backup_required: true, + }, + }, + ); } return await withFileMutationLock( legacyCoordinationLeaseLockPath(request.runtime_root, request.goal_id), diff --git a/loopx/control_plane/work_items/team_plan.ts b/loopx/control_plane/work_items/team_plan.ts index f8affc1793..5099909b3d 100644 --- a/loopx/control_plane/work_items/team_plan.ts +++ b/loopx/control_plane/work_items/team_plan.ts @@ -133,7 +133,7 @@ export function replayTeamTransaction(request: JsonObject, previous: JsonObject) lane_settlements: (result.lane_settlements as JsonObject[]).map(lane => ({...lane, disposition: "reused"}))}; } -/** Pure whole-batch planner shared by legacy Markdown and AuthorityStore. */ +/** Pure whole-batch planner for AuthorityStore commits and historical receipt replay. */ export function planTeamTransaction(value: unknown): JsonObject { const request = requireJsonObject(value, "team transaction"); const identity = teamTransactionIdentity(request); diff --git a/loopx/control_plane/work_items/team_plan_adapter.py b/loopx/control_plane/work_items/team_plan_adapter.py index 714cd603de..7b4b833dfc 100644 --- a/loopx/control_plane/work_items/team_plan_adapter.py +++ b/loopx/control_plane/work_items/team_plan_adapter.py @@ -1,8 +1,7 @@ """Storage adapters for the typed team-plan transaction. -Legacy records and their operation receipt share one atomic Markdown write. -Promoted goals use the existing AuthorityStore transaction and projection outbox. -Only the unpromoted branch loads capture and Markdown editing dependencies. +Canonical Goals use the existing AuthorityStore transaction and projection outbox. +Pre-cutoff Markdown receipts remain read-only historical results. Source locks and historical receipt readers remain shared IO. Neither adapter grants receiver adoption or execution authority. """ @@ -24,9 +23,13 @@ ) from ...registry import find_registry_goal from ...state_refresh import now_local -from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result +from ..effect_runtime import effect_runtime_result from ..runtime.public_safety import validate_public_safe_value -from ..coordination.local_authority import read_canonical_todos_if_promoted +from ..coordination.local_authority import ( + raise_canonical_write_failure, + read_canonical_todos_if_promoted, + require_canonical_authority, +) from ...paths import effective_runtime_root from ..coordination.legacy_writer_fence import legacy_coordination_todo_lock_path from ..coordination.shadow_management import shadow_maintenance_lock_target @@ -42,18 +45,6 @@ def __init__(self, code: str, message: str, current_fingerprint: str | None = No self.current_fingerprint = current_fingerprint -def write_captured_todo_state(capture: Any, *, runtime_root: Path, goal_id: str, - state_path: Path, text: str) -> None: - """Keep the legacy batch IO seam available without importing its producer. - - Existing failure/recovery callers instrument this seam; canonical commits - never call it. The capture owner still controls preparation and durability. - """ - from ..coordination.runtime_shadow_writer_adapter import write_captured_todo_state as write - - write(capture, runtime_root=runtime_root, goal_id=goal_id, state_path=state_path, text=text) - - def team_plan_state_fingerprint( *, registry_path: Path, goal_id: str, basis_agent_id: str | None, ) -> str: @@ -96,9 +87,11 @@ def team_plan_state_fingerprint( state_digest = None canonical = read_canonical_todos_if_promoted( runtime_root=effective_runtime_root(registry_path, None), goal_id=goal_id) + if canonical is None: + require_canonical_authority(goal_id=goal_id, operation="team_plan") payload = { "registry": registry_digest, - "provider_revision": canonical.get("provider_revision") if canonical else None, + "provider_revision": canonical.get("provider_revision"), "goal_id": goal_id, "active_state": state_digest, "intent_basis": steward_team_plan_source_basis( @@ -184,109 +177,51 @@ def previous_receipt(original: str) -> dict[str, Any] | None: return json.loads(base64.b64decode(matches[0], validate=True)) if matches else None canonical = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=goal_id) - if canonical is not None: - targets = ( - shadow_maintenance_lock_target(runtime, goal_id), - legacy_coordination_todo_lock_path(runtime_root=runtime, goal_id=goal_id), - state, registry_path, - ) - with ExitStack() as guards: - for target in targets[:-1]: - guards.enter_context(exclusive_cross_runtime_file_lock(target, operation="team_plan_commit")) - try: - # Registry administration can hold R before T/S. Like promotion, - # never wait for R while holding source locks in reverse order. - guards.enter_context(exclusive_cross_runtime_file_lock( - registry_path, operation="team_plan_commit", timeout_seconds=0)) - except LockAcquireTimeoutError: - raise TeamPlanCommitError("team_plan_commit_failed", - "team plan registry is busy; retry the original confirmation") from None - read_source_request() - # Pre-promotion receipts remain historical facts; never fall back - # to a legacy writer for a promoted Goal. - previous = previous_receipt(state.read_text(encoding="utf-8")) - if previous is not None: - return dict(effect_runtime_result("work_items.team_plan.plan", { - **request, "previous_receipt": previous})["result"]) - response = effect_runtime_result("work_items.team_plan.commit", { - **request, "runtime_root": str(runtime.resolve()), - "expected_provider_revision": canonical["provider_revision"], - "current_state_fingerprint": read_fingerprint() if read_fingerprint else None, - "source": { - "registry": str(registry_path.resolve()), "state_file": str(state.resolve()), - "registry_sha256": hashlib.sha256(registry_path.read_bytes()).hexdigest(), - "state_sha256": hashlib.sha256(state.read_bytes()).hexdigest(), - }, - "locks": [cross_runtime_lock_witness(target) for target in targets], - }) - if response.get("status") not in {"applied", "recovered", "replayed", "no_change"}: - raise TeamPlanCommitError(str(response.get("reason_code") or "team_plan_commit_failed"), - str(response.get("reason") or "team plan commit could not be verified"), - read_fingerprint() if read_fingerprint else None) - projected = settle_canonical_todo_projection(dict(response), registry_path=registry_path, - runtime_root=runtime, goal_id=goal_id, project=project, state_file=state) - if projected.get("projection_delivery") == "pending": - raise TeamPlanCommitError("team_plan_projection_pending", "Tasks committed; display readback is pending") - result = dict(response["result"]) - result["projection_delivery"] = projected.get("projection_delivery") - return result - - # Only unpromoted Goals need the source writer and capture producer. Keep - # canonical replay and projection independent of this compatibility path. - from ..coordination.legacy_writer_fence import legacy_todo_write_transaction - from ..coordination.runtime_shadow_writer_adapter import ( - begin_todo_runtime_shadow_capture, settle_todo_runtime_shadow_capture, - ) - from ..runtime.document_io import verify_state_text_durable - from ..todos.active_state_editing import ( - insert_into_existing_section, insert_new_section, section_bounds, - replace_updated_at, + if canonical is None: + require_canonical_authority(goal_id=goal_id, operation="team_plan") + targets = ( + shadow_maintenance_lock_target(runtime, goal_id), + legacy_coordination_todo_lock_path(runtime_root=runtime, goal_id=goal_id), + state, registry_path, ) - from ..todos.contract import format_todo_metadata_line, parse_todo_metadata_line - - with exclusive_cross_runtime_file_lock(registry_path, operation="team_plan_commit"), \ - legacy_todo_write_transaction(registry_path, goal_id, state, agent_id, - "todo_add", False, runtime_root=runtime): - read_source_request() - original = state.read_text(encoding="utf-8") - previous = previous_receipt(original) - existing = [metadata for line in original.splitlines() - if (metadata := parse_todo_metadata_line(line)) is not None] - current_fingerprint = read_fingerprint() if read_fingerprint else None + with ExitStack() as guards: + for target in targets[:-1]: + guards.enter_context(exclusive_cross_runtime_file_lock(target, operation="team_plan_commit")) try: - planned = effect_runtime_result("work_items.team_plan.plan", { - **request, "previous_receipt": previous, "todos": existing, - "current_state_fingerprint": current_fingerprint, - }) - except EffectRuntimeRejected as error: - raise TeamPlanCommitError(error.diagnostic_code, str(error), current_fingerprint) from None - result = dict(planned["result"]) - if planned["replayed"]: - verify_state_text_durable(state, original) - return result - capture = begin_todo_runtime_shadow_capture(registry_path=registry_path, - runtime_root=runtime, goal_id=goal_id, state_path=state, - write_class="todo_add", original_text=original) - lines = original.splitlines() - for todo in planned["todos"]: - metadata = format_todo_metadata_line(**{key: todo[key] for key in ( - "todo_id", "status", "task_class", "action_kind", "claimed_by", - "updated_at") if key in todo}) - block = f"- [ ] {todo['text']}\n{metadata}" - bounds = section_bounds(lines, "agent") - if bounds: - insert_into_existing_section(lines, bounds[0], bounds[1], block) - else: - insert_new_section(lines, "agent", block) - # Store outside Todo sections so archival and canonical projections do - # not remove operation history. Encoded JSON cannot terminate a comment. - encoded = base64.b64encode(json.dumps(planned["receipt"], ensure_ascii=False, - separators=(",", ":")).encode()).decode() - first_heading = next((i for i, line in enumerate(lines) if line.startswith("#")), len(lines)) - lines[first_heading:first_heading] = [marker + encoded + " -->", ""] - text = replace_updated_at("\n".join(lines) + "\n", str(request["observed_at"])) - write_captured_todo_state(capture, runtime_root=runtime, goal_id=goal_id, - state_path=state, text=text) - verify_state_text_durable(state, text) - return settle_todo_runtime_shadow_capture(result, registry_path=registry_path, - runtime_root=runtime, goal_id=goal_id, capture=capture) + # Registry administration can hold R before T/S. Like promotion, + # never wait for R while holding source locks in reverse order. + guards.enter_context(exclusive_cross_runtime_file_lock( + registry_path, operation="team_plan_commit", timeout_seconds=0)) + except LockAcquireTimeoutError: + raise TeamPlanCommitError("team_plan_commit_failed", + "team plan registry is busy; retry the original confirmation") from None + read_source_request() + # Pre-promotion receipts remain historical facts, but only after the + # canonical route has been admitted and its provider is readable. + previous = previous_receipt(state.read_text(encoding="utf-8")) + if previous is not None: + return dict(effect_runtime_result("work_items.team_plan.plan", { + **request, "previous_receipt": previous})["result"]) + response = effect_runtime_result("work_items.team_plan.commit", { + **request, "runtime_root": str(runtime.resolve()), + "expected_provider_revision": canonical["provider_revision"], + "current_state_fingerprint": read_fingerprint() if read_fingerprint else None, + "source": { + "registry": str(registry_path.resolve()), "state_file": str(state.resolve()), + "registry_sha256": hashlib.sha256(registry_path.read_bytes()).hexdigest(), + "state_sha256": hashlib.sha256(state.read_bytes()).hexdigest(), + }, + "locks": [cross_runtime_lock_witness(target) for target in targets], + }) + raise_canonical_write_failure(response, goal_id=goal_id, operation="team_plan") + if response.get("status") not in {"applied", "recovered", "replayed", "no_change"}: + raise TeamPlanCommitError(str(response.get("reason_code") or "team_plan_commit_failed"), + str(response.get("reason") or "team plan commit could not be verified"), + read_fingerprint() if read_fingerprint else None) + projected = settle_canonical_todo_projection(dict(response), registry_path=registry_path, + runtime_root=runtime, goal_id=goal_id, project=project, state_file=state) + if projected.get("projection_delivery") == "pending": + raise TeamPlanCommitError("team_plan_projection_pending", "Tasks committed; display readback is pending") + result = dict(response["result"]) + result["projection_delivery"] = projected.get("projection_delivery") + return result diff --git a/loopx/control_plane/work_items/team_plan_authority.ts b/loopx/control_plane/work_items/team_plan_authority.ts index 4957e4e071..1077e16a06 100644 --- a/loopx/control_plane/work_items/team_plan_authority.ts +++ b/loopx/control_plane/work_items/team_plan_authority.ts @@ -12,6 +12,10 @@ import {openLocalAuthorityStore, localAuthorityOpenFailure, requireLocalAuthorit import {type AuthoritySourceCheck, uncheckedAuthoritySource} from "../coordination/authority_source.ts"; import {legacyCoordinationTodoLockPath} from "../coordination/legacy_writer_lock_paths.ts"; import {requireShadowPrimaryWriteAllowed, shadowMaintenanceLockPath} from "../coordination/shadow_management.ts"; +import { + canonicalWriteRouteFailure, + requireCanonicalBusinessWrite, +} from "../coordination/local_authority_write.ts"; import {claimFileMutationLock, mutationLockOwner, releaseFileMutationLock, releaseFileMutationLockClaim, type FileMutationLockClaim} from "../effect_runtime_io.ts"; import {planTeamTransaction, replayTeamTransaction, teamTransactionIdentity, TEAM_TRANSACTION_SCHEMA} from "./team_plan.ts"; @@ -86,12 +90,13 @@ export async function commitLocalTeamPlan(value: unknown): Promise { if (owner?.token !== witness.token || owner.pid !== witness.pid) break; if (i === witnesses.length - 1) adopted = true; } + await requireShadowPrimaryWriteAllowed(root, goal); + await requireCanonicalBusinessWrite(root, goal, "team_plan"); const store = await openLocalAuthorityStore(root, goal); const result = await commitTeamPlan(store, request, async () => { // Exact committed receipts replay before these admission checks. An // expired handoff may read history, but can never authorize a new write. if (!adopted) throw new EffectRuntimeRequestError("team plan source lock handoff expired; retry the original confirmation"); - await requireShadowPrimaryWriteAllowed(root, goal); const digest = async (path: string) => createHash("sha256").update(await readFile(path)).digest("hex"); return await digest(registry) === source.registry_sha256 && await digest(state) === source.state_sha256; }); @@ -99,7 +104,8 @@ export async function commitLocalTeamPlan(value: unknown): Promise { decision_read_from_provider: true, legacy_fallback_used: false}; } catch (error) { return {status: "failed", reason_code: error instanceof EffectRuntimeRequestError ? error.code : "team_plan_commit_failed", - reason: error instanceof Error ? error.message : String(error), ...localAuthorityOpenFailure(error)}; + reason: error instanceof Error ? error.message : String(error), + ...localAuthorityOpenFailure(error), ...canonicalWriteRouteFailure(error)}; } finally { // Claims keep the source markers alive even after a requesting Python // process exits. The native effect releases the handed-off critical section. diff --git a/loopx/semantics/goal_instance_binding_inventory_v1.json b/loopx/semantics/goal_instance_binding_inventory_v1.json index fc63ae2615..23720079be 100644 --- a/loopx/semantics/goal_instance_binding_inventory_v1.json +++ b/loopx/semantics/goal_instance_binding_inventory_v1.json @@ -121,7 +121,6 @@ "content_digest_signal": "outbox entry source digest", "observed_reference": "goal_id + goal_instance_id", "producer_sites": [ - "loopx/control_plane/coordination/local_authority_shadow_outbox.ts::beginLeaseOutboxEntry", "loopx/control_plane/todos/handoff_note.py::attach_todo_handoff_note" ], "consumer_sites": [ diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index a7cc284380..2963d59890 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -127,7 +127,7 @@ }, { "site": "loopx/bootstrap.py::.bootstrap_project::codec_transaction:project_registry_transaction#1", - "line": 543, + "line": 550, "column": 14, "kind": "codec_transaction", "api": "project_registry_transaction", @@ -135,7 +135,7 @@ }, { "site": "loopx/bootstrap.py::.read_json_if_exists::codec_read:load_project_registry#1", - "line": 72, + "line": 78, "column": 15, "kind": "codec_read", "api": "load_project_registry", @@ -431,7 +431,7 @@ }, { "site": "loopx/chat_actions.py::.ChatActionService._registry::codec_read:load_registry#1", - "line": 249, + "line": 251, "column": 16, "kind": "codec_read", "api": "load_registry", @@ -1093,17 +1093,9 @@ "api": "load_project_registry", "classification": "codec_api" }, - { - "site": "loopx/control_plane/coordination/legacy_writer_fence.py::.legacy_todo_write_transaction::codec_read:load_registry#1", - "line": 362, - "column": 9, - "kind": "codec_read", - "api": "load_registry", - "classification": "codec_api" - }, { "site": "loopx/control_plane/coordination/legacy_writer_fence.py::.require_prose_state_write_allowed::codec_read:load_registry#1", - "line": 219, + "line": 218, "column": 35, "kind": "codec_read", "api": "load_registry", @@ -1111,7 +1103,7 @@ }, { "site": "loopx/control_plane/coordination/legacy_writer_fence.py::.require_registry_source_write_allowed::codec_read:load_registry#1", - "line": 161, + "line": 160, "column": 16, "kind": "codec_read", "api": "load_registry", @@ -1525,9 +1517,33 @@ "api": "load_registry", "classification": "codec_api" }, + { + "site": "loopx/control_plane/projects/registry.py::._register_project_goal_unlocked::codec_read:load_project_registry#1", + "line": 185, + "column": 25, + "kind": "codec_read", + "api": "load_project_registry", + "classification": "codec_api" + }, + { + "site": "loopx/control_plane/projects/registry.py::._register_project_goal_unlocked::codec_transaction:project_registry_transaction#1", + "line": 332, + "column": 10, + "kind": "codec_transaction", + "api": "project_registry_transaction", + "classification": "codec_api" + }, + { + "site": "loopx/control_plane/projects/registry.py::._register_project_goal_unlocked::codec_read:load_project_registry#2", + "line": 414, + "column": 24, + "kind": "codec_read", + "api": "load_project_registry", + "classification": "codec_api" + }, { "site": "loopx/control_plane/projects/registry.py::.bind_session::codec_write:mutate_project_registry#1", - "line": 449, + "line": 587, "column": 12, "kind": "codec_write", "api": "mutate_project_registry", @@ -1535,23 +1551,15 @@ }, { "site": "loopx/control_plane/projects/registry.py::.register_project_goal::codec_read:load_project_registry#1", - "line": 177, - "column": 25, + "line": 463, + "column": 9, "kind": "codec_read", "api": "load_project_registry", "classification": "codec_api" }, - { - "site": "loopx/control_plane/projects/registry.py::.register_project_goal::codec_transaction:project_registry_transaction#1", - "line": 256, - "column": 10, - "kind": "codec_transaction", - "api": "project_registry_transaction", - "classification": "codec_api" - }, { "site": "loopx/control_plane/projects/registry.py::.resolve_project::codec_read:load_project_registry#1", - "line": 572, + "line": 710, "column": 16, "kind": "codec_read", "api": "load_project_registry", @@ -1559,7 +1567,7 @@ }, { "site": "loopx/control_plane/projects/registry.py::.unbind_session::codec_write:mutate_project_registry#1", - "line": 532, + "line": 670, "column": 12, "kind": "codec_write", "api": "mutate_project_registry", @@ -1709,14 +1717,6 @@ "api": "load_registry", "classification": "codec_api" }, - { - "site": "loopx/control_plane/todos/handoff_mode_source.py::.handoff_mode_source::codec_read:load_registry#1", - "line": 28, - "column": 31, - "kind": "codec_read", - "api": "load_registry", - "classification": "codec_api" - }, { "site": "loopx/control_plane/todos/list_readback.py::.list_goal_todos::codec_read:load_registry#1", "line": 82, @@ -1735,7 +1735,7 @@ }, { "site": "loopx/control_plane/todos/provider_handoff_mode.py::.migrate_registered_handoff_mode::codec_read:load_registry#1", - "line": 84, + "line": 87, "column": 20, "kind": "codec_read", "api": "load_registry", @@ -1767,7 +1767,7 @@ }, { "site": "loopx/control_plane/work_items/task_lease.py::.runtime_root_from_registry::codec_read:load_registry#1", - "line": 564, + "line": 133, "column": 16, "kind": "codec_read", "api": "load_registry", @@ -1775,7 +1775,7 @@ }, { "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::._canonical_lease_authority_facts::codec_read:load_registry#1", - "line": 312, + "line": 306, "column": 31, "kind": "codec_read", "api": "load_registry", @@ -1783,7 +1783,7 @@ }, { "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::._task_lease_authority_snapshot_attempt::codec_read:load_registry#1", - "line": 227, + "line": 221, "column": 16, "kind": "codec_read", "api": "load_registry", @@ -1791,31 +1791,15 @@ }, { "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::._task_lease_authority_snapshot_attempt::codec_read:load_registry#2", - "line": 250, + "line": 244, "column": 22, "kind": "codec_read", "api": "load_registry", "classification": "codec_api" }, - { - "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::.execute_native_task_lease_acquire::codec_read:load_registry#1", - "line": 406, - "column": 20, - "kind": "codec_read", - "api": "load_registry", - "classification": "codec_api" - }, - { - "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::.execute_native_task_lease_lifecycle::codec_read:load_registry#1", - "line": 720, - "column": 24, - "kind": "codec_read", - "api": "load_registry", - "classification": "codec_api" - }, { "site": "loopx/control_plane/work_items/team_plan_adapter.py::.apply_team_plan::codec_read:load_registry#1", - "line": 147, + "line": 140, "column": 27, "kind": "codec_read", "api": "load_registry", @@ -1823,7 +1807,7 @@ }, { "site": "loopx/control_plane/work_items/team_plan_adapter.py::.apply_team_plan.read_source_request::codec_read:load_registry#1", - "line": 162, + "line": 155, "column": 35, "kind": "codec_read", "api": "load_registry", @@ -1831,7 +1815,7 @@ }, { "site": "loopx/control_plane/work_items/team_plan_adapter.py::.team_plan_state_fingerprint::codec_read:load_registry#1", - "line": 86, + "line": 77, "column": 31, "kind": "codec_read", "api": "load_registry", diff --git a/loopx/todos.py b/loopx/todos.py index a9d37b9376..50f9335af3 100644 --- a/loopx/todos.py +++ b/loopx/todos.py @@ -23,7 +23,7 @@ resolve_todo_state_path as resolve_todo_state_path, ) -# Preserve the existing import surface without loading unpromoted writer code. +# Retain read and semantic compatibility imports without loading retired writers. _EXPORTS = { "add_goal_todo": "loopx.control_plane.todos.mutation_api", "update_goal_todo": "loopx.control_plane.todos.mutation_api", @@ -97,12 +97,9 @@ "todo_summaries_from_fields": "loopx.control_plane.todos.goal_todo_projection", "parse_todo_source": "loopx.control_plane.todos.active_state_todo_parser", "todo_monitor_metadata": "loopx.control_plane.todos", - "authorize_todo_lifecycle_mutation": "loopx.control_plane.todos.mutation_authority", - "todo_update_authority_action": "loopx.control_plane.todos.mutation_authority", "build_open_parent_successor_advisory": "loopx.control_plane.todos.succession_warning", "build_successor_intents": "loopx.control_plane.todos.successor_derivation", "derive_successor_proposals": "loopx.control_plane.todos.successor_derivation", - "successor_add_kwargs": "loopx.control_plane.todos.successor_derivation", "MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL": "loopx.control_plane.todos.todo_index", "normalize_new_todo": "loopx.control_plane.todos.text", "list_goal_todos": "loopx.control_plane.todos.list_readback", @@ -112,32 +109,16 @@ "require_completion_decision_outcome": "loopx.control_plane.todos.unblock_resume", "_attach_todo_write_correctness_dry_run_packet": "loopx.control_plane.todos.write_correctness", "require_user_todo_task_class": "loopx.control_plane.todos.authoring_scope", - "legacy_todo_write_transaction": "loopx.control_plane.coordination.legacy_writer_fence", "canonical_todo_items": "loopx.control_plane.coordination.local_authority", "canonical_todo_summary_fields": "loopx.control_plane.coordination.local_authority", "read_canonical_todos_if_promoted": "loopx.control_plane.coordination.local_authority", "resolve_todo_state_path": "loopx.control_plane.todos.path_resolution", - "provider_first_terminal_lifecycle": "loopx.control_plane.todos.provider_terminal_lifecycle", "goal_handoff_mode": "loopx.control_plane.todos.handoff_mode", - "enter_added_todo_ownership_handoff_gate": "loopx.control_plane.todos.handoff_mode", - "enter_todo_ownership_handoff_gate": "loopx.control_plane.todos.handoff_mode", - "resolve_todo_completion_handoff": "loopx.control_plane.todos.handoff_mode", "effective_runtime_root": "loopx.paths", - "enter_terminal_todo_lease_fence": "loopx.control_plane.work_items.task_lease", - "hold_task_lease_mutation_fence": "loopx.control_plane.work_items.task_lease", - "release_verified_task_lease_fence": "loopx.control_plane.work_items.task_lease", "supersede_goal_todo": "loopx.control_plane.todos.mutation_api", "archive_completed_todos": "loopx.control_plane.todos.mutation_api", "ARCHIVE_COMPLETED_DEFAULT_MAX_ACTIVE_DONE": "loopx.control_plane.todos.mutation_api", "resolve_todo_state": "loopx.control_plane.todos.path_resolution", - "add_todo_to_lines": "loopx.control_plane.todos.legacy_mutation", - "_add_goal_todo_legacy": "loopx.control_plane.todos.legacy_mutation", - "_update_goal_todo_legacy": "loopx.control_plane.todos.legacy_mutation", - "_complete_goal_todo_legacy": "loopx.control_plane.todos.legacy_mutation", - "apply_todo_update_to_lines": "loopx.control_plane.todos.line_update", - "link_generated_successor_todo_ids": "loopx.control_plane.todos.line_update", - "link_superseding_todo_id": "loopx.control_plane.todos.line_update", - "upsert_todo_metadata": "loopx.control_plane.todos.line_update", } diff --git a/pyproject.toml b/pyproject.toml index e26d5fabf3..acc124474b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,8 +195,6 @@ files = [ "loopx/control_plane/testing/authority_e2e_fixtures.py", "loopx/control_plane/testing/authority_e2e_ladder.py", "loopx/control_plane/testing/authority_e2e_row_support.py", - "loopx/control_plane/testing/authority_e2e_rows_stage2c.py", - "loopx/control_plane/testing/authority_e2e_rows_stage2c2.py", "loopx/control_plane/work_items/delivery_batch_scale.py", "loopx/control_plane/work_items/delivery_outcome.py", "loopx/control_plane/work_items/lifecycle.py", diff --git a/tests/architecture/test_content_digest_single_owner.py b/tests/architecture/test_content_digest_single_owner.py index dbad12cf89..877b791be2 100644 --- a/tests/architecture/test_content_digest_single_owner.py +++ b/tests/architecture/test_content_digest_single_owner.py @@ -199,7 +199,6 @@ "loopx.control_plane.todos.machine_section_projection", "loopx.control_plane.work_items.governed_transition_proposal", "loopx.control_plane.work_items.progress_review_policy", - "loopx.control_plane.work_items.task_lease", "loopx.domain_packs.issue_fix", "loopx.extensions.lark.document_comment_provider", "loopx.extensions.openviking_semantic_preference.history_export", diff --git a/tests/architecture/test_legacy_markdown_writer_retirement.py b/tests/architecture/test_legacy_markdown_writer_retirement.py new file mode 100644 index 0000000000..fd39f1f4a8 --- /dev/null +++ b/tests/architecture/test_legacy_markdown_writer_retirement.py @@ -0,0 +1,208 @@ +"""Prevent the retired Markdown coordination writers from returning.""" + +from __future__ import annotations + +import ast +from importlib.util import resolve_name +import json +from pathlib import Path +import subprocess + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +PACKAGE_ROOT = REPOSITORY_ROOT / "loopx" + +RETIRED_MODULES = { + "loopx.control_plane.coordination.runtime_shadow_writer_adapter", + "loopx.control_plane.scheduler.legacy_monitor_poll", + "loopx.control_plane.testing.authority_e2e_rows_stage2c", + "loopx.control_plane.testing.authority_e2e_rows_stage2c2", + "loopx.control_plane.todos.handoff_mode_source", + "loopx.control_plane.todos.legacy_mutation", + "loopx.control_plane.todos.line_update", +} +RETIRED_FILES = { + Path(*module.split(".")).with_suffix(".py") + for module in RETIRED_MODULES +} | { + Path("loopx/control_plane/coordination/handoff_mode_legacy_plan.ts"), +} +RETIRED_PYTHON_SYMBOLS = { + "_VerifiedTaskLeaseFence", + "_add_goal_todo_legacy", + "_archive_completed_todos_legacy", + "_complete_goal_todo_legacy", + "_supersede_goal_todo_legacy", + "_update_goal_todo_legacy", + "apply_legacy_monitor_poll", + "apply_todo_update_to_lines", + "begin_todo_runtime_shadow_capture", + "enter_terminal_todo_lease_fence", + "goal_handoff_mode_for_lease", + "handoff_mode_source", + "hold_handoff_lease_holder_gate", + "hold_task_lease_mutation_fence", + "legacy_todo_write_transaction", + "link_generated_successor_todo_ids", + "link_superseding_todo_id", + "release_verified_task_lease_fence", + "require_runtime_shadow_capture_prepared", + "settle_lease_runtime_shadow_capture", + "settle_todo_runtime_shadow_capture", + "successor_add_kwargs", + "upsert_todo_metadata", + "write_captured_todo_state", +} +RETIRED_TYPESCRIPT_SYMBOLS = { + "LEGACY_HANDOFF_PLAN_SCHEMA", + "patchHandoffMode", + "planLegacyMonitorBatch", + "planLegacyHandoffMode", +} + +TYPESCRIPT_RETIREMENT_SCAN = r""" +import fs from "node:fs"; +import path from "node:path"; +import ts from "typescript"; + +let input = ""; +for await (const chunk of process.stdin) input += chunk; +const request = JSON.parse(input); +const retiredModule = path.resolve( + request.root, + "loopx/control_plane/coordination/handoff_mode_legacy_plan", +); +const retiredSymbols = new Set(request.retired_symbols); +const offenders = []; + +const normalizedModule = (sourcePath, specifier) => { + if (!specifier.startsWith(".")) return path.resolve(request.root, specifier); + return path.resolve(path.dirname(sourcePath), specifier); +}; +const withoutScriptExtension = value => + value.replace(/\.(?:[cm]?[jt]s)$/, "").replace(/[/\\]index$/, ""); +const noteModule = (sourcePath, node) => { + if (!node || !ts.isStringLiteralLike(node)) return; + const resolved = withoutScriptExtension(normalizedModule(sourcePath, node.text)); + if (resolved === retiredModule) { + offenders.push({path: path.relative(request.root, sourcePath), kind: "import", name: node.text}); + } +}; +const noteName = (sourcePath, node) => { + if (node && ts.isIdentifier(node) && retiredSymbols.has(node.text)) { + offenders.push({path: path.relative(request.root, sourcePath), kind: "symbol", name: node.text}); + } +}; + +for (const relative of request.sources) { + const sourcePath = path.resolve(request.root, relative); + const text = fs.readFileSync(sourcePath, "utf8"); + const tree = ts.createSourceFile( + relative, + text, + ts.ScriptTarget.Latest, + true, + relative.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + if (tree.parseDiagnostics.length) { + const diagnostic = tree.parseDiagnostics[0]; + const line = tree.getLineAndCharacterOfPosition(diagnostic.start ?? 0).line + 1; + throw new Error(`${relative}:${line}: invalid TypeScript source`); + } + const visit = node => { + if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { + noteModule(sourcePath, node.moduleSpecifier); + } else if (ts.isImportEqualsDeclaration(node) && + ts.isExternalModuleReference(node.moduleReference)) { + noteModule(sourcePath, node.moduleReference.expression); + } else if (ts.isCallExpression(node) && + node.expression.kind === ts.SyntaxKind.ImportKeyword) { + noteModule(sourcePath, node.arguments[0]); + } + if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || + ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || + ts.isEnumDeclaration(node) || ts.isVariableDeclaration(node)) { + noteName(sourcePath, node.name); + } + ts.forEachChild(node, visit); + }; + visit(tree); +} +process.stdout.write(JSON.stringify(offenders)); +""" + + +def _module_name(path: Path) -> str: + relative = path.relative_to(PACKAGE_ROOT).with_suffix("") + parts = list(relative.parts) + if parts[-1] == "__init__": + parts.pop() + return ".".join(("loopx", *parts)) + + +def _python_offenders() -> list[tuple[str, str, str]]: + offenders: list[tuple[str, str, str]] = [] + for path in sorted(PACKAGE_ROOT.rglob("*.py")): + relative = str(path.relative_to(REPOSITORY_ROOT)) + module = _module_name(path) + package = module if path.name == "__init__.py" else module.rpartition(".")[0] + tree = ast.parse(path.read_text(encoding="utf-8"), filename=relative) + for node in ast.walk(tree): + if isinstance(node, ast.Import): + imported = {alias.name for alias in node.names} + elif isinstance(node, ast.ImportFrom): + imported_module = node.module or "" + if node.level: + imported_module = resolve_name( + "." * node.level + imported_module, + package, + ) + imported = {imported_module} + imported.update( + f"{imported_module}.{alias.name}" for alias in node.names + ) + else: + imported = set() + for imported_module in imported & RETIRED_MODULES: + offenders.append((relative, "import", imported_module)) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + if node.name in RETIRED_PYTHON_SYMBOLS: + offenders.append((relative, "symbol", node.name)) + return offenders + + +def _typescript_offenders() -> list[dict[str, str]]: + sources = sorted( + str(path.relative_to(REPOSITORY_ROOT)) + for suffix in ("*.ts", "*.tsx", "*.mts") + for path in PACKAGE_ROOT.rglob(suffix) + ) + completed = subprocess.run( + ["node", "--input-type=module", "-e", TYPESCRIPT_RETIREMENT_SCAN], + input=json.dumps( + { + "root": str(REPOSITORY_ROOT), + "sources": sources, + "retired_symbols": sorted(RETIRED_TYPESCRIPT_SYMBOLS), + } + ), + cwd=REPOSITORY_ROOT, + capture_output=True, + text=True, + timeout=60, + ) + assert completed.returncode == 0, completed.stderr + result = json.loads(completed.stdout) + assert isinstance(result, list) + return result + + +def test_legacy_markdown_writer_retirement_is_structural() -> None: + existing = sorted( + str(path) + for relative in RETIRED_FILES + if (path := REPOSITORY_ROOT / relative).exists() + ) + assert existing == [] + assert _python_offenders() == [] + assert _typescript_offenders() == [] diff --git a/tests/capabilities/test_issue_fix_grouped_monitor_materialization.py b/tests/capabilities/test_issue_fix_grouped_monitor_materialization.py index 229ed721af..7472e13de3 100644 --- a/tests/capabilities/test_issue_fix_grouped_monitor_materialization.py +++ b/tests/capabilities/test_issue_fix_grouped_monitor_materialization.py @@ -77,8 +77,6 @@ def _fixture( def _use_provider(state, registry, provider, monkeypatch, *, handoff_mode="soft_claim"): if handoff_mode == "hard_lease": state.write_text("---\nhandoff_mode: hard_lease\n---\n" + state.read_text()) - if provider == "legacy": - return from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection isolate_sqlite_runtime(registry.parent, monkeypatch) @@ -127,7 +125,7 @@ def _packet( ) -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("handoff_mode", ["soft_claim", "hard_lease"]) def test_grouped_monitor_materialization_is_one_per_bucket_and_retires_empty_bucket( tmp_path: Path, monkeypatch, provider, handoff_mode, @@ -213,11 +211,10 @@ def test_grouped_monitor_materialization_is_one_per_bucket_and_retires_empty_buc assert reopened_state.count("task_class=continuous_monitor") == 1 assert "status=open" in reopened_state assert "no_followup=true" not in reopened_state - if provider != "legacy": - assert reopened["projection_delivery"] in {"delivered", "current"} + assert reopened["projection_delivery"] in {"delivered", "current"} -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_group_membership_change_advances_monitor_generation(tmp_path: Path, monkeypatch, provider) -> None: project, state, registry = _fixture(tmp_path) _use_provider(state, registry, provider, monkeypatch) @@ -251,8 +248,9 @@ def test_group_membership_change_advances_monitor_generation(tmp_path: Path, mon assert state.read_bytes() == unchanged -def test_grouped_monitors_are_isolated_per_repository(tmp_path: Path) -> None: +def test_grouped_monitors_are_isolated_per_repository(tmp_path: Path, monkeypatch) -> None: project, state, registry = _fixture(tmp_path) + _use_provider(state, registry, "file", monkeypatch) ledger = tmp_path / "pr-lifecycle.jsonl" upsert_issue_fix_pr_lifecycle_ledger_jsonl(ledger, _packet(101)) upsert_issue_fix_pr_lifecycle_ledger_jsonl( @@ -280,11 +278,13 @@ def test_grouped_monitors_are_isolated_per_repository(tmp_path: Path) -> None: def test_grouped_monitor_keeps_creator_ownership_across_turns_and_due_poll( tmp_path: Path, + monkeypatch, ) -> None: project, state, registry = _fixture( tmp_path, registered_agents=[AGENT_ID, PEER_AGENT_ID], ) + _use_provider(state, registry, "file", monkeypatch) ledger = tmp_path / "pr-lifecycle.jsonl" upsert_issue_fix_pr_lifecycle_ledger_jsonl(ledger, _packet(101)) @@ -305,10 +305,13 @@ def test_grouped_monitor_keeps_creator_ownership_across_turns_and_due_poll( todo_id = monitor["todo_id"] state_before_peer_poll = state.read_text(encoding="utf-8") - with pytest.raises(ValueError, match=f"claimed_by={AGENT_ID!r}"): + with pytest.raises( + ValueError, + match="outside the actor's registered owner/binding scope", + ): write_monitor_poll_todo_state( registry_path=registry, - runtime_root=tmp_path / "runtime", + runtime_root=tmp_path, goal_id=GOAL_ID, todo_id=todo_id, result_hash=monitor["result_hash"], @@ -320,7 +323,7 @@ def test_grouped_monitor_keeps_creator_ownership_across_turns_and_due_poll( polled = write_monitor_poll_todo_state( registry_path=registry, - runtime_root=tmp_path / "runtime", + runtime_root=tmp_path, goal_id=GOAL_ID, todo_id=todo_id, result_hash=monitor["result_hash"], @@ -329,16 +332,18 @@ def test_grouped_monitor_keeps_creator_ownership_across_turns_and_due_poll( agent_id=AGENT_ID, ) assert polled is not None - assert polled["todo_update"]["mutation_authority"]["actor_agent_id"] == ( - AGENT_ID - ) + assert polled["source_authority"] in {"file_v0", "sqlite_v0"} + assert polled["todo_update"]["todo_id"] == todo_id assert datetime.fromisoformat(polled["next_due_at"]).timestamp() == ( datetime.fromisoformat("2026-08-01T17:00:00+00:00").timestamp() ) upsert_issue_fix_pr_lifecycle_ledger_jsonl(ledger, _packet(102)) state_before_peer_attempt = state.read_text(encoding="utf-8") - with pytest.raises(ValueError, match=f"claimed_by={AGENT_ID!r}"): + with pytest.raises( + ValueError, + match="claimed_by does not grant this actor update authority", + ): materialize_issue_fix_grouped_monitors( registry_path=registry, goal_id=GOAL_ID, @@ -428,7 +433,7 @@ def test_pr_lifecycle_command_contract_is_shared_by_all_operator_surfaces() -> N assert "--fetch-metadata" not in command -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_pr_lifecycle_execute_materializes_pending_monitor_and_keeps_goal_runnable( tmp_path: Path, monkeypatch, provider, ) -> None: @@ -533,6 +538,7 @@ def test_pr_lifecycle_execute_fetches_public_metadata_by_default( monkeypatch: pytest.MonkeyPatch, ) -> None: project, state, registry = _fixture(tmp_path) + _use_provider(state, registry, "file", monkeypatch) def fake_fetch(reference: dict, *, timeout_seconds: int) -> dict: assert reference["repo"] == "huangruiteng/loopx" diff --git a/tests/capabilities/test_issue_fix_monitor_execution.py b/tests/capabilities/test_issue_fix_monitor_execution.py index 8102f42c5e..5b21796421 100644 --- a/tests/capabilities/test_issue_fix_monitor_execution.py +++ b/tests/capabilities/test_issue_fix_monitor_execution.py @@ -24,7 +24,7 @@ def scenario(tmp_path, monkeypatch, provider): return arguments, monitor, state -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_hard_lease_membership_observation_releases_only_its_execution(tmp_path, monkeypatch, provider): args, before, state = scenario(tmp_path, monkeypatch, provider) result = materialization.materialize_issue_fix_grouped_monitors(**args, generated_at="2030-01-01T01:00:00Z") @@ -39,7 +39,7 @@ def test_hard_lease_membership_observation_releases_only_its_execution(tmp_path, assert snapshot == state.read_bytes() -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_another_execution_even_for_same_actor_is_not_borrowed(tmp_path, monkeypatch, provider): args, monitor, state = scenario(tmp_path, monkeypatch, provider) acquired = acquire_task_lease(registry_path=args["registry_path"], runtime_root=tmp_path, @@ -52,7 +52,7 @@ def test_another_execution_even_for_same_actor_is_not_borrowed(tmp_path, monkeyp goal_id=GOAL_ID, todo_id=monitor["todo_id"])["lease"] == acquired["lease"] -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_retry_after_interrupted_acquisition_recovers_its_own_attempt(tmp_path, monkeypatch, provider): args, monitor, _ = scenario(tmp_path, monkeypatch, provider) original = materialization.acquire_task_lease @@ -75,7 +75,7 @@ def interrupt(**kwargs): assert lease_after["status"] == "released" -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_retry_recovers_its_own_attempt_despite_reordered_ledger(tmp_path, monkeypatch, provider): """The attempt identity is the reconciliation subject, not the row order.""" args, monitor, _ = scenario(tmp_path, monkeypatch, provider) @@ -131,7 +131,7 @@ def test_attempt_decision_ignores_only_the_observation_clock(): assert materialization._decision_signature(changed) != materialization._decision_signature(step) -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_failed_write_cleans_attempt_and_identical_retry_can_acquire_again(tmp_path, monkeypatch, provider): args, monitor, _ = scenario(tmp_path, monkeypatch, provider) original = materialization.update_goal_todo @@ -147,7 +147,7 @@ def unavailable(**kwargs): assert materialization.materialize_issue_fix_grouped_monitors(**args, generated_at="2030-01-01T01:00:00Z")["write_performed"] -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_missing_or_stale_ledger_never_means_empty_current_work(tmp_path, monkeypatch, provider): args, monitor, state = scenario(tmp_path, monkeypatch, provider) before = state.read_bytes() diff --git a/tests/cli_commands/test_project_registry.py b/tests/cli_commands/test_project_registry.py index c1c501251e..322345f16e 100644 --- a/tests/cli_commands/test_project_registry.py +++ b/tests/cli_commands/test_project_registry.py @@ -14,6 +14,14 @@ from loopx.control_plane.projects import registry_codec +def canonical_storage_target(_runtime_root: Path) -> dict[str, object]: + return { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "sqlite", + "handoff_mode": "hard_lease", + } + + @pytest.mark.parametrize("objective", [ "Deliver the Atlas import pipeline.", "```text Deliver the Atlas import pipeline. ```", @@ -107,6 +115,61 @@ def test_project_register_creates_project_goal_and_resumable_state( assert active_state_section_text(state, "Objective") == " ".join(objective.split()) +def test_project_register_resumes_interrupted_canonical_initialization( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + from loopx.control_plane.effect_runtime import restart_effect_runtime + from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime + + isolate_sqlite_runtime(tmp_path, monkeypatch) + knowledge_root = tmp_path / "atlas" + registry_path = knowledge_root / ".loopx" / "registry.json" + runtime_root = tmp_path / "runtime" + arguments = { + "registry_path": registry_path, + "runtime_root": runtime_root, + "project_id": "atlas", + "project_kind": "work", + "knowledge_root": knowledge_root, + "goal_id": "atlas-import", + "objective": "Deliver the Atlas import pipeline.", + "non_goals": [], + "acceptance": ["A verified import report is produced."], + "unknowns": [], + "next_effect": "Inspect Atlas.", + "stop_condition": "Stop while Atlas is unavailable.", + "repository_bindings": [], + "external_locator_bindings": [], + "resolve_new_goal_storage_target": canonical_storage_target, + } + initialize = project_registry.initialize_goal_storage_target + + def interrupt(*_args, **_kwargs): + raise TimeoutError("canonical initialization interrupted") + + monkeypatch.setattr(project_registry, "initialize_goal_storage_target", interrupt) + with pytest.raises(TimeoutError, match="initialization interrupted"): + project_registry.register_project_goal(**arguments) + + pending = json.loads(registry_path.read_text(encoding="utf-8"))["goals"][0] + assert pending["coordination"]["storage_target"]["schema_version"] == ( + "loopx_new_goal_storage_target_v1" + ) + assert pending["creation_operation_id"].startswith("goal-create:project-register:") + + monkeypatch.setattr(project_registry, "initialize_goal_storage_target", initialize) + try: + recovered = project_registry.register_project_goal(**arguments) + assert recovered["changed"] is False + assert recovered["storage_selection"]["authority_initialized"] is True + assert recovered["storage_selection"]["creation_operation_id"] == ( + pending["creation_operation_id"] + ) + finally: + restart_effect_runtime() + + def test_project_register_defaults_to_knowledge_root_when_global_registry_exists( tmp_path: Path, capsys, @@ -221,23 +284,28 @@ def test_project_register_serializes_one_state_across_distinct_registries( knowledge_root = tmp_path / "atlas" registry_paths = [tmp_path / f"registry-{index}.json" for index in range(8)] - def register(registry_path: Path) -> dict[str, object]: - return project_registry.register_project_goal( - registry_path=registry_path, - runtime_root=tmp_path / "runtime", - project_id="atlas", - project_kind="work", - knowledge_root=knowledge_root, - goal_id="atlas-import", - objective="Deliver the Atlas import pipeline.", - non_goals=[], - acceptance=["A verified import report is produced."], - unknowns=[], - next_effect="Inspect Atlas.", - stop_condition="Stop while Atlas is unavailable.", - repository_bindings=[], - external_locator_bindings=[], - ) + def register(registry_path: Path) -> tuple[str, object]: + try: + result = project_registry.register_project_goal( + registry_path=registry_path, + runtime_root=tmp_path / "runtime", + project_id="atlas", + project_kind="work", + knowledge_root=knowledge_root, + goal_id="atlas-import", + objective="Deliver the Atlas import pipeline.", + non_goals=[], + acceptance=["A verified import report is produced."], + unknowns=[], + next_effect="Inspect Atlas.", + stop_condition="Stop while Atlas is unavailable.", + repository_bindings=[], + external_locator_bindings=[], + resolve_new_goal_storage_target=canonical_storage_target, + ) + except ValueError as error: + return "rejected", str(error) + return "registered", result with ThreadPoolExecutor(max_workers=len(registry_paths)) as executor: results = list(executor.map(register, registry_paths)) @@ -245,8 +313,14 @@ def register(registry_path: Path) -> dict[str, object]: state_file = ( knowledge_root / ".loopx" / "goals" / "atlas-import" / "ACTIVE_GOAL_STATE.md" ) - assert all(result["ok"] is True for result in results) - assert all(path.exists() for path in registry_paths) + registered = [result for status, result in results if status == "registered"] + rejected = [result for status, result in results if status == "rejected"] + assert len(registered) == 1 + assert registered[0]["ok"] is True + assert len(rejected) == len(registry_paths) - 1 + assert all("bound to canonical authority through another registry" in str(error) + for error in rejected) + assert sum(path.exists() for path in registry_paths) == 1 assert state_file.exists() @@ -273,6 +347,7 @@ def register(knowledge_root: Path) -> tuple[str, Path]: stop_condition="Stop while Atlas is unavailable.", repository_bindings=[], external_locator_bindings=[], + resolve_new_goal_storage_target=canonical_storage_target, ) except ValueError: return "conflict", knowledge_root @@ -386,7 +461,7 @@ def test_project_register_repeated_identical_request_is_a_noop( assert registry_path.read_bytes() == registry_before -def test_project_register_reuses_legacy_state_from_strict_registry( +def test_project_register_rejects_rebinding_canonical_creation_source_from_strict_registry( tmp_path: Path, capsys, ) -> None: project = tmp_path / "atlas" @@ -421,10 +496,9 @@ def test_project_register_reuses_legacy_state_from_strict_registry( registry_path.write_text(json.dumps(envelope, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") before = registry_path.read_bytes() - assert main(args) == 0 + assert main(args) == 1 result = json.loads(capsys.readouterr().out) - assert result["changed"] is False - assert result["state_file"] == str(legacy_state) + assert "not bound to the registered original operation" in result["error"] assert registry_path.read_bytes() == before assert legacy_state.exists() and not new_state.exists() diff --git a/tests/control_plane/canonical_authority_fixture.py b/tests/control_plane/canonical_authority_fixture.py index b8d8fbd004..f2deeb13d8 100644 --- a/tests/control_plane/canonical_authority_fixture.py +++ b/tests/control_plane/canonical_authority_fixture.py @@ -73,7 +73,14 @@ def single_snapshot_page(result: dict, goal_id: str = "goal-a") -> dict: } -def promoted_create_fixture(tmp_path: Path, *, provider: str = "file") -> tuple[Path, Path, Path]: +def promoted_create_fixture( + tmp_path: Path, + *, + provider: str = "file", + goal_id: str = "goal-a", + handoff_mode: str = "soft_claim", + registered_agents: list[str] | None = None, +) -> tuple[Path, Path, Path]: from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.coordination.coordination_state_contract import ( TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, TODO_DOMAIN_RECORD_FIELDS, @@ -81,7 +88,7 @@ def promoted_create_fixture(tmp_path: Path, *, provider: str = "file") -> tuple[ runtime_root = tmp_path / "runtime" project = tmp_path / "project" - state_file = project / ".codex/goals/goal-a/ACTIVE_GOAL_STATE.md" + state_file = project / f".codex/goals/{goal_id}/ACTIVE_GOAL_STATE.md" state_file.parent.mkdir(parents=True) state_file.write_text( "# Goal\n\n## User Todo / Owner Review Reading Queue\n\n" @@ -96,10 +103,16 @@ def promoted_create_fixture(tmp_path: Path, *, provider: str = "file") -> tuple[ "common_runtime_root": str(runtime_root), "goals": [ { - "id": "goal-a", + "id": goal_id, "repo": str(project), - "state_file": ".codex/goals/goal-a/ACTIVE_GOAL_STATE.md", - "coordination": {"registered_agents": ["agent-a"]}, + "state_file": f".codex/goals/{goal_id}/ACTIVE_GOAL_STATE.md", + "coordination": { + "registered_agents": ( + registered_agents + if registered_agents is not None + else ["agent-a"] + ) + }, } ], } @@ -107,7 +120,7 @@ def promoted_create_fixture(tmp_path: Path, *, provider: str = "file") -> tuple[ encoding="utf-8", ) projection = build_todo_runtime_shadow_projection( - goal_id="goal-a", todos=[], handoff_mode="soft_claim" + goal_id=goal_id, todos=[], handoff_mode=handoff_mode ) projection["todo_read_model"] = { **projection["todo_read_model"], @@ -115,6 +128,6 @@ def promoted_create_fixture(tmp_path: Path, *, provider: str = "file") -> tuple[ "contract_fields": list(TODO_DOMAIN_RECORD_FIELDS), } initialize_canonical_authority( - runtime_root, "goal-a", projection, state_path=state_file, provider=provider + runtime_root, goal_id, projection, state_path=state_file, provider=provider ) return registry_path, runtime_root, state_file diff --git a/tests/control_plane/shadow_e2e_fixture.py b/tests/control_plane/shadow_e2e_fixture.py index 0469688896..757ce17f6a 100644 --- a/tests/control_plane/shadow_e2e_fixture.py +++ b/tests/control_plane/shadow_e2e_fixture.py @@ -1,18 +1,12 @@ -"""Disposable public CLI fixtures and scheduling-only process crash seams.""" +"""Disposable public CLI fixtures for retained shadow management tests.""" from __future__ import annotations import json from pathlib import Path -import select import subprocess import sys from dataclasses import dataclass -from tempfile import TemporaryDirectory - -import loopx - -from loopx.control_plane.testing.authority_e2e_rows_stage2c2 import CRASH_WORKER REPO = Path(__file__).resolve().parents[2] @@ -25,7 +19,6 @@ class ShadowWorkspace: goal: str = "goal-e2e" command: tuple[str, ...] = (sys.executable, "-m", "loopx.cli") cwd: Path = REPO - package: Path = Path(loopx.__file__).resolve().parent def arguments(self, *args: str) -> list[str]: return [ @@ -56,75 +49,6 @@ def cli(self, *args: str, success: bool = True) -> dict: def add(self, text: str) -> dict: return self.cli("todo", "add", "--role", "agent", "--text", text) - def drain(self, **limits: str) -> dict: - args = [ - item - for key, value in limits.items() - for item in ("--" + key.replace("_", "-"), value) - ] - return self.cli("authority-shadow", "drain", *args, success=False) - - def crash(self, window: str, *args: str) -> dict: - # Test-only fault assets are not wheel data. Generate the scheduling - # driver here and bind it to the actual package's production owner. - with TemporaryDirectory(prefix="loopx-drain-fault-") as directory: - driver = Path(directory) / "drain_fault.mts" - module = self.package / "control_plane/coordination/shadow_drain.ts" - driver.write_text( - "import {drainShadowOutbox} from " + json.dumps(module.as_uri()) + ";" - "let input=''; for await (const bytes of process.stdin) input+=bytes;" - "const phase=process.argv[2]==='between_unlinks'?'after_unlink':process.argv[2];" - "const request=JSON.parse(input);" - "const result=await drainShadowOutbox(request,{afterEffect:async observed=>{" - "if(observed===phase){" - "process.stdout.write('BARRIER '+JSON.stringify({native_pid:process.pid,request})+'\\n');" - "await new Promise(()=>{setInterval(()=>{},1000);});" - "}}});process.stdout.write(JSON.stringify(result)+'\\n');", - encoding="utf-8", - ) - relative_driver = "'loopx/control_plane/testing/shadow_drain_fault_process.ts'" - assert CRASH_WORKER.count(relative_driver) == 1 - worker = CRASH_WORKER.replace(relative_driver, repr(str(driver))) - child = subprocess.Popen( - [ - sys.executable, - "-c", - worker, - window, - str(self.state), - *self.arguments(*args), - ], - cwd=self.cwd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - assert child.stdout is not None - try: - readable, _, _ = select.select([child.stdout], [], [], 30) - assert readable, "public CLI did not reach the requested persistence window" - line = child.stdout.readline() - if not line.startswith("BARRIER "): - child.kill() - stdout, stderr = child.communicate(timeout=10) - raise AssertionError(f"No process barrier: {line}{stdout}\n{stderr}") - payload = json.loads(line.removeprefix("BARRIER ")) - if payload.get("native_pid"): - assert child.stdin is not None - child.stdin.write("terminate_native\n") - child.stdin.flush() - readable, _, _ = select.select([child.stdout], [], [], 10) - assert readable and child.stdout.readline().strip() == "REAPED", "native owner must be reaped" - child.kill() - child.communicate(timeout=10) - assert child.returncode == -9 - return payload - finally: - if child.poll() is None: - child.kill() - child.communicate(timeout=10) - def workspace(path: Path, *, bootstrap: bool = True) -> ShadowWorkspace: path.mkdir(parents=True, exist_ok=True) diff --git a/tests/control_plane/test_actual_default_model_behavior_portfolio.py b/tests/control_plane/test_actual_default_model_behavior_portfolio.py index bcf654e9fa..b7ad274cec 100644 --- a/tests/control_plane/test_actual_default_model_behavior_portfolio.py +++ b/tests/control_plane/test_actual_default_model_behavior_portfolio.py @@ -74,6 +74,7 @@ _build_fixture as _build_selected_fixture, ) from loopx.control_plane.testing.terminal_settlement_tool_behavior import ( + TERMINAL_SETTLEMENT_FIXTURE_TODO_ID, DoubaoTerminalSettlementToolBehaviorActor, ) from loopx.quota import build_quota_should_run @@ -460,7 +461,7 @@ def _capability_monitor_repair_actor(_: str) -> dict[str, Any]: def _terminal_settlement_actor(_: str) -> dict[str, Any]: return _passing_tool_receipt( "terminal_settlement_tool_behavior_receipt_v0", - selected_todo_id="todo_terminal001", + selected_todo_id=TERMINAL_SETTLEMENT_FIXTURE_TODO_ID, user_action_required=False, delivery_allowed=True, terminal_order_observed=True, diff --git a/tests/control_plane/test_archive_dependency_capture.py b/tests/control_plane/test_archive_dependency_capture.py index 002ef99f69..479435076b 100644 --- a/tests/control_plane/test_archive_dependency_capture.py +++ b/tests/control_plane/test_archive_dependency_capture.py @@ -7,7 +7,6 @@ import pytest from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime -from test_runtime_shadow_bounded_e2e import workspace, enable, cli from loopx.control_plane.coordination.authority_source_capture import ( build_runtime_shadow_source_snapshot, capture_todo_archive_dependencies, ) @@ -18,6 +17,23 @@ from loopx.control_plane.testing.canary_harness import run_json_cli_result +def workspace(tmp_path: Path) -> tuple[Path, Path, Path]: + runtime = tmp_path / "runtime" + state = tmp_path / "ACTIVE_GOAL_STATE.md" + state.write_text("---\ngoal_id: goal-a\nhandoff_mode: legacy\n---\n") + registry = tmp_path / "registry.json" + registry.write_text(json.dumps({ + "common_runtime_root": str(runtime), + "goals": [{ + "id": "goal-a", + "repo": str(tmp_path), + "state_file": state.name, + "coordination": {"registered_agents": ["agent-a"]}, + }], + })) + return registry, runtime, state + + def row(todo_id: str, *, text: str = "Delivered result", metadata: str = "", marker: str = "x") -> str: return f"- [{marker}] {text}\n \n" @@ -112,16 +128,3 @@ def test_complete_snapshot_and_real_provider_cli_keep_dependency_semantics(tmp_p assert code == 0, listed assert [r["todo_id"] for r in listed["todos"]] == ["todo_next"] assert not state.exists() - - -def test_public_bootstrap_and_writer_outbox_share_legacy_archive_admission(tmp_path: Path): - registry, runtime, state = workspace(tmp_path) - state.write_text("---\ngoal_id: goal-a\nhandoff_mode: legacy\n---\n" + source_graph()) - enable(registry) - boot = cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - assert boot["ok"] is True - write = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", - "--text", "New captured work", "--task-class", "advancement_task") - assert write["coordination_runtime_shadow"]["outcome"] == "delivered" - inspected = cli(registry, runtime, "coordination-shadow", "inspect", "--goal-id", "goal-a") - assert inspected["inspection"]["status"] == "matched", inspected diff --git a/tests/control_plane/test_archive_retry_delivery.py b/tests/control_plane/test_archive_retry_delivery.py index 40241aedc6..828d59d7f4 100644 --- a/tests/control_plane/test_archive_retry_delivery.py +++ b/tests/control_plane/test_archive_retry_delivery.py @@ -348,6 +348,15 @@ def test_unpromoted_archive_does_not_rebuild_missing_display(tmp_path: Path) -> before = {path: path.read_bytes() for path in authority.rglob("*") if path.is_file()} result = _archive_cli(registry) assert result.returncode == 1 - assert "active state file does not exist" in json.loads(result.stdout)["error"] + failure = json.loads(result.stdout) + assert failure["error_code"] == "canonical_authority_required" + assert failure["operation"] == "todo_archive" + assert failure["write_admitted"] is False + assert failure["decision_read_from_provider"] is False + assert failure["legacy_fallback_used"] is False + assert failure["recovery"] == { + "action": "migrate_goal_authority", + "backup_required": True, + } assert not state.exists() assert {path: path.read_bytes() for path in authority.rglob("*") if path.is_file()} == before diff --git a/tests/control_plane/test_auxiliary_scoped_gate.py b/tests/control_plane/test_auxiliary_scoped_gate.py index e688414494..fa61a1e8fb 100644 --- a/tests/control_plane/test_auxiliary_scoped_gate.py +++ b/tests/control_plane/test_auxiliary_scoped_gate.py @@ -21,6 +21,20 @@ def _scoped_fixture(tmp_path, monkeypatch, provider, route="source", *, with_gate=True): isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, source = _write_fixture(tmp_path) + _append_newly_due_monitor(project, priority="P1", watch_only=True) + todos = list_goal_todos(registry_path=source, goal_id=GOAL_ID)["todos"] + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=todos, + handoff_mode="soft_claim", + ) + initialize_canonical_authority( + runtime, + GOAL_ID, + projection, + state_path=project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md", + provider=provider, + ) update_goal_todo(registry_path=source, goal_id=GOAL_ID, todo_id=TODO_ID, claimed_by=AGENT_ID, agent_id=AGENT_ID, priority="P0") gated = add_goal_todo(registry_path=source, goal_id=GOAL_ID, role="agent", @@ -28,12 +42,12 @@ def _scoped_fixture(tmp_path, monkeypatch, provider, route="source", *, with_gat gate = (add_goal_todo(registry_path=source, goal_id=GOAL_ID, role="user", text="Decide the gated work", task_class="user_gate", bound_agent=AGENT_ID, blocks_agent=AGENT_ID, unblocks_todo_id=gated["todo_id"], agent_id=AGENT_ID) if with_gate else None) - _append_newly_due_monitor(project, priority="P1", watch_only=True) - if provider != "legacy": - todos = list_goal_todos(registry_path=source, goal_id=GOAL_ID)["todos"] - projection = build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim") - initialize_canonical_authority(runtime, GOAL_ID, projection, - state_path=project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md", provider=provider) + if gate is not None: + gate = list_goal_todos( + registry_path=source, + goal_id=GOAL_ID, + todo_id=gate["todo_id"], + )["todo"] registry = source if route == "global": registry = runtime / "registry.global.json" @@ -44,7 +58,7 @@ def _scoped_fixture(tmp_path, monkeypatch, provider, route="source", *, with_gat return project, runtime, source, registry, gate -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("route", ["source", "global"]) @pytest.mark.parametrize("gate_change", ["none", "targets_monitor", "unknown_scope", "global_gate", "primary_blocked"]) def test_emitted_auxiliary_command_honors_an_unrelated_gate(tmp_path, monkeypatch, provider, route, gate_change): @@ -111,7 +125,7 @@ def emitted(): assert rc == 0 and after["selected_todo"]["todo_id"] == TODO_ID, after -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_gate_changed_after_preflight_cannot_commit_observation(tmp_path, monkeypatch, provider): from loopx.quota import record_quota_monitor_poll from loopx.status import collect_status @@ -137,14 +151,10 @@ def invoke(): target_key="due-monitor-fixture", result_hash="commit-observation", cadence="30m", turn_instance_id=turn, receipt_bound_todo_id=TODO_ID, available_capabilities=["network", "external_evidence_poll"]) - if provider == "legacy": - result = invoke() - assert result["ok"] is False and "current User gate dependencies" in result["reason"], result - else: - from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable - with pytest.raises(LocalCoordinationAuthorityUnavailable, match="current User gate dependencies") as rejected: - invoke() - assert rejected.value.payload["no_effect"]["operation_id"] + rejected = invoke() + assert rejected["ok"] is False + assert rejected["error_code"] == "monitor_poll_rejected" + assert "current User gate dependencies" in rejected["reason"] row = list_goal_todos(registry_path=source, goal_id=GOAL_ID, todo_id=DUE_MONITOR_TODO_ID)["todo"] assert row.get("result_hash") is None @@ -164,13 +174,12 @@ def invoke(): assert _spend_run_count(runtime) == 0 -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("business_committed", [False, True]) def test_old_pending_plan_preserves_identity_but_not_new_write_authority(tmp_path, monkeypatch, provider, business_committed): from loopx.quota import record_quota_monitor_poll from loopx.status import collect_status from loopx.control_plane.quota import monitor_poll - from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable project, runtime, source, registry, gate = _scoped_fixture(tmp_path, monkeypatch, provider) turn = "historical-auxiliary-plan-turn" rc, guard = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, @@ -206,17 +215,16 @@ def invoke(): assert recovered["ok"] is True, recovered assert invoke()["replayed"] is True else: - if provider == "legacy": - assert invoke()["ok"] is False - else: - with pytest.raises(LocalCoordinationAuthorityUnavailable, match="current User gate dependencies"): - invoke() + rejected = invoke() + assert rejected["ok"] is False + assert rejected["error_code"] == "monitor_poll_rejected" + assert "current User gate dependencies" in rejected["reason"] row = list_goal_todos(registry_path=source, goal_id=GOAL_ID, todo_id=DUE_MONITOR_TODO_ID)["todo"] assert row.get("result_hash") is None assert _spend_run_count(runtime) == 0 -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_no_gate_auxiliary_command_retains_commit_and_replay_parity(tmp_path, monkeypatch, provider): project, runtime, _source, registry, _gate = _scoped_fixture(tmp_path, monkeypatch, provider, with_gate=False) turn = "auxiliary-no-gate-parity" diff --git a/tests/control_plane/test_backup_registered_source_retention.py b/tests/control_plane/test_backup_registered_source_retention.py index 5e73573908..ab16e8f98a 100644 --- a/tests/control_plane/test_backup_registered_source_retention.py +++ b/tests/control_plane/test_backup_registered_source_retention.py @@ -1,6 +1,5 @@ """Whole-source backup is distinct from canonical projection or reactivation.""" import json -import hashlib import os import sqlite3 import subprocess @@ -9,12 +8,10 @@ from pathlib import Path import pytest -import loopx from loopx.control_plane.effect_runtime import restart_effect_runtime from loopx.state_backup import build_state_backup_plan, execute_state_backup_plan from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime -from tests.control_plane.shadow_e2e_fixture import workspace @pytest.fixture(autouse=True) @@ -141,137 +138,3 @@ def test_backup_retains_original_registry_outside_conventional_directories(tmp_p source = next(item for item in manifest["included"] if item["source_path"] == str(registry)) with tarfile.open(result["archive_path"], "r:gz") as archive: assert archive.extractfile(source["archive_path"]).read() == original - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_cold_import_then_new_writes_full_backup_and_independent_history_readback(tmp_path, provider): - """Recover inert bytes and real provider history, without activating a copy. - - The old source includes actual capture/rollback and released lease receipts. - A canonical archive alone cannot witness those original files. All commands - run against disposable Goals, including when invoked by a wheel interpreter. - """ - fixture = workspace(tmp_path / "source", bootstrap=False) - project_registry = fixture.state.parent / ".loopx/registry.json" - project_registry.parent.mkdir() - fixture.registry.rename(project_registry) - fixture.registry = project_registry - fixture.cli("coordination-shadow", "bootstrap", "--execute") - todo_id = fixture.add("Original requirement before cold import")["todo_id"] - lease = fixture.cli("task-lease", "acquire", "--todo-id", todo_id, "--owner", "agent-a", - "--idempotency-key", "old-execution")["lease"] - released = fixture.cli("task-lease", "release", "--todo-id", todo_id, "--owner", "agent-a", - "--idempotency-key", "old-execution", "--expected-version", str(lease["version"]))["lease"] - revision = fixture.cli("coordination-shadow", "inspect")["inspection"]["provider_revision"] - fixture.cli("coordination-shadow", "rollback", "--provider-revision", revision, "--execute") - archived_body = "完整且未被引用的历史要求 " * 100 - with fixture.state.open("a", encoding="utf-8") as stream: - stream.write("\n## Completed Work Archive\n\n- [x] " + archived_body + "\n" - " \n") - registry_document = json.loads(fixture.registry.read_text()) - registry_document["goals"][0]["extension"] = {"unknown": None, "disabled": False, "number": 1} - fixture.registry.write_text(json.dumps(registry_document)) - original_state = fixture.state.read_bytes() - historical = {str(path.relative_to(fixture.runtime)): path.read_bytes() - for root in (fixture.runtime / "authority-shadow", fixture.runtime / "authority-transition", - fixture.runtime / "goals/goal-e2e/task-leases") - for path in root.rglob("*") if path.is_file() and not path.name.endswith(".lock")} - assert historical and any("rollback" in path for path in historical) - - def command(*args, runtime=fixture.runtime, registry=fixture.registry, success=True): - child = subprocess.run([sys.executable, "-c", - "import loopx,runpy,sys; print(loopx.__file__,file=sys.stderr); runpy.run_module('loopx.cli',run_name='__main__')", - "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", *map(str, args)], - cwd=tmp_path, env={**os.environ, "LOOPX_USAGE_PING": "0"}, - capture_output=True, text=True, timeout=90) - assert str(Path(loopx.__file__).resolve()) in child.stderr - assert (child.returncode == 0) is success, child.stdout + child.stderr - return json.loads(child.stdout) - - def backup(name): - return command("backup-state", "--project", fixture.state.parent, "--output-dir", tmp_path / "backups", - "--backup-id", name, "--current-project-only", "--no-skills", "--no-automations", "--execute") - - def authority(root): - # Fresh selected provider, no registry adoption or Markdown projection. - module = Path(loopx.__file__).parent / "control_plane/coordination/local_authority_provider.ts" - script = (f"import {{openLocalAuthorityStoreHandle}} from {json.dumps(module.as_uri())};" - f"const {{store}}=await openLocalAuthorityStoreHandle({json.dumps(str(root))},'goal-e2e');" - "console.log(JSON.stringify({head:await store.loadAuthority(), history:await store.scanCommitted(null,1000)}));") - result = subprocess.run(["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", script], - cwd=tmp_path, capture_output=True, text=True, check=True, timeout=45) - return json.loads(result.stdout) - - saved = backup("before-import") - prepared = fixture.cli("coordination-shadow", "prepare-import", "--operation-id", "cold-original", - "--backup-manifest", saved["manifest_path"], "--provider", provider, - "--target-handoff-mode", "hard_lease")["cold_import"] - assert prepared["status"] == "prepared", prepared - applied = fixture.cli("coordination-shadow", "apply-import", "--operation-id", "cold-original", - "--plan-sha256", prepared["plan_sha256"], "--writers-stopped", "--execute")["cold_import"] - assert applied["status"] == "applied" and not applied["execution_authority_granted"] - later = fixture.cli("todo", "add", "--role", "agent", "--text", "New canonical data after import", - "--operation-id", "after-import", "--claimed-by", "agent-a") - assert later["ok"] is True - observed = authority(fixture.runtime) - expected = {row["todo_id"]: row for row in observed["head"]["head"]["todos"]} - assert {todo_id, "todo_unreferenced", later["todo_id"]} <= expected.keys() - assert expected["todo_unreferenced"]["text"] == archived_body.strip() - assert expected["todo_unreferenced"]["evidence"] == "original" - assert fixture.cli("task-lease", "inspect", "--todo-id", todo_id)["lease"] == released - - authority_archive = tmp_path / "canonical.ndjson" - command("authority-archive", "export", "--goal-id", fixture.goal, "--archive", authority_archive) - verified = command("authority-archive", "verify", "--archive", authority_archive)["archive"] - assert int(verified["commits"]) >= 2 - post_import_state = fixture.state.read_bytes() - saved = backup("after-import") - assert saved["ok"] is True - copy = tmp_path / "independent-empty-recovery" - assert not copy.exists() - with tarfile.open(saved["archive_path"]) as archive: - archive.extractall(copy, filter="data") - manifest = json.loads((copy / "manifest.json").read_bytes()) - for member in manifest["execution"]["file_members"]: - data = (copy / member["archive_path"]).read_bytes() - assert len(data) == member["size_bytes"] - assert hashlib.sha256(data).hexdigest() == member["sha256"] - routes = {row["key"]: row["archive_path"] for row in saved["included"]} - assert (copy / routes["registry_active_state:goal-e2e"]).read_bytes() == post_import_state - assert (copy / routes["configuration_source_registry"]).read_bytes() == fixture.registry.read_bytes() - configuration = json.loads((copy / "configuration-backup.json").read_bytes()) - assert configuration["data"]["goals"][0]["goal_configuration"]["extension"] == registry_document["goals"][0]["extension"] - for relative, data in historical.items(): - assert (copy / "runtime-root" / relative).read_bytes() == data - # The original source and its later projection have separate saved copies. - with tarfile.open(Path(saved["archive_path"]).parent / "loopx-state-before-import.tar.gz") as archive: - original_manifest = json.load(archive.extractfile("manifest.json")) - original_route = next(row["archive_path"] for row in original_manifest["included"] - if row["key"] == "registry_active_state:goal-e2e") - assert archive.extractfile(original_route).read() == original_state - - # Remove access to every source route: the recovery cannot accidentally read - # the live registry, original store or old absolute paths in historical data. - fixture.state.parent.rename(tmp_path / "unavailable-original") - restored_runtime = copy / "runtime-root" - recovered_registry = copy / routes["configuration_source_registry"] - audited = command("authority-archive", "audit", "--goal-id", fixture.goal, "--archive", authority_archive, - "--archive-sha256", verified["archive_sha256"], runtime=restored_runtime, registry=recovered_registry) - assert audited["audit"]["status"] == "matched" and audited["audit"]["scope"] == "exact" - assert audited["audit"]["compared_commits"] == verified["commits"] - recovered = authority(restored_runtime) - assert recovered == observed - assert len(recovered["history"]["transactions"]) == int(verified["commits"]) - assert next(row for row in recovered["head"]["head"]["leases"] if row["todo_id"] == todo_id) == released - assert recovered["history"]["transactions"][0]["operation_id"] == applied["operation_id"] - # This is isolated data recovery, not an alternate live writer or Host grant. - assert not fixture.state.parent.exists() - selected_directory = restored_runtime / "authority" / f"{provider}-v0" - selected_directory.rename(tmp_path / "unavailable-selected-provider") - rejected = command("authority-archive", "audit", "--goal-id", fixture.goal, "--archive", authority_archive, - "--archive-sha256", verified["archive_sha256"], runtime=restored_runtime, - registry=recovered_registry, success=False) - assert rejected["status"] == "failed" and rejected["authority_changed"] is False - assert not selected_directory.exists() - if provider == "sqlite": - assert not (restored_runtime / "authority/file-v0").exists() diff --git a/tests/control_plane/test_canonical_authority_source.py b/tests/control_plane/test_canonical_authority_source.py index 59c75d862f..a9b91dfd65 100644 --- a/tests/control_plane/test_canonical_authority_source.py +++ b/tests/control_plane/test_canonical_authority_source.py @@ -10,6 +10,7 @@ import pytest +from canonical_authority_fixture import isolate_sqlite_runtime from loopx.todos import add_goal_todo from loopx.chat_action_store import ChatActionStore from loopx.chat_actions import ChatActionService @@ -19,8 +20,15 @@ @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("entrypoint", ["cli", "chat"]) -def test_completion_rejects_registration_revoked_by_real_validation(tmp_path: Path, provider: str, entrypoint: str): - registry, _state = fixture(tmp_path, True, provider) +def test_completion_rejects_registration_revoked_by_real_validation( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + provider: str, + entrypoint: str, +) -> None: + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, _state = fixture(tmp_path, provider) marker = tmp_path / "validation-ran" # The validation is real and succeeds. It revokes this synthetic actor # between admission and commit; no mock supplies the postcondition. @@ -68,10 +76,11 @@ def test_completion_rejects_registration_revoked_by_real_validation(tmp_path: Pa headers={"Content-Type": "application/json"}) response = connection.getresponse() payload = json.loads(response.read()) - assert response.status == 400, payload - assert "authority registration changed" in payload["error"] + assert response.status == 409, payload + assert payload["error_code"] == "action_stale" + assert payload["write_attempted"] is False stored = store.load(proposal["proposal_id"]) - assert stored["status"] == "failed" + assert stored["status"] == "stale" assert not stored.get("receipt"), "Chat must not report canonical success" finally: connection.close() diff --git a/tests/control_plane/test_canonical_claim_transfer.py b/tests/control_plane/test_canonical_claim_transfer.py index 0d830beb13..5a87bda83a 100644 --- a/tests/control_plane/test_canonical_claim_transfer.py +++ b/tests/control_plane/test_canonical_claim_transfer.py @@ -8,8 +8,9 @@ import pytest from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime +from loopx.control_plane.coordination.local_authority import CanonicalAuthorityRequired from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection -from loopx.control_plane.work_items.task_lease import TaskLeaseError, transfer_task_lease +from loopx.control_plane.work_items.task_lease import transfer_task_lease REPO = Path(__file__).resolve().parents[2] @@ -98,10 +99,24 @@ def cli(command, *args, expected_exit=0): def test_claim_transfer_never_emulates_a_legacy_two_write_handover(tmp_path): registry = tmp_path / "registry.json" - registry.write_text(json.dumps({"goals": []})) - with pytest.raises(TaskLeaseError, match="requires canonical authority") as caught: - transfer_task_lease(registry_path=registry, runtime_root=tmp_path / "runtime", goal_id="example", + runtime = tmp_path / "runtime" + registry.write_text(json.dumps({ + "common_runtime_root": str(runtime), + "goals": [{ + "id": "example", + "repo": str(tmp_path), + "state_file": "state.md", + "coordination": { + "registered_agents": ["agent-a", "agent-b"], + "agent_model": "peer_v1", + }, + }], + })) + with pytest.raises(CanonicalAuthorityRequired, match="Canonical authority is required") as caught: + transfer_task_lease(registry_path=registry, runtime_root=runtime, goal_id="example", todo_id="todo_example", owner="agent-a", idempotency_key="source-a", expected_version=1, new_owner="agent-b", new_idempotency_key="receiver-b", transfer_claim=True) - assert caught.value.code == "claim_transfer_requires_canonical_authority" - assert not (tmp_path / "runtime").exists() + assert caught.value.code == "canonical_authority_required" + assert caught.value.payload["operation"] == "task_lease_transfer" + assert caught.value.payload["write_admitted"] is False + assert not (runtime / "goals/example/task-leases/todo_example.json").exists() diff --git a/tests/control_plane/test_canonical_todo_writer_isolation.py b/tests/control_plane/test_canonical_todo_writer_isolation.py index 68a4942c0c..68b88847b0 100644 --- a/tests/control_plane/test_canonical_todo_writer_isolation.py +++ b/tests/control_plane/test_canonical_todo_writer_isolation.py @@ -1,7 +1,6 @@ """Canonical callers must not load unpromoted Todo writers or capture producers.""" from __future__ import annotations -import ast import json import os from pathlib import Path @@ -18,18 +17,16 @@ import test_quota_settlement_cli as settlement -@pytest.mark.parametrize("module", ["mutation_api", "legacy_mutation"]) -def test_todo_mutation_import_does_not_load_status(module): +def test_todo_mutation_import_does_not_load_status(): result = subprocess.run( [sys.executable, "-c", "import importlib,sys;sys.modules['loopx.status']=None;" - f"owner=importlib.import_module('loopx.control_plane.todos.{module}');" + "owner=importlib.import_module('loopx.control_plane.todos.mutation_api');" "assert owner.ARCHIVE_COMPLETED_DEFAULT_MAX_ACTIVE_DONE == 10"], cwd=Path(__file__).resolve().parents[2], capture_output=True, text=True, timeout=45, ) assert result.returncode == 0, result.stdout + result.stderr - @pytest.fixture(scope="session") def retirement_wheel(): # Supply a normally built artifact: unit runs must not build the frontend @@ -77,107 +74,27 @@ def isolated_todo_distribution(tmp_path, monkeypatch, request): # The packaged typed owner must be shipped, not borrowed from checkout. assert (package / "control_plane/todos/succession.ts").is_file() assert (package / "control_plane/effect_runtime_server.ts").is_file() + _assert_source_todo_writers_absent(package) return package, command @pytest.fixture def without_source_todo_writers(isolated_todo_distribution): - package, command = isolated_todo_distribution - _remove_source_todo_writers(package) + _package, command = isolated_todo_distribution return command -def _remove_source_todo_writers(package): - (package / "control_plane/todos/line_update.py").unlink() - (package / "control_plane/todos/legacy_mutation.py").unlink(missing_ok=True) - capture_adapter = package / "control_plane/coordination/runtime_shadow_writer_adapter.py" - source = capture_adapter.read_text() - lines = source.splitlines(keepends=True) - producers = {"begin_todo_runtime_shadow_capture", "require_runtime_shadow_capture_prepared", - "write_captured_todo_state", "settle_todo_runtime_shadow_capture"} - definitions = [node for node in ast.parse(source).body - if isinstance(node, ast.FunctionDef) and node.name in producers] - assert {node.name for node in definitions} == producers - for node in reversed(definitions): - del lines[node.lineno - 1:node.end_lineno] - capture_adapter.write_text("".join(lines)) - - -def _remove_monitor_source_modules(package): - (package / "control_plane/scheduler/legacy_monitor_poll.py").unlink() - for name in ("runtime_shadow", "runtime_shadow_writer_adapter", - "local_authority_shadow_adapter", "local_authority_shadow_outbox"): - (package / f"control_plane/coordination/{name}.py").unlink() - - -@pytest.mark.parametrize("window", ["before_commit", "after_commit"]) -@pytest.mark.parametrize("corrupt_cursor", [False, True], ids=["recover", "reject-unproved-cursor"]) -def test_pending_source_outbox_survives_producer_retirement( - tmp_path, isolated_todo_distribution, window, corrupt_cursor, -): - from shadow_e2e_fixture import workspace - - package, command = isolated_todo_distribution - w = workspace(tmp_path / "shadow", bootstrap=False) - w.command, w.cwd, w.package = tuple(command), tmp_path, package - assert w.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" - original = w.add("Already delivered source work")["todo_id"] - w.crash(window, "todo", "add", "--role", "agent", "--text", "Pending source work") - - def history(): - result = subprocess.run( - [sys.executable, "-c", - "import json,sys;from pathlib import Path;" - "from loopx.control_plane.coordination.local_authority_shadow_adapter " - "import read_local_authority_shadow;" - "print(json.dumps(read_local_authority_shadow(runtime_root=Path(sys.argv[1])," - "goal_id=sys.argv[2],scan_limit=10000)))", str(w.runtime), w.goal], - cwd=tmp_path, capture_output=True, text=True, timeout=45, - ) - assert result.returncode == 0, result.stdout + result.stderr - return json.loads(result.stdout) +def _assert_source_todo_writers_absent(package): + retired = ( + "control_plane/todos/legacy_mutation.py", + "control_plane/todos/line_update.py", + "control_plane/scheduler/legacy_monitor_poll.py", + "control_plane/todos/handoff_mode_source.py", + "control_plane/coordination/runtime_shadow_writer_adapter.py", + "control_plane/coordination/handoff_mode_legacy_plan.ts", + ) + assert all(not (package / relative).exists() for relative in retired) - before = history() - assert len(before["proof"]["transactions"]) == (2 if window == "before_commit" else 3) - assert original in {todo["todo_id"] for todo in before["head"]["todos"]} - # Use the existing partition owner rather than assuming its on-disk layout. - from loopx.control_plane.coordination.local_authority_shadow_outbox import partition_directory - directory = partition_directory(w.runtime, w.goal, "todos") - assert len(list(directory.glob("*.prepared.json"))) == 1 - assert len(list(directory.glob("*.committed.json"))) == 1 - source_bytes, registry_bytes = w.state.read_bytes(), w.registry.read_bytes() - _remove_source_todo_writers(package) - - if corrupt_cursor: - cursor_path = directory / "drain-cursor.json" - cursor = json.loads(cursor_path.read_text()) - cursor["last_partition_digest"] = "sha256:" + "f" * 64 - cursor_path.write_text(json.dumps(cursor)) - pending_bytes = {f.name: f.read_bytes() for f in directory.iterdir() if f.is_file()} - result = w.drain() - assert result["ok"] is False, result - assert result["reason_code"] == "outbox_cursor_unproved", result - assert {f.name: f.read_bytes() for f in directory.iterdir() if f.is_file()} == pending_bytes - assert history()["proof"]["transactions"] == before["proof"]["transactions"] - else: - result = w.drain() - assert result["ok"] is True, result - assert result["delivered"] == (1 if window == "before_commit" else 0) - assert result["replayed"] == (0 if window == "before_commit" else 1) - recovered = history() - transactions = recovered["proof"]["transactions"] - assert transactions[:len(before["proof"]["transactions"])] == before["proof"]["transactions"] - assert len(transactions) == 3 - assert transactions[-1]["receipts"][0]["write_class"] == "todo_add" - assert transactions[-1]["receipts"][0]["seq"] == 2 - assert {todo["text"] for todo in recovered["head"]["todos"]} == { - "Already delivered source work", "Pending source work"} - assert json.loads((directory / "drain-cursor.json").read_text())["last_seq"] == 2 - assert sorted(f.name for f in directory.iterdir()) == ["drain-cursor.json"] - assert w.drain()["outcome"] == "nothing_pending" - assert history()["proof"]["transactions"] == transactions - assert w.state.read_bytes() == source_bytes - assert w.registry.read_bytes() == registry_bytes @pytest.mark.parametrize("provider", ["file", "sqlite"]) @@ -452,104 +369,30 @@ def guard(turn): assert settlement._spend_run_count(runtime) == 1 -def test_explicit_source_writer_import_preserves_the_existing_seam(tmp_path): +def test_retired_source_writer_modules_and_exports_are_absent(): result = subprocess.run( [sys.executable, "-c", """ +import importlib.util import sys -import sysconfig import loopx.todos as todos -assert 'loopx.control_plane.todos.line_update' not in sys.modules -assert 'loopx.control_plane.todos.legacy_mutation' not in sys.modules -from loopx.todos import apply_todo_update_to_lines -from loopx.control_plane.todos import line_update -assert apply_todo_update_to_lines is line_update.apply_todo_update_to_lines -for name in ('link_generated_successor_todo_ids', 'link_superseding_todo_id', 'upsert_todo_metadata'): - assert getattr(todos, name) is getattr(line_update, name) -try: - getattr(todos, 'unknown_writer_export') -except AttributeError: - pass -else: - raise AssertionError('unknown export must remain unavailable') +for module in ( + 'loopx.control_plane.todos.line_update', + 'loopx.control_plane.todos.legacy_mutation', + 'loopx.control_plane.scheduler.legacy_monitor_poll', + 'loopx.control_plane.todos.handoff_mode_source', + 'loopx.control_plane.coordination.runtime_shadow_writer_adapter', +): + assert module not in sys.modules + assert importlib.util.find_spec(module) is None +for name in ( + 'apply_todo_update_to_lines', + 'link_generated_successor_todo_ids', + 'link_superseding_todo_id', + 'successor_add_kwargs', + 'upsert_todo_metadata', +): + assert not hasattr(todos, name) """], capture_output=True, text=True, timeout=45, ) assert result.returncode == 0, result.stdout + result.stderr - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("material_change", [False, True]) -@pytest.mark.parametrize("source_writers_present", [False, True], ids=["absent", "present"]) -def test_monitor_recovery_with_source_writer_isolation( - tmp_path, request, provider, material_change, source_writers_present, isolated_todo_distribution, -): - from test_leased_monitor_poll import LEASE, PROOF - from test_monitor_followthrough_contract import GOAL_ID, AGENT_ID - from test_native_monitor_poll import _canonical - - _package, command = isolated_todo_distribution - if not source_writers_present: - command = request.getfixturevalue("without_source_todo_writers") - _remove_monitor_source_modules(_package) - registry, runtime, display, monitor = _canonical( - tmp_path, native=True, provider=provider, lease=LEASE, - ) - display.unlink() - - def cli(*args, succeeds=True): - result = subprocess.run( - [*command, "--format", "json", - "--registry", str(registry), "--runtime-root", str(runtime), *args], - cwd=tmp_path, capture_output=True, text=True, timeout=45, - ) - assert (result.returncode == 0) is succeeds, result.stdout + result.stderr - return json.loads(result.stdout) - - def current(): - return cli("todo", "list", "--goal-id", GOAL_ID) - - identity = ["--goal-id", GOAL_ID, "--agent-id", AGENT_ID, - "--runtime-profile", "generic_cli", "--turn-instance-id", "monitor-retirement", - "--available-capability", "network", "--available-capability", "external_evidence_poll"] - guard = cli("quota", "should-run", *identity) - assert guard["selected_todo"]["todo_id"] == monitor["todo_id"] - before = current() - poll = ["quota", "monitor-poll", *identity, "--todo-id", monitor["todo_id"], - "--result-hash", "observed-revision", "--use-current-task-lease", "--execute"] - if material_change: - poll += ["--material-change", "--next-agent-todo", "Validate the observed revision", - "--next-action-kind", "validate"] - - # A missing provider must not use the display or create another authority. - backend = runtime / "authority" / f"{provider}-v0" - offline = backend.with_name(backend.name + "-offline") - backend.rename(offline) - try: - failed = cli(*poll, succeeds=False) - assert failed["ok"] is False - assert not backend.exists() and not display.exists() - finally: - offline.rename(backend) - assert current()["todos"] == before["todos"] - - committed = cli(*poll) - assert committed["todo_writeback"]["lease_proof"] == PROOF - assert committed["todo_writeback"]["material_change_generation"] == int(material_change) - after = current() - assert len(after["todos"]) == len(before["todos"]) + int(material_change) - observed = next(t for t in after["todos"] if t["todo_id"] == monitor["todo_id"]) - assert observed["result_hash"] == "observed-revision" - assert int(observed["consecutive_no_change"]) == int(not material_change) - assert display.exists() - - # Recover the same observation after the execution lease is released. - assert cli("task-lease", "release", "--goal-id", GOAL_ID, - "--todo-id", monitor["todo_id"], "--owner", AGENT_ID, - "--idempotency-key", PROOF["idempotency_key"], - "--expected-version", str(PROOF["expected_version"]))["released"] - assert cli(*poll)["replayed"] is True - assert current()["todos"] == after["todos"] - rows = [json.loads(line) for line in - (runtime / "goals" / GOAL_ID / "runs/index.jsonl").read_text().splitlines()] - assert sum(row.get("classification") == "quota_monitor_poll" for row in rows) == 1 - assert all(row.get("classification") != "quota_slot_spend" for row in rows) diff --git a/tests/control_plane/test_canonical_update_dispatch.py b/tests/control_plane/test_canonical_update_dispatch.py index 98fd55e39b..8caa930ed3 100644 --- a/tests/control_plane/test_canonical_update_dispatch.py +++ b/tests/control_plane/test_canonical_update_dispatch.py @@ -11,10 +11,11 @@ from canonical_authority_fixture import isolate_sqlite_runtime, promoted_create_fixture from loopx.control_plane.coordination.local_authority import ( + CanonicalAuthorityRequired, LocalCoordinationAuthorityUnavailable, read_canonical_todos_if_promoted, ) -from loopx.todos import add_goal_todo, update_goal_todo +from loopx.todos import add_goal_todo, complete_goal_todo, update_goal_todo @pytest.fixture(params=["file", "sqlite"]) @@ -27,14 +28,12 @@ def update_workspace(tmp_path, monkeypatch, request): return registry, runtime, state, created["todo_id"] -@pytest.fixture(params=[False, True], ids=["writer-present", "writer-absent"]) -def cli_update(tmp_path, monkeypatch, request, update_workspace): +@pytest.fixture +def cli_update(tmp_path, monkeypatch, update_workspace): package_root = tmp_path / "package" package = package_root / "loopx" shutil.copytree(Path(__file__).resolve().parents[2] / "loopx", package, ignore=shutil.ignore_patterns("__pycache__")) - if request.param: - (package / "control_plane/todos/line_update.py").unlink() provenance = tmp_path / "cli-origin.txt" (package_root / "sitecustomize.py").write_text( "import importlib.util\nfrom pathlib import Path\n" @@ -135,7 +134,10 @@ def test_public_facade_empty_edits_use_the_typed_decoder(update_workspace): assert state.read_bytes() == display -def test_unpromoted_facade_preserves_empty_edit_no_change(tmp_path): +@pytest.mark.parametrize("dry_run", [False, True], ids=["execute", "dry-run"]) +def test_unpromoted_facade_rejects_create_and_update_without_writes( + tmp_path, dry_run, +): project = tmp_path / "project" project.mkdir() state = project / "state.md" @@ -144,11 +146,48 @@ def test_unpromoted_facade_preserves_empty_edit_no_change(tmp_path): registry.write_text(json.dumps({"schema_version": 1, "common_runtime_root": str(tmp_path / "runtime"), "goals": [{"id": "goal-a", "repo": str(project), "state_file": "state.md", "coordination": {"registered_agents": ["agent-a"]}}]})) - created = add_goal_todo(registry_path=registry, goal_id="goal-a", role="agent", - text="Keep legacy compatibility", agent_id="agent-a", note="Keep the note") before = state.read_bytes() - for intent in [{}, {"note": ""}, {"note": " \t\n"}]: - result = update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id=created["todo_id"], - agent_id="agent-a", **intent) - assert result["ok"] and result["changed"] is False - assert state.read_bytes() == before + with pytest.raises(CanonicalAuthorityRequired) as create_error: + add_goal_todo( + registry_path=registry, + goal_id="goal-a", + role="agent", + text="Migrate before creating this Todo", + agent_id="agent-a", + dry_run=dry_run, + ) + with pytest.raises(CanonicalAuthorityRequired) as update_error: + update_goal_todo( + registry_path=registry, + goal_id="goal-a", + todo_id="todo_existing", + note="Migrate before editing this Todo", + agent_id="agent-a", + dry_run=dry_run, + ) + with pytest.raises(CanonicalAuthorityRequired) as terminal_error: + complete_goal_todo( + registry_path=registry, + goal_id="goal-a", + todo_id="todo_existing", + agent_id="agent-a", + evidence="Migrate before completing this Todo", + no_followup=True, + dry_run=dry_run, + ) + for error, operation in ( + (create_error.value, "todo_create"), + (update_error.value, "todo_update"), + (terminal_error.value, "todo_terminal"), + ): + assert error.code == "canonical_authority_required" + assert error.payload["operation"] == operation + assert error.payload["write_admitted"] is False + assert error.payload["decision_read_from_provider"] is False + assert error.payload["legacy_fallback_used"] is False + assert error.payload["recovery"] == { + "action": "migrate_goal_authority", + "backup_required": True, + } + assert state.read_bytes() == before + assert not (tmp_path / "runtime" / "authority").exists() diff --git a/tests/control_plane/test_chat_cold_source_import.py b/tests/control_plane/test_chat_cold_source_import.py index e3ca2d93ab..b64a9681dc 100644 --- a/tests/control_plane/test_chat_cold_source_import.py +++ b/tests/control_plane/test_chat_cold_source_import.py @@ -14,17 +14,19 @@ from test_cold_source_import_cli import workspace -@pytest.fixture(params=[True, False], ids=["producers-present", "four-producers-absent"]) -def capture_present(request): +@pytest.fixture(params=[True, False], ids=["source-modules-present", "source-modules-absent"]) +def source_modules_present(request): return request.param @pytest.fixture(params=[False, True], ids=["physical-registry", "external-registry-parent-alias"]) -def cold_api(tmp_path, monkeypatch, request, capture_present): +def cold_api(tmp_path, monkeypatch, request, source_modules_present): _, state, _, body, runtime, receiver, env = workspace(tmp_path, monkeypatch) - if not capture_present: + assert not ( + receiver / "loopx/control_plane/coordination/runtime_shadow_writer_adapter.py" + ).exists() + if not source_modules_present: for name in ("todos.py", "bootstrap.py", - "control_plane/coordination/runtime_shadow_writer_adapter.py", "control_plane/coordination/local_authority_shadow_outbox.py"): (receiver / "loopx" / name).unlink() registry = tmp_path / "project/.loopx/registry.json" diff --git a/tests/control_plane/test_chat_gate_decisions.py b/tests/control_plane/test_chat_gate_decisions.py index 6f8e57dd11..8263532394 100644 --- a/tests/control_plane/test_chat_gate_decisions.py +++ b/tests/control_plane/test_chat_gate_decisions.py @@ -12,6 +12,10 @@ ) from loopx.chat_action_store import ChatActionStore from loopx.chat_actions import ChatActionService, ProtectedActionGate +from loopx.control_plane.coordination.coordination_state_contract import ( + TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + TODO_DOMAIN_RECORD_FIELDS, +) from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.todos import add_goal_todo, list_goal_todos @@ -23,16 +27,20 @@ def _goal(tmp_path: Path, monkeypatch, provider: str, handoff_mode: str = "soft_ _, state, registry = _write_fixture(tmp_path) config = json.loads(registry.read_text()) config["common_runtime_root"] = str(tmp_path / "runtime") + config["goals"][0]["coordination"]["handoff_mode"] = handoff_mode registry.write_text(json.dumps(config)) + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, handoff_mode=handoff_mode, todos=[], + ) + projection["todo_read_model"] = { + **projection["todo_read_model"], + "schema_version": TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + "contract_fields": list(TODO_DOMAIN_RECORD_FIELDS), + } + initialize_canonical_authority( + tmp_path / "runtime", GOAL_ID, projection, state_path=state, provider=provider, + ) target, gate = _add_target_and_gate(registry, required_scopes=[PUBLISH_SCOPE], target_status="blocked") - if provider != "legacy": - config["goals"][0]["coordination"]["handoff_mode"] = handoff_mode - registry.write_text(json.dumps(config)) - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, handoff_mode=handoff_mode, - todos=list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"]) - initialize_canonical_authority(tmp_path / "runtime", GOAL_ID, projection, - state_path=state, provider=provider) service = ChatActionService(store=ChatActionStore(tmp_path / "actions"), registry_path=registry) return registry, service, target["todo_id"], gate["todo_id"] @@ -51,15 +59,14 @@ def _rows(registry: Path) -> dict[str, dict]: @pytest.mark.parametrize(("provider", "handoff_mode"), - [("legacy", "soft_claim"), ("file", "hard_lease"), ("sqlite", "soft_claim")]) + [("file", "hard_lease"), ("sqlite", "soft_claim")]) @pytest.mark.parametrize("decision", ["approve", "reject", "cancel"]) def test_app_decision_commits_through_user_completion(tmp_path, monkeypatch, provider, handoff_mode, decision): registry, service, target_id, gate_id = _goal(tmp_path, monkeypatch, provider, handoff_mode) proposal = _preview(service, gate_id, decision) assert proposal["status"] == "preview_ready" basis = proposal.get("canonical_update_basis") - assert (basis or {}).get("schema_version") == ( - None if provider == "legacy" else "loopx_chat_canonical_terminal_basis_v0") + assert (basis or {}).get("schema_version") == "loopx_chat_canonical_terminal_basis_v0" assert _rows(registry)[gate_id]["status"] != "done", "preview must not write" applied = service.apply(proposal["proposal_id"])["proposal"] @@ -75,7 +82,7 @@ def test_app_decision_commits_through_user_completion(tmp_path, monkeypatch, pro assert _rows(registry) == rows -@pytest.mark.parametrize("provider", ["legacy", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_changed_goal_turns_decision_stale_without_writing(tmp_path, monkeypatch, provider): registry, service, target_id, gate_id = _goal(tmp_path, monkeypatch, provider) proposal = _preview(service, gate_id, "approve") diff --git a/tests/control_plane/test_chat_goal_storage.py b/tests/control_plane/test_chat_goal_storage.py index 9da0e1d39b..8f6ef23c51 100644 --- a/tests/control_plane/test_chat_goal_storage.py +++ b/tests/control_plane/test_chat_goal_storage.py @@ -245,17 +245,23 @@ def test_a_preview_cannot_bypass_a_later_host_lease_and_requires_a_fresh_plan(ap assert read(runtime) == copied -@pytest.fixture(params=[False, True], ids=["cold", "retained-capture"]) -def old_api(tmp_path, monkeypatch, request): - from tests.control_plane.test_cold_source_inspection import cold_workspace +@pytest.fixture +def old_api(tmp_path, monkeypatch): from tests.control_plane.shadow_e2e_fixture import workspace isolate_sqlite_runtime(tmp_path, monkeypatch) - ws = workspace(tmp_path) if request.param else cold_workspace(tmp_path) - if request.param: - ws.add("Private original history must stay on disk") - outbox = ws.runtime / "authority-shadow" / "outbox" / ws.goal / "todos" - (outbox / "original-unrecognized.json").write_bytes(b"{malformed original outbox") + ws = workspace(tmp_path, bootstrap=False) + registry = json.loads(ws.registry.read_text()) + registry["goals"][0]["coordination"].pop("runtime_shadow") + ws.registry.write_text(json.dumps(registry)) + ws.state.write_text( + "---\ngoal_id: goal-e2e\nhandoff_mode: soft_claim\n---\n" + "## Agent Todo\n- [ ] Keep active\n" + " \n" + "## Todo Archive\n- [x] Keep archived evidence\n" + " \n" + ) directory = ws.runtime / "goals" / ws.goal / "task-leases" directory.mkdir(parents=True, exist_ok=True) (directory / "removed.json").write_text(json.dumps({ @@ -280,7 +286,7 @@ def call(): assert "old-work" not in encoded and "malformed original outbox" not in encoded return response.status, result - yield call, ws, request.param + yield call, ws, False server.shutdown() thread.join(5) server.server_close() @@ -300,11 +306,12 @@ def test_registered_old_source_read_failure_is_unavailable_and_recovers(old_api) def test_old_source_http_inventory_keeps_history_and_does_not_grant_import(old_api): - from tests.control_plane.test_cold_source_inspection import capture_bytes - call, ws, captured = old_api - before = capture_bytes(ws) source = ws.state.read_bytes(), ws.registry.read_bytes() + leases = { + path: path.read_bytes() + for path in (ws.runtime / "goals" / ws.goal / "task-leases").glob("*.json") + } for _ in range(2): code, result = call() assert code == 200 and result["ok"], result @@ -319,8 +326,8 @@ def test_old_source_http_inventory_keeps_history_and_does_not_grant_import(old_a assert old["writer_stop_verified"] is old["outbox_reconciliation_verified"] is old["import_ready"] is False assert result["authority_changed"] is result["execution_authority_granted"] is False assert "outbox_review" not in json.dumps(result) - assert before == capture_bytes(ws) assert source == (ws.state.read_bytes(), ws.registry.read_bytes()) + assert leases == {path: path.read_bytes() for path in leases} assert not (ws.runtime / "authority").exists() diff --git a/tests/control_plane/test_chat_user_action_handling.py b/tests/control_plane/test_chat_user_action_handling.py index f9685e7484..c05e9cf228 100644 --- a/tests/control_plane/test_chat_user_action_handling.py +++ b/tests/control_plane/test_chat_user_action_handling.py @@ -15,11 +15,15 @@ from test_todo_decision_scope_lifecycle import AGENT_ID, GOAL_ID, _write_fixture from loopx.chat_action_store import ChatActionStore from loopx.chat_actions import ChatActionService +from loopx.control_plane.coordination.coordination_state_contract import ( + TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + TODO_DOMAIN_RECORD_FIELDS, +) from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.todos import add_goal_todo, list_goal_todos -PROVIDERS = [("legacy", "soft_claim"), ("file", "hard_lease"), ("sqlite", "soft_claim")] +PROVIDERS = [("file", "hard_lease"), ("sqlite", "soft_claim")] def _goal(tmp_path: Path, monkeypatch, provider: str, handoff_mode: str): @@ -27,21 +31,25 @@ def _goal(tmp_path: Path, monkeypatch, provider: str, handoff_mode: str): _, state, registry = _write_fixture(tmp_path) config = json.loads(registry.read_text()) config["common_runtime_root"] = str(tmp_path / "runtime") + config["goals"][0]["coordination"]["handoff_mode"] = handoff_mode registry.write_text(json.dumps(config)) + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, handoff_mode=handoff_mode, todos=[], + ) + projection["todo_read_model"] = { + **projection["todo_read_model"], + "schema_version": TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + "contract_fields": list(TODO_DOMAIN_RECORD_FIELDS), + } + initialize_canonical_authority( + tmp_path / "runtime", GOAL_ID, projection, state_path=state, provider=provider, + ) target = add_goal_todo(registry_path=registry, goal_id=GOAL_ID, role="agent", text="Continue once the owner has created the sessions.", status="blocked", task_class="advancement_task", claimed_by=AGENT_ID) action = add_goal_todo(registry_path=registry, goal_id=GOAL_ID, role="user", text="Create the remaining role sessions in the desktop App.", task_class="user_action", bound_agent=AGENT_ID, unblocks_todo_id=target["todo_id"]) - if provider != "legacy": - config["goals"][0]["coordination"]["handoff_mode"] = handoff_mode - registry.write_text(json.dumps(config)) - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, handoff_mode=handoff_mode, - todos=list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"]) - initialize_canonical_authority(tmp_path / "runtime", GOAL_ID, projection, - state_path=state, provider=provider) service = ChatActionService(store=ChatActionStore(tmp_path / "actions"), registry_path=registry) return registry, service, target["todo_id"], action["todo_id"] diff --git a/tests/control_plane/test_closed_monitor_replay.py b/tests/control_plane/test_closed_monitor_replay.py index be38f7a6f2..85cd58c06b 100644 --- a/tests/control_plane/test_closed_monitor_replay.py +++ b/tests/control_plane/test_closed_monitor_replay.py @@ -5,31 +5,24 @@ import pytest -from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime -from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection +from canonical_authority_fixture import isolate_sqlite_runtime from loopx.control_plane.testing.canary_harness import run_json_cli, run_json_cli_result -from loopx.todos import add_goal_todo, list_goal_todos +from loopx.todos import add_goal_todo from tests.control_plane.test_monitor_followthrough_contract import ( AGENT_ID, GOAL_ID, _add_monitor, _write_fixture, ) -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("material", [False, True]) @pytest.mark.parametrize("lifecycle", ["complete", "supersede"]) def test_closed_monitor_replays_original_no_spend_turn( tmp_path, monkeypatch, provider, material, lifecycle, ): isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state = _write_fixture(tmp_path) + registry, runtime, _state = _write_fixture(tmp_path, provider=provider) monitor = _add_monitor(registry, text="Observe a public target", target_key="public-target", next_due_at="2000-01-01T00:00:00Z") - if provider != "legacy": - todos = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] - projection = build_todo_runtime_shadow_projection(goal_id=GOAL_ID, - todos=todos, handoff_mode="soft_claim") - initialize_canonical_authority(runtime, GOAL_ID, projection, - state_path=state, provider=provider) def call(*args): return run_json_cli(*args, registry_path=registry, runtime_root=runtime) diff --git a/tests/control_plane/test_cold_source_disposition_e2e.py b/tests/control_plane/test_cold_source_disposition_e2e.py deleted file mode 100644 index bcdd9be27e..0000000000 --- a/tests/control_plane/test_cold_source_disposition_e2e.py +++ /dev/null @@ -1,436 +0,0 @@ -"""Retained originals are handled without bringing back their Python producers. - -The original CLI creates real interrupted writes. A separate copied runtime -then dispatches the shipped TS effects with those producer files absent. The -OS-lock adapter remains: language retirement must preserve useful Host IO. -""" -from __future__ import annotations - -import json -import os -from pathlib import Path -import shutil -import subprocess -import sys -import tarfile - -import loopx -import pytest - -from loopx.control_plane.coordination.coordination_state_contract_generated import ( - LOCAL_AUTHORITY_SHADOW_READ_REQUEST_SCHEMA, -) -from tests.control_plane.shadow_e2e_fixture import workspace -from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime - - -pytestmark = pytest.mark.stage2c_e2e - - -@pytest.fixture(scope="module") -def receiver_package(tmp_path_factory): - root = tmp_path_factory.mktemp("receiver") - package = root / "loopx" - shutil.copytree(Path(loopx.__file__).parent, package, - ignore=shutil.ignore_patterns("__pycache__", "testing", "*.pyc")) - removed = ["todos.py", "bootstrap.py", - "control_plane/coordination/runtime_shadow_writer_adapter.py", - "control_plane/coordination/local_authority_shadow_outbox.py"] - for relative in removed: - (package / relative).unlink() - assert all(not (package / relative).exists() for relative in removed) - assert (package / "control_plane/coordination/shadow_lock_host.py").is_file() - return package - - -@pytest.fixture(scope="module") -def receiver(receiver_package): - package = receiver_package - root = package.parent - module = package / "control_plane/effect_runtime_handlers.ts" - script = ( - f"import {{createEffectRuntimeHandlers, dispatchEffectRuntimeMethod}} from {json.dumps(module.as_uri())};" - "let raw=''; for await (const chunk of process.stdin) raw+=chunk;" - "const {method,request}=JSON.parse(raw);" - "const handlers=createEffectRuntimeHandlers({fingerprint:'disposable',requestShutdown:()=>{}});" - "console.log(JSON.stringify(await dispatchEffectRuntimeMethod(handlers,method,request)));" - ) - - def dispatch(method, request): - result = subprocess.run( - ["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", script], - input=json.dumps({"method": method, "request": request}), - cwd=root, capture_output=True, text=True, timeout=45, - ) - assert result.returncode == 0, result.stdout + result.stderr - return json.loads(result.stdout) - - return dispatch - - -def drain(receiver, fixture): - return receiver("coordination.runtime_shadow.drain", { - "schema_version": "loopx_shadow_drain_v0", "runtime_root": str(fixture.runtime), - "goal_id": fixture.goal, "python_executable": sys.executable, "config_enabled": True, - "max_entries": 32, "budget_seconds": 10, "lock_timeout_seconds": 2, - }) - - -def inspect(fixture): - return fixture.cli("coordination-shadow", "inspect-source")["source_inventory"] - - -def originals(path): - return {str(p.relative_to(path)): p.read_bytes() for p in path.rglob("*") - if p.is_file() and not p.name.endswith(".lock")} - - -def cold_workspace(path): - fixture = workspace(path, bootstrap=False) - registry = fixture.state.parent / ".loopx/registry.json" - registry.parent.mkdir() - fixture.registry.rename(registry) - fixture.registry = registry - fixture.cli("coordination-shadow", "bootstrap", "--execute") - return fixture - - -def cold_cli(fixture, receiver_package, action, *args, success=True): - child = subprocess.run([sys.executable, "-c", - "import loopx,sys;print(loopx.__file__,file=sys.stderr);" - "from loopx.entrypoint import main;raise SystemExit(main())", - *fixture.arguments("coordination-shadow", action, *map(str, args))], - cwd=fixture.state.parent.parent, - env={**os.environ, "PYTHONPATH": str(receiver_package.parent)}, - capture_output=True, text=True, timeout=60) - assert str(receiver_package / "__init__.py") in child.stderr - assert (child.returncode == 0) is success, child.stdout + child.stderr - assert "Traceback" not in child.stderr - return json.loads(child.stdout)["cold_import"] - - -def backup(fixture, name): - child = subprocess.run([sys.executable, "-c", - "import loopx,sys;print(loopx.__file__,file=sys.stderr);" - "from loopx.entrypoint import main;raise SystemExit(main())", - "--registry", str(fixture.registry), "--runtime-root", str(fixture.runtime), - "--format", "json", "backup-state", "--project", str(fixture.state.parent), - "--output-dir", str(fixture.state.parent.parent / "backups"), "--backup-id", name, - "--current-project-only", "--no-skills", "--no-automations", "--execute"], - cwd=fixture.state.parent.parent, capture_output=True, text=True, timeout=60) - imported_package = Path(child.stderr.partition("\n")[0]).resolve() - assert imported_package.is_file(), child.stderr - assert imported_package.name == "__init__.py" - assert imported_package.parent.name == "loopx" - assert child.returncode == 0, child.stdout + child.stderr - result = json.loads(child.stdout) - assert result["ok"], result - return result - - -def provider_todos(receiver, fixture): - return receiver("coordination.local_authority.todo_list", { - "schema_version": "loopx_local_coordination_todo_list_request_v0", - "runtime_root": str(fixture.runtime), "goal_id": fixture.goal, - "role": None, "status": None, "todo_id": None, "agent_id": None, "limit": None}) - - -@pytest.mark.parametrize("window,resolution,no_op", [ - ("before_replace", "abandoned", True), - ("before_marker", "committed_proven_by_readback", False), - ("before_commit", "committed", False), - ("after_commit", "committed", False), -]) -def test_stopped_original_todo_recovers_once_without_python_producer( - tmp_path, receiver, window, resolution, no_op, -): - fixture = workspace(tmp_path) - fixture.crash(window, "todo", "add", "--role", "agent", "--text", "Interrupted original") - source = fixture.state.read_bytes() - before = inspect(fixture) - partition = fixture.runtime / "authority-shadow/outbox" / fixture.goal / "todos" - prepared = json.loads(next(partition.glob("*.prepared.json")).read_bytes()) - initial = before["capture"]["runtime_shadow_readback"]["proof"]["last_applied_sequences"]["todos"] - result = drain(receiver, fixture) - assert result["ok"] is True, result - assert result["replayed"] == (1 if window == "after_commit" else 0) - assert result["delivered"] + result["reconciled"] == (0 if window == "after_commit" else 1) - assert fixture.state.read_bytes() == source - after = inspect(fixture) - view = after["capture"]["runtime_shadow_readback"] - receipt_view = receiver("coordination.runtime_shadow.outbox_read", { - "schema_version": LOCAL_AUTHORITY_SHADOW_READ_REQUEST_SCHEMA, - "runtime_root": str(fixture.runtime), "goal_id": fixture.goal, "store_kind": "runtime_shadow", - "scan_after_cursor": None, "scan_limit": 0, "read_model": "proof", - "receipt_operation_id": prepared["entry_id"], - }) - assert receipt_view["status"] == "loaded", receipt_view - receipt = receipt_view["proof"]["receipt"]["receipts"][0] - assert receipt["entry_id"] == prepared["entry_id"] - assert receipt["resolution"] == resolution and receipt["no_op"] is no_op - assert receipt["seq"] == 1 and view["proof"]["last_sequences"]["todos"] == 1 - assert view["proof"]["last_applied_sequences"]["todos"] == (0 if no_op else 1) - assert initial == (1 if window == "after_commit" else 0) - assert [p.name for p in partition.iterdir()] == ["drain-cursor.json"] - retained = originals(fixture.runtime) - assert drain(receiver, fixture)["outcome"] == "nothing_pending" - assert originals(fixture.runtime) == retained - # Successful original disposition is not stopped-Host/import qualification. - assert after["import_ready"] is False and after["writer_stop_verified"] is False - assert not (fixture.runtime / "authority").exists() - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("window", ["before_commit", "after_commit"]) -def test_original_lease_disposition_requires_release_before_cold_import( - tmp_path, monkeypatch, receiver, receiver_package, provider, window, -): - isolate_sqlite_runtime(tmp_path, monkeypatch) - fixture = cold_workspace(tmp_path / "project") - todo = fixture.add("Original leased task")["todo_id"] - fixture.crash(window, "task-lease", "acquire", "--todo-id", todo, "--owner", "agent-a", - "--idempotency-key", "original", "--ttl-seconds", "120") - path = fixture.runtime / "goals" / fixture.goal / "task-leases" / (todo + ".json") - lease = path.read_bytes() - assert drain(receiver, fixture)["ok"] is True - assert path.read_bytes() == lease - after = inspect(fixture) - assert after["leases_requiring_settlement"] == [todo] - assert after["retained_leases"][0]["status"] == "active" - assert after["import_ready"] is False - retained = originals(fixture.runtime) - assert drain(receiver, fixture)["outcome"] == "nothing_pending" - assert originals(fixture.runtime) == retained and path.read_bytes() == lease - inventory = inspect(fixture) - rolled = receiver("coordination.runtime_shadow.rollback", { - "schema_version": "loopx_coordination_runtime_shadow_rollback_v0", - "runtime_root": str(fixture.runtime), "goal_id": fixture.goal, - "operation_id": "retain-lease-original", "expected_bootstrap_operation_id": None, - "expected_provider_revision": inventory["capture"]["runtime_shadow_readback"]["provider_revision"], - "projection": inventory["projection"], "source_snapshot": inventory["source_snapshot"]}) - assert rolled["status"] == "applied", rolled - archived = originals(fixture.runtime / "authority-shadow") - active_backup = backup(fixture, "inactive-capture-active-lease") - before = originals(fixture.runtime) - rejected = cold_cli(fixture, receiver_package, "prepare-import", "--operation-id", "lease-import", - "--backup-manifest", active_backup["manifest_path"], "--provider", provider, - "--target-handoff-mode", "hard_lease", success=False) - assert rejected["reason_code"] == "cold_import_lease_requires_settlement", rejected - assert originals(fixture.runtime) == before and path.read_bytes() == lease - assert not list(fixture.runtime.rglob("writer-fence.json")) - assert not (fixture.runtime / "authority").exists() - # Capture disposition does not settle the live source lease. Its original - # owner must release it; neither its old receipt nor import grants work. - released = fixture.cli("task-lease", "release", "--todo-id", todo, "--owner", "agent-a", - "--idempotency-key", "original", "--expected-version", str(json.loads(lease)["version"])) - assert released["released"] and released["lease"]["status"] == "released", released - settled = path.read_bytes() - fresh_backup = backup(fixture, "released-before-import") - plan = cold_cli(fixture, receiver_package, "prepare-import", "--operation-id", "lease-import", - "--backup-manifest", fresh_backup["manifest_path"], "--provider", provider, - "--target-handoff-mode", "hard_lease") - assert plan["source_inventory"]["lease_count"] == 1 - applied = cold_cli(fixture, receiver_package, "apply-import", "--operation-id", "lease-import", - "--plan-sha256", plan["plan_sha256"], "--writers-stopped", "--execute") - assert applied["status"] == "applied" and applied["execution_authority_granted"] is False - current_lease = fixture.cli("task-lease", "inspect", "--todo-id", todo) - assert current_lease["source_authority"] == f"{provider}_v0" - assert current_lease["lease"] == released["lease"] - fixture.cli("todo", "add", "--role", "agent", "--text", "Later lease-import write", - "--operation-id", "later-lease-import", "--claimed-by", "agent-a") - current = provider_todos(receiver, fixture) - assert {row["text"] for row in current["todos"]} == {"Original leased task", "Later lease-import write"} - fixture.state.unlink() - replay = cold_cli(fixture, receiver_package, "recover-import", "--operation-id", "lease-import", - "--plan-sha256", plan["plan_sha256"], "--execute") - assert replay["status"] == "replayed" and replay["operation_id"] == applied["operation_id"] - assert provider_todos(receiver, fixture) == current - assert fixture.cli("task-lease", "inspect", "--todo-id", todo)["lease"] == released["lease"] - assert path.read_bytes() == settled and originals(fixture.runtime / "authority-shadow") == archived - with tarfile.open(fresh_backup["archive_path"]) as archive: - assert archive.extractfile("runtime-root/" + str(path.relative_to(fixture.runtime))).read() == settled - for relative, data in archived.items(): - assert archive.extractfile("runtime-root/authority-shadow/" + relative).read() == data - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_unproved_original_archival_then_cold_import_keeps_history_inert( - tmp_path, monkeypatch, receiver, receiver_package, provider, -): - isolate_sqlite_runtime(tmp_path, monkeypatch) - fixture = cold_workspace(tmp_path / "project") - fixture.crash("before_marker", "handoff-mode", "set", "--mode", "soft_claim") - fixture.cli("handoff-mode", "set", "--mode", "hard_lease") - source_todo = fixture.add("Later source requirement")["todo_id"] - pending_backup = backup(fixture, "ambiguous-original") - before = originals(fixture.runtime) - result = drain(receiver, fixture) - assert result["ok"] is False and result["reason_code"] == "outbox_source_unproved" - assert originals(fixture.runtime) == before - inventory = inspect(fixture) - source = fixture.state.read_bytes() - artifacts = inventory["capture"]["artifacts"] - candidate = Path(artifacts["runtime_store"]["path"]).read_bytes() - pending = originals(Path(artifacts["outbox"]["path"])) - rejected = cold_cli(fixture, receiver_package, "prepare-import", "--operation-id", "ambiguous-import", - "--backup-manifest", pending_backup["manifest_path"], "--provider", provider, - "--target-handoff-mode", "hard_lease", success=False) - assert rejected["reason_code"] == "cold_import_capture_requires_disposition", rejected - assert originals(fixture.runtime) == before and fixture.state.read_bytes() == source - request = {"schema_version": "loopx_coordination_runtime_shadow_rollback_v0", - "runtime_root": str(fixture.runtime), "goal_id": fixture.goal, - "operation_id": "retain-original", "expected_bootstrap_operation_id": None, - "expected_provider_revision": inventory["capture"]["runtime_shadow_readback"]["provider_revision"], - "projection": inventory["projection"], "source_snapshot": inventory["source_snapshot"]} - rolled = receiver("coordination.runtime_shadow.rollback", request) - assert rolled["status"] == "applied", rolled - assert Path(rolled["candidate_archive_path"]).read_bytes() == candidate - assert originals(Path(rolled["outbox_archive_path"])) == pending - retained = originals(fixture.runtime) - replay = receiver("coordination.runtime_shadow.rollback", request) - assert replay["status"] == "replayed" and replay["operation_id"] == rolled["operation_id"] - assert originals(fixture.runtime) == retained and fixture.state.read_bytes() == source - refused = drain(receiver, fixture) - assert refused["ok"] is False and refused["outcome"] == "stopped" - assert refused["delivered"] == 0 and refused["replayed"] == 0 - assert originals(fixture.runtime) == retained - assert inspect(fixture)["capture"]["outbox_review"] is None - assert not (fixture.runtime / "authority").exists() - archived = originals(fixture.runtime / "authority-shadow") - fresh_backup = backup(fixture, "archived-unproved-original") - plan = cold_cli(fixture, receiver_package, "prepare-import", "--operation-id", "ambiguous-import", - "--backup-manifest", fresh_backup["manifest_path"], "--provider", provider, - "--target-handoff-mode", "hard_lease") - applied = cold_cli(fixture, receiver_package, "apply-import", "--operation-id", "ambiguous-import", - "--plan-sha256", plan["plan_sha256"], "--writers-stopped", "--execute") - assert applied["status"] == "applied" and applied["execution_authority_granted"] is False - imported = provider_todos(receiver, fixture) - assert imported["source_authority"] == f"{provider}_v0" - assert [(row["todo_id"], row["text"]) for row in imported["todos"]] == [(source_todo, "Later source requirement")] - # The later current source, not the candidate or ambiguous queue, is the - # import basis. Unproved entries remain archived and never become receipts. - assert Path(rolled["candidate_archive_path"]).read_bytes() == candidate - assert originals(Path(rolled["outbox_archive_path"])) == pending - fixture.cli("todo", "add", "--role", "agent", "--text", "Later canonical write", - "--operation-id", "after-ambiguous-import", "--claimed-by", "agent-a") - current = provider_todos(receiver, fixture) - assert {row["text"] for row in current["todos"]} == {"Later source requirement", "Later canonical write"} - fixture.state.unlink() - recovered = cold_cli(fixture, receiver_package, "recover-import", "--operation-id", "ambiguous-import", - "--plan-sha256", plan["plan_sha256"], "--execute") - assert recovered["status"] == "replayed" and recovered["operation_id"] == applied["operation_id"] - assert provider_todos(receiver, fixture) == current - assert originals(fixture.runtime / "authority-shadow") == archived - assert originals(Path(rolled["outbox_archive_path"])) == pending - for saved in (pending_backup, fresh_backup): - with tarfile.open(saved["archive_path"]) as archive: - for relative, data in pending.items(): - prefix = ("runtime-root/authority-shadow/outbox/" + fixture.goal if saved == pending_backup - else "runtime-root/" + str(Path(rolled["outbox_archive_path"]).relative_to(fixture.runtime))) - assert archive.extractfile(prefix + "/" + relative).read() == data - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("window,committed", [ - ("before_replace", False), ("before_marker", True), - ("before_commit", True), ("after_commit", True), -]) -def test_original_outbox_disposition_then_cold_import_preserves_receipts_and_later_writes( - tmp_path, monkeypatch, receiver, receiver_package, provider, window, committed, -): - """Join original effect recovery to cold import, without replaying history. - - Crash only disposable source writers. The receiver uses the shipped owners - with the four normal Python producers absent; the real CLI imports the - settled source, including when the originating interpreter is a wheel. - """ - isolate_sqlite_runtime(tmp_path, monkeypatch) - fixture = cold_workspace(tmp_path / "project") - fixture.crash(window, "todo", "add", "--role", "agent", "--text", "Interrupted original") - source = fixture.state.read_bytes() - partition = fixture.runtime / "authority-shadow/outbox" / fixture.goal / "todos" - prepared_path = next(partition.glob("*.prepared.json")) - prepared_bytes = prepared_path.read_bytes() - original_entry = json.loads(prepared_bytes) - - pending_backup = backup(fixture, "original-pending") - before = originals(fixture.runtime) - refused = cold_cli(fixture, receiver_package, "prepare-import", "--operation-id", "settled-import", - "--backup-manifest", pending_backup["manifest_path"], "--provider", provider, - "--target-handoff-mode", "hard_lease", success=False) - assert refused["reason_code"] == "cold_import_capture_requires_disposition", refused - assert originals(fixture.runtime) == before - assert not list(fixture.runtime.rglob("writer-fence.json")) - assert not (fixture.runtime / "authority").exists() - - drained = drain(receiver, fixture) - assert drained["ok"], drained - assert fixture.state.read_bytes() == source - inventory = inspect(fixture) - proof = receiver("coordination.runtime_shadow.outbox_read", { - "schema_version": LOCAL_AUTHORITY_SHADOW_READ_REQUEST_SCHEMA, - "runtime_root": str(fixture.runtime), "goal_id": fixture.goal, "store_kind": "runtime_shadow", - "scan_after_cursor": None, "scan_limit": 0, "read_model": "proof", - "receipt_operation_id": original_entry["entry_id"], - }) - receipt = proof["proof"]["receipt"]["receipts"][0] - assert receipt["entry_id"] == original_entry["entry_id"] - assert receipt["no_op"] is (not committed) - assert receipt["resolution"] == ({ - "before_replace": "abandoned", "before_marker": "committed_proven_by_readback", - "before_commit": "committed", "after_commit": "committed", - }[window]) - # Draining an empty queue is insufficient: capture must be stopped through - # its own revision-bound lifecycle, retaining the original receipt bytes. - settled_backup = backup(fixture, "drained-active") - before = originals(fixture.runtime) - refused = cold_cli(fixture, receiver_package, "prepare-import", "--operation-id", "settled-import", - "--backup-manifest", settled_backup["manifest_path"], "--provider", provider, - "--target-handoff-mode", "hard_lease", success=False) - assert refused["reason_code"] == "cold_import_capture_requires_disposition", refused - assert originals(fixture.runtime) == before - request = {"schema_version": "loopx_coordination_runtime_shadow_rollback_v0", - "runtime_root": str(fixture.runtime), "goal_id": fixture.goal, - "operation_id": "retain-settled-original", "expected_bootstrap_operation_id": None, - "expected_provider_revision": inventory["capture"]["runtime_shadow_readback"]["provider_revision"], - "projection": inventory["projection"], "source_snapshot": inventory["source_snapshot"]} - rolled = receiver("coordination.runtime_shadow.rollback", request) - assert rolled["status"] == "applied", rolled - candidate = Path(rolled["candidate_archive_path"]).read_bytes() - retained = originals(fixture.runtime / "authority-shadow") - assert receiver("coordination.runtime_shadow.rollback", request)["status"] == "replayed" - assert originals(fixture.runtime / "authority-shadow") == retained - final_backup = backup(fixture, "settled-before-import") - plan = cold_cli(fixture, receiver_package, "prepare-import", "--operation-id", "settled-import", - "--backup-manifest", final_backup["manifest_path"], "--provider", provider, - "--target-handoff-mode", "hard_lease") - assert plan["status"] == "prepared", plan - applied = cold_cli(fixture, receiver_package, "apply-import", "--operation-id", "settled-import", - "--plan-sha256", plan["plan_sha256"], "--writers-stopped", "--execute") - assert applied["status"] == "applied" and applied["execution_authority_granted"] is False - assert applied["operation_id"] != original_entry["entry_id"] - assert fixture.state.read_bytes() == source - later = fixture.cli("todo", "add", "--role", "agent", "--text", "Later canonical write", - "--operation-id", "later-write", "--claimed-by", "agent-a") - # Independent complete provider read, rather than the active-Todo CLI view. - current = provider_todos(receiver, fixture) - assert current["source_authority"] == f"{provider}_v0" - assert {row["text"] for row in current["todos"]} == ( - {"Interrupted original", "Later canonical write"} if committed else {"Later canonical write"}) - assert len(current["todos"]) == (2 if committed else 1) - assert later["todo_id"] in {row["todo_id"] for row in current["todos"]} - fixture.state.unlink() - replay = cold_cli(fixture, receiver_package, "recover-import", "--operation-id", "settled-import", - "--plan-sha256", plan["plan_sha256"], "--execute") - assert replay["status"] == "replayed" and replay["operation_id"] == applied["operation_id"] - assert provider_todos(receiver, fixture) == current - assert Path(rolled["candidate_archive_path"]).read_bytes() == candidate - assert originals(fixture.runtime / "authority-shadow") == retained - # The original queue and receipts have their own saved history; import does - # not convert them into a new queue or silently discard the pending backup. - with tarfile.open(pending_backup["archive_path"]) as archive: - member = "runtime-root/" + str(prepared_path.relative_to(fixture.runtime)) - assert archive.extractfile(member).read() == prepared_bytes - with tarfile.open(final_backup["archive_path"]) as archive: - for relative, data in retained.items(): - assert archive.extractfile("runtime-root/authority-shadow/" + relative).read() == data diff --git a/tests/control_plane/test_cold_source_import_cli.py b/tests/control_plane/test_cold_source_import_cli.py index e15e4af456..b881d8b143 100644 --- a/tests/control_plane/test_cold_source_import_cli.py +++ b/tests/control_plane/test_cold_source_import_cli.py @@ -79,84 +79,6 @@ def cli(action, *args, success=True, output_format="json", runtime_override=None return cli, state, backup, body, runtime, receiver, env -@pytest.mark.parametrize("old_producers_present", [True, False]) -@pytest.mark.parametrize("configuration", ["absent", "disabled", "retired"]) -def test_unpromoted_default_off_lease_lifecycle_without_shadow_producers( - tmp_path, monkeypatch, old_producers_present, configuration, -): - _, state, _, _, runtime, receiver, env = workspace(tmp_path, monkeypatch) - registry = tmp_path / "project/.loopx/registry.json" - data = json.loads(registry.read_text()) - coordination = data["goals"][0]["coordination"] - coordination["registered_agents"] = ["agent-a", "agent-b"] - if configuration == "disabled": - coordination["runtime_shadow"] = { - "schema_version": "loopx_coordination_runtime_shadow_config_v0", - "enabled": False, "provider": "file_v0", - } - elif configuration == "retired": - coordination["authority_shadow"] = { - "schema_version": "loopx_local_authority_shadow_config_v0", - "mode": "file_one_way", - } - registry.write_text(json.dumps(data)) - state.write_text(state.read_text().replace("handoff_mode: legacy", "handoff_mode: hard_lease") - .replace(" claimed_by=agent-a", "")) - original_state, original_registry = state.read_bytes(), registry.read_bytes() - if not old_producers_present: - for name in ("runtime_shadow.py", "runtime_shadow_writer_adapter.py", - "local_authority_shadow_adapter.py", "local_authority_shadow_outbox.py"): - (receiver / "loopx/control_plane/coordination" / name).unlink() - - def lease(action, *args, owner="agent-a", key="original", success=True): - child = subprocess.run([sys.executable, "-c", - "import loopx,runpy,sys; print(loopx.__file__,file=sys.stderr); runpy.run_module('loopx.cli',run_name='__main__')", - "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", - "task-lease", action, "--goal-id", "cold", "--todo-id", "todo_current", - "--owner", owner, "--idempotency-key", key, *map(str, args)], - cwd=tmp_path, env=env, capture_output=True, text=True, timeout=60) - assert str(receiver / "loopx/__init__.py") in child.stderr - assert "Traceback" not in child.stderr, child.stderr - assert (child.returncode == 0) is success, child.stdout + child.stderr - result = json.loads(child.stdout) - assert "coordination_runtime_shadow_capture" not in result - assert "coordination_runtime_shadow" not in result - assert state.read_bytes() == original_state - assert registry.read_bytes() == original_registry - assert not (runtime / "authority-shadow").exists() - assert not any(path.is_file() for path in (runtime / "authority-transition").rglob("*")) - return result - - acquired = lease("acquire", "--ttl-seconds", 120, "--write-scope", "docs/**") - assert acquired["acquired"] is True - version = acquired["lease"]["version"] - path = Path(acquired["lease_path"]) - original_lease = path.read_bytes() - replay = lease("acquire", "--ttl-seconds", 120, "--write-scope", "docs/**") - assert replay["idempotent"] is True and replay["lease"] == acquired["lease"] - assert path.read_bytes() == original_lease - rejected = lease("renew", "--expected-version", version + 1, "--ttl-seconds", 180, success=False) - assert rejected["error_code"] == "version_mismatch" - assert path.read_bytes() == original_lease - renewed = lease("renew", "--expected-version", version, "--ttl-seconds", 180) - assert renewed["lease"]["version"] == version + 1 - assert renewed["lease"]["lease_epoch"] == acquired["lease"]["lease_epoch"] - transferred = lease("transfer", "--expected-version", version + 1, - "--new-owner", "agent-b", "--new-idempotency-key", "receiver", - "--ttl-seconds", 180) - assert transferred["lease"]["owner"] == "agent-b" - assert transferred["lease"]["version"] == version + 2 - assert transferred["lease"]["lease_epoch"] == acquired["lease"]["lease_epoch"] + 1 - released = lease("release", "--expected-version", version + 2, owner="agent-b", key="receiver") - assert released["released"] is True and released["lease"]["status"] == "released" - assert released["lease"]["write_scopes"] == ["docs/**"] - assert released["lease"]["lease_epoch"] == transferred["lease"]["lease_epoch"] - retained = path.read_bytes() - assert lease("release", "--expected-version", version + 2, - owner="agent-b", key="receiver")["idempotent"] is True - assert path.read_bytes() == retained - - def test_coordination_command_help_and_rejections_match_full_cli(tmp_path): # Independently compare the existing compatibility parser/handler with the # selected dispatcher, including diagnostics that deliberately fall back. @@ -199,12 +121,14 @@ def test_coordination_command_help_and_rejections_match_full_cli(tmp_path): assert all(row[0] == 2 for row in observations[0][5:]) -@pytest.mark.parametrize("old_producers_present", [True, False]) -def test_unconfigured_shadow_inspection_matches_full_cli_without_import(tmp_path, monkeypatch, old_producers_present): +@pytest.mark.parametrize("bridge_modules_present", [True, False]) +def test_unconfigured_shadow_inspection_matches_full_cli_without_import( + tmp_path, monkeypatch, bridge_modules_present, +): _, state, _, _, runtime, receiver, env = workspace(tmp_path, monkeypatch) - if not old_producers_present: - for name in ("runtime_shadow.py", "runtime_shadow_writer_adapter.py", - "local_authority_shadow_adapter.py", "local_authority_shadow_outbox.py"): + if not bridge_modules_present: + for name in ("runtime_shadow.py", "local_authority_shadow_adapter.py", + "local_authority_shadow_outbox.py"): (receiver / "loopx/control_plane/coordination" / name).unlink() registry = tmp_path / "project/.loopx/registry.json" original = state.read_bytes() @@ -227,14 +151,12 @@ def test_unconfigured_shadow_inspection_matches_full_cli_without_import(tmp_path assert not (runtime / "authority-transition").exists() -@pytest.mark.parametrize("command,action,missing,expected_ok", [ - ("coordination-shadow", "inspect", "runtime_shadow", False), - ("authority-shadow", "status", "local_authority_shadow_adapter", True), - ("authority-shadow", "status", "local_authority_shadow_readback", False), - ("authority-shadow", "drain", "local_authority_shadow_adapter", False), +@pytest.mark.parametrize("command,action,missing", [ + ("coordination-shadow", "inspect", "runtime_shadow"), + ("authority-shadow", "status", "local_authority_shadow_adapter"), ]) def test_explicit_shadow_action_uses_only_its_required_modules_without_writing( - tmp_path, monkeypatch, command, action, missing, expected_ok, + tmp_path, monkeypatch, command, action, missing, ): _, state, _, _, runtime, receiver, env = workspace(tmp_path, monkeypatch) registry = tmp_path / "project/.loopx/registry.json" @@ -256,11 +178,9 @@ def test_explicit_shadow_action_uses_only_its_required_modules_without_writing( capture_output=True, text=True, timeout=60) assert str(receiver / "loopx/__init__.py") in child.stderr assert "Traceback" not in child.stderr, child.stderr - assert child.returncode == (0 if expected_ok else 1) + assert child.returncode == 0 result = json.loads(child.stdout) - assert result["ok"] is expected_ok - if not expected_ok: - assert missing in result["error"] + assert result["ok"] is True after = {str(path.relative_to(tmp_path)): path.read_bytes() for root in (registry.parent, state.parent, runtime) for path in root.rglob("*") if path.is_file()} @@ -268,17 +188,16 @@ def test_explicit_shadow_action_uses_only_its_required_modules_without_writing( @pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("old_producers_present", [True, False]) +@pytest.mark.parametrize("source_modules_present", [True, False]) def test_cold_cli_import_and_source_free_original_recovery( - tmp_path, monkeypatch, provider, old_producers_present, + tmp_path, monkeypatch, provider, source_modules_present, ): cli, state, backup, body, runtime, receiver, env = workspace(tmp_path, monkeypatch) - if not old_producers_present: - for path in ("todos.py", "bootstrap.py", "control_plane/coordination/runtime_shadow_writer_adapter.py", + if not source_modules_present: + for path in ("todos.py", "bootstrap.py", "control_plane/coordination/local_authority_shadow_outbox.py", "control_plane/coordination/runtime_shadow.py", - "control_plane/coordination/local_authority_shadow_adapter.py", - "control_plane/todos/legacy_mutation.py", "control_plane/todos/line_update.py"): + "control_plane/coordination/local_authority_shadow_adapter.py"): (receiver / "loopx" / path).unlink() original = state.read_bytes() prepared = cli("prepare-import", "--backup-manifest", backup["manifest_path"], @@ -309,18 +228,16 @@ def test_cold_cli_import_and_source_free_original_recovery( @pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("old_producers_present", [True, False]) +@pytest.mark.parametrize("bridge_modules_present", [True, False]) @pytest.mark.parametrize("old_target", [False, True], ids=["no-creation-target", "v0-opposite-target"]) def test_cold_import_recovery_preserves_later_canonical_writes( - tmp_path, monkeypatch, provider, old_producers_present, old_target, + tmp_path, monkeypatch, provider, bridge_modules_present, old_target, ): target = ({"schema_version": "loopx_new_goal_storage_target_v0", "provider": "sqlite" if provider == "file" else "file"} if old_target else None) cli, state, backup, body, runtime, receiver, env = workspace(tmp_path, monkeypatch, storage_target=target) - if not old_producers_present: - for path in ("control_plane/todos/legacy_mutation.py", "control_plane/todos/line_update.py", - "control_plane/coordination/runtime_shadow.py", - "control_plane/coordination/runtime_shadow_writer_adapter.py", + if not bridge_modules_present: + for path in ("control_plane/coordination/runtime_shadow.py", "control_plane/coordination/local_authority_shadow_adapter.py", "control_plane/coordination/local_authority_shadow_outbox.py"): (receiver / "loopx" / path).unlink() diff --git a/tests/control_plane/test_cold_source_import_host.py b/tests/control_plane/test_cold_source_import_host.py index f7d4079001..9a0e261a51 100644 --- a/tests/control_plane/test_cold_source_import_host.py +++ b/tests/control_plane/test_cold_source_import_host.py @@ -6,7 +6,7 @@ """ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta, timezone import json import os import subprocess @@ -58,9 +58,31 @@ def command(action, *args): return json.loads(child.stdout) identity = ["--owner", "agent-a", "--idempotency-key", "old-execution"] - acquired = command("acquire", *identity, "--ttl-seconds", ttl) - assert acquired["ok"] and acquired["lease"]["status"] == "active", acquired - context = {"lease": acquired["lease"], "ttl_seconds": ttl, + now = datetime.now(timezone.utc) + + def timestamp(value: datetime) -> str: + return value.isoformat().replace("+00:00", "Z") + + lease = { + "schema_version": "task_lease_v0", + "goal_id": "cold", + "todo_id": "todo_current", + "owner": "agent-a", + "idempotency_key": "old-execution", + "write_scopes": [], + "acquire_ttl_seconds": ttl, + "version": 1, + "lease_epoch": 1, + "acquired_at": timestamp(now), + "updated_at": timestamp(now), + "expires_at": timestamp(now + timedelta(seconds=ttl)), + "status": "active", + } + lease_path = runtime / "goals/cold/task-leases/todo_current.json" + lease_path.parent.mkdir(parents=True) + lease_path.write_text(json.dumps(lease), encoding="utf-8") + assert command("inspect")["lease"] == lease + context = {"lease": lease, "ttl_seconds": ttl, "read_argv": [*argv, "inspect", *selected], "renew_argv": [*argv, "renew", *selected, *identity]} diff --git a/tests/control_plane/test_cold_source_import_runtime.py b/tests/control_plane/test_cold_source_import_runtime.py index 68e3f920e0..26d1ce136e 100644 --- a/tests/control_plane/test_cold_source_import_runtime.py +++ b/tests/control_plane/test_cold_source_import_runtime.py @@ -54,7 +54,10 @@ def test_cold_import_real_runtime_without_original_producers(tmp_path, monkeypat receiver = tmp_path / "receiver" package = Path(os.environ.get("LOOPX_COLD_IMPORT_PACKAGE", str(REPO / "loopx"))) shutil.copytree(package, receiver / "loopx", ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) - for path in ("todos.py", "bootstrap.py", "control_plane/coordination/runtime_shadow_writer_adapter.py", + assert not ( + receiver / "loopx/control_plane/coordination/runtime_shadow_writer_adapter.py" + ).exists() + for path in ("todos.py", "bootstrap.py", "control_plane/coordination/local_authority_shadow_outbox.py"): (receiver / "loopx" / path).unlink() env = {**os.environ, "PYTHONPATH": str(receiver)} diff --git a/tests/control_plane/test_cold_source_inspection.py b/tests/control_plane/test_cold_source_inspection.py index 2620fd7ea0..44c10456e6 100644 --- a/tests/control_plane/test_cold_source_inspection.py +++ b/tests/control_plane/test_cold_source_inspection.py @@ -1,7 +1,8 @@ -"""A cold source can be inventoried without manufacturing capture history.""" +"""Cold Markdown sources can be inventoried without creating capture history.""" + +from __future__ import annotations + import json -import hashlib -from pathlib import Path import subprocess import sys @@ -10,196 +11,6 @@ from tests.control_plane.shadow_e2e_fixture import workspace -def capture_bytes(fixture): - """Original history files; transient lock files are not receipt sources.""" - return {str(p.relative_to(fixture.runtime)): p.read_bytes() - for root in (fixture.runtime / "authority-shadow", fixture.runtime / "authority-transition") - for p in root.rglob("*") if p.is_file() and not p.name.endswith(".lock")} - - -def test_capture_inventory_keeps_original_history_and_outbox_bytes(tmp_path): - fixture = workspace(tmp_path) - fixture.add("Original captured operation") - outbox = fixture.runtime / "authority-shadow" / "outbox" / fixture.goal / "todos" - outbox.mkdir(exist_ok=True) - residue = outbox / "unrecognized-original-receipt.json" - residue.write_bytes(b"{malformed original bytes") - before = capture_bytes(fixture) - result = fixture.cli("coordination-shadow", "inspect-source")["source_inventory"] - capture = result["capture"] - assert capture["management_state"]["status"] == "active" - assert capture["runtime_shadow_readback"]["status"] == "loaded" - assert capture["runtime_shadow_readback"]["proof"]["last_applied_sequences"]["todos"] >= 1 - witness = capture["artifacts"]["runtime_store"] - original = next(data for path, data in before.items() - if path == str(Path(witness["path"]).relative_to(fixture.runtime))) - assert witness["sha256"] == "sha256:" + hashlib.sha256(original).hexdigest() - entries = capture["artifacts"]["outbox"]["inventory"]["entries"] - raw = next(e for e in entries if e["path"] == "todos/" + residue.name) - assert raw["sha256"] == "sha256:" + hashlib.sha256(residue.read_bytes()).hexdigest() - assert result["outbox_reconciliation_verified"] is False - assert result["import_ready"] is False - review = capture["outbox_review"] - assert review["status"] == "failed" - assert review["reason_code"] == "outbox_file_invalid" - assert review["executed"] is False - assert before == capture_bytes(fixture) - assert not (fixture.runtime / "authority").exists() - - -def test_capture_inventory_keeps_rolled_back_operation_archives(tmp_path): - fixture = workspace(tmp_path) - fixture.add("Original operation before rollback") - revision = fixture.cli("coordination-shadow", "inspect")["inspection"]["provider_revision"] - rollback = fixture.cli("coordination-shadow", "rollback", "--provider-revision", revision, "--execute") - assert rollback["rollback"]["status"] == "applied" - before = capture_bytes(fixture) - capture = fixture.cli("coordination-shadow", "inspect-source")["source_inventory"]["capture"] - assert capture["management_state"]["status"] == "inactive" - assert capture["artifacts"]["runtime_store"] is None - assert capture["runtime_shadow_readback"] is None - assert capture["outbox_review"] is None - archives = capture["artifacts"]["rollback_archives"] - assert len(archives) == 1 - assert archives[0]["path"] == rollback["rollback"]["candidate_archive_path"] - assert archives[0]["sha256"] == "sha256:" + hashlib.sha256(Path(archives[0]["path"]).read_bytes()).hexdigest() - assert before == capture_bytes(fixture) - - -@pytest.mark.parametrize("window", ["before_commit", "after_commit"]) -def test_original_lease_outbox_uses_its_own_partition_without_renewal_or_cleanup(tmp_path, window): - fixture = workspace(tmp_path) - todo_id = fixture.add("Original leased task")["todo_id"] - fixture.crash(window, "task-lease", "acquire", "--todo-id", todo_id, - "--owner", "agent-a", "--idempotency-key", "original-lease", "--ttl-seconds", "120") - before = capture_bytes(fixture) - lease_path = fixture.runtime / "goals" / fixture.goal / "task-leases" / (todo_id + ".json") - lease_bytes = lease_path.read_bytes() - result = fixture.cli("coordination-shadow", "inspect-source")["source_inventory"] - review = result["capture"]["outbox_review"] - assert review["status"] == "planned" and review["executed"] is False - leases = next(p for p in review["partitions"] if p["partition"] == "leases") - assert len(leases["pending_entry_ids"]) == (1 if window == "before_commit" else 0) - assert len(leases["reclaim_entry_ids"]) == (0 if window == "before_commit" else 1) - assert leases["next_seq"] == (1 if window == "before_commit" else 2) - assert todo_id in result["leases_requiring_settlement"] - assert lease_path.read_bytes() == lease_bytes and before == capture_bytes(fixture) - - -@pytest.mark.parametrize("corrupt", [False, True]) -def test_capture_inventory_reads_original_observation_through_existing_store(tmp_path, corrupt): - fixture = workspace(tmp_path) - source = fixture.runtime / "authority-shadow" / "file-v0" - retained = fixture.runtime / "authority-shadow" / "file" / fixture.goal - retained.mkdir(parents=True) - # A synthetic historical copy of a real provider document, not a mock or - # an import receipt. The historical reader owns its schema validation. - for path in source.iterdir(): - if path.is_file() and not path.name.endswith(".lock"): - (retained / path.name).write_bytes(path.read_bytes()) - if corrupt: - next(retained.glob("authority-store-*.json")).write_text('{"invalid":true}') - before = capture_bytes(fixture) - result = fixture.cli("coordination-shadow", "inspect-source", success=not corrupt) - if corrupt: - assert result["source_inventory"]["status"] == "failed" - else: - capture = result["source_inventory"]["capture"] - assert capture["legacy_observation_readback"]["status"] == "loaded" - assert capture["artifacts"]["legacy_store"]["path"].startswith(str(retained)) - assert result["source_inventory"]["import_ready"] is False - assert before == capture_bytes(fixture) - - -@pytest.mark.parametrize("unsafe", ["corrupt_history", "symlink_outbox", "symlink_shadow_root", "invalid_original_result"]) -def test_capture_inventory_refuses_unreadable_or_unsafe_original_history(tmp_path, unsafe): - fixture = workspace(tmp_path) - if unsafe == "corrupt_history": - store = next((fixture.runtime / "authority-shadow" / "file-v0").glob("authority-store-*.json")) - store.write_text('{"invalid":true}') - elif unsafe == "symlink_outbox": - outbox = fixture.runtime / "authority-shadow" / "outbox" / fixture.goal - (outbox / "external").symlink_to(fixture.state) - elif unsafe == "symlink_shadow_root": - shadow = fixture.runtime / "authority-shadow" - original = tmp_path / "external-shadow" - shadow.rename(original) - shadow.symlink_to(original, target_is_directory=True) - else: - state = next((fixture.runtime / "authority-transition").rglob("state.json")) - original = json.loads(state.read_text()) - original["result"]["primary_writeback_preserved"] = False - state.write_text(json.dumps(original)) - before = capture_bytes(fixture) - result = fixture.cli("coordination-shadow", "inspect-source", success=False) - assert result["ok"] is False - assert result["source_inventory"]["status"] == "failed" - assert result["source_inventory"]["import_ready"] is False - assert before == capture_bytes(fixture) - - -@pytest.mark.parametrize("window", ["before_commit", "after_commit"]) -def test_capture_inventory_does_not_drain_crashed_original_outbox(tmp_path, window): - fixture = workspace(tmp_path) - fixture.crash(window, "todo", "add", "--role", "agent", "--text", "Retained original operation") - before = capture_bytes(fixture) - result = fixture.cli("coordination-shadow", "inspect-source")["source_inventory"] - entries = result["capture"]["artifacts"]["outbox"]["inventory"]["entries"] - assert any(e["path"].endswith(".prepared.json") for e in entries) - assert any(e["path"].endswith(".committed.json") for e in entries) - assert result["import_ready"] is False and result["outbox_reconciliation_verified"] is False - review = result["capture"]["outbox_review"] - assert review["status"] == "planned" and review["executed"] is False - plans = review["partitions"] - todos = next(p for p in plans if p["partition"] == "todos") - assert len(todos["pending_entry_ids"]) == (1 if window == "before_commit" else 0) - assert len(todos["reclaim_entry_ids"]) == (0 if window == "before_commit" else 1) - assert len(todos["replay_entries"]) == (0 if window == "before_commit" else 1) - assert result["writer_stop_verified"] is False - assert before == capture_bytes(fixture) - - -@pytest.mark.parametrize("defect", ["receipt_bytes", "orphan_marker", "foreign_lineage"]) -def test_outbox_review_refuses_unproved_disposition_but_retains_original_bytes(tmp_path, defect): - fixture = workspace(tmp_path) - window = "after_commit" if defect == "receipt_bytes" else "before_commit" - fixture.crash(window, "todo", "add", "--role", "agent", "--text", "Original operation") - directory = fixture.runtime / "authority-shadow" / "outbox" / fixture.goal / "todos" - prepared = next(directory.glob("*.prepared.json")) - if defect == "orphan_marker": - prepared.unlink() - else: - record = json.loads(prepared.read_text()) - if defect == "receipt_bytes": - record["writer"]["operation_id"] = "different-original-operation" - else: - record["capture_lineage_id"] = "foreign-lineage" - prepared.write_text(json.dumps(record)) - before = capture_bytes(fixture) - result = fixture.cli("coordination-shadow", "inspect-source")["source_inventory"] - review = result["capture"]["outbox_review"] - assert review["status"] == "failed" - assert review["reason_code"] == ("outbox_receipt_mismatch" if defect == "receipt_bytes" else "outbox_file_invalid") - assert review["executed"] is False - assert result["import_ready"] is False and result["outbox_reconciliation_verified"] is False - assert before == capture_bytes(fixture) - - -@pytest.mark.parametrize("archive", ["candidate", "outbox"]) -def test_capture_inventory_refuses_missing_terminal_rollback_archive(tmp_path, archive): - fixture = workspace(tmp_path) - revision = fixture.cli("coordination-shadow", "inspect")["inspection"]["provider_revision"] - result = fixture.cli("coordination-shadow", "rollback", "--provider-revision", revision, "--execute") - if archive == "candidate": - Path(result["rollback"]["candidate_archive_path"]).unlink() - else: - Path(result["rollback"]["outbox_archive_path"], "manifest.json").unlink() - before = capture_bytes(fixture) - refused = fixture.cli("coordination-shadow", "inspect-source", success=False) - assert refused["source_inventory"]["reason_code"] == "rollback_archive_readback_mismatch" - assert before == capture_bytes(fixture) - - def cold_workspace(tmp_path): fixture = workspace(tmp_path, bootstrap=False) registry = json.loads(fixture.registry.read_text()) @@ -208,24 +19,31 @@ def cold_workspace(tmp_path): fixture.state.write_text( "---\ngoal_id: goal-e2e\nhandoff_mode: soft_claim\n---\n" "## Agent Todo\n- [ ] Keep active\n" - " \n" + " \n" "## Todo Archive\n- [x] Keep unrelated archived evidence\n" - " \n" + " \n" ) return fixture def test_inventory_preserves_unreferenced_archive_without_shadow_or_effects(tmp_path): fixture = cold_workspace(tmp_path) - before = {p: p.read_bytes() for p in (fixture.registry, fixture.state)} + before = {path: path.read_bytes() for path in (fixture.registry, fixture.state)} result = fixture.cli("coordination-shadow", "inspect-source") inventory = result["source_inventory"] assert inventory["status"] == "inspected" assert inventory["active_todo_count"] == 1 assert inventory["archived_todo_count"] == 1 - archived = next(t for t in inventory["projection"]["todos"] if t["todo_id"] == "todo_archived") + archived = next( + todo + for todo in inventory["projection"]["todos"] + if todo["todo_id"] == "todo_archived" + ) assert archived["text"] == "Keep unrelated archived evidence" - assert archived["note"] == "kept" and archived["watch_only"] == "false" + assert archived["note"] == "kept" + assert archived["watch_only"] == "false" assert inventory["import_ready"] is False assert inventory["writer_stop_verified"] is False assert inventory["outbox_reconciliation_verified"] is False @@ -233,36 +51,67 @@ def test_inventory_preserves_unreferenced_archive_without_shadow_or_effects(tmp_ assert result["executed"] is False assert not (fixture.runtime / "authority-shadow").exists() assert not (fixture.runtime / "authority").exists() - assert before == {p: p.read_bytes() for p in before} - - -@pytest.mark.parametrize("original, todo_id, heading", [ - ("Keep unrelated archived evidence", "todo_archived", "Todo Archive"), - ("Keep active", "todo_active", "Agent Todo"), -]) -def test_source_text_is_not_an_attention_summary(tmp_path, original, todo_id, heading): + assert before == {path: path.read_bytes() for path in before} + + +@pytest.mark.parametrize( + ("original", "todo_id", "heading"), + [ + ("Keep unrelated archived evidence", "todo_archived", "Todo Archive"), + ("Keep active", "todo_active", "Agent Todo"), + ], +) +def test_source_text_is_not_an_attention_summary( + tmp_path, + original, + todo_id, + heading, +): fixture = cold_workspace(tmp_path) full_text = "Preserve " + "explicit recovery requirement " * 70 fixture.state.write_text(fixture.state.read_text().replace(original, full_text)) - inventory = fixture.cli("coordination-shadow", "inspect-source")["source_inventory"] - archive = next(t for t in inventory["projection"]["todos"] if t["todo_id"] == todo_id) - assert archive["text"] == full_text.strip() - assert archive["source_section"] == heading + inventory = fixture.cli( + "coordination-shadow", + "inspect-source", + )["source_inventory"] + todo = next( + item for item in inventory["projection"]["todos"] + if item["todo_id"] == todo_id + ) + assert todo["text"] == full_text.strip() + assert todo["source_section"] == heading def test_stale_source_is_rejected_by_native_owner(tmp_path): - from loopx.control_plane.coordination.runtime_shadow import build_runtime_shadow_source_snapshot - from loopx.control_plane.coordination.local_authority_shadow_projection import source_effect_runtime_result + from loopx.control_plane.coordination.runtime_shadow import ( + build_runtime_shadow_source_snapshot, + ) + from loopx.control_plane.coordination.local_authority_shadow_projection import ( + source_effect_runtime_result, + ) fixture = cold_workspace(tmp_path) goal = json.loads(fixture.registry.read_text())["goals"][0] - projection, snapshot = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=fixture.runtime, - state_path=fixture.state, registry_path=fixture.registry, include_all_archived_todos=True) + projection, snapshot = build_runtime_shadow_source_snapshot( + goal=goal, + runtime_root=fixture.runtime, + state_path=fixture.state, + registry_path=fixture.registry, + include_all_archived_todos=True, + ) fixture.state.write_text(fixture.state.read_text() + "\nNew source bytes\n") - result = source_effect_runtime_result("coordination.source.inspect", { - "schema_version": "loopx_cold_source_inspection_request_v0", "goal_id": fixture.goal, - "runtime_root": str(fixture.runtime), "projection": projection, "source_snapshot": snapshot}) - assert result["status"] == "failed" and result["reason_code"] == "source_changed_retry" + result = source_effect_runtime_result( + "coordination.source.inspect", + { + "schema_version": "loopx_cold_source_inspection_request_v0", + "goal_id": fixture.goal, + "runtime_root": str(fixture.runtime), + "projection": projection, + "source_snapshot": snapshot, + }, + ) + assert result["status"] == "failed" + assert result["reason_code"] == "source_changed_retry" assert result["import_ready"] is False @@ -270,36 +119,57 @@ def test_selected_canonical_source_is_not_reinterpreted_as_markdown(tmp_path): import hashlib fixture = cold_workspace(tmp_path) - marker = fixture.runtime / "authority" / ("provider-" + hashlib.sha256(fixture.goal.encode()).hexdigest() + ".json") + marker = fixture.runtime / "authority" / ( + "provider-" + hashlib.sha256(fixture.goal.encode()).hexdigest() + ".json" + ) marker.parent.mkdir(parents=True) marker.write_text('{"provider":"sqlite"}') result = fixture.cli("coordination-shadow", "inspect-source", success=False) - assert result["source_inventory"]["reason_code"] == "cold_source_canonical_authority_present" + assert result["source_inventory"]["reason_code"] == ( + "cold_source_canonical_authority_present" + ) assert marker.read_text() == '{"provider":"sqlite"}' -def test_existing_capture_still_excludes_unreferenced_archives(tmp_path): - from loopx.control_plane.coordination.runtime_shadow import build_runtime_shadow_source_snapshot +def test_capture_projection_excludes_unreferenced_archives(tmp_path): + from loopx.control_plane.coordination.runtime_shadow import ( + build_runtime_shadow_source_snapshot, + ) fixture = cold_workspace(tmp_path) goal = json.loads(fixture.registry.read_text())["goals"][0] - projection, _ = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=fixture.runtime, - state_path=fixture.state, registry_path=fixture.registry) - assert [t["todo_id"] for t in projection["todos"]] == ["todo_active"] + projection, _ = build_runtime_shadow_source_snapshot( + goal=goal, + runtime_root=fixture.runtime, + state_path=fixture.state, + registry_path=fixture.registry, + ) + assert [todo["todo_id"] for todo in projection["todos"]] == ["todo_active"] def test_expired_orphan_active_lease_still_requires_settlement(tmp_path): fixture = cold_workspace(tmp_path) directory = fixture.runtime / "goals" / fixture.goal / "task-leases" directory.mkdir(parents=True) - lease = {"schema_version": "task_lease_v0", "goal_id": fixture.goal, - "todo_id": "removed", "owner": "agent-a", "status": "active", - "idempotency_key": "old-work", "version": 4, "lease_epoch": 2, - "expires_at": "2000-01-01T00:00:00Z", "write_scopes": []} + lease = { + "schema_version": "task_lease_v0", + "goal_id": fixture.goal, + "todo_id": "removed", + "owner": "agent-a", + "status": "active", + "idempotency_key": "old-work", + "version": 4, + "lease_epoch": 2, + "expires_at": "2000-01-01T00:00:00Z", + "write_scopes": [], + } path = directory / "removed.json" path.write_text(json.dumps(lease)) before = path.read_bytes() - inventory = fixture.cli("coordination-shadow", "inspect-source")["source_inventory"] + inventory = fixture.cli( + "coordination-shadow", + "inspect-source", + )["source_inventory"] assert inventory["lease_file_count"] == 1 assert inventory["leases_requiring_settlement"] == ["removed"] assert inventory["retained_leases"] == [lease] @@ -308,17 +178,36 @@ def test_expired_orphan_active_lease_still_requires_settlement(tmp_path): assert path.read_bytes() == before -@pytest.mark.parametrize("corruption", ["duplicate_archive", "unknown_archive_role", "invalid_orphan_lease"]) -def test_ambiguous_or_unsupported_history_is_not_an_empty_source(tmp_path, corruption): +@pytest.mark.parametrize( + "corruption", + ["duplicate_archive", "unknown_archive_role", "invalid_orphan_lease"], +) +def test_ambiguous_or_unsupported_history_is_not_an_empty_source( + tmp_path, + corruption, +): fixture = cold_workspace(tmp_path) if corruption == "duplicate_archive": - fixture.state.write_text(fixture.state.read_text().replace("todo_id=todo_archived", "todo_id=todo_active")) + fixture.state.write_text( + fixture.state.read_text().replace( + "todo_id=todo_archived", + "todo_id=todo_active", + ) + ) elif corruption == "unknown_archive_role": - fixture.state.write_text(fixture.state.read_text().replace("todo_id=todo_archived role=agent", "todo_id=todo_archived")) + fixture.state.write_text( + fixture.state.read_text().replace( + "todo_id=todo_archived role=agent", + "todo_id=todo_archived", + ) + ) else: directory = fixture.runtime / "goals" / fixture.goal / "task-leases" directory.mkdir(parents=True) - (directory / "removed.json").write_text(json.dumps({"goal_id": fixture.goal, "todo_id": "removed"})) + (directory / "removed.json").write_text(json.dumps({ + "goal_id": fixture.goal, + "todo_id": "removed", + })) result = fixture.cli("coordination-shadow", "inspect-source", success=False) assert result["ok"] is False assert not (fixture.runtime / "authority").exists() @@ -326,10 +215,22 @@ def test_ambiguous_or_unsupported_history_is_not_an_empty_source(tmp_path, corru def test_source_inspection_has_no_execute_switch(tmp_path): fixture = cold_workspace(tmp_path) - result = subprocess.run([sys.executable, "-m", "loopx.cli", - *fixture.arguments("coordination-shadow", "inspect-source", "--execute")], - capture_output=True, text=True) - assert result.returncode == 2 and "unrecognized arguments" in result.stderr + result = subprocess.run( + [ + sys.executable, + "-m", + "loopx.cli", + *fixture.arguments( + "coordination-shadow", + "inspect-source", + "--execute", + ), + ], + capture_output=True, + text=True, + ) + assert result.returncode == 2 + assert "unrecognized arguments" in result.stderr @pytest.mark.parametrize("kind", ["unsupported_name", "directory", "symlink"]) @@ -338,7 +239,7 @@ def test_unsafe_lease_inventory_is_not_silently_omitted(tmp_path, kind): directory = fixture.runtime / "goals" / fixture.goal / "task-leases" directory.mkdir(parents=True) if kind == "unsupported_name": - (directory / "历史.json").write_text("{}") + (directory / "unsafe lease.json").write_text("{}") elif kind == "directory": (directory / "todo_removed.json").mkdir() else: diff --git a/tests/control_plane/test_completion_policy.py b/tests/control_plane/test_completion_policy.py index b6472ab877..37f525bf6f 100644 --- a/tests/control_plane/test_completion_policy.py +++ b/tests/control_plane/test_completion_policy.py @@ -4,6 +4,9 @@ from dataclasses import fields from pathlib import Path +from loopx.control_plane.coordination.goal_storage_initialization import ( + initialize_goal_storage_target, +) from loopx.control_plane.todos.completion_policy import CompletionPolicy from loopx.control_plane.testing.canary_harness import ( run_json_cli, @@ -41,7 +44,27 @@ def test_completion_rejects_unknown_runtime_model_before_write( adapter_kind="harness_self_improvement", registered_agents=[AGENT_ID, "codex-main-control"], quota_allowed_slots=None, + extra_goal_fields={ + "creation_operation_id": f"goal-create:{GOAL_ID}", + "coordination": { + "registered_agents": [AGENT_ID, "codex-main-control"], + "agent_model": "peer_v1", + "storage_target": { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "file", + "handoff_mode": "soft_claim", + }, + }, + }, ) + registry = json.loads(registry_path.read_text(encoding="utf-8")) + initialized = initialize_goal_storage_target( + runtime, + registry["goals"][0], + registry_path=registry_path, + ) + assert initialized is not None + assert initialized["authority_initialized"] is True source = run_json_cli( "todo", "add", @@ -55,7 +78,6 @@ def test_completion_rejects_unknown_runtime_model_before_write( AGENT_ID, registry_path=registry_path, ) - registry = json.loads(registry_path.read_text(encoding="utf-8")) registry["goals"][0]["coordination"]["agent_model"] = "hierarchy_v2" registry_path.write_text(json.dumps(registry), encoding="utf-8") before = state_file.read_text(encoding="utf-8") diff --git a/tests/control_plane/test_declared_terminal_settlement_cli.py b/tests/control_plane/test_declared_terminal_settlement_cli.py index 00711d05df..2d7c3a97ac 100644 --- a/tests/control_plane/test_declared_terminal_settlement_cli.py +++ b/tests/control_plane/test_declared_terminal_settlement_cli.py @@ -159,7 +159,7 @@ def test_declared_leased_completion_writeback_spend_terminal_and_replay( code, early_reentry = run("todo", "complete", "--goal-id", cli.GOAL_ID, "--todo-id", cli.TODO_ID, "--agent-id", cli.AGENT_ID, "--completion-identity-key", completed["completion_identity_key"], - "--no-follow-up", "--evidence", complete_args[-1]) + "--no-follow-up") assert code == 1, early_reentry code, continuing_guard = run(*guard_args) assert code == 0, continuing_guard @@ -194,7 +194,7 @@ def test_declared_leased_completion_writeback_spend_terminal_and_replay( terminal_args = (*complete_args, "--no-follow-up") if closeout_source == "turn" else ( "todo", "complete", "--goal-id", cli.GOAL_ID, "--todo-id", cli.TODO_ID, "--agent-id", cli.AGENT_ID, "--completion-identity-key", completed["completion_identity_key"], - "--no-follow-up", "--evidence", complete_args[-1]) + "--no-follow-up") code, terminal = run(*terminal_args) assert code == 0, terminal.get("error") or terminal.get("reason") or terminal assert terminal["completion_continuation"] == "no_followup" @@ -296,7 +296,7 @@ def call(*args: str, expected: int = 0): assert len(admitted["autonomous_replan_obligation"]["triggers"]) == 1 call("todo", "complete", "--goal-id", cli.GOAL_ID, "--todo-id", cli.TODO_ID, "--agent-id", cli.AGENT_ID, "--completion-identity-key", done["completion_identity_key"], - "--no-follow-up", "--evidence", "Original declared source check passed.") + "--no-follow-up") recovered = call(*guard) assert recovered["heartbeat_receipt"]["settlement_identity"] == identity assert recovered.get("selected_todo") is None diff --git a/tests/control_plane/test_effective_turn_replan.py b/tests/control_plane/test_effective_turn_replan.py index e28c8c1318..6796759312 100644 --- a/tests/control_plane/test_effective_turn_replan.py +++ b/tests/control_plane/test_effective_turn_replan.py @@ -17,6 +17,7 @@ GOAL_ID, TODO_ID, _configure_selectable_alternative, + _initialize_fixture_authority, ALTERNATIVE_TODO_ID, ) from loopx.control_plane.work_items.replan_history_codec import ( @@ -312,6 +313,7 @@ def test_effective_cadence_rejects_invalid_units(invalid): def test_verified_negative_work_counts_without_a_score_gain(tmp_path: Path) -> None: project, runtime, registry = _write_fixture(tmp_path) _configure_selectable_alternative(project) + _initialize_fixture_authority(project, runtime) binding = ( "--goal-id", GOAL_ID, diff --git a/tests/control_plane/test_goal_handoff_mode.py b/tests/control_plane/test_goal_handoff_mode.py deleted file mode 100644 index fd2a148c53..0000000000 --- a/tests/control_plane/test_goal_handoff_mode.py +++ /dev/null @@ -1,1755 +0,0 @@ -"""Per-goal handoff-mode gate over the soft-claim / hard-lease split brain. - -Modes (goal front-matter ``handoff_mode``): - -* absent / ``legacy``: today's dual behavior, byte-for-byte. The split-brain - hole stays open BY DESIGN; the legacy tests below are characterization pins - that document it, not aspirations. -* ``soft_claim``: markdown claim is the only ownership record; task-lease - acquire/renew/transfer are typed-rejected, release/inspect stay allowed. -* ``hard_lease``: ownership changes on an existing todo require the actor to - hold that todo's time-active lease; completion fence is mandatory and the - legacy self-disarm state becomes a loud typed error. The delegated - ``todo_lifecycle_authority`` override is the one audited door through the - gate. -""" - -from __future__ import annotations - -import json -from contextlib import ExitStack -from datetime import timedelta -from pathlib import Path -from typing import Any - -import pytest - -from loopx.control_plane.todos.contract import ( - TODO_TASK_CLASS_USER_ACTION, -) -from loopx.control_plane.todos.handoff_mode import ( - HANDOFF_MODE_HARD_LEASE, - HANDOFF_MODE_LEGACY, - HANDOFF_MODE_SOFT_CLAIM, - HandoffModeError, - goal_handoff_mode, - set_goal_handoff_mode, -) -from loopx.control_plane.work_items import task_lease -from loopx.control_plane.work_items.task_lease import ( - TaskLeaseError, - acquire_task_lease, - build_lease, - hold_handoff_lease_holder_gate, - inspect_task_lease, - release_task_lease, - renew_task_lease, - task_lease_path, - transfer_task_lease, - write_lease, -) -from loopx.status import parse_active_state_todos -from loopx.todos import ( - add_goal_todo, - complete_goal_todo, - supersede_goal_todo, - update_goal_todo, -) - -GOAL_ID = "handoff-mode-gate" -AGENT_A = "agent-a" -AGENT_B = "agent-b" -ORCHESTRATOR = "agent-orchestrator" -DECISION_SCOPE = "direction:action:approve_handoff" - - -def _write_workspace( - tmp_path: Path, - *, - handoff_mode: str | None = None, - lifecycle_authority: list[dict[str, Any]] | None = None, -) -> tuple[Path, Path]: - repo = tmp_path / "repo" - repo.mkdir(exist_ok=True) - state = repo / "ACTIVE_GOAL_STATE.md" - front_matter = [ - "---", - f"goal_id: {GOAL_ID}", - "updated_at: 2026-08-01T00:00:00+00:00", - ] - if handoff_mode is not None: - front_matter.append(f"handoff_mode: {handoff_mode}") - front_matter.append("---") - state.write_text( - "\n".join([*front_matter, "", "## Agent Todo", ""]) + "\n", - encoding="utf-8", - ) - coordination: dict[str, Any] = { - "agent_model": "peer_v1", - "registered_agents": [AGENT_A, AGENT_B, ORCHESTRATOR], - } - if lifecycle_authority is not None: - coordination["todo_lifecycle_authority"] = lifecycle_authority - registry = tmp_path / "registry.global.json" - registry.write_text( - json.dumps( - { - "common_runtime_root": str(tmp_path / "runtime"), - "goals": [ - { - "id": GOAL_ID, - "domain": "harness_self_improvement", - "status": "active", - "repo": str(repo), - "state_file": state.name, - "adapter": {"kind": "harness_self_improvement"}, - "coordination": coordination, - } - ], - } - ), - encoding="utf-8", - ) - return registry, state - - -def _set_frontmatter_mode(state: Path, mode: str) -> None: - """Hand-edit the front-matter mode (simulates the out-of-contract path).""" - - lines = state.read_text(encoding="utf-8").splitlines() - close = next(i for i in range(1, len(lines)) if lines[i].strip() == "---") - for index in range(1, close): - if lines[index].split(":", 1)[0].strip() == "handoff_mode": - lines[index] = f"handoff_mode: {mode}" - break - else: - lines.insert(close, f"handoff_mode: {mode}") - state.write_text("\n".join(lines) + "\n", encoding="utf-8") - - -def _add_todo(registry: Path, *, claimed_by: str | None = None) -> dict[str, Any]: - return add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Deliver one bounded control-plane change.", - task_class="advancement_task", - claimed_by=claimed_by, - ) - - -def _agent_todo(state: Path, todo_id: str) -> dict[str, Any]: - todos = parse_active_state_todos(state.read_text(encoding="utf-8")) - return next( - item for item in todos["agent_todos"]["items"] if item["todo_id"] == todo_id - ) - - -def _user_todo(state: Path, todo_id: str) -> dict[str, Any]: - todos = parse_active_state_todos(state.read_text(encoding="utf-8")) - return next( - item for item in todos["user_todos"]["items"] if item["todo_id"] == todo_id - ) - - -def _add_exact_user_gate_pair( - registry: Path, -) -> tuple[dict[str, Any], dict[str, Any]]: - target = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Continue after the exact owner decision.", - task_class="advancement_task", - claimed_by=AGENT_A, - required_decision_scopes=[DECISION_SCOPE], - ) - gate = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="user", - text="Approve the exact handoff decision.", - task_class="user_gate", - blocks_agent=AGENT_A, - decision_scope=DECISION_SCOPE, - unblocks_todo_id=target["todo_id"], - ) - return target, gate - - -def _acquire( - registry: Path, - tmp_path: Path, - todo_id: str, - *, - owner: str, - key: str = "turn-lease-1", - ttl_seconds: int = 600, -) -> dict[str, Any]: - return acquire_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo_id, - owner=owner, - idempotency_key=key, - ttl_seconds=ttl_seconds, - ) - - -def _claim(registry: Path, todo_id: str, agent: str, **kwargs: Any) -> dict[str, Any]: - return update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo_id, - claimed_by=agent, - agent_id=agent, - claim_only=True, - **kwargs, - ) - - -# --------------------------------------------------------------------------- -# Mode reader -# --------------------------------------------------------------------------- - - -def test_goal_handoff_mode_defaults_to_legacy_when_absent() -> None: - text = "---\ngoal_id: g\n---\n\n## Agent Todo\n" - assert goal_handoff_mode(text) == HANDOFF_MODE_LEGACY - - -def test_goal_handoff_mode_reads_explicit_values() -> None: - for mode in (HANDOFF_MODE_LEGACY, HANDOFF_MODE_SOFT_CLAIM, HANDOFF_MODE_HARD_LEASE): - text = f"---\ngoal_id: g\nhandoff_mode: {mode}\n---\n" - assert goal_handoff_mode(text) == mode - - -def test_goal_handoff_mode_rejects_unknown_value_loudly() -> None: - with pytest.raises(HandoffModeError) as error: - goal_handoff_mode("---\ngoal_id: g\nhandoff_mode: banana\n---\n") - - assert error.value.code == "invalid_handoff_mode" - - -def test_invalid_goal_handoff_mode_runtime_read_fails_closed_without_mutation( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry) - _set_frontmatter_mode(state, "banana") - before = state.read_bytes() - - with pytest.raises(HandoffModeError) as error: - _claim(registry, todo["todo_id"], AGENT_A) - - assert error.value.code == "invalid_handoff_mode" - assert error.value.payload["handoff_mode"] == "banana" - assert state.read_bytes() == before - - -def test_goal_handoff_mode_without_frontmatter_is_legacy() -> None: - assert goal_handoff_mode("## Agent Todo\n") == HANDOFF_MODE_LEGACY - - - - -# --------------------------------------------------------------------------- -# (a) Legacy characterization pins: the split-brain hole stays open, loudly. -# --------------------------------------------------------------------------- - - -def test_legacy_pin_claim_over_foreign_active_lease_silently_succeeds( - tmp_path: Path, -) -> None: - """S1 pin: soft claim never reads the lease store, so two owners coexist.""" - - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - result = _claim(registry, todo["todo_id"], AGENT_B) - - assert result["ok"] is True - assert result["handoff_mode"] == HANDOFF_MODE_LEGACY - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_B - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - ) - lease = json.loads(lease_file.read_text(encoding="utf-8")) - assert lease["owner"] == AGENT_A - assert lease["status"] == "active" - - -def test_legacy_pin_foreign_claim_makes_lease_renew_fail_typed( - tmp_path: Path, -) -> None: - registry, _state = _write_workspace(tmp_path) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - _claim(registry, todo["todo_id"], AGENT_B) - - with pytest.raises(TaskLeaseError) as error: - renew_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AGENT_A, - idempotency_key="turn-lease-1", - expected_version=1, - ) - - assert error.value.code == "owner_conflicts_with_claim" - - -def test_legacy_pin_fence_self_disarms_and_claimant_completes_keyless( - tmp_path: Path, -) -> None: - """The completion fence disarms itself in the split-brain state: the - markdown claimant completes WITHOUT any lease credentials while the - foreign lease sits on disk status=active.""" - - registry, _state = _write_workspace(tmp_path) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - _claim(registry, todo["todo_id"], AGENT_B) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_B, - evidence="done without lease credentials", - ) - - assert result["ok"] is True - assert result["task_lease_fence"] == { - "schema_version": "task_lease_v0", - "required": False, - "active": False, - } - assert result["handoff_mode"] == HANDOFF_MODE_LEGACY - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - ) - assert json.loads(lease_file.read_text(encoding="utf-8"))["status"] == "active" - - -def test_legacy_pin_lease_holder_cannot_complete_after_foreign_claim( - tmp_path: Path, -) -> None: - registry, _state = _write_workspace(tmp_path) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - _claim(registry, todo["todo_id"], AGENT_B) - - with pytest.raises(ValueError, match="cannot complete"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - task_lease_idempotency_key="turn-lease-1", - ) - - -def test_legacy_lease_verbs_report_handoff_mode(tmp_path: Path) -> None: - registry, _state = _write_workspace(tmp_path) - todo = _add_todo(registry) - - acquired = _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - assert acquired["handoff_mode"] == HANDOFF_MODE_LEGACY - assert "settlement" not in acquired - - inspected = inspect_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - ) - assert inspected["handoff_mode"] == HANDOFF_MODE_LEGACY - - -# --------------------------------------------------------------------------- -# (b) soft_claim: markdown claim only; lease mutations typed-rejected. -# --------------------------------------------------------------------------- - - -def test_soft_claim_rejects_lease_acquire(tmp_path: Path) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_SOFT_CLAIM) - todo = _add_todo(registry) - - with pytest.raises(TaskLeaseError) as error: - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - assert error.value.code == "handoff_mode_forbids_lease" - assert error.value.payload["handoff_mode"] == HANDOFF_MODE_SOFT_CLAIM - - -def test_soft_claim_rejects_renew_and_transfer_of_legacy_leftover( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - _set_frontmatter_mode(state, HANDOFF_MODE_SOFT_CLAIM) - - with pytest.raises(TaskLeaseError) as renew_error: - renew_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AGENT_A, - idempotency_key="turn-lease-1", - ) - assert renew_error.value.code == "handoff_mode_forbids_lease" - - with pytest.raises(TaskLeaseError) as transfer_error: - transfer_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AGENT_A, - idempotency_key="turn-lease-1", - new_owner=AGENT_B, - new_idempotency_key="turn-lease-2", - ) - assert transfer_error.value.code == "handoff_mode_forbids_lease" - - -def test_soft_claim_allows_release_and_inspect_of_legacy_leftover( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - _set_frontmatter_mode(state, HANDOFF_MODE_SOFT_CLAIM) - - inspected = inspect_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - ) - assert inspected["ok"] is True - assert inspected["handoff_mode"] == HANDOFF_MODE_SOFT_CLAIM - - released = release_task_lease( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AGENT_A, - idempotency_key="turn-lease-1", - expected_version=1, - registry_path=registry, - ) - assert released["ok"] is True - assert released["released"] is True - - -def test_soft_claim_todo_verbs_behave_like_legacy(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_SOFT_CLAIM) - todo = _add_todo(registry) - - claim = _claim(registry, todo["todo_id"], AGENT_B) - assert claim["ok"] is True - assert claim["handoff_mode"] == HANDOFF_MODE_SOFT_CLAIM - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_B - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_B, - evidence="soft mode keyless completion", - ) - assert result["ok"] is True - assert result["handoff_mode"] == HANDOFF_MODE_SOFT_CLAIM - assert result["task_lease_fence"]["required"] is False - - -@pytest.mark.parametrize( - ("configured_mode", "expected_mode"), - [ - (None, HANDOFF_MODE_LEGACY), - (HANDOFF_MODE_SOFT_CLAIM, HANDOFF_MODE_SOFT_CLAIM), - ], -) -def test_local_terminal_replay_matches_completion_response_shape_without_lease( - tmp_path: Path, - configured_mode: str | None, - expected_mode: str, -) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=configured_mode) - todo = _add_todo(registry, claimed_by=AGENT_A) - turn_key = "turn-terminal-response-parity" - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - evidence="complete the lease-free handoff-mode case", - completion_turn_key=turn_key, - no_followup=True, - ) - completed_state = state.read_text(encoding="utf-8") - replayed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - evidence="retry the same local completion turn", - completion_turn_key=turn_key, - no_followup=True, - ) - - assert completed["handoff_mode"] == expected_mode - assert replayed["handoff_mode"] == expected_mode - assert replayed["completed"] is True - assert replayed["changed"] is False - assert replayed["idempotent_replay"] is True - assert state.read_text(encoding="utf-8") == completed_state - - -def test_local_terminal_replay_reports_current_mode_not_original_result_proof( - tmp_path: Path, -) -> None: - """Replay parity reads current goal mode; it is not an original receipt.""" - - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry, claimed_by=AGENT_A) - turn_key = "turn-current-mode-parity" - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - evidence="complete before a legal quiescent mode transition", - completion_turn_key=turn_key, - no_followup=True, - ) - assert completed["handoff_mode"] == HANDOFF_MODE_LEGACY - changed_mode = set_goal_handoff_mode( - registry_path=registry, - goal_id=GOAL_ID, - mode=HANDOFF_MODE_SOFT_CLAIM, - ) - assert changed_mode["changed"] is True - mode_changed_state = state.read_text(encoding="utf-8") - - replayed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - evidence="retry after the goal mode changed", - completion_turn_key=turn_key, - no_followup=True, - ) - - assert replayed["handoff_mode"] == HANDOFF_MODE_SOFT_CLAIM - assert replayed["changed"] is False - assert replayed["idempotent_replay"] is True - assert state.read_text(encoding="utf-8") == mode_changed_state - - -def test_local_terminal_replay_fails_closed_on_current_invalid_mode( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry, claimed_by=AGENT_A) - turn_key = "turn-invalid-mode-replay" - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - completion_turn_key=turn_key, - no_followup=True, - ) - _set_frontmatter_mode(state, "banana") - invalid_state = state.read_text(encoding="utf-8") - - with pytest.raises(HandoffModeError) as error: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - completion_turn_key=turn_key, - no_followup=True, - ) - - assert error.value.code == "invalid_handoff_mode" - assert state.read_text(encoding="utf-8") == invalid_state - - -# --------------------------------------------------------------------------- -# (c) hard_lease: ownership mutations require the actor to hold the lease. -# --------------------------------------------------------------------------- - - -def test_hard_lease_claim_without_lease_is_typed_rejected(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry) - before = state.read_text(encoding="utf-8") - - with pytest.raises(TaskLeaseError) as error: - _claim(registry, todo["todo_id"], AGENT_B) - - assert error.value.code == "handoff_mode_requires_lease" - assert error.value.payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert state.read_text(encoding="utf-8") == before - - -def test_hard_lease_claim_by_non_owner_is_typed_rejected(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - before = state.read_text(encoding="utf-8") - - with pytest.raises(TaskLeaseError) as error: - _claim(registry, todo["todo_id"], AGENT_B) - - assert error.value.code == "handoff_mode_requires_lease" - assert error.value.payload["lease_owner"] == AGENT_A - assert state.read_text(encoding="utf-8") == before - - -def test_hard_lease_holder_claim_succeeds(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - result = _claim(registry, todo["todo_id"], AGENT_A) - - assert result["ok"] is True - assert result["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_A - - -def test_hard_lease_holder_can_handoff_claim_and_peer_can_finish( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - acquired_by_a = _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - ) - - handed_off = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AGENT_B, - agent_id=AGENT_A, - ) - - assert handed_off["ok"] is True - assert handed_off["mutation_authority"]["actor_agent_id"] == AGENT_A - assert handed_off["task_lease_holder_gate"]["owner"] == AGENT_A - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_B - lease_held_by_a = json.loads(lease_file.read_text(encoding="utf-8")) - assert lease_held_by_a["owner"] == AGENT_A - assert lease_held_by_a["status"] == "active" - - state_during_handoff = state.read_text(encoding="utf-8") - with pytest.raises(TaskLeaseError) as claim_error: - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_B, - clear_claim=True, - ) - assert claim_error.value.code == "handoff_mode_requires_lease" - assert claim_error.value.payload["actor_agent_id"] == AGENT_B - assert claim_error.value.payload["lease_owner"] == AGENT_A - assert state.read_text(encoding="utf-8") == state_during_handoff - assert json.loads(lease_file.read_text(encoding="utf-8")) == lease_held_by_a - - with pytest.raises(TaskLeaseError) as completion_error: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_B, - evidence="peer cannot complete before acquiring the hard lease", - ) - assert completion_error.value.code == "handoff_mode_lease_claim_divergence" - assert completion_error.value.payload["lease_owner"] == AGENT_A - assert state.read_text(encoding="utf-8") == state_during_handoff - assert json.loads(lease_file.read_text(encoding="utf-8")) == lease_held_by_a - - released_by_a = release_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AGENT_A, - idempotency_key="turn-lease-1", - expected_version=acquired_by_a["lease"]["version"], - ) - assert released_by_a["released"] is True - assert json.loads(lease_file.read_text(encoding="utf-8"))["status"] == "released" - - acquired_by_b = _acquire( - registry, - tmp_path, - todo["todo_id"], - owner=AGENT_B, - key="turn-lease-2", - ) - completed_by_b = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_B, - task_lease_idempotency_key="turn-lease-2", - task_lease_expected_version=acquired_by_b["lease"]["version"], - evidence="peer completed after acquiring the hard lease", - ) - - assert completed_by_b["ok"] is True - assert completed_by_b["task_lease_fence"]["execution_instance_verified"] is True - assert completed_by_b["task_lease_fence"]["released"] is True - assert _agent_todo(state, todo["todo_id"])["status"] == "done" - assert json.loads(lease_file.read_text(encoding="utf-8"))["status"] == "released" - - -def test_hard_lease_blocks_todo_add_reassigning_a_leased_todo(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - ) - state_before = state.read_text(encoding="utf-8") - lease_before = json.loads(lease_file.read_text(encoding="utf-8")) - - with pytest.raises(TaskLeaseError) as add_error: - _add_todo(registry, claimed_by=AGENT_B) - - assert add_error.value.code == "handoff_mode_requires_lease" - assert add_error.value.payload["actor_agent_id"] == AGENT_B - assert add_error.value.payload["lease_owner"] == AGENT_A - assert state.read_text(encoding="utf-8") == state_before - assert json.loads(lease_file.read_text(encoding="utf-8")) == lease_before - - -def test_hard_lease_allows_todo_add_creating_a_claimed_todo(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - - created = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Draft the follow-up migration note.", - task_class="advancement_task", - claimed_by=AGENT_A, - ) - - assert created["ok"] is True - assert created["added"] is True - assert created["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert "task_lease_holder_gate" not in created - assert _agent_todo(state, created["todo_id"])["claimed_by"] == AGENT_A - - -def test_hard_lease_allows_todo_add_repeating_the_existing_claim( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - repeated = _add_todo(registry, claimed_by=AGENT_A) - - assert repeated["ok"] is True - assert repeated["already_exists"] is True - assert repeated["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert "task_lease_holder_gate" not in repeated - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_A - - -def test_hard_lease_lease_holder_may_reassign_through_todo_add(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - handed_off = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Deliver one bounded control-plane change.", - task_class="advancement_task", - claimed_by=AGENT_B, - agent_id=AGENT_A, - ) - - assert handed_off["ok"] is True - assert handed_off["already_exists"] is True - assert handed_off["task_lease_holder_gate"]["owner"] == AGENT_A - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_B - - -def test_legacy_pin_todo_add_reassigns_a_leased_todo(tmp_path: Path) -> None: - """Characterizes today's behavior: the add path is ungated in legacy mode.""" - - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - ) - - reassigned = _add_todo(registry, claimed_by=AGENT_B) - - assert reassigned["ok"] is True - assert reassigned["already_exists"] is True - assert reassigned["handoff_mode"] == HANDOFF_MODE_LEGACY - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_B - assert json.loads(lease_file.read_text(encoding="utf-8"))["owner"] == AGENT_A - - -def test_hard_lease_expired_lease_does_not_authorize_claim( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A, ttl_seconds=60) - real_now = task_lease.now_utc() - monkeypatch.setattr(task_lease, "now_utc", lambda: real_now + timedelta(hours=2)) - - with pytest.raises(TaskLeaseError) as error: - _claim(registry, todo["todo_id"], AGENT_A) - - assert error.value.code == "handoff_mode_requires_lease" - - -def test_hard_lease_clear_claim_requires_lease(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - - with pytest.raises(TaskLeaseError) as error: - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - clear_claim=True, - ) - assert error.value.code == "handoff_mode_requires_lease" - - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - clear_claim=True, - ) - assert result["ok"] is True - assert not _agent_todo(state, todo["todo_id"]).get("claimed_by") - - -def test_hard_lease_claimed_by_update_requires_lease(tmp_path: Path) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - - with pytest.raises(TaskLeaseError) as error: - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AGENT_A, - agent_id=AGENT_A, - note="keep ownership, add note", - ) - - assert error.value.code == "handoff_mode_requires_lease" - - -def test_hard_lease_non_ownership_update_needs_no_lease(tmp_path: Path) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry) - - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_B, - note="metadata-only update", - ) - - assert result["ok"] is True - assert result["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - - -def test_hard_lease_creation_time_assignment_stays_allowed(tmp_path: Path) -> None: - """A fresh todo id can never hold a lease; add --claimed-by is a - deliberate boundary that stays open in hard_lease mode.""" - - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - - todo = _add_todo(registry, claimed_by=AGENT_A) - - assert todo["ok"] is True - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_A - - -def test_hard_lease_completion_successor_assignment_stays_allowed( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - task_lease_idempotency_key="turn-lease-1", - task_lease_expected_version=1, - evidence="done with verified lease key", - next_agent_todo="Follow-up slice for the peer lane.", - next_claimed_by=AGENT_B, - ) - - assert result["ok"] is True - successor_id = result["next_todos"][0]["todo_id"] - assert _agent_todo(state, successor_id)["claimed_by"] == AGENT_B - - -def test_hard_lease_completion_without_lease_is_typed_rejected( - tmp_path: Path, -) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - - with pytest.raises(TaskLeaseError) as error: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - evidence="no lease exists", - ) - - assert error.value.code == "handoff_mode_requires_lease" - assert error.value.payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - - -def test_hard_lease_completion_with_verified_key_succeeds(tmp_path: Path) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - task_lease_idempotency_key="turn-lease-1", - task_lease_expected_version=1, - evidence="done with verified lease key", - ) - - assert result["ok"] is True - assert result["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert result["task_lease_fence"]["required"] is True - assert result["task_lease_fence"]["execution_instance_verified"] is True - - -def test_hard_lease_non_gate_user_todo_completion_without_lease_is_typed_rejected( - tmp_path: Path, -) -> None: - registry, state = _write_workspace( - tmp_path, - handoff_mode=HANDOFF_MODE_HARD_LEASE, - ) - action = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="user", - text="Owner follow-up action.", - task_class=TODO_TASK_CLASS_USER_ACTION, - bound_agent=AGENT_A, - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(TaskLeaseError) as error: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=action["todo_id"], - role="user", - agent_id=AGENT_A, - evidence="non-gate user todos must still cross the lease fence", - ) - - assert error.value.code == "handoff_mode_requires_lease" - assert state.read_text(encoding="utf-8") == before - - -def test_hard_lease_exact_user_gate_completion_uses_fence_and_releases_lease( - tmp_path: Path, -) -> None: - registry, state = _write_workspace( - tmp_path, - handoff_mode=HANDOFF_MODE_HARD_LEASE, - ) - target, gate = _add_exact_user_gate_pair(registry) - _acquire(registry, tmp_path, gate["todo_id"], owner=AGENT_A) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - ) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - role="user", - decision_outcome="approve", - agent_id=AGENT_A, - task_lease_idempotency_key="turn-lease-1", - task_lease_expected_version=1, - evidence="lease holder approved the exact decision scope", - ) - - assert result["ok"] is True - assert result["mutation_authority"]["mode"] == ( - "exact_user_gate_decision_scope_override" - ) - assert "handoff_gate_overridden" not in result - assert result["task_lease_fence"] == { - "schema_version": "task_lease_v0", - "required": True, - "active": True, - "owner": AGENT_A, - "version": 1, - "lease_epoch": 1, - "execution_instance_verified": True, - "released": True, - } - assert json.loads(lease_file.read_text(encoding="utf-8"))["status"] == "released" - assert _user_todo(state, gate["todo_id"])["status"] == "done" - target_after = _agent_todo(state, target["todo_id"]) - assert target_after["status"] == "open" - assert not target_after.get("required_decision_scopes") - assert result["decision_scope_resolution"]["state"] == "resolved" - assert ( - result["decision_scope_resolution"]["remaining_required_decision_scopes"] == [] - ) - - -@pytest.mark.parametrize( - ("lease_owner", "actor_agent_id", "completion_key"), - [ - (AGENT_A, AGENT_A, "wrong-turn-key"), - (AGENT_B, AGENT_A, "turn-lease-1"), - ], - ids=("wrong-key", "foreign-holder"), -) -def test_hard_lease_exact_user_gate_rejects_invalid_lease_fence_without_state_change( - tmp_path: Path, - lease_owner: str, - actor_agent_id: str, - completion_key: str, -) -> None: - registry, state = _write_workspace( - tmp_path, - handoff_mode=HANDOFF_MODE_HARD_LEASE, - ) - _target, gate = _add_exact_user_gate_pair(registry) - _acquire(registry, tmp_path, gate["todo_id"], owner=lease_owner) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(TaskLeaseError) as error: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - role="user", - decision_outcome="approve", - agent_id=actor_agent_id, - task_lease_idempotency_key=completion_key, - task_lease_expected_version=1, - evidence="invalid execution instance must not consume the gate", - ) - - assert error.value.code == "lease_cas_mismatch" - assert state.read_text(encoding="utf-8") == before - persisted_lease = json.loads(lease_file.read_text(encoding="utf-8")) - assert persisted_lease["status"] == "active" - assert persisted_lease["owner"] == lease_owner - - -def test_hard_lease_user_gate_completion_auto_acquires_key(tmp_path: Path) -> None: - """A user-gate decision completes without caller-supplied lease credentials. - - The completion fence mints the key under the per-goal lease lock, verifies - it, and releases it after the committed writeback - the human decision is - the authority, so the presenter never needs to hold a key across the wait. - """ - - registry, state = _write_workspace( - tmp_path, - handoff_mode=HANDOFF_MODE_HARD_LEASE, - ) - target, gate = _add_exact_user_gate_pair(registry) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - ) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - role="user", - decision_outcome="approve", - agent_id=AGENT_A, - evidence="owner decision recorded under an auto-acquired completion key", - ) - - assert result["ok"] is True - assert result["task_lease_fence"]["auto_acquired"] is True - assert result["task_lease_fence"]["required"] is True - assert result["task_lease_fence"]["execution_instance_verified"] is True - assert result["task_lease_fence"]["released"] is True - assert json.loads(lease_file.read_text(encoding="utf-8"))["status"] == "released" - assert _user_todo(state, gate["todo_id"])["status"] == "done" - target_after = _agent_todo(state, target["todo_id"]) - assert target_after["status"] == "open" - assert not target_after.get("required_decision_scopes") - - -def test_hard_lease_user_gate_auto_acquire_does_not_displace_existing_lease( - tmp_path: Path, -) -> None: - """An active lease held by another agent is never displaced automatically.""" - - registry, state = _write_workspace( - tmp_path, - handoff_mode=HANDOFF_MODE_HARD_LEASE, - ) - _target, gate = _add_exact_user_gate_pair(registry) - _acquire(registry, tmp_path, gate["todo_id"], owner=AGENT_B, key="turn-lease-b") - before = state.read_text(encoding="utf-8") - - with pytest.raises(TaskLeaseError) as error: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - role="user", - decision_outcome="approve", - agent_id=AGENT_A, - evidence="agent A must not displace B's completion key", - ) - - assert error.value.code == "lease_fence_required" - assert state.read_text(encoding="utf-8") == before - - -def test_hard_lease_local_terminal_replay_keeps_response_shape_after_release( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Same-turn local replay does not re-enter lease authority or mutate state.""" - - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - lease_key = "turn-hard-lease-instance" - turn_key = "turn-hard-lease-completion" - _acquire( - registry, - tmp_path, - todo["todo_id"], - owner=AGENT_A, - key=lease_key, - ) - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=1, - completion_turn_key=turn_key, - evidence="complete under the held hard lease", - no_followup=True, - ) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - ) - assert completed["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert completed["task_lease_fence"]["released"] is True - terminal_lease = lease_file.read_text(encoding="utf-8") - assert json.loads(terminal_lease)["status"] == "released" - completed_state = state.read_text(encoding="utf-8") - - def unexpected_lease_effect(*_args: Any, **_kwargs: Any) -> None: - pytest.fail("terminal replay must not re-enter the task-lease effect path") - - monkeypatch.setattr( - "loopx.todos.hold_task_lease_mutation_fence", - unexpected_lease_effect, - ) - monkeypatch.setattr( - "loopx.todos.release_verified_task_lease_fence", - unexpected_lease_effect, - ) - replayed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=1, - completion_turn_key=turn_key, - evidence="retry the same completion after lease release", - no_followup=True, - ) - - assert replayed["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert replayed["completed"] is True - assert replayed["changed"] is False - assert replayed["idempotent_replay"] is True - assert state.read_text(encoding="utf-8") == completed_state - assert lease_file.read_text(encoding="utf-8") == terminal_lease - - -def test_hard_lease_self_disarm_state_becomes_loud_typed_error( - tmp_path: Path, -) -> None: - """Legacy remnants or manual edits can leave claimed_by diverged from a - time-active lease. In hard_lease mode that must fail loudly instead of - silently completing keyless.""" - - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - _claim(registry, todo["todo_id"], AGENT_B) - _set_frontmatter_mode(state, HANDOFF_MODE_HARD_LEASE) - - with pytest.raises(TaskLeaseError) as error: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_B, - evidence="split-brain keyless completion attempt", - ) - - assert error.value.code == "handoff_mode_lease_claim_divergence" - assert error.value.payload["lease_owner"] == AGENT_A - - -# --------------------------------------------------------------------------- -# (d) the door: delegated todo_lifecycle_authority passes the hard gate. -# --------------------------------------------------------------------------- - - -def _orchestrator_grant() -> list[dict[str, Any]]: - return [ - { - "agent_id": ORCHESTRATOR, - "actions": ["reassign", "update", "complete"], - "requires_reason": True, - } - ] - - -def test_hard_lease_delegated_reassign_passes_gate_with_marker( - tmp_path: Path, -) -> None: - registry, state = _write_workspace( - tmp_path, - handoff_mode=HANDOFF_MODE_HARD_LEASE, - lifecycle_authority=_orchestrator_grant(), - ) - todo = _add_todo(registry, claimed_by=AGENT_A) - - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AGENT_B, - agent_id=ORCHESTRATOR, - authority_reason="peer lane is stalled; reassigning", - ) - - assert result["ok"] is True - assert result["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert result["handoff_gate_overridden"] is True - assert result["mutation_authority"]["mode"] == "delegated_orchestration_override" - assert ( - result["mutation_authority"]["authority_reason"] - == "peer lane is stalled; reassigning" - ) - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_B - - -def test_hard_lease_delegated_complete_passes_gate_with_marker( - tmp_path: Path, -) -> None: - registry, _state = _write_workspace( - tmp_path, - handoff_mode=HANDOFF_MODE_HARD_LEASE, - lifecycle_authority=_orchestrator_grant(), - ) - todo = _add_todo(registry, claimed_by=AGENT_A) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=ORCHESTRATOR, - authority_reason="closing stalled item for the goal owner", - evidence="delegated completion", - ) - - assert result["ok"] is True - assert result["handoff_gate_overridden"] is True - assert result["task_lease_fence"]["required"] is False - - -def test_delegated_terminal_replay_parity_excludes_original_gate_effect_marker( - tmp_path: Path, -) -> None: - """Current mode is replayable; an original gate override effect is not.""" - - registry, state = _write_workspace( - tmp_path, - handoff_mode=HANDOFF_MODE_HARD_LEASE, - lifecycle_authority=_orchestrator_grant(), - ) - todo = _add_todo(registry, claimed_by=AGENT_A) - turn_key = "turn-delegated-terminal-replay" - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=ORCHESTRATOR, - authority_reason="close the item through delegated authority", - completion_turn_key=turn_key, - no_followup=True, - ) - completed_state = state.read_text(encoding="utf-8") - - replayed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=ORCHESTRATOR, - authority_reason="retry the delegated completion turn", - completion_turn_key=turn_key, - no_followup=True, - ) - - assert completed["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert completed["handoff_gate_overridden"] is True - assert replayed["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert replayed["mutation_authority"]["mode"] == ( - "delegated_orchestration_override" - ) - assert "handoff_gate_overridden" not in replayed - assert replayed["changed"] is False - assert replayed["idempotent_replay"] is True - assert state.read_text(encoding="utf-8") == completed_state - - -def test_hard_lease_gate_has_no_untyped_bypass_without_grant( - tmp_path: Path, -) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - - with pytest.raises(ValueError): - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AGENT_B, - agent_id=ORCHESTRATOR, - authority_reason="no grant exists for this actor", - ) - - -# --------------------------------------------------------------------------- -# (f) identity normalization on both sides of the holder check. -# --------------------------------------------------------------------------- - - -def test_hard_lease_holder_check_normalizes_both_sides(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=" Agent-A ", - agent_id="AGENT-A", - claim_only=True, - ) - - assert result["ok"] is True - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_A - - -def test_holder_gate_normalizes_lease_owner_case_variants(tmp_path: Path) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - ) - now = task_lease.now_utc() - write_lease( - lease_file, - build_lease( - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner="Agent-A", - idempotency_key="turn-lease-1", - write_scopes=[], - acquire_ttl_seconds=600, - version=1, - lease_epoch=1, - acquired_at=task_lease.isoformat(now), - updated_at=task_lease.isoformat(now), - expires_at=task_lease.isoformat(now + timedelta(seconds=600)), - ), - ) - - with ExitStack() as stack: - gate = stack.enter_context( - hold_handoff_lease_holder_gate( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - actor_agent_id=" agent-a ", - ) - ) - assert gate["active"] is True - assert gate["owner"] == AGENT_A - - -# --------------------------------------------------------------------------- -# supersede is a completion-equivalent transition (status -> done) and must -# cross the same lease fence as complete -# --------------------------------------------------------------------------- - - -def test_hard_lease_supersede_without_lease_is_typed_rejected(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - before = state.read_bytes() - - with pytest.raises(TaskLeaseError) as error: - supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - reason="no lease exists", - ) - - assert error.value.code == "handoff_mode_requires_lease" - assert error.value.payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert state.read_bytes() == before - - -def test_hard_lease_supersede_by_non_holder_without_key_is_typed_rejected( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", goal_id=GOAL_ID, todo_id=todo["todo_id"] - ) - before = state.read_bytes() - - with pytest.raises(TaskLeaseError) as error: - supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_B, - reason="peer supersedes without any key", - ) - - assert error.value.code == "lease_fence_required" - assert state.read_bytes() == before - assert _agent_todo(state, todo["todo_id"])["done"] is False - assert lease_file.exists() - - -def test_hard_lease_supersede_by_holder_without_key_is_typed_rejected( - tmp_path: Path, -) -> None: - """The fence is keyed to the execution instance, not to the agent identity.""" - - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - with pytest.raises(TaskLeaseError) as error: - supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - reason="holder forgot the key", - ) - - assert error.value.code == "lease_fence_required" - assert _agent_todo(state, todo["todo_id"])["done"] is False - - -def test_hard_lease_supersede_with_verified_key_succeeds_and_releases_lease( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", goal_id=GOAL_ID, todo_id=todo["todo_id"] - ) - - result = supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - reason="superseded with the verified key", - next_agent_todo="Continue the superseding work.", - task_lease_idempotency_key="turn-lease-1", - task_lease_expected_version=1, - ) - - assert result["ok"] is True - assert result["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert result["task_lease_fence"]["required"] is True - assert result["task_lease_fence"]["execution_instance_verified"] is True - assert result["task_lease_fence"]["released"] is True - assert _agent_todo(state, todo["todo_id"])["done"] is True - assert json.loads(lease_file.read_text(encoding="utf-8"))["status"] == "released" - assert result["next_todos"][0]["added"] is True - - -def test_hard_lease_supersede_dry_run_keeps_lease_and_state(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - lease_file = task_lease_path( - runtime_root=tmp_path / "runtime", goal_id=GOAL_ID, todo_id=todo["todo_id"] - ) - before = state.read_bytes() - - result = supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - reason="preview only", - task_lease_idempotency_key="turn-lease-1", - task_lease_expected_version=1, - dry_run=True, - ) - - assert result["dry_run"] is True - assert result["task_lease_fence"]["execution_instance_verified"] is True - assert result["task_lease_fence"].get("released") is not True - assert lease_file.exists() - assert state.read_bytes() == before - - -def test_hard_lease_delegated_supersede_passes_gate_with_marker(tmp_path: Path) -> None: - """The delegated door mirrors complete: it opens the claim gate, never an - effective lease. Without a lease the fence is optional and the marker rides.""" - - registry, state = _write_workspace( - tmp_path, - handoff_mode=HANDOFF_MODE_HARD_LEASE, - lifecycle_authority=[ - { - "agent_id": ORCHESTRATOR, - "actions": ["supersede"], - "requires_reason": True, - } - ], - ) - todo = _add_todo(registry, claimed_by=AGENT_A) - - result = supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=ORCHESTRATOR, - authority_reason="retire stale work", - reason="delegated supersede", - ) - - assert result["ok"] is True - assert result["handoff_gate_overridden"] is True - assert result["task_lease_fence"]["required"] is False - assert _agent_todo(state, todo["todo_id"])["done"] is True - - -def test_legacy_pin_supersede_ignores_missing_lease_but_fences_effective_lease( - tmp_path: Path, -) -> None: - """Legacy parity with complete since the completion fence landed: no lease - means no fence; an effective foreign lease requires the key.""" - - registry, state = _write_workspace(tmp_path) - free_todo = _add_todo(registry, claimed_by=AGENT_A) - free = supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=free_todo["todo_id"], - agent_id=AGENT_A, - reason="legacy supersede without any lease", - ) - assert free["ok"] is True - assert free["handoff_mode"] == HANDOFF_MODE_LEGACY - assert free["task_lease_fence"]["required"] is False - - leased_todo = _add_todo(registry) - _acquire(registry, tmp_path, leased_todo["todo_id"], owner=AGENT_A) - with pytest.raises(TaskLeaseError) as error: - supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=leased_todo["todo_id"], - agent_id=AGENT_B, - reason="legacy peer supersedes a leased todo", - ) - assert error.value.code == "lease_fence_required" - assert _agent_todo(state, leased_todo["todo_id"])["done"] is False - - -# --------------------------------------------------------------------------- -# force bootstrap rewrites the state file: the declared mode must travel with it -# --------------------------------------------------------------------------- - - -def _force_bootstrap( - registry: Path, state: Path, tmp_path: Path, **overrides: Any -) -> dict[str, Any]: - from loopx.bootstrap import bootstrap_project - - options: dict[str, Any] = dict( - project=state.parent, - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - objective="Rebuild the active state without changing the handoff contract.", - domain="harness_self_improvement", - role="owner", - parent_goal_id=None, - state_file=state, - goal_doc=None, - adapter_kind="harness_self_improvement", - adapter_status="active", - next_probe=None, - spawn_allowed=False, - max_children=0, - allowed_domains=None, - write_scope=None, - preserve_todos=False, - force=True, - dry_run=False, - sync_global=False, - ) - options.update(overrides) - return bootstrap_project(**options) - - -def test_force_bootstrap_replace_preserves_declared_handoff_mode( - tmp_path: Path, -) -> None: - """Appendix B rule 5: a mode change needs quiescence; a forced state rebuild - is not a mode change and must not silently fall back to legacy.""" - - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) - todo = _add_todo(registry, claimed_by=AGENT_A) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - - payload = _force_bootstrap(registry, state, tmp_path) - - assert payload["state_action"] == "replaced" - assert payload["force_bootstrap_warning"]["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - rebuilt = state.read_text(encoding="utf-8") - assert goal_handoff_mode(rebuilt) == HANDOFF_MODE_HARD_LEASE - assert todo["todo_id"] not in rebuilt - - -def test_force_bootstrap_replace_keeps_legacy_default_absent(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path) - - payload = _force_bootstrap(registry, state, tmp_path) - - assert payload["state_action"] == "replaced" - assert payload["force_bootstrap_warning"]["handoff_mode"] == HANDOFF_MODE_LEGACY - assert "handoff_mode:" not in state.read_text(encoding="utf-8") - assert goal_handoff_mode(state.read_text(encoding="utf-8")) == HANDOFF_MODE_LEGACY - - -def test_force_bootstrap_dry_run_reports_preserved_mode_without_writing( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_SOFT_CLAIM) - before = state.read_bytes() - - payload = _force_bootstrap(registry, state, tmp_path, dry_run=True) - - assert payload["force_bootstrap_warning"]["handoff_mode"] == HANDOFF_MODE_SOFT_CLAIM - assert state.read_bytes() == before - - -def test_force_bootstrap_replace_refuses_invalid_declared_mode(tmp_path: Path) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode="banana") - before = state.read_bytes() - - with pytest.raises(HandoffModeError) as error: - _force_bootstrap(registry, state, tmp_path) - - assert error.value.code == "invalid_handoff_mode" - assert state.read_bytes() == before diff --git a/tests/control_plane/test_goal_handoff_mode_cli.py b/tests/control_plane/test_goal_handoff_mode_cli.py index 28cf4ec2c3..897d42f445 100644 --- a/tests/control_plane/test_goal_handoff_mode_cli.py +++ b/tests/control_plane/test_goal_handoff_mode_cli.py @@ -1,12 +1,4 @@ -"""CLI surface for the per-goal handoff mode: show/set plus error-code mapping. - -``loopx handoff-mode show|set`` reads and writes the ``handoff_mode`` key in -the goal's ACTIVE_GOAL_STATE.md YAML front-matter. Setting requires quiescence -(no open claimed todo, no time-active lease) and refuses with a typed offender -list otherwise; there is no --force in v0. Gate rejections raised by todo and -task-lease verbs must surface their typed error codes through the existing -CLI error_code mapping. -""" +"""CLI coverage for canonical handoff mode and pre-cutoff read compatibility.""" from __future__ import annotations @@ -23,7 +15,8 @@ HANDOFF_MODE_SOFT_CLAIM, ) from loopx.control_plane.work_items.task_lease import acquire_task_lease -from loopx.todos import add_goal_todo, update_goal_todo +from loopx.todos import add_goal_todo +from tests.control_plane.canonical_authority_fixture import promoted_create_fixture GOAL_ID = "handoff-mode-cli" AGENT_A = "agent-a" @@ -76,6 +69,20 @@ def _write_workspace( return registry, state +def _write_canonical_workspace( + tmp_path: Path, + *, + handoff_mode: str, +) -> tuple[Path, Path]: + registry, _runtime, state = promoted_create_fixture( + tmp_path, + goal_id=GOAL_ID, + handoff_mode=handoff_mode, + registered_agents=[AGENT_A, AGENT_B], + ) + return registry, state + + def _run_cli( capsys: pytest.CaptureFixture[str], registry: Path, @@ -99,18 +106,6 @@ def _add_todo(registry: Path, *, claimed_by: str | None = None) -> dict[str, Any ) -def _set_frontmatter_mode(state: Path, mode: str) -> None: - lines = state.read_text(encoding="utf-8").splitlines() - close = next(i for i in range(1, len(lines)) if lines[i].strip() == "---") - for index in range(1, close): - if lines[index].split(":", 1)[0].strip() == "handoff_mode": - lines[index] = f"handoff_mode: {mode}" - break - else: - lines.insert(close, f"handoff_mode: {mode}") - state.write_text("\n".join(lines) + "\n", encoding="utf-8") - - # --------------------------------------------------------------------------- # (e) mode switch CLI: show / set with quiescence enforcement. # --------------------------------------------------------------------------- @@ -137,7 +132,15 @@ def test_unpromoted_legacy_set_is_retired_without_mutation(tmp_path, capsys): before = state.read_bytes() code, payload = _run_cli(capsys, registry, "handoff-mode", "set", "--goal-id", GOAL_ID, "--mode", "legacy") - assert code == 1 and payload["error_code"] == "handoff_mode_retired", payload + assert code == 1 and payload["error_code"] == "canonical_authority_required", payload + assert payload["operation"] == "handoff_mode_set" + assert payload["write_admitted"] is False + assert payload["decision_read_from_provider"] is False + assert payload["legacy_fallback_used"] is False + assert payload["recovery"] == { + "action": "migrate_goal_authority", + "backup_required": True, + } assert state.read_bytes() == before @@ -159,41 +162,15 @@ def test_handoff_mode_show_rejects_invalid_frontmatter_without_mutation( assert state.read_bytes() == before -def test_handoff_mode_set_repairs_invalid_frontmatter_on_quiescent_goal( +def test_handoff_mode_set_on_quiescent_goal_updates_provider( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode="banana") - before = state.read_bytes() - - exit_code, payload = _run_cli( - capsys, - registry, - "handoff-mode", - "set", - "--goal-id", - GOAL_ID, - "--mode", - HANDOFF_MODE_HARD_LEASE, + registry, state = _write_canonical_workspace( + tmp_path, + handoff_mode=HANDOFF_MODE_SOFT_CLAIM, ) - - assert exit_code == 0 - assert payload["ok"] is True - assert payload["changed"] is True - assert payload["previous_mode"] == "banana" - assert payload["previous_mode_valid"] is False - assert payload["previous_mode_error_code"] == "invalid_handoff_mode" - assert payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert state.read_bytes() == before.replace( - b"handoff_mode: banana", b"handoff_mode: hard_lease" - ) - - -def test_handoff_mode_set_on_quiescent_goal_updates_frontmatter( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - registry, state = _write_workspace(tmp_path) + before = state.read_bytes() exit_code, payload = _run_cli( capsys, @@ -209,56 +186,28 @@ def test_handoff_mode_set_on_quiescent_goal_updates_frontmatter( assert exit_code == 0 assert payload["ok"] is True assert payload["changed"] is True - assert payload["previous_mode"] == HANDOFF_MODE_LEGACY + assert payload["previous_mode"] == HANDOFF_MODE_SOFT_CLAIM assert payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - text = state.read_text(encoding="utf-8") - assert "handoff_mode: hard_lease" in text.split("---")[1] - assert f"goal_id: {GOAL_ID}" in text + assert payload["decision_read_from_provider"] is True + assert payload["legacy_fallback_used"] is False + assert state.read_bytes() == before show_code, show_payload = _run_cli( capsys, registry, "handoff-mode", "show", "--goal-id", GOAL_ID ) assert show_code == 0 assert show_payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert show_payload["source"] == "frontmatter" - - -def test_handoff_mode_set_invalid_previous_mode_still_refuses_claimed_todo( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry, claimed_by=AGENT_A) - _set_frontmatter_mode(state, "banana") - before = state.read_bytes() - - exit_code, payload = _run_cli( - capsys, - registry, - "handoff-mode", - "set", - "--goal-id", - GOAL_ID, - "--mode", - HANDOFF_MODE_SOFT_CLAIM, - ) - - assert exit_code == 1 - assert payload["error_code"] == "handoff_mode_not_quiescent" - assert payload["previous_mode"] == "banana" - assert payload["previous_mode_valid"] is False - assert payload["previous_mode_error_code"] == "invalid_handoff_mode" - assert [item["todo_id"] for item in payload["claimed_todos"]] == [ - todo["todo_id"] - ] - assert state.read_bytes() == before + assert show_payload["source"] == "canonical_provider" def test_handoff_mode_set_refused_when_open_claimed_todo_exists( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - registry, state = _write_workspace(tmp_path) + registry, state = _write_canonical_workspace( + tmp_path, + handoff_mode=HANDOFF_MODE_HARD_LEASE, + ) todo = _add_todo(registry, claimed_by=AGENT_A) before = state.read_text(encoding="utf-8") @@ -282,51 +231,14 @@ def test_handoff_mode_set_refused_when_open_claimed_todo_exists( assert state.read_text(encoding="utf-8") == before -def test_handoff_mode_set_invalid_previous_mode_still_refuses_active_lease( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - registry, state = _write_workspace(tmp_path) - todo = _add_todo(registry) - acquire_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AGENT_A, - idempotency_key="turn-invalid-mode-lease", - ttl_seconds=600, - ) - _set_frontmatter_mode(state, "banana") - before = state.read_bytes() - - exit_code, payload = _run_cli( - capsys, - registry, - "handoff-mode", - "set", - "--goal-id", - GOAL_ID, - "--mode", - HANDOFF_MODE_HARD_LEASE, - ) - - assert exit_code == 1 - assert payload["error_code"] == "handoff_mode_not_quiescent" - assert payload["previous_mode"] == "banana" - assert payload["previous_mode_valid"] is False - assert payload["previous_mode_error_code"] == "invalid_handoff_mode" - assert [item["todo_id"] for item in payload["active_leases"]] == [ - todo["todo_id"] - ] - assert state.read_bytes() == before - - def test_handoff_mode_set_refused_when_time_active_lease_exists( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - registry, _state = _write_workspace(tmp_path) + registry, _state = _write_canonical_workspace( + tmp_path, + handoff_mode=HANDOFF_MODE_HARD_LEASE, + ) todo = _add_todo(registry) acquire_task_lease( registry_path=registry, @@ -346,7 +258,7 @@ def test_handoff_mode_set_refused_when_time_active_lease_exists( "--goal-id", GOAL_ID, "--mode", - HANDOFF_MODE_HARD_LEASE, + HANDOFF_MODE_SOFT_CLAIM, ) assert exit_code == 1 @@ -356,41 +268,14 @@ def test_handoff_mode_set_refused_when_time_active_lease_exists( assert leases[0]["owner"] == AGENT_A -def test_handoff_mode_set_allowed_after_goal_becomes_quiescent( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - registry, _state = _write_workspace(tmp_path) - todo = _add_todo(registry, claimed_by=AGENT_A) - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AGENT_A, - clear_claim=True, - ) - - exit_code, payload = _run_cli( - capsys, - registry, - "handoff-mode", - "set", - "--goal-id", - GOAL_ID, - "--mode", - HANDOFF_MODE_SOFT_CLAIM, - ) - - assert exit_code == 0 - assert payload["ok"] is True - assert payload["handoff_mode"] == HANDOFF_MODE_SOFT_CLAIM - - def test_handoff_mode_set_same_value_is_idempotent( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_SOFT_CLAIM) + registry, _state = _write_canonical_workspace( + tmp_path, + handoff_mode=HANDOFF_MODE_SOFT_CLAIM, + ) exit_code, payload = _run_cli( capsys, @@ -417,7 +302,10 @@ def test_cli_todo_claim_in_hard_mode_maps_typed_error_code( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) + registry, _state = _write_canonical_workspace( + tmp_path, + handoff_mode=HANDOFF_MODE_HARD_LEASE, + ) todo = _add_todo(registry) exit_code, payload = _run_cli( @@ -445,7 +333,10 @@ def test_cli_task_lease_acquire_in_soft_mode_maps_typed_error_code( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_SOFT_CLAIM) + registry, _state = _write_canonical_workspace( + tmp_path, + handoff_mode=HANDOFF_MODE_SOFT_CLAIM, + ) todo = _add_todo(registry) exit_code, payload = _run_cli( @@ -478,7 +369,10 @@ def test_todo_supersede_cli_requires_key_in_hard_lease_and_accepts_it( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) + registry, _state = _write_canonical_workspace( + tmp_path, + handoff_mode=HANDOFF_MODE_HARD_LEASE, + ) todo = _add_todo(registry, claimed_by=AGENT_A) acquire_task_lease( registry_path=registry, @@ -528,6 +422,15 @@ def test_todo_supersede_cli_requires_key_in_hard_lease_and_accepts_it( ) assert exit_code == 0 assert payload["ok"] is True - assert payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert payload["task_lease_fence"]["execution_instance_verified"] is True - assert payload["task_lease_fence"]["released"] is True + inspect_code, inspect_payload = _run_cli( + capsys, + registry, + "task-lease", + "inspect", + "--goal-id", + GOAL_ID, + "--todo-id", + todo["todo_id"], + ) + assert inspect_code == 0 + assert inspect_payload["lease"]["status"] == "released" diff --git a/tests/control_plane/test_handoff_mode_source.py b/tests/control_plane/test_handoff_mode_source.py deleted file mode 100644 index e925ca563c..0000000000 --- a/tests/control_plane/test_handoff_mode_source.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Real compatibility sources: complete overlays and their mutation locks.""" -from __future__ import annotations - -import json -from pathlib import Path - -import pytest - -from loopx.control_plane.todos.handoff_mode import HandoffModeError, set_goal_handoff_mode -from loopx.control_plane.goals.legacy_event_source import RetiredTodoEventSourceError - - -def workspace(root: Path) -> tuple[Path, Path, Path]: - state = root / "ACTIVE_GOAL_STATE.md" - state.write_text("---\ngoal_id: mode-source\nhandoff_mode: legacy\n---\n\n## Agent Todo\n", encoding="utf-8") - registry = root / "registry.json" - runtime = root / "runtime" - registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{"id": "mode-source", - "repo": str(root), "state_file": state.name, "adapter": {"kind": "harness_self_improvement"}}]})) - return registry, state, runtime - - - - -def switch(registry: Path, **kwargs): - return set_goal_handoff_mode(registry_path=registry, goal_id="mode-source", mode="hard_lease", **kwargs) - - - - - - -@pytest.mark.parametrize("contents", ["not json\n", '{"schema_version":"unknown"}\n']) -def test_unreadable_event_source_is_not_quiescence(tmp_path: Path, contents: str) -> None: - registry, state, _ = workspace(tmp_path) - path = state.with_name("events.jsonl") - path.write_text(contents) - before = state.read_bytes() - with pytest.raises(RetiredTodoEventSourceError): - switch(registry) - assert state.read_bytes() == before - - - - -def test_retired_source_refuses_even_a_mode_noop(tmp_path: Path) -> None: - registry, state, _ = workspace(tmp_path) - state.write_text(state.read_text().replace("legacy", "hard_lease")) - state.with_name("events.jsonl").write_text("corrupt unrelated event\n") - before = state.read_bytes() - with pytest.raises(RetiredTodoEventSourceError): - switch(registry) - assert state.read_bytes() == before - - -@pytest.mark.parametrize("ending", [b"", b"\r\n"]) -def test_public_mode_write_preserves_unrelated_bytes(tmp_path: Path, ending: bytes) -> None: - registry, state, _ = workspace(tmp_path) - source = '---\r\ngoal_id: mode-source\r\ntitle: "a\u2028b"\r\nhandoff_mode: legacy\r\n---\r\n\r\n## Agent Todo'.encode() + ending - state.write_bytes(source) - assert switch(registry)["changed"] is True - assert state.read_bytes() == source.replace(b"handoff_mode: legacy", b"handoff_mode: hard_lease") - - -def test_duplicate_mode_fields_reject_without_partial_repair(tmp_path: Path) -> None: - registry, state, _ = workspace(tmp_path) - state.write_text(state.read_text().replace("handoff_mode: legacy", "handoff_mode: legacy\nhandoff_mode: banana")) - before = state.read_bytes() - with pytest.raises(HandoffModeError) as caught: - switch(registry) - assert caught.value.code == "handoff_mode_duplicate_field" - assert state.read_bytes() == before - - -def test_large_prose_never_crosses_the_mode_plan_transport(tmp_path: Path) -> None: - registry, state, _ = workspace(tmp_path) - original = state.read_bytes() + b"\n## Evidence\n" + b"Unrelated durable prose.\n" * 150_000 - state.write_bytes(original) - assert switch(registry)["changed"] is True - assert state.read_bytes() == original.replace(b"handoff_mode: legacy", b"handoff_mode: hard_lease") diff --git a/tests/control_plane/test_in_flight_turn_replay_cli.py b/tests/control_plane/test_in_flight_turn_replay_cli.py index ae7699d5ff..eb5420c4ae 100644 --- a/tests/control_plane/test_in_flight_turn_replay_cli.py +++ b/tests/control_plane/test_in_flight_turn_replay_cli.py @@ -27,7 +27,7 @@ ) -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("waiting", [False, True]) def test_in_flight_turn_replay_preserves_open_todo_and_fresh_turn_admission( tmp_path: Path, @@ -51,14 +51,13 @@ def todo() -> dict[str, Any]: listed = cli("todo", "list", "--goal-id", GOAL_ID) return next(item for item in listed["todos"] if item["todo_id"] == TODO_ID) - if provider != "legacy": - listed = cli("todo", "list", "--goal-id", GOAL_ID) - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, todos=listed["todos"], handoff_mode="soft_claim", - ) - initialize_canonical_authority( - runtime, GOAL_ID, projection, state_path=state, provider=provider, - ) + listed = cli("todo", "list", "--goal-id", GOAL_ID) + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=listed["todos"], handoff_mode="soft_claim", + ) + initialize_canonical_authority( + runtime, GOAL_ID, projection, state_path=state, provider=provider, + ) before = todo() turn_id = f"inflight-{provider}-{waiting}" binding = ("--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", turn_id) diff --git a/tests/control_plane/test_issue_fix_private_evidence_settlement.py b/tests/control_plane/test_issue_fix_private_evidence_settlement.py index 4907f8703f..7bfe85fa81 100644 --- a/tests/control_plane/test_issue_fix_private_evidence_settlement.py +++ b/tests/control_plane/test_issue_fix_private_evidence_settlement.py @@ -57,18 +57,6 @@ def run(*command: str) -> dict: assert code == 0, json.dumps(result, indent=2) return result - run( - "todo", - "claim", - "--goal-id", - GOAL_ID, - "--todo-id", - TODO_ID, - "--agent-id", - AGENT_ID, - "--claimed-by", - AGENT_ID, - ) todos = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] initialize_canonical_authority( runtime, @@ -79,29 +67,45 @@ def run(*command: str) -> dict: state_path=state, provider="sqlite", ) - binding = ( + run( + "task-lease", + "acquire", "--goal-id", GOAL_ID, "--todo-id", TODO_ID, - "--agent-id", + "--owner", AGENT_ID, - "--turn-instance-id", - TURN_ID, + "--idempotency-key", + "private-evidence-fixture", ) - run("quota", "should-run", "--codex-app", *binding, "--scan-path", str(project)) run( - "task-lease", - "acquire", + "todo", + "claim", "--goal-id", GOAL_ID, "--todo-id", TODO_ID, - "--owner", + "--agent-id", AGENT_ID, - "--idempotency-key", + "--claimed-by", + AGENT_ID, + "--task-lease-idempotency-key", "private-evidence-fixture", + "--task-lease-expected-version", + "1", ) + binding = ( + "--goal-id", + GOAL_ID, + "--todo-id", + TODO_ID, + "--agent-id", + AGENT_ID, + "--turn-instance-id", + TURN_ID, + ) + run("quota", "should-run", "--codex-app", *binding, "--scan-path", str(project)) refresh = ( "refresh-state", *binding, diff --git a/tests/control_plane/test_large_source_journey.py b/tests/control_plane/test_large_source_journey.py deleted file mode 100644 index 3d9e839301..0000000000 --- a/tests/control_plane/test_large_source_journey.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Full source transfer through public qualification and reviewed cutover. - -Large notes are semantic source fields, not discarded prose or padding outside -Todos. Every CLI call runs in a fresh process against disposable real storage. -""" -import json - -import pytest - -from test_runtime_shadow_bounded_e2e import cli, enable, workspace -from loopx.control_plane.coordination.runtime_shadow import build_runtime_shadow_source_snapshot -from loopx.control_plane.effect_runtime import MAX_REQUEST_BYTES -from loopx.control_plane.todos.contract import format_todo_metadata_line - -pytestmark = pytest.mark.stage2c_e2e - - -def test_large_complete_source_can_qualify_promote_and_read_back(tmp_path): - registry, runtime, state = workspace(tmp_path) - note = "完整🙂" * 1600 - blocks = [f"- [ ] Retained work {index}\n " + format_todo_metadata_line( - todo_id=f"todo_{index:03}", role="agent", status="open", - task_class="advancement_task", note=note, - ) for index in range(160)] - state.write_text(state.read_text() + "\n".join(blocks) + "\n", encoding="utf-8") - goal = enable(registry) - projection, _ = build_runtime_shadow_source_snapshot( - goal=goal, runtime_root=runtime, state_path=state, registry_path=registry) - assert len(json.dumps(projection, ensure_ascii=False).encode()) > MAX_REQUEST_BYTES - assert len(projection["todos"]) == 160 - assert all(todo["note"] == note for todo in projection["todos"]) - - boot = cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - assert boot["bootstrap"]["status"] == "applied" - added = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Captured after full bootstrap") - assert added["coordination_runtime_shadow"]["source_transaction_correlated"] is True - inspected = cli(registry, runtime, "coordination-shadow", "inspect", "--goal-id", "goal-a") - assert inspected["inspection"]["status"] == "matched" - qualified = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", - "--minimum-operations", "1", "--require-event-kind", "todo_add") - assert qualified["qualification"]["qualified"] is True - assert qualified["qualification"]["sustained_parity_verified"] is False - common = ("coordination-shadow", "promote", "--goal-id", "goal-a", - "--minimum-operations", "1", "--require-event-kind", "todo_add") - preview = cli(registry, runtime, *common) - assert preview["promotion"]["status"] == "preview_ready" - assert not (runtime / "authority" / "file-v0").exists() - applied = cli(registry, runtime, *common, "--execute") - assert applied["promotion"]["status"] == "applied" - assert applied["promotion"]["legacy_writer_fenced"] is True - read = cli(registry, runtime, "todo", "list", "--goal-id", "goal-a", "--role", "agent") - assert len(read["todos"]) == 161 - retained = [row for row in read["todos"] if row["todo_id"] != added["todo_id"]] - assert {row["todo_id"] for row in retained} == {f"todo_{i:03}" for i in range(160)} - assert all(row["note"] == note for row in retained) diff --git a/tests/control_plane/test_leased_monitor_poll.py b/tests/control_plane/test_leased_monitor_poll.py index ad9ce3bcf1..8214e2080d 100644 --- a/tests/control_plane/test_leased_monitor_poll.py +++ b/tests/control_plane/test_leased_monitor_poll.py @@ -9,8 +9,11 @@ from canonical_authority_fixture import isolate_sqlite_runtime from test_native_monitor_poll import _canonical -from test_monitor_followthrough_contract import GOAL_ID, AGENT_ID, _write_fixture, _add_monitor -from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted +from test_monitor_followthrough_contract import GOAL_ID, AGENT_ID, _write_fixture +from loopx.control_plane.coordination.local_authority import ( + CanonicalAuthorityRequired, + read_canonical_todos_if_promoted, +) from loopx.control_plane.quota.error_codes import QuotaCommandValidationError from loopx.control_plane.quota.monitor_poll_lease_transport import current_monitor_lease_proof from loopx.control_plane.scheduler.monitor_poll_writeback import write_monitor_poll_todo_state @@ -216,13 +219,20 @@ def test_old_observation_time_cannot_revive_expired_proof(tmp_path, monkeypatch, assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) == before -def test_explicit_proof_never_falls_back_to_legacy_writer(tmp_path): - registry, runtime, state = _write_fixture(tmp_path) - monitor = _add_monitor(registry, text="Observe public progress", target_key="watch", next_due_at="2000-01-01T00:00:00Z") +def test_explicit_proof_requires_canonical_authority(tmp_path): + registry, runtime, state = _write_fixture(tmp_path, canonical=False) + state.write_text( + state.read_text(encoding="utf-8") + + "\n- [ ] [P1] Observe public progress.\n" + + " \n", + encoding="utf-8", + ) before = state.read_bytes() - with pytest.raises(ValueError, match="requires promoted canonical authority"): + with pytest.raises(CanonicalAuthorityRequired): write_monitor_poll_todo_state(registry_path=registry, runtime_root=runtime, goal_id=GOAL_ID, - execute=True, todo_id=monitor["todo_id"], agent_id=AGENT_ID, + execute=True, todo_id="todo_unpromoted_monitor", agent_id=AGENT_ID, generated_at="2026-09-01T00:00:00Z", result_hash="a", material_change=False, task_lease_idempotency_key=PROOF["idempotency_key"], task_lease_expected_version=3) assert state.read_bytes() == before diff --git a/tests/control_plane/test_legacy_coordination_writer_fence.py b/tests/control_plane/test_legacy_coordination_writer_fence.py index 0e5b13014d..48e9d6e436 100644 --- a/tests/control_plane/test_legacy_coordination_writer_fence.py +++ b/tests/control_plane/test_legacy_coordination_writer_fence.py @@ -12,15 +12,12 @@ from loopx.control_plane.coordination.legacy_writer_fence import ( LegacyCoordinationWriterFenced, legacy_coordination_write_remediation, - legacy_coordination_todo_lock_path, legacy_coordination_writer_fence_path, - legacy_todo_write_transaction, require_legacy_coordination_write_allowed, ) from loopx.control_plane.coordination.local_authority import ( LocalCoordinationAuthorityUnavailable, ) -from loopx.file_lock import lock_holder_path from loopx.todos import add_goal_todo SPLIT_ROOT_GOAL_ID = "legacy-fence-split-root" @@ -236,86 +233,6 @@ def test_remediation_renders_identically_in_python_and_typescript( assert rendered.stdout == expected, name -def test_todo_write_transaction_fences_under_the_effective_runtime_root( - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - """A fence engaged under --runtime-root must fence the legacy writer.""" - - registry, state, _runtime_registry, runtime_override = _write_split_root_fixture( - tmp_path - ) - _engage_fence(runtime_override) - _blocked_effect_runtime(monkeypatch) - - with pytest.raises(LegacyCoordinationWriterFenced): - with legacy_todo_write_transaction( - registry, - SPLIT_ROOT_GOAL_ID, - state, - SPLIT_ROOT_AGENT, - "todo_add", - False, - runtime_root=runtime_override, - ): - pytest.fail("transaction body must not run while fenced") - - -def test_todo_write_transaction_keeps_registry_root_without_runtime_root( - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - """Omitting runtime_root keeps the registry-derived root (backward compat).""" - - registry, state, runtime_registry, _runtime_override = _write_split_root_fixture( - tmp_path - ) - _engage_fence(runtime_registry) - _blocked_effect_runtime(monkeypatch) - - with pytest.raises(LegacyCoordinationWriterFenced): - with legacy_todo_write_transaction( - registry, - SPLIT_ROOT_GOAL_ID, - state, - SPLIT_ROOT_AGENT, - "todo_add", - False, - ): - pytest.fail("transaction body must not run while fenced") - - -def test_todo_write_lock_lands_under_the_effective_runtime_root( - tmp_path: Path, -) -> None: - """The todo mutex must serialize with promotion under the override root.""" - - registry, state, runtime_registry, runtime_override = _write_split_root_fixture( - tmp_path - ) - override_lock = legacy_coordination_todo_lock_path( - runtime_root=runtime_override, - goal_id=SPLIT_ROOT_GOAL_ID, - ) - - registry_lock = legacy_coordination_todo_lock_path( - runtime_root=runtime_registry, - goal_id=SPLIT_ROOT_GOAL_ID, - ) - - with legacy_todo_write_transaction( - registry, - SPLIT_ROOT_GOAL_ID, - state, - SPLIT_ROOT_AGENT, - "todo_add", - False, - runtime_root=runtime_override, - ): - assert lock_holder_path(override_lock).exists() - assert not lock_holder_path(registry_lock).exists() - - def test_goal_todo_add_uses_provider_at_override_root_when_promoted( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, diff --git a/tests/control_plane/test_local_authority_shadow_cli_e2e.py b/tests/control_plane/test_local_authority_shadow_cli_e2e.py deleted file mode 100644 index 4e05b66688..0000000000 --- a/tests/control_plane/test_local_authority_shadow_cli_e2e.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Real CLI upgrade from a retired setting to an explicit capture lineage.""" -from __future__ import annotations - -import json -from pathlib import Path - -from tests.control_plane.shadow_e2e_fixture import workspace - - -def test_retired_setting_cannot_enable_capture_or_satisfy_bootstrap(tmp_path: Path) -> None: - ws = workspace(tmp_path, bootstrap=False) - data = json.loads(ws.registry.read_text()) - config = data["goals"][0]["coordination"] - config.pop("runtime_shadow") - config["authority_shadow"] = {"schema_version": "loopx_local_authority_shadow_config_v0", "mode": "file_one_way"} - ws.registry.write_text(json.dumps(data)) - original = ws.registry.read_bytes() - rejected = ws.cli("configure-goal", "--local-authority-shadow-file", "--execute", success=False) - assert rejected["ok"] is False - assert "local_authority_shadow_retired" in rejected["error"] - assert ws.registry.read_bytes() == original - initial = ws.add("Continue work with a retained retired configuration.") - assert "authority_shadow" not in initial - status = ws.cli("authority-shadow", "status") - assert status["config"]["status"] == "retired" - assert status["config"]["enabled"] is False - assert status["candidate"]["status"] == "missing" - assert status["management"]["status"] == "missing" - drain = ws.drain() - assert drain["config_enabled"] is False - assert not (ws.runtime / "authority-shadow").exists() - - # Configuration is not a source transaction or an automatic bootstrap. - ws.cli("configure-goal", "--clear-local-authority-shadow", "--coordination-runtime-shadow-file", "--execute") - assert not (ws.runtime / "authority-shadow" / "file-v0").exists() - boot = ws.cli("coordination-shadow", "bootstrap", "--execute") - assert boot["bootstrap"]["status"] == "applied" - added = ws.add("Capture the next transaction using the existing outbox.") - assert added["coordination_runtime_shadow"]["outcome"] in {"delivered", "replayed"} - assert "authority_shadow" not in added - status = ws.cli("authority-shadow", "status") - assert status["config"]["status"] == "disabled" - assert status["management"]["status"] == "active" - assert status["candidate"]["codec_agreement"] is True - assert not (ws.runtime / "authority-shadow" / "file" / ws.goal).exists() - - -def test_clearing_old_setting_does_not_retire_an_active_capture(tmp_path: Path) -> None: - ws = workspace(tmp_path) - data = json.loads(ws.registry.read_text()) - data["goals"][0]["coordination"]["authority_shadow"] = { - "schema_version": "loopx_local_authority_shadow_config_v0", "mode": "file_one_way"} - ws.registry.write_text(json.dumps(data)) - before = ws.cli("authority-shadow", "status")["management"] - ws.cli("configure-goal", "--clear-local-authority-shadow", "--execute") - after = ws.cli("authority-shadow", "status")["management"] - assert after == before - # Existing process-loss recovery still owns the source transaction. - crashed = ws.crash("before_marker", "todo", "add", "--role", "agent", "--text", "Recover captured source.") - assert crashed is not None - result = ws.drain() - assert result["ok"] is True - assert result["drained_count"] == 1 - assert ws.drain()["drained_count"] == 0 diff --git a/tests/control_plane/test_local_authority_shadow_drain.py b/tests/control_plane/test_local_authority_shadow_drain.py deleted file mode 100644 index 9998e4de0c..0000000000 --- a/tests/control_plane/test_local_authority_shadow_drain.py +++ /dev/null @@ -1,500 +0,0 @@ -# Native persistence-window regressions live in shadow_drain.test.ts and the real-process CLI suite. -from __future__ import annotations - -import json -import uuid -from pathlib import Path - -import pytest - -from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox -from loopx.control_plane.coordination.local_authority_shadow_projection import ( - head_digest, - partition_digest, - text_digest, -) -from loopx.file_lock import exclusive_file_lock, exclusive_cross_runtime_file_lock -from loopx.control_plane.coordination.shadow_management import require_shadow_primary_write_allowed -from shadow_e2e_fixture import workspace -from loopx.history import load_registry -from loopx.registry import find_registry_goal -from loopx.todos import list_goal_todos - - -GOAL_ID = "goal-e2e" - - -def test_shadow_parity_ignores_only_resume_evaluation_observation_clock() -> None: - head = { - "handoff_mode": "hard_lease", - "todos": [{ - "todo_id": "todo-a", - "resume_ready": False, - "resume_condition": { - "evaluated_at": "2026-09-20T00:00:00Z", - "satisfied": False, - "availability_reason": "resume_condition_pending", - }, - }], - "leases": [], - } - later_observation = json.loads(json.dumps(head)) - later_observation["todos"][0]["resume_condition"]["evaluated_at"] = ( - "2026-09-21T00:00:00Z" - ) - - assert head_digest(head) == head_digest(later_observation) - assert partition_digest({ - "handoff_mode": head["handoff_mode"], - "todos": head["todos"], - }) == partition_digest({ - "handoff_mode": later_observation["handoff_mode"], - "todos": later_observation["todos"], - }) - - changed_decision = json.loads(json.dumps(later_observation)) - changed_decision["todos"][0]["resume_condition"]["satisfied"] = True - assert head_digest(head) != head_digest(changed_decision) - assert partition_digest({ - "handoff_mode": head["handoff_mode"], - "todos": head["todos"], - }) != partition_digest({ - "handoff_mode": changed_decision["handoff_mode"], - "todos": changed_decision["todos"], - }) - - -def _fixture(tmp_path: Path) -> tuple[Path, Path, Path]: - real = workspace(tmp_path / "repo") - return real.registry, real.state, real.runtime - - -def _record_todo_write( - registry: Path, - state: Path, - runtime_root: Path, - text: str, - *, - mark_committed: bool = True, - write_file: bool = True, -) -> outbox.TodoPartitionCapture: - """Prepare a source transaction without invoking a second capture-enabled writer.""" - - original = state.read_text(encoding="utf-8") - goal = find_registry_goal(load_registry(registry), GOAL_ID) - capture = outbox.TodoPartitionCapture.begin( - enabled=True, - runtime_root=runtime_root, - goal_id=GOAL_ID, - state_path=state, - write_class="todo_add", - original_text=original, - projector=adapter.todo_partition_projector(goal, state_path=state), - ) - todo_id = f"todo_{uuid.uuid4().hex[:12]}" - new_text = original + ( - f"- [ ] {text}\n" - f" \n" - ) - capture.prepare(new_text) - assert capture.outcome.failure is None - assert capture.outcome.entry_id is not None - if write_file: - state.write_text(new_text, encoding="utf-8") - if mark_committed: - capture.committed() - return capture - - -def _drain(registry: Path, runtime_root: Path, **overrides: object) -> adapter.DrainResult: - limits = {"max_entries": 20, "budget_seconds": 10, **overrides} - return adapter.drain_local_authority_shadow_outbox( - registry_path=registry, runtime_root=runtime_root, goal_id=GOAL_ID, - **limits, # type: ignore[arg-type] - ) - - -def _todo_dir(runtime_root: Path) -> Path: - return outbox.partition_directory(runtime_root, GOAL_ID, "todos") - - -def test_drain_delivers_each_committed_entry_once_in_order_and_verifies_readback(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - captures = [ - _record_todo_write(registry, state, runtime_root, f"Fact {index}") for index in range(3) - ] - assert [capture.outcome.seq for capture in captures] == [1, 2, 3] - - result = _drain(registry, runtime_root) - - assert result.ok is True - assert result.outcome == "drained" - assert result.config_enabled is True - assert (result.delivered, result.replayed, result.no_op) == (3, 0, 0) - assert result.pending_after == 0 - assert result.prepared_only_after == 0 - assert result.budget_exhausted is False - assert result.candidate_readback_verified is True - assert result.last_cursor == "4" - assert (result.cursor_before, result.cursor_after, result.drained_count) == ("1", "4", 3) - payload = result.to_payload() - assert payload["ok"] is True - assert payload["drained_count"] == 3 - assert payload["cursor_after"] == "4" - assert [item["outcome"] for item in result.entries] == ["delivered"] * 3 - assert [item["cursor"] for item in result.entries] == ["2", "3", "4"] - assert [item["entry_id"] for item in result.entries] == [ - capture.outcome.entry_id for capture in captures - ] - assert result.store_identity is not None - assert result.store_identity.startswith("file:") - assert list(_todo_dir(runtime_root).iterdir()) == [_todo_dir(runtime_root) / "drain-cursor.json"] - cursor = outbox.read_cursor(_todo_dir(runtime_root)) - assert cursor is not None - assert cursor["last_seq"] == 3 - assert cursor["last_entry_id"] == captures[-1].outcome.entry_id - assert cursor["last_partition_digest"] == captures[-1].outcome.partition_digest - - view = adapter.read_local_authority_shadow(runtime_root=runtime_root, goal_id=GOAL_ID, scan_limit=10) - assert view["status"] == "loaded" - head = view["head"] - assert head["schema_version"] == "loopx_coordination_runtime_shadow_projection_v0" - assert head["partitions"]["todos"] == { - "seq": 3, - "partition_digest": captures[-1].outcome.partition_digest, - } - assert head["partitions"]["leases"] is None - listed = list_goal_todos(registry_path=registry, goal_id=GOAL_ID, runtime_root_arg=str(runtime_root)) - assert [todo["todo_id"] for todo in head["todos"]] == sorted( - todo["todo_id"] for todo in listed["todos"] - ) - assert view["head_digest"] == head_digest(head) == result.head_digest - assert [tx["operation_id"] for tx in view["scan"]["transactions"][1:]] == [ - capture.outcome.entry_id for capture in captures - ] - receipts = [tx["receipts"][0] for tx in view["scan"]["transactions"][1:]] - assert all(receipt["source_transaction_correlated"] is True for receipt in receipts) - assert all(receipt["durable_source_outbox"] is True for receipt in receipts) - assert all(receipt["parity_verdict"] == "not_evaluated" for receipt in receipts) - assert [receipt["source_bytes_digest"] for receipt in receipts] == [ - capture.outcome.source_bytes_digest for capture in captures - ] - assert str(runtime_root) not in json.dumps(view) - - again = _drain(registry, runtime_root) - assert again.outcome == "nothing_pending" - assert again.ok is True - - -def test_drain_defers_when_another_drainer_holds_the_lock(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - _record_todo_write(registry, state, runtime_root, "Pending behind a drainer") - with exclusive_cross_runtime_file_lock( - outbox.drain_lock_target(runtime_root, GOAL_ID), timeout_seconds=1.0, operation="test_hold" - ): - result = _drain(registry, runtime_root, lock_timeout_seconds=0.05) - assert result.outcome == "drain_deferred" - assert result.reason_code == "drain_lock_busy" - assert result.ok is False - assert result.pending_after == 1 - assert not (runtime_root / "authority-shadow" / "file").exists() - - -def test_drain_batch_is_bounded_and_reports_what_it_left(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - for index in range(3): - _record_todo_write(registry, state, runtime_root, f"Bounded {index}") - - first = _drain(registry, runtime_root, max_entries=2) - assert first.outcome == "drained" - assert first.ok is True - assert first.delivered == 2 - assert first.budget_exhausted is True - assert first.pending_after == 1 - assert first.candidate_readback_verified is True - - second = _drain(registry, runtime_root) - assert second.delivered == 1 - assert second.pending_after == 0 - assert (second.cursor_before, second.cursor_after) == ("3", "4") - view = adapter.read_local_authority_shadow(runtime_root=runtime_root, goal_id=GOAL_ID) - assert view["cursor"] == "4" - assert view["head"]["partitions"]["todos"]["seq"] == 3 - - -def test_prepared_only_entries_resolve_only_under_a_free_primary_lock(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - proven = _record_todo_write(registry, state, runtime_root, "Marker lost after write", mark_committed=False) - with exclusive_file_lock(state, timeout_seconds=1.0, operation="writer_in_flight"): - busy = _drain(registry, runtime_root) - assert busy.outcome == "stopped" - assert busy.reason_code == "primary_writer_busy" - assert busy.prepared_only_after == 1 - assert busy.delivered == 0 - - result = _drain(registry, runtime_root) - assert result.ok is True - assert result.delivered == 1 - assert result.no_op == 0 - assert result.entries[0]["resolution"] == "committed_proven_by_readback" - assert result.entries[0]["entry_id"] == proven.outcome.entry_id - view = adapter.read_local_authority_shadow(runtime_root=runtime_root, goal_id=GOAL_ID, scan_limit=5) - assert view["head"]["partitions"]["todos"]["partition_digest"] == proven.outcome.partition_digest - receipt = view["scan"]["transactions"][1]["receipts"][0] - assert receipt["resolution"] == "committed_proven_by_readback" - - abandoned = _record_todo_write( - registry, state, runtime_root, "Never landed", mark_committed=False, write_file=False - ) - result = _drain(registry, runtime_root) - assert result.delivered == 1 - assert result.no_op == 1 - assert result.entries[0]["resolution"] == "abandoned" - assert result.entries[0]["entry_id"] == abandoned.outcome.entry_id - view = adapter.read_local_authority_shadow(runtime_root=runtime_root, goal_id=GOAL_ID, scan_limit=5) - assert view["cursor"] == "3" - assert view["head"]["partitions"]["todos"]["seq"] == 1 - assert view["scan"]["transactions"][2]["events"][0]["kind"] == "source_transaction_abandoned" - - -def test_unexplained_prepared_entry_holds_without_reseed_or_deletion(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - _record_todo_write(registry, state, runtime_root, "Baseline") - assert _drain(registry, runtime_root).delivered == 1 - stale = _record_todo_write(registry, state, runtime_root, "Half-recorded", mark_committed=False) - state.write_text(state.read_text() + "\nExternal source change after the writer stopped.\n") - before = {str(path.relative_to(runtime_root)): path.read_bytes() - for path in (runtime_root / "authority-shadow").rglob("*") if path.is_file()} - result = _drain(registry, runtime_root) - assert result.outcome == "stopped" - assert result.reason_code == "outbox_source_unproved" - assert result.delivered == result.no_op == 0 - assert result.entries == [] - assert [entry.entry_id for entry in outbox.list_entries(_todo_dir(runtime_root))] == [stale.outcome.entry_id] - assert {str(path.relative_to(runtime_root)): path.read_bytes() - for path in (runtime_root / "authority-shadow").rglob("*") if path.is_file()} == before - - -def test_lease_partition_entries_retain_complete_records_at_drain(tmp_path: Path) -> None: - registry, _state, runtime_root = _fixture(tmp_path) - directory = outbox.partition_directory(runtime_root, GOAL_ID, "leases") - record = { - "goal_id": GOAL_ID, - "todo_id": "todo_00000000000a", - "owner": "agent-a", - "idempotency_key": "k1", - "write_scopes": ["loopx/**"], - "version": 2, - "lease_epoch": 1, - "acquired_at": "2026-09-03T00:00:00+00:00", - "updated_at": "2026-09-03T00:01:00+00:00", - "expires_at": "2026-09-03T00:31:00+00:00", - "released_at": None, - "status": "active", - "lease_path": "/should/never/be/compared", - "acquire_ttl_seconds": 1800, - } - bytes_digest = text_digest(json.dumps(record, indent=2) + "\n") - binding = require_shadow_primary_write_allowed(runtime_root, GOAL_ID) - assert binding is not None - entry_id = outbox.entry_identity(goal_id=GOAL_ID, partition="leases", seq=1, source_ref=bytes_digest, - capture_lineage_id=binding["capture_lineage_id"], source_root_digest=binding["source_root_digest"]) - outbox.durable_write_json( - directory / outbox.entry_file_name(1, entry_id, "prepared"), - { - "schema_version": outbox.OUTBOX_ENTRY_SCHEMA, - "goal_id": GOAL_ID, - "capture_lineage_id": binding["capture_lineage_id"], - "partition": "leases", - "seq": 1, - "entry_id": entry_id, - "writer": {"runtime": "typescript", "write_class": "task_lease_acquire", "operation_id": "op-1"}, - "source": { - "kind": "task_lease_record", - "previous_bytes_digest": None, - "previous_partition_digest": partition_digest({"leases": []}), - "bytes_digest": bytes_digest, - "lease": {"todo_id": record["todo_id"], "version": 2, "lease_epoch": 1, "status": "active", "updated_at": record["updated_at"]}, - "previous_lease": None, - "event_id": None, - }, - "source_root_digest": outbox.runtime_root_digest(runtime_root), - "projection": {"leases": [{"file_stem": record["todo_id"], "record": record}]}, - "partition_digest": None, - "prepared_at": "2026-09-03T00:01:00.000Z", - }, - ) - outbox.durable_write_json( - directory / outbox.entry_file_name(1, entry_id, "committed"), - {"schema_version": outbox.OUTBOX_COMMIT_SCHEMA, "entry_id": entry_id, "capture_lineage_id": binding["capture_lineage_id"], "committed_at": "2026-09-03T00:01:00.100Z"}, - ) - - primary_lease = outbox.lease_directory(runtime_root, GOAL_ID) / (record["todo_id"] + ".json") - primary_lease.parent.mkdir(parents=True, exist_ok=True) - primary_lease.write_text(json.dumps(record, indent=2) + "\n") - result = _drain(registry, runtime_root) - - assert result.ok is True - assert result.delivered == 1 - view = adapter.read_local_authority_shadow(runtime_root=runtime_root, goal_id=GOAL_ID, scan_limit=5) - head = view["head"] - assert head["todos"] == [] - assert head["handoff_mode"] == "hard_lease" - assert head["leases"] == [record] - expected_digest = partition_digest({"leases": head["leases"]}) - assert head["partitions"]["leases"] == {"seq": 1, "partition_digest": expected_digest} - assert result.entries[0]["partition_digest"] == expected_digest - assert "/should/never/be/compared" in json.dumps(view) - - -def test_status_reports_backlog_candidate_and_growth_facts(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - empty = adapter.local_authority_shadow_status(registry_path=registry, runtime_root=runtime_root, goal_id=GOAL_ID) - assert empty["ok"] is True - assert empty["config"]["status"] == "disabled" - assert empty["candidate"]["status"] == "loaded" - assert empty["candidate"]["cursor"] == "1" - assert empty["store_bytes"] > 0 - assert empty["retention_pressure"] is False - assert str(runtime_root) not in json.dumps(empty) - - _record_todo_write(registry, state, runtime_root, "Status fact") - pending = adapter.local_authority_shadow_status(registry_path=registry, runtime_root=runtime_root, goal_id=GOAL_ID) - assert pending["outbox"]["todos"]["committed_pending"] == 1 - assert _drain(registry, runtime_root).delivered == 1 - - drained = adapter.local_authority_shadow_status(registry_path=registry, runtime_root=runtime_root, goal_id=GOAL_ID) - assert drained["outbox"]["todos"] == { - "committed_pending": 0, - "prepared_only": 0, - "retired_residue": 0, - "next_seq": 0, - "cursor_last_seq": 1, - "cursor_last_entry_id": outbox.read_cursor(_todo_dir(runtime_root))["last_entry_id"], - "invalid": None, - } - assert drained["candidate"]["status"] == "loaded" - assert drained["candidate"]["cursor"] == "2" - assert drained["candidate"]["codec_agreement"] is True - assert drained["candidate"]["head_schema_version"] == "loopx_coordination_runtime_shadow_projection_v0" - assert drained["store_bytes"] > 0 - - -def test_capture_evidence_v1_reports_measured_facts_only(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - capture = _record_todo_write(registry, state, runtime_root, "Evidence fact") - # A prepared entry and its commit marker are the two files this pass retires. - assert len(list(_todo_dir(runtime_root).iterdir())) == 2 - drain = _drain(registry, runtime_root) - evidence = adapter.capture_evidence(goal_id=GOAL_ID, capture=capture.outcome, drain=drain) - assert evidence["outcome"] == "delivered" - assert evidence["reason_code"] is None - assert evidence["entry"] == { - "entry_id": capture.outcome.entry_id, - "partition": "todos", - "seq": 1, - "partition_digest": capture.outcome.partition_digest, - "source_bytes_digest": text_digest(state.read_text(encoding="utf-8")), - } - assert evidence["source_transaction_correlated"] is True - assert evidence["durable_source_outbox"] is True - assert evidence["source_candidate_compared"] is False - assert evidence["parity_verdict"] == "not_evaluated" - assert evidence["drain"] == { - "outcome": "drained", - "delivered": 1, - "replayed": 0, - "reclaimed_residue": 2, - "pending_after": 0, - "prepared_only_after": 0, - "stopped_at": None, - "last_cursor": "2", - "provider_revision": drain.provider_revision, - "candidate_readback_verified": True, - } - assert evidence["store_identity"] == drain.store_identity - - deferred = adapter.DrainResult(goal_id=GOAL_ID, outcome="drain_deferred", reason_code="drain_lock_busy") - deferred_evidence = adapter.capture_evidence(goal_id=GOAL_ID, capture=capture.outcome, drain=deferred) - assert deferred_evidence["outcome"] == "drain_deferred" - assert deferred_evidence["reason_code"] == "drain_lock_busy" - assert deferred_evidence["drain"]["candidate_readback_verified"] is None - pending = adapter.capture_evidence(goal_id=GOAL_ID, capture=capture.outcome, drain=None) - assert pending["outcome"] == "pending" - assert pending["drain"] is None - observed = [evidence, deferred_evidence, pending] - - for skipped_reason in ("partition_unchanged", "shadow_disabled"): - skipped = outbox.CaptureOutcome(partition="todos", skipped_reason=skipped_reason) - no_transaction = adapter.capture_evidence(goal_id=GOAL_ID, capture=skipped, drain=None) - assert no_transaction["outcome"] == "no_transaction" - assert no_transaction["reason_code"] == skipped_reason - # Nothing was recorded, so nothing durable or correlated may be claimed. - assert no_transaction["durable_source_outbox"] is False - assert no_transaction["source_transaction_correlated"] is False - assert no_transaction["entry"] == { - "entry_id": None, - "partition": "todos", - "seq": None, - "partition_digest": None, - "source_bytes_digest": None, - } - observed.append(no_transaction) - failed = outbox.CaptureOutcome(partition="todos", failure={"reason_code": "outbox_prepare_failed", "error_class": "OSError"}) - failed_evidence = adapter.capture_evidence(goal_id=GOAL_ID, capture=failed, drain=None) - assert failed_evidence["outcome"] == "capture_failed" - assert failed_evidence["durable_source_outbox"] is False - assert failed_evidence["source_transaction_correlated"] is False - assert failed_evidence["reason_code"] == "outbox_prepare_failed" - assert failed_evidence["entry"]["entry_id"] is None - observed.append(failed_evidence) - - # Independent wire expectations: observation never grants candidate authority. - common_facts = { - "schema_version": "loopx_local_authority_shadow_evidence_v1", - "goal_id": GOAL_ID, - "capture_kind": "source_transaction_outbox", - "source_candidate_compared": False, - "parity_verdict": "not_evaluated", - "primary_authority": "legacy_local", - "candidate_provider": "file", - "candidate_read_for_decision": False, - "provider_to_local_writes": False, - "primary_writeback_preserved": True, - } - for payload in observed: - assert {key: payload[key] for key in common_facts} == common_facts - - -def test_an_orphan_marker_above_the_cursor_is_still_corruption(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - capture = _record_todo_write(registry, state, runtime_root, "Marker without its prepared file") - (_todo_dir(runtime_root) / outbox.entry_file_name(1, str(capture.outcome.entry_id), "prepared")).unlink() - with pytest.raises(outbox.OutboxError) as raised: - outbox.list_entries(_todo_dir(runtime_root)) - assert raised.value.reason_code == "outbox_file_invalid" - result = _drain(registry, runtime_root) - assert result.outcome == "stopped" - assert result.reason_code == "outbox_file_invalid" - status = adapter.local_authority_shadow_status(registry_path=registry, runtime_root=runtime_root, goal_id=GOAL_ID) - assert status["ok"] is False - assert status["outbox"]["todos"]["invalid"] == "outbox_file_invalid" - - -def test_drain_fails_closed_on_an_entry_recorded_for_another_runtime_root(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - capture = _record_todo_write(registry, state, runtime_root, "Written under a foreign root") - entry_id = str(capture.outcome.entry_id) - prepared_path = _todo_dir(runtime_root) / outbox.entry_file_name(1, entry_id, "prepared") - record = json.loads(prepared_path.read_text()) - record["source_root_digest"] = outbox.runtime_root_digest(tmp_path / "elsewhere") - outbox.durable_write_json(prepared_path, record) - before = prepared_path.read_bytes() - result = _drain(registry, runtime_root) - assert result.outcome == "stopped" - assert result.reason_code == "outbox_file_invalid" - assert result.delivered == 0 - assert prepared_path.read_bytes() == before - with pytest.raises(outbox.OutboxError): - outbox.list_entries(_todo_dir(runtime_root)) diff --git a/tests/control_plane/test_local_authority_shadow_outbox.py b/tests/control_plane/test_local_authority_shadow_outbox.py deleted file mode 100644 index f00bb2e994..0000000000 --- a/tests/control_plane/test_local_authority_shadow_outbox.py +++ /dev/null @@ -1,546 +0,0 @@ -from __future__ import annotations - -import json -import uuid -from pathlib import Path - -import pytest - -from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox -from loopx.control_plane.coordination.coordination_state_contract_generated import ( - COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_RESULT_SCHEMA, -) -from loopx.control_plane.coordination.local_authority_shadow_projection import ( - ProjectionValueError, - canonical_bytes, - compact_lease, - partition_digest, - sha256_digest, - source_effect_runtime_result, - text_digest, - todo_partition_projection, -) -from loopx.control_plane.coordination.runtime_shadow import ( - bootstrap_coordination_runtime_shadow, - build_runtime_shadow_source_snapshot, - RuntimeInvoker, -) -from loopx.control_plane.coordination.shadow_goal_scope import shadow_goal_scope -from loopx.control_plane.coordination.shadow_management import ( - read_shadow_bootstrap_source_path, - read_shadow_management_state, - require_shadow_primary_write_allowed, - shadow_maintenance_lock_target, - shadow_management_directory, - shadow_management_state_path, -) -from loopx.control_plane.effect_runtime import EffectRuntimeResponseAmbiguous, EffectRuntimeRejected -from loopx.file_lock import exclusive_mutation_file_lock -from loopx.history import load_registry -from loopx.registry import find_registry_goal - - -GOAL_ID = "goal-outbox" - - -def _fixture( - tmp_path: Path, - *, - bootstrap: bool = True, - incidental_goal_instance_id: bool = False, - runtime_invoker: RuntimeInvoker = source_effect_runtime_result, -) -> tuple[Path, Path, Path]: - repo = tmp_path / "repo" - repo.mkdir() - state = repo / "ACTIVE_GOAL_STATE.md" - state.write_text( - "---\n" - f"goal_id: {GOAL_ID}\n" - "handoff_mode: hard_lease\n" - "updated_at: 2026-09-03T00:00:00+00:00\n" - "---\n\n" - "## Agent Todo\n\n", - encoding="utf-8", - ) - runtime_root = tmp_path / "runtime" - registry = tmp_path / "registry.json" - goal = { - "id": GOAL_ID, - "domain": "harness_self_improvement", - "status": "active", - "repo": str(repo), - "state_file": state.name, - "adapter": {"kind": "harness_self_improvement"}, - "coordination": { - "agent_model": "peer_v1", - "registered_agents": ["agent-a"], - }, - } - if incidental_goal_instance_id: - goal["goal_instance_id"] = "ginst_0123456789abcdef0123456789abcdef" - registry.write_text( - json.dumps( - { - "common_runtime_root": str(runtime_root), - "goals": [goal], - } - ), - encoding="utf-8", - ) - if bootstrap: - goal = find_registry_goal(load_registry(registry), GOAL_ID) - projection, snapshot = build_runtime_shadow_source_snapshot( - goal=goal, runtime_root=runtime_root, state_path=state, registry_path=registry, - ) - enabled_goal = {**goal, "coordination": {**goal["coordination"], "runtime_shadow": { - "schema_version": "loopx_coordination_runtime_shadow_config_v0", - "enabled": True, "provider": "file_v0", - }}} - def bootstrap_or_receipt(method: str, request: dict) -> dict: - try: - return runtime_invoker(method, request) - except EffectRuntimeResponseAmbiguous: - # The RPC deadline still applies. Do not resend an uncertain - # write; read its exact completed receipt after the existing - # maintenance lock releases, using the normal lock budget. - with exclusive_mutation_file_lock(shadow_maintenance_lock_target(runtime_root, GOAL_ID)): - journal = read_shadow_management_state(runtime_root, GOAL_ID) - if journal is None: - raise - assert journal["status"] == "active", journal - assert journal["operation"]["operation_id"] == request["operation_id"], journal - assert journal["operation"]["request_digest"] == sha256_digest(request), journal - binding = require_shadow_primary_write_allowed(runtime_root, GOAL_ID) - assert binding is not None, journal - assert read_shadow_bootstrap_source_path(runtime_root, GOAL_ID, binding) == state.resolve() - receipt = journal["result"] - assert receipt["operation_id"] == request["operation_id"], receipt - assert {key: receipt[key] for key in binding} == binding, receipt - return {"schema_version": COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_RESULT_SCHEMA, **receipt} - - with shadow_goal_scope(registry, goal_id=GOAL_ID) as scope: - result = bootstrap_coordination_runtime_shadow( - goal=enabled_goal, runtime_root=runtime_root, goal_id=GOAL_ID, - operation_id="bootstrap:outbox-test", source_version="source:initial", - projection=projection, source_snapshot=snapshot, - runtime_invoker=bootstrap_or_receipt, - goal_ref=scope.goal_ref, - ) - # Outbox assertions require a verified bootstrap, including an exact - # durable receipt when the managed RPC response is ambiguous. - assert result["status"] in {"applied", "recovered", "replayed"}, result - assert result["operation_id"] == "bootstrap:outbox-test", result - assert result["cursor"] == "1", result - assert result["provider_revision"] == result["bootstrap_provider_revision"], result - binding = require_shadow_primary_write_allowed(runtime_root, GOAL_ID) - assert binding is not None, result - assert binding["bootstrap_operation_id"] == result["operation_id"], result - assert binding["bootstrap_provider_revision"] == result["provider_revision"], result - return registry, state, runtime_root - - -def _capture( - registry: Path, - state: Path, - runtime_root: Path, - *, - original_text: str, - enabled: bool = True, - write_class: str = "todo_add", -) -> outbox.TodoPartitionCapture: - goal = find_registry_goal(load_registry(registry), GOAL_ID) - return outbox.TodoPartitionCapture.begin( - enabled=enabled, - runtime_root=runtime_root, - goal_id=GOAL_ID, - state_path=state, - write_class=write_class, - original_text=original_text, - projector=adapter.todo_partition_projector(goal, state_path=state), - ) - - -def _planned_todo(original: str, text: str) -> tuple[str, str]: - """Build source bytes for a low-level capture test; do not run another writer.""" - todo_id = f"todo_{uuid.uuid4().hex[:12]}" - return todo_id, original + ( - f"- [ ] {text}\n" - f" \n" - ) - - -def _todo_dir(runtime_root: Path) -> Path: - return outbox.partition_directory(runtime_root, GOAL_ID, "todos") - - -def _drain(registry: Path, runtime_root: Path) -> adapter.DrainResult: - original = registry.read_bytes() - data = json.loads(original) - data["goals"][0]["coordination"]["runtime_shadow"] = { - "schema_version": "loopx_coordination_runtime_shadow_config_v0", - "enabled": True, "provider": "file_v0", - } - registry.write_text(json.dumps(data)) - try: - return adapter.drain_local_authority_shadow_outbox( - registry_path=registry, runtime_root=runtime_root, goal_id=GOAL_ID, - max_entries=10, budget_seconds=10, lock_timeout_seconds=2, - ) - finally: - registry.write_bytes(original) - - -def _record_change(registry: Path, state: Path, runtime_root: Path, text: str) -> outbox.TodoPartitionCapture: - original = state.read_text() - _, proposed = _planned_todo(original, text) - capture = _capture(registry, state, runtime_root, original_text=original) - capture.prepare(proposed) - state.write_text(proposed) - capture.committed() - assert capture.outcome.failure is None, capture.outcome.failure - return capture - - -def test_legacy_profile_ignores_an_incidental_goal_instance_id( - tmp_path: Path, -) -> None: - registry, state, runtime_root = _fixture( - tmp_path, - incidental_goal_instance_id=True, - ) - - capture = _record_change( - registry, - state, - runtime_root, - "Capture work through the legacy profile.", - ) - - assert capture.outcome.failure is None - binding = require_shadow_primary_write_allowed(runtime_root, GOAL_ID) - assert binding is not None - assert "goal_ref" not in binding - - -def _files(directory: Path) -> dict[str, bytes]: - return {str(path.relative_to(directory)): path.read_bytes() for path in directory.rglob("*") if path.is_file()} - - -def test_outbox_fixture_reads_exact_bootstrap_receipt_after_lost_response(tmp_path: Path) -> None: - calls = [] - committed = {} - - def lose_response(method: str, request: dict) -> dict: - result = source_effect_runtime_result(method, request) - assert result["status"] in {"applied", "recovered", "replayed"}, result - calls.append(request) - path = shadow_management_state_path(Path(request["runtime_root"]), GOAL_ID) - committed["journal_bytes"] = path.read_bytes() - raise EffectRuntimeResponseAmbiguous(method, timeout=10) - - registry, state, runtime_root = _fixture(tmp_path, runtime_invoker=lose_response) - assert len(calls) == 1 # A receipt readback must never dispatch another write. - assert shadow_management_state_path(runtime_root, GOAL_ID).read_bytes() == committed["journal_bytes"] - _record_change(registry, state, runtime_root, "Continue after verified bootstrap readback.") - assert _drain(registry, runtime_root).reason_code is None - - -@pytest.mark.parametrize("damage", ["missing", "pending", "other_operation", "changed_request", "changed_manifest"]) -def test_outbox_fixture_rejects_unverified_bootstrap_after_lost_response(tmp_path: Path, damage: str) -> None: - calls = [] - - def lose_response(method: str, request: dict) -> dict: - calls.append(request) - if damage != "missing": - source_effect_runtime_result(method, request) - root = Path(request["runtime_root"]) - path = shadow_management_state_path(root, GOAL_ID) - journal = json.loads(path.read_text(encoding="utf-8")) - if damage == "pending": - journal.update(status="bootstrapping", binding=None, result=None) - journal["operation"]["phase"] = "prepared" - elif damage == "other_operation": - journal["operation"]["operation_id"] = "bootstrap:another-operation" - elif damage == "changed_request": - journal["operation"]["request_digest"] = "sha256:" + "0" * 64 - else: - manifest = next((shadow_management_directory(root, GOAL_ID) / "operations").glob("*/manifest.json")) - manifest.write_text("{}", encoding="utf-8") - path.write_text(json.dumps(journal), encoding="utf-8") - raise EffectRuntimeResponseAmbiguous(method, timeout=10) - - with pytest.raises(AssertionError): - _fixture(tmp_path, runtime_invoker=lose_response) - assert len(calls) == 1 - assert not _todo_dir(tmp_path / "runtime").exists() - - -def test_outbox_fixture_preserves_semantic_rejection_even_with_a_bootstrap_receipt(tmp_path: Path) -> None: - calls = [] - - def reject_response(method: str, request: dict) -> dict: - source_effect_runtime_result(method, request) - calls.append(request) - raise EffectRuntimeRejected("deliberate semantic rejection", diagnostic_code="invalid_request") - - with pytest.raises(AssertionError, match="deliberate semantic rejection"): - _fixture(tmp_path, runtime_invoker=reject_response) - assert len(calls) == 1 - - -def test_capture_records_prepared_then_committed_and_skips_prose_only_writes(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - original = state.read_text(encoding="utf-8") - todo_id, new_text = _planned_todo(original, "Bind the shadow to the primary transaction.") - - capture = _capture(registry, state, runtime_root, original_text=original) - capture.prepare(new_text) - names = sorted(path.name for path in _todo_dir(runtime_root).iterdir()) - assert len(names) == 1 - assert names[0].endswith(".prepared.json") - assert capture.outcome.entry_id is not None - assert capture.outcome.seq == 1 - assert capture.outcome.source_bytes_digest == text_digest(new_text) - assert capture.outcome.entry_id == outbox.entry_identity( - goal_id=GOAL_ID, partition="todos", seq=1, source_ref=text_digest(new_text), - capture_lineage_id=require_shadow_primary_write_allowed(runtime_root, GOAL_ID)["capture_lineage_id"], - source_root_digest=outbox.runtime_root_digest(runtime_root), - ) - - state.write_text(new_text) - capture.committed() - entries = outbox.list_entries(_todo_dir(runtime_root)) - assert [entry.is_committed for entry in entries] == [True] - entry = entries[0] - assert entry.prepared["schema_version"] == outbox.OUTBOX_ENTRY_SCHEMA - assert entry.committed is not None - assert entry.committed["schema_version"] == outbox.OUTBOX_COMMIT_SCHEMA - assert entry.prepared["source"] == { - "kind": "markdown_active_state", - "previous_bytes_digest": text_digest(original), - "previous_partition_digest": partition_digest({"handoff_mode": "hard_lease", "todos": []}), - "bytes_digest": text_digest(new_text), - "lease": None, - "event_id": None, - } - assert entry.prepared["writer"] == { - "runtime": "python", - "write_class": "todo_add", - "operation_id": None, - } - projection = entry.projection() - assert projection is not None - assert projection["handoff_mode"] == "hard_lease" - assert [item["todo_id"] for item in projection["todos"]] == [todo_id] - assert entry.recorded_partition_digest() == partition_digest(projection) - assert str(runtime_root) not in json.dumps(entry.prepared) - - prose_only = new_text + "\nOperator note that changes no coordination fact.\n" - prose = _capture(registry, state, runtime_root, original_text=new_text, write_class="todo_update") - prose.prepare(prose_only) - prose.committed() - assert prose.outcome.entry_id is None - assert prose.outcome.skipped_reason == "partition_unchanged" - assert len(outbox.list_entries(_todo_dir(runtime_root))) == 1 - - _, third_text = _planned_todo(new_text, "Second coordination fact.") - third = _capture(registry, state, runtime_root, original_text=new_text) - third.prepare(third_text) - state.write_text(third_text) - third.committed() - assert third.outcome.seq == 2 - assert [entry.seq for entry in outbox.list_entries(_todo_dir(runtime_root))] == [1, 2] - - -def test_capture_and_drain_use_the_active_binding_digest_through_a_root_alias( - tmp_path: Path, -) -> None: - target = tmp_path / "target" - target.mkdir() - alias = tmp_path / "alias" - try: - alias.symlink_to(target, target_is_directory=True) - except OSError as exc: - pytest.skip(f"directory symlinks unavailable: {exc}") - registry, state, runtime_root = _fixture(alias) - - capture = _record_change( - registry, state, runtime_root, "Capture through the runtime alias." - ) - binding = require_shadow_primary_write_allowed(runtime_root, GOAL_ID) - assert binding is not None - [entry] = outbox.list_entries(_todo_dir(runtime_root)) - assert entry.prepared["source_root_digest"] == binding["source_root_digest"] - - drained = _drain(registry, runtime_root) - assert drained.outcome == "drained" - assert drained.delivered == 1 - assert capture.outcome.entry_id == drained.entries[0]["entry_id"] - - -def test_disabled_capture_creates_nothing(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path, bootstrap=False) - capture = _capture(registry, state, runtime_root, original_text="", enabled=False) - capture.prepare("# anything") - capture.committed() - assert capture.outcome.skipped_reason == "shadow_disabled" - assert capture.outcome.failure is None - assert not (runtime_root / "authority-shadow").exists() - - - - -def test_cursor_allocation_hint_does_not_authorize_a_gap_or_candidate_write(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - directory = _todo_dir(runtime_root) - directory.mkdir(parents=True) - # Synthetic wire input: only the native drain may publish real cursors. - outbox.durable_write_json(outbox.cursor_path(directory), { - "schema_version": outbox.DRAIN_CURSOR_SCHEMA, "partition": "todos", "last_seq": 5, - "last_entry_id": "local-shadow-tx-" + "a" * 64, "last_partition_digest": None, - "last_cursor": "5", "last_provider_revision": "rev-5", "updated_at": "2026-09-05T00:00:00Z", - }) - assert outbox.next_seq(directory) == 6 - first = _record_change(registry, state, runtime_root, "Recorded after the cursor hint.") - second = _record_change(registry, state, runtime_root, "Another source transaction.") - assert (first.outcome.seq, second.outcome.seq) == (6, 7) - assert [entry.seq for entry in outbox.list_entries(directory)] == [6, 7] - summary = outbox.outbox_summary(runtime_root, GOAL_ID) - assert summary["todos"]["committed_pending"] == 2 - assert summary["todos"]["cursor_last_seq"] == 5 - assert summary["leases"] == { - "committed_pending": 0, "prepared_only": 0, "retired_residue": 0, "next_seq": 0, - "cursor_last_seq": None, "cursor_last_entry_id": None, "invalid": None, - } - before = _files(runtime_root / "authority-shadow") - result = _drain(registry, runtime_root) - assert result.reason_code == "outbox_cursor_unproved" - assert result.delivered == 0 - assert _files(runtime_root / "authority-shadow") == before - - -def test_canonical_projection_rejects_floats_and_bad_lease_identity() -> None: - with pytest.raises(ProjectionValueError): - canonical_bytes({"version": 1.0}) - with pytest.raises(ProjectionValueError): - todo_partition_projection(handoff_mode="hard_lease", todos=[{"todo_id": "a", "status": 2.5}]) - assert canonical_bytes({"b": 1, "a": [True, None, "\u00e9"]}) == '{"a":[true,null,"\u00e9"],"b":1}'.encode("utf-8") - assert sha256_digest({"b": [1, None], "a": "x"}) == sha256_digest({"a": "x", "b": [1, None]}) - with pytest.raises(ProjectionValueError): - compact_lease({"goal_id": "other", "todo_id": "todo-a"}, goal_id=GOAL_ID, file_stem="todo-a") - lease = compact_lease( - {"goal_id": GOAL_ID, "todo_id": "todo-b", "version": 1, "extra": "retained"}, - goal_id=GOAL_ID, file_stem="todo-b", - ) - assert lease["extra"] == "retained" - - -def test_capture_failure_is_typed_and_preserves_the_primary_result(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - original = state.read_text() - _, new_text = _planned_todo(original, "Primary survives an unavailable outbox directory.") - blocker = _todo_dir(runtime_root) - blocker.write_text("not a directory") - capture = _capture(registry, state, runtime_root, original_text=original) - capture.prepare(new_text) - state.write_text(new_text) - capture.committed() - assert capture.outcome.entry_id is None - assert capture.outcome.failure is not None - assert capture.outcome.failure["reason_code"] == "outbox_prepare_failed" - assert state.read_text() == new_text - - -def test_prepared_records_must_bind_their_directory_identity_and_source(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - original = state.read_text(encoding="utf-8") - _, new_text = _planned_todo(original, "Bound to this goal and partition.") - capture = _capture(registry, state, runtime_root, original_text=original) - capture.prepare(new_text) - state.write_text(new_text) - capture.committed() - directory = _todo_dir(runtime_root) - [entry] = outbox.list_entries(directory) - assert entry.prepared["goal_id"] == GOAL_ID - assert entry.prepared["partition"] == "todos" - assert entry.prepared["source_root_digest"] == outbox.runtime_root_digest(runtime_root) - assert outbox.record_source_ref(entry.prepared) == text_digest(state.read_text(encoding="utf-8")) - - def tampered(**changes: object) -> None: - record = dict(entry.prepared) - for key, value in changes.items(): - if isinstance(value, dict) and isinstance(record.get(key), dict): - record[key] = {**record[key], **value} - else: - record[key] = value - outbox.durable_write_json(entry.prepared_path, record) - with pytest.raises(outbox.OutboxError) as raised: - outbox.list_entries(directory) - assert raised.value.reason_code == "outbox_file_invalid" - - tampered(goal_id="goal-other") - tampered(partition="leases") - tampered(source={"bytes_digest": text_digest("some other bytes")}) - tampered(source_root_digest="not-a-digest") - tampered(writer={"runtime": "ruby"}) - tampered(source={"kind": "unknown_source"}) - outbox.durable_write_json(entry.prepared_path, entry.prepared) - assert len(outbox.list_entries(directory)) == 1 - - # A valid-looking root/lineage from another generation cannot reuse this ID. - tampered(capture_lineage_id="another-lineage") - tampered(source_root_digest="sha256:" + "1" * 64) - tampered(schema_version="loopx_local_authority_shadow_outbox_entry_v0") - outbox.durable_write_json(entry.prepared_path, entry.prepared) - assert entry.committed_path is not None and entry.committed is not None - outbox.durable_write_json(entry.committed_path, {**entry.committed, "capture_lineage_id": "another-lineage"}) - with pytest.raises(outbox.OutboxError) as invalid_marker: - outbox.list_entries(directory) - assert invalid_marker.value.reason_code == "outbox_file_invalid" - outbox.durable_write_json(entry.committed_path, entry.committed) - assert len(outbox.list_entries(directory)) == 1 - - -def test_cursor_residue_is_diagnostic_and_requires_an_exact_receipt(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - _record_change(registry, state, runtime_root, "First source transaction.") - _record_change(registry, state, runtime_root, "Second source transaction.") - directory = _todo_dir(runtime_root) - first, second = outbox.list_entries(directory) - assert outbox.retired_residue(directory) == [] - outbox.durable_write_json(outbox.cursor_path(directory), { - "schema_version": outbox.DRAIN_CURSOR_SCHEMA, "partition": "todos", "last_seq": first.seq, - "last_entry_id": first.entry_id, "last_partition_digest": first.recorded_partition_digest(), - "last_cursor": "2", "last_provider_revision": "file:2:" + "1" * 24, - "updated_at": "2026-09-05T00:00:00Z", - }) - assert [path.name for path in outbox.retired_residue(directory)] == sorted( - [first.committed_path.name, first.prepared_path.name] - ) - # Hiding entries below an unproved watermark would lose the only delivery evidence. - assert [entry.seq for entry in outbox.list_entries(directory)] == [first.seq, second.seq] - assert outbox.next_seq(directory) == 3 - before = _files(runtime_root / "authority-shadow") - result = _drain(registry, runtime_root) - assert result.reason_code == "outbox_cursor_unproved" - assert _files(runtime_root / "authority-shadow") == before - - -def test_exact_receipts_allow_cursor_then_residue_cleanup(tmp_path: Path) -> None: - registry, state, runtime_root = _fixture(tmp_path) - _record_change(registry, state, runtime_root, "An exact transaction to deliver.") - directory = _todo_dir(runtime_root) - original = _files(directory) - delivered = _drain(registry, runtime_root) - assert delivered.delivered == 1, delivered.reason_code - cursor = outbox.read_cursor(directory) - assert cursor is not None and cursor["last_seq"] == 1 - assert outbox.list_entries(directory) == [] - for name, raw in original.items(): - (directory / name).write_bytes(raw) - assert len(outbox.retired_residue(directory)) == 2 - recovered = _drain(registry, runtime_root) - assert recovered.reason_code is None, recovered.reason_code - assert outbox.list_entries(directory) == [] - assert outbox.read_cursor(directory) == cursor diff --git a/tests/control_plane/test_local_coordination_authority.py b/tests/control_plane/test_local_coordination_authority.py index d40b850ecc..2f419fd250 100644 --- a/tests/control_plane/test_local_coordination_authority.py +++ b/tests/control_plane/test_local_coordination_authority.py @@ -9,7 +9,7 @@ import pytest from canonical_authority_fixture import ( - initialize_canonical_authority, single_snapshot_page, + initialize_canonical_authority, isolate_sqlite_runtime, single_snapshot_page, promoted_create_fixture as _promoted_create_fixture, ) @@ -37,8 +37,6 @@ ) from loopx.control_plane.todos.active_state_editing import ( TODO_SECTION_HEADINGS, - section_bounds, - todo_blocks, ) from loopx.control_plane.todos.completion_validation_projection import ( completion_validation_declaration_sha256, @@ -1030,9 +1028,11 @@ def test_promoted_claim_protocol_failure_stays_infrastructure_outage( assert not isinstance(exc_info.value, LocalCoordinationAuthorityRejection) -def test_promoted_claim_folds_agent_id_whitespace_like_legacy(tmp_path: Path) -> None: +def test_canonical_claim_uses_public_agent_id_whitespace_normalization( + tmp_path: Path, +) -> None: """Every Python whitespace character (including U+0085 NEL, U+001C..U+001F, - tabs, NBSP) in claimed_by must fold to '-' identically before and after promotion. + tabs, NBSP) in claimed_by must fold to '-' through the public normalizer. """ from loopx.control_plane.todos.contract import normalize_todo_claimed_by @@ -1594,9 +1594,14 @@ def count_authority_runtime_call( assert "Continue provider-first delivery." in rendered -def test_public_terminal_optional_prose_matches_before_and_after_promotion( +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_public_terminal_optional_prose_matches_canonical_providers( tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + provider: str, ) -> None: + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) cases = [ ("note", None, None), ("note", "", None), @@ -1615,7 +1620,7 @@ def test_public_terminal_optional_prose_matches_before_and_after_promotion( ("reason", " first\u0085 second ", "first second"), ] - def exercise(root: Path, *, promoted: bool) -> dict[str, object]: + def exercise(root: Path) -> dict[str, object]: runtime_root = root / "runtime" project = root / "project" state_file = project / "ACTIVE_GOAL_STATE.md" @@ -1675,17 +1680,17 @@ def exercise(root: Path, *, promoted: bool) -> dict[str, object]: ), encoding="utf-8", ) - if promoted: - initialize_canonical_authority( - runtime_root, - "goal-a", - build_todo_runtime_shadow_projection( - goal_id="goal-a", - todos=records, - handoff_mode="soft_claim", - ), - state_path=state_file, - ) + initialize_canonical_authority( + runtime_root, + "goal-a", + build_todo_runtime_shadow_projection( + goal_id="goal-a", + todos=records, + handoff_mode="soft_claim", + ), + state_path=state_file, + provider=provider, + ) for index, (field, value, _expected) in enumerate(cases, start=1): common = { @@ -1708,44 +1713,36 @@ def exercise(root: Path, *, promoted: bool) -> dict[str, object]: ) assert result["completed"] is True - if promoted: - projection = read_canonical_todos_if_promoted( - runtime_root=runtime_root, - goal_id="goal-a", - ) - assert projection is not None - todos = projection["todos"] - else: - lines = state_file.read_text(encoding="utf-8").splitlines() - bounds = section_bounds(lines, "agent") - assert bounds is not None - todos = todo_blocks( - lines, - bounds[0], - bounds[1], - role="agent", - source_section=bounds[2], - ) + projection = read_canonical_todos_if_promoted( + runtime_root=runtime_root, + goal_id="goal-a", + ) + assert projection is not None + todos = projection["todos"] by_id = {todo["todo_id"]: todo for todo in todos} return { f"{field}:{index}": by_id[f"todo_prose_{index:02d}"].get(field) for index, (field, _value, _expected) in enumerate(cases, start=1) } - legacy = exercise(tmp_path / "legacy", promoted=False) - canonical = exercise(tmp_path / "canonical", promoted=True) + canonical = exercise(tmp_path / provider) expected = { f"{field}:{index}": value for index, (field, _input, value) in enumerate(cases, start=1) } - assert legacy == expected assert canonical == expected -def test_illegal_terminal_actor_is_a_domain_valueerror_before_and_after_promotion( +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_illegal_terminal_actor_is_rejected_by_canonical_provider( tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + provider: str, ) -> None: - def rejected(root: Path, *, promoted: bool) -> ValueError: + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + + def rejected(root: Path) -> ValueError: runtime_root = root / "runtime" project = root / "project" state_file = project / "ACTIVE_GOAL_STATE.md" @@ -1783,31 +1780,31 @@ def rejected(root: Path, *, promoted: bool) -> ValueError: ), encoding="utf-8", ) - if promoted: - initialize_canonical_authority( - runtime_root, - "goal-a", - build_todo_runtime_shadow_projection( - goal_id="goal-a", - handoff_mode="soft_claim", - todos=[ - { - "schema_version": "todo_item_v0", - "index": 1, - "done": False, - "text": "Reject an illegal terminal actor", - "todo_id": "todo_terminal_actor", - "role": "agent", - "status": "open", - "archive_state": "active", - "source_section": TODO_SECTION_HEADINGS["agent"], - "task_class": "advancement_task", - "claimed_by": "agent-a", - } - ], - ), - state_path=state_file, - ) + initialize_canonical_authority( + runtime_root, + "goal-a", + build_todo_runtime_shadow_projection( + goal_id="goal-a", + handoff_mode="soft_claim", + todos=[ + { + "schema_version": "todo_item_v0", + "index": 1, + "done": False, + "text": "Reject an illegal terminal actor", + "todo_id": "todo_terminal_actor", + "role": "agent", + "status": "open", + "archive_state": "active", + "source_section": TODO_SECTION_HEADINGS["agent"], + "task_class": "advancement_task", + "claimed_by": "agent-a", + } + ], + ), + state_path=state_file, + provider=provider, + ) with pytest.raises(ValueError) as exc_info: complete_goal_todo( registry_path=registry_path, @@ -1821,9 +1818,7 @@ def rejected(root: Path, *, promoted: bool) -> ValueError: ) return exc_info.value - legacy = rejected(tmp_path / "legacy", promoted=False) - canonical = rejected(tmp_path / "canonical", promoted=True) - assert not isinstance(legacy, LocalCoordinationAuthorityUnavailable) + canonical = rejected(tmp_path / provider) assert isinstance(canonical, LocalCoordinationAuthorityRejection) assert canonical.code == "actor_not_registered" diff --git a/tests/control_plane/test_migrated_terminal_closeout_cli.py b/tests/control_plane/test_migrated_terminal_closeout_cli.py deleted file mode 100644 index 30701ae252..0000000000 --- a/tests/control_plane/test_migrated_terminal_closeout_cli.py +++ /dev/null @@ -1,55 +0,0 @@ -"""A legacy completion stays settled across real reviewed provider promotion.""" - -import json -import sys - -import pytest - -from tests.control_plane.test_reviewed_promotion_cli import prepare - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_completed_legacy_todo_closes_after_promotion_without_revalidation(tmp_path, provider): - ws, _, _ = prepare(tmp_path, provider, source_mode="legacy") - marker = tmp_path / "validation-count" - validator = tmp_path / "validate.py" - validator.write_text(f"from pathlib import Path\np=Path({str(marker)!r})\np.write_text(p.read_text()+'run\\n' if p.exists() else 'run\\n')\n") - todo_id = ws.cli("todo", "add", "--role", "agent", "--text", "Preserve the completed migration work", - "--validation-command-json", json.dumps([sys.executable, str(validator)]), - "--validation-timeout-seconds", "5")["todo_id"] - completed = ws.cli("todo", "complete", "--todo-id", todo_id, "--agent-id", "agent-a", - "--evidence", "Original check passed") - assert completed["ok"], completed - assert marker.read_text() == "run\n" - before = ws.cli("todo", "list", "--todo-id", todo_id)["todo"] - assert before["completion_continuation"] == "active_goal" - key = before["completion_turn_key"] - assert ws.drain(budget_seconds="60")["ok"] - preview = ws.cli("coordination-shadow", "promote") - saved = tmp_path / "completed-review.json" - saved.write_text(json.dumps(preview), encoding="utf-8") - promoted = ws.cli("coordination-shadow", "promote", "--reviewed-plan", str(saved), "--execute") - assert promoted["ok"], promoted - # This intentionally minimal migration fixture is not a healthy runnable - # Goal. Its guard still exposes the canonical lifecycle diagnostic. - guard_args = ("quota", "should-run", "--codex-app", "--agent-id", "agent-a", - "--turn-instance-id", "migration-readback") - guard_before = ws.cli(*guard_args, success=False) - assert guard_before["agent_todo_summary"]["todo_succession_warning"]["count"] == 1 - validator.unlink() # A metadata closeout must not need the old validator. - closeout_args = ("todo", "complete", "--todo-id", todo_id, "--agent-id", "agent-a", - "--completion-identity-key", key, "--evidence", "Original check passed", "--no-follow-up") - closed = ws.cli(*closeout_args) - assert closed["ok"] and closed["changed"], closed - after = ws.cli("todo", "list", "--todo-id", todo_id)["todo"] - assert after["no_followup"] is True - assert after["completion_continuation"] == "no_followup" - assert after["completion_recovery"] == "lifecycle_reentry_terminal_closeout" - for field in ("completed_at", "completion_turn_key", "completion_receipt_id", "evidence"): - assert after[field] == before[field] - replay = ws.cli(*closeout_args) - assert replay["ok"] and replay["idempotent_replay"], replay - assert ws.cli("todo", "list", "--todo-id", todo_id)["todo"] == after - assert marker.read_text() == "run\n" - guard_after = ws.cli(*guard_args, success=False) - assert "todo_succession_warning" not in guard_after["agent_todo_summary"] diff --git a/tests/control_plane/test_monitor_followthrough_contract.py b/tests/control_plane/test_monitor_followthrough_contract.py index 7a3ee6e811..80ea23edd7 100644 --- a/tests/control_plane/test_monitor_followthrough_contract.py +++ b/tests/control_plane/test_monitor_followthrough_contract.py @@ -5,6 +5,13 @@ import pytest +from canonical_authority_fixture import initialize_canonical_authority +from loopx.control_plane.coordination.local_authority import ( + CanonicalAuthorityRequired, +) +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.control_plane.scheduler.monitor_poll_writeback import ( resolve_monitor_todo_item, write_monitor_poll_todo_state, @@ -35,7 +42,13 @@ AGENT_ID = "codex-quality-qualification" -def _write_fixture(tmp_path: Path) -> tuple[Path, Path, Path]: +def _write_fixture( + tmp_path: Path, + *, + provider: str = "file", + canonical: bool = True, + handoff_mode: str = "soft_claim", +) -> tuple[Path, Path, Path]: project = tmp_path / "project" runtime = tmp_path / "runtime" state = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -55,6 +68,18 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path, Path]: registered_agents=[AGENT_ID, "codex-main-control"], quota_allowed_slots=None, ) + if canonical: + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[], + handoff_mode=handoff_mode, + ), + state_path=state, + provider=provider, + ) return registry, runtime, state @@ -129,10 +154,15 @@ def test_todo_update_preserves_omitted_monitor_schedule_fields(tmp_path: Path) - runtime_root=runtime, ) - assert updated["target_key"] == "public-release:42" - assert updated["cadence"] == "1h" - assert updated["next_due_at"] == "2099-01-01T00:00:00+00:00" - assert updated["watch_only"] == "true" + persisted = resolve_monitor_todo_item( + registry_path=registry, + goal_id=GOAL_ID, + todo_id=updated["todo_id"], + ) + assert persisted["target_key"] == "public-release:42" + assert persisted["cadence"] == "1h" + assert persisted["next_due_at"] == "2099-01-01T00:00:00+00:00" + assert persisted["watch_only"] == "true" def test_monitor_resume_when_is_a_supported_bounded_policy( @@ -150,14 +180,13 @@ def test_monitor_resume_when_is_a_supported_bounded_policy( resume_when="pr_merged:owner/repo#42", monitor_metadata={"target_key": "public-pr:42", "cadence": "1h"}, ) - assert bounded["resume_when"] == "pr_merged:owner/repo#42" - assert bounded["next_due_at"] persisted = resolve_monitor_todo_item( registry_path=registry, goal_id=GOAL_ID, todo_id=bounded["todo_id"], ) - assert persisted["next_due_at"] == bounded["next_due_at"] + assert persisted["resume_when"] == "pr_merged:owner/repo#42" + assert persisted["next_due_at"] monitor = _add_monitor( registry, @@ -171,7 +200,12 @@ def test_monitor_resume_when_is_a_supported_bounded_policy( agent_id=AGENT_ID, resume_when="pr_merged:owner/repo#42", ) - assert updated["resume_when"] == "pr_merged:owner/repo#42" + persisted = resolve_monitor_todo_item( + registry_path=registry, + goal_id=GOAL_ID, + todo_id=updated["todo_id"], + ) + assert persisted["resume_when"] == "pr_merged:owner/repo#42" assert monitor_todo_is_actionable_open( { @@ -212,19 +246,20 @@ def test_todo_add_cli_materializes_cadence_only_monitor_due_time( runtime_root=runtime, ) - assert added["cadence"] == "30m" - assert added["next_due_at"] persisted = resolve_monitor_todo_item( registry_path=registry, goal_id=GOAL_ID, todo_id=added["todo_id"], ) - assert persisted["next_due_at"] == added["next_due_at"] + assert persisted["cadence"] == "30m" + assert persisted["next_due_at"] assert persisted["target_key"] == "public-dependency:cadence-only" -def test_runtime_writeback_can_read_legacy_unbounded_monitor(tmp_path: Path) -> None: - registry, _runtime, state = _write_fixture(tmp_path) +def test_runtime_writeback_rejects_unpromoted_legacy_monitor( + tmp_path: Path, +) -> None: + registry, runtime, state = _write_fixture(tmp_path, canonical=False) state.write_text( state.read_text(encoding="utf-8") + "\n- [ ] [P2] Legacy public monitor.\n" @@ -233,20 +268,21 @@ def test_runtime_writeback_can_read_legacy_unbounded_monitor(tmp_path: Path) -> + "target_key=legacy-public cadence=1h -->\n", encoding="utf-8", ) + before = state.read_bytes() - result = write_monitor_poll_todo_state( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - generated_at="2026-08-11T00:00:00Z", - execute=False, - todo_id="todo_legacy_monitor", - result_hash="unchanged", - agent_id=AGENT_ID, - ) + with pytest.raises(CanonicalAuthorityRequired): + write_monitor_poll_todo_state( + registry_path=registry, + runtime_root=runtime, + goal_id=GOAL_ID, + generated_at="2026-08-11T00:00:00Z", + execute=False, + todo_id="todo_legacy_monitor", + result_hash="unchanged", + agent_id=AGENT_ID, + ) - assert result is not None - assert result["todo_update"]["changed"] is True + assert state.read_bytes() == before def test_material_poll_reloads_status_and_projects_declared_successor( @@ -1017,8 +1053,8 @@ def test_material_poll_user_action_does_not_block_existing_advancement( reminder = result["todo_writeback"]["next_todos"][0] assert reminder["task_class"] == "user_action" assert reminder["bound_agent"] == AGENT_ID - assert reminder["blocks_agent"] is None - assert reminder["unblocks_todo_id"] is None + assert reminder.get("blocks_agent") is None + assert reminder.get("unblocks_todo_id") is None assert result["after"]["selected_todo"]["todo_id"] == advancement["todo_id"] diff --git a/tests/control_plane/test_monitor_observation_admission.py b/tests/control_plane/test_monitor_observation_admission.py index 79648c207d..9654170bd7 100644 --- a/tests/control_plane/test_monitor_observation_admission.py +++ b/tests/control_plane/test_monitor_observation_admission.py @@ -13,6 +13,7 @@ TODO_ID, _append_newly_due_monitor, _classification_count, + _initialize_fixture_authority, _projected_cli_args, _run_cli, _spend_run_count, @@ -44,6 +45,7 @@ def test_settled_advancement_hold_preserves_independent_monitor( rc, first = _run_cli(registry, runtime, *guard) assert rc == 0, first _append_newly_due_monitor(project, watch_only=True) + _initialize_fixture_authority(project, runtime) rc, admitted = _run_cli(registry, runtime, *guard) assert rc == 0, admitted poll_command = admitted["interaction_contract"]["cli_channel"][ @@ -114,6 +116,7 @@ def test_receipt_bound_advancement_allows_one_auxiliary_due_monitor_receipt( assert first["heartbeat_receipt"]["settlement_identity"]["todo_id"] == TODO_ID _append_newly_due_monitor(project, watch_only=True) + _initialize_fixture_authority(project, runtime) capability_args = ( "--available-capability", "network", @@ -203,11 +206,9 @@ def test_receipt_bound_advancement_allows_one_auxiliary_due_monitor_receipt( @pytest.mark.parametrize( ("provider", "writeback", "spend_first"), [ - ("legacy", False, False), - ("legacy", True, False), + ("file", False, False), ("file", True, False), ("sqlite", True, False), - ("legacy", True, True), ("file", True, True), ("sqlite", True, True), ], @@ -220,13 +221,7 @@ def test_completed_advancement_retains_auxiliary_monitor_admission( spend_first: bool, ) -> None: tmp_path = settlement_budget_root - from canonical_authority_fixture import ( - initialize_canonical_authority, - isolate_sqlite_runtime, - ) - from loopx.control_plane.coordination.runtime_shadow import ( - build_todo_runtime_shadow_projection, - ) + from canonical_authority_fixture import isolate_sqlite_runtime from loopx.control_plane.coordination.local_authority import ( read_canonical_todos_if_promoted, ) @@ -261,6 +256,32 @@ def test_completed_advancement_retains_auxiliary_monitor_admission( rc, first = _run_cli(registry_path, runtime, *guard) assert rc == 0, first _append_newly_due_monitor(project, watch_only=True) + leases = [ + { + "schema_version": "task_lease_v0", + "goal_id": GOAL_ID, + "todo_id": todo_id, + "owner": AGENT_ID, + "write_scopes": [], + "status": "active", + "idempotency_key": idempotency_key, + "version": 3, + "lease_epoch": 2, + "acquired_at": "2026-01-01T00:00:00Z", + "expires_at": "2099-01-01T00:00:00Z", + } + for todo_id, idempotency_key in ( + (TODO_ID, "primary-execution"), + (DUE_MONITOR_TODO_ID, "own-monitor-execution"), + ) + ] + _initialize_fixture_authority( + project, + runtime, + provider=provider, + handoff_mode="hard_lease", + leases=leases, + ) rc, open_guard = _run_cli(registry_path, runtime, *guard) assert rc == 0, open_guard auxiliary = open_guard["interaction_contract"]["cli_channel"][ @@ -288,41 +309,18 @@ def test_completed_advancement_retains_auxiliary_monitor_admission( "--evidence", "read-only fixture delivery validated", "--no-follow-up", + "--task-lease-idempotency-key", + "primary-execution", + "--task-lease-expected-version", + "3", ) assert rc == 0, complete - if provider != "legacy": - rc, listed = _run_cli( - registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID - ) - assert rc == 0, listed - primary = next(todo for todo in listed["todos"] if todo["todo_id"] == TODO_ID) - assert primary["status"] == "done" - lease = { - "schema_version": "task_lease_v0", - "goal_id": GOAL_ID, - "todo_id": DUE_MONITOR_TODO_ID, - "owner": AGENT_ID, - "write_scopes": [], - "status": "active", - "idempotency_key": "own-monitor-execution", - "version": 3, - "lease_epoch": 2, - "acquired_at": "2026-01-01T00:00:00Z", - "expires_at": "2099-01-01T00:00:00Z", - } - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, - todos=listed["todos"], - handoff_mode="hard_lease", - leases=[lease], - ) - initialize_canonical_authority( - runtime, - GOAL_ID, - projection, - state_path=project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md", - provider=provider, - ) + rc, listed = _run_cli( + registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID + ) + assert rc == 0, listed + primary = next(todo for todo in listed["todos"] if todo["todo_id"] == TODO_ID) + assert primary["status"] == "done" if writeback: rc, refresh = _run_cli( registry_path, @@ -358,21 +356,20 @@ def test_completed_advancement_retains_auxiliary_monitor_admission( assert poll["settlement_todo_id"] == TODO_ID assert poll["turn_continuation"]["current_turn_settled"] is spend_first assert poll["turn_continuation"]["next_turn_required"] is spend_first - if provider != "legacy": - canonical = read_canonical_todos_if_promoted( - runtime_root=runtime, goal_id=GOAL_ID, include_leases=True - ) - assert canonical["source_authority"] == f"{provider}_v0" - observed = next( - todo - for todo in canonical["todos"] - if todo["todo_id"] == DUE_MONITOR_TODO_ID - ) - assert observed["result_hash"] == "completed-primary-observation" - assert poll["todo_writeback"]["lease_proof"] == { - "idempotency_key": "own-monitor-execution", - "expected_version": 3, - } + canonical = read_canonical_todos_if_promoted( + runtime_root=runtime, goal_id=GOAL_ID, include_leases=True + ) + assert canonical["source_authority"] == f"{provider}_v0" + observed = next( + todo + for todo in canonical["todos"] + if todo["todo_id"] == DUE_MONITOR_TODO_ID + ) + assert observed["result_hash"] == "completed-primary-observation" + assert poll["todo_writeback"]["lease_proof"] == { + "idempotency_key": "own-monitor-execution", + "expected_version": 3, + } resume = poll["settlement_resume"] assert resume["identity"]["todo_id"] == TODO_ID assert resume["identity"]["turn_instance_id"] == turn @@ -402,24 +399,23 @@ def test_completed_advancement_retains_auxiliary_monitor_admission( assert replay["replayed"] is True assert _classification_count(runtime, "quota_monitor_poll") == 1 assert _spend_run_count(runtime) == int(spend_first) - if provider != "legacy": - rc, released = _run_cli( - registry_path, - runtime, - "task-lease", - "release", - "--goal-id", - GOAL_ID, - "--todo-id", - DUE_MONITOR_TODO_ID, - "--owner", - AGENT_ID, - "--idempotency-key", - "own-monitor-execution", - "--expected-version", - "3", - ) - assert rc == 0, released + rc, released = _run_cli( + registry_path, + runtime, + "task-lease", + "release", + "--goal-id", + GOAL_ID, + "--todo-id", + DUE_MONITOR_TODO_ID, + "--owner", + AGENT_ID, + "--idempotency-key", + "own-monitor-execution", + "--expected-version", + "3", + ) + assert rc == 0, released if spend_first: assert resume["next_step"] is None assert "settlement_owed" not in poll diff --git a/tests/control_plane/test_monitor_poll_transaction_recovery.py b/tests/control_plane/test_monitor_poll_transaction_recovery.py index 802eca1ac5..299a74a342 100644 --- a/tests/control_plane/test_monitor_poll_transaction_recovery.py +++ b/tests/control_plane/test_monitor_poll_transaction_recovery.py @@ -1,626 +1,257 @@ -"""Legacy Monitor poll recovery at the public CLI boundary.""" +"""Historical Markdown Monitor receipt replay after canonical cutover.""" from __future__ import annotations -import json import base64 -import os -import re -import subprocess -import sys +import hashlib +import json from pathlib import Path import pytest -from loopx.control_plane.testing.canary_harness import ( - read_run_index, - write_fixture_registry, -) -from loopx.control_plane.coordination.legacy_writer_fence import ( - legacy_coordination_writer_fence_path, -) +from canonical_authority_fixture import initialize_canonical_authority from loopx.control_plane.coordination.local_authority import ( + CanonicalAuthorityRequired, read_canonical_todos_if_promoted, ) +from loopx.control_plane.coordination.local_authority_shadow_projection import ( + canonical_bytes, +) from loopx.control_plane.coordination.runtime_shadow import ( build_todo_runtime_shadow_projection, ) -from loopx.control_plane.coordination.local_authority_shadow_adapter import ( - read_local_authority_shadow, -) -from loopx.control_plane.effect_runtime import EffectRuntimeRejected, effect_runtime_result -from loopx.todos import add_goal_todo, list_goal_todos - -from canonical_authority_fixture import ( - initialize_canonical_authority, - isolate_sqlite_runtime, +from loopx.control_plane.scheduler.monitor_poll_writeback import ( + write_monitor_poll_todo_state, ) +from loopx.control_plane.testing.canary_harness import write_fixture_registry GOAL = "synthetic-monitor-recovery" AGENT = "codex-quality-qualification" -MONITOR_TARGET = "synthetic-target" +OPERATION = "historical-monitor-effect" +MONITOR_ID = "todo_historical_monitor" +AGENT_SUCCESSOR = "todo_historical_agent" +USER_SUCCESSOR = "todo_historical_user" AGENT_TASK = "Validate observed synthetic head" USER_TASK = "Review observed synthetic head" -REPO = Path(__file__).resolve().parents[2] - -# A fresh CLI process dies at the one state-file install boundary. The process -# exits without Python cleanup, as a real interrupted writer would. -_FAULT_WRAPPER = """ -import os, runpy, sys -original_replace = os.replace -def interrupted_replace(source, target, *args, **kwargs): - if str(target).endswith('/ACTIVE_GOAL_STATE.md'): - if os.environ['MONITOR_FAULT_PHASE'] == 'before': - os._exit(83) - original_replace(source, target, *args, **kwargs) - os._exit(83) - return original_replace(source, target, *args, **kwargs) -os.replace = interrupted_replace -sys.argv = ['loopx.cli', *sys.argv[1:]] -runpy.run_module('loopx.cli', run_name='__main__') -""" -_QUOTA_COMMIT_FAULT_WRAPPER = """ -import os, runpy, sys -import loopx.control_plane.quota.monitor_poll as monitor -original = monitor._native_result -def interrupted_commit(request): - if request.get('phase') == 'commit' and request.get('provider_receipt'): - os._exit(73) - return original(request) -monitor._native_result = interrupted_commit -sys.argv = ['loopx.cli', *sys.argv[1:]] -runpy.run_module('loopx.cli', run_name='__main__') -""" - - -def _cli( - registry: Path, runtime: Path, *args: str, fault: str | None = None, -) -> tuple[int, dict | None]: - if fault == "quota_commit": - launcher = [sys.executable, "-c", _QUOTA_COMMIT_FAULT_WRAPPER] - elif fault: - launcher = [sys.executable, "-c", _FAULT_WRAPPER] - else: - launcher = [sys.executable, "-m", "loopx.cli"] - result = subprocess.run( - [*launcher, "--registry", str(registry), "--format", "json", - "--runtime-root", str(runtime), *args], - cwd=REPO, - env={**os.environ, "LOOPX_USAGE_PING": "0", - "MONITOR_FAULT_PHASE": fault or ""}, - capture_output=True, text=True, check=False, timeout=60, - ) - return result.returncode, json.loads(result.stdout) if result.stdout.strip() else None - - -def _ok(registry: Path, runtime: Path, *args: str) -> dict: - code, result = _cli(registry, runtime, *args) - assert code == 0, result - assert isinstance(result, dict) - return result -def _fixture(tmp_path: Path) -> tuple[Path, Path, Path, str, tuple[str, ...]]: - project, runtime = tmp_path / "project", tmp_path / "runtime" +def _fixture(tmp_path: Path) -> tuple[Path, Path, Path]: + project = tmp_path / "project" + runtime = tmp_path / "runtime" state = project / ".codex" / "goals" / GOAL / "ACTIVE_GOAL_STATE.md" registry = project / ".loopx" / "registry.json" state.parent.mkdir(parents=True) - state.write_text("---\nstatus: active\n---\n\n# Active Goal State\n\n## Agent Todo\n", - encoding="utf-8") + state.write_text( + "---\nstatus: active\n---\n\n# Active Goal State\n\n## Agent Todo\n", + encoding="utf-8", + ) write_fixture_registry( - project=project, runtime_root=runtime, registry_path=registry, - goal_id=GOAL, domain="loopx-platform", - adapter_kind="harness_self_improvement", registered_agents=[AGENT], + project=project, + runtime_root=runtime, + registry_path=registry, + goal_id=GOAL, + domain="loopx-platform", + adapter_kind="harness_self_improvement", + registered_agents=[AGENT], quota_allowed_slots=None, ) - monitor = add_goal_todo( - registry_path=registry, goal_id=GOAL, role="agent", - text="Observe public synthetic target", task_class="continuous_monitor", - claimed_by=AGENT, - monitor_metadata={"target_key": MONITOR_TARGET, "cadence": "1h", - "next_due_at": "2000-01-01T00:00:00+00:00", - "watch_only": "true"}, - ) - turn = "synthetic-monitor-turn-1" - _ok(registry, runtime, "quota", "should-run", "--goal-id", GOAL, - "--agent-id", AGENT, "--runtime-profile", "generic_cli", - "--turn-instance-id", turn) - poll = ( - "quota", "monitor-poll", "--goal-id", GOAL, "--agent-id", AGENT, - "--runtime-profile", "generic_cli", "--turn-instance-id", turn, - "--todo-id", monitor["todo_id"], "--target-key", MONITOR_TARGET, - "--result-hash", "synthetic-head-1", "--material-change", - "--next-agent-todo", AGENT_TASK, "--next-action-kind", "validate", - "--next-user-todo", USER_TASK, "--next-user-task-class", "user_action", - "--execute", - ) - return registry, runtime, state, monitor["todo_id"], poll + return registry, runtime, state + + +def _request() -> tuple[dict, dict]: + observation = { + "todo_id": MONITOR_ID, + "target_key": "synthetic-target", + "result_hash": "synthetic-head-1", + "material_change": True, + "generated_at": "2026-09-01T00:00:00Z", + "cadence": None, + "next_due_at": None, + "reason_summary": None, + } + intent = { + "next_agent_todo": AGENT_TASK, + "next_action_kind": "validate", + "next_task_repository": None, + "next_required_capabilities": [], + "next_continuation_policy": None, + "next_target_key": None, + "next_claimed_by": None, + "next_user_todo": USER_TASK, + "next_user_task_class": "user_action", + } + return observation, intent + + +def _historical_receipt() -> dict: + observation, intent = _request() + request_sha256 = hashlib.sha256( + canonical_bytes( + { + "goal_id": GOAL, + "observation": observation, + "intent": intent, + "actor_agent_id": AGENT, + "dry_run": False, + } + ) + ).hexdigest() + next_todos = [ + {"todo_id": AGENT_SUCCESSOR, "role": "agent", "todo": AGENT_TASK}, + {"todo_id": USER_SUCCESSOR, "role": "user", "todo": USER_TASK}, + ] + successor_receipts = [ + {"todo_id": AGENT_SUCCESSOR, "role": "agent"}, + {"todo_id": USER_SUCCESSOR, "role": "user"}, + ] + return { + "schema_version": "loopx_monitor_batch_receipt_v0", + "goal_id": GOAL, + "operation_id": OPERATION, + "request_sha256": request_sha256, + "writeback": { + "schema_version": "monitor_poll_todo_writeback_v0", + "dry_run": False, + "goal_id": GOAL, + "todo_id": MONITOR_ID, + "monitor_effect_id": OPERATION, + "result_hash": observation["result_hash"], + "material_change": True, + "last_checked_at": observation["generated_at"], + "todo_update": {"ok": True}, + "next_todos": next_todos, + "successor_receipts": successor_receipts, + "provider_replayed": False, + }, + } + +def _seed_receipt(state: Path, receipt: object) -> None: + encoded = base64.b64encode( + json.dumps(receipt, separators=(",", ":")).encode("utf-8") + ).decode("ascii") + digest = hashlib.sha256(OPERATION.encode("utf-8")).hexdigest() + with state.open("a", encoding="utf-8") as handle: + handle.write(f"\n\n") -def _successors(registry: Path, runtime: Path) -> dict[str, dict]: - todos = _ok(registry, runtime, "todo", "list", "--goal-id", GOAL)["todos"] - return {todo["text"]: todo for todo in todos - if todo["text"] in (AGENT_TASK, USER_TASK)} +def _promote(runtime: Path, state: Path) -> None: + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL, + todos=[], + handoff_mode="soft_claim", + ) + initialize_canonical_authority( + runtime, + GOAL, + projection, + state_path=state, + ) -def _poll_events(runtime: Path) -> list[dict]: - return [row for row in read_run_index(runtime, GOAL) - if row.get("classification") == "quota_monitor_poll"] +def _replay(registry: Path, runtime: Path, **overrides: object) -> dict: + observation, intent = _request() + arguments = { + "registry_path": registry, + "runtime_root": runtime, + "goal_id": GOAL, + "generated_at": observation["generated_at"], + "execute": True, + "monitor_effect_id": OPERATION, + "todo_id": observation["todo_id"], + "target_key": observation["target_key"], + "result_hash": observation["result_hash"], + "material_change": True, + "cadence": None, + "next_due_at": None, + "reason_summary": None, + "next_agent_todo": intent["next_agent_todo"], + "next_action_kind": intent["next_action_kind"], + "next_task_repository": None, + "next_required_capabilities": [], + "next_continuation_policy": None, + "next_target_key": None, + "next_claimed_by": None, + "next_user_todo": intent["next_user_todo"], + "next_user_task_class": intent["next_user_task_class"], + "agent_id": AGENT, + } + arguments.update(overrides) + result = write_monitor_poll_todo_state(**arguments) + assert isinstance(result, dict) + return result -def _quota_receipt(runtime: Path) -> Path: - directory = (runtime / "goals" / GOAL / "runs" / ".transactions" / - "quota-monitor-poll") - (receipt,) = directory.glob("*.json") - return receipt +def test_historical_receipt_replays_without_mutating_canonical_authority( + tmp_path: Path, +) -> None: + registry, runtime, state = _fixture(tmp_path) + _seed_receipt(state, _historical_receipt()) + _promote(runtime, state) + state_before = state.read_bytes() + provider_before = read_canonical_todos_if_promoted( + runtime_root=runtime, + goal_id=GOAL, + ) -def _business_receipt(state: Path) -> dict: - match = re.search(r"", - state.read_text(encoding="utf-8")) - assert match is not None - return json.loads(base64.b64decode(match[1])) + replayed = _replay(registry, runtime) + + assert replayed["provider_replayed"] is True + assert [item["todo_id"] for item in replayed["next_todos"]] == [ + AGENT_SUCCESSOR, + USER_SUCCESSOR, + ] + assert state.read_bytes() == state_before + assert read_canonical_todos_if_promoted( + runtime_root=runtime, + goal_id=GOAL, + ) == provider_before + + +def test_historical_receipt_rejects_changed_intent(tmp_path: Path) -> None: + registry, runtime, state = _fixture(tmp_path) + _seed_receipt(state, _historical_receipt()) + _promote(runtime, state) + state_before = state.read_bytes() + provider_before = read_canonical_todos_if_promoted( + runtime_root=runtime, + goal_id=GOAL, + ) + with pytest.raises(ValueError, match="request mismatch"): + _replay(registry, runtime, next_user_todo="Request a different review") -def _engage_fence(runtime: Path) -> None: - fence = legacy_coordination_writer_fence_path(runtime_root=runtime, goal_id=GOAL) - fence.parent.mkdir(parents=True, exist_ok=True) - fence.write_text(json.dumps({ - "schema_version": "loopx_legacy_coordination_writer_fence_v0", - "state": "engaged", "goal_id": GOAL, "fence_id": "synthetic-fence", - "source_version": "synthetic-source", - "source_projection_sha256": "a" * 64, - "expected_shadow_provider_revision": "file:1:aaaaaaaaaaaaaaaaaaaaaaaa", - }), encoding="utf-8") + assert state.read_bytes() == state_before + assert read_canonical_todos_if_promoted( + runtime_root=runtime, + goal_id=GOAL, + ) == provider_before -@pytest.mark.parametrize("owner", [AGENT, "registered-builder", "unknown-builder"]) -def test_public_monitor_successor_claim_checks_registered_peer_before_writing( - tmp_path: Path, owner: str, +def test_malformed_historical_receipt_is_not_treated_as_absent( + tmp_path: Path, ) -> None: - registry, runtime, state, _, poll = _fixture(tmp_path) - data = json.loads(registry.read_text(encoding="utf-8")) - data["goals"][0]["coordination"]["registered_agents"].append("registered-builder") - registry.write_text(json.dumps(data), encoding="utf-8") - before_state = state.read_bytes() - before_index = read_run_index(runtime, GOAL) - transaction_dir = runtime / "goals" / GOAL / "runs" / ".transactions" / "quota-monitor-poll" - before_receipts = {path.name: path.read_bytes() for path in transaction_dir.glob("*.json")} - code, response = _cli(registry, runtime, *poll[:-1], "--next-claimed-by", owner, "--execute") - assert response is not None - if owner == "unknown-builder": - assert code != 0 - assert "is not registered" in response["reason"] - assert "quota_unexpected_collection_error" not in json.dumps(response) - assert state.read_bytes() == before_state - assert read_run_index(runtime, GOAL) == before_index - assert {path.name: path.read_bytes() for path in transaction_dir.glob("*.json")} == before_receipts - assert _poll_events(runtime) == [] - else: - assert code == 0, json.dumps(response, indent=2) - successor = response["todo_writeback"]["next_todos"][0] - assert successor["claimed_by"] == owner - assert _successors(registry, runtime)[AGENT_TASK]["claimed_by"] == owner - assert len(_poll_events(runtime)) == 1 - if owner == "registered-builder": - saved_state = state.read_bytes() - saved_events = read_run_index(runtime, GOAL) - data["goals"][0]["coordination"]["registered_agents"].remove(owner) - registry.write_text(json.dumps(data), encoding="utf-8") - replay_code, replay = _cli(registry, runtime, *poll[:-1], "--next-claimed-by", owner, "--execute") - assert replay_code == 0, replay - assert state.read_bytes() == saved_state - assert read_run_index(runtime, GOAL) == saved_events - - -def test_monitor_batch_handler_classifies_expected_input_errors() -> None: - request = { - "schema_version": "loopx_monitor_batch_plan_request_v0", "legacy_batch_version": 1, - "goal_id": "synthetic-monitor", "operation_id": "synthetic-effect", - "actor_agent_id": "observer", "registered_agents": ["observer"], - "dry_run": False, "observation": { - "todo_id": "todo_watch", "target_key": "watch", "generated_at": "2026-09-01T00:00:00Z", - "result_hash": "head", "material_change": True, - }, - "intent": {"next_agent_todo": "Validate head", "next_action_kind": "validate", - "next_claimed_by": "unknown-builder"}, - "todos": [{"todo_id": "todo_watch", "role": "agent", "status": "open", - "done": False, "archive_state": "active", "text": "Observe head", - "task_class": "continuous_monitor", "target_key": "watch", "cadence": "1h"}], - } - with pytest.raises(EffectRuntimeRejected, match="claim owner is not registered") as rejected: - effect_runtime_result("scheduler.monitor_batch.plan", request) - assert rejected.value.error_kind == "request_rejected" - assert rejected.value.diagnostic_code == "invalid_request" + registry, runtime, state = _fixture(tmp_path) + _seed_receipt(state, None) + _promote(runtime, state) + state_before = state.read_bytes() + with pytest.raises(ValueError, match="receipt must be an object"): + _replay(registry, runtime) -@pytest.mark.parametrize("fault", ["before", "after"]) -def test_interrupted_legacy_poll_retries_one_complete_batch( - tmp_path: Path, fault: str, -) -> None: - registry, runtime, state, monitor_id, poll = _fixture(tmp_path) - before = state.read_bytes() - - code, result = _cli(registry, runtime, *poll, fault=fault) - assert (code, result) == (83, None) - if fault == "before": - assert state.read_bytes() == before - assert _successors(registry, runtime) == {} - else: - assert set(_successors(registry, runtime)) == {AGENT_TASK, USER_TASK} - assert _poll_events(runtime) == [] - - recovered = _ok(registry, runtime, *poll) - successors = _successors(registry, runtime) - assert set(successors) == {AGENT_TASK, USER_TASK} - assert all(item["unblocks_todo_id"] == monitor_id for item in successors.values() - if item["role"] == "agent") - ids = {item["todo_id"] for item in successors.values()} - assert set(recovered["successor_todo_ids"]) == ids - assert len(_poll_events(runtime)) == 1 - - state_after, index_after = state.read_bytes(), read_run_index(runtime, GOAL) - replay = _ok(registry, runtime, *poll) - assert set(replay["successor_todo_ids"]) == ids - assert state.read_bytes() == state_after - assert read_run_index(runtime, GOAL) == index_after - - -def test_identity_less_legacy_pending_cannot_guess_successors(tmp_path: Path) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - before = state.read_bytes() - code, result = _cli(registry, runtime, *poll, fault="before") - assert (code, result) == (83, None) - pending_path = _quota_receipt(runtime) - pending = json.loads(pending_path.read_text(encoding="utf-8")) - assert pending["status"] == "provider_pending" - # This represents an already persisted older pending admission. Its plan - # did not freeze the complete legacy batch identity before any business - # write, so recovery must preserve it for reconciliation. - pending["provider_plan"].pop("legacy_batch_version", None) - pending_path.write_text(json.dumps(pending), encoding="utf-8") - - code, rejected = _cli(registry, runtime, *poll) - assert code != 0, rejected - assert "reconciliation" in str(rejected.get("reason")), rejected - assert state.read_bytes() == before - assert _successors(registry, runtime) == {} - assert _poll_events(runtime) == [] - assert _quota_receipt(runtime).exists() - - -def test_malformed_business_receipt_is_not_treated_as_absent(tmp_path: Path) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - assert _cli(registry, runtime, *poll, fault="after") == (83, None) - # A present JSON null receipt is corrupt history, not an unstarted effect. - text = re.sub(r"(", - r"\g<1>bnVsbA== -->", state.read_text(encoding="utf-8")) - state.write_text(text, encoding="utf-8") - code, rejected = _cli(registry, runtime, *poll) - assert code != 0 and "receipt must be an object" in rejected["reason"] - assert state.read_text(encoding="utf-8") == text - assert _poll_events(runtime) == [] - - -def test_enabled_shadow_ordinary_retry_recovers_one_prepared_batch( + assert state.read_bytes() == state_before + + +def test_unpromoted_goal_cannot_replay_historical_receipt( tmp_path: Path, ) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - config = json.loads(registry.read_text(encoding="utf-8")) - config["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": True, - "schema_version": "loopx_coordination_runtime_shadow_config_v0", - "provider": "file_v0", - } - registry.write_text(json.dumps(config), encoding="utf-8") - _ok(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", GOAL, - "--execute") - - assert _cli(registry, runtime, *poll, fault="after") == (83, None) - outbox = runtime / "authority-shadow" / "outbox" / GOAL / "todos" - assert len(list(outbox.glob("*.prepared.json"))) == 1 - assert list(outbox.glob("*.committed.json")) == [] - assert "coordination_runtime_shadow" not in _business_receipt(state)["writeback"] - - before = read_local_authority_shadow(runtime_root=runtime, goal_id=GOAL, - scan_limit=20) - assert {todo["text"] for todo in before["head"]["todos"]} == { - "Observe public synthetic target"} - state_after = state.read_bytes() - retry = _ok(registry, runtime, *poll) - assert {todo["todo"] for todo in retry["todo_writeback"]["next_todos"]} == { - AGENT_TASK, USER_TASK} - assert state.read_bytes() == state_after - view = read_local_authority_shadow(runtime_root=runtime, goal_id=GOAL, - scan_limit=20) - assert {todo["text"] for todo in view["head"]["todos"]} >= { - AGENT_TASK, USER_TASK} - assert list(outbox.glob("*.prepared.json")) == [] - - -def test_committed_receipt_without_batch_version_still_replays(tmp_path: Path) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - committed = _ok(registry, runtime, *poll) - receipt = json.loads(_quota_receipt(runtime).read_text(encoding="utf-8")) - assert receipt["status"] != "provider_pending" - assert "legacy_batch_version" not in receipt - assert "provider_plan" not in receipt - state_after = state.read_bytes() - index_after = read_run_index(runtime, GOAL) - - replay = _ok(registry, runtime, *poll) - assert replay["replayed"] is True - assert replay["successor_todo_ids"] == committed["successor_todo_ids"] - original_writeback = dict(committed["todo_writeback"]) - repeated_writeback = dict(_business_receipt(state)["writeback"]) - for payload in (original_writeback, repeated_writeback): - payload.pop("provider_replayed", None) - payload.pop("monitor_effect_id", None) - # One batch capture is reported at the original call. Its post-commit - # drain receipt cannot be in the atomically installed business receipt. - original_writeback.pop("coordination_runtime_shadow", None) - for result in [original_writeback["todo_update"], *original_writeback["next_todos"]]: - result.pop("coordination_runtime_shadow", None) - assert repeated_writeback == original_writeback - assert state.read_bytes() == state_after - assert read_run_index(runtime, GOAL) == index_after - - -@pytest.mark.parametrize("dry_run", [True, False]) -@pytest.mark.parametrize("user_class", ["user_action", "user_gate"]) -def test_public_monitor_writeback_retains_todo_result_contract( - tmp_path: Path, dry_run: bool, user_class: str, -) -> None: - registry, runtime, state, monitor_id, poll = _fixture(tmp_path) - before = state.read_bytes() - args = list(poll) - args[args.index("user_action")] = user_class - if dry_run: - args.remove("--execute") - writeback = _ok(registry, runtime, *args)["todo_writeback"] - update = writeback["todo_update"] - assert update["dry_run"] is dry_run - assert update["changed"] is True - assert update["mutation_authority"]["actor_agent_id"] == AGENT - assert update["agent_id"] == AGENT - assert update["handoff_mode"] == "legacy" - assert update["role"] == "agent" and update["section"] == "Agent Todo" - assert update["status_changed"] is False - assert update["text_changed"] is False - assert update["metadata_updated"] is True - assert update["required_capabilities"] == [] - assert update["state_file"] == str(state) - assert update["project"] == str(state.parents[3]) - assert update["updated_at"] == writeback["last_checked_at"] - assert len(writeback["next_todos"]) == 2 - agent, user = writeback["next_todos"] - for item in (agent, user): - assert item["ok"] is True and item["dry_run"] is dry_run - assert item["added"] is True and item["already_exists"] is False - assert item["metadata_updated"] is False - assert item["status_changed"] is False - assert item["goal_id"] == GOAL - assert item["required_write_scopes"] == [] - assert item["required_capabilities"] == [] - assert item["excluded_agents"] == [] - assert item["state_file"] == str(state) - assert item["project"] == str(state.parents[3]) - assert item["handoff_mode"] == "legacy" - assert item["coordination_runtime_shadow"] == update["coordination_runtime_shadow"] - assert agent["unblocks_todo_id"] == monitor_id - assert agent["agent_id"] is None - assert user["agent_id"] == AGENT - assert user["task_class"] == user_class - assert user["action_kind"] == ("gate" if user_class == "user_gate" else None) - assert user["global_gate"] is None and user["goal_bound"] is None - if user_class == "user_gate": - assert user["blocks_agent"] == AGENT - assert user["unblocks_todo_id"] == monitor_id - else: - assert user["blocks_agent"] is None - assert user["unblocks_todo_id"] is None - if dry_run: - packet = update["local_state_write_correctness"] - assert packet["write_intent"]["target_refs"]["todo_ids"] == [ - monitor_id, agent["todo_id"], user["todo_id"]] - assert packet["write_intent"]["expected_revision"]["kind"] == "active_state_revision" - assert packet["apply_result"]["status"] == "preview_only" - assert agent["local_state_write_correctness"] == packet - assert user["local_state_write_correctness"] == packet - assert state.read_bytes() == before - - -@pytest.mark.parametrize("conflict", [False, True]) -def test_existing_successor_semantics_are_checked_before_batch_write( - tmp_path: Path, conflict: bool, -) -> None: - registry, runtime, state, monitor_id, poll = _fixture(tmp_path) - preview = _ok(registry, runtime, *poll[:-1])["todo_writeback"] - route = preview["next_todos"][0] - existing = add_goal_todo( - registry_path=registry, goal_id=GOAL, role="agent", text=AGENT_TASK, - task_class="advancement_task", - action_kind="implement" if conflict else "validate", - continuation_policy=route["continuation_policy"], - unblocks_todo_id=monitor_id, - monitor_metadata={"target_key": route["target_key"]}, - ) - before = state.read_bytes() - if conflict: - code, rejected = _cli(registry, runtime, *poll) - assert code != 0 and "different action_kind" in rejected["reason"] - assert state.read_bytes() == before - assert _poll_events(runtime) == [] - else: - writeback = _ok(registry, runtime, *poll)["todo_writeback"] - agent = writeback["next_todos"][0] - assert agent["todo_id"] == existing["todo_id"] - assert agent["added"] is False and agent["already_exists"] is True - assert agent["metadata_updated"] is False - assert len(_poll_events(runtime)) == 1 - - -def test_pending_legacy_receipt_replays_after_canonical_cutover(tmp_path: Path) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - code, result = _cli(registry, runtime, *poll, fault="after") - assert (code, result) == (83, None) - original_ids = {todo["todo_id"] for todo in _successors(registry, runtime).values()} - state_after = state.read_bytes() - assert _poll_events(runtime) == [] - pending_path = _quota_receipt(runtime) - pending = json.loads(pending_path.read_text(encoding="utf-8")) - pending["provider_plan"].pop("legacy_batch_version", None) - pending_path.write_text(json.dumps(pending), encoding="utf-8") - items = list_goal_todos(registry_path=registry, goal_id=GOAL)["todos"] - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=items, handoff_mode="soft_claim") - initialize_canonical_authority(runtime, GOAL, projection, state_path=state) - before_provider = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL) - - replay = _ok(registry, runtime, *poll) - assert set(replay["successor_todo_ids"]) == original_ids - assert state.read_bytes() == state_after - assert len(_poll_events(runtime)) == 1 - assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL) == before_provider - index_after = read_run_index(runtime, GOAL) - repeated = _ok(registry, runtime, *poll) - assert repeated["replayed"] is True - assert set(repeated["successor_todo_ids"]) == original_ids - assert state.read_bytes() == state_after - assert read_run_index(runtime, GOAL) == index_after - assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL) == before_provider - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_historical_canonical_pending_plan_settles_existing_business_receipt( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, -) -> None: - isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - items = list_goal_todos(registry_path=registry, goal_id=GOAL)["todos"] - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=items, handoff_mode="soft_claim") - initialize_canonical_authority(runtime, GOAL, projection, - state_path=state, provider=provider) - _ok(registry, runtime, "quota", "should-run", "--goal-id", GOAL, - "--agent-id", AGENT, "--runtime-profile", "generic_cli", - "--turn-instance-id", "synthetic-monitor-turn-1") - - code, result = _cli(registry, runtime, *poll, fault="quota_commit") - assert (code, result) == (73, None) - committed = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL) - successor_ids = {todo["todo_id"] for todo in committed["todos"] - if todo["text"] in (AGENT_TASK, USER_TASK)} - assert len(successor_ids) == 2 - assert _poll_events(runtime) == [] - pending_path = _quota_receipt(runtime) - pending = json.loads(pending_path.read_text(encoding="utf-8")) - assert pending["status"] == "provider_pending" - # Historical canonical plans predate the legacy batch version. The - # already committed native provider receipt remains the recovery source. - pending["provider_plan"].pop("legacy_batch_version", None) - pending_path.write_text(json.dumps(pending), encoding="utf-8") - - settled = _ok(registry, runtime, *poll) - assert set(settled["successor_todo_ids"]) == successor_ids - assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL) == committed - assert len(_poll_events(runtime)) == 1 - index_after = read_run_index(runtime, GOAL) - repeated = _ok(registry, runtime, *poll) - assert repeated["replayed"] is True - assert set(repeated["successor_todo_ids"]) == successor_ids - assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL) == committed - assert read_run_index(runtime, GOAL) == index_after - - -def test_legacy_writer_fence_rejects_full_monitor_batch(tmp_path: Path) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - before = state.read_bytes() - _engage_fence(runtime) - - code, rejected = _cli(registry, runtime, *poll) - assert code != 0, rejected - assert state.read_bytes() == before - assert _poll_events(runtime) == [] - - -def test_atomic_successor_reconciles_existing_typed_next_action(tmp_path: Path) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - lower = _ok(registry, runtime, "todo", "add", "--goal-id", GOAL, - "--role", "agent", "--text", "[P1] Existing lower priority task", - "--task-class", "advancement_task", "--action-kind", "validate") - with state.open("a", encoding="utf-8") as handle: - handle.write("\n## Next Action\n\n- [P1] Existing lower priority task\n" - "\n") - command = list(poll) - command[command.index(AGENT_TASK)] = "[P0] Validate urgent observed result" - code, result = _cli(registry, runtime, *command, fault="after") - assert (code, result) == (83, None) - successor = next(todo for todo in _ok(registry, runtime, "todo", "list", - "--goal-id", GOAL)["todos"] if todo.get("unblocks_todo_id") == _monitor_id) - expected = ("") - assert expected in state.read_text(encoding="utf-8") - committed = state.read_bytes() - retry = _ok(registry, runtime, *command) - assert successor["todo_id"] in retry["successor_todo_ids"] - assert state.read_bytes() == committed - - -@pytest.mark.parametrize("change", ["rename", "complete", "archive"]) -def test_pending_retry_keeps_original_successor_identity_after_public_edit( - tmp_path: Path, change: str, -) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - code, result = _cli(registry, runtime, *poll, fault="after") - assert (code, result) == (83, None) - first = _successors(registry, runtime) - assert set(first) == {AGENT_TASK, USER_TASK} - original_ids = {item["todo_id"] for item in first.values()} - agent_id = first[AGENT_TASK]["todo_id"] - - if change == "rename": - _ok(registry, runtime, "todo", "update", "--goal-id", GOAL, - "--todo-id", agent_id, "--role", "agent", "--agent-id", AGENT, - "--text", "Validate observed synthetic head with revised scope") - else: - _ok(registry, runtime, "todo", "complete", "--goal-id", GOAL, - "--todo-id", agent_id, "--role", "agent", "--agent-id", AGENT, - "--no-follow-up", "--evidence", "Synthetic validation complete") - if change == "archive": - _ok(registry, runtime, "todo", "archive-completed", "--goal-id", GOAL, - "--role", "agent", "--max-active-done", "0", "--execute") - archived = _ok(registry, runtime, "todo", "list", "--goal-id", GOAL, - "--role", "agent", "--todo-id", agent_id) - assert archived["todo"]["archive_state"] == "archive" - edited_state = state.read_bytes() - assert len(_poll_events(runtime)) == 0 - - retry = _ok(registry, runtime, *poll) - writeback = retry["todo_writeback"] - assert writeback["todo_update"]["todo"] == "Observe public synthetic target" - assert [todo["todo"] for todo in writeback["next_todos"]] == [AGENT_TASK, USER_TASK] - assert all(todo["added"] is True for todo in writeback["next_todos"]) - assert set(retry["successor_todo_ids"]) == original_ids - assert state.read_bytes() == edited_state - assert len(_poll_events(runtime)) == 1 - index_after = read_run_index(runtime, GOAL) - - replay = _ok(registry, runtime, *poll) - receipt = _business_receipt(state) - assert receipt["writeback"]["next_todos"] == [ - {key: value for key, value in todo.items() - if key != "coordination_runtime_shadow"} - for todo in writeback["next_todos"]] - assert set(replay["successor_todo_ids"]) == original_ids - assert state.read_bytes() == edited_state - assert read_run_index(runtime, GOAL) == index_after - - changed_intent = list(poll) - changed_intent[changed_intent.index(USER_TASK)] = "Request a different review" - code, conflict = _cli(registry, runtime, *changed_intent) - assert code != 0, conflict - assert state.read_bytes() == edited_state - assert read_run_index(runtime, GOAL) == index_after + registry, runtime, state = _fixture(tmp_path) + _seed_receipt(state, _historical_receipt()) + state_before = state.read_bytes() + + with pytest.raises(CanonicalAuthorityRequired): + _replay(registry, runtime) + + assert state.read_bytes() == state_before + assert not (runtime / "goals" / GOAL / "authority").exists() diff --git a/tests/control_plane/test_monitor_quiet_due_recovery.py b/tests/control_plane/test_monitor_quiet_due_recovery.py index 47bfbcc192..54b0419410 100644 --- a/tests/control_plane/test_monitor_quiet_due_recovery.py +++ b/tests/control_plane/test_monitor_quiet_due_recovery.py @@ -12,24 +12,19 @@ import pytest -from canonical_authority_fixture import ( - initialize_canonical_authority, - isolate_sqlite_runtime, -) +from canonical_authority_fixture import isolate_sqlite_runtime from tests.control_plane.test_monitor_followthrough_contract import ( AGENT_ID, GOAL_ID, _add_monitor, _write_fixture, ) -from loopx.control_plane.coordination.runtime_shadow import ( - build_todo_runtime_shadow_projection, -) +from tests.control_plane.test_native_monitor_poll import _canonical from loopx.control_plane.testing.canary_harness import run_json_cli, run_json_cli_result -from loopx.todos import add_goal_todo, list_goal_todos +from loopx.todos import add_goal_todo -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("material", [False, True], ids=["unchanged", "material"]) @pytest.mark.parametrize("peer_gate", [False, True], ids=["no-gate", "peer-gate"]) def test_quiet_turn_binds_due_monitor_and_recovers_lost_response( @@ -40,7 +35,7 @@ def test_quiet_turn_binds_due_monitor_and_recovers_lost_response( peer_gate, ): isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state = _write_fixture(tmp_path) + registry, runtime, state = _write_fixture(tmp_path, provider=provider) add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -54,15 +49,6 @@ def test_quiet_turn_binds_due_monitor_and_recovers_lost_response( monitor = _add_monitor( registry, text="Observe a public target", target_key="public-target" ) - if provider != "legacy": - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, - todos=list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"], - handoff_mode="soft_claim", - ) - initialize_canonical_authority( - runtime, GOAL_ID, projection, state_path=state, provider=provider - ) def call(*args): return run_json_cli(*args, registry_path=registry, runtime_root=runtime) @@ -234,7 +220,7 @@ def todos(): @pytest.mark.parametrize(("provider", "handoff_mode"), [ - ("legacy", "soft_claim"), ("file", "soft_claim"), ("sqlite", "soft_claim"), + ("file", "soft_claim"), ("sqlite", "soft_claim"), ("file", "hard_lease"), ("sqlite", "hard_lease"), ]) def test_blocked_bound_monitor_requires_verified_lifecycle_repair( @@ -245,25 +231,17 @@ def test_blocked_bound_monitor_requires_verified_lifecycle_repair( ): """A blocker changes execution eligibility, never the committed binding.""" isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state = _write_fixture(tmp_path) + registry, runtime, state = _write_fixture( + tmp_path, + provider=provider, + handoff_mode=handoff_mode, + ) monitor = _add_monitor( registry, text="Observe a public target", target_key="public-target", next_due_at="2000-01-01T00:00:00Z", ) - if provider != "legacy": - initialize_canonical_authority( - runtime, - GOAL_ID, - build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, - todos=list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"], - handoff_mode=handoff_mode, - ), - state_path=state, - provider=provider, - ) def call(*args): return run_json_cli(*args, registry_path=registry, runtime_root=runtime) @@ -372,20 +350,18 @@ def test_projected_restore_rejects_an_active_execution_lease(tmp_path, monkeypat from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state = _write_fixture(tmp_path) - monitor = _add_monitor(registry, text="Observe a public target", target_key="public-target") - run_json_cli("todo", "update", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, - "--todo-id", monitor["todo_id"], "--status", "blocked", registry_path=registry, - runtime_root=runtime) - lease = {"schema_version": "task_lease_v0", "goal_id": GOAL_ID, - "todo_id": monitor["todo_id"], "owner": AGENT_ID, "idempotency_key": "active-monitor", - "status": "active", "expires_at": "2099-01-01T00:00:00Z", "version": 1, - "lease_epoch": 1, "write_scopes": [], "acquire_ttl_seconds": 900} - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, todos=list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"], - handoff_mode="hard_lease", leases=[lease], + registry, runtime, state, monitor = _canonical( + tmp_path, + provider=provider, + monitor_status="blocked", + lease={ + "status": "active", + "idempotency_key": "active-monitor", + "version": 1, + "lease_epoch": 1, + "expires_at": "2099-01-01T00:00:00Z", + }, ) - initialize_canonical_authority(runtime, GOAL_ID, projection, state_path=state, provider=provider) actions = recovery_cli_actions( {"unsettled_host_turn_recovery": {"repair": "lifecycle", "scope": "current_turn", "binding_id": monitor["todo_id"], "turn_instance_id": "held-monitor-turn"}}, diff --git a/tests/control_plane/test_monitor_receipt_writer_isolation.py b/tests/control_plane/test_monitor_receipt_writer_isolation.py deleted file mode 100644 index 425a06c8d1..0000000000 --- a/tests/control_plane/test_monitor_receipt_writer_isolation.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Retained Monitor receipts survive retirement of their original writer.""" -from __future__ import annotations - -import json -import subprocess -import sys - -import pytest - -from canonical_authority_fixture import initialize_canonical_authority -from test_canonical_todo_writer_isolation import ( - isolated_todo_distribution, retirement_wheel, # noqa: F401 -- shared distribution fixtures - _remove_monitor_source_modules, -) -from test_monitor_poll_transaction_recovery import ( - AGENT, GOAL, _business_receipt, _cli, _fixture, _poll_events, _quota_receipt, -) - -from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted -from loopx.control_plane.coordination.local_authority_shadow_outbox import outbox_root -from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection -from loopx.todos import list_goal_todos - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("outbox_present", [False, True]) -@pytest.mark.parametrize("writer_present", [False, True], ids=["absent", "present"]) -def test_imported_monitor_receipt_settles_without_legacy_writer( - tmp_path, provider, outbox_present, writer_present, isolated_todo_distribution, # noqa: F811 -): - package, command = isolated_todo_distribution - registry, runtime, state, _monitor, poll = _fixture(tmp_path) - code, response = _cli(registry, runtime, *poll, fault="after") - assert (code, response) == (83, None) - original = _business_receipt(state)["writeback"] - successor_ids = {item["todo_id"] for item in original["next_todos"]} - pending_path = _quota_receipt(runtime) - pending = json.loads(pending_path.read_text()) - pending["provider_plan"].pop("legacy_batch_version", None) - pending_path.write_text(json.dumps(pending)) - items = list_goal_todos(registry_path=registry, goal_id=GOAL)["todos"] - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=items, handoff_mode="soft_claim") - initialize_canonical_authority(runtime, GOAL, projection, - state_path=state, provider=provider) - - def cli(*args): - result = subprocess.run( - [*command, "--registry", str(registry), "--runtime-root", str(runtime), - "--format", "json", *args], 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"], payload - return payload - - if not writer_present: - _remove_monitor_source_modules(package) - # New work after cutover must survive recovery of the old operation. - later = cli("todo", "add", "--goal-id", GOAL, "--role", "agent", - "--text", "Later canonical work", "--claimed-by", AGENT, - "--operation-id", "later-canonical-work") - root = outbox_root(runtime, GOAL) - if outbox_present: - root.mkdir(parents=True, exist_ok=True) - # Deliberately ambiguous old evidence must remain inert after cutover. - (root / "retained.json").write_text('{"ambiguous_original":true}') - - def facts(): - return (state.read_bytes(), read_canonical_todos_if_promoted( - runtime_root=runtime, goal_id=GOAL), - {str(p.relative_to(root)): p.read_bytes() for p in root.rglob("*") if p.is_file()}) - - before = facts() - assert later["todo_id"] in {t["todo_id"] for t in before[1]["todos"]} - settled = cli(*poll) - assert set(settled["successor_todo_ids"]) == successor_ids - assert facts() == before - assert len(_poll_events(runtime)) == 1 - events = _poll_events(runtime) - assert cli(*poll)["replayed"] is True - assert _poll_events(runtime) == events and facts() == before - - # Cold provider replay must remain independent of the same source modules. - request = tmp_path / "request.json" - request.write_text(json.dumps({"plan": pending["provider_plan"], - "registry": str(registry), "runtime": str(runtime)})) - child = """ -import json, sys -from pathlib import Path -from loopx.control_plane.quota.monitor_poll import _provider_writeback -request = json.loads(Path(sys.argv[1]).read_text()) -result = _provider_writeback(request['plan'], registry_path=Path(request['registry']), - runtime_root=Path(request['runtime'])) -for changed in ({'next_agent_todo': 'Changed historical intent'}, - {'agent_id': 'different-observer'}, {'result_hash': 'different-evidence'}): - try: - _provider_writeback({**request['plan'], **changed}, - registry_path=Path(request['registry']), - runtime_root=Path(request['runtime'])) - except ValueError as error: - assert 'request mismatch' in str(error), str(error) - else: - raise AssertionError('different original-operation intent must be rejected') -assert 'loopx.control_plane.scheduler.legacy_monitor_poll' not in sys.modules -assert 'loopx.control_plane.coordination.local_authority_shadow_adapter' not in sys.modules -print(json.dumps(result)) -""" - result = subprocess.run([sys.executable, "-c", child, str(request)], cwd=tmp_path, - capture_output=True, text=True, timeout=60) - assert result.returncode == 0, result.stdout + result.stderr - replay = json.loads(result.stdout) - assert replay["provider_replayed"] is True - assert {k: v for k, v in replay.items() if k != "provider_replayed"} == { - k: v for k, v in original.items() if k != "provider_replayed"} - assert facts() == before diff --git a/tests/control_plane/test_monitor_state_owner.py b/tests/control_plane/test_monitor_state_owner.py index 386a32c2c4..cd06e0f6c6 100644 --- a/tests/control_plane/test_monitor_state_owner.py +++ b/tests/control_plane/test_monitor_state_owner.py @@ -65,7 +65,7 @@ def test_monitor_effect_replay_and_generation_are_locked_public_writer_semantics before = state.read_bytes() replay = _poll(registry, todo["todo_id"], "2030-01-01T02:00:00Z", "new", monitor_effect_id="effect-one") - assert replay["monitor_poll_transition"]["provider_replayed"] is True + assert replay["status"] == "replayed" assert state.read_bytes() == before with pytest.raises(ValueError, match="different observation"): _poll(registry, todo["todo_id"], "2030-01-01T02:00:00Z", "other", diff --git a/tests/control_plane/test_native_monitor_configuration.py b/tests/control_plane/test_native_monitor_configuration.py index efa94e53f6..21df19460f 100644 --- a/tests/control_plane/test_native_monitor_configuration.py +++ b/tests/control_plane/test_native_monitor_configuration.py @@ -6,7 +6,7 @@ import pytest from canonical_authority_fixture import isolate_sqlite_runtime from test_native_monitor_poll import _canonical -from test_monitor_followthrough_contract import _write_fixture, _add_monitor, GOAL_ID, AGENT_ID +from test_monitor_followthrough_contract import GOAL_ID, AGENT_ID from loopx.control_plane.scheduler.monitor_poll_writeback import write_monitor_poll_todo_state from loopx.control_plane.testing.canary_harness import run_json_cli from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted @@ -19,10 +19,6 @@ def setup(tmp_path, provider): - if provider == "legacy": - registry, runtime, state = _write_fixture(tmp_path) - monitor = _add_monitor(registry, text="Observe public changes", target_key="public-watch", next_due_at="2000-01-01T00:00:00Z") - return registry, runtime, state, monitor return _canonical(tmp_path, provider=provider) @@ -41,7 +37,7 @@ def _observe(registry, runtime, monitor, result_hash="observed-before-configurat return receipt -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_configuration_cli_and_clear_preserve_observation(tmp_path, monkeypatch, provider): isolate_sqlite_runtime(tmp_path, monkeypatch) registry, runtime, state, monitor = setup(tmp_path, provider) @@ -53,24 +49,21 @@ def test_configuration_cli_and_clear_preserve_observation(tmp_path, monkeypatch, assert before["monitor_effect_id"] == "configuration-observation" assert before["last_checked_at"] assert int(before["material_change_generation"]) >= 1 - if provider != "legacy": - state.unlink() + state.unlink() args = ("todo", "update", "--goal-id", GOAL_ID, "--todo-id", monitor["todo_id"], "--agent-id", AGENT_ID, "--cadence", "2h", "--next-due-at", "2099-01-01T02:00:00Z") - identity = () if provider == "legacy" else ("--update-operation-id", "configuration-a") + identity = ("--update-operation-id", "configuration-a") run_json_cli(*args, *identity, "--dry-run", registry_path=registry, runtime_root=runtime) - if provider != "legacy": - assert not state.exists() + assert not state.exists() result = run_json_cli(*args, *identity, registry_path=registry, runtime_root=runtime) current = _row(registry, monitor["todo_id"]) assert current["cadence"] == "2h" assert current["next_due_at"] == "2099-01-01T02:00:00Z" for field in OBSERVATION_FIELDS: assert current.get(field) == before.get(field), field - if provider != "legacy": - assert result["source_authority"] == ("file_v0" if provider == "file" else "sqlite_v0") - replay = run_json_cli(*args, *identity, registry_path=registry, runtime_root=runtime) - assert replay["status"] == "replayed" + assert result["source_authority"] == ("file_v0" if provider == "file" else "sqlite_v0") + replay = run_json_cli(*args, *identity, registry_path=registry, runtime_root=runtime) + assert replay["status"] == "replayed" # Clearing watch_only needs another retained bound, and an expiry-only edit # must keep the due time the explicit cadence edit pinned. update_goal_todo(registry_path=registry, runtime_root_arg=str(runtime), goal_id=GOAL_ID, @@ -94,7 +87,7 @@ def test_configuration_cli_and_clear_preserve_observation(tmp_path, monkeypatch, assert recadenced.get(field) == before.get(field), field -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_route_identity_target_key_is_not_schedule_configuration(tmp_path, monkeypatch, provider): """A Monitor successor keeps its routing target without becoming a Monitor.""" isolate_sqlite_runtime(tmp_path, monkeypatch) @@ -137,7 +130,7 @@ def unavailable(**kwargs): assert replay["projection_delivery"] == "delivered" -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_observed_target_cannot_be_repurposed_by_configuration(tmp_path, monkeypatch, provider): isolate_sqlite_runtime(tmp_path, monkeypatch) registry, runtime, _state, monitor = setup(tmp_path, provider) diff --git a/tests/control_plane/test_native_monitor_poll.py b/tests/control_plane/test_native_monitor_poll.py index 8f8fd64e2d..d1f30f791c 100644 --- a/tests/control_plane/test_native_monitor_poll.py +++ b/tests/control_plane/test_native_monitor_poll.py @@ -6,24 +6,41 @@ import json from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime -from test_monitor_followthrough_contract import _write_fixture, _add_monitor, GOAL_ID, AGENT_ID +from test_monitor_followthrough_contract import _write_fixture, GOAL_ID, AGENT_ID from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted from loopx.control_plane.scheduler.monitor_poll_writeback import write_monitor_poll_todo_state from loopx.control_plane.testing.canary_harness import run_json_cli -from loopx.todos import list_goal_todos -def _canonical(tmp_path, native=False, provider="file", lease=None): - registry, runtime, state = _write_fixture(tmp_path) - monitor = _add_monitor(registry, text="Observe a public target", target_key="public-watch", - next_due_at="2000-01-01T00:00:00Z") - items = list_goal_todos(registry_path=registry, goal_id=GOAL_ID, role="agent")["todos"] +def _canonical( + tmp_path, + native=False, + provider="file", + lease=None, + monitor_status="open", +): + registry, runtime, state = _write_fixture(tmp_path, canonical=False) + monitor = { + "schema_version": "todo_item_v0", + "todo_id": "todo_public_watch", + "role": "agent", + "status": monitor_status, + "done": monitor_status == "done", + "text": "Observe a public target", + "task_class": "continuous_monitor", + "archive_state": "active", + "source_section": "Agent Todo", + "index": 1, + "claimed_by": AGENT_ID, + "target_key": "public-watch", + "cadence": "1h", + "next_due_at": "2000-01-01T00:00:00Z", + "watch_only": "true", + } + items = [monitor] leases = [] if lease is not None: - for item in items: - if item["todo_id"] == monitor["todo_id"]: - item["claimed_by"] = AGENT_ID leases = [{"schema_version": "task_lease_v0", "goal_id": GOAL_ID, "todo_id": monitor["todo_id"], "owner": AGENT_ID, "write_scopes": [], **lease}] projection = build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=items, leases=leases, diff --git a/tests/control_plane/test_native_todo_ownership_update.py b/tests/control_plane/test_native_todo_ownership_update.py index ccf5c984cb..4625822166 100644 --- a/tests/control_plane/test_native_todo_ownership_update.py +++ b/tests/control_plane/test_native_todo_ownership_update.py @@ -6,15 +6,12 @@ from loopx.todos import update_goal_todo -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_public_transfer_clear_exclusions_and_replay(tmp_path, monkeypatch, provider): isolate_sqlite_runtime(tmp_path, monkeypatch) - promoted = provider != "legacy" - registry, state = fixture(tmp_path, promoted, provider) + registry, state = fixture(tmp_path, provider) before = records(registry) - transfer = ["--claimed-by", "Agent B"] - if promoted: - transfer += ["--update-operation-id", "transfer-first"] + transfer = ["--claimed-by", "Agent B", "--update-operation-id", "transfer-first"] update(registry, *transfer, "--dry-run") assert records(registry) == before update(registry, *transfer) @@ -23,9 +20,8 @@ def test_public_transfer_clear_exclusions_and_replay(tmp_path, monkeypatch, prov update(registry, "--agent-id", "agent-b", "--clear-claim") cleared = records(registry) assert not cleared["todo_target"].get("claimed_by") - if promoted: - assert update(registry, *transfer)["status"] == "replayed" - assert records(registry) == cleared + assert update(registry, *transfer)["status"] == "replayed" + assert records(registry) == cleared update(registry, "--excluded-agent", "agent-b") excluded = records(registry) assert excluded["todo_target"]["excluded_agents"] == ["agent-b"] @@ -37,27 +33,24 @@ def test_public_transfer_clear_exclusions_and_replay(tmp_path, monkeypatch, prov assert state.exists(), "canonical commit still delivers its permanent Markdown projection" -@pytest.mark.parametrize("promoted", [False, True]) @pytest.mark.parametrize("intent", [ {"claimed_by": "unregistered"}, {"excluded_agents": ["agent-b", "unregistered"]}, {"claimed_by": "agent-b", "clear_claim": True}, {"claimed_by": "agent-b", "excluded_agents": ["agent-b"]}, {"excluded_agents": ["agent-a"]}, ]) -def test_invalid_ownership_intent_never_partially_writes_copy(tmp_path, promoted, intent): - registry, state = fixture(tmp_path, promoted) +def test_invalid_ownership_intent_never_partially_writes_copy(tmp_path, intent): + registry, state = fixture(tmp_path) before = records(registry) with pytest.raises((ValueError, RuntimeError)): update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id="todo_target", agent_id="agent-a", text="Must not partially commit", **intent) assert records(registry) == before - if promoted: - assert not state.exists() + assert not state.exists() -@pytest.mark.parametrize("promoted", [False, True]) -def test_clear_claim_and_exclude_former_holder_in_one_edit(tmp_path, promoted): - registry, _ = fixture(tmp_path, promoted) +def test_clear_claim_and_exclude_former_holder_in_one_edit(tmp_path): + registry, _ = fixture(tmp_path) result = update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id="todo_target", agent_id="agent-a", clear_claim=True, excluded_agents=["agent-a"]) assert result["ok"] diff --git a/tests/control_plane/test_native_todo_planning_update.py b/tests/control_plane/test_native_todo_planning_update.py index d69fd0822d..7f0782bc24 100644 --- a/tests/control_plane/test_native_todo_planning_update.py +++ b/tests/control_plane/test_native_todo_planning_update.py @@ -15,7 +15,7 @@ from loopx.todos import list_goal_todos, update_goal_todo -def fixture(tmp_path: Path, promoted: bool, provider: str = "file") -> tuple[Path, Path]: +def source_fixture(tmp_path: Path) -> tuple[Path, Path]: project = tmp_path / "project" state = project / ".codex/goals/goal-a/ACTIVE_GOAL_STATE.md" state.parent.mkdir(parents=True) @@ -37,11 +37,19 @@ def fixture(tmp_path: Path, promoted: bool, provider: str = "file") -> tuple[Pat "state_file": ".codex/goals/goal-a/ACTIVE_GOAL_STATE.md", "coordination": {"registered_agents": ["agent-a", "agent-b"]}, }]}), encoding="utf-8") - if promoted: - todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] - projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=todos, handoff_mode="soft_claim") - initialize_canonical_authority(runtime, "goal-a", projection, state_path=state, provider=provider) - state.unlink() # The update must neither require nor import a Markdown authority source. + return registry, state + + +def fixture(tmp_path: Path, provider: str = "file") -> tuple[Path, Path]: + registry, state = source_fixture(tmp_path) + todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] + projection = build_todo_runtime_shadow_projection( + goal_id="goal-a", todos=todos, handoff_mode="soft_claim", + ) + initialize_canonical_authority( + tmp_path / "runtime", "goal-a", projection, state_path=state, provider=provider, + ) + state.unlink() # The update must neither require nor import a Markdown authority source. return registry, state @@ -59,9 +67,13 @@ def records(registry: Path) -> dict[str, dict]: return {item["todo_id"]: item for item in list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"]} -@pytest.mark.parametrize("promoted", [False, True]) -def test_work_requirements_update_preserves_authority_and_supports_explicit_empty(tmp_path: Path, promoted: bool) -> None: - registry, _state = fixture(tmp_path, promoted) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_work_requirements_update_preserves_authority_and_supports_explicit_empty( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, +) -> None: + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, _state = fixture(tmp_path, provider) before = records(registry) result = update_goal_todo( registry_path=registry, goal_id="goal-a", todo_id="todo_target", agent_id="agent-a", @@ -91,7 +103,7 @@ def test_work_requirements_update_preserves_authority_and_supports_explicit_empt assert cleared["task_repository"] == todo["task_repository"] -@pytest.mark.parametrize("promoted", [False, True]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("intent", [ {"required_capabilities": ["code_review", "bad/token"]}, {"required_write_scopes": ["src/**", "../escape"]}, @@ -99,33 +111,37 @@ def test_work_requirements_update_preserves_authority_and_supports_explicit_empt {"task_repository": "user:password@example.com:project"}, {"explore_result_node_refs": ["Node:alpha", "bad/ref"]}, ]) -def test_invalid_work_requirement_is_not_silently_dropped(tmp_path: Path, promoted: bool, intent: dict) -> None: - registry, state = fixture(tmp_path, promoted) +def test_invalid_work_requirement_is_not_silently_dropped( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, intent: dict, +) -> None: + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, state = fixture(tmp_path, provider) before = records(registry) with pytest.raises((ValueError, RuntimeError)): update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id="todo_target", agent_id="agent-a", text="Must not partially commit", **intent) assert records(registry) == before - if promoted: - assert not state.exists() + assert not state.exists() -@pytest.mark.parametrize("promoted", [False, True]) -def test_cli_routes_work_requirements_without_display_dependency(tmp_path: Path, promoted: bool) -> None: - registry, state = fixture(tmp_path, promoted) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_cli_routes_work_requirements_without_display_dependency( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, +) -> None: + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, state = fixture(tmp_path, provider) args = ["--action-kind", "IMPLEMENT", "--task-domain", "code", "--task-repository", "https://github.com/example/project", "--required-capability", "Code-Review", "--target-capability", "Delivery", - "--required-write-scope", "src/**", "--explore-result-node-ref", "Node:alpha"] - if promoted: - args += ["--update-operation-id", "requirements-cli"] + "--required-write-scope", "src/**", "--explore-result-node-ref", "Node:alpha", + "--update-operation-id", "requirements-cli"] update(registry, *args, "--dry-run") - if promoted: - assert not state.exists() + assert not state.exists() update(registry, *args) assert records(registry)["todo_target"]["required_capabilities"] == ["code_review"] - if promoted: - assert update(registry, *args)["status"] == "replayed" + assert update(registry, *args)["status"] == "replayed" update(registry, "--clear-explore-result-node-refs") assert not records(registry)["todo_target"].get("explore_result_node_refs") before = records(registry) @@ -133,7 +149,7 @@ def test_cli_routes_work_requirements_without_display_dependency(tmp_path: Path, assert records(registry) == before -@pytest.mark.parametrize("promoted", [False, True]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize(("remote", "expected"), [ ("https://github.com:443/example/project", "git:github.com/example/project"), ("ssh://git@github.com:22/example/project", "git:github.com/example/project"), @@ -143,24 +159,27 @@ def test_cli_routes_work_requirements_without_display_dependency(tmp_path: Path, ("git://github.com:80/example/project", "git:github.com:80/example/project"), ("git://github.com:0/example/project", "git:github.com:0/example/project"), ]) -def test_cli_repository_ports_match_before_and_after_promotion( - tmp_path: Path, promoted: bool, remote: str, expected: str, +def test_cli_repository_ports_match_across_canonical_providers( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, + remote: str, expected: str, ) -> None: - registry, _state = fixture(tmp_path, promoted) + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, _state = fixture(tmp_path, provider) before = records(registry) - args = ["--task-repository", remote] - if promoted: - args += ["--update-operation-id", "repository-port-cli"] + args = [ + "--task-repository", remote, + "--update-operation-id", "repository-port-cli", + ] update(registry, *args) after = records(registry) assert after["todo_target"]["task_repository"] == expected assert after["todo_other"] == before["todo_other"] - if promoted: - assert update(registry, *args)["status"] == "replayed" - assert records(registry) == after + assert update(registry, *args)["status"] == "replayed" + assert records(registry) == after -@pytest.mark.parametrize("promoted", [False, True]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("surface", ["cli", "python_api"]) @pytest.mark.parametrize(("label", "note", "expected_note"), [ ("omitted", None, "Preserved note"), @@ -168,18 +187,20 @@ def test_cli_repository_ports_match_before_and_after_promotion( ("unicode_whitespace", " \t\u2003\n", "Preserved note"), ("nonempty", " Updated\u2003note ", "Updated note"), ]) -def test_note_input_semantics_match_before_and_after_promotion( - tmp_path: Path, promoted: bool, surface: str, label: str, +def test_note_input_semantics_match_across_canonical_providers( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, + surface: str, label: str, note: str | None, expected_note: str, ) -> None: - registry, _state = fixture(tmp_path, promoted) - operation_id = f"note-{surface}-{label}" if promoted else None + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, _state = fixture(tmp_path, provider) + operation_id = f"note-{surface}-{label}" if surface == "cli": args = ["--text", "Corrected task"] if note is not None: args += ["--note", note] - if operation_id is not None: - args += ["--update-operation-id", operation_id] + args += ["--update-operation-id", operation_id] update(registry, *args) else: result = update_goal_todo( @@ -194,7 +215,7 @@ def test_note_input_semantics_match_before_and_after_promotion( def test_promoted_v0_replay_uses_normalized_empty_note_identity(tmp_path: Path) -> None: - registry, _state = fixture(tmp_path, True) + registry, _state = fixture(tmp_path) base = ["--text", "Corrected task", "--update-operation-id", "note-v0-replay"] assert update(registry, *base)["status"] == "applied" assert update(registry, *base, "--note", "")["status"] == "replayed" @@ -203,18 +224,20 @@ def test_promoted_v0_replay_uses_normalized_empty_note_identity(tmp_path: Path) assert records(registry)["todo_target"]["note"] == "Preserved note" -@pytest.mark.parametrize("promoted", [False, True]) -def test_public_cli_nonterminal_wait_update_and_clear(tmp_path: Path, promoted: bool) -> None: - registry, state = fixture(tmp_path, promoted) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_public_cli_nonterminal_wait_update_and_clear( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, +) -> None: + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, state = fixture(tmp_path, provider) before = records(registry) args = ["--status", "deferred", "--resume-when", "pr_merged:#123", "--reason", "Await upstream", - "--evidence", "Synthetic evidence", "--text", " Corrected\u2003task "] - if promoted: - args += ["--update-operation-id", "planning-attempt"] + "--evidence", "Synthetic evidence", "--text", " Corrected\u2003task ", + "--update-operation-id", "planning-attempt"] update(registry, *args, "--dry-run") assert records(registry) == before - if promoted: - assert not state.exists() + assert not state.exists() update(registry, *args) waiting = records(registry) todo = waiting["todo_target"] @@ -225,11 +248,10 @@ def test_public_cli_nonterminal_wait_update_and_clear(tmp_path: Path, promoted: assert todo["note"] == "Preserved note" assert todo["claimed_by"] == "agent-a" assert waiting["todo_other"] == before["todo_other"] - if promoted: - assert update(registry, *args)["status"] == "replayed" - update(registry, *args, "--note", "Changed retry", ok=False) - assert records(registry) == waiting - assert state.exists(), "accepted commit should drain its independent display projection" + assert update(registry, *args)["status"] == "replayed" + update(registry, *args, "--note", "Changed retry", ok=False) + assert records(registry) == waiting + assert state.exists(), "accepted commit should drain its independent display projection" update(registry, "--status", "open", "--clear-resume-when") resumed = records(registry)["todo_target"] assert resumed["status"] == "open" and resumed["done"] is False @@ -242,7 +264,7 @@ def deferred_hard_lease_fixture( ) -> tuple[Path, Path]: if provider == "sqlite": isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, state = fixture(tmp_path, False) + registry, state = source_fixture(tmp_path) todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] target = next(todo for todo in todos if todo["todo_id"] == "todo_target") target.update(status="deferred", done=True, resume_when="resume_at:2020-01-01T00:00:00Z") @@ -261,7 +283,7 @@ def test_dependency_replan_uses_existing_inactive_lifecycle_then_fresh_lease( ) -> None: if provider == "sqlite": isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, state = fixture(tmp_path, False) + registry, state = source_fixture(tmp_path) todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] target = next(todo for todo in todos if todo["todo_id"] == "todo_target") target.update(resume_when="todo_done:todo_other", successor_todo_ids=["todo_other"]) @@ -343,8 +365,9 @@ def lease(action: str, *args: str) -> tuple[int, dict]: def test_bound_user_action_metadata_through_real_cli( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, ) -> None: - isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, state = fixture(tmp_path, False) + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, state = source_fixture(tmp_path) todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] target = next(todo for todo in todos if todo["todo_id"] == "todo_target") target.pop("claimed_by") @@ -411,8 +434,9 @@ def test_bound_user_action_metadata_through_real_cli( def test_bound_user_action_leased_metadata_through_real_cli( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, ) -> None: - isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, state = fixture(tmp_path, False) + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, state = source_fixture(tmp_path) todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] target = next(todo for todo in todos if todo["todo_id"] == "todo_target") target.pop("claimed_by") @@ -548,7 +572,7 @@ def supersede(*args: str) -> tuple[int, dict]: ["--task-class", "continuous_monitor"], ["--status", "blocked", "--agent-id", "agent-b"], ]) def test_promoted_unsupported_or_unauthorized_update_never_falls_back(tmp_path: Path, args: list[str]) -> None: - registry, state = fixture(tmp_path, True) + registry, state = fixture(tmp_path) before = records(registry) update(registry, *args, ok=False) assert records(registry) == before @@ -560,7 +584,7 @@ def test_promoted_unsupported_or_unauthorized_update_never_falls_back(tmp_path: ("todo_target", "user", "Todo does not have the requested role"), ]) def test_native_target_lookup_keeps_public_value_error(tmp_path: Path, todo_id: str, role: str, reason: str) -> None: - registry, state = fixture(tmp_path, True) + registry, state = fixture(tmp_path) before = records(registry) with pytest.raises(ValueError, match=reason): update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id=todo_id, @@ -569,7 +593,7 @@ def test_native_target_lookup_keeps_public_value_error(tmp_path: Path, todo_id: assert not state.exists() -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("condition_kind", ["todo_done", "monitor_changed"]) @pytest.mark.parametrize("satisfied", [False, True]) def test_public_cli_external_wait_uses_current_successor_source( @@ -577,7 +601,7 @@ def test_public_cli_external_wait_uses_current_successor_source( ) -> None: if provider == "sqlite": isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, state = fixture(tmp_path, False) + registry, state = source_fixture(tmp_path) items = list(records(registry).values()) successor = next(item for item in items if item["todo_id"] == "todo_other") successor.update(claimed_by="agent-a", resume_when=f"{condition_kind}:todo_evidence", @@ -594,24 +618,20 @@ def test_public_cli_external_wait_uses_current_successor_source( "task_class": "continuous_monitor", "claimed_by": "agent-a", "material_change_generation": 3, "cadence": "1h", "watch_only": "true"}, ]) - if provider == "legacy": - rows = [] - for item in items: - metadata = {key: item[key] for key in ("todo_id", "status", "task_class", "claimed_by", - "note", "resume_when", "resume_monitor_generation", "material_change_generation", - "cadence", "watch_only") if item.get(key) is not None} - rows.append(f"- [{'x' if item['status'] == 'done' else ' '}] {item['text']}\n" - + format_todo_metadata_line(**metadata)) - state.write_text("# Goal\n\n## Agent Todo\n\n" + "\n".join(rows) + "\n", encoding="utf-8") - else: - projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=items, handoff_mode="soft_claim") - initialize_canonical_authority(tmp_path / "runtime", "goal-a", projection, state_path=state, provider=provider) - state.unlink() + projection = build_todo_runtime_shadow_projection( + goal_id="goal-a", todos=items, handoff_mode="soft_claim", + ) + initialize_canonical_authority( + tmp_path / "runtime", "goal-a", projection, state_path=state, provider=provider, + ) + state.unlink() before = records(registry) assert before["todo_other"]["resume_ready"] is satisfied - args = ["--resume-when", "monitor_changed:todo_monitor", "--successor-todo-id", "todo_other"] - if provider != "legacy": - args += ["--update-operation-id", "source-satisfied-successor"] + args = [ + "--resume-when", "monitor_changed:todo_monitor", + "--successor-todo-id", "todo_other", + "--update-operation-id", "source-satisfied-successor", + ] if not satisfied: source = list_goal_todos(registry_path=registry, goal_id="goal-a") display = state.read_bytes() if state.exists() else None @@ -630,6 +650,5 @@ def test_public_cli_external_wait_uses_current_successor_source( assert after["todo_target"]["successor_todo_ids"] == ["todo_other"] assert after["todo_other"] == before["todo_other"] assert after["todo_other"]["resume_ready"] is True - if provider != "legacy": - assert update(registry, *args)["status"] == "replayed" - assert records(registry) == after + assert update(registry, *args)["status"] == "replayed" + assert records(registry) == after diff --git a/tests/control_plane/test_native_user_completion_update.py b/tests/control_plane/test_native_user_completion_update.py index f4c9ca5143..e58dbca6ff 100644 --- a/tests/control_plane/test_native_user_completion_update.py +++ b/tests/control_plane/test_native_user_completion_update.py @@ -22,15 +22,22 @@ def fixture(tmp_path: Path, provider: str, *, validation: str | None = None) -> registry.write_text(json.dumps({"schema_version": 1, "common_runtime_root": str(tmp_path / "runtime"), "goals": [{"id": "goal-a", "status": "active", "repo": str(project), "state_file": state.name, "coordination": {"registered_agents": ["agent-a"]}}]}), encoding="utf-8") + projection = build_todo_runtime_shadow_projection( + goal_id="goal-a", todos=[], handoff_mode="soft_claim" + ) + initialize_canonical_authority( + tmp_path / "runtime", + "goal-a", + projection, + state_path=state, + provider=provider, + ) dependent = add_goal_todo(registry_path=registry, goal_id="goal-a", role="agent", text="Continue after the observed outcome", task_class="advancement_task", status="blocked", claimed_by="agent-a") added = add_goal_todo(registry_path=registry, goal_id="goal-a", role="user", text="Record the observed outcome", task_class="user_action", validation_command=validation, unblocks_todo_id=dependent["todo_id"]) - todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] - projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=todos, handoff_mode="soft_claim") - initialize_canonical_authority(tmp_path / "runtime", "goal-a", projection, state_path=state, provider=provider) return registry, state, str(added["todo_id"]) diff --git a/tests/control_plane/test_new_goal_storage_defaults.py b/tests/control_plane/test_new_goal_storage_defaults.py index 9f86f04654..a616a6ee73 100644 --- a/tests/control_plane/test_new_goal_storage_defaults.py +++ b/tests/control_plane/test_new_goal_storage_defaults.py @@ -25,10 +25,13 @@ def environment(tmp_path, monkeypatch): project.mkdir() config = runtime / "machine/configuration.json" config.parent.mkdir(parents=True) - def configure(provider, *, mode=None): - defaults = {"schema_version": "loopx_goal_storage_defaults_v0", "new_goal_provider": provider} - if mode is not None: - defaults.update(schema_version="loopx_goal_storage_defaults_v1", canonical_creation=True, new_goal_handoff_mode=mode) + def configure(provider, *, mode="hard_lease"): + defaults = { + "schema_version": "loopx_goal_storage_defaults_v1", + "new_goal_provider": provider, + "canonical_creation": True, + "new_goal_handoff_mode": mode, + } config.write_text(json.dumps({"schema_version": "loopx_machine_configuration_v0", "namespaces": { "goal_storage": defaults}})) def bootstrap(goal="first", *extra, expected_code=0): @@ -107,16 +110,23 @@ def test_unconfigured_new_goal_defaults_to_canonical_sqlite_and_retains_later_wr @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_explicit_canonical_creation_disabled_remains_target_only(environment, provider): - _, bootstrap, _, _, runtime = environment +def test_explicit_canonical_creation_disabled_is_rejected(environment, provider): + _, bootstrap, _, project, runtime = environment configuration = runtime / "machine/configuration.json" configuration.write_text(json.dumps({"schema_version": "loopx_machine_configuration_v0", "namespaces": { "goal_storage": {"schema_version": "loopx_goal_storage_defaults_v1", "new_goal_provider": provider, "canonical_creation": False, "new_goal_handoff_mode": "soft_claim"}}})) - created = bootstrap() - assert created["storage_target"] == {"schema_version": "loopx_new_goal_storage_target_v0", "provider": provider} - assert created["storage_selection"]["promotion_performed"] is False - assert "authority_initialized" not in created["storage_selection"] + rejected = bootstrap(expected_code=1) + assert rejected["error_code"] == "canonical_authority_required" + assert rejected["operation"] == "goal_create" + assert rejected["write_admitted"] is False + assert rejected["decision_read_from_provider"] is False + assert rejected["legacy_fallback_used"] is False + assert rejected["recovery"] == { + "action": "migrate_goal_authority", + "backup_required": True, + } + assert not (project / ".loopx/registry.json").exists() @pytest.mark.parametrize("invalid_configuration", ["invalid_json", "invalid_namespace", "directory", "broken_symlink"]) @@ -233,19 +243,28 @@ def test_cli_creation_recovery_cannot_recreate_or_adopt_authority(environment, p assert not wrong_source.exists() -def test_pending_creation_uses_frozen_intent_after_machine_default_changes(environment): - configure, bootstrap, marker, project, _ = environment - # Independently model the durable boundary: registry/state published, selector absent. +def test_completed_canonical_creation_ignores_stale_v0_target(environment): + from loopx.control_plane.coordination.local_authority import ( + read_canonical_todos_if_promoted, + ) + + configure, bootstrap, marker, project, runtime = environment configure("file") bootstrap() + before = read_canonical_todos_if_promoted( + runtime_root=runtime, goal_id="first", include_leases=True, + ) registry = project / ".loopx/registry.json" data = json.loads(registry.read_text()) data["goals"][0].setdefault("coordination", {})["storage_target"] = { "schema_version": "loopx_new_goal_storage_target_v0", "provider": "sqlite"} registry.write_text(json.dumps(data)) configure("file") - assert bootstrap()["storage_selection"]["provider"] == "sqlite" - assert json.loads(marker().read_text())["provider"] == "sqlite" + assert bootstrap()["storage_selection"] is None + assert not marker().exists() + assert read_canonical_todos_if_promoted( + runtime_root=runtime, goal_id="first", include_leases=True, + ) == before @pytest.fixture @@ -629,9 +648,9 @@ def test_app_creation_selects_server_configured_non_git_workspace(environment, a @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("relative_runtime", [False, True]) -@pytest.mark.parametrize("mode", [None, "soft_claim", "hard_lease"]) +@pytest.mark.parametrize("mode", ["soft_claim", "hard_lease"]) def test_app_creation_retries_storage_before_reporting_success(environment, app, monkeypatch, provider, relative_runtime, mode): - from loopx.capabilities.machine_configuration import goal_storage + from loopx.control_plane.coordination import goal_storage_initialization from loopx.todos import add_goal_todo configure, _, marker, project, runtime = environment @@ -648,7 +667,7 @@ def test_app_creation_retries_storage_before_reporting_success(environment, app, registry.write_text(json.dumps(data)) unavailable = True selections = [] - effect = goal_storage.effect_runtime_result + effect = goal_storage_initialization.effect_runtime_result def storage_effect(method, payload): if payload.get("action") == "initialize": @@ -659,7 +678,11 @@ def storage_effect(method, payload): return result return effect(method, payload) - monkeypatch.setattr(goal_storage, "effect_runtime_result", storage_effect) + monkeypatch.setattr( + goal_storage_initialization, + "effect_runtime_result", + storage_effect, + ) code, preview = request("/api/actions/preview", { "action_kind": "goal.create", "summary": "Create a Goal", "normalized_parameters": { @@ -698,14 +721,13 @@ def storage_effect(method, payload): assert recovered["proposal"]["receipt"]["outcome"] == "goal_created" assert selections[-1]["provider"] == provider assert selections[-1]["promotion_performed"] is False - if mode is not None: - from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted - source = read_canonical_todos_if_promoted(runtime_root=project.parent / "runtime", goal_id="recovery", include_leases=True) - assert source["handoff_mode"] == mode - assert len(source["todos"]) == 1 - assert source["todos"][0]["text"] == "Verify recovery" - assert source["source_authority"] == f"{provider}_v0" - assert selections[-1]["authority_initialized"] is True + from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted + source = read_canonical_todos_if_promoted(runtime_root=project.parent / "runtime", goal_id="recovery", include_leases=True) + assert source["handoff_mode"] == mode + assert len(source["todos"]) == 1 + assert source["todos"][0]["text"] == "Verify recovery" + assert source["source_authority"] == f"{provider}_v0" + assert selections[-1]["authority_initialized"] is True if provider == "sqlite": assert json.loads(marker("recovery").read_text())["provider"] == provider else: @@ -713,7 +735,7 @@ def storage_effect(method, payload): assert add.call_count == 1 assert request(apply_path, {})[1]["proposal"]["receipt"] == recovered["proposal"]["receipt"] assert add.call_count == 1 - assert len(selections) == (1 if mode is None else 2) + assert len(selections) == 2 @pytest.mark.parametrize("provider", ["file", "sqlite"]) @@ -803,7 +825,8 @@ def test_machine_editor_is_machine_only_and_configuration_rejects_activation(): @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_created_target_is_used_by_reviewed_promotion(environment, provider): +def test_created_target_is_canonical_and_rejects_legacy_bootstrap(environment, provider): + from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted from tests.control_plane.shadow_e2e_fixture import ShadowWorkspace configure, bootstrap, _, project, runtime = environment configure(provider) @@ -813,21 +836,30 @@ def test_created_target_is_used_by_reviewed_promotion(environment, provider): ws = ShadowWorkspace(registry, runtime, state, "first") configured = ws.cli("configure-goal", "--coordination-runtime-shadow-file", "--execute") assert configured["ok"] is True - assert ws.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" - for n in range(3): - assert ws.add(f"Qualify creation target {n}")["ok"] is True - assert ws.drain(budget_seconds="60")["ok"] is True - assert bootstrap()["storage_selection"]["changed"] is False - preview = ws.cli("coordination-shadow", "promote", "--handoff-mode-migration", "preserve") - assert preview["promotion"]["status"] == "preview_ready", preview - plan = project / "reviewed.json" - plan.write_text(json.dumps(preview)) - promoted = ws.cli("coordination-shadow", "promote", "--reviewed-plan", str(plan), "--execute") - assert promoted["promotion"]["canonical_authority"] == f"{provider}_v0", promoted + rejected = ws.cli("coordination-shadow", "bootstrap", "--execute", success=False) + assert rejected["bootstrap"]["status"] == "failed" + assert rejected["bootstrap"]["reason_code"] == "legacy_authority_already_promoted" + source = read_canonical_todos_if_promoted( + runtime_root=runtime, + goal_id="first", + include_leases=True, + ) + assert source["source_authority"] == f"{provider}_v0" + reconnected = bootstrap()["storage_selection"] + assert reconnected["provider"] == provider + assert reconnected["authority_initialized"] is True created = ws.add("Continue on selected provider") assert ws.cli("todo", "list", "--todo-id", created["todo_id"])["authority_read"]["source_authority"] == f"{provider}_v0" - # The initializer must not override a promoted selection. The separate legacy - # bootstrap command still rejects a fenced Goal before reaching this helper. - from loopx.capabilities.machine_configuration.goal_storage import initialize_goal_storage_target + # Reconnects preserve the creation-time authority instead of opening a + # second migration path for a Goal that was born canonical. + from loopx.control_plane.coordination.goal_storage_initialization import ( + initialize_goal_storage_target, + ) goal = json.loads(registry.read_text())["goals"][0] - assert initialize_goal_storage_target(runtime, goal)["status"] == "existing_authority_preserved" + replayed = initialize_goal_storage_target( + runtime, + goal, + registry_path=registry, + ) + assert replayed["status"] == "replayed" + assert replayed["provider"] == provider diff --git a/tests/control_plane/test_post_writeback_capability_hooks.py b/tests/control_plane/test_post_writeback_capability_hooks.py index 07839b015b..72e6803857 100644 --- a/tests/control_plane/test_post_writeback_capability_hooks.py +++ b/tests/control_plane/test_post_writeback_capability_hooks.py @@ -1378,7 +1378,10 @@ def test_todo_complete_survives_unreadable_periodic_report_machine_store( "id": "goal-1", "repo": str(tmp_path), "state_file": "STATE.md", - "agents": [{"id": "agent-a"}], + "coordination": { + "registered_agents": ["agent-a"], + "agent_model": "peer_v1", + }, } ], "common_runtime_root": str(runtime_root), @@ -1386,6 +1389,23 @@ def test_todo_complete_survives_unreadable_periodic_report_machine_store( ), encoding="utf-8", ) + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + from loopx.todos import list_goal_todos + + todos = list_goal_todos(registry_path=registry_path, goal_id="goal-1")["todos"] + initialize_canonical_authority( + runtime_root, + "goal-1", + build_todo_runtime_shadow_projection( + goal_id="goal-1", + todos=todos, + handoff_mode="soft_claim", + ), + state_path=state, + ) _write_unreadable_periodic_report_machine_store(runtime_root) with pytest.warns(UserWarning, match="periodic_report.enabled must be a boolean"): @@ -1410,7 +1430,7 @@ def test_todo_complete_survives_unreadable_periodic_report_machine_store( captured = capsys.readouterr() payload = json.loads(captured.out) - assert exit_code == 0 + assert exit_code == 0, json.dumps(payload, indent=2) assert payload["ok"] is True assert payload["completed"] is True diff --git a/tests/control_plane/test_promoted_archive_retention.py b/tests/control_plane/test_promoted_archive_retention.py deleted file mode 100644 index f35b74b6d0..0000000000 --- a/tests/control_plane/test_promoted_archive_retention.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Canonical archives retain promoted history, not the complete old source. - -This characterizes the existing qualified shadow route. It cannot qualify a -cold Markdown import or authorize removal of historical source readers. -""" -from __future__ import annotations - -import json -import subprocess -import pytest - -from canonical_authority_fixture import isolate_sqlite_runtime -from shadow_e2e_fixture import REPO, workspace -from loopx.control_plane.effect_runtime import restart_effect_runtime -from loopx.rollout_event_log import ROLLOUT_EVENT_SCHEMA_VERSION - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_promoted_archive_scope_and_later_write_recovery(tmp_path, monkeypatch, provider): - isolate_sqlite_runtime(tmp_path, monkeypatch) - ws = workspace(tmp_path / "source", bootstrap=False) - ws.state.write_text( - "---\ngoal_id: goal-e2e\nhandoff_mode: hard_lease\n---\n\n## Agent Todo\n" - "- [ ] Continue work\n" - " \n" - "- [x] Retained work\n" - " \n" - "\n## Completed Work Archive\n" - "- [x] Referenced work\n" - " \n" - "- [x] Unreferenced historical result\n" - " \n" - ) - terminal = { - "schema_version": "task_lease_v0", "goal_id": ws.goal, - "todo_id": "todo_retained", "owner": "agent-a", "version": 2, - "lease_epoch": 1, "status": "released", - "released_at": "2026-09-01T00:00:00Z", "idempotency_key": "old-retained", - "future_extension": {"missing_value": None, "flag": False}, - } - orphan = {**terminal, "todo_id": "todo_unrelated", "idempotency_key": "old-orphan"} - directory = ws.runtime / "goals" / ws.goal / "task-leases" - directory.mkdir(parents=True) - for record in (terminal, orphan): - (directory / (record["todo_id"] + ".json")).write_text(json.dumps(record)) - lease_bytes = {path: path.read_bytes() for path in directory.glob("*.json")} - rollout = directory.parent / "rollout-event-log.jsonl" - original_receipt = json.dumps({ - "schema_version": ROLLOUT_EVENT_SCHEMA_VERSION, "kind": "todo_complete", - "goal_id": ws.goal, "todo_id": "todo_unrelated", - "timestamp": "2026-09-01T00:00:00Z", "receipt_id": "original-unrelated-receipt", - }) + "\n" - rollout.write_text(original_receipt) - if provider == "sqlite": - selected = subprocess.run([ - "node", "--no-warnings", "--experimental-strip-types", "--experimental-sqlite", - str(REPO / "loopx/control_plane/coordination/local_authority_provider.ts"), - "--runtime-root", str(ws.runtime), "--goal-id", ws.goal, "--execute", - ], cwd=REPO, capture_output=True, text=True, timeout=45, check=True) - assert json.loads(selected.stdout)["provider"] == "sqlite" - - assert ws.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" - cold_bytes = ws.state.read_bytes() - refused = ws.cli("coordination-shadow", "promote", success=False) - assert refused["ok"] is False - assert refused["promotion"]["reason_code"] == "local_authority_shadow_not_qualified" - assert ws.state.read_bytes() == cold_bytes - - # Real warm-profile operations are not evidence of a cold import. - captured = {ws.add(f"Independent qualification work {index}")["todo_id"] for index in range(3)} - assert ws.drain(budget_seconds="60")["ok"] is True - preview = ws.cli("coordination-shadow", "promote") - assert preview["promotion"]["status"] == "preview_ready" - reviewed = tmp_path / "reviewed.json" - reviewed.write_text(json.dumps(preview)) - source_bytes = ws.state.read_bytes() - promoted = ws.cli("coordination-shadow", "promote", "--reviewed-plan", str(reviewed), "--execute") - assert promoted["promotion"]["canonical_authority"] == f"{provider}_v0" - assert ws.state.read_bytes() == source_bytes - assert {path: path.read_bytes() for path in lease_bytes} == lease_bytes - assert rollout.read_text().startswith(original_receipt) - assert ws.cli("task-lease", "inspect", "--todo-id", "todo_retained")["lease"] == terminal - - later = ws.add("Later canonical work") - ws.state.unlink() # Only the disposable source: canonical recovery must not need it. - assert restart_effect_runtime()["status"] in {"stopped", "not_running"} - recovered = ws.cli("coordination-shadow", "recover-promotion", "--reviewed-plan", str(reviewed), "--execute") - assert recovered["promotion"]["status"] == "replayed" - current = ws.cli("todo", "list", "--todo-id", later["todo_id"]) - assert current["todo"]["text"] == "Later canonical work" - assert current["authority_read"]["legacy_fallback_used"] is False - assert not ws.state.exists() - - archive = tmp_path / "authority.ndjson" - exported = ws.cli("authority-archive", "export", "--archive", str(archive)) - assert exported["status"] == "exported" - assert "original-unrelated-receipt" not in archive.read_text() - digest = exported["archive"]["archive_sha256"] - for target in ("file", "sqlite"): - destination = tmp_path / f"restore-{target}" - restored = ws.cli("authority-archive", "restore", "--archive", str(archive), - "--archive-sha256", digest, "--destination", str(destination), "--provider", target, "--execute") - assert restored["status"] == "restored" - assert restored["execution_authority_granted"] is False - assert restored["requires_separate_authority_cutover"] is True - audit = ws.cli("authority-archive", "audit", "--archive", str(archive), - "--archive-sha256", digest, "--destination", str(destination)) - assert audit["audit"]["status"] == "matched" - - # Independently reopen the real restored store, including the other provider. - module = REPO / f"loopx/control_plane/coordination/{target}_authority_store.ts" - owner = "FileAuthorityStore" if target == "file" else "SqliteAuthorityStore" - script = ( - f"import {{{owner}}} from {json.dumps(module.as_uri())};" - f"const store=new {owner}({json.dumps(str(destination / 'store'))},{json.dumps(ws.goal)});" - "process.stdout.write(JSON.stringify(await store.loadAuthority()));" - ) - process = subprocess.run([ - "node", "--no-warnings", "--experimental-strip-types", "--experimental-sqlite", - "--input-type=module", "-e", script, - ], capture_output=True, text=True, timeout=45, check=True) - head = json.loads(process.stdout)["head"] - assert {row["todo_id"] for row in head["todos"]} == { - "todo_active", "todo_retained", "todo_prior", later["todo_id"], - } | captured - prior = next(row for row in head["todos"] if row["todo_id"] == "todo_prior") - assert prior["archive_state"] == "archive" - assert prior["completion_receipt_id"] == "receipt-prior" - assert head["leases"] == [terminal] diff --git a/tests/control_plane/test_public_todo_update_plan.py b/tests/control_plane/test_public_todo_update_plan.py index 789be33c56..b14f465111 100644 --- a/tests/control_plane/test_public_todo_update_plan.py +++ b/tests/control_plane/test_public_todo_update_plan.py @@ -1,15 +1,25 @@ """Public update invariants: a partial edit must not invalidate an armed wait.""" import pytest +from loopx.control_plane.coordination.local_authority import ( + LocalCoordinationAuthorityUnavailable, + read_canonical_todos_if_promoted, +) from loopx.todos import add_goal_todo, update_goal_todo from loopx.control_plane.testing.canary_harness import run_json_cli_result from tests.control_plane.test_monitor_followthrough_contract import ( - AGENT_ID, GOAL_ID, _write_fixture, + AGENT_ID, GOAL_ID, ) +from tests.control_plane.canonical_authority_fixture import promoted_create_fixture def waiting_goal(tmp_path): - registry, runtime, state = _write_fixture(tmp_path) + registry, runtime, state = promoted_create_fixture( + tmp_path, + goal_id=GOAL_ID, + handoff_mode="soft_claim", + registered_agents=[AGENT_ID], + ) # Initialize historical evidence through the create/import boundary; # ordinary configuration cannot manufacture an observation generation. monitor = add_goal_todo(registry_path=registry, goal_id=GOAL_ID, role="agent", @@ -27,31 +37,44 @@ def add(text): return registry, runtime, state, waiting, successor +def current_todo(runtime, todo_id): + source = read_canonical_todos_if_promoted( + runtime_root=runtime, + goal_id=GOAL_ID, + ) + return next(todo for todo in source["todos"] if todo["todo_id"] == todo_id) + + @pytest.mark.parametrize("edit", [{"successor_todo_ids": []}, {"status": "blocked"}, {"task_class": "continuous_monitor"}]) def test_partial_edit_cannot_invalidate_retained_monitor_wait(tmp_path, edit): registry, _, state, waiting, _ = waiting_goal(tmp_path) before = state.read_bytes() - with pytest.raises(ValueError, match="external-wait"): + with pytest.raises( + (ValueError, LocalCoordinationAuthorityUnavailable), + match="external-wait", + ): update_goal_todo(registry_path=registry, goal_id=GOAL_ID, todo_id=waiting["todo_id"], agent_id=AGENT_ID, **edit) assert state.read_bytes() == before def test_clear_condition_and_successors_together_is_valid(tmp_path): - registry, _, state, waiting, _ = waiting_goal(tmp_path) - result = update_goal_todo(registry_path=registry, goal_id=GOAL_ID, + registry, runtime, state, waiting, _ = waiting_goal(tmp_path) + update_goal_todo(registry_path=registry, goal_id=GOAL_ID, todo_id=waiting["todo_id"], agent_id=AGENT_ID, clear_resume_when=True, successor_todo_ids=[]) - assert result["resume_when"] is None - assert result["resume_monitor_generation"] is None - assert result["successor_todo_ids"] == [] + result = current_todo(runtime, waiting["todo_id"]) + assert result.get("resume_when") is None + assert result.get("resume_monitor_generation") is None + assert result.get("successor_todo_ids", []) == [] def test_copy_edit_preserves_existing_wait_fence(tmp_path): - registry, _, _, waiting, _ = waiting_goal(tmp_path) - result = update_goal_todo(registry_path=registry, goal_id=GOAL_ID, + registry, runtime, _, waiting, _ = waiting_goal(tmp_path) + update_goal_todo(registry_path=registry, goal_id=GOAL_ID, todo_id=waiting["todo_id"], agent_id=AGENT_ID, note="Clarify context") + result = current_todo(runtime, waiting["todo_id"]) assert result["resume_when"].startswith("monitor_changed:") assert result["resume_monitor_generation"] == 3 assert "external_wait_transition" not in result @@ -71,18 +94,23 @@ def test_real_cli_rejects_partial_wait_edit_and_supports_explicit_repair(tmp_pat assert state.read_bytes() == before -def test_public_update_uses_one_scope_wait_field_crossing(tmp_path, monkeypatch): - from loopx.control_plane.todos import line_update, authoring_scope, resume_condition +def test_public_update_commits_through_canonical_transaction(tmp_path, monkeypatch): + from loopx.control_plane.todos import authoring_scope, provider_update, resume_condition + registry, _, _, waiting, successor = waiting_goal(tmp_path) - actual = line_update.effect_runtime_result + actual = provider_update.effect_runtime_result calls = [] - def traced(method, params): + + def traced(method, params, **kwargs): calls.append(method) - return actual(method, params) - for module in (line_update, authoring_scope, resume_condition): + return actual(method, params, **kwargs) + + for module in (provider_update, authoring_scope, resume_condition): monkeypatch.setattr(module, "effect_runtime_result", traced) - result = update_goal_todo(registry_path=registry, goal_id=GOAL_ID, + update_goal_todo(registry_path=registry, goal_id=GOAL_ID, todo_id=waiting["todo_id"], agent_id=AGENT_ID, successor_todo_ids=[successor["todo_id"]]) + result = current_todo(tmp_path / "runtime", waiting["todo_id"]) assert result["resume_monitor_generation"] == 3 - assert calls == ["todo.public_update.plan"] + assert calls.count("coordination.local_authority.todo_update") == 1 + assert "todo.public_update.plan" not in calls diff --git a/tests/control_plane/test_quota_monitor_clock.py b/tests/control_plane/test_quota_monitor_clock.py index a6f4b40b5f..fff0672c23 100644 --- a/tests/control_plane/test_quota_monitor_clock.py +++ b/tests/control_plane/test_quota_monitor_clock.py @@ -13,7 +13,6 @@ from loopx.control_plane.todos.active_state_todo_parser import parse_active_state_todos from loopx.control_plane.todos import quota_selection from loopx.control_plane.todos.quota_summary import summarize_user_todos_for_quota -from loopx.todos import list_goal_todos from tests.control_plane.test_monitor_followthrough_contract import ( AGENT_ID, GOAL_ID, _add_monitor, _write_fixture, ) @@ -94,7 +93,7 @@ def test_gap_presentation_order_does_not_inherit_claim_or_profile_order() -> Non assert result["monitor_schedule_gap_items"][0]["todo_id"] == "todo_unclaimed" -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_public_quota_gap_count_uses_real_full_source_without_writing_it(tmp_path, monkeypatch, provider) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) runtime, registry, state = tmp_path / "runtime", tmp_path / "registry.json", tmp_path / "state.md" @@ -110,13 +109,12 @@ def test_public_quota_gap_count_uses_real_full_source_without_writing_it(tmp_pat write_fixture_registry(project=tmp_path, runtime_root=runtime, registry_path=registry, goal_id="goal-a", domain="monitor-clock", adapter_kind="generic_project_goal_v0", state_file=str(state), registered_agents=["agent-a", "agent-b"], quota_allowed_slots=None) - if provider != "legacy": - goal = json.loads(registry.read_text())["goals"][0] - fields = parse_active_state_todos(state.read_text(), goal=goal, item_limit=None) - projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=fields["agent_todos"]["items"], - handoff_mode="soft_claim") - initialize_canonical_authority(runtime, "goal-a", projection, state_path=state, provider=provider) - state.unlink() + goal = json.loads(registry.read_text())["goals"][0] + fields = parse_active_state_todos(state.read_text(), goal=goal, item_limit=None) + projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=fields["agent_todos"]["items"], + handoff_mode="soft_claim") + initialize_canonical_authority(runtime, "goal-a", projection, state_path=state, provider=provider) + state.unlink() before = state.read_bytes() if state.exists() else None code, packet = run_json_cli_result("quota", "should-run", "--goal-id", "goal-a", "--agent-id", "agent-a", "--include-detail", "agent-todos", "--scan-path", str(tmp_path), registry_path=registry, runtime_root=runtime) @@ -144,12 +142,12 @@ def unavailable(*_args, **_kwargs): assert source == before -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_cli_due_order_and_successive_turns_preserve_exact_poll_settlement( tmp_path, monkeypatch, provider, ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state = _write_fixture(tmp_path) + registry, runtime, state = _write_fixture(tmp_path, provider=provider) # Deliberately create out of deadline order, so display order cannot # accidentally satisfy the independently specified scheduling oracle. monitors = [ @@ -160,13 +158,7 @@ def test_cli_due_order_and_successive_turns_preserve_exact_poll_settlement( _add_monitor(registry, text="[P1] Observe middle target", target_key="middle", next_due_at="2001-01-01T00:00:00Z"), ] - if provider != "legacy": - rows = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, todos=rows, handoff_mode="soft_claim") - initialize_canonical_authority(runtime, GOAL_ID, projection, - state_path=state, provider=provider) - state.unlink() # Canonical authority, not a stale Markdown fallback. + state.unlink() # Canonical authority, not a stale Markdown fallback. def call(*args): code, result = run_json_cli_result( @@ -177,6 +169,12 @@ def call(*args): scope = ["--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--runtime-profile", "codex_app_ssh_goal"] expected = [monitors[1], monitors[2], monitors[0]] + expected_targets = ["oldest", "middle", "recent"] + expected_due_at = { + monitors[0]["todo_id"]: "2002-01-01T00:00:00Z", + monitors[1]["todo_id"]: "2000-01-01T00:00:00Z", + monitors[2]["todo_id"]: "2001-01-01T00:00:00Z", + } previous_guard = None for ordinal, item in enumerate(expected): turn = f"monitor-order-{ordinal}" @@ -191,7 +189,7 @@ def call(*args): if previous_guard: assert call(*previous_guard)["should_run"] is False poll = ["quota", "monitor-poll", *scope, "--turn-instance-id", turn, - "--todo-id", item["todo_id"], "--target-key", item["target_key"], + "--todo-id", item["todo_id"], "--target-key", expected_targets[ordinal], "--result-hash", f"checked-{ordinal}", "--next-due-at", "2099-01-01T00:00:00Z", "--execute"] settled = call(*poll) @@ -205,7 +203,7 @@ def call(*args): records = call("todo", "list", "--goal-id", GOAL_ID, "--role", "agent")["todos"] by_id = {row["todo_id"]: row for row in records} for unpolled in expected[ordinal + 1:]: - assert by_id[unpolled["todo_id"]]["next_due_at"] == unpolled["next_due_at"] + assert by_id[unpolled["todo_id"]]["next_due_at"] == expected_due_at[unpolled["todo_id"]] assert not by_id[unpolled["todo_id"]].get("last_checked_at") previous_guard = guard events = [json.loads(line) for line in diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index 792582f8f6..ca83590d50 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -189,6 +189,67 @@ def _run_cli( return result.returncode, json.loads(result.stdout) +def _initialize_fixture_authority( + project: Path, + runtime: Path, + *, + provider: str = "file", + handoff_mode: str = "soft_claim", + leases: list[dict[str, Any]] | None = None, +) -> None: + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + + state_path = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + summary = parse_active_state_todos( + state_path.read_text(encoding="utf-8"), + state_path=state_path, + item_limit=None, + ) + todos = [ + *(summary.get("user_todos", {}).get("items") or []), + *(summary.get("agent_todos", {}).get("items") or []), + ] + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=todos, + handoff_mode=handoff_mode, + leases=leases or [], + ), + state_path=state_path, + provider=provider, + ) + + +def _write_codex_heartbeat_rrule(codex_home: Path, rrule: str) -> None: + automation_path = codex_home / "automations" / "fixture" / "automation.toml" + automation_path.parent.mkdir(parents=True, exist_ok=True) + automation_path.write_text( + "\n".join( + [ + "version = 1", + 'id = "fixture"', + 'kind = "heartbeat"', + 'name = "Settlement CLI fixture"', + ( + f'prompt = "Advance `{GOAL_ID}` from active state. ' + f'Agent: `{AGENT_ID}`."' + ), + 'status = "ACTIVE"', + f'rrule = "{rrule}"', + 'target_thread_id = "settlement-cli-thread"', + "", + ] + ), + encoding="utf-8", + ) + + def _run_generated_cli( command: str, *, @@ -850,6 +911,7 @@ def test_codex_app_refresh_stages_validated_memory_and_spend_finalizes_hook( ), encoding="utf-8", ) + _initialize_fixture_authority(project, runtime) turn_id = "turn-reward-memory-app" binding = ( "--agent-id", @@ -992,6 +1054,7 @@ def test_typed_outcome_gap_settles_exact_turn_without_becoming_progress( ) _configure_completion_validation_todo(project) _configure_selectable_alternative(project) + _initialize_fixture_authority(project, runtime) turn_id = "turn-typed-blocker-settlement" binding = ( "--agent-id", @@ -1078,22 +1141,6 @@ def test_typed_outcome_gap_settles_exact_turn_without_becoming_progress( assert mismatch_rc == 1, mismatch assert "settlement binding does not match" in mismatch["error"] - unscheduled_rc, unscheduled = _run_cli( - registry_path, - runtime, - *common_refresh_args, - "--progress-result-class", - "blocked", - "--progress-blocker-id", - "blocker:runtime-boundary", - "--progress-evidence-id", - "evidence:runtime-boundary", - cwd=project, - ) - assert unscheduled_rc == 1, unscheduled - assert "pending resume_when=resume_at" in unscheduled["error"] - assert _classification_count(runtime, "typed_blocker_writeback") == 0 - due_at = (datetime.now(timezone.utc) + timedelta(minutes=5)).replace( microsecond=0 ).isoformat().replace("+00:00", "Z") @@ -1204,6 +1251,7 @@ def test_typed_blocked_retry_without_successor_defers_the_only_todo( ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_completion_validation_todo(project) + _initialize_fixture_authority(project, runtime) turn_id = "turn-typed-blocker-only-todo" binding = ( "--agent-id", AGENT_ID, @@ -1262,6 +1310,7 @@ def test_scheduler_cache_misses_after_todo_state_changes_without_run( tmp_path: Path, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) + _initialize_fixture_authority(project, runtime) guard = ( "quota", "should-run", @@ -1302,7 +1351,16 @@ def test_scheduler_cache_misses_after_todo_state_changes_without_run( "resume_at:2099-01-01T00:00:00Z", ) assert rc == 0, updated - assert updated["status"] == "deferred" + assert updated["status"] == "applied" + listed_rc, listed = _run_cli( + registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID + ) + assert listed_rc == 0, listed + deferred = next( + item for item in listed["todos"] if item["todo_id"] == TODO_ID + ) + assert deferred["status"] == "deferred" + assert deferred["resume_when"] == "resume_at:2099-01-01T00:00:00Z" assert (index_path.read_bytes() if index_path.exists() else None) == index_before rc, refreshed = _run_cli( @@ -1338,6 +1396,7 @@ def test_scheduler_cache_tracks_todo_changes_beyond_display_limit( state_path.read_text(encoding="utf-8").rstrip() + "\n" + tail_rows, encoding="utf-8", ) + _initialize_fixture_authority(project, runtime) guard = ( "quota", "should-run", @@ -2264,6 +2323,7 @@ def test_recovery_does_not_bind_current_replan_and_reenters_same_turn( # append-only heartbeat receipt. The same recovery Turn must then be able # to bind an independent successor instead of preserving an obsolete replan. state_path.write_text(state_before_replan, encoding="utf-8") + _initialize_fixture_authority(project, runtime) wait_rc, wait = _run_cli( registry_path, @@ -2284,9 +2344,13 @@ def test_recovery_does_not_bind_current_replan_and_reenters_same_turn( ALTERNATIVE_TODO_ID, ) assert wait_rc == 0, wait - assert wait["external_wait_transition"]["successor_todo_ids"] == [ - ALTERNATIVE_TODO_ID - ] + listed_rc, listed = _run_cli( + registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID + ) + assert listed_rc == 0, listed + waiting = next(item for item in listed["todos"] if item["todo_id"] == TODO_ID) + assert waiting["resume_when"] == f"monitor_changed:{DUE_MONITOR_TODO_ID}" + assert waiting["successor_todo_ids"] == [ALTERNATIVE_TODO_ID] current_turn_id = recovery["heartbeat_receipt"]["turn_instance_id"] resumed_rc, resumed = _run_cli( @@ -2462,7 +2526,7 @@ def test_prior_turn_with_several_receipts_recovers_once(tmp_path: Path) -> None: assert packet["prior_event_id"] == "multi-receipt-identity-upgrade" -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("hidden_count", [0, 6]) def test_prior_host_closeout_survives_hidden_todo_lifecycle( tmp_path: Path, @@ -2570,7 +2634,7 @@ def test_prior_host_closeout_survives_hidden_todo_lifecycle( assert resumed["quota"]["spent_slots"] == prior["quota"]["spent_slots"] -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_prior_host_closeout_reads_archived_exact_todo( tmp_path: Path, provider: str, @@ -2698,9 +2762,11 @@ def test_prior_host_closeout_reads_archived_exact_todo( def test_standard_codex_app_settlement_is_receipted_and_idempotent( tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_read_only_todo(project) + _initialize_fixture_authority(project, runtime) binding = ( "--agent-id", AGENT_ID, @@ -2924,6 +2990,11 @@ def test_standard_codex_app_settlement_is_receipted_and_idempotent( assert fresh_turn_rc == 0, fresh_turn assert fresh_turn["selected_todo"]["todo_id"] == successor_id fresh_ack_args = fresh_turn["scheduler_hint"]["app_automation"]["ack_hint"]["cli_args"] + codex_home = tmp_path / "codex-home" + monkeypatch.setenv("CODEX_HOME", str(codex_home)) + monkeypatch.setenv("CODEX_THREAD_ID", "settlement-cli-thread") + applied_rrule_index = fresh_ack_args.index("--applied-rrule") + 1 + _write_codex_heartbeat_rrule(codex_home, fresh_ack_args[applied_rrule_index]) fresh_ack_rc, fresh_ack = _run_cli(registry_path, runtime, *fresh_ack_args) assert fresh_ack_rc == 0, fresh_ack assert fresh_ack["schema_version"] == "loopx_scheduler_host_followup_result_v0" @@ -3118,6 +3189,7 @@ def test_same_turn_identityless_guard_upgrades_and_settles_full_chain( ), encoding="utf-8", ) + _initialize_fixture_authority(project, runtime) binding = ( "--agent-id", AGENT_ID, @@ -4980,6 +5052,7 @@ def test_pending_action_selection_does_not_preempt_newly_due_monitor( assert "settlement_identity" not in first["heartbeat_receipt"] _append_newly_due_monitor(project) + _initialize_fixture_authority(project, runtime) selected_rc, selected = _run_cli( registry_path, runtime, @@ -5157,6 +5230,7 @@ def test_pending_action_selection_can_bind_exact_newly_due_monitor( assert "settlement_identity" not in first["heartbeat_receipt"] _append_newly_due_monitor(project) + _initialize_fixture_authority(project, runtime) selected_rc, selected = _run_cli( registry_path, runtime, @@ -5239,6 +5313,7 @@ def test_prior_monitor_turn_accepts_exact_committed_receipt( ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _append_newly_due_monitor(project) + _initialize_fixture_authority(project, runtime) prior_turn_id = "turn-monitor-exact-closeout" guard_args = ( "quota", @@ -5314,6 +5389,7 @@ def test_prior_monitor_turn_recovery_rejects_wrong_identity_receipt( ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _append_newly_due_monitor(project) + _initialize_fixture_authority(project, runtime) prior_turn_id = "turn-monitor-wrong-identity" guard_args = ( "quota", @@ -5420,6 +5496,7 @@ def test_receipt_bound_advancement_turn_records_multiple_due_monitors( ("todo_fixture_monitor_gamma", "monitor-gamma"), ] _append_due_monitors(project, monitors) + _initialize_fixture_authority(project, runtime) turn_instance_id = "turn-multiple-auxiliary-monitors" guard_args = ( "quota", @@ -5589,7 +5666,9 @@ def test_pending_action_selection_does_not_commit_after_new_user_gate( @pytest.mark.parametrize("write_vision", [True, False], ids=["qualified-vision", "missing-vision"]) def test_todoless_autonomous_replan_settles_quota_refresh_spend_chain( - tmp_path: Path, write_vision: bool, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + write_vision: bool, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_autonomous_replan_fixture(project, runtime, registry_path) @@ -5605,6 +5684,7 @@ def test_todoless_autonomous_replan_settles_quota_refresh_spend_chain( json.dumps(registry, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) + _initialize_fixture_authority(project, runtime) turn_instance_id = "turn-autonomous-replan-settlement-1" guard_rc, guard = _run_cli( @@ -5646,6 +5726,14 @@ def test_todoless_autonomous_replan_settles_quota_refresh_spend_chain( assert original_scheduler_ack_args[:2] == ["quota", "scheduler-ack-current"] assert "--turn-instance-id" in original_scheduler_ack_args assert turn_instance_id in original_scheduler_ack_args + codex_home = tmp_path / "codex-home" + monkeypatch.setenv("CODEX_HOME", str(codex_home)) + monkeypatch.setenv("CODEX_THREAD_ID", "settlement-cli-thread") + applied_rrule_index = original_scheduler_ack_args.index("--applied-rrule") + 1 + _write_codex_heartbeat_rrule( + codex_home, + original_scheduler_ack_args[applied_rrule_index], + ) actions = cli_channel["next_cli_actions"] refresh_command = next(action for action in actions if "refresh-state" in action) spend_command = next(action for action in actions if "spend-slot" in action) @@ -6501,6 +6589,7 @@ def test_same_turn_receipt_replay_defers_newly_due_higher_priority_monitor( assert first["heartbeat_receipt"]["settlement_identity"]["todo_id"] == TODO_ID _append_newly_due_monitor(project) + _initialize_fixture_authority(project, runtime) capability_args = ( "--available-capability", "network", @@ -6656,7 +6745,7 @@ def test_same_turn_receipt_replay_defers_newly_due_higher_priority_monitor( assert resumed_turn["selected_todo"]["todo_id"] == DUE_MONITOR_TODO_ID -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_read_only_settlement_omits_non_causal_delivery_workspace( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -6669,10 +6758,9 @@ def test_read_only_settlement_omits_non_causal_delivery_workspace( def frozen_completion_clock(argv, *args, **kwargs): if isinstance(argv, list) and argv[1:3] == ["-m", "loopx.cli"]: program = ( - "import loopx.control_plane.todos.legacy_mutation as legacy; " "import loopx.control_plane.todos.provider_terminal_lifecycle as native; " "clock=lambda:'2026-09-02T12:00:00+00:00'; " - "legacy.now_local=clock; native.now_local=clock; " + "native.now_local=clock; " "from loopx.cli import main; raise SystemExit(main())" ) argv = [argv[0], "-c", program, *argv[3:]] @@ -6978,6 +7066,7 @@ def test_same_turn_terminal_receipt_replay_preempts_autonomous_replan( ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_read_only_todo(project) + _initialize_fixture_authority(project, runtime) guard_args = ( "quota", "should-run", @@ -7087,6 +7176,7 @@ def test_settled_turn_defers_prior_unsettled_history_to_fresh_turn( ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_selectable_alternative(project) + _initialize_fixture_authority(project, runtime) guard_args = ( "quota", "should-run", @@ -7221,122 +7311,3 @@ def test_settled_turn_defers_prior_unsettled_history_to_fresh_turn( assert fresh["unsettled_host_turn_recovery"]["binding_id"] == ( ALTERNATIVE_TODO_ID ) - - -def test_legacy_read_only_workspace_mismatch_fails_then_corrects_from_todo_contract( - tmp_path: Path, -) -> None: - project, runtime, registry_path = _write_fixture(tmp_path) - state_path = _configure_read_only_todo(project) - _initialize_git_checkout(project) - binding = ( - "--agent-id", - AGENT_ID, - "--todo-id", - TODO_ID, - "--turn-instance-id", - TURN_ID, - ) - - guard_rc, guard = _run_cli( - registry_path, - runtime, - "quota", - "should-run", - "--codex-app", - "--goal-id", - GOAL_ID, - "--agent-id", - AGENT_ID, - "--turn-instance-id", - TURN_ID, - "--scan-path", - str(project), - ) - assert guard_rc == 0, guard - _strip_heartbeat_workspace_causality(runtime) - - complete_rc, complete = _run_cli( - registry_path, - runtime, - "todo", - "complete", - "--goal-id", - GOAL_ID, - "--todo-id", - TODO_ID, - "--agent-id", - AGENT_ID, - "--claimed-by", - AGENT_ID, - "--evidence", - "legacy read-only characterization validated", - "--no-follow-up", - ) - assert complete_rc == 0, complete - - refresh_rc, refresh = _run_cli( - registry_path, - runtime, - "refresh-state", - "--goal-id", - GOAL_ID, - "--classification", - "legacy_read_only_characterization_complete", - "--delivery-batch-scale", - "single_surface", - "--delivery-outcome", - "outcome_progress", - "--delivery-workspace-path", - str(project), - *binding, - "--no-global-sync", - "--suppress-external-sinks", - ) - assert refresh_rc == 0, refresh - assert refresh["delivery_workspace"]["workspace_kind"] == "canonical_checkout" - - causal_state = state_path.read_text(encoding="utf-8") - state_path.write_text( - causal_state.replace( - "continuation_policy=same_agent_non_delivery " - "required_capabilities=shell%2Cfilesystem_read ", - "", - ), - encoding="utf-8", - ) - spend_args = ( - "quota", - "spend-slot", - "--goal-id", - GOAL_ID, - "--slots", - "1", - "--source", - "heartbeat", - "--execute", - *binding, - "--scan-path", - str(project), - ) - failed_rc, failed = _run_cli(registry_path, runtime, *spend_args) - assert failed_rc == 1, failed - assert failed["workspace_guard"]["current_workspace"] == "foreign_git_worktree" - assert _spend_run_count(runtime) == 0 - - state_path.write_text(causal_state, encoding="utf-8") - corrected_rc, corrected = _run_cli(registry_path, runtime, *spend_args) - replay_rc, replay = _run_cli(registry_path, runtime, *spend_args) - - assert corrected_rc == 0, corrected - assert corrected["delivery_workspace_causality"]["source"] == ( - "completed_todo_contract_fallback" - ) - assert corrected["delivery_workspace_causality"]["requirement"] == ("not_required") - assert corrected["delivery_workspace_validated"] is False - assert replay_rc == 0, replay - assert replay["idempotent_replay"] is True - assert [ - receipt["step_kind"] for receipt in replay["settlement_result"]["receipts"] - ] == ["validation", "durable_writeback", "quota_spend"] - assert _spend_run_count(runtime) == 1 diff --git a/tests/control_plane/test_recovery_due_successor_identity.py b/tests/control_plane/test_recovery_due_successor_identity.py index 804754a2ae..9ab8e6a342 100644 --- a/tests/control_plane/test_recovery_due_successor_identity.py +++ b/tests/control_plane/test_recovery_due_successor_identity.py @@ -29,7 +29,7 @@ ) -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("crowded", [False, True]) def test_interrupted_binding_survives_new_due_successor( tmp_path: Path, @@ -101,30 +101,29 @@ def cli(*args: str) -> tuple[int, dict]: ) ) - if provider != "legacy": - rc, listed = cli("todo", "list", "--goal-id", GOAL_ID, "--limit", "200") - assert rc == 0, listed - initialize_canonical_authority( - runtime, - GOAL_ID, - build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, - todos=[ - canonical_todo_read_record( - structured_todo_item( - row, - role="agent", - source_section="Agent Todo", - ) + rc, listed = cli("todo", "list", "--goal-id", GOAL_ID, "--limit", "200") + assert rc == 0, listed + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[ + canonical_todo_read_record( + structured_todo_item( + row, + role="agent", + source_section="Agent Todo", ) - for row in listed["todos"] - ], - handoff_mode="hard_lease", - leases=[], - ), - state_path=state, - provider=provider, - ) + ) + for row in listed["todos"] + ], + handoff_mode="hard_lease", + leases=[], + ), + state_path=state, + provider=provider, + ) old_turn, new_turn = "interrupted-original", "current-after-interruption" old_binding = ( @@ -158,7 +157,8 @@ def cli(*args: str) -> tuple[int, dict]: GOAL_ID, "--role", "agent", - *(("--operation-id", "add-new-due-successor") if provider != "legacy" else ()), + "--operation-id", + "add-new-due-successor", "--text", "[P0] Evaluate the fixed deadline once runnable.", "--priority", @@ -233,9 +233,6 @@ def cli(*args: str) -> tuple[int, dict]: assert peer["heartbeat_receipt"]["status"] == "not_committed" assert _spend_run_count(runtime) == 0 - if provider == "legacy": - # A legacy writer has no promoted Turn-owned retry closeout. - return before = state.read_bytes() refresh_args = ( "refresh-state", diff --git a/tests/control_plane/test_refresh_state_replan_gate.py b/tests/control_plane/test_refresh_state_replan_gate.py index b251bae03c..ccbec53bce 100644 --- a/tests/control_plane/test_refresh_state_replan_gate.py +++ b/tests/control_plane/test_refresh_state_replan_gate.py @@ -1042,6 +1042,21 @@ def test_todo_complete_then_refresh_cli_projects_direct_settlement( ), encoding="utf-8", ) + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + + initialize_canonical_authority( + runtime_root, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[], + handoff_mode="soft_claim", + ), + state_path=state, + ) assert cli_main( [ "--registry", diff --git a/tests/control_plane/test_replan_completion_validation.py b/tests/control_plane/test_replan_completion_validation.py index 9bf87993b2..5622ff6d34 100644 --- a/tests/control_plane/test_replan_completion_validation.py +++ b/tests/control_plane/test_replan_completion_validation.py @@ -29,6 +29,7 @@ TODO_ID, _configure_completion_validation_todo, _configure_selectable_alternative, + _initialize_fixture_authority, _run_cli, _spend_run_count, _write_fixture, @@ -78,6 +79,16 @@ def test_qualified_path_replan_settles_without_completing_validation_todo( project, runtime, registry = _write_fixture(tmp_path) state_path = _configure_completion_validation_todo(project) _configure_selectable_alternative(project) + _initialize_fixture_authority(project, runtime) + from loopx.control_plane.todos.provider_projection import ( + project_current_canonical_todos, + ) + + project_current_canonical_todos( + registry_path=registry, + runtime_root=runtime, + goal_id=GOAL_ID, + ) initial_state = state_path.read_text(encoding="utf-8") turn_id = "turn-validation-bearing-replan" diff --git a/tests/control_plane/test_replan_semantic_action_behavior.py b/tests/control_plane/test_replan_semantic_action_behavior.py index bc168bf7dd..d6cb93fc76 100644 --- a/tests/control_plane/test_replan_semantic_action_behavior.py +++ b/tests/control_plane/test_replan_semantic_action_behavior.py @@ -394,7 +394,7 @@ def test_model_exits_after_replan_confirms_goal_coverage_is_exhausted( assert receipt["semantic_reentry"] == { "decision": "skip", "effective_action": "monitor_quiet_skip", - "replan_rule": "future_monitor_wait", + "replan_rule": "terminal_coverage_exhausted", "replan_closed": True, "exited": True, } @@ -715,6 +715,20 @@ def test_successor_dry_run_shares_quota_agent_scope_for_user_gates( fixture.project_root / ".loopx" / "registry.json", ): registry_path.write_text(registry_text, encoding="utf-8") + from loopx.todos import add_goal_todo + + added = add_goal_todo( + registry_path=fixture.global_registry_path, + runtime_root_arg=str(fixture.runtime_root), + goal_id="replan-semantic-action-fixture", + role="user", + text="Review the other agent's independent delivery.", + priority="P0", + task_class="user_action", + bound_agent=other_agent_id, + operation_id="fixture-other-agent-review", + ) + assert added["added"] is True state_path = ( fixture.project_root / ".codex" @@ -722,18 +736,6 @@ def test_successor_dry_run_shares_quota_agent_scope_for_user_gates( / "replan-semantic-action-fixture" / "ACTIVE_GOAL_STATE.md" ) - state_text = state_path.read_text(encoding="utf-8").replace( - "## Agent Todo\n", - ( - "## User Todo / Owner Review Reading Queue\n\n" - "- [ ] [P0] Review the other agent's independent delivery.\n" - " \n\n" - "## Agent Todo\n" - ), - ) - state_path.write_text(state_text, encoding="utf-8") quota = json.loads( _execute_loopx( diff --git a/tests/control_plane/test_replan_successor_durable_ack.py b/tests/control_plane/test_replan_successor_durable_ack.py index 703b15b7a6..3a63f18b29 100644 --- a/tests/control_plane/test_replan_successor_durable_ack.py +++ b/tests/control_plane/test_replan_successor_durable_ack.py @@ -40,6 +40,11 @@ def successor_state(obligation_id: str, *, owner: str = AGENT) -> str: def test_cli_successor_refresh_resets_periodic_window(tmp_path: Path, capsys) -> None: + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + project = tmp_path / "project" project.mkdir() state = project / "ACTIVE_GOAL_STATE.md" @@ -56,6 +61,16 @@ def test_cli_successor_refresh_resets_periodic_window(tmp_path: Path, capsys) -> "execution_profile": {"replan_after_completed_todos": 1}, "coordination": {"agent_model": "peer_v1", "registered_agents": [AGENT]}, }]})) + initialize_canonical_authority( + runtime, + GOAL, + build_todo_runtime_shadow_projection( + goal_id=GOAL, + todos=[], + handoff_mode="soft_claim", + ), + state_path=state, + ) obligation = autonomous_replan_obligation_from_runs(runs, agent_todos={}, agent_id=AGENT) assert obligation is not None common = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] @@ -261,11 +276,19 @@ def call(*args: str, expected_error: str | None = None) -> dict: expected_error="controller-declared completion validation") -@pytest.mark.parametrize("route", ["writeback", "successor", "canonical-successor"]) +@pytest.mark.parametrize("route", ["writeback", "successor"]) def test_long_chain_ack_survives_real_cli_history_and_peer_claim(tmp_path: Path, route: str) -> None: import subprocess import sys + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + from loopx.control_plane.todos.active_state_todo_parser import ( + parse_active_state_todos, + ) + project = tmp_path / "project" project.mkdir() runtime = tmp_path / "runtime" @@ -290,17 +313,20 @@ def test_long_chain_ack_survives_real_cli_history_and_peer_claim(tmp_path: Path, "coordination": {"agent_model": "peer_v1", "registered_agents": [AGENT, "peer-agent"]}, }]})) - if route == "canonical-successor": - from canonical_authority_fixture import initialize_canonical_authority - from loopx.control_plane.todos.active_state_todo_parser import parse_active_state_todos - todos = parse_active_state_todos(state.read_text(), item_limit=None)["agent_todos"]["items"] - module = (Path(__file__).resolve().parents[2] / "tests/control_plane_ts/authority_projection_fixture.ts").as_uri() - built = subprocess.run(["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", - f"import {{authorityProjectionFixture}} from {json.dumps(module)};" - "let s='';for await(const c of process.stdin)s+=c;" - "process.stdout.write(JSON.stringify(authorityProjectionFixture(process.argv[1],JSON.parse(s),[], 'legacy')));", GOAL], - input=json.dumps(todos), capture_output=True, text=True, check=True, timeout=30) - initialize_canonical_authority(runtime, GOAL, json.loads(built.stdout), state_path=state) + todos = parse_active_state_todos( + state.read_text(), + item_limit=None, + )["agent_todos"]["items"] + initialize_canonical_authority( + runtime, + GOAL, + build_todo_runtime_shadow_projection( + goal_id=GOAL, + todos=todos, + handoff_mode="soft_claim", + ), + state_path=state, + ) def call(*args): completed = subprocess.run([sys.executable, "-m", "loopx.cli", "--registry", str(registry), diff --git a/tests/control_plane/test_retired_todo_event_source.py b/tests/control_plane/test_retired_todo_event_source.py index c2ac09760d..23d16fcaef 100644 --- a/tests/control_plane/test_retired_todo_event_source.py +++ b/tests/control_plane/test_retired_todo_event_source.py @@ -11,7 +11,10 @@ import pytest from loopx.control_plane.goals.legacy_event_source import ( - RetiredTodoEventSourceError, require_no_legacy_todo_events, + require_no_legacy_todo_events, +) +from loopx.control_plane.coordination.local_authority import ( + CanonicalAuthorityRequired, ) from loopx.control_plane.testing.canary_harness import run_json_cli_result from loopx.control_plane.todos import completion_validation @@ -59,28 +62,44 @@ def test_all_old_source_selectors_refuse_reads_and_writes_without_data_loss(tmp_ registry = tmp_path / "registry.json" registry.write_text(json.dumps({"common_runtime_root": str(tmp_path / "runtime"), "goals": [goal]})) before = state.read_bytes(), old.read_bytes() - for args in [("todo", "list"), - ("shared-goal-alignment", "--agent-id", "agent-a", "--project", str(tmp_path)), ("todo", "add", "--text", "New work", "--role", "agent"), - ("todo", "complete", "--todo-id", "todo_existing", "--evidence", "validated")]: + for args in [ + ("todo", "list"), + ("shared-goal-alignment", "--agent-id", "agent-a", "--project", str(tmp_path)), + ]: code, result = run_json_cli_result(*args, "--goal-id", goal["id"], registry_path=registry) assert code != 0 or result.get("ok") is False, result assert "legacy_todo_event_source_retired" in json.dumps(result), result assert (state.read_bytes(), old.read_bytes()) == before + for args in [ + ("todo", "add", "--text", "New work", "--role", "agent"), + ("todo", "complete", "--todo-id", "todo_existing", "--evidence", "validated"), + ]: + code, result = run_json_cli_result(*args, "--goal-id", goal["id"], registry_path=registry) + assert code != 0 or result.get("ok") is False, result + assert result["error_code"] == "canonical_authority_required" + assert result["write_admitted"] is False + assert result["legacy_fallback_used"] is False + assert (state.read_bytes(), old.read_bytes()) == before -def test_completion_rejects_before_any_validation_effect(tmp_path, monkeypatch): +def test_completion_requires_canonical_authority_before_any_validation_effect(tmp_path, monkeypatch): state = tmp_path / "ACTIVE_GOAL_STATE.md" state.write_text("## Agent Todo\n- [ ] [P1] Work\n" " \n") - state.with_name("events.jsonl").write_text("retired\n") + old = state.with_name("events.jsonl") + old.write_text("retired\n") registry = tmp_path / "registry.json" registry.write_text(json.dumps({"goals": [{"id": "retired-fixture", "repo": str(tmp_path), "state_file": state.name}]})) def forbidden(**kwargs): raise AssertionError("a retired source executed a completion effect") monkeypatch.setattr(completion_validation, "run_declared_completion_validation_effect", forbidden) - with pytest.raises(RetiredTodoEventSourceError): + before = state.read_bytes(), old.read_bytes() + with pytest.raises(CanonicalAuthorityRequired) as error: complete_goal_todo(registry_path=registry, goal_id="retired-fixture", todo_id="todo_existing", evidence="validated", no_followup=True) + assert error.value.code == "canonical_authority_required" + assert error.value.payload["operation"] == "todo_terminal" + assert (state.read_bytes(), old.read_bytes()) == before def test_empty_file_is_not_an_event_authority_and_is_preserved(tmp_path): @@ -158,7 +177,9 @@ def test_retired_source_is_scoped_and_later_healthy_goal_remains_visible(tmp_pat code, mutation = run_json_cli_result("todo", "add", "--goal-id", "retired", "--role", "agent", "--text", "Must refuse", registry_path=registry) assert code != 0 or mutation.get("ok") is False - assert "legacy_todo_event_source_retired" in json.dumps(mutation) + assert mutation["error_code"] == "canonical_authority_required" + assert mutation["write_admitted"] is False + assert mutation["legacy_fallback_used"] is False assert old.read_bytes() == before old.unlink() # Explicit owner recovery in a disposable synthetic fixture. code, recovered = run_json_cli_result("status", registry_path=registry) diff --git a/tests/control_plane/test_reviewed_canonical_edits.py b/tests/control_plane/test_reviewed_canonical_edits.py index 3759a23a1a..53fed908ee 100644 --- a/tests/control_plane/test_reviewed_canonical_edits.py +++ b/tests/control_plane/test_reviewed_canonical_edits.py @@ -15,7 +15,7 @@ def service_fixture(tmp_path: Path, provider: str = "file"): - registry, state = fixture(tmp_path, True, provider) + registry, state = fixture(tmp_path, provider) service = ChatActionService( store=ChatActionStore(tmp_path / "actions"), registry_path=registry, ) @@ -185,20 +185,8 @@ def test_monitor_preview_and_apply_share_the_real_update(tmp_path, operation, pr "normalized_parameters": {**parameters, "agent_id": "agent-b"}}) -def test_unpromoted_review_keeps_legacy_shape_and_freshness(tmp_path): - registry, state = fixture(tmp_path, False) - service = ChatActionService(store=ChatActionStore(tmp_path / "actions"), registry_path=registry) - proposal = preview(service) - assert "canonical_update_basis" not in proposal - state.write_text(state.read_text() + "\nOwner changed the source.\n") - before = state.read_bytes() - assert service.apply(proposal["proposal_id"])["proposal"]["status"] == "stale" - assert state.read_bytes() == before - - -@pytest.mark.parametrize("promoted", [False, True]) -def test_update_basis_cannot_be_silently_discarded_by_claim(tmp_path, promoted): - registry, _state = fixture(tmp_path, promoted) +def test_update_basis_cannot_be_silently_discarded_by_claim(tmp_path): + registry, _state = fixture(tmp_path) before = records(registry) with pytest.raises(ValueError, match="cannot be used for todo claim"): update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id="todo_target", diff --git a/tests/control_plane/test_reviewed_promotion_cli.py b/tests/control_plane/test_reviewed_promotion_cli.py deleted file mode 100644 index 63820a886f..0000000000 --- a/tests/control_plane/test_reviewed_promotion_cli.py +++ /dev/null @@ -1,172 +0,0 @@ -"""Real public CLI cutover, registration drift and receipt recovery on local stores.""" - -from __future__ import annotations - -import json -import os -from pathlib import Path -import subprocess - -import pytest - -from tests.control_plane.shadow_e2e_fixture import REPO, workspace - - -def prepare(tmp_path: Path, provider: str, strategy: str | None = None, source_mode: str | None = None): - ws = workspace(tmp_path, bootstrap=False) - if strategy is not None or source_mode is not None: - ws.state.write_text( - ws.state.read_text().replace( - "handoff_mode: hard_lease", f"handoff_mode: {source_mode or 'legacy'}" - ) - ) - if provider == "sqlite": - subprocess.run( - [ - os.environ.get("LOOPX_CONTROL_PLANE_NODE", "node"), - "--no-warnings", - "--experimental-strip-types", - "--experimental-sqlite", - "loopx/control_plane/coordination/local_authority_provider.ts", - "--runtime-root", - str(ws.runtime), - "--goal-id", - ws.goal, - "--execute", - ], - cwd=REPO, - check=True, - capture_output=True, - text=True, - ) - assert ( - ws.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] - == "applied" - ) - for index in range(3): - ws.add(f"Preserve migration record {index}") - drained = ws.drain(budget_seconds="60") - assert drained["ok"] is True, drained - arguments = () if strategy is None else ("--handoff-mode-migration", strategy) - preview = ws.cli("coordination-shadow", "promote", *arguments) - assert preview["promotion"]["status"] == "preview_ready", preview - saved = tmp_path / "reviewed.json" - saved.write_text(json.dumps(preview), encoding="utf-8") - return ws, saved, preview - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("strategy", [None, "preserve", "hard_lease"]) -def test_saved_plan_cutover_and_recovery_after_canonical_write_and_missing_legacy( - tmp_path, provider, strategy -): - ws, saved, preview = prepare(tmp_path, provider, strategy) - arguments = ("coordination-shadow", "promote", "--reviewed-plan", str(saved)) - dry_run = ws.cli(*arguments) - assert dry_run["promotion"]["status"] == "preview_ready" - assert dry_run["executed"] is False - applied = ws.cli(*arguments, "--execute") - assert applied["promotion"]["status"] == "applied", applied - assert applied["promotion"]["canonical_authority"] == f"{provider}_v0" - assert ( - applied["promotion"]["promotion_plan_sha256"] - == preview["promotion"]["plan"]["promotion_plan_sha256"] - ) - created = ws.add("Continue after provider cutover") - readback = ws.cli("todo", "list", "--todo-id", created["todo_id"]) - assert readback["authority_read"]["source_authority"] == f"{provider}_v0" - ws.state.unlink() - # Recovery follows durable proof, not a fresh source or shadow opt-in. - config = json.loads(ws.registry.read_text()) - config["goals"][0]["coordination"].pop("runtime_shadow", None) - ws.registry.write_text(json.dumps(config)) - for execution in [(), ("--execute",)]: - replay = ws.cli( - "coordination-shadow", - "recover-promotion", - "--reviewed-plan", - str(saved), - *execution, - ) - assert replay["promotion"]["status"] == "replayed", replay - assert replay["promotion"]["cursor"] == "1" - assert ( - replay["promotion"]["provider_revision"] - == applied["promotion"]["provider_revision"] - ) - assert replay["executed"] is False - assert not ws.state.exists() - - -def test_saved_plan_source_drift_does_not_freeze_legacy_writes(tmp_path): - ws, saved, _ = prepare(tmp_path, "file") - ws.add("New work before cutover") - result = ws.cli( - "coordination-shadow", - "promote", - "--reviewed-plan", - str(saved), - "--execute", - success=False, - ) - assert result["ok"] is False - assert result["promotion"]["reason_code"] == "local_authority_reviewed_plan_changed" - assert result["promotion"]["legacy_writer_fenced"] is False - assert ws.add("Legacy writer remains usable")["ok"] is True - - -def test_saved_plan_rejects_policy_override_and_recovery_without_fence(tmp_path): - ws, saved, _ = prepare(tmp_path, "file") - result = ws.cli( - "coordination-shadow", - "promote", - "--reviewed-plan", - str(saved), - "--minimum-operations", - "1", - "--execute", - success=False, - ) - assert result["ok"] is False and "owns qualification policy" in result["error"] - result = ws.cli( - "coordination-shadow", - "recover-promotion", - "--reviewed-plan", - str(saved), - "--execute", - success=False, - ) - assert result["ok"] is False - assert ( - result["promotion"]["reason_code"] - == "local_authority_writer_fence_not_verified" - ) - - -def test_saved_plan_rechecks_current_registered_agents(tmp_path): - ws, saved, _ = prepare(tmp_path, "file", "preserve") - registry = json.loads(ws.registry.read_text()) - registry["goals"][0]["coordination"]["registered_agents"].remove("agent-b") - ws.registry.write_text(json.dumps(registry)) - result = ws.cli( - "coordination-shadow", - "promote", - "--reviewed-plan", - str(saved), - "--execute", - success=False, - ) - assert result["ok"] is False - assert result["promotion"]["reason_code"] == "promotion_registration_changed_retry" - assert result["promotion"]["legacy_writer_fenced"] is False - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_default_promotion_preserves_soft_policy(tmp_path, provider): - ws, saved, preview = prepare(tmp_path, provider, source_mode="soft_claim") - policy = preview["promotion"]["plan"]["handoff_mode_migration"] - assert policy["strategy"] == "preserve" and policy["target_mode"] == "soft_claim" - applied = ws.cli("coordination-shadow", "promote", "--reviewed-plan", str(saved), "--execute") - assert applied["ok"], applied - shown = ws.cli("handoff-mode", "show") - assert shown["handoff_mode"] == "soft_claim" diff --git a/tests/control_plane/test_reviewed_terminal_actions.py b/tests/control_plane/test_reviewed_terminal_actions.py index 0116b2b097..3b3348b159 100644 --- a/tests/control_plane/test_reviewed_terminal_actions.py +++ b/tests/control_plane/test_reviewed_terminal_actions.py @@ -24,15 +24,24 @@ def fixture(tmp_path: Path, provider: str, operation: str, *, validation: str | registry.write_text(json.dumps({"schema_version": 1, "common_runtime_root": str(tmp_path / "runtime"), "goals": [{"id": "goal-a", "status": "active", "repo": str(project), "state_file": state.name, "coordination": {"registered_agents": ["agent-a"]}}]})) + projection = build_todo_runtime_shadow_projection( + goal_id="goal-a", + todos=[], + handoff_mode="soft_claim", + ) + initialize_canonical_authority( + tmp_path / "runtime", + "goal-a", + projection, + state_path=state, + provider=provider, + ) monitor = operation == "stop" added = add_goal_todo(registry_path=registry, goal_id="goal-a", role="agent", text="Observe the supported release" if monitor else "Deliver the bounded result", task_class="continuous_monitor" if monitor else "advancement_task", claimed_by="agent-a", validation_command=validation, **({"monitor_metadata": {"target_key": "release-status", "cadence": "1d", "watch_only": "true"}} if monitor else {})) - todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] - projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=todos, handoff_mode="soft_claim") - initialize_canonical_authority(tmp_path / "runtime", "goal-a", projection, state_path=state, provider=provider) service = ChatActionService(store=ChatActionStore(tmp_path / "actions"), registry_path=registry) proposal = service.preview({"action_kind": "monitor.update" if monitor else "todo.update", "summary": "Close the reviewed work", "normalized_parameters": {"goal_id": "goal-a", diff --git a/tests/control_plane/test_runtime_shadow_bounded_e2e.py b/tests/control_plane/test_runtime_shadow_bounded_e2e.py deleted file mode 100644 index 61762edbe8..0000000000 --- a/tests/control_plane/test_runtime_shadow_bounded_e2e.py +++ /dev/null @@ -1,595 +0,0 @@ -"""Real public CLI and independent native TypeScript file-profile qualification. - -The production writers, file provider and parsers run unchanged. Tests control -only process scheduling and deliberately edited source bytes. -""" -from __future__ import annotations - -import hashlib -import json -import os -import subprocess -import sys -from pathlib import Path - -import pytest - -from loopx.control_plane.effect_runtime import EffectRuntimeRejected - -from loopx.control_plane.coordination.coordination_state_contract_generated import ( - LOCAL_AUTHORITY_SHADOW_COMMIT_ENTRY_REQUEST_SCHEMA, - TASK_LEASE_ACQUIRE_REQUEST_SCHEMA, -) -from loopx.control_plane.coordination.runtime_shadow import ( - build_runtime_shadow_source_snapshot, -) -from loopx.control_plane.work_items.task_lease_acquire_adapter import ( - task_lease_acquire_authority_facts, -) - -REPO = Path(__file__).resolve().parents[2] - - -pytestmark = pytest.mark.stage2c_e2e - - -def workspace(tmp_path: Path) -> tuple[Path, Path, Path]: - state = tmp_path / "ACTIVE_GOAL_STATE.md" - state.write_text("---\ngoal_id: goal-a\nhandoff_mode: hard_lease\n---\n\n## Agent Todo\n\n", encoding="utf-8") - runtime = tmp_path / "runtime" - registry = tmp_path / "registry.json" - registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{ - "id": "goal-a", "status": "active", "repo": str(tmp_path), "state_file": state.name, - "coordination": {"agent_model": "peer_v1", "registered_agents": ["agent-a", "agent-b"], - "runtime_shadow": {"schema_version": "loopx_coordination_runtime_shadow_config_v0", "enabled": False, "provider": "file_v0"}}, - }]}), encoding="utf-8") - return registry, runtime, state - - -def cli(registry: Path, runtime: Path, *arguments: str, success: bool = True) -> dict: - completed = subprocess.run([sys.executable, "-m", "loopx.cli", "--registry", str(registry), - "--runtime-root", str(runtime), "--format", "json", *arguments], cwd=REPO, - env={**os.environ, "PYTHONPATH": str(REPO)}, capture_output=True, text=True, - timeout=45, check=False) - assert completed.stdout.strip(), completed.stderr - payload = json.loads(completed.stdout) - if success: - assert completed.returncode == 0, completed.stderr + "\n" + json.dumps(payload, indent=2) - assert payload.get("ok") is True, payload - return payload - - -def enable(registry: Path) -> dict: - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"]["enabled"] = True - registry.write_text(json.dumps(value), encoding="utf-8") - return value["goals"][0] - - -def native(tmp_path: Path, module: str, function: str, request: dict) -> dict: - path = tmp_path / "native-request.json" - path.write_text(json.dumps(request), encoding="utf-8") - script = (f"import {{ {function} }} from {json.dumps((REPO / module).as_uri())};" - "import {readFile} from 'node:fs/promises';" - f"process.stdout.write(JSON.stringify(await {function}(JSON.parse(await readFile(process.argv[1],'utf8')))));" ) - process = subprocess.run(["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", script, str(path)], - cwd=tmp_path, capture_output=True, text=True, timeout=45, check=False) - assert process.returncode == 0, process.stderr - return json.loads(process.stdout) - - -def history(tmp_path: Path, runtime: Path) -> list[dict]: - result = native(tmp_path, "loopx/control_plane/coordination/local_authority_shadow.ts", "readLocalAuthorityShadow", { - "schema_version": "loopx_coordination_runtime_shadow_outbox_read_v0", "runtime_root": str(runtime), - "goal_id": "goal-a", "scan_limit": 10000, - }) - assert result["status"] == "loaded", result - return result["proof"]["transactions"] - - -def acquire_native(tmp_path: Path, registry: Path, runtime: Path, todo_id: str) -> dict: - return native(tmp_path, "loopx/control_plane/work_items/task_lease_acquire.ts", "executeTaskLeaseAcquire", { - "schema_version": TASK_LEASE_ACQUIRE_REQUEST_SCHEMA, "runtime_root": str(runtime), "goal_id": "goal-a", - "todo_id": todo_id, "owner": "agent-b", "idempotency_key": "native-" + todo_id, "ttl_seconds": 120, - "write_scopes": [], "expected_version": None, - "authority": task_lease_acquire_authority_facts(registry_path=registry, goal_id="goal-a", todo_id=todo_id), - "runtime_shadow": {"schema_version": "loopx_coordination_runtime_shadow_binding_v0", "provider": "file_v0"}, - }) - - -def test_source_snapshot_preserves_inventory_without_projecting_orphan_leases( - tmp_path: Path, -) -> None: - registry, runtime, state = workspace(tmp_path) - directory = runtime / "goals" / "goal-a" / "task-leases" - directory.mkdir(parents=True) - # Imported baseline files use the snapshot contract's ASCII filename range. - # Their order is independent of locale and filesystem enumeration order. - for todo_id in ("todo_alpha", "todo_Zulu", "todo_Bravo"): - (directory / f"{todo_id}.json").write_text(json.dumps({ - "schema_version": "task_lease_v0", "goal_id": "goal-a", "todo_id": todo_id, - "owner": "agent-a", "status": "released", "version": 1, - "write_scopes": ["src/"], "idempotency_key": f"baseline-{todo_id}", - }), encoding="utf-8") - goal = enable(registry) - projection, snapshot = build_runtime_shadow_source_snapshot( - goal=goal, runtime_root=runtime, state_path=state, registry_path=registry) - expected = ["todo_Bravo", "todo_Zulu", "todo_alpha"] - assert [entry["name"] for entry in snapshot["lease_inventory"]] == [f"{name}.json" for name in expected] - assert projection["leases"] == [] - boot = cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - assert boot["bootstrap"]["status"] == "applied", boot - inspected = cli(registry, runtime, "coordination-shadow", "inspect", "--goal-id", "goal-a") - assert inspected["inspection"]["status"] == "matched", inspected - assert history(tmp_path, runtime)[0]["projection"]["leases"] == [] - - -def test_public_cli_and_independent_native_writer_qualify_one_complete_lineage(tmp_path: Path) -> None: - registry, runtime, state = workspace(tmp_path) - cli(registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "soft_claim") - archived = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Previously completed task", "--claimed-by", "agent-a") - cli(registry, runtime, "todo", "complete", "--goal-id", "goal-a", "--todo-id", archived["todo_id"], - "--agent-id", "agent-a", "--evidence", "validation://completed", "--no-follow-up") - cli(registry, runtime, "todo", "archive-completed", "--goal-id", "goal-a", "--max-active-done", "0", "--execute") - cli(registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "hard_lease") - first = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Existing first task") - second = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Existing second task") - baseline_lease = cli(registry, runtime, "task-lease", "acquire", "--goal-id", "goal-a", "--todo-id", first["todo_id"], - "--owner", "agent-a", "--idempotency-key", "baseline-lease", "--ttl-seconds", "120") - assert not (runtime / "authority-shadow" / "file-v0").exists() - enable(registry) - boot = cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - assert boot["projection_summary"] == {"todo_count": 2, "lease_count": 1} - assert boot["bootstrap"]["cursor"] == "1" - native_request = { - "schema_version": TASK_LEASE_ACQUIRE_REQUEST_SCHEMA, "runtime_root": str(runtime), "goal_id": "goal-a", - "todo_id": second["todo_id"], "owner": "agent-b", "idempotency_key": "native-second", "ttl_seconds": 120, - "write_scopes": [], "expected_version": None, - "authority": task_lease_acquire_authority_facts(registry_path=registry, goal_id="goal-a", todo_id=second["todo_id"]), - "runtime_shadow": {"schema_version": "loopx_coordination_runtime_shadow_binding_v0", "provider": "file_v0"}, - } - acquired = native(tmp_path, "loopx/control_plane/work_items/task_lease_acquire.ts", "executeTaskLeaseAcquire", native_request) - assert acquired["acquired"] is True, acquired - additions = [cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", f"New captured task {index}") for index in range(2)] - for addition in additions: - assert addition["coordination_runtime_shadow"]["source_transaction_correlated"] is True - inspect = cli(registry, runtime, "coordination-shadow", "inspect", "--goal-id", "goal-a") - assert inspect["inspection"]["status"] == "matched", inspect - qualified = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", "--minimum-operations", "3", - "--require-event-kind", "todo_add", "--require-event-kind", "task_lease_acquire") - assert qualified["qualification"]["scope"] == "bounded" - assert qualified["qualification"]["sustained_parity_verified"] is False - assert qualified["qualification"]["sustained_parity_verdict"] == "not_evaluated" - assert qualified["qualification"]["evidence"]["operation_count"] == 3 - assert qualified["qualification"]["cursor"] == "4" - read = cli(registry, runtime, "coordination-shadow", "read-candidate", "--goal-id", "goal-a", "--todo-id", first["todo_id"]) - assert read["read_candidate"]["read_candidate_qualified"] is True - assert read["read_candidate"]["decision_read_from_shadow"] is False - proof = native(tmp_path, "loopx/control_plane/coordination/local_authority_shadow.ts", "readLocalAuthorityShadow", { - "schema_version": "loopx_coordination_runtime_shadow_outbox_read_v0", "runtime_root": str(runtime), - "goal_id": "goal-a", "scan_limit": 10000, - }) - transactions = proof["proof"]["transactions"] - assert len(transactions) == 4 - assert transactions[0]["receipts"] == [] - assert transactions[0]["projection"]["leases"] == [baseline_lease["lease"]] - baseline_ids = [todo["todo_id"] for todo in transactions[0]["projection"]["todos"]] - assert baseline_ids == sorted([first["todo_id"], second["todo_id"]]) - assert archived["todo_id"] not in baseline_ids - assert "Previously completed task" in state.read_text() - receipts = [transaction["receipts"][0] for transaction in transactions[1:]] - assert {receipt["writer_runtime"] for receipt in receipts} == {"python", "typescript"} - assert len({receipt["entry_id"] for receipt in receipts}) == 3 - assert all(receipt["capture_lineage_id"] == boot["bootstrap"]["capture_lineage_id"] for receipt in receipts) - - -def test_reviewed_promotion_survives_restart_and_enables_managed_codex_preflight( - tmp_path: Path, -) -> None: - registry, runtime, _state = workspace(tmp_path) - registry_payload = json.loads(registry.read_text(encoding="utf-8")) - registry_payload["goals"][0]["adapter"] = { - "kind": "generic_project_goal_v0", - "status": "connected", - } - registry_payload["goals"][0]["domain"] = "coordination-promotion" - registry_payload["goals"][0]["quota"] = { - "compute": 1.0, - "window_hours": 24, - "slot_minutes": 1, - "allowed_slots": 10, - } - registry.write_text( - json.dumps(registry_payload, ensure_ascii=False, indent=2) + "\n", - encoding="utf-8", - ) - delegated = cli( - registry, - runtime, - "todo", - "add", - "--goal-id", - "goal-a", - "--role", - "agent", - "--text", - "Validate a managed worker only after reviewed promotion", - "--task-class", - "advancement_task", - "--action-kind", - "validate", - "--claimed-by", - "agent-b", - ) - cli( - registry, - runtime, - "task-lease", - "acquire", - "--goal-id", - "goal-a", - "--todo-id", - delegated["todo_id"], - "--owner", - "agent-b", - "--idempotency-key", - "managed-worker-before-promotion", - "--ttl-seconds", - "600", - ) - enable(registry) - cli( - registry, - runtime, - "coordination-shadow", - "bootstrap", - "--goal-id", - "goal-a", - "--execute", - ) - cli( - registry, - runtime, - "todo", - "add", - "--goal-id", - "goal-a", - "--role", - "agent", - "--text", - "Capture one promotion qualification mutation", - "--task-class", - "advancement_task", - "--action-kind", - "capture", - "--claimed-by", - "agent-a", - ) - common = ( - "coordination-shadow", - "promote", - "--goal-id", - "goal-a", - "--minimum-operations", - "1", - "--require-event-kind", - "todo_add", - ) - preview = cli(registry, runtime, *common) - assert preview["promotion"]["status"] == "preview_ready" - assert preview["promotion"]["promotion_ready"] is True - assert not (runtime / "authority" / "file-v0").exists() - assert not (runtime / ".local" / "manager-context" / "executions").exists() - - applied = cli(registry, runtime, *common, "--execute") - assert applied["promotion"]["status"] == "applied" - assert applied["promotion"]["legacy_writer_fenced"] is True - - # A fresh CLI process owns each call below. The first post-promotion - # canonical mutation therefore proves that runtime-root identity and the - # promoted authority survive process restart before any worker can launch. - inspected = cli(registry, runtime, "goal-acceptance", "inspect", "--goal-id", "goal-a") - document = tmp_path / "managed-acceptance.json" - document.write_text( - json.dumps( - { - "objective": "Validate managed worker acceptance after restart", - "scope": {"kind": "selected_work", "todo_ids": [delegated["todo_id"]]}, - "non_goals": ["Start the managed worker during inspection"], - "criteria": [ - { - "id": "ready", - "description": "The fixture validator succeeds", - "validation_argv": [sys.executable, "-c", "raise SystemExit(0)"], - } - ], - "bindings": [ - {"todo_id": delegated["todo_id"], "criterion_ids": ["ready"]} - ], - } - ), - encoding="utf-8", - ) - configured = cli( - registry, - runtime, - "goal-acceptance", - "configure", - "--goal-id", - "goal-a", - "--document", - str(document), - "--expected-provider-revision", - inspected["provider_revision"], - "--operation-id", - "post-promotion-managed-acceptance", - "--execute", - ) - bound = next( - item - for item in configured["goal_acceptance_contract"]["tasks"] - if item["todo_id"] == delegated["todo_id"] - ) - assert bound == { - "todo_id": delegated["todo_id"], - "state": "ready", - "criterion_ids": ["ready"], - "reason": "The owner confirmed this work's current acceptance association.", - "reason_code": "goal_acceptance_ready", - "applicable": True, - } - - worker = tmp_path / "managed-worker" - worker.mkdir() - config = tmp_path / "delegations.json" - config.write_text( - json.dumps( - { - "schema_version": "loopx_local_delegation_v0", - "bindings": [ - { - "id": "managed-sol", - "agent_id": "agent-b", - "todo_id": delegated["todo_id"], - "requesters": ["agent-a"], - "workspace": str(worker), - "timeout_seconds": 60, - "output_refs": ["output.json"], - "host_args": [ - "--host", - "codex-cli", - "--codex-model", - "gpt-5.6-sol", - "--codex-reasoning-effort", - "xhigh", - ], - } - ], - } - ), - encoding="utf-8", - ) - preflight = cli( - registry, - runtime, - "delegation", - "inspect", - "--goal-id", - "goal-a", - "--agent-id", - "agent-a", - "--execution-config", - str(config), - "--binding-id", - "managed-sol", - ) - assert preflight["authority_state"] == "promoted" - assert preflight["authority_ready"] is True - assert preflight["acceptance_ready"] is True - assert preflight["executor"]["profile"] == "gpt-5.6-sol@xhigh" - assert preflight["state"] in {"launchable", "runtime_unverified"} - assert not any(preflight["effects"].values()) - assert not (runtime / ".local" / "manager-context" / "executions").exists() - assert not (runtime / "goals" / "goal-a" / "turns").exists() - - -def test_unrecorded_canonical_change_cannot_become_qualified_after_a_later_public_write(tmp_path: Path) -> None: - registry, runtime, state = workspace(tmp_path) - enable(registry) - cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - original = state.read_text() - state.write_text(original.replace("handoff_mode: hard_lease", "handoff_mode: soft_claim")) - added = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "After unrecorded mutation") - assert added["added"] is True - qualified = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", success=False) - assert qualified["qualification"]["qualified"] is False - assert list((runtime / "authority-shadow" / "outbox" / "goal-a" / "todos").glob("*.prepared.json")) - state.write_text(original) - again = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", success=False) - assert again["qualification"]["qualified"] is False - - -def test_snapshot_changed_between_python_builder_and_native_inspection_is_rejected(tmp_path: Path) -> None: - registry, runtime, state = workspace(tmp_path) - goal = enable(registry) - cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - projection, snapshot = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=runtime, state_path=state, registry_path=registry) - state.write_text(state.read_text() + "\n## Notes\nProse changed after snapshot.\n") - result = native(tmp_path, "loopx/control_plane/coordination/runtime_shadow.ts", "inspectCoordinationRuntimeShadow", { - "schema_version": "loopx_coordination_runtime_shadow_inspect_v0", "runtime_root": str(runtime), "goal_id": "goal-a", - "projection": projection, "source_snapshot": snapshot, - }) - assert result["status"] == "failed" - assert result["reason_code"] == "source_changed_retry" - - -def test_public_handoff_todo_and_atomic_monitor_batch_capture_each_primary_mutation(tmp_path: Path) -> None: - registry, runtime, _state = workspace(tmp_path) - enable(registry) - cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - cli(registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "soft_claim") - assert len(history(tmp_path, runtime)) == 2 - cli( - registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", - "--text", "Validate the retained projection", "--evidence", "validation://todo-add", - ) - assert len(history(tmp_path, runtime)) == 3 - monitor = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", - "--text", "Observe the public release", "--task-class", "continuous_monitor", "--action-kind", "monitor", - "--claimed-by", "agent-a", "--target-key", "release:bounded", "--cadence", "30m", - "--next-due-at", "2000-01-01T00:00:00+00:00", "--watch-only") - assert len(history(tmp_path, runtime)) == 4 - result = cli(registry, runtime, "quota", "monitor-poll", "--goal-id", "goal-a", "--agent-id", "agent-a", - "--runtime-profile", "generic_cli", "--available-capability", "network", "--todo-id", monitor["todo_id"], - "--target-key", "release:bounded", "--result-hash", "release-v1", "--material-change", - "--next-agent-todo", "Validate the released head", "--next-action-kind", "validate_release_head", - "--next-task-repository", "git:github.com/huangruiteng/loopx", "--next-required-capability", "network", - "--next-continuation-policy", "same_agent_non_delivery", "--next-claimed-by", "agent-a", "--execute") - assert len(result["successor_todo_ids"]) == 1 - transactions = history(tmp_path, runtime) - assert len(transactions) == 5 # Baseline, handoff, todo add, monitor add, atomic Monitor batch. - batch_todos = {todo["todo_id"]: todo for todo in transactions[-1]["projection"]["todos"]} - assert batch_todos[monitor["todo_id"]]["result_hash"] == "release-v1" - assert batch_todos[result["successor_todo_ids"][0]]["text"] == "Validate the released head" - receipts = [transaction["receipts"][0] for transaction in transactions[1:]] - assert len({receipt["entry_id"] for receipt in receipts}) == 4 - assert [receipt["seq"] for receipt in receipts] == [1, 2, 3, 4] - assert {receipt["write_class"] for receipt in receipts} >= {"handoff_mode_set", "todo_add", "todo_update"} - qualified = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", "--minimum-operations", "4") - assert qualified["qualification"]["qualified"] is True - assert qualified["qualification"]["evidence"]["operation_count"] == 4 - - -def test_disabling_configuration_cannot_cancel_an_active_capture_obligation(tmp_path: Path) -> None: - registry, runtime, state = workspace(tmp_path) - enable(registry) - cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - initial = state.read_bytes() - config = json.loads(registry.read_text()) - config["goals"][0]["coordination"]["runtime_shadow"]["enabled"] = False - registry.write_text(json.dumps(config)) - cli(registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "soft_claim") - cli(registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "hard_lease") - assert state.read_bytes() == initial - enable(registry) - # The identical final source must not erase the two intervening mutations. - transactions = history(tmp_path, runtime) - assert len(transactions) == 3 - assert [row["receipts"][0]["seq"] for row in transactions[1:]] == [1, 2] - qualified = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", "--minimum-operations", "2") - assert qualified["qualification"]["qualified"] is True - assert qualified["qualification"]["evidence"]["operation_count"] == 2 - - -def test_native_lease_writer_cannot_reuse_a_sequence_when_its_cursor_is_missing(tmp_path: Path) -> None: - registry, runtime, _state = workspace(tmp_path) - first = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "First lease task") - second = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Second lease task") - enable(registry) - cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - cli(registry, runtime, "task-lease", "acquire", "--goal-id", "goal-a", "--todo-id", first["todo_id"], - "--owner", "agent-a", "--idempotency-key", "first-lease", "--ttl-seconds", "120") - assert len(history(tmp_path, runtime)) == 2 - cursor = runtime / "authority-shadow/outbox/goal-a/leases/drain-cursor.json" - cursor.unlink() - acquired = acquire_native(tmp_path, registry, runtime, second["todo_id"]) - assert acquired["acquired"] is True, acquired - pending = list(cursor.parent.glob("*.prepared.json")) - assert len(pending) == 1 - assert json.loads(pending[0].read_text())["seq"] == 2 - assert not cursor.exists() # The writer recovers a sequence, never a cursor. - cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Trigger bounded drain") - transactions = history(tmp_path, runtime) - lease_receipts = [row["receipts"][0] for row in transactions[1:] if row["receipts"][0]["partition"] == "leases"] - assert [receipt["seq"] for receipt in lease_receipts] == [1, 2] - assert len({receipt["entry_id"] for receipt in lease_receipts}) == 2 - assert cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a")["qualification"]["qualified"] is True - - -def test_public_committed_primary_cannot_be_relabelled_abandoned_by_native_request(tmp_path: Path) -> None: - from shadow_e2e_fixture import workspace as crash_workspace - - from loopx.control_plane.coordination import ( - local_authority_shadow_adapter as adapter, - ) - from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox - - w = crash_workspace(tmp_path) - w.crash("before_commit", "todo", "add", "--role", "agent", "--text", "A committed primary is never abandoned") - directory = outbox.partition_directory(w.runtime, w.goal, "todos") - [entry] = outbox.list_entries(directory) - # The batch drain owns public commits now, so the witnessed selection is - # built here from durable entry bytes instead of a retired private helper. - request = { - "schema_version": LOCAL_AUTHORITY_SHADOW_COMMIT_ENTRY_REQUEST_SCHEMA, - "runtime_root": str(w.runtime), - "goal_id": w.goal, - "entry_id": entry.entry_id, - "partition": entry.partition, - "seq": entry.seq, - "capture_lineage_id": entry.prepared.get("capture_lineage_id"), - "prepared_sha256": "sha256:" + hashlib.sha256(entry.prepared_path.read_bytes()).hexdigest(), - "committed_sha256": ( - "sha256:" + hashlib.sha256(entry.committed_path.read_bytes()).hexdigest() - if entry.committed_path - else None - ), - } - assert request["committed_sha256"] is not None - request["resolution"] = "abandoned" - before = {path.name: path.read_bytes() for path in directory.iterdir()} - primary = w.state.read_bytes() - with pytest.raises(EffectRuntimeRejected, match="shadow_entry_selection_invalid"): - adapter.effect_runtime_result("coordination.runtime_shadow.commit_entry", request, timeout=15) - assert {path.name: path.read_bytes() for path in directory.iterdir()} == before - assert w.state.read_bytes() == primary - # The byte witness itself is legal; only caller-supplied resolution rejects. - request.pop("resolution") - delivered = adapter.effect_runtime_result("coordination.runtime_shadow.commit_entry", request, timeout=15) - assert delivered["outcome"] == "delivered" and delivered["resolution"] == "committed" - assert w.drain()["ok"] is True - view = adapter.read_local_authority_shadow(runtime_root=w.runtime, goal_id=w.goal, scan_limit=20) - [transaction] = view["proof"]["transactions"][1:] - assert transaction["receipts"][0]["resolution"] == "committed" - assert transaction["receipts"][0]["no_op"] is False - - -def test_controller_cannot_bind_the_registered_source_to_an_override_runtime_root(tmp_path: Path) -> None: - registry, runtime, state = workspace(tmp_path) - enable(registry) - original = state.read_bytes() - override = tmp_path / "override-runtime" - for registered_is_active in (False, True): - if registered_is_active: - cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - rejected = cli(registry, override, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute", success=False) - assert rejected["bootstrap"]["status"] == "failed" - assert rejected["bootstrap"]["reason_code"] == "shadow_source_runtime_root_mismatch", rejected - assert not (override / "authority-shadow/file-v0").exists() - assert state.read_bytes() == original - qualified = cli(registry, override, "coordination-shadow", "qualify", "--goal-id", "goal-a", success=False) - assert qualified["qualification"]["qualified"] is False - assert qualified["qualification"]["reason_code"] == "shadow_source_runtime_root_mismatch" - - -def test_controller_cannot_bind_an_alternate_state_file_before_or_after_bootstrap(tmp_path: Path) -> None: - registry, runtime, state = workspace(tmp_path) - enable(registry) - alternate = tmp_path / "alternate-state.md" - alternate.write_bytes(state.read_bytes()) - original = state.read_bytes() - for active in (False, True): - if active: - cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") - rejected = cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", - "--state-file", str(alternate), "--execute", success=False) - assert rejected["bootstrap"]["status"] == "failed" - expected = "management_operation_identity_mismatch" if active else "shadow_source_state_path_mismatch" - assert rejected["bootstrap"]["reason_code"] == expected, rejected - assert state.read_bytes() == alternate.read_bytes() == original - qualified = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", - "--state-file", str(alternate), success=False) - assert qualified["qualification"]["qualified"] is False - assert qualified["qualification"]["reason_code"] == "shadow_source_state_path_mismatch" - if not active: - assert not (runtime / "authority-shadow/file-v0").exists() diff --git a/tests/control_plane/test_runtime_shadow_writer_capture.py b/tests/control_plane/test_runtime_shadow_writer_capture.py deleted file mode 100644 index 58a3fc5879..0000000000 --- a/tests/control_plane/test_runtime_shadow_writer_capture.py +++ /dev/null @@ -1,263 +0,0 @@ -from __future__ import annotations - -import json -import subprocess -import sys -from pathlib import Path - -import pytest - -from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from loopx.control_plane.work_items.task_lease import ( - acquire_task_lease, - release_task_lease, - renew_task_lease, - transfer_task_lease, -) -from loopx.todos import add_goal_todo, update_goal_todo - - -GOAL_ID = "runtime-shadow-writer" - - -def _fixture( - tmp_path: Path, - *, - enabled: bool, - handoff_mode: str = "hard_lease", -) -> tuple[Path, Path, Path]: - repo = tmp_path / "repo" - repo.mkdir() - state = repo / "ACTIVE_GOAL_STATE.md" - state.write_text( - "---\n" - f"goal_id: {GOAL_ID}\n" - f"handoff_mode: {handoff_mode}\n" - "updated_at: 2026-09-04T00:00:00+00:00\n" - "---\n\n## Agent Todo\n\n", - encoding="utf-8", - ) - runtime_root = tmp_path / "runtime" - coordination: dict[str, object] = { - "agent_model": "peer_v1", - "registered_agents": ["agent-a", "agent-b"], - } - if enabled: - coordination["runtime_shadow"] = { - "enabled": True, - "schema_version": "loopx_coordination_runtime_shadow_config_v0", - "provider": "file_v0", - } - registry = tmp_path / "registry.json" - registry.write_text( - json.dumps( - { - "common_runtime_root": str(runtime_root), - "goals": [ - { - "id": GOAL_ID, - "domain": "harness_self_improvement", - "status": "active", - "repo": str(repo), - "state_file": state.name, - "adapter": {"kind": "harness_self_improvement"}, - "coordination": coordination, - } - ], - } - ), - encoding="utf-8", - ) - if enabled: - boot = subprocess.run([sys.executable, "-m", "loopx.entrypoint", "--registry", str(registry), - "--format", "json", "coordination-shadow", "bootstrap", "--goal-id", GOAL_ID, "--execute"], - cwd=Path(__file__).resolve().parents[2], text=True, capture_output=True, timeout=30) - assert boot.returncode == 0, boot.stdout + boot.stderr - return registry, state, runtime_root - - -def test_runtime_shadow_todo_writer_captures_full_records_and_reuses_one_store( - tmp_path: Path, -) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=True) - - added = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Retain complete canonical Todo fields.", - task_class="advancement_task", - action_kind="implement", - claimed_by="agent-a", - ) - updated = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(added["todo_id"]), - note="transaction-bound", - agent_id="agent-a", - ) - - assert added["coordination_runtime_shadow"]["outcome"] == "delivered" - assert updated["coordination_runtime_shadow"]["outcome"] == "delivered" - view = adapter.read_local_authority_shadow( - runtime_root=runtime_root, - goal_id=GOAL_ID, - scan_limit=10, - ) - head = view["head"] - assert head["schema_version"] == "loopx_coordination_runtime_shadow_projection_v0" - assert head["todos"][0]["text"] == "Retain complete canonical Todo fields." - assert head["todos"][0]["note"] == "transaction-bound" - assert head["todo_read_model"]["todo_count"] == 1 - assert view["cursor"] == "3" - assert not (runtime_root / "authority-shadow" / "file" / GOAL_ID).exists() - - -def test_runtime_shadow_todo_claim_preserves_claim_write_class(tmp_path: Path) -> None: - registry, _state, runtime_root = _fixture( - tmp_path, - enabled=True, - handoff_mode="legacy", - ) - added = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Qualify the legacy claim path.", - task_class="advancement_task", - ) - - claimed = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(added["todo_id"]), - claimed_by="agent-a", - agent_id="agent-a", - claim_only=True, - ) - - assert claimed["coordination_runtime_shadow"]["outcome"] == "delivered" - view = adapter.read_local_authority_shadow( - runtime_root=runtime_root, - goal_id=GOAL_ID, - scan_limit=10, - ) - receipt = view["proof"]["transactions"][-1]["receipts"][0] - assert receipt["write_class"] == "todo_claim" - - -def test_runtime_shadow_todo_writer_is_zero_effect_by_default(tmp_path: Path) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=False) - - result = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Default-off capture.", - task_class="advancement_task", - ) - - assert result["coordination_runtime_shadow"]["outcome"] == "no_transaction" - assert result["coordination_runtime_shadow"]["reason_code"] == "shadow_disabled" - assert not (runtime_root / "authority-shadow").exists() - - -@pytest.mark.parametrize("configuration_cleared", [False, True]) -def test_runtime_shadow_native_lease_writers_capture_complete_records( - tmp_path: Path, configuration_cleared: bool, -) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=True) - added = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Capture the native lease transaction.", - task_class="advancement_task", - ) - todo_id = str(added["todo_id"]) - if configuration_cleared: - config = json.loads(registry.read_text(encoding="utf-8")) - config["goals"][0]["coordination"].pop("runtime_shadow") - registry.write_text(json.dumps(config), encoding="utf-8") - - acquired = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner="agent-a", - idempotency_key="runtime-shadow-lease", - write_scopes=["loopx/**"], - ttl_seconds=120, - ) - renewed = renew_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner="agent-a", - idempotency_key="runtime-shadow-lease", - ttl_seconds=180, - expected_version=int(acquired["lease"]["version"]), - ) - transferred = transfer_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner="agent-a", - idempotency_key="runtime-shadow-lease", - new_owner="agent-b", - new_idempotency_key="runtime-shadow-lease-b", - ttl_seconds=180, - expected_version=int(renewed["lease"]["version"]), - ) - released = release_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner="agent-b", - idempotency_key="runtime-shadow-lease-b", - expected_version=int(transferred["lease"]["version"]), - ) - - assert acquired["coordination_runtime_shadow"]["outcome"] == "delivered" - assert renewed["coordination_runtime_shadow"]["outcome"] == "delivered" - assert transferred["coordination_runtime_shadow"]["outcome"] == "delivered" - assert released["coordination_runtime_shadow"]["outcome"] == "delivered" - view = adapter.read_local_authority_shadow(runtime_root=runtime_root, goal_id=GOAL_ID) - lease = view["head"]["leases"][0] - assert lease["goal_id"] == GOAL_ID - assert lease["owner"] == "agent-b" - assert lease["idempotency_key"] == "runtime-shadow-lease-b" - assert lease["write_scopes"] == ["loopx/**"] - assert lease["status"] == "released" - - -def test_runtime_shadow_native_lease_writer_is_zero_effect_by_default(tmp_path: Path) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=False) - added = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Do not capture an unconfigured lease.", - task_class="advancement_task", - claimed_by="agent-a", - ) - - acquired = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=str(added["todo_id"]), - owner="agent-a", - idempotency_key="default-off-lease", - write_scopes=["loopx/**"], - ttl_seconds=120, - ) - - assert "coordination_runtime_shadow_capture" not in acquired - assert "coordination_runtime_shadow" not in acquired - assert not (runtime_root / "authority-shadow").exists() diff --git a/tests/control_plane/test_selected_work_requirements.py b/tests/control_plane/test_selected_work_requirements.py index 4105ce56a4..136a9c56d3 100644 --- a/tests/control_plane/test_selected_work_requirements.py +++ b/tests/control_plane/test_selected_work_requirements.py @@ -22,6 +22,8 @@ from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, render_prompt +REPO = Path(__file__).resolve().parents[2] + def test_legacy_selected_todo_requires_same_goal_state_revision(tmp_path: Path) -> None: state = tmp_path / "state.md" @@ -114,7 +116,7 @@ def guard(*extra): assert guard()["interaction_contract"]["agent_channel"]["work_context"]["complete"] is True -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_current_work_requirements_reach_real_guard_and_host_without_display_loss( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, ) -> None: @@ -135,15 +137,22 @@ def test_current_work_requirements_reach_real_guard_and_host_without_display_los goal_id="requirements-goal", domain="requirements", adapter_kind="generic_project_goal_v0", state_file=str(state), registered_agents=["agent-a", "agent-b"], quota_allowed_slots=None) expected = "[P1] " + work - if provider != "legacy": - goal = json.loads(registry.read_text())["goals"][0] - fields, _archived, _sections = parse_todo_source(state.read_text(), goal=goal, state_path=state) - full_items = retained_todo_summary_fields(fields["agent"], rollout_events=[])["agent_todos"]["items"] - snapshot = build_todo_runtime_shadow_projection( - goal_id="requirements-goal", todos=full_items, handoff_mode="soft_claim") - initialize_canonical_authority(runtime, "requirements-goal", snapshot, - state_path=state, provider=provider) - state.write_text(goal_requirements + "## Agent Todo\n- [ ] Stale display: do only an easier check.\n") + goal = json.loads(registry.read_text())["goals"][0] + fields, _archived, _sections = parse_todo_source( + state.read_text(), goal=goal, state_path=state, + ) + full_items = retained_todo_summary_fields( + fields["agent"], rollout_events=[], + )["agent_todos"]["items"] + snapshot = build_todo_runtime_shadow_projection( + goal_id="requirements-goal", todos=full_items, handoff_mode="soft_claim", + ) + initialize_canonical_authority( + runtime, "requirements-goal", snapshot, state_path=state, provider=provider, + ) + state.write_text( + goal_requirements + "## Agent Todo\n- [ ] Stale display: do only an easier check.\n", + ) def guard(*extra: str) -> dict: code, packet = run_json_cli_result("quota", "should-run", "--goal-id", "requirements-goal", @@ -164,9 +173,13 @@ def guard(*extra: str) -> dict: assert "work_context" in body script = re.search(r"```sh\n(.*?)\n```", body, re.S).group(1) assert "--turn-envelope" not in script and "--decision-output-root" not in script - env = {**os.environ, "LOOPX_REGISTRY": str(registry)} + env = { + **os.environ, + "LOOPX_REGISTRY": str(registry), + "PYTHONPATH": str(REPO), + } result = subprocess.run(["sh", "-c", script.replace("", "requirements-default-wake")], - cwd=tmp_path, env=env, capture_output=True, text=True, check=True) + cwd=REPO, env=env, capture_output=True, text=True, check=True) full = json.loads(result.stdout) assert full["should_run"] is True, full.get("reason") channel = full["interaction_contract"]["agent_channel"] @@ -374,9 +387,13 @@ def test_short_selected_todo_deduplicates_body_but_keeps_canonical_note( registry_path=registry, runtime_root=runtime) assert code == 0 and generated["ok"], generated script = re.search(r"```sh\n(.*?)\n```", generated["task_body"], re.S).group(1) - env = {**os.environ, "LOOPX_REGISTRY": str(registry)} + env = { + **os.environ, + "LOOPX_REGISTRY": str(registry), + "PYTHONPATH": str(REPO), + } result = subprocess.run(["sh", "-c", script.replace("", "short-note-wake")], - cwd=tmp_path, env=env, capture_output=True, text=True, check=True) + cwd=REPO, env=env, capture_output=True, text=True, check=True) full = json.loads(result.stdout) channel = full["interaction_contract"]["agent_channel"] context = channel["work_context"] diff --git a/tests/control_plane/test_settled_monitor_user_gate.py b/tests/control_plane/test_settled_monitor_user_gate.py index 1db31a85ca..a84e6cc050 100644 --- a/tests/control_plane/test_settled_monitor_user_gate.py +++ b/tests/control_plane/test_settled_monitor_user_gate.py @@ -5,13 +5,12 @@ import pytest -from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime +from canonical_authority_fixture import isolate_sqlite_runtime from tests.control_plane.test_monitor_followthrough_contract import ( AGENT_ID, GOAL_ID, _add_monitor, _write_fixture, ) -from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.testing.canary_harness import run_json_cli -from loopx.todos import add_goal_todo, list_goal_todos +from loopx.todos import add_goal_todo from loopx.control_plane.effect_program import ReceiptBoundMonitorPhase from loopx.control_plane.quota.should_run import build_quota_should_run from loopx.control_plane.testing.quota_fixtures import ( @@ -44,11 +43,11 @@ def test_incomplete_frontier_cannot_erase_settled_monitor_receipt(): assert replay["interaction_contract"]["agent_channel"]["delivery_allowed"] is False -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("material", [False, True]) def test_scoped_gate_cannot_reopen_an_exact_settled_monitor(tmp_path, monkeypatch, provider, material): isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state = _write_fixture(tmp_path) + registry, runtime, _state = _write_fixture(tmp_path, provider=provider) gated = add_goal_todo(registry_path=registry, goal_id=GOAL_ID, role="agent", text="Work awaiting an owner decision", task_class="advancement_task", status="blocked", claimed_by=AGENT_ID, agent_id=AGENT_ID) @@ -57,11 +56,6 @@ def test_scoped_gate_cannot_reopen_an_exact_settled_monitor(tmp_path, monkeypatc blocks_agent=AGENT_ID, unblocks_todo_id=gated["todo_id"], agent_id=AGENT_ID) monitor = _add_monitor(registry, text="Observe an independent public target", target_key="public-target", next_due_at="2000-01-01T00:00:00Z") - if provider != "legacy": - todos = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] - projection = build_todo_runtime_shadow_projection(goal_id=GOAL_ID, - todos=todos, handoff_mode="soft_claim") - initialize_canonical_authority(runtime, GOAL_ID, projection, state_path=state, provider=provider) turn = "monitor-scoped-gate-turn" guard_args = ["quota", "should-run", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--runtime-profile", "generic_cli", "--turn-instance-id", turn, diff --git a/tests/control_plane/test_settlement_input_discovery_cli.py b/tests/control_plane/test_settlement_input_discovery_cli.py index 4132559a5f..f1f4f68286 100644 --- a/tests/control_plane/test_settlement_input_discovery_cli.py +++ b/tests/control_plane/test_settlement_input_discovery_cli.py @@ -4,6 +4,7 @@ import json import shlex from copy import deepcopy +from pathlib import Path import pytest from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime @@ -15,11 +16,42 @@ AGENT_ID, GOAL_ID, TODO_ID, TURN_ID, _run_cli, _run_generated_cli, _spend_run_count, _write_fixture, ) +from loopx.todos import complete_goal_todo BODY = "[P1] " + "Retain the original requirements and verify the result. " * 24 + "TAIL: preserve cancellation." -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +def _initialize_authority( + runtime: Path, + registry: Path, + state: Path, + provider: str, +) -> None: + goal = json.loads(registry.read_text())["goals"][0] + active, archived, _ = parse_todo_source( + state.read_text(), + goal=goal, + state_path=state, + ) + rows = [ + {"schema_version": "todo_item_v0", **row} + for row in [*active["agent"], *active["user"], *archived] + ] + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=rows, + handoff_mode="soft_claim", + leases=[], + ), + state_path=state, + provider=provider, + ) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("in_flight", [False, True]) def test_projected_inputs_support_real_readback_writeback_and_one_debit(tmp_path, monkeypatch, provider, in_flight): isolate_sqlite_runtime(tmp_path, monkeypatch) @@ -27,14 +59,7 @@ def test_projected_inputs_support_real_readback_writeback_and_one_debit(tmp_path state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" state.write_text(state.read_text().replace("[P1] Validate and settle the selected delivery.", BODY).replace( "action_kind=validate -->", f"action_kind=validate claimed_by={AGENT_ID} -->")) - if provider != "legacy": - goal = json.loads(registry.read_text())["goals"][0] - active, archived, _ = parse_todo_source(state.read_text(), goal=goal, state_path=state) - rows = [{"schema_version": "todo_item_v0", **row} - for row in [*active["agent"], *active["user"], *archived]] - initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, todos=rows, handoff_mode="legacy", leases=[]), - state_path=state, provider=provider) + _initialize_authority(runtime, registry, state, provider) def run(*args): return _run_cli(registry, runtime, *args, cwd=project) @@ -104,10 +129,12 @@ def run(*args): assert rc == 0 and final_todo["todo"]["status"] == ("open" if in_flight else "done") -@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_todoless_periodic_review_uses_default_authoring_and_settles_once(tmp_path, monkeypatch, provider): isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + _initialize_authority(runtime, registry, state, provider) def run(*args): return _run_cli(registry, runtime, *args, cwd=project) @@ -127,21 +154,16 @@ def run(*args): rc, spent = run("quota", "spend-slot", *binding, "--slots", "1", "--source", "heartbeat", "--execute") assert rc == 0 and _spend_run_count(runtime) == 1, spent - # Synthetic absence of runnable work; retain the real committed receipt. - # Initialize each authority from this fixture, never rewrite a live store. - state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" - state.write_text(state.read_text().replace( - "- [ ] [P1] Validate and settle the selected delivery.", - "- [x] [P1] Validate and settle the selected delivery.").replace( - f"todo_id={TODO_ID} status=open", f"todo_id={TODO_ID} status=done")) - if provider != "legacy": - goal = json.loads(registry.read_text())["goals"][0] - active, archived, _ = parse_todo_source(state.read_text(), goal=goal, state_path=state) - rows = [{"schema_version": "todo_item_v0", **row} - for row in [*active["agent"], *active["user"], *archived]] - initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, todos=rows, handoff_mode="legacy", leases=[]), - state_path=state, provider=provider) + # Synthetic absence of runnable work; retain the real committed receipt and + # close the selected Todo through the canonical public writer. + complete_goal_todo( + registry_path=registry, + goal_id=GOAL_ID, + todo_id=TODO_ID, + role="agent", + agent_id=AGENT_ID, + evidence="Synthetic fixture completion after receipt commit.", + ) current = ("--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--turn-instance-id", "periodic-without-todo", "--scan-path", str(project)) diff --git a/tests/control_plane/test_shadow_cursor_recovery_e2e.py b/tests/control_plane/test_shadow_cursor_recovery_e2e.py deleted file mode 100644 index 105f632c6a..0000000000 --- a/tests/control_plane/test_shadow_cursor_recovery_e2e.py +++ /dev/null @@ -1,181 +0,0 @@ -"""A settled position and its last applied digest are separate cursor facts.""" - -from __future__ import annotations - -import json -from pathlib import Path -import select -import subprocess - -import pytest - -from shadow_e2e_fixture import REPO, ShadowWorkspace, workspace -from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox -from loopx.control_plane.coordination.coordination_state_contract_generated import TASK_LEASE_ACQUIRE_REQUEST_SCHEMA -from loopx.control_plane.work_items.task_lease_acquire_adapter import task_lease_acquire_authority_facts - -pytestmark = pytest.mark.stage2c_e2e - -# Pause only the actual primary rename, after durable prepare. No result is replaced. -LEASE_WORKER = r""" -import fs from 'node:fs'; -import {syncBuiltinESMExports} from 'node:module'; -const input = JSON.parse(process.argv[1]); -const rename = fs.promises.rename; -fs.promises.rename = async (source, target) => { - if (String(target) === input.stop_before) { - process.stdout.write('BARRIER primary-rename\n'); - await new Promise(resolve => setTimeout(resolve, 40000)); - throw new Error('parent did not terminate the paused writer'); - } - return await rename(source, target); -}; -syncBuiltinESMExports(); -const {executeTaskLeaseAcquire} = await import(input.module); -process.stdout.write(JSON.stringify(await executeTaskLeaseAcquire(input.request)) + '\n'); -""" - - -def acquire(w: ShadowWorkspace, todo: str, *, crash: bool = False) -> None: - lease = w.runtime / 'goals' / w.goal / 'task-leases' / f'{todo}.json' - request = { - 'schema_version': TASK_LEASE_ACQUIRE_REQUEST_SCHEMA, - 'runtime_root': str(w.runtime), 'goal_id': w.goal, 'todo_id': todo, - 'owner': 'agent-a', 'idempotency_key': f'acquire-{todo}', - 'ttl_seconds': 3600, 'write_scopes': [], 'expected_version': None, - 'authority': task_lease_acquire_authority_facts( - registry_path=w.registry, goal_id=w.goal, todo_id=todo), - } - args = ['node', '--no-warnings', '--experimental-strip-types', '--input-type=module', '-e', - LEASE_WORKER, json.dumps({'request': request, 'stop_before': str(lease) if crash else None, - 'module': (REPO / 'loopx/control_plane/work_items/task_lease_acquire.ts').as_uri()})] - if not crash: - result = subprocess.run(args, cwd=REPO, capture_output=True, text=True, timeout=30, check=True) - assert json.loads(result.stdout)['acquired'] is True, result.stdout - assert json.loads(lease.read_bytes())['owner'] == 'agent-a' - return - child = subprocess.Popen(args, cwd=REPO, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - try: - assert child.stdout is not None - assert select.select([child.stdout], [], [], 30)[0], 'native writer missed primary rename' - assert child.stdout.readline().strip() == 'BARRIER primary-rename' - child.kill() - child.communicate(timeout=10) - assert child.returncode == -9 - assert not lease.exists() - finally: - if child.poll() is None: - child.kill() - child.communicate(timeout=10) - - -@pytest.mark.parametrize('partition', ['todos', 'leases']) -@pytest.mark.parametrize('prior_mutations', [0, 1]) -@pytest.mark.parametrize('abandoned', [1, 2]) -def test_abandoned_cursor_survives_all_consumers( - tmp_path: Path, partition: str, prior_mutations: int, abandoned: int, -) -> None: - w = workspace(tmp_path, bootstrap=False) - # A nonempty baseline must still have no applied-mutation digest. - ids = [w.add(f'Baseline task {index}')['todo_id'] for index in range(3)] - assert w.cli('coordination-shadow', 'bootstrap', '--execute')['bootstrap']['status'] == 'applied' - - def mutate(index: int) -> None: - if partition == 'todos': - ids.append(w.add(f'Applied task {index}')['todo_id']) - else: - acquire(w, ids[index]) - assert w.drain()['ok'] is True - - for index in range(prior_mutations): - mutate(index) - before = w.state.read_bytes() - for index in range(abandoned): - if partition == 'todos': - w.crash('before_replace', 'todo', 'add', '--role', 'agent', '--text', f'Never applied {index}') - else: - acquire(w, ids[prior_mutations], crash=True) - assert w.state.read_bytes() == before - recovered = w.drain() - assert recovered['ok'] is True and recovered['no_op'] == 1, recovered - inspected = w.cli('coordination-shadow', 'inspect', success=False)['inspection'] - assert inspected['status'] == 'matched', inspected - assert inspected['evidence']['operation_count'] == prior_mutations - - directory = outbox.partition_directory(w.runtime, w.goal, partition) - path = directory / 'drain-cursor.json' - cursor = json.loads(path.read_bytes()) - assert cursor['last_seq'] == prior_mutations + abandoned - assert (cursor['last_partition_digest'] is None) == (prior_mutations == 0) - assert w.drain()['outcome'] == 'nothing_pending' - # Legitimate cursor loss reconstructs the same facts from real history. - path.unlink() - assert w.drain()['ok'] is True - restored = json.loads(path.read_bytes()) - assert {k: v for k, v in restored.items() if k != 'updated_at'} == { - k: v for k, v in cursor.items() if k != 'updated_at'} - assert w.cli('coordination-shadow', 'inspect')['inspection']['status'] == 'matched' - early = w.cli('coordination-shadow', 'qualify', success=False)['qualification'] - assert early['qualified'] is False and early['evidence']['operation_count'] == prior_mutations - early_read = w.cli('coordination-shadow', 'read-candidate', '--todo-id', ids[-1], success=False) - assert early_read['read_candidate']['read_candidate_qualified'] is False - for index in range(prior_mutations, 3): - mutate(index) - qualified = w.cli('coordination-shadow', 'qualify')['qualification'] - assert qualified['qualified'] is True and qualified['scope'] == 'bounded', qualified - assert qualified['evidence']['operation_count'] == 3 - assert qualified['sustained_parity_verdict'] == 'not_evaluated' - transactions = adapter.read_local_authority_shadow( - runtime_root=w.runtime, goal_id=w.goal, scan_limit=100, - )['proof']['transactions'] - assert len(transactions) == 4 + abandoned - assert [tx['receipts'][0]['seq'] for tx in transactions[1:]] == list(range(1, 4 + abandoned)) - assert sum(tx['receipts'][0]['no_op'] is True for tx in transactions[1:]) == abandoned - candidate = w.cli('coordination-shadow', 'read-candidate', '--todo-id', ids[-1]) - assert candidate['read_candidate']['read_candidate_qualified'] is True, candidate - - -@pytest.mark.parametrize('partition', ['todos', 'leases']) -@pytest.mark.parametrize('applied', [False, True]) -def test_forged_applied_digest_holds_every_consumer_without_rewriting_bytes( - tmp_path: Path, partition: str, applied: bool, -) -> None: - import hashlib - - w = workspace(tmp_path, bootstrap=False) - todo = w.add('Baseline is not mutation coverage')['todo_id'] - w.cli('coordination-shadow', 'bootstrap', '--execute') - if applied: - if partition == 'todos': - w.add('Real mutation with an applied digest') - else: - acquire(w, todo) - elif partition == 'todos': - w.crash('before_replace', 'todo', 'add', '--role', 'agent', '--text', 'Abandoned') - else: - acquire(w, todo, crash=True) - assert w.drain()['ok'] is True - view = adapter.read_local_authority_shadow(runtime_root=w.runtime, goal_id=w.goal, scan_limit=20) - projection = view['proof']['transactions'][-1]['projection'] - fields = ('handoff_mode', 'todos') if partition == 'todos' else ('leases',) - snapshot = {key: projection[key] for key in fields} - snapshot_digest = 'sha256:' + hashlib.sha256(json.dumps( - snapshot, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode()).hexdigest() - path = outbox.partition_directory(w.runtime, w.goal, partition) / 'drain-cursor.json' - cursor = json.loads(path.read_bytes()) - cursor['last_partition_digest'] = None if applied else snapshot_digest - path.write_text(json.dumps(cursor, indent=3) + '\n') - # Exact raw bytes matter even when all the cursor's identity fields are valid. - # Lock diagnostics change and dead writer locks are reclaimed; authority evidence does not. - before = {p.relative_to(w.runtime): p.read_bytes() for p in w.runtime.rglob('*') - if p.is_file() and not p.name.endswith('.lock')} - for command, key in [('inspect', 'inspection'), ('qualify', 'qualification'), ('read-candidate', 'read_candidate')]: - args = ('--todo-id', todo) if command == 'read-candidate' else () - result = w.cli('coordination-shadow', command, *args, success=False)[key] - assert result.get('reason_code') == 'outbox_cursor_unproved', result - result = w.drain() - assert result['ok'] is False and result['reason_code'] == 'outbox_cursor_unproved', result - after = {p.relative_to(w.runtime): p.read_bytes() for p in w.runtime.rglob('*') - if p.is_file() and not p.name.endswith('.lock')} - assert after == before diff --git a/tests/control_plane/test_shadow_drain_adversarial.py b/tests/control_plane/test_shadow_drain_adversarial.py deleted file mode 100644 index 26aeb6b39a..0000000000 --- a/tests/control_plane/test_shadow_drain_adversarial.py +++ /dev/null @@ -1,224 +0,0 @@ -"""Adversarial recovery ordering through the public CLI and real file provider.""" -from __future__ import annotations - -import hashlib -from pathlib import Path - -import pytest - -from loopx.control_plane.effect_runtime import EffectRuntimeRejected - - -from shadow_e2e_fixture import workspace -from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox - - -pytestmark = pytest.mark.stage2c_e2e - - -@pytest.mark.parametrize("window", ["before_commit", "after_commit", "after_cursor"]) -def test_cleanup_permission_failure_reports_verified_commit_and_recovers( - tmp_path: Path, window: str, -) -> None: - """A failed local checkpoint cannot hide a proven candidate transaction.""" - w = workspace(tmp_path) - w.crash(window, "todo", "add", "--role", "agent", "--text", "Durable despite cleanup failure") - directory = outbox.partition_directory(w.runtime, w.goal, "todos") - before = {path.name: path.read_bytes() for path in directory.iterdir()} - mode = directory.stat().st_mode & 0o777 - directory.chmod(0o500) - try: - # Exercise a real filesystem denial, with the provider still writable. - with pytest.raises(PermissionError): - (directory / "permission-control").write_bytes(b"must not be writable") - stopped = w.drain() - assert stopped["ok"] is False and stopped["reason_code"], stopped - assert {path.name: path.read_bytes() for path in directory.iterdir()} == before - view = adapter.read_local_authority_shadow(runtime_root=w.runtime, goal_id=w.goal, scan_limit=20) - assert len(view["proof"]["transactions"]) == 2 # Baseline and the actual mutation. - assert stopped["candidate_readback_verified"] is True, stopped - assert stopped["provider_revision"] == view["provider_revision"], stopped - finally: - directory.chmod(mode) - recovered = w.drain() - assert recovered["ok"] is True and recovered["replayed"] == 1, recovered - assert recovered["delivered"] == 0, recovered - assert outbox.read_cursor(directory)["last_seq"] == 1 - assert {path.name for path in directory.iterdir()} == {"drain-cursor.json"} - assert adapter.read_local_authority_shadow( - runtime_root=w.runtime, goal_id=w.goal, scan_limit=20, - )["proof"]["transactions"] == view["proof"]["transactions"] - - -def test_missing_cursor_cannot_reuse_a_sequence_when_the_next_writer_arrives_first(tmp_path: Path) -> None: - w = workspace(tmp_path) - w.add("First complete transaction") - w.add("Second complete transaction") - directory = outbox.partition_directory(w.runtime, w.goal, "todos") - (directory / "drain-cursor.json").unlink() - # No operator drain runs between evidence loss and the next public writer. - result = w.add("Writer arrives before cursor recovery") - assert result["ok"] is True - recovered = w.drain() - assert recovered["ok"] is True, recovered - view = adapter.read_local_authority_shadow(runtime_root=w.runtime, goal_id=w.goal, scan_limit=20) - receipts = [tx["receipts"][0] for tx in view["proof"]["transactions"][1:]] - assert [receipt["seq"] for receipt in receipts] == [1, 2, 3] - assert outbox.read_cursor(directory)["last_seq"] == 3 - - -def test_small_recovery_budget_makes_progress_through_verified_residue(tmp_path: Path) -> None: - w = workspace(tmp_path) - directory = outbox.partition_directory(w.runtime, w.goal, "todos") - residue: dict[str, bytes] = {} - for index in range(3): - w.crash("after_commit", "todo", "add", "--role", "agent", "--text", f"Acknowledged transaction {index}") - residue.update({path.name: path.read_bytes() for path in directory.glob("*.json") if path.name != "drain-cursor.json"}) - assert w.drain()["ok"] is True - for name, data in residue.items(): - (directory / name).write_bytes(data) - before = adapter.read_local_authority_shadow(runtime_root=w.runtime, goal_id=w.goal, scan_limit=20) - for left in (2, 1, 0): - result = w.drain(max_entries="1") - assert result["ok"] is True, result - assert result["replayed"] == 1, result - assert len(outbox.list_entries(directory)) == left - after = adapter.read_local_authority_shadow(runtime_root=w.runtime, goal_id=w.goal, scan_limit=20) - assert after["proof"]["transactions"] == before["proof"]["transactions"] - - -def test_raw_residue_mismatch_preserves_every_file_before_any_cleanup(tmp_path: Path) -> None: - w = workspace(tmp_path) - directory = outbox.partition_directory(w.runtime, w.goal, "todos") - residue: dict[str, bytes] = {} - for index in range(2): - w.crash("after_commit", "todo", "add", "--role", "agent", "--text", f"Exact receipt {index}") - residue.update({path.name: path.read_bytes() for path in directory.glob("*.json") if path.name != "drain-cursor.json"}) - assert w.drain()["ok"] is True - for name, data in residue.items(): - (directory / name).write_bytes(data) - last = sorted(directory.glob("*.prepared.json"))[-1] - # Valid identical JSON semantics still do not match the receipt's exact bytes. - last.write_bytes(last.read_bytes() + b"\n") - before = {path.name: path.read_bytes() for path in directory.iterdir()} - result = w.drain() - assert result["ok"] is False and result["reason_code"] == "outbox_receipt_mismatch", result - assert result["reclaimed_residue"] == 0 - assert {path.name: path.read_bytes() for path in directory.iterdir()} == before - - -def test_real_prepare_io_failure_holds_public_primary_before_replace(tmp_path: Path) -> None: - w = workspace(tmp_path) - directory = outbox.partition_directory(w.runtime, w.goal, "todos") - directory.write_text("A real filesystem obstruction, not a directory") - before = w.state.read_bytes() - rejected = w.cli("handoff-mode", "set", "--mode", "soft_claim", success=False) - assert rejected["ok"] is False, rejected - assert w.state.read_bytes() == before - assert directory.read_text() == "A real filesystem obstruction, not a directory" - directory.unlink() - w.cli("handoff-mode", "set", "--mode", "soft_claim") - assert w.drain()["ok"] is True - view = adapter.read_local_authority_shadow(runtime_root=w.runtime, goal_id=w.goal, scan_limit=20) - assert view["head"]["handoff_mode"] == "soft_claim" - assert len(view["proof"]["transactions"]) == 2 - assert view["proof"]["transactions"][1]["receipts"][0]["seq"] == 1 - - -@pytest.mark.parametrize( - ("window", "claimed_resolution"), - [("before_replace", "committed_proven_by_readback"), ("before_marker", "abandoned")], -) -def test_native_markerless_resolution_requires_source_evidence( - tmp_path: Path, window: str, claimed_resolution: str, -) -> None: - w = workspace(tmp_path) - w.crash(window, "todo", "add", "--role", "agent", "--text", "Source proof is not a caller flag") - directory = outbox.partition_directory(w.runtime, w.goal, "todos") - [entry] = outbox.list_entries(directory) - request = {"schema_version": "loopx_shadow_entry_delivery_request_v0", "runtime_root": str(w.runtime), - "goal_id": w.goal, "partition": entry.partition, "seq": entry.seq, "entry_id": entry.entry_id, - "capture_lineage_id": entry.prepared["capture_lineage_id"], - "prepared_sha256": "sha256:" + hashlib.sha256(entry.prepared_path.read_bytes()).hexdigest(), - "committed_sha256": None} - request["resolution"] = claimed_resolution - before = {path.name: path.read_bytes() for path in directory.iterdir()} - with pytest.raises(EffectRuntimeRejected, match="shadow_entry_selection_invalid"): - adapter.effect_runtime_result("coordination.runtime_shadow.commit_entry", request, timeout=15) - assert {path.name: path.read_bytes() for path in directory.iterdir()} == before - view = adapter.read_local_authority_shadow(runtime_root=w.runtime, goal_id=w.goal, scan_limit=20) - assert len(view["proof"]["transactions"]) == 1 - - # Without the caller flag, the same legal witness derives from source bytes. - request.pop("resolution") - delivered = adapter.effect_runtime_result("coordination.runtime_shadow.commit_entry", request, timeout=15) - assert delivered["outcome"] == "delivered" - assert delivered["resolution"] == ( - "abandoned" if window == "before_replace" else "committed_proven_by_readback" - ) - assert delivered["no_op"] is (window == "before_replace") - - -def test_registry_runtime_override_cannot_bypass_an_active_source_binding(tmp_path: Path) -> None: - import json - import subprocess - import sys - from shadow_e2e_fixture import REPO - - w = workspace(tmp_path) - for index in range(3): - w.add(f"Qualification evidence {index}") - before = w.state.read_bytes() - other_root = tmp_path / "other-runtime" - results = [] - for mode in ("soft_claim", "hard_lease"): - argv = w.arguments("handoff-mode", "set", "--mode", mode) - argv[argv.index("--runtime-root") + 1] = str(other_root) - command = subprocess.run([sys.executable, "-m", "loopx.cli", *argv], cwd=REPO, - capture_output=True, text=True, timeout=45) - assert "Traceback" not in command.stderr, command.stderr - results.append(json.loads(command.stdout)) - after_qualification = w.cli("coordination-shadow", "qualify", success=False) - assert not any(result.get("ok") and result.get("changed") for result in results), { - "override_results": results, "qualification_at_original_root": after_qualification, - } - assert w.state.read_bytes() == before - - - - -@pytest.mark.parametrize("missing", ["identity", "candidate"]) -def test_missing_cursor_source_allocation_never_recreates_missing_store_files( - tmp_path: Path, missing: str, -) -> None: - w = workspace(tmp_path) - w.add("Existing generation") - directory = outbox.partition_directory(w.runtime, w.goal, "todos") - (directory / "drain-cursor.json").unlink() - store = w.runtime / "authority-shadow" / "file-v0" - path = store / "store-identity" if missing == "identity" else next(store.glob("authority-store-*.json")) - path.unlink() - shadow = w.runtime / "authority-shadow" - before = {str(item.relative_to(shadow)): item.read_bytes() for item in shadow.rglob("*") if item.is_file()} - primary = w.state.read_bytes() - result = w.cli("todo", "add", "--role", "agent", "--text", "Read-only recovery cannot bootstrap", success=False) - assert result["ok"] is False, result - assert w.state.read_bytes() == primary - assert not path.exists() - assert {str(item.relative_to(shadow)): item.read_bytes() for item in shadow.rglob("*") if item.is_file()} == before - - -def test_state_file_override_cannot_attribute_an_unbound_source_to_active_lineage(tmp_path: Path) -> None: - w = workspace(tmp_path) - other = tmp_path / "OTHER_ACTIVE_STATE.md" - other.write_bytes(w.state.read_bytes()) - original = w.state.read_bytes() - outcomes = [] - for mode in ("soft_claim", "hard_lease", "soft_claim", "hard_lease"): - outcomes.append(w.cli("handoff-mode", "set", "--mode", mode, "--state-file", str(other), success=False)) - qualification = w.cli("coordination-shadow", "qualify", success=False) - assert not any(result.get("ok") and result.get("changed") for result in outcomes), { - "override_results": outcomes, "qualification_for_bound_source": qualification, - } - assert w.state.read_bytes() == original diff --git a/tests/control_plane/test_shadow_drain_e2e.py b/tests/control_plane/test_shadow_drain_e2e.py deleted file mode 100644 index 760069d95e..0000000000 --- a/tests/control_plane/test_shadow_drain_e2e.py +++ /dev/null @@ -1,268 +0,0 @@ -"""Receipt, cursor and crash invariants through real CLI and independent TS processes.""" - -from __future__ import annotations - -import json -from pathlib import Path -import subprocess -import sys - -import pytest - -from shadow_e2e_fixture import REPO, ShadowWorkspace, workspace -from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox - - -pytestmark = pytest.mark.stage2c_e2e - - -def directory(w: ShadowWorkspace) -> Path: - return outbox.partition_directory(w.runtime, w.goal, "todos") - - -def history(w: ShadowWorkspace) -> dict: - return adapter.read_local_authority_shadow( - runtime_root=w.runtime, goal_id=w.goal, scan_limit=10_000 - ) - - -def pending(w: ShadowWorkspace, text: str = "One primary mutation") -> dict: - return w.crash("before_commit", "todo", "add", "--role", "agent", "--text", text) - - -def snapshot(path: Path) -> dict[str, bytes]: - return { - str(item.relative_to(path)): item.read_bytes() - for item in path.rglob("*") - if item.is_file() - } - - -def test_public_primary_maps_one_to_one_to_receipts_and_replays_idempotently( - tmp_path: Path, -) -> None: - w = workspace(tmp_path) - ids = [w.add(f"Public fact {index}")["todo_id"] for index in range(3)] - w.drain() - view = history(w) - transactions = view["proof"]["transactions"] - assert len(transactions) == 4 # One complete baseline, three actual mutations. - assert [item["todo_id"] for item in view["head"]["todos"]] == sorted(ids) - assert view["head"]["handoff_mode"] == "hard_lease" - assert view["head"]["leases"] == [] - assert [tx["receipts"][0]["seq"] for tx in transactions[1:]] == [1, 2, 3] - assert all(tx["receipts"][0]["no_op"] is False for tx in transactions[1:]) - assert w.drain()["outcome"] == "nothing_pending" - assert history(w)["proof"]["transactions"] == transactions - assert [p.name for p in directory(w).iterdir()] == ["drain-cursor.json"] - for args in (("qualify",), ("read-candidate", "--todo-id", ids[0])): - arguments = w.arguments("coordination-shadow", *args) - arguments[arguments.index("--format") + 1] = "markdown" - result = subprocess.run([sys.executable, "-m", "loopx.cli", *arguments], - cwd=REPO, capture_output=True, text=True, timeout=30) - assert result.returncode == 0, result.stdout + result.stderr - assert "qualification_scope: `bounded`" in result.stdout - assert "sustained_parity_verdict: `not_evaluated`" in result.stdout - assert "minimum_primary_mutations: `3`" in result.stdout - - -def test_public_mutation_has_no_second_snapshot_mirror(tmp_path: Path) -> None: - w = workspace(tmp_path) - assert w.add("Exactly one durable primary mutation")["added"] is True - module = REPO / "loopx/control_plane/coordination/file_authority_store.ts" - script = ( - f"import {{FileAuthorityStore}} from {json.dumps(module.as_uri())};" - "const store=new FileAuthorityStore(process.argv[1],process.argv[2],{existingOnly:true});" - "process.stdout.write(JSON.stringify(await store.scanCommitted(null,10000)));" - ) - readback = subprocess.run( - ["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", script, - str(w.runtime / "authority-shadow/file-v0"), w.goal], - capture_output=True, text=True, check=True, timeout=30, - ) - page = json.loads(readback.stdout) - assert page["status"] == "page", page - assert len(page["transactions"]) == 2, page # Full baseline plus one mutation. - assert page["transactions"][0]["receipts"] == [] - assert len(page["transactions"][1]["receipts"]) == 1 - assert page["transactions"][1]["receipts"][0]["write_class"] == "todo_add" - - -@pytest.mark.parametrize( - "attack", - [ - "high_water", - "fake_digest", - "string_seq", - "boolean_seq", - "missing_anchor", - "wrong_root", - "wrong_lineage", - ], -) -def test_cursor_cannot_authorize_deletion_or_hide_mutations( - tmp_path: Path, attack: str -) -> None: - w = workspace(tmp_path) - w.add("First captured mutation") - pending(w, "Second real mutation") - d = directory(w) - cursor_path = d / "drain-cursor.json" - value = json.loads(cursor_path.read_bytes()) - if attack == "high_water": - value["last_seq"] = 9_999_999_999 - elif attack == "fake_digest": - value["last_partition_digest"] = "sha256:" + "f" * 64 - elif attack == "string_seq": - value["last_seq"] = "1" - elif attack == "boolean_seq": - value["last_seq"] = True - elif attack == "missing_anchor": - value["last_cursor"] = "uncommitted-cursor" - else: - entry = next(d.glob("*.prepared.json")) - record = json.loads(entry.read_bytes()) - record[ - "source_root_digest" if attack == "wrong_root" else "capture_lineage_id" - ] = "sha256:" + "a" * 64 if attack == "wrong_root" else "other-lineage" - entry.write_text(json.dumps(record)) - cursor_path.write_text(json.dumps(value)) - before = snapshot(d) - result = w.drain() - assert result["ok"] is False, result - assert result["reason_code"] in { - "outbox_cursor_invalid", - "outbox_cursor_unproved", - "outbox_file_invalid", - "stale_generation", - } - assert snapshot(d) == before - assert len(history(w)["proof"]["transactions"]) == 2 - status = w.cli("authority-shadow", "status", success=False) - assert "outbox" in status - - -@pytest.mark.parametrize("window", ["after_commit", "after_cursor", "between_unlinks"]) -def test_real_sigkill_drain_windows_recover_from_exact_receipts( - tmp_path: Path, window: str -) -> None: - w = workspace(tmp_path) - w.crash( - window, - "todo", - "add", - "--role", - "agent", - "--text", - "Committed before process death", - ) - before = history(w)["proof"]["transactions"] - assert len(before) == 2 - result = w.drain() - assert result["ok"] is True, result - assert result["replayed"] == 1 - assert result["delivered"] == 0 - assert history(w)["proof"]["transactions"] == before - assert outbox.read_cursor(directory(w))["last_seq"] == 1 - assert [p.name for p in directory(w).iterdir()] == ["drain-cursor.json"] - - -def test_missing_cursor_recovers_only_from_complete_verified_history( - tmp_path: Path, -) -> None: - w = workspace(tmp_path) - w.add("First") - w.add("Second") - before = history(w)["proof"]["transactions"] - (directory(w) / "drain-cursor.json").unlink() - assert w.drain()["ok"] is True - assert outbox.read_cursor(directory(w))["last_seq"] == 2 - w.add("Third") - assert history(w)["proof"]["transactions"][:3] == before - assert outbox.read_cursor(directory(w))["last_seq"] == 3 - - -@pytest.mark.parametrize("window", ["before_replace", "after_replace", "before_marker"]) -def test_primary_sigkill_preserves_complete_bytes_and_proves_before_marker( - tmp_path: Path, window: str -) -> None: - w = workspace(tmp_path) - old = w.state.read_bytes() - w.crash( - window, "todo", "add", "--role", "agent", "--text", "Atomic primary sentence" - ) - current = w.state.read_bytes() - listed = w.cli("todo", "list")["todos"] - assert (current == old) is (window == "before_replace") - assert len(listed) == (0 if window == "before_replace" else 1) - assert not list(directory(w).glob("*.committed.json")) - result = w.drain() - assert result["ok"] is True, result - receipt = history(w)["proof"]["transactions"][-1]["receipts"][0] - assert receipt["resolution"] == ( - "abandoned" if window == "before_replace" else "committed_proven_by_readback" - ) - assert receipt["no_op"] is (window == "before_replace") - - -def test_prepared_a_b_a_never_infers_first_write_was_abandoned(tmp_path: Path) -> None: - w = workspace(tmp_path) - w.crash("before_marker", "handoff-mode", "set", "--mode", "soft_claim") - # A later real writer returns the canonical primary to its initial A. - w.cli("handoff-mode", "set", "--mode", "hard_lease") - assert w.cli("handoff-mode", "show")["handoff_mode"] == "hard_lease" - before = snapshot(directory(w)) - result = w.drain() - assert ( - result["ok"] is False and result["reason_code"] == "outbox_source_unproved" - ), result - assert snapshot(directory(w)) == before - assert len(history(w)["proof"]["transactions"]) == 1 - qualify = w.cli("coordination-shadow", "qualify", success=False) - assert qualify["ok"] is False - - -def test_concurrent_real_drainers_commit_once_without_cursor_regression( - tmp_path: Path, -) -> None: - w = workspace(tmp_path) - for index in range(3): - pending(w, f"Concurrent {index}") - argv = [ - sys.executable, - "-m", - "loopx.cli", - *w.arguments("authority-shadow", "drain", "--lock-timeout-seconds", "2"), - ] - children = [ - subprocess.Popen( - argv, cwd=REPO, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True - ) - for _ in range(2) - ] - for child in children: - stdout, stderr = child.communicate(timeout=45) - assert "Traceback" not in stderr - result = json.loads(stdout) - assert result["outcome"] in {"drained", "nothing_pending", "drain_deferred"}, ( - result - ) - assert w.drain()["ok"] is True - txs = history(w)["proof"]["transactions"] - assert len(txs) == 4 - assert len({tx["operation_id"] for tx in txs}) == 4 - assert outbox.read_cursor(directory(w))["last_seq"] == 3 - - -def test_bounded_drain_preserves_unprocessed_entries(tmp_path: Path) -> None: - w = workspace(tmp_path) - for index in range(3): - pending(w, f"Bounded {index}") - result = w.drain(max_entries="1") - assert result["ok"] and result["delivered"] == 1 and result["budget_exhausted"], ( - result - ) - assert [entry.seq for entry in outbox.list_entries(directory(w))] == [2, 3] - assert w.drain()["ok"] - assert len(history(w)["proof"]["transactions"]) == 4 diff --git a/tests/control_plane/test_shadow_drain_native_plan.py b/tests/control_plane/test_shadow_drain_native_plan.py index c2b5801f31..973619bda4 100644 --- a/tests/control_plane/test_shadow_drain_native_plan.py +++ b/tests/control_plane/test_shadow_drain_native_plan.py @@ -1,21 +1,15 @@ -"""The Python entry transports one batch, with no source/history decision owner. - -Filesystem mutation and failure-window cases moved to shadow_drain.test.ts and -real TS-process SIGKILL tests; patching retired Python internals proves nothing. -""" +"""The retained Python drain adapter stays inert when no history exists.""" from __future__ import annotations -from pathlib import Path import json -from typing import Any - -import pytest from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from test_local_authority_shadow_drain import _drain, _fixture, _record_todo_write -def test_disabled_without_capture_state_does_not_start_native_runtime(tmp_path, monkeypatch): +def test_disabled_without_history_does_not_start_native_runtime( + tmp_path, + monkeypatch, +): registry = tmp_path / "registry.json" registry.write_text(json.dumps({"goals": [{"id": "goal-e2e"}]})) @@ -23,52 +17,13 @@ def unexpected(*args, **kwargs): raise AssertionError("feature-off drain must not invoke the runtime") monkeypatch.setattr(adapter, "effect_runtime_result", unexpected) - result = _drain(registry, tmp_path / "runtime") - assert result.ok and result.outcome == "nothing_pending" + result = adapter.drain_local_authority_shadow_outbox( + registry_path=registry, + runtime_root=tmp_path / "runtime", + goal_id="goal-e2e", + max_entries=20, + budget_seconds=10, + ) + assert result.ok + assert result.outcome == "nothing_pending" assert not (tmp_path / "runtime").exists() - - -def test_adapter_transports_one_batch_without_per_entry_rpc( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, state, runtime = _fixture(tmp_path) - for i in range(3): - _record_todo_write(registry, state, runtime, f"Private source text {i}") - actual = adapter.effect_runtime_result - seen = [] - - def rpc(method: str, request: dict, **kwargs: Any) -> dict: - result = actual(method, request, **kwargs) - seen.append((method, request, result, kwargs)) - return result - - monkeypatch.setattr(adapter, "effect_runtime_result", rpc) - result = _drain(registry, runtime) - assert result.ok and result.delivered == 3 - assert len(seen) == 1 - method, request, response, kwargs = seen[0] - assert method == "coordination.runtime_shadow.drain" - assert kwargs["retry_safe"] is False - assert "projection" not in request and "entries" not in request - assert "proof" not in response and "transactions" not in response and "head" not in response - - -def test_lost_batch_response_reports_unknown_and_explicit_retry_reads_durable_facts( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, state, runtime = _fixture(tmp_path) - _record_todo_write(registry, state, runtime, "Retain this committed write") - actual = adapter.effect_runtime_result - - def lose_response(method: str, request: dict, **kwargs: Any) -> dict: - actual(method, request, **kwargs) - raise TimeoutError("lost response after successful native drain") - - monkeypatch.setattr(adapter, "effect_runtime_result", lose_response) - failed = _drain(registry, runtime) - assert not failed.ok and failed.reason_code == "shadow_drain_outcome_unknown" - monkeypatch.setattr(adapter, "effect_runtime_result", actual) - recovered = _drain(registry, runtime) - assert recovered.ok and recovered.outcome == "nothing_pending" - view = adapter.read_local_authority_shadow(runtime_root=runtime, goal_id="goal-e2e", scan_limit=10) - assert len(view["proof"]["transactions"]) == 2 diff --git a/tests/control_plane/test_shadow_fence_caller_parity_e2e.py b/tests/control_plane/test_shadow_fence_caller_parity_e2e.py deleted file mode 100644 index db803da2d7..0000000000 --- a/tests/control_plane/test_shadow_fence_caller_parity_e2e.py +++ /dev/null @@ -1,330 +0,0 @@ -"""Fenced sibling-caller parity through the real CLI (Stage 2C E2E). - -Every row runs one public command against a workspace whose durable legacy -writer fence is in one state, and compares the complete printed envelope, the -exit status, and the primary-record effect with the literal expectation in -tests/fixtures/control_plane/legacy_writer_fence_caller_parity_v0.json. -Nothing is mocked; the fence is engaged by the real TypeScript owner. -""" -from __future__ import annotations - -import json -import shutil -import sys -from collections.abc import Callable, Iterator -from pathlib import Path - -import pytest -from test_shadow_observable_e2e import Caller -from test_shadow_observable_native_e2e import native - -pytestmark = pytest.mark.stage2c_e2e - -FIXTURE = Path(__file__).resolve().parents[1] / "fixtures" / "control_plane" / "legacy_writer_fence_caller_parity_v0.json" -BUILDER = ( - "from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection as build; " - "import json,sys; value=build(goal_id='observable', todos=json.loads(sys.argv[1]), " - "leases=json.loads(sys.argv[2])); " - "value['handoff_mode']='hard_lease'; print(json.dumps(value))" -) -PLACEHOLDERS = ("runtime_root", "todo_a", "todo_b", "todo_c", "todo_gate") - - -class Workspace: - """One seeded goal; rows are executed against it in fixture order.""" - - def __init__(self, path: Path, mode: str, name: str) -> None: - self.w = Caller(path, mode, name) - self.ids: dict[str, str] = {} - self.lease_version = 0 - self.lease_owner = "agent-a" - self.lease_key = "parity-lease" - self.control: dict[str, dict] | None = None - - def call(self, *args: str) -> dict: - return self.w.call(*args) - - def exit(self) -> int: - return int(self.w.rows[-1]["exit"]) - - def outbox(self) -> set[str]: - return {k for k in self.w.files() if k.startswith("runtime/authority-shadow/outbox/")} - - def fence_path(self) -> Path: - from loopx.control_plane.coordination.legacy_writer_fence import ( - legacy_coordination_writer_fence_path, - ) - - return legacy_coordination_writer_fence_path(runtime_root=self.w.root, goal_id="observable") - - def normalize(self, value: object) -> object: - text = json.dumps(value) - text = text.replace(json.dumps(str(self.w.root))[1:-1], "{runtime_root}") - for key in ("todo_gate", "todo_c", "todo_b", "todo_a"): - if key in self.ids: - text = text.replace(self.ids[key], "{" + key + "}") - return json.loads(text) - - def observe(self, args: tuple[str, ...]) -> dict: - before, outbox_before = self.w.primary(), self.outbox() - envelope = self.call(*args) - after, outbox_after = self.w.primary(), self.outbox() - return { - "envelope": self.normalize(envelope), - "exit": self.exit(), - "effect": { - "added": sorted(k for k in after if k not in before), - "removed": sorted(k for k in before if k not in after), - "changed": sorted(k for k in after if k in before and after[k] != before[k]), - }, - "outbox_added": sorted(outbox_after - outbox_before), - } - - -def seed_and_fence(ws: Workspace, todo_keys: tuple[str, ...]) -> None: - records = [ws.w.read(ws.ids[key]) for key in todo_keys] - lease_path = ( - ws.w.root / "goals" / "observable" / "task-leases" / - f"{ws.ids['todo_b']}.json" - ) - leases = [json.loads(lease_path.read_text(encoding="utf-8"))] - projection = ws.w.invoke( - [sys.executable, "-c", BUILDER, json.dumps(records), json.dumps(leases)], - ["fixture-projection", json.dumps(records), ""], - ) - assert native(ws.w, "seed", projection)["status"] == "applied" - - -def build_seeded(path: Path, mode: str, name: str, *, gate: bool) -> Workspace: - ws = Workspace(path, mode, name) - assert ws.call("handoff-mode", "set", "--mode", "hard_lease")["ok"] is True - ws.ids["todo_a"] = ws.w.add("Parity target A") - ws.ids["todo_b"] = ws.w.add("Parity leased B") - ws.ids["todo_c"] = ws.w.add("Parity fresh acquisition C") - if gate: - gate_add = ws.call("todo", "add", "--role", "user", "--task-class", "user_gate", "--global-gate", - "--text", "Approve the parity plan") - assert gate_add["ok"] is True, gate_add - ws.ids["todo_gate"] = gate_add["todo_id"] - ws.control = {"cli-task_lease_acquire-absent": ws.observe(( - "task-lease", "acquire", "--todo-id", ws.ids["todo_b"], "--owner", "agent-a", - "--idempotency-key", "parity-lease", "--ttl-seconds", "3600"))} - lease = ws.control["cli-task_lease_acquire-absent"]["envelope"] - assert lease.get("acquired") is True, lease - ws.lease_version = int(lease["lease"]["version"]) - seed_and_fence(ws, ("todo_a", "todo_b", "todo_c", "todo_gate") if gate else ("todo_a", "todo_b", "todo_c")) - return ws - - -def build_w3(path: Path) -> Workspace: - ws = build_seeded(path, "absent", "fence-parity-invalid", gate=False) - marker = json.loads(ws.fence_path().read_text(encoding="utf-8")) - marker["state"] = "disengaged" - ws.fence_path().write_text(json.dumps(marker), encoding="utf-8") - return ws - - -def build_w4(path: Path) -> Workspace: - ws = build_seeded(path, "absent", "fence-parity-unreadable", gate=False) - ws.fence_path().unlink() - ws.fence_path().mkdir() - return ws - - -def build_w5(path: Path) -> Workspace: - ws = Workspace(path, "absent", "fence-parity-no-store") - assert ws.call("handoff-mode", "set", "--mode", "hard_lease")["ok"] is True - ws.ids["todo_a"] = ws.w.add("Parity target A") - ws.fence_path().parent.mkdir(parents=True, exist_ok=True) - ws.fence_path().write_text(json.dumps({ - "schema_version": "loopx_legacy_coordination_writer_fence_v0", "state": "engaged", "goal_id": "observable", - "fence_id": "caller-fixture", "source_version": "caller-fixture", - "source_projection_sha256": "a" * 64, "expected_shadow_provider_revision": "file:1:aaaaaaaaaaaaaaaaaaaaaaaa", - }), encoding="utf-8") - return ws - - -def lease_args(ws: Workspace, verb: str) -> tuple[str, ...]: - todo_b, version = ws.ids.get("todo_b", ""), str(ws.lease_version) - if verb == "acquire": - return ("task-lease", "acquire", "--todo-id", ws.ids.get("todo_c", ws.ids.get("todo_a", "")), "--owner", "agent-a", - "--idempotency-key", "parity-acquire", "--ttl-seconds", "3600") - common = ("task-lease", verb, "--todo-id", todo_b, "--owner", ws.lease_owner, - "--idempotency-key", ws.lease_key, "--expected-version", version) - if verb == "renew": - return (*common, "--ttl-seconds", "7200") - if verb == "transfer": - return (*common, "--new-owner", "agent-b", "--new-idempotency-key", "parity-transfer") - return common - - -def row_args(ws: Workspace, caller: str) -> tuple[str, ...]: - a = ws.ids.get("todo_a", "") - b = ws.ids.get("todo_b", "") - gate = ws.ids.get("todo_gate", "") - version = str(ws.lease_version) - table: dict[str, tuple[str, ...]] = { - "task_lease_acquire": lease_args(ws, "acquire"), - "task_lease_renew": lease_args(ws, "renew"), - "task_lease_transfer": lease_args(ws, "transfer"), - "task_lease_release": lease_args(ws, "release"), - "todo_complete": ("todo", "complete", "--todo-id", a, "--agent-id", "agent-a", - "--evidence", "validation://parity", "--no-follow-up"), - "todo_update_status": ("todo", "update", "--todo-id", a, "--agent-id", "agent-a", "--status", "deferred"), - "todo_supersede": ("todo", "supersede", "--todo-id", a, "--agent-id", "agent-a", - "--text", "Parity successor", "--evidence", "validation://parity"), - "todo_archive_completed_execute": ("todo", "archive-completed", "--execute"), - "todo_archive_completed_preview": ("todo", "archive-completed"), - "handoff_mode_set": ("handoff-mode", "set", "--mode", "soft_claim"), - "todo_complete_dry_run_gate": ("todo", "complete", "--todo-id", gate, "--agent-id", "agent-a", - "--decision-outcome", "approve", "--evidence", "validation://parity", - "--no-follow-up", "--dry-run"), - "todo_supersede_dry_run": ("todo", "supersede", "--todo-id", a, "--agent-id", "agent-a", - "--text", "Parity successor", "--evidence", "validation://parity", "--dry-run"), - "todo_complete_dry_run_leased": ("todo", "complete", "--todo-id", b, "--agent-id", ws.lease_owner, - "--task-lease-idempotency-key", ws.lease_key, - "--task-lease-expected-version", version, - "--evidence", "validation://parity", "--no-follow-up", "--dry-run"), - } - return table[caller] - - -WORKSPACES = { - "w1": lambda path: build_seeded(path, "absent", "fence-parity-engaged", gate=True), - "w2": lambda path: build_seeded(path, "enabled", "fence-parity-engaged-capture", gate=False), - "w3": build_w3, - "w4": build_w4, - "w5": build_w5, -} - - -def load_rows() -> list[dict]: - if not FIXTURE.exists(): - # Keep the module importable for the fixture recorder; the test itself fails loudly. - return [{"id": "fixture-missing", "surface": "cli", "workspace": None}] - return [row for row in json.loads(FIXTURE.read_text(encoding="utf-8"))["rows"] if row["surface"] == "cli"] - - -@pytest.fixture(scope="module") -def workspaces(tmp_path_factory: pytest.TempPathFactory) -> Iterator[Callable[[str], Workspace]]: - built: dict[str, Workspace] = {} - - def get(key: str) -> Workspace: - # Single-row mutation probes need only this row's workspace. Keep reuse - # and ordered mutations within a module, never across worker processes. - if key not in built: - built[key] = WORKSPACES[key](tmp_path_factory.mktemp(key)) - return built[key] - - yield get - for ws in built.values(): - shutil.rmtree(ws.w.path, ignore_errors=True) - - -def observe_row(ws: Workspace, row: dict) -> dict: - if ws.control and row["id"] in ws.control: - return ws.control[row["id"]] - return ws.observe(row_args(ws, row["caller"])) - - -def assert_canonical_lease_transition(ws: Workspace, caller: str, before: dict, observed: dict) -> None: - """Prove the caller's transition and carry only independently checked proof.""" - assert "lease_path" not in observed["envelope"], observed - canonical_after = native(ws.w, "read", {}) - old_head, new_head = before["head"], canonical_after["head"] - assert int(new_head["cursor"]) == int(old_head["cursor"]) + 1 - assert new_head["head"]["todos"] == old_head["head"]["todos"] - target = ws.ids["todo_b"] - old_leases, new_leases = old_head["head"]["leases"], new_head["head"]["leases"] - assert [lease for lease in new_leases if lease["todo_id"] != target] == [ - lease for lease in old_leases if lease["todo_id"] != target - ] - old = next(lease for lease in old_leases if lease["todo_id"] == target) - current = next(lease for lease in new_leases if lease["todo_id"] == target) - assert observed["envelope"]["lease"] == ws.normalize(current) - released, transferred = caller == "task_lease_release", caller == "task_lease_transfer" - expected = { - "version": old["version"] + (0 if released else 1), - "lease_epoch": old["lease_epoch"] + (1 if transferred else 0), - "owner": "agent-b" if transferred else old["owner"], - "idempotency_key": "parity-transfer" if transferred else old["idempotency_key"], - "write_scopes": old["write_scopes"], - "status": "released" if released else "active", - } - assert {key: current[key] for key in expected} == expected - if released: - assert current["expires_at"] == old["expires_at"] - assert current["released_at"] == current["updated_at"] - else: - # Renew invalidates the old version; transfer also retires the sender's - # owner/key. Preserve those distinct completion-proof rejections. - stale = ws.call(*row_args(ws, "todo_complete_dry_run_leased")) - code = "lease_cas_mismatch" if transferred else "version_mismatch" - assert stale["ok"] is False and stale["error_code"] == code, stale - assert native(ws.w, "read", {}) == canonical_after - ws.lease_version = expected["version"] - ws.lease_owner, ws.lease_key = expected["owner"], expected["idempotency_key"] - # The receiver's current proof previews successfully, while a released - # proof cannot authorize completion. Neither preview may mutate authority. - preview = ws.call(*row_args(ws, "todo_complete_dry_run_leased")) - if released: - assert preview["ok"] is False and preview["error_code"] == "handoff_mode_requires_lease", preview - else: - assert preview["ok"] is True and preview["completed"] is True, preview - assert native(ws.w, "read", {}) == canonical_after - - -def assert_canonical_acquisition(ws: Workspace, before: dict, observed: dict) -> None: - """New execution C must not change the existing B proof or keyless A tests.""" - after = native(ws.w, "read", {}) - old, current = before["head"], after["head"] - assert int(current["cursor"]) == int(old["cursor"]) + 1 - assert current["head"]["todos"] == old["head"]["todos"] - target = ws.ids["todo_c"] - assert [r for r in current["head"]["leases"] if r["todo_id"] != target] == old["head"]["leases"] - lease = next(r for r in current["head"]["leases"] if r["todo_id"] == target) - assert lease["owner"] == "agent-a" and lease["idempotency_key"] == "parity-acquire" - assert lease["version"] == lease["lease_epoch"] == 1 and lease["status"] == "active" - assert observed["envelope"]["lease"] == ws.normalize(lease) - assert "lease_path" not in observed["envelope"] - replay = ws.call(*lease_args(ws, "acquire")) - assert replay["ok"] and replay["idempotent"] and not replay["acquired"] - assert replay["lease"] == lease - assert native(ws.w, "read", {}) == after - - -@pytest.mark.parametrize("row", load_rows(), ids=[row["id"] for row in load_rows()]) -def test_fence_caller_parity(workspaces: Callable[[str], Workspace], row: dict) -> None: - if row["id"] == "fixture-missing": - pytest.fail(f"parity fixture is missing: {FIXTURE}") - ws = workspaces(row["workspace"]) - canonical_before = native(ws.w, "read", {}) if row["caller"] in { - "task_lease_renew", "task_lease_transfer", "task_lease_release", - } or (row["caller"] == "task_lease_acquire" and row["fence_state"] == "engaged") else None - observed = observe_row(ws, row) - assert observed["exit"] == row["exit"], observed - if row.get("match") == "subset": - assert {key: observed["envelope"].get(key) for key in row["expect"]} == row["expect"], observed - else: - assert observed["envelope"] == row["expect"], observed - if row["caller"] == "handoff_mode_set": - # This command now crosses the canonical boundary. Its dynamic revision, - # operation ID and lease expiry are not a literal legacy-writer envelope. - assert observed["envelope"].get("claimed_todos") or observed["envelope"].get("active_leases"), observed - assert observed["envelope"].get("provider_revision"), observed - if canonical_before is not None: - # Public canonical callers change the provider; legacy wire rows stay fenced. - if row["caller"] == "task_lease_acquire": - assert_canonical_acquisition(ws, canonical_before, observed) - else: - assert_canonical_lease_transition(ws, row["caller"], canonical_before, observed) - assert observed["effect"] == row["effect"], observed - assert observed["outbox_added"] == row.get("outbox_added", []), observed - - -def test_baseline_annotations_are_not_stale() -> None: - for row in load_rows(): - for revision, delta in (row.get("baseline") or {}).items(): - if revision == "note": - continue - assert delta != row["expect"], row["id"] diff --git a/tests/control_plane/test_shadow_goal_instance.py b/tests/control_plane/test_shadow_goal_instance.py deleted file mode 100644 index ff979fbd17..0000000000 --- a/tests/control_plane/test_shadow_goal_instance.py +++ /dev/null @@ -1,379 +0,0 @@ -from __future__ import annotations - -import json -import os -import subprocess -import sys -from pathlib import Path - -import pytest - -from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from loopx.control_plane.coordination.runtime_shadow import ( - bootstrap_coordination_runtime_shadow, - build_runtime_shadow_source_snapshot, - rollback_coordination_runtime_shadow, -) -from loopx.control_plane.coordination.runtime_shadow_writer_adapter import ( - begin_todo_runtime_shadow_capture, - write_captured_todo_state, -) -from loopx.control_plane.coordination.shadow_management import ( - ShadowManagementError, - read_shadow_management_state, -) -from loopx.control_plane.coordination.shadow_goal_scope import shadow_goal_scope -from loopx.control_plane.goals.source_session_recreation import ( - RecreateGoalRequest, - recreate_goal_instance, -) -from loopx.control_plane.goals.source_session_registration import ( - FreshSourceSessionRegistration, - register_fresh_source_session_project, -) -from loopx.control_plane.projects.registry_codec import load_project_registry -from loopx.registry import find_registry_goal - - -GOAL_ID = "shadow-lifetime" -AGENT_ID = "shadow-worker" -REPO_ROOT = Path(__file__).resolve().parents[2] - - -def _register(tmp_path: Path) -> tuple[Path, Path, Path, dict[str, str]]: - project_root = tmp_path / "project" - project_root.mkdir() - runtime_root = tmp_path / "runtime" - registry_path = project_root / ".loopx" / "registry.json" - result = register_fresh_source_session_project( - FreshSourceSessionRegistration( - registry_path=registry_path, - runtime_root=runtime_root, - operation_id="register-shadow-lifetime", - project_id="project", - goal_id=GOAL_ID, - objective="Qualify exact shadow delivery.", - non_goals=[], - acceptance=["A replacement cannot drain the retired Goal outbox."], - unknowns=[], - next_effect="Bootstrap the exact shadow.", - stop_condition="Stop when the exact drain is fenced.", - project_record={ - "id": "project", - "kind": "work", - "path": str(project_root), - }, - goal_record={ - "id": GOAL_ID, - "project_id": "project", - "title": "Shadow lifetime", - "status": "active", - "repo": str(project_root), - "state_file": "GOAL.md", - "coordination": { - "registered_agents": [AGENT_ID], - "runtime_shadow": { - "schema_version": ( - "loopx_coordination_runtime_shadow_config_v0" - ), - "enabled": True, - "provider": "file_v0", - }, - }, - }, - state_file=project_root / "GOAL.md", - ) - ) - goal_ref = dict(result["goal_ref"]) - return registry_path, runtime_root, project_root / "GOAL.md", goal_ref - - -def _bootstrap( - registry_path: Path, - runtime_root: Path, - state_path: Path, - goal_ref: dict[str, str], - operation_id: str, -) -> dict[str, object]: - registry = load_project_registry(registry_path) - goal = find_registry_goal(registry, GOAL_ID) - assert goal is not None - projection, snapshot = build_runtime_shadow_source_snapshot( - goal=goal, - runtime_root=runtime_root, - state_path=state_path, - registry_path=registry_path, - ) - result = bootstrap_coordination_runtime_shadow( - goal=goal, - runtime_root=runtime_root, - goal_id=GOAL_ID, - operation_id=operation_id, - source_version=operation_id, - projection=projection, - source_snapshot=snapshot, - goal_ref=goal_ref, - ) - assert result["status"] in {"applied", "recovered", "replayed"}, result - return result - - -def _add_todo( - registry_path: Path, - runtime_root: Path, - state_path: Path, - text: str, - todo_id: str, -) -> str: - original = state_path.read_text(encoding="utf-8") - heading = "## Agent Todo\n" - planned = original.replace( - heading, - heading - + f"\n- [ ] {text}\n" - + f" \n", - 1, - ) - capture = begin_todo_runtime_shadow_capture( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=GOAL_ID, - state_path=state_path, - write_class="todo_add", - original_text=original, - ) - write_captured_todo_state( - capture, - runtime_root=runtime_root, - goal_id=GOAL_ID, - state_path=state_path, - text=planned, - ) - assert capture.outcome.entry_id is not None - return capture.outcome.entry_id - - -def _outbox_bytes(runtime_root: Path) -> dict[str, bytes]: - root = runtime_root / "authority-shadow" / "outbox" / GOAL_ID - return { - str(path.relative_to(root)): path.read_bytes() - for path in root.rglob("*") - if path.is_file() - } - - -def test_source_session_bootstrap_is_reachable_from_shipped_cli( - tmp_path: Path, -) -> None: - registry_path, runtime_root, _state_path, goal_ref = _register(tmp_path) - - command = subprocess.run( - [ - sys.executable, - "-m", - "loopx.entrypoint", - "--registry", - str(registry_path), - "--format", - "json", - "coordination-shadow", - "bootstrap", - "--goal-id", - GOAL_ID, - "--execute", - ], - cwd=REPO_ROOT, - env={**os.environ, "PYTHONPATH": str(REPO_ROOT), "LOOPX_USAGE_PING": "0"}, - text=True, - capture_output=True, - check=False, - timeout=45, - ) - - assert command.returncode == 0, command.stdout + command.stderr - assert json.loads(command.stdout)["bootstrap"]["status"] in { - "applied", - "recovered", - "replayed", - } - state = read_shadow_management_state(runtime_root, GOAL_ID) - assert state is not None - assert state["schema_version"] == "loopx_shadow_management_state_v2" - assert state["binding"]["goal_ref"] == goal_ref - - -def test_source_session_bootstrap_uses_the_current_typed_goal_scope( - tmp_path: Path, -) -> None: - registry_path, runtime_root, state_path, goal_ref = _register(tmp_path) - goal = find_registry_goal(load_project_registry(registry_path), GOAL_ID) - assert goal is not None - projection, snapshot = build_runtime_shadow_source_snapshot( - goal=goal, - runtime_root=runtime_root, - state_path=state_path, - registry_path=registry_path, - ) - with shadow_goal_scope(registry_path, goal_id=GOAL_ID) as scope: - result = bootstrap_coordination_runtime_shadow( - goal=scope.goal, - runtime_root=runtime_root, - goal_id=GOAL_ID, - operation_id="bootstrap:implicit-goal-ref", - source_version="bootstrap:implicit-goal-ref", - projection=projection, - source_snapshot=snapshot, - goal_ref=scope.goal_ref, - ) - - assert result["status"] in {"applied", "recovered", "replayed"}, result - _add_todo( - registry_path, - runtime_root, - state_path, - "Capture work after an implicit exact bootstrap.", - "todo_shadow_implicit", - ) - state = read_shadow_management_state(runtime_root, GOAL_ID) - assert state is not None - assert state["binding"]["goal_ref"] == goal_ref - - -def test_recreated_goal_cannot_capture_or_drain_the_retired_shadow( - tmp_path: Path, -) -> None: - registry_path, runtime_root, state_path, goal_a = _register(tmp_path) - first = _bootstrap( - registry_path, - runtime_root, - state_path, - goal_a, - "bootstrap:goal-a", - ) - _add_todo( - registry_path, - runtime_root, - state_path, - "Persist Goal A work before replacement.", - "todo_shadow_a", - ) - pending_a = _outbox_bytes(runtime_root) - in_flight_original = state_path.read_text(encoding="utf-8") - in_flight_a = begin_todo_runtime_shadow_capture( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=GOAL_ID, - state_path=state_path, - write_class="todo_update", - original_text=in_flight_original, - ) - - recreated = recreate_goal_instance( - RecreateGoalRequest( - registry_path=registry_path, - goal_id=GOAL_ID, - goal_instance_id=goal_a["goal_instance_id"], - operation_id="recreate-shadow-lifetime", - ) - ) - goal_b = dict(recreated["goal_ref"]) - assert goal_b != goal_a - - with pytest.raises(ShadowManagementError, match="retired Goal instance"): - write_captured_todo_state( - in_flight_a, - runtime_root=runtime_root, - goal_id=GOAL_ID, - state_path=state_path, - text=in_flight_original.replace( - "Persist Goal A", - "Continue retired Goal A", - ), - ) - assert state_path.read_text(encoding="utf-8") == in_flight_original - - stale = adapter.drain_local_authority_shadow_outbox( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=GOAL_ID, - max_entries=10, - budget_seconds=10, - lock_timeout_seconds=2, - ) - assert stale.outcome == "stopped" - assert stale.reason_code == "stale_goal_instance" - assert stale.delivered == 0 - assert _outbox_bytes(runtime_root) == pending_a - - original = state_path.read_text(encoding="utf-8") - capture_b = begin_todo_runtime_shadow_capture( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=GOAL_ID, - state_path=state_path, - write_class="todo_add", - original_text=original, - ) - with pytest.raises(ShadowManagementError, match="durable shadow preparation"): - write_captured_todo_state( - capture_b, - runtime_root=runtime_root, - goal_id=GOAL_ID, - state_path=state_path, - text=original.replace("Persist Goal A", "Mutate Goal B"), - ) - assert state_path.read_text(encoding="utf-8") == original - - goal = find_registry_goal(load_project_registry(registry_path), GOAL_ID) - assert goal is not None - retired = rollback_coordination_runtime_shadow( - goal=goal, - runtime_root=runtime_root, - goal_id=GOAL_ID, - operation_id="rollback:goal-a", - expected_provider_revision=str(first["provider_revision"]), - projection={}, - source_snapshot={"state_path": str(state_path)}, - ) - assert retired["status"] == "applied", retired - archived = Path(str(retired["outbox_archive_path"])) - assert { - str(path.relative_to(archived)): path.read_bytes() - for path in archived.rglob("*") - if path.is_file() - } == pending_a - - _bootstrap( - registry_path, - runtime_root, - state_path, - goal_b, - "bootstrap:goal-b", - ) - _add_todo( - registry_path, - runtime_root, - state_path, - "Persist Goal B work after replacement.", - "todo_shadow_b", - ) - current = adapter.drain_local_authority_shadow_outbox( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=GOAL_ID, - max_entries=10, - budget_seconds=10, - lock_timeout_seconds=2, - ) - assert current.outcome == "drained" - assert current.delivered == 1 - replay = adapter.drain_local_authority_shadow_outbox( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=GOAL_ID, - max_entries=10, - budget_seconds=10, - lock_timeout_seconds=2, - ) - assert replay.outcome == "nothing_pending" diff --git a/tests/control_plane/test_shadow_management_e2e.py b/tests/control_plane/test_shadow_management_e2e.py deleted file mode 100644 index 7491afec69..0000000000 --- a/tests/control_plane/test_shadow_management_e2e.py +++ /dev/null @@ -1,312 +0,0 @@ -"""Public CLI and real Python -> TypeScript management interleavings. - -All goals and providers are disposable. The scheduling seam delays a real RPC; -it never supplies a substitute provider result or edits an active user goal. -""" - -from __future__ import annotations - -import hashlib -import json -from pathlib import Path -import select -import subprocess -import sys -import time - -import pytest - -from loopx.control_plane.coordination.shadow_management import read_shadow_management_state -from loopx.control_plane.coordination.runtime_shadow import build_runtime_shadow_source_snapshot -from loopx.control_plane.coordination.coordination_state_contract_generated import ( - COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA, -) -from loopx.control_plane.effect_runtime import effect_runtime_result - -REPO_ROOT = Path(__file__).resolve().parents[2] - - -pytestmark = pytest.mark.stage2c_e2e - - -def _workspace(tmp_path: Path) -> tuple[Path, Path]: - runtime = tmp_path / "runtime" - goals = [] - for goal_id in ("goal-a", "goal-b"): - repo = tmp_path / goal_id - repo.mkdir() - (repo / "ACTIVE_GOAL_STATE.md").write_text( - f"---\ngoal_id: {goal_id}\nhandoff_mode: hard_lease\n" - "updated_at: 2026-01-01T00:00:00+00:00\n---\n\n## Agent Todo\n\n" - ) - goals.append({ - "id": goal_id, "status": "active", "repo": str(repo), - "state_file": "ACTIVE_GOAL_STATE.md", - "coordination": { - "agent_model": "peer_v1", "registered_agents": ["agent-a", "agent-b"], - "runtime_shadow": { - "schema_version": "loopx_coordination_runtime_shadow_config_v0", - "enabled": True, "provider": "file_v0", - }, - }, - }) - registry = tmp_path / "registry.json" - registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": goals})) - return registry, runtime - - -def _arguments(registry: Path, runtime: Path, *args: str) -> list[str]: - return ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", *args] - - -def _cli(registry: Path, runtime: Path, *args: str, success: bool = True) -> dict: - result = subprocess.run( - [sys.executable, "-m", "loopx.cli", *_arguments(registry, runtime, *args)], - cwd=REPO_ROOT, capture_output=True, text=True, timeout=30, - ) - if success: - assert result.returncode == 0, f"{result.stdout}\n{result.stderr}" - return json.loads(result.stdout) - - -def _candidate(runtime: Path, goal: str) -> Path: - digest = hashlib.sha256(goal.encode()).hexdigest()[:16] - return runtime / "authority-shadow" / "file-v0" / f"authority-store-{digest}.json" - - -def _bootstrap(registry: Path, runtime: Path, goal: str = "goal-a") -> dict: - return _cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", goal, "--execute")["bootstrap"] - - -def test_public_rollback_preserves_other_goal_and_replays_after_primary_changes(tmp_path: Path) -> None: - registry, runtime = _workspace(tmp_path) - first = _bootstrap(registry, runtime) - _bootstrap(registry, runtime, "goal-b") - other = _candidate(runtime, "goal-b").read_bytes() - identity = (runtime / "authority-shadow" / "file-v0" / "store-identity").read_bytes() - result = _cli(registry, runtime, "coordination-shadow", "rollback", "--goal-id", "goal-a", - "--provider-revision", first["provider_revision"], "--execute")["rollback"] - assert result["status"] == "applied" - _cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", - "--text", "Write primary while awaiting bootstrap", "--claimed-by", "agent-a") - assert not _candidate(runtime, "goal-a").exists() - second = _bootstrap(registry, runtime) - assert first["capture_lineage_id"] != second["capture_lineage_id"] - current = _candidate(runtime, "goal-a").read_bytes() - historical = _cli(registry, runtime, "coordination-shadow", "rollback", "--goal-id", "goal-a", - "--provider-revision", first["provider_revision"], "--execute")["rollback"] - assert historical["status"] == "replayed" - assert historical["current_capture_lineage_id"] == second["capture_lineage_id"] - assert _candidate(runtime, "goal-a").read_bytes() == current - assert _candidate(runtime, "goal-b").read_bytes() == other - assert (runtime / "authority-shadow" / "file-v0" / "store-identity").read_bytes() == identity - - -_DRAIN_PHASE = {"before": "before_commit", "after": "after_commit"} - -# The public write keeps its own inline drain, but the pause now happens inside -# the real native batch: the private fault driver runs the same -# ``drainShadowOutbox`` the production RPC handler runs and stops at the actual -# commit boundary. Releasing it lets that same batch continue, so a commit or a -# cursor update attempted after the generation changed is still real. -_DELAYED_WRITER = """ -import json, pathlib, subprocess, sys, time -from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter -from loopx.cli import main -barrier, release, phase = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2]), sys.argv[3] -actual = adapter.effect_runtime_result -def delayed(method, request, **kwargs): - if method != 'coordination.runtime_shadow.drain': - return actual(method, request, **kwargs) - child = subprocess.Popen( - ['node', '--no-warnings', '--experimental-strip-types', - 'loopx/control_plane/testing/shadow_drain_fault_process.ts', phase, str(release)], - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, - ) - child.stdin.write(json.dumps(request)) - child.stdin.close() - for line in child.stdout: - if line.startswith('BARRIER '): - barrier.write_text(line.removeprefix('BARRIER ')) - break - else: - raise RuntimeError('native barrier missing: ' + child.stderr.read()) - deadline = time.monotonic() + 60 - while not release.exists(): - if time.monotonic() > deadline: - child.kill(); child.wait() - raise RuntimeError('test scheduling barrier timed out') - time.sleep(.01) - remaining, errors = child.stdout.read(), child.stderr.read() - if child.wait() != 0: - raise RuntimeError(errors or remaining) - return json.loads(remaining.strip().splitlines()[-1]) -adapter.effect_runtime_result = delayed -raise SystemExit(main(sys.argv[4:])) -""" - - -def _paused_writer(tmp_path: Path, registry: Path, runtime: Path, timing: str) -> tuple[subprocess.Popen, Path, dict]: - barrier = tmp_path / "commit-barrier.json" - release = tmp_path / "commit-release" - args = _arguments(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", - "--text", "Transaction across a management boundary", "--claimed-by", "agent-a") - child = subprocess.Popen( - [sys.executable, "-c", _DELAYED_WRITER, str(barrier), str(release), _DRAIN_PHASE[timing], *args], - cwd=REPO_ROOT, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, - ) - deadline = time.monotonic() + 20 - while not barrier.exists(): - if child.poll() is not None: - stdout, stderr = child.communicate() - pytest.fail(f"public writer exited before real commit RPC: {stdout}\n{stderr}") - if time.monotonic() > deadline: - child.kill() - stdout, stderr = child.communicate() - pytest.fail(f"public writer did not reach real commit RPC: {stdout}\n{stderr}") - time.sleep(.01) - # Atomic JSON is unnecessary for authority here; retry the test notification only. - for _ in range(100): - try: - return child, release, json.loads(barrier.read_text()) - except json.JSONDecodeError: - time.sleep(.01) - child.kill() - raise AssertionError("incomplete test barrier") - - -def _release(child: subprocess.Popen, path: Path) -> dict: - path.write_text("continue") - try: - stdout, stderr = child.communicate(timeout=30) - except subprocess.TimeoutExpired: - child.kill() - child.communicate() - raise - assert child.returncode == 0, f"{stdout}\n{stderr}" - return json.loads(stdout) - - -@pytest.mark.parametrize("timing", ["before", "after"]) -def test_late_real_commit_cannot_cross_rollback_and_rebootstrap(tmp_path: Path, timing: str) -> None: - registry, runtime = _workspace(tmp_path) - first = _bootstrap(registry, runtime) - _bootstrap(registry, runtime, "goal-b") - other = _candidate(runtime, "goal-b").read_bytes() - child, release, barrier = _paused_writer(tmp_path, registry, runtime, timing) - try: - request = barrier["request"] - assert request["schema_version"] == "loopx_shadow_drain_v0" - assert request["goal_id"] == "goal-a" - assert request["runtime_root"] == str(runtime.resolve()) - # The paused batch still runs under the generation that is about to be - # superseded, and the revision it targets is durable readback. - management = read_shadow_management_state(runtime, "goal-a") - assert management is not None and management["status"] == "active" - assert management["binding"]["capture_lineage_id"] == first["capture_lineage_id"] - revision = json.loads(_candidate(runtime, "goal-a").read_text())["provider_revision"] - if timing == "before": - assert revision == first["provider_revision"] - else: - assert revision != first["provider_revision"] - rollback = _cli(registry, runtime, "coordination-shadow", "rollback", "--goal-id", "goal-a", - "--provider-revision", revision, "--execute")["rollback"] - archived = Path(rollback["outbox_archive_path"]) - retained = {str(path.relative_to(archived)): path.read_bytes() for path in archived.rglob("*") if path.is_file()} - assert any(name.endswith(".prepared.json") for name in retained) - second = _bootstrap(registry, runtime) - assert second["capture_lineage_id"] != first["capture_lineage_id"] - candidate = _candidate(runtime, "goal-a").read_bytes() - payload = _release(child, release) - # The resumed batch reports this write from its own drain evidence; it - # must never claim a delivery into the generation that replaced it. - assert payload["coordination_runtime_shadow"]["outcome"] != "delivered" - assert _candidate(runtime, "goal-a").read_bytes() == candidate - assert _candidate(runtime, "goal-b").read_bytes() == other - assert {str(path.relative_to(archived)): path.read_bytes() for path in archived.rglob("*") if path.is_file()} == retained - active_outbox = runtime / "authority-shadow" / "outbox" / "goal-a" - assert not list(active_outbox.rglob("drain-cursor.json")) - assert not list(active_outbox.rglob("*.prepared.json")) - # An explicit retry of the superseded batch reads durable receipts and - # still cannot deliver into the generation that replaced it. - late = effect_runtime_result("coordination.runtime_shadow.drain", request) - assert late["outcome"] not in {"delivered", "replayed", "reconciled"} - assert late["delivered"] == 0 and late["replayed"] == 0 - assert _candidate(runtime, "goal-a").read_bytes() == candidate - finally: - if child.poll() is None: - child.kill() - child.communicate() - - -def test_corrupt_history_after_real_commit_cannot_authorize_cursor_cleanup(tmp_path: Path) -> None: - registry, runtime = _workspace(tmp_path) - first = _bootstrap(registry, runtime) - child, release, barrier = _paused_writer(tmp_path, registry, runtime, "after") - try: - # The commit already happened: the durable candidate moved past bootstrap. - assert barrier["request"]["goal_id"] == "goal-a" - candidate = _candidate(runtime, "goal-a") - assert json.loads(candidate.read_text())["provider_revision"] != first["provider_revision"] - directory = runtime / "authority-shadow" / "outbox" / "goal-a" / "todos" - entries = {path.name: path.read_bytes() for path in directory.glob("*.json")} - assert any(name.endswith(".prepared.json") for name in entries) - record = json.loads(candidate.read_text()) - record["committed"][0]["provider_revision"] = "file:1:" + "0" * 24 - candidate.write_text(json.dumps(record)) - corrupt = candidate.read_bytes() - _release(child, release) - assert {path.name: path.read_bytes() for path in directory.glob("*.json")} == entries - assert candidate.read_bytes() == corrupt - assert not (directory / "drain-cursor.json").exists() - finally: - if child.poll() is None: - child.kill() - child.communicate() - - -@pytest.mark.parametrize("phase", ["bootstrap_prepared", "bootstrap_candidate_committed", "bootstrap_outbox_ready"]) -def test_public_bootstrap_operation_selector_aborts_real_crash(tmp_path: Path, phase: str) -> None: - registry, runtime = _workspace(tmp_path) - _bootstrap(registry, runtime, "goal-b") - other = _candidate(runtime, "goal-b").read_bytes() - goal = json.loads(registry.read_text())["goals"][0] - state_path = Path(goal["repo"]) / goal["state_file"] - original = state_path.read_bytes() - projection, snapshot = build_runtime_shadow_source_snapshot( - goal=goal, runtime_root=runtime, state_path=state_path, registry_path=registry, - ) - operation = "bootstrap:public-crash" - request = {"schema_version": COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA, - "runtime_root": str(runtime), "goal_id": "goal-a", "operation_id": operation, - "source_version": "source:before-crash", "projection": projection, "source_snapshot": snapshot} - worker = REPO_ROOT / "tests" / "control_plane_ts" / "fixtures" / "shadow_management_crash_worker.ts" - child = subprocess.Popen( - ["node", "--no-warnings", "--experimental-strip-types", str(worker), "bootstrap-public", json.dumps(request), phase], - cwd=REPO_ROOT, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, - ) - try: - ready, _, _ = select.select([child.stdout], [], [], 10) - assert ready, "bootstrap did not reach the real filesystem barrier" - line = child.stdout.readline() - assert line.strip() == f"ready:{phase}", line - child.kill() - child.communicate(timeout=10) - pending = read_shadow_management_state(runtime, "goal-a") - assert pending is not None and pending["status"] == "bootstrapping" - blocked = _cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", - "--text", "Must not write while bootstrap is pending", "--claimed-by", "agent-a", success=False) - assert blocked["ok"] is False - assert state_path.read_bytes() == original - stopped = _cli(registry, runtime, "coordination-shadow", "rollback", "--goal-id", "goal-a", - "--bootstrap-operation-id", operation, "--execute")["rollback"] - assert stopped["status"] == "applied" - assert read_shadow_management_state(runtime, "goal-a")["status"] == "inactive" - assert _candidate(runtime, "goal-b").read_bytes() == other - current = _bootstrap(registry, runtime) - assert current["status"] == "applied" - assert current["capture_lineage_id"] != stopped["capture_lineage_id"] - finally: - if child.poll() is None: - child.kill() - child.communicate() diff --git a/tests/control_plane/test_shadow_management_variant_e2e.py b/tests/control_plane/test_shadow_management_variant_e2e.py index ab045bfd02..601afaa992 100644 --- a/tests/control_plane/test_shadow_management_variant_e2e.py +++ b/tests/control_plane/test_shadow_management_variant_e2e.py @@ -11,6 +11,7 @@ from pathlib import Path import select import subprocess +import sys import pytest @@ -22,12 +23,92 @@ from loopx.control_plane.coordination.runtime_shadow import build_runtime_shadow_source_snapshot from loopx.control_plane.coordination.shadow_management import read_shadow_management_state from loopx.control_plane.effect_runtime import effect_runtime_result -from tests.control_plane.shadow_e2e_fixture import ShadowWorkspace -from tests.control_plane.test_shadow_management_e2e import ( - REPO_ROOT, _bootstrap, _candidate, _cli, _workspace, -) pytestmark = pytest.mark.stage2c_e2e +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _workspace(tmp_path: Path) -> tuple[Path, Path]: + runtime = tmp_path / "runtime" + goals = [] + for goal_id in ("goal-a", "goal-b"): + repo = tmp_path / goal_id + repo.mkdir() + (repo / "ACTIVE_GOAL_STATE.md").write_text( + f"---\ngoal_id: {goal_id}\nhandoff_mode: hard_lease\n" + "updated_at: 2026-01-01T00:00:00+00:00\n---\n\n## Agent Todo\n\n" + ) + goals.append({ + "id": goal_id, + "status": "active", + "repo": str(repo), + "state_file": "ACTIVE_GOAL_STATE.md", + "coordination": { + "agent_model": "peer_v1", + "registered_agents": ["agent-a", "agent-b"], + "runtime_shadow": { + "schema_version": "loopx_coordination_runtime_shadow_config_v0", + "enabled": True, + "provider": "file_v0", + }, + }, + }) + registry = tmp_path / "registry.json" + registry.write_text(json.dumps({ + "common_runtime_root": str(runtime), + "goals": goals, + })) + return registry, runtime + + +def _cli( + registry: Path, + runtime: Path, + *args: str, + success: bool = True, +) -> dict: + result = subprocess.run( + [ + sys.executable, + "-m", + "loopx.cli", + "--registry", + str(registry), + "--runtime-root", + str(runtime), + "--format", + "json", + *args, + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + timeout=30, + ) + if success: + assert result.returncode == 0, f"{result.stdout}\n{result.stderr}" + return json.loads(result.stdout) + + +def _candidate(runtime: Path, goal: str) -> Path: + digest = hashlib.sha256(goal.encode()).hexdigest()[:16] + return runtime / "authority-shadow" / "file-v0" / f"authority-store-{digest}.json" + + +def _bootstrap( + registry: Path, + runtime: Path, + goal: str = "goal-a", +) -> dict: + return _cli( + registry, + runtime, + "coordination-shadow", + "bootstrap", + "--goal-id", + goal, + "--execute", + )["bootstrap"] _CRASH_RPC = r""" @@ -120,49 +201,6 @@ def _durable_files(root: Path) -> dict[str, bytes]: } -@pytest.mark.parametrize("archive", ["candidate", "outbox"]) -def test_pending_rollback_checks_corrupted_archive_before_finishing_and_can_retry_exact_repair( - tmp_path: Path, archive: str, -) -> None: - registry, runtime = _workspace(tmp_path) - first = _bootstrap(registry, runtime) - _bootstrap(registry, runtime, "goal-b") - _, state = _source(registry, runtime) - w = ShadowWorkspace(registry, runtime, state, "goal-a") - w.crash("before_commit", "todo", "add", "--role", "agent", "--text", "Pending retained mutation") - candidate, outbox = _candidate(runtime, "goal-a"), runtime / "authority-shadow/outbox/goal-a" - pending_bytes = _durable_files(outbox) - assert any(name.endswith(".prepared.json") for name in pending_bytes) - candidate_bytes, primary_bytes = candidate.read_bytes(), state.read_bytes() - request = _rollback_request(runtime, state, first["provider_revision"]) - moved = _kill_rpc("rollback", request, candidate if archive == "candidate" else outbox) - archived = Path(moved["destination"]) - damaged = archived if archive == "candidate" else next(archived.rglob("*.prepared.json")) - original = damaged.read_bytes() - damaged.write_bytes(original + b"\ncorrupted fixture archive") - before = _durable_files(runtime) - result = _cli(registry, runtime, "coordination-shadow", "rollback", "--goal-id", "goal-a", - "--provider-revision", first["provider_revision"], "--execute", success=False) - assert result["ok"] is False, result - assert result["rollback"]["reason_code"] == f"rollback_{archive}_identity_mismatch", result - after = _durable_files(runtime) - journal = str((_management(runtime) / "state.json").relative_to(runtime)) - # Recovery may persist the same verified phase in canonical JSON order; - # only representation may differ, never transition state or evidence bytes. - assert json.loads(after.pop(journal)) == json.loads(before.pop(journal)) - assert after == before - assert state.read_bytes() == primary_bytes - assert read_shadow_management_state(runtime, "goal-a")["status"] == "rolling_back" - damaged.write_bytes(original) - recovered = _cli(registry, runtime, "coordination-shadow", "rollback", "--goal-id", "goal-a", - "--provider-revision", first["provider_revision"], "--execute")["rollback"] - assert recovered["status"] == "recovered", recovered - assert Path(recovered["candidate_archive_path"]).read_bytes() == candidate_bytes - assert _durable_files(Path(recovered["outbox_archive_path"])) == pending_bytes - assert read_shadow_management_state(runtime, "goal-a")["status"] == "inactive" - assert state.read_bytes() == primary_bytes - - def test_changed_source_after_bootstrap_commit_requires_abort_before_new_baseline(tmp_path: Path) -> None: registry, runtime = _workspace(tmp_path) request, state = _source(registry, runtime) diff --git a/tests/control_plane/test_shadow_observable_e2e.py b/tests/control_plane/test_shadow_observable_e2e.py deleted file mode 100644 index c6541a8378..0000000000 --- a/tests/control_plane/test_shadow_observable_e2e.py +++ /dev/null @@ -1,389 +0,0 @@ -"""Caller semantics across capture activation, with optional base/head evidence. - -LOOPX_SHADOW_COMPARISON_SOURCE selects an immutable checkout for every child; -LOOPX_SHADOW_COMPARISON_OUTPUT retains complete responses and persisted bytes. -Neither changes the oracle. No product result or persistence operation is mocked. -""" -from __future__ import annotations - -import json -import os -from pathlib import Path -import subprocess -import sys - -import pytest - -pytestmark = pytest.mark.stage2c_e2e -SOURCE = Path(os.environ.get('LOOPX_SHADOW_COMPARISON_SOURCE', Path(__file__).resolve().parents[2])).resolve() - - -class Caller: - def __init__(self, path: Path, mode: str, name: str): - self.path, self.mode, self.name = path, mode, name - self.state, self.registry, self.root = path / 'STATE.md', path / 'registry.json', path / 'runtime' - self.env = dict(os.environ, PYTHONPATH=str(SOURCE)) - self.rows: list[dict] = [] - self.state.write_text('---\ngoal_id: observable\nhandoff_mode: soft_claim\n' - 'updated_at: 2026-09-01T00:00:00+00:00\n---\n\n## Agent Todo\n\n' - '## Progress Ledger\n\n## Next Action\n\n- Inspect.\n') - coordination = {'agent_model': 'peer_v1', 'registered_agents': ['agent-a', 'agent-b']} - if mode != 'absent': - coordination['runtime_shadow'] = {'schema_version': 'loopx_coordination_runtime_shadow_config_v0', - 'enabled': mode == 'enabled', 'provider': 'file_v0'} - self.registry.write_text(json.dumps({'common_runtime_root': str(self.root), 'goals': [{ - 'id': 'observable', 'status': 'active', 'repo': str(path), 'state_file': self.state.name, - 'coordination': coordination}]})) - actual = subprocess.check_output([sys.executable, '-c', 'import loopx; print(loopx.__file__)'], - env=self.env, cwd=path, text=True).strip() - assert Path(actual).resolve().is_relative_to(SOURCE) - if mode == 'enabled': - assert self.call('coordination-shadow', 'bootstrap', '--execute')['bootstrap']['status'] == 'applied' - - def files(self) -> dict[str, str]: - # All fixture files, including receipts. Locks are retained in raw evidence too. - return {str(p.relative_to(self.path)): p.read_bytes().hex() - for p in sorted(self.path.rglob('*')) if p.is_file()} - - def primary(self) -> dict[str, str]: - return {k: v for k, v in self.files().items() - if k == 'STATE.md' or k.startswith('runtime/goals/') and not k.endswith('.lock')} - - def call(self, *args: str, goal: str = 'observable') -> dict: - command = [sys.executable, '-m', 'loopx.cli', '--registry', str(self.registry), - '--runtime-root', str(self.root), '--format', 'json', *args, '--goal-id', goal] - return self.invoke(command, command[3:]) - - def invoke(self, command: list[str], arguments: list[str], *, cwd: Path | None = None) -> dict: - before = self.files() - result = subprocess.run(command, env=self.env, cwd=cwd or self.path, capture_output=True, text=True, timeout=45) - assert 'Traceback' not in result.stderr, result.stderr - row = {'arguments': arguments, 'exit': result.returncode, 'stdout': result.stdout, - 'stderr': result.stderr, 'files_before': before, 'files_after': self.files()} - self.rows.append(row) - destination = os.environ.get('LOOPX_SHADOW_COMPARISON_OUTPUT') - if destination: - output = Path(destination) - output.mkdir(parents=True, exist_ok=True) - (output / (self.name + '.json')).write_text(json.dumps({ - 'source': str(SOURCE), 'workspace': str(self.path), 'mode': self.mode, 'rows': self.rows}, indent=2)) - return json.loads(result.stdout) if result.stdout else {'stderr': result.stderr, 'exit': result.returncode} - - def add(self, text: str, *extra: str) -> str: - result = self.call('todo', 'add', '--role', 'agent', '--text', text, *extra) - assert result['ok'] is True, result - return result['todo_id'] - - def read(self, todo: str) -> dict: - result = self.call('todo', 'list') - assert result['ok'] is True, result - return next(item for item in result['todos'] if item['todo_id'] == todo) - - -@pytest.fixture(params=['absent', 'disabled', 'enabled']) -def caller(tmp_path: Path, request: pytest.FixtureRequest) -> Caller: - return Caller(tmp_path, request.param, request.node.name) - - -def test_todo_argument_intent_and_rejections(caller: Caller) -> None: - w = caller - before = w.primary() - preview = w.call('todo', 'add', '--role', 'agent', '--text', 'Retain operator intent', '--dry-run') - assert preview['ok'] is True and w.primary() == before - todo = w.add('Retain operator intent', '--claimed-by', 'agent-a', '--note', 'Initial context') - assert w.read(todo)['claimed_by'] == 'agent-a' - for args, expected_note in [(('--note', 'Updated context'), 'Updated context'), - (('--text', 'Corrected operator intent'), 'Updated context')]: - result = w.call('todo', 'update', '--todo-id', todo, '--agent-id', 'agent-a', *args) - assert result['ok'] is True, result - current = w.read(todo) - assert current['note'] == expected_note and current['claimed_by'] == 'agent-a' - assert w.read(todo)['text'] == 'Corrected operator intent' - # CLI mutable-field validation treats an empty note as absent, not a clear. - before = w.primary() - empty = w.call('todo', 'update', '--todo-id', todo, '--agent-id', 'agent-a', '--note', '') - assert empty['error'] == 'todo update requires at least one mutable todo field' - assert w.primary() == before - assert w.call('todo', 'update', '--todo-id', todo, '--agent-id', 'agent-a', '--clear-claim')['ok'] is True - assert not w.read(todo).get('claimed_by') - before = w.primary() - # Overlapping invalid target and actor: preserve the complete diagnostic/priority. - rejected = w.call('todo', 'update', '--todo-id', 'todo_missing', '--agent-id', 'unknown', '--note', 'Must not write') - assert rejected['ok'] is False and w.primary() == before, rejected - assert w.call('todo', 'claim', '--todo-id', todo, '--claimed-by', 'agent-a', '--agent-id', 'agent-a')['ok'] is True - complete = w.call('todo', 'complete', '--todo-id', todo, '--agent-id', 'agent-a', - '--evidence', 'validation://caller', '--no-follow-up') - assert complete['ok'] is True, complete - assert w.read(todo)['done'] is True - archived = w.call('todo', 'archive-completed', '--max-active-done', '0', '--execute') - assert archived['ok'] is True, archived - assert 'Corrected operator intent' in w.state.read_text() - - -def test_handoff_mode_quiescence(caller: Caller) -> None: - w = caller - assert w.call('handoff-mode', 'set', '--mode', 'hard_lease')['changed'] is True - before = w.primary() - assert w.call('handoff-mode', 'set', '--mode', 'hard_lease')['changed'] is False - assert w.primary() == before - todo = w.add('Quiescence ownership') - acquired = w.call('task-lease', 'acquire', '--todo-id', todo, '--owner', 'agent-a', '--idempotency-key', 'quiescence') - assert acquired['acquired'] is True, acquired - before = w.primary() - blocked = w.call('handoff-mode', 'set', '--mode', 'soft_claim') - assert blocked['error_code'] == 'handoff_mode_not_quiescent' and w.primary() == before - malformed = w.call('handoff-mode', 'set', '--mode', 'invalid') - assert malformed['exit'] == 2 and w.primary() == before - - -def test_lease_arguments_cas_transfer_and_replay(caller: Caller) -> None: - w = caller - assert w.call('handoff-mode', 'set', '--mode', 'hard_lease')['ok'] is True - todo = w.add('Lease argument round trip') - args = ('task-lease', 'acquire', '--todo-id', todo, '--owner', 'agent-a', - '--idempotency-key', 'caller-acquire', '--ttl-seconds', '3600', '--write-scope', 'src/**') - result = w.call(*args) - assert result['acquired'] is True, result - lease_path = w.root / 'goals' / 'observable' / 'task-leases' / f'{todo}.json' - persisted = json.loads(lease_path.read_bytes()) - assert persisted['owner'] == 'agent-a' and persisted['write_scopes'] == ['src/**'] - assert persisted['acquire_ttl_seconds'] == 3600 - before = lease_path.read_bytes() - assert w.call(*args)['ok'] is True - assert lease_path.read_bytes() == before - before = w.primary() - rejected = w.call('task-lease', 'renew', '--todo-id', todo, '--owner', 'agent-b', - '--idempotency-key', 'wrong-key', '--expected-version', '999') - assert rejected['ok'] is False and w.primary() == before, rejected - renewed = w.call('task-lease', 'renew', '--todo-id', todo, '--owner', 'agent-a', - '--idempotency-key', 'caller-acquire', '--expected-version', str(persisted['version']), '--ttl-seconds', '7200') - assert renewed['ok'] is True, renewed - transferred = w.call('task-lease', 'transfer', '--todo-id', todo, '--owner', 'agent-a', - '--idempotency-key', 'caller-acquire', '--expected-version', str(renewed['lease']['version']), - '--new-owner', 'agent-b', '--new-idempotency-key', 'caller-transfer') - assert transferred['ok'] is True, transferred - assert json.loads(lease_path.read_bytes())['owner'] == 'agent-b' - released = w.call('task-lease', 'release', '--todo-id', todo, '--owner', 'agent-b', - '--idempotency-key', 'caller-transfer', '--expected-version', str(transferred['lease']['version'])) - assert released['ok'] is True, released - assert json.loads(lease_path.read_bytes())['status'] == 'released' - assert w.call('task-lease', 'inspect', '--todo-id', todo)['ok'] is True - - -def test_refresh_bound_step_and_reward_owned_prose(caller: Caller) -> None: - w = caller - todo = w.add('Canonical record must survive prose') - record = w.read(todo) - args = ('refresh-state', '--agent-id', 'agent-a', '--progress-scope', 'goal', '--classification', 'continue', - '--recommended-action', 'Read the independent lease snapshot.', '--vision-unchanged-reason', 'Same bounded validation.', - '--next-action', 'Read the independent lease snapshot.', '--no-global-sync') - before = w.primary() - assert w.call(*args, '--dry-run')['ok'] is True - assert w.primary() == before - refreshed = w.call(*args) - assert refreshed['ok'] is True, refreshed - assert 'Read the independent lease snapshot.' not in w.state.read_text() - assert refreshed['recommended_action_resolution']['recommended_action_source'] == 'agent_lane_step' - assert w.read(todo) == record - args = ('reward', '--actor-kind', 'owner', '--recorded-at', '2026-09-01T12:00:00+00:00', '--decision', 'continue', - '--reward', 'positive', '--reason-summary', 'Retained argument evidence.', '--write-active-state-summary') - before = w.primary() - assert w.call(*args, '--dry-run')['ok'] is True - assert w.primary() == before - reward = w.call(*args) - assert reward['ok'] is True, reward - assert 'Retained argument evidence.' in w.state.read_text() - index = w.root / 'goals' / 'observable' / 'runs' / 'index.jsonl' - assert 'Retained argument evidence.' in index.read_text() - assert w.read(todo) == record - before = w.primary() - rejected = w.call('reward', '--run-generated-at', 'missing', '--decision', '', '--reward', 'positive', '--reason-summary', '') - assert rejected['ok'] is False and w.primary() == before, rejected - - -@pytest.mark.parametrize('replacement', ['force', 'missing']) -def test_bootstrap_replacement_preserves_existing_authority(caller: Caller, replacement: str) -> None: - w = caller - w.add('Existing canonical state') - args = ('bootstrap', '--project', str(w.path), '--state-file', 'STATE.md', - '--objective', 'Replacement objective', '--no-global-sync') - assert w.call(*args, '--dry-run')['ok'] is True - if replacement == 'missing': - w.state.unlink() - before = w.registry.read_bytes(), w.state.read_bytes() if w.state.exists() else None - result = w.call(*args, *(['--force'] if replacement == 'force' else [])) - if w.mode == 'enabled': - assert result['ok'] is False, result - assert (w.registry.read_bytes(), w.state.read_bytes() if w.state.exists() else None) == before - else: - assert result['ok'] is True, result - assert 'Replacement objective' in w.state.read_text() - assert json.loads(w.registry.read_bytes())['common_runtime_root'] == str(w.root) - - -def test_project_registration_and_missing_state_reconstruction(caller: Caller) -> None: - w = caller - args = ('project', 'register', '--project-id', 'registered-project', '--project-kind', 'work', - '--knowledge-root', str(w.path / 'knowledge'), '--objective', 'Preserve project intent', - '--acceptance', 'Independent state readback', '--non-goal', 'Remote promotion', - '--next-effect', 'Inspect the source', '--stop-condition', 'Readback matches') - created = w.call(*args, goal='registered') - assert created['ok'] is True, created - state = Path(created['state_file']) - assert 'Preserve project intent' in state.read_text() - assert 'Independent state readback' in state.read_text() - assert w.call(*args, goal='registered')['ok'] is True - if w.mode == 'enabled': - registry = json.loads(w.registry.read_bytes()) - goal = next(g for g in registry['goals'] if g['id'] == 'registered') - goal.setdefault('coordination', {})['runtime_shadow'] = { - 'schema_version': 'loopx_coordination_runtime_shadow_config_v0', 'enabled': True, 'provider': 'file_v0'} - w.registry.write_text(json.dumps(registry)) - assert w.call('coordination-shadow', 'bootstrap', '--execute', goal='registered')['bootstrap']['status'] == 'applied' - state.unlink() - before = w.registry.read_bytes() - rebuilt = w.call(*args, goal='registered') - if w.mode == 'enabled': - assert rebuilt['ok'] is False, rebuilt - assert not state.exists() and w.registry.read_bytes() == before - else: - assert rebuilt['ok'] is True, rebuilt - assert 'Preserve project intent' in state.read_text() - - -def test_migration_target_and_preview_ownership(caller: Caller) -> None: - w = caller - legacy = w.path / 'legacy' - legacy.mkdir() - source = legacy / 'STATE.md' - source.write_text('---\ngoal_id: legacy\nhandoff_mode: soft_claim\n---\n\n## Agent Todo\n\n- [ ] Migrated source.\n') - source_registry = legacy / 'registry.json' - source_registry.write_text(json.dumps({'goals': [{'id': 'legacy', 'repo': str(legacy), 'state_file': 'STATE.md'}]})) - args = ('migrate-state', '--legacy-registry', str(source_registry), - '--legacy-runtime-root', str(legacy / 'runtime'), '--target-runtime-root', str(w.root), - '--goal-id-map', 'legacy=observable', '--path-map', f'{legacy}={w.path}', - '--copy-active-state', '--no-global-sync') - before = w.registry.read_bytes(), w.state.read_bytes() - preview = w.call(*args, goal='legacy') - assert preview['ok'] is True, preview - assert (w.registry.read_bytes(), w.state.read_bytes()) == before - result = w.call(*args, '--execute', goal='legacy') - if w.mode == 'enabled': - assert result['ok'] is False, result - assert (w.registry.read_bytes(), w.state.read_bytes()) == before - else: - assert result['ok'] is True, result - assert 'goal_id: observable' in w.state.read_text() and 'Migrated source.' in w.state.read_text() - assert json.loads(w.registry.read_bytes())['goals'][0]['id'] == 'observable' - - -def test_operator_reads_and_invalid_selector_have_no_primary_effect(caller: Caller) -> None: - w = caller - todo = w.add('Read policy fixture') - before = w.primary() - for command in ['inspect', 'qualify', 'read-candidate']: - args = ('--todo-id', todo) if command == 'read-candidate' else () - result = w.call('coordination-shadow', command, *args) - assert result['ok'] is (w.mode == 'enabled' and command == 'inspect'), result - assert w.primary() == before - invalid = w.call('coordination-shadow', 'rollback', '--provider-revision', '', '--execute') - assert invalid['ok'] is False and w.primary() == before, invalid - # Availability alone never constructs a candidate/outbox in the disabled goal. - if w.mode != 'enabled': - assert not (w.root / 'authority-shadow').exists() - - -def test_monitor_successor_retains_caller_routing(caller: Caller) -> None: - w = caller - todo = w.add('Observe the public release', '--task-class', 'continuous_monitor', '--action-kind', 'monitor', - '--claimed-by', 'agent-a', '--target-key', 'release:bounded', '--cadence', '30m', - '--next-due-at', '2000-01-01T00:00:00+00:00', '--watch-only') - result = w.call('quota', 'monitor-poll', '--agent-id', 'agent-a', '--runtime-profile', 'generic_cli', - '--available-capability', 'network', '--todo-id', todo, '--target-key', 'release:bounded', - '--result-hash', 'release-v1', '--material-change', '--next-agent-todo', 'Validate released head', - '--next-action-kind', 'validate_release_head', '--next-task-repository', 'git:github.com/huangruiteng/loopx', - '--next-required-capability', 'network', '--next-continuation-policy', 'same_agent_non_delivery', - '--next-claimed-by', 'agent-a', '--execute') - assert result['ok'] is True and len(result['successor_todo_ids']) == 1, result - successor = w.read(result['successor_todo_ids'][0]) - assert successor['text'] == 'Validate released head' and successor['claimed_by'] == 'agent-a' - assert successor['action_kind'] == 'validate_release_head' and successor['required_capabilities'] == ['network'] - - -def test_retired_observation_request_preserves_runtime_capture(caller: Caller) -> None: - w = caller - before = w.primary() - for extra in ((), ('--execute',)): - rejected = w.call('configure-goal', '--local-authority-shadow-file', *extra) - assert rejected['ok'] is False and 'local_authority_shadow_retired' in rejected['error'], rejected - assert w.primary() == before - assert not (w.root / 'authority-shadow' / 'file' / 'observable').exists() - todo = w.add('Independent observation contract') - assert w.read(todo)['text'] == 'Independent observation contract' - - -def test_turn_input_rejection_has_no_host_or_primary_effect(caller: Caller) -> None: - w = caller - before = w.primary() - result = w.call('turn', 'run-once', '--project', str(w.path), '--agent-id', 'unknown', '--no-global-sync') - assert result['ok'] is False, result - assert result['effects']['host_invoked'] is False and result['effects']['state_written'] is False - assert w.primary() == before - - -def test_registry_relative_root_does_not_depend_on_callers_cwd(caller: Caller) -> None: - w = caller - registry = json.loads(w.registry.read_bytes()) - registry['common_runtime_root'] = 'runtime' - w.registry.write_text(json.dumps(registry)) - cwd = w.path / 'unrelated-cwd' - cwd.mkdir() - command = [sys.executable, '-m', 'loopx.cli', '--registry', str(w.registry), '--format', 'json', - 'todo', 'add', '--goal-id', 'observable', '--role', 'agent', '--text', 'Registry relative root'] - added = w.invoke(command, command[3:], cwd=cwd) - assert added['ok'] is True, added - assert w.read(added['todo_id'])['text'] == 'Registry relative root' - assert (w.root / 'goals' / 'observable' / 'rollout-event-log.jsonl').exists() - assert not (cwd / 'runtime').exists() - assert w.call('handoff-mode', 'set', '--mode', 'hard_lease')['ok'] is True - command = [*command[:7], 'task-lease', 'acquire', '--goal-id', 'observable', - '--todo-id', added['todo_id'], '--owner', 'agent-a', '--idempotency-key', 'relative-lease'] - lease = w.invoke(command, command[3:], cwd=cwd) - assert lease['ok'] is True, lease - assert (w.root / 'goals' / 'observable' / 'task-leases' / f"{added['todo_id']}.json").exists() - assert not (cwd / 'runtime').exists() - - -def test_legacy_holder_verify_and_terminal_release(caller: Caller) -> None: - w = caller - w.call('handoff-mode', 'set', '--mode', 'hard_lease') - todo = w.add('Holder and terminal write boundaries') - before = w.primary() - rejected_claim = w.call('todo', 'claim', '--todo-id', todo, '--claimed-by', 'agent-a', '--agent-id', 'agent-a', - '--task-lease-idempotency-key', 'caller-holder', '--task-lease-expected-version', '0') - assert rejected_claim['error'] == '--task-lease-idempotency-key on todo claim requires promoted canonical authority; no legacy write attempted' - assert w.primary() == before - assert w.call('task-lease', 'acquire', '--todo-id', todo, '--owner', 'agent-a', '--idempotency-key', 'caller-holder')['ok'] is True - claim = w.call('todo', 'claim', '--todo-id', todo, '--claimed-by', 'agent-a', '--agent-id', 'agent-a') - assert claim['ok'] is True, claim - path = w.root / 'goals' / 'observable' / 'task-leases' / f'{todo}.json' - lease = json.loads(path.read_bytes()) - assert lease['owner'] == 'agent-a' and lease['status'] == 'active' - before = w.primary() - rejected = w.call('todo', 'complete', '--todo-id', todo, '--agent-id', 'agent-a', - '--evidence', 'validation://terminal', '--no-follow-up') - assert rejected['error_code'] == 'lease_fence_required', rejected - after = w.primary() - assert {key: after[key] for key in before} == before - # The existing verify protocol retains an acquired intent, never a lease or success receipt. - added = set(after) - set(before) - assert len(added) == 1 - intent = json.loads(bytes.fromhex(after[added.pop()])) - assert intent['schema_version'] == 'task_lease_fence_receipt_v0' and intent['state'] == 'acquired' - assert intent['lease'] is None and intent['response'] is None and intent['verify_response'] is None - result = w.call('todo', 'complete', '--todo-id', todo, '--agent-id', 'agent-a', - '--task-lease-idempotency-key', 'caller-holder', '--task-lease-expected-version', str(lease['version']), - '--evidence', 'validation://terminal', '--no-follow-up') - assert result['ok'] is True and result['task_lease_fence']['released'] is True, result - assert json.loads(path.read_bytes())['status'] == 'released' - assert w.read(todo)['done'] is True diff --git a/tests/control_plane/test_shadow_observable_native_e2e.py b/tests/control_plane/test_shadow_observable_native_e2e.py deleted file mode 100644 index fc927e75a9..0000000000 --- a/tests/control_plane/test_shadow_observable_native_e2e.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Independent native and HTTP consumers of the shared changed writer boundary.""" -from __future__ import annotations - -import json -import sys - -import pytest - -from test_shadow_observable_e2e import Caller, SOURCE, caller as caller - -pytestmark = pytest.mark.stage2c_e2e - -NATIVE = r""" -import {join} from 'node:path'; -const input = JSON.parse(process.argv[1]); -const {FileAuthorityStore} = await import(input.base + '/file_authority_store.ts'); -const store = new FileAuthorityStore(join(input.root, 'authority', 'file-v0'), 'observable'); -let result; -if (input.action === 'seed') { - result = await store.commitAuthority({expected_provider_revision:null, operation_id:'caller-fixture', - events:[], next_projection:input.request, receipts:[]}); - const {engageLegacyCoordinationWriterFence} = await import(input.base + '/legacy_writer_fence.ts'); - const {canonicalAuthoritySha256} = await import(input.base + '/authority_store_codec.ts'); - const fence = await engageLegacyCoordinationWriterFence({schema_version:'loopx_legacy_coordination_writer_fence_engage_request_v0', - runtime_root:input.root,goal_id:'observable',state_path:input.state, - fence:{schema_version:'loopx_legacy_coordination_writer_fence_v0',state:'engaged',goal_id:'observable', - fence_id:'caller-fixture',source_version:'caller-fixture',source_projection_sha256:canonicalAuthoritySha256(input.request), - expected_shadow_provider_revision:result.provider_revision}}); - if(fence.status !== 'applied') throw new Error(JSON.stringify(fence)); -} else if (input.action === 'read') { - result = {head:await store.loadAuthority(), history:await store.scanCommitted(null, 100)}; -} else { - const owner = await import(input.base + '/local_authority_runtime.ts'); - result = await owner[input.action](input.request); -} -process.stdout.write(JSON.stringify(result) + '\n'); -""" - - -def native(w: Caller, action: str, request: dict) -> dict: - value = {'base': (SOURCE / 'loopx/control_plane/coordination').as_uri(), - 'root': str(w.root), 'state': str(w.state), 'action': action, 'request': request} - return w.invoke(['node', '--no-warnings', '--experimental-strip-types', '--input-type=module', - '-e', NATIVE, json.dumps(value)], ['native', action, json.dumps(request)]) - - -def test_canonical_argument_intent_and_atomic_claim(caller: Caller) -> None: - w = caller - todo = w.add('Native canonical input', '--note', 'Preserve operator note', '--required-write-scope', 'src/**') - record = w.read(todo) - builder = "from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection as build; import json,sys; value=build(goal_id='observable', todos=[json.loads(sys.argv[1])]); value['handoff_mode']='hard_lease'; print(json.dumps(value))" - projection = w.invoke([sys.executable, '-c', builder, json.dumps(record)], ['fixture-projection', json.dumps(record)]) - assert native(w, 'seed', projection)['status'] == 'applied' - w.state.unlink() - before = native(w, 'read', {}) - claim = ('todo', 'claim', '--todo-id', todo, '--claimed-by', 'agent-a', '--agent-id', 'agent-a', - '--claim-operation-id', 'caller-atomic-claim', '--task-lease-idempotency-key', 'caller-ownership', - '--task-lease-expected-version', '0') - assert w.call(*claim, '--dry-run')['status'] == 'planned' - assert native(w, 'read', {}) == before - applied = w.call(*claim) - assert applied['status'] == 'applied', applied - assert applied['projection_delivery'] == 'delivered', applied - recovered = w.state.read_bytes() - assert b'claimed_by=agent-a' in recovered - replay = w.call(*claim) - assert replay['status'] == 'replayed' and replay['original_receipt'] == applied['original_receipt'] - stored = native(w, 'read', {})['head']['head'] - assert stored['todos'][0]['claimed_by'] == 'agent-a' - assert stored['leases'][0]['owner'] == 'agent-a' and stored['leases'][0]['write_scopes'] == ['src/**'] - before = native(w, 'read', {}) - rejected = w.call('todo', 'update', '--todo-id', todo, '--agent-id', 'agent-b', '--note', 'Foreign owner edit') - assert rejected.get('error_code') == 'update_owner_mismatch', rejected - assert rejected['error'] == rejected['reason'] == "Todo update cannot edit another claim owner's work" - assert native(w, 'read', {}) == before - assert w.state.read_bytes() == recovered - - -def test_native_unclaimed_edit_and_explicit_note_clear(caller: Caller) -> None: - w = caller - todo = w.add('Unclaimed correction', '--note', 'Keep until explicitly cleared') - record = w.read(todo) - builder = "from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection as build; import json,sys; value=build(goal_id='observable', todos=[json.loads(sys.argv[1])]); value['handoff_mode']='soft_claim'; print(json.dumps(value))" - projection = w.invoke([sys.executable, '-c', builder, json.dumps(record)], ['fixture-projection', json.dumps(record)]) - assert native(w, 'seed', projection)['status'] == 'applied' - w.state.unlink() - updated = w.call('todo', 'update', '--todo-id', todo, '--agent-id', 'agent-b', '--note', 'Claim-neutral context') - assert updated['ok'] is True and updated['projection_delivery'] == 'delivered', updated - recovered = w.state.read_bytes() - assert todo.encode() in recovered - stored = native(w, 'read', {})['head']['head'] - assert stored['todos'][0]['note'] == 'Claim-neutral context' - assert not stored['todos'][0].get('claimed_by') and stored['leases'] == [] - request = {'schema_version': 'loopx_local_coordination_todo_update_request_v0', - 'runtime_root': str(w.root), 'goal_id': 'observable', 'todo_id': todo, 'role': 'agent', - 'actor_agent_id': 'agent-b', 'registered_agents': ['agent-a', 'agent-b'], - 'operation_id': 'caller-native-clear', 'patch': {}, 'clear_fields': ['note'], - 'dry_run': False, 'observed_at': '2026-09-07T00:00:00Z'} - cleared = native(w, 'updateLocalCoordinationTodo', request) - assert cleared['status'] == 'applied', cleared - after = native(w, 'read', {}) - assert 'note' not in after['head']['head']['todos'][0] - assert after['head']['head']['leases'] == stored['leases'] - rejected = native(w, 'updateLocalCoordinationTodo', {**request, 'operation_id': 'rejected', - 'todo_id': 'todo_missing', 'actor_agent_id': 'unknown', 'patch': {'text': 'Must not persist'}, 'clear_fields': []}) - assert rejected['status'] in {'failed', 'rejected'}, rejected - assert native(w, 'read', {}) == after - # Native transactions do not own display delivery; only the CLI facade - # regenerated it. Neither direct-native update nor rejection rewrites it. - assert w.state.read_bytes() == recovered - - -HTTP = r""" -import http.client, json, pathlib, sys, threading -from loopx.status_server import StatusHTTPServer, StatusRequestHandler -args=json.loads(sys.argv[1]); server=StatusHTTPServer(('127.0.0.1',0),StatusRequestHandler) -server.verbose=False; server.registry_path=pathlib.Path(args['registry']); server.runtime_root_override=args['root'] -server.reward_write_enabled=True; server.reward_dry_run_path='/reward/dry-run'; server.reward_append_path='/reward/append' -thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start() -rows=[] -def post(path,body): - conn=http.client.HTTPConnection('127.0.0.1',server.server_address[1],timeout=10) - conn.request('POST',path,json.dumps(body),{'Content-Type':'application/json','Origin':'http://localhost'}) - response=conn.getresponse(); payload=json.loads(response.read()); conn.close() - rows.append({'request':body,'status':response.status,'body':payload}); return payload -try: - body={'goal_id':'observable','run_generated_at':'2026-09-01T00:00:00Z', - 'recorded_at':'2026-09-01T01:00:00Z','decision':'continue','reward':'positive', - 'reason_summary':'HTTP argument readback','write_active_state_summary':True} - post('/reward/append',body) - preview=post('/reward/dry-run',{k:v for k,v in body.items() if k!='write_active_state_summary'}) - if 'preview_id' in preview: - post('/reward/append',{**body,'preview_id':preview['preview_id']}) -finally: - server.shutdown(); server.server_close(); thread.join(timeout=5) -print(json.dumps({'rows':rows})) -""" - - -def test_http_reward_preserves_preview_gate_and_persisted_arguments(caller: Caller) -> None: - w = caller - todo = w.add('HTTP must preserve Todo ownership', '--claimed-by', 'agent-a') - record = w.read(todo) - index = w.root / 'goals' / 'observable' / 'runs' / 'index.jsonl' - index.parent.mkdir(parents=True, exist_ok=True) - index.write_text(json.dumps({'generated_at': '2026-09-01T00:00:00Z', 'json_path': 'run.json', - 'markdown_path': 'run.md', 'classification': 'continue'}) + '\n') - result = w.invoke([sys.executable, '-c', HTTP, json.dumps({'registry': str(w.registry), 'root': str(w.root)})], - ['HTTP', 'reward preview then append']) - first, preview, appended = result['rows'] - assert first['status'] == 400 and first['body']['error'] == 'preview_id is required' - assert preview['status'] == 200 and preview['body']['dry_run'] is True - assert appended['status'] == 200 and appended['body']['appended'] is True - assert len(index.read_text().splitlines()) == 2 - assert 'HTTP argument readback' in index.read_text() and 'HTTP argument readback' in w.state.read_text() - assert w.read(todo) == record - - - - -def test_retired_event_source_refuses_without_primary_write(caller: Caller) -> None: - w = caller - log = w.path / "events.jsonl" - log.write_text('{"event_type":"todo_added","refs":{"todo_id":"todo_event_fixture"}}\n') - before = w.state.read_bytes(), log.read_bytes() - result = w.call('todo', 'complete', '--todo-id', 'todo_event_fixture', '--agent-id', 'agent-a', - '--evidence', 'validation://retired-source', '--no-follow-up') - assert result['ok'] is False, result - assert (w.state.read_bytes(), log.read_bytes()) == before diff --git a/tests/control_plane/test_shadow_observation_retirement.py b/tests/control_plane/test_shadow_observation_retirement.py deleted file mode 100644 index 1b748b0e39..0000000000 --- a/tests/control_plane/test_shadow_observation_retirement.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Retained migration observations must survive source-producer retirement.""" -from __future__ import annotations - -import json -from pathlib import Path -import subprocess - -import pytest - -from shadow_e2e_fixture import workspace -from test_canonical_todo_writer_isolation import ( - isolated_todo_distribution, retirement_wheel, # noqa: F401,F811 -) - - -def _bytes(root: Path): - return {str(path.relative_to(root)): path.read_bytes() - for path in root.rglob("*") if path.is_file() and path.suffix != ".lock"} - - -@pytest.mark.parametrize("producers_present", [True, False], ids=["present", "absent"]) -def test_complete_shadow_observation_without_producers( - tmp_path, isolated_todo_distribution, producers_present, # noqa: F811 -- imported pytest fixture -): - package, command = isolated_todo_distribution - w = workspace(tmp_path / "shadow", bootstrap=False) - w.command, w.cwd, w.package = tuple(command), tmp_path, package - assert w.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" - todo_id = w.add("Preserve the original source work")["todo_id"] - w.add("Second source mutation") - w.add("Third source mutation") - if not producers_present: - for name in ("runtime_shadow", "runtime_shadow_writer_adapter", - "local_authority_shadow_adapter", "local_authority_shadow_outbox"): - (package / f"control_plane/coordination/{name}.py").unlink() - check = subprocess.run([command[0], "-S", "-c", - "import importlib.util; assert importlib.util.find_spec(" - "'loopx.control_plane.coordination.runtime_shadow') is None"], - cwd=tmp_path, capture_output=True, text=True, timeout=45) - assert check.returncode == 0, check.stdout + check.stderr - - def observe(*args, success=True): - before = _bytes(w.registry.parent) - result = w.cli("coordination-shadow", *args, success=success) - assert _bytes(w.registry.parent) == before - assert result["executed"] is False - assert result["decision_read_from_shadow"] is False - return result - - inspected = observe("inspect") - assert inspected["inspection"]["status"] == "matched" - qualified = observe("qualify", "--require-event-kind", "todo_add") - assert qualified["qualification"]["status"] == "qualified" - insufficient = observe("qualify", "--minimum-operations", "4", success=False) - assert insufficient["ok"] is False - assert insufficient["qualification"]["qualified"] is False - candidate = observe("read-candidate", "--todo-id", todo_id) - assert candidate["read_candidate"]["read_candidate_qualified"] is True - missing = observe("read-candidate", "--todo-id", "todo_missing", success=False) - assert missing["ok"] is False - assert missing["read_candidate"]["read_candidate_qualified"] is False - - original = w.state.read_bytes() - w.state.write_text(w.state.read_text().replace("Third source mutation", "Changed source work")) - drifted = observe("inspect", success=False) - assert drifted["inspection"]["parity_matches"] is False - refused = observe("read-candidate", "--todo-id", todo_id, success=False) - assert refused["ok"] is False - assert refused["read_candidate"]["read_candidate_qualified"] is False - w.state.write_bytes(original) - assert observe("inspect")["inspection"]["status"] == "matched" - - store = next((w.runtime / "authority-shadow/file-v0").glob("authority-store-*.json")) - stored = store.read_bytes() - store.write_text("{unreadable historical store") - for args in [("inspect",), ("qualify",), ("read-candidate", "--todo-id", todo_id)]: - unavailable = observe(*args, success=False) - assert unavailable["ok"] is False - detail = unavailable[{"inspect": "inspection", "qualify": "qualification", - "read-candidate": "read_candidate"}[args[0]]] - assert detail["reason_code"] == "provider_protocol_violation" - store.write_bytes(stored) - assert observe("inspect")["inspection"]["status"] == "matched" - - store.rename(store.with_suffix(".retained")) - store.mkdir() - for args in [("inspect",), ("qualify",), ("read-candidate", "--todo-id", todo_id)]: - unavailable = observe(*args, success=False) - assert unavailable["ok"] is False - detail = unavailable[{"inspect": "inspection", "qualify": "qualification", - "read-candidate": "read_candidate"}[args[0]]] - assert detail["reason_code"] == "provider_read_unavailable" - store.rmdir() - store.with_suffix(".retained").rename(store) - assert observe("inspect")["inspection"]["status"] == "matched" - - directory = store.parent - retained = directory.with_name("retained-original") - directory.rename(retained) - assert observe("inspect", success=False)["inspection"]["parity_matches"] is False - for args in [("qualify",), ("read-candidate", "--todo-id", todo_id)]: - assert observe(*args, success=False)["ok"] is False - assert not directory.exists() - retained.rename(directory) - assert observe("inspect")["inspection"]["status"] == "matched" - - registry = json.loads(w.registry.read_text()) - registry["goals"][0]["coordination"]["runtime_shadow"]["enabled"] = False - w.registry.write_text(json.dumps(registry)) - for args in [("inspect",), ("qualify",), ("read-candidate", "--todo-id", todo_id)]: - disabled = observe(*args, success=False) - assert disabled["error_code"] == "coordination_shadow_not_enabled" - - -def test_historical_observation_imports_share_the_retained_owner(): - from loopx.control_plane.coordination import local_authority_shadow_readback as reader - from loopx.control_plane.coordination import runtime_shadow as old - - for name in ("inspect_coordination_runtime_shadow", "qualify_coordination_runtime_shadow", - "read_coordination_runtime_shadow_todo_candidate"): - assert getattr(old, name) is getattr(reader, name) diff --git a/tests/control_plane/test_shadow_readback_writer_isolation.py b/tests/control_plane/test_shadow_readback_writer_isolation.py deleted file mode 100644 index d1b452b819..0000000000 --- a/tests/control_plane/test_shadow_readback_writer_isolation.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Retained shadow diagnostics must survive source-producer retirement.""" -from __future__ import annotations - -import json -import subprocess - -import pytest - -from canonical_authority_fixture import initialize_canonical_authority -from test_canonical_todo_writer_isolation import ( - isolated_todo_distribution, retirement_wheel, # noqa: F401 -- distribution fixtures -) -from test_local_authority_shadow_outbox import ( - GOAL_ID, _capture, _fixture, _planned_todo, _record_change, -) -from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection -from loopx.todos import list_goal_todos - - -def _remove_capture_producers(package): - for name in ("runtime_shadow", "runtime_shadow_writer_adapter", - "local_authority_shadow_adapter", "local_authority_shadow_outbox"): - (package / "control_plane/coordination" / f"{name}.py").unlink() - (package / "control_plane/todos/legacy_mutation.py").unlink() - - -@pytest.mark.parametrize("configuration", [None, {"enabled": False}, {"enabled": True}]) -def test_empty_status_without_capture_producers_preserves_default_off( - tmp_path, configuration, isolated_todo_distribution, # noqa: F811 -): - package, command = isolated_todo_distribution - registry, state, runtime = _fixture(tmp_path, bootstrap=False) - if configuration is not None: - config = json.loads(registry.read_text()) - # Retired legacy observation configuration has no runtime schema. - config["goals"][0]["coordination"]["runtime_shadow"] = configuration - registry.write_text(json.dumps(config)) - original = registry.read_bytes(), state.read_bytes() - - def status(): - process = subprocess.run( - [*command, "--registry", str(registry), "--runtime-root", str(runtime), - "--format", "json", "authority-shadow", "status", "--goal-id", GOAL_ID], - cwd=tmp_path, capture_output=True, text=True, timeout=60, - ) - assert process.returncode == 0, process.stdout + process.stderr - result = json.loads(process.stdout) - assert result["runtime_config"]["enabled"] is False - assert result["candidate"]["status"] == "missing" - assert result["management"]["status"] == "missing" - assert not runtime.exists() - assert (registry.read_bytes(), state.read_bytes()) == original - return result - - expected = status() - _remove_capture_producers(package) - assert status() == expected - - -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_retained_status_is_read_only_with_capture_producers_absent( - tmp_path, provider, isolated_todo_distribution, # noqa: F811 -): - package, command = isolated_todo_distribution - registry, state, runtime = _fixture(tmp_path) - committed = _record_change(registry, state, runtime, "Retained committed work") - original = state.read_text() - _, planned = _planned_todo(original, "Prepared work never written") - prepared = _capture(registry, state, runtime, original_text=original) - prepared.prepare(planned) - assert prepared.outcome.failure is None - todos = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] - initialize_canonical_authority( - runtime, GOAL_ID, - build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=todos, - handoff_mode="hard_lease"), - state_path=state, provider=provider, - ) - directory = runtime / "authority-shadow/outbox" / GOAL_ID / "todos" - candidate = next((runtime / "authority-shadow/file-v0").glob("authority-store-*.json")) - original_candidate = candidate.read_bytes() - - def files(): - return {str(p.relative_to(tmp_path)): p.read_bytes() - for root in (runtime, state.parent) for p in root.rglob("*") if p.is_file()} - - def status(): - before = files() - process = subprocess.run( - [*command, "--registry", str(registry), "--runtime-root", str(runtime), - "--format", "json", "authority-shadow", "status", "--goal-id", GOAL_ID], - cwd=tmp_path, capture_output=True, text=True, timeout=60, - ) - assert "Traceback" not in process.stderr, process.stderr - payload = json.loads(process.stdout) - assert files() == before, "a status read must not drain, repair or initialize anything" - assert process.returncode == (0 if payload["ok"] else 1), payload - return payload - - # Each oracle has independent invariant checks as well as complete - # base/head parity; original bytes and identities are never rewritten. - def scenarios(): - answers = {} - answers["pending"] = status() - assert answers["pending"]["ok"] - assert answers["pending"]["outbox"]["todos"]["committed_pending"] == 1 - assert answers["pending"]["outbox"]["todos"]["prepared_only"] == 1 - assert answers["pending"]["candidate"]["codec_agreement"] is True - assert answers["pending"]["management"]["status"] == "active" - - cursor = directory / "drain-cursor.json" - cursor.write_text(json.dumps({ - "schema_version": "loopx_local_authority_shadow_drain_cursor_v0", - "partition": "todos", "last_seq": committed.outcome.seq, - "last_entry_id": committed.outcome.entry_id, - "last_partition_digest": committed.outcome.partition_digest, - "last_cursor": "retained:1", "last_provider_revision": "retained:1", - "updated_at": "2026-09-03T00:00:00Z", - })) - answers["residue"] = status() - assert answers["residue"]["ok"] - assert answers["residue"]["outbox"]["todos"]["retired_residue"] == 2 - assert answers["residue"]["outbox"]["todos"]["committed_pending"] == 1 - cursor.write_text('{"invalid_original":true}') - answers["bad_cursor"] = status() - assert not answers["bad_cursor"]["ok"] - assert answers["bad_cursor"]["outbox"]["todos"]["invalid"] == "outbox_file_invalid" - cursor.unlink() - - extra = directory / "unclassified.json" - extra.write_text('{"retain_original":true}') - answers["bad_entry"] = status() - assert not answers["bad_entry"]["ok"] - assert answers["bad_entry"]["outbox"]["todos"]["invalid"] == "outbox_file_invalid" - extra.unlink() - - candidate.write_text('{"damaged_candidate":true}') - answers["bad_candidate"] = status() - # The typed store reader returns a failed result for invalid bytes; - # only transport exceptions become the adapter's unavailable status. - assert answers["bad_candidate"]["candidate"]["status"] == "failed" - assert answers["bad_candidate"]["candidate"]["reason_code"] is not None - candidate.unlink() - answers["missing_candidate"] = status() - assert answers["missing_candidate"]["candidate"]["status"] == "missing" - assert not candidate.exists(), "status must not mint a replacement store identity" - candidate.write_bytes(original_candidate) - return answers - - expected = scenarios() - _remove_capture_producers(package) - assert scenarios() == expected - assert scenarios() == expected, "cold repeated CLI reads preserve the original history" diff --git a/tests/control_plane/test_shadow_registry_source.py b/tests/control_plane/test_shadow_registry_source.py index f35162f080..d5c8a5a0ea 100644 --- a/tests/control_plane/test_shadow_registry_source.py +++ b/tests/control_plane/test_shadow_registry_source.py @@ -4,8 +4,6 @@ import hashlib import json -import os -import subprocess from pathlib import Path import pytest @@ -25,7 +23,10 @@ from loopx.control_plane.coordination.local_authority_shadow_projection import ( ProjectionValueError, ) -from tests.control_plane.shadow_e2e_fixture import REPO, workspace as cli_workspace +from tests.control_plane.shadow_e2e_fixture import ( + ShadowWorkspace, + workspace as cli_workspace, +) def workspace(root: Path): @@ -103,6 +104,45 @@ def test_real_cli_rejects_unsafe_lease_source_before_bootstrap(tmp_path: Path, k assert list(outside.iterdir()) == [retained] +def test_source_snapshot_preserves_inventory_without_projecting_orphan_leases( + tmp_path: Path, +): + state, runtime, registry, goal, _ = workspace(tmp_path) + directory = runtime / "goals" / goal["id"] / "task-leases" + directory.mkdir(parents=True) + todo_ids = ("todo_alpha", "todo_Zulu", "todo_Bravo") + for todo_id in todo_ids: + (directory / f"{todo_id}.json").write_text( + json.dumps( + { + "schema_version": "task_lease_v0", + "goal_id": goal["id"], + "todo_id": todo_id, + "owner": "agent-a", + "status": "released", + "version": 1, + "write_scopes": ["src/"], + "idempotency_key": f"baseline-{todo_id}", + } + ), + encoding="utf-8", + ) + + projection, snapshot = capture(state, runtime, registry, goal) + + assert [entry["name"] for entry in snapshot["lease_inventory"]] == [ + "todo_Bravo.json", + "todo_Zulu.json", + "todo_alpha.json", + ] + assert projection["leases"] == [] + assert bootstrap(runtime, goal, projection, snapshot)["status"] == "applied" + view = read_local_authority_shadow( + runtime_root=runtime, goal_id=goal["id"], scan_limit=1 + ) + assert view["proof"]["transactions"][0]["projection"]["leases"] == [] + + @pytest.mark.parametrize("extension", [ {}, {"future_extension": None}, {"future_extension": False}, {"future_extension": {"missing_value": None, "flag": False}}, @@ -164,53 +204,6 @@ def test_terminal_lease_source_rejects_drift_before_publication(tmp_path: Path, assert lease.read_bytes() == changed_bytes -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_terminal_lease_survives_public_cutover_and_original_recovery(tmp_path: Path, provider): - ws = cli_workspace(tmp_path, bootstrap=False) - todo_id = "todo_0123456789ab" - ws.state.write_text(ws.state.read_text() + - f'- [x] Retained work\n \n') - expected = { - "schema_version": "task_lease_v0", "goal_id": ws.goal, "todo_id": todo_id, - "owner": "agent-a", "version": 2, "lease_epoch": 1, "status": "released", - "released_at": "2026-09-01T00:00:00Z", "idempotency_key": "retained-identity", - "future_extension": {"missing_value": None, "flag": False}, - } - lease = ws.runtime / "goals" / ws.goal / "task-leases" / f"{todo_id}.json" - lease.parent.mkdir(parents=True) - original = json.dumps(expected, indent=2).encode() - lease.write_bytes(original) - if provider == "sqlite": - selection = subprocess.run([os.environ.get("LOOPX_CONTROL_PLANE_NODE", "node"), - "--no-warnings", "--experimental-strip-types", "--experimental-sqlite", - str((REPO / "loopx/control_plane/coordination/local_authority_provider.ts").resolve()), "--runtime-root", - str(ws.runtime), "--goal-id", ws.goal, "--execute"], cwd=REPO, - capture_output=True, text=True, timeout=45, check=True) - assert json.loads(selection.stdout) == { - "ok": True, "provider": "sqlite", "changed": True, "executed": True, - } - assert ws.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" - for index in range(3): - ws.add(f"Independent migration work {index}") - assert ws.drain(budget_seconds="60")["ok"] is True - preview = ws.cli("coordination-shadow", "promote") - assert preview["promotion"]["status"] == "preview_ready", preview - saved = tmp_path / "reviewed.json" - saved.write_text(json.dumps(preview)) - applied = ws.cli("coordination-shadow", "promote", "--reviewed-plan", str(saved), "--execute") - assert applied["promotion"]["canonical_authority"] == f"{provider}_v0" - assert ws.cli("task-lease", "inspect", "--todo-id", todo_id)["lease"] == expected - later = ws.add("Later canonical work must survive recovery") - ws.state.unlink() - recovered = ws.cli("coordination-shadow", "recover-promotion", "--reviewed-plan", str(saved), "--execute") - assert recovered["promotion"]["status"] == "replayed", recovered - assert ws.cli("task-lease", "inspect", "--todo-id", todo_id)["lease"] == expected - retained = ws.cli("todo", "list", "--todo-id", later["todo_id"]) - assert retained["todo"]["text"] == "Later canonical work must survive recovery" - assert lease.read_bytes() == original - assert not ws.state.exists() - - @pytest.mark.parametrize( "change", ["delete_goal", "move_state", "move_runtime", "remove_agent", "disable_capture"], @@ -300,6 +293,60 @@ def test_strict_registry_envelope_keeps_existing_codec(tmp_path: Path): assert bootstrap(runtime, goal, projection, snapshot)["status"] == "applied" +def test_controller_rejects_an_override_runtime_root(tmp_path: Path): + ws = cli_workspace(tmp_path / "workspace", bootstrap=False) + original = ws.state.read_bytes() + override = ShadowWorkspace(ws.registry, tmp_path / "override-runtime", ws.state) + + for registered_is_active in (False, True): + if registered_is_active: + result = ws.cli("coordination-shadow", "bootstrap", "--execute") + assert result["bootstrap"]["status"] == "applied", result + + rejected = override.cli( + "coordination-shadow", "bootstrap", "--execute", success=False + ) + assert rejected["bootstrap"]["status"] == "failed" + assert ( + rejected["bootstrap"]["reason_code"] + == "shadow_source_runtime_root_mismatch" + ) + assert not (override.runtime / "authority-shadow/file-v0").exists() + assert ws.state.read_bytes() == original + + +def test_controller_rejects_an_alternate_state_file(tmp_path: Path): + ws = cli_workspace(tmp_path / "workspace", bootstrap=False) + alternate = ws.state.parent / "alternate-state.md" + alternate.write_bytes(ws.state.read_bytes()) + original = ws.state.read_bytes() + + for active in (False, True): + if active: + result = ws.cli("coordination-shadow", "bootstrap", "--execute") + assert result["bootstrap"]["status"] == "applied", result + + rejected = ws.cli( + "coordination-shadow", + "bootstrap", + "--project", + str(ws.state.parent), + "--state-file", + str(alternate), + "--execute", + success=False, + ) + assert rejected["bootstrap"]["status"] == "failed" + assert rejected["bootstrap"]["reason_code"] == ( + "management_operation_identity_mismatch" + if active + else "shadow_source_state_path_mismatch" + ) + assert ws.state.read_bytes() == alternate.read_bytes() == original + if not active: + assert not (ws.runtime / "authority-shadow/file-v0").exists() + + @pytest.mark.parametrize("writer", ["sync", "activation", "deletion"]) def test_global_registry_writers_respect_native_promotion_marker(tmp_path, writer): from loopx.file_lock import exclusive_mutation_file_lock, LockAcquireTimeoutError diff --git a/tests/control_plane/test_shadow_writer_boundaries.py b/tests/control_plane/test_shadow_writer_boundaries.py index 71be3c41b1..34503c6e8a 100644 --- a/tests/control_plane/test_shadow_writer_boundaries.py +++ b/tests/control_plane/test_shadow_writer_boundaries.py @@ -1,21 +1,17 @@ from __future__ import annotations -import hashlib import json import subprocess import sys -import time from pathlib import Path import pytest from loopx.control_plane.coordination.legacy_writer_fence import ( - LegacyCoordinationWriterFenced, + ActiveStateAuthorityMutationError, legacy_coordination_writer_fence_path, - legacy_todo_write_transaction, + require_prose_state_write_allowed, ) -from loopx.control_plane.todos.handoff_mode import set_goal_handoff_mode -from loopx.todos import add_goal_todo GOAL = "writer-boundary" @@ -42,6 +38,28 @@ def fixture(tmp_path: Path) -> tuple[Path, Path, Path]: return registry, state, root +def seed_todo( + state: Path, + text: str = "Retain canonical facts.", + *, + status: str = "open", + resume_when: str | None = None, +) -> None: + source = state.read_text(encoding="utf-8") + resume = f" resume_when={resume_when}" if resume_when else "" + state.write_text( + source.replace( + "## Agent Todo\n", + "## Agent Todo\n\n" + f"- [ ] {text}\n" + f" \n", + 1, + ), + encoding="utf-8", + ) + + @pytest.mark.parametrize("registry_directory", [".loopx", "standalone"]) @pytest.mark.parametrize("override", [None, "", "override", Path("path-override")]) def test_runtime_route_keeps_project_relative_precedence_without_writes( @@ -64,74 +82,6 @@ def test_runtime_route_keeps_project_relative_precedence_without_writes( assert not (owner / "declared").exists() -def test_handoff_writer_refuses_a_fence_before_primary( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, state, root = fixture(tmp_path) - fence = legacy_coordination_writer_fence_path(runtime_root=root, goal_id=GOAL) - fence.parent.mkdir(parents=True) - fence.write_text("{}", encoding="utf-8") - monkeypatch.setattr( - "loopx.control_plane.coordination.legacy_writer_fence.effect_runtime_result", - lambda *_args, **_kwargs: { - "status": "blocked", "reason_code": "legacy_coordination_writer_fenced", - }, - ) - before = state.read_bytes() - with pytest.raises(LegacyCoordinationWriterFenced): - set_goal_handoff_mode(registry_path=registry, goal_id=GOAL, mode="soft_claim") - assert state.read_bytes() == before - assert not (root / "authority-shadow").exists() - - -def test_handoff_mode_rechecks_legacy_fence_when_canonical_unavailable( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - """A provider outage must not bypass an already-present legacy fence.""" - - registry, state, root = fixture(tmp_path) - fence = legacy_coordination_writer_fence_path(runtime_root=root, goal_id=GOAL) - fence.parent.mkdir(parents=True) - fence.write_text("{}", encoding="utf-8") - monkeypatch.setattr( - "loopx.control_plane.todos.provider_handoff_mode.effect_runtime_result", - lambda *_args, **_kwargs: { - "status": "unavailable", - "reason_code": "canonical_provider_unavailable", - "reason": "canonical provider is unavailable", - }, - ) - monkeypatch.setattr( - "loopx.control_plane.coordination.legacy_writer_fence.effect_runtime_result", - lambda *_args, **_kwargs: { - "status": "blocked", - "reason_code": "legacy_coordination_writer_fenced", - }, - ) - before = state.read_bytes() - with pytest.raises(LegacyCoordinationWriterFenced) as error: - set_goal_handoff_mode(registry_path=registry, goal_id=GOAL, mode="soft_claim") - assert error.value.code == "legacy_coordination_writer_fenced" - assert error.value.payload["write_check"]["reason_code"] == "legacy_coordination_writer_fenced" - assert state.read_bytes() == before - assert not (root / "authority-shadow").exists() - - -def test_corrupt_management_state_blocks_before_transaction_body(tmp_path: Path) -> None: - registry, state, root = fixture(tmp_path) - digest = hashlib.sha256(GOAL.encode()).hexdigest()[:16] - management = root / "authority-transition" / "file-v0" / f"shadow-management-{digest}" / "state.json" - management.parent.mkdir(parents=True) - management.write_text("{corrupt", encoding="utf-8") - reached = False - try: - with legacy_todo_write_transaction(registry, GOAL, state, None, "test", False): - reached = True - except RuntimeError as error: - assert getattr(error, "reason_code", "") - assert not reached, "maintenance errors must not fall through to a primary write" - - def test_atomic_state_writer_keeps_original_on_failed_replace( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -167,7 +117,6 @@ def reward_fixture(tmp_path: Path) -> tuple[Path, Path, Path, dict[str, str]]: def test_reward_summary_cannot_inject_a_canonical_todo(tmp_path: Path) -> None: from loopx.feedback import append_human_reward - from loopx.control_plane.coordination.runtime_shadow_writer_adapter import ActiveStateAuthorityMutationError registry, state, index, reward = reward_fixture(tmp_path) # Insert before the first recognized Todo source, rather than after it. @@ -202,27 +151,6 @@ def plan_then_edit(**kwargs: object): assert "Review accepted." in state.read_text() -@pytest.mark.parametrize("preserve", [False, True]) -def test_force_bootstrap_cannot_erase_an_active_shadow_binding(tmp_path: Path, preserve: bool) -> None: - from loopx.bootstrap import bootstrap_project - - registry, state, root = fixture(tmp_path) - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": True, "schema_version": "loopx_coordination_runtime_shadow_config_v0", "provider": "file_v0", - } - registry.write_text(json.dumps(value)) - before = registry.read_bytes(), state.read_bytes() - with pytest.raises(RuntimeError, match="shadow"): - bootstrap_project(project=tmp_path, registry_path=registry, runtime_root=root, - goal_id=GOAL, objective="Rebuild safely.", domain="test", role="primary", - parent_goal_id=None, state_file=state, goal_doc=None, adapter_kind="generic_project_goal_v0", - adapter_status="connected", next_probe=None, spawn_allowed=False, max_children=0, - allowed_domains=[], write_scope=[], - force=True, preserve_todos=preserve, dry_run=False, sync_global=False) - assert (registry.read_bytes(), state.read_bytes()) == before - - def cli(registry: Path, *args: str) -> dict: result = subprocess.run([sys.executable, "-m", "loopx.entrypoint", "--registry", str(registry), "--format", "json", *args], cwd=REPO, text=True, capture_output=True, timeout=30) @@ -230,307 +158,6 @@ def cli(registry: Path, *args: str) -> dict: return json.loads(result.stdout) -@pytest.mark.parametrize("configured", [None, False]) -@pytest.mark.parametrize("operation", ["add", "bootstrap"]) -def test_disabled_capture_skips_shadow_inputs_on_real_writers( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, configured: bool | None, operation: str, -) -> None: - from loopx import bootstrap, rollout_event_log - from loopx.control_plane.coordination import local_authority_shadow_adapter as projection - from loopx.control_plane.coordination import runtime_shadow_writer_adapter as writer - - registry, state, root = fixture(tmp_path) - if configured is not None: - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": configured, - "schema_version": "loopx_coordination_runtime_shadow_config_v0", - "provider": "file_v0", - } - registry.write_text(json.dumps(value)) - begin = writer.begin_todo_runtime_shadow_capture - load = rollout_event_log.load_rollout_events - project = projection.todo_partition_projector - observed: list[str] = [] - - def read_events(*args, **kwargs): - observed.append("events") - return load(*args, **kwargs) - - def build_projector(*args, **kwargs): - observed.append("projector") - return project(*args, **kwargs) - - def observe_capture(**kwargs): - observed.append("begin") - # Observe only capture IO; other writer readers retain their real inputs. - with monkeypatch.context() as capture_observer: - capture_observer.setattr(rollout_event_log, "load_rollout_events", read_events) - capture_observer.setattr(projection, "todo_partition_projector", build_projector) - return begin(**kwargs) - - monkeypatch.setattr(writer, "begin_todo_runtime_shadow_capture", observe_capture) - before = state.read_bytes() - if operation == "add": - result = add_goal_todo(registry_path=registry, goal_id=GOAL, role="agent", - text="Keep disabled capture inert.", task_class="advancement_task", agent_id="agent-a") - assert result["added"] is True - assert "Keep disabled capture inert." in state.read_text() - assert result["coordination_runtime_shadow"]["outcome"] == "no_transaction" - assert result["coordination_runtime_shadow"]["reason_code"] == "shadow_disabled" - else: - result = bootstrap.bootstrap_project(project=tmp_path, registry_path=registry, - runtime_root=root, goal_id=GOAL, objective="Keep the legacy source complete.", - domain="test", role="primary", parent_goal_id=None, state_file=state, - goal_doc=None, adapter_kind="generic_project_goal_v0", adapter_status="connected", - next_probe=None, spawn_allowed=False, max_children=0, allowed_domains=[], - write_scope=[], force=False, preserve_todos=False, dry_run=False, sync_global=False) - assert result["ok"] is True - assert "## User Todo" in state.read_text() - assert state.read_bytes() != before - assert observed == ["begin"] - assert not (root / "authority-shadow").exists() - - -@pytest.mark.parametrize("projector_fails", [False, True]) -def test_active_binding_keeps_capture_after_configuration_is_disabled( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, projector_fails: bool, -) -> None: - from loopx.control_plane.coordination import local_authority_shadow_adapter as projection - from loopx.control_plane.coordination.shadow_management import ShadowManagementError - - registry, state, root = fixture(tmp_path) - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": True, "schema_version": "loopx_coordination_runtime_shadow_config_v0", "provider": "file_v0", - } - registry.write_text(json.dumps(value)) - cli(registry, "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute") - value["goals"][0]["coordination"]["runtime_shadow"]["enabled"] = False - registry.write_text(json.dumps(value)) - before = state.read_bytes() - if projector_fails: - def unavailable(*args, **kwargs): - raise OSError("projection input unavailable") - monkeypatch.setattr(projection, "todo_partition_projector", unavailable) - with pytest.raises(ShadowManagementError) as error: - add_goal_todo(registry_path=registry, goal_id=GOAL, role="agent", - text="Preserve the active lineage.", task_class="advancement_task", agent_id="agent-a") - assert error.value.reason_code == "shadow_capture_prepare_failed" - assert state.read_bytes() == before - assert not list((root / "authority-shadow" / "outbox" / GOAL / "todos").glob("*.committed.json")) - else: - result = add_goal_todo(registry_path=registry, goal_id=GOAL, role="agent", - text="Preserve the active lineage.", task_class="advancement_task", agent_id="agent-a") - assert result["added"] is True - assert result["coordination_runtime_shadow"]["outcome"] == "delivered" - assert "Preserve the active lineage." in state.read_text() - digest = hashlib.sha256(GOAL.encode()).hexdigest()[:16] - candidate = json.loads((root / "authority-shadow" / "file-v0" / f"authority-store-{digest}.json").read_text()) - assert candidate["cursor"] == "2" - - -def test_real_cli_handoff_and_todo_add_have_one_receipt_each(tmp_path: Path) -> None: - registry, state, root = fixture(tmp_path) - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": True, "schema_version": "loopx_coordination_runtime_shadow_config_v0", "provider": "file_v0", - } - registry.write_text(json.dumps(value)) - cli(registry, "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute") - handoff = cli(registry, "handoff-mode", "set", "--goal-id", GOAL, "--mode", "soft_claim") - added = cli(registry, "todo", "add", "--goal-id", GOAL, "--role", "agent", - "--text", "Inspect the read path.", "--task-class", "advancement_task", - "--evidence", "review fixture") - assert handoff["coordination_runtime_shadow"]["outcome"] == "delivered", handoff - assert added["coordination_runtime_shadow"]["outcome"] == "delivered", added - assert added["added"] is True - digest = hashlib.sha256(GOAL.encode()).hexdigest()[:16] - candidate = json.loads((root / "authority-shadow" / "file-v0" / f"authority-store-{digest}.json").read_text()) - assert candidate["cursor"] == "3", "bootstrap plus two primary writes must not get CLI mirror receipts" - assert len(candidate["committed"]) == 3 - assert "Inspect the read path." in state.read_text() - - -@pytest.mark.parametrize("phase", ["before", "after"]) -def test_real_process_kill_around_state_replace_preserves_complete_bytes(tmp_path: Path, phase: str) -> None: - state = tmp_path / "state.md" - state.write_bytes(b"original\r\n") - code = """ -import sys -from pathlib import Path -from loopx.control_plane.runtime import document_io as editing -original = editing.os.replace -def replace(source, target): - if sys.argv[2] == 'after': original(source, target) - print('replace-cut', flush=True) - sys.stdin.readline() - if sys.argv[2] == 'before': original(source, target) -editing.os.replace = replace -editing.atomic_write_state_text(Path(sys.argv[1]), 'replacement\\r\\n') -""" - child = subprocess.Popen([sys.executable, "-c", code, str(state), phase], cwd=REPO, - text=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - try: - assert child.stdout is not None - assert child.stdout.readline().strip() == "replace-cut" - child.kill() - child.communicate(timeout=10) - finally: - if child.poll() is None: - child.kill() - child.communicate(timeout=10) - assert state.read_bytes() == (b"original\r\n" if phase == "before" else b"replacement\r\n") - - -def test_cli_waiting_for_todo_mutex_rechecks_fence_after_engagement(tmp_path: Path) -> None: - from loopx.file_lock import exclusive_cross_runtime_file_lock - from loopx.control_plane.coordination.legacy_writer_fence import legacy_coordination_todo_lock_path - - registry, state, root = fixture(tmp_path) - before = state.read_bytes() - code = """ -import sys -from contextlib import contextmanager -from loopx.control_plane.coordination import legacy_writer_fence as guard -original = guard.exclusive_cross_runtime_file_lock -@contextmanager -def traced(path, **kwargs): - if path.name.startswith('legacy-todo-writer-'): print('waiting-for-todo-lock', flush=True) - with original(path, **kwargs) as held: yield held -guard.exclusive_cross_runtime_file_lock = traced -from loopx.entrypoint import main -sys.argv = ['loopx', *sys.argv[1:]] -raise SystemExit(main()) -""" - target = legacy_coordination_todo_lock_path(runtime_root=root, goal_id=GOAL) - lease_lock = root / "goals" / GOAL / "task-leases" / ".task-leases" - request = {"schema_version": "loopx_legacy_coordination_writer_fence_engage_request_v0", - "runtime_root": str(root), "goal_id": GOAL, "state_path": str(state), - "fence": {"schema_version": "loopx_legacy_coordination_writer_fence_v0", - "state": "engaged", "goal_id": GOAL, "fence_id": "race-fence", - "source_version": "race-source", "source_projection_sha256": "a" * 64, - "expected_shadow_provider_revision": "file:1:aaaaaaaaaaaaaaaaaaaaaaaa"}} - engage_code = "from loopx.control_plane.effect_runtime import effect_runtime_result; import sys,json; print(json.dumps(effect_runtime_result('coordination.local_authority.legacy_writer_fence.engage', json.loads(sys.argv[1]))))" - children = [] - try: - # Real engagement owns T and waits behind K; the public writer then waits - # behind engagement's T. No fixture writes a fence marker on its behalf. - with exclusive_cross_runtime_file_lock(lease_lock): - engage = subprocess.Popen([sys.executable, "-c", engage_code, json.dumps(request)], - cwd=REPO, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - children.append(engage) - deadline = time.monotonic() + 10 - while not Path(str(target) + ".ts-effect.lock").exists(): - assert engage.poll() is None, engage.communicate(timeout=1) - assert time.monotonic() < deadline, "engagement did not acquire the Todo lock" - time.sleep(0.01) - child = subprocess.Popen([sys.executable, "-c", code, "--registry", str(registry), "--format", "json", - "todo", "add", "--goal-id", GOAL, "--role", "agent", "--text", "Must be fenced.", - "--evidence", "race fixture"], cwd=REPO, - text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - children.append(child) - assert child.stdout is not None - assert child.stdout.readline().strip() == "waiting-for-todo-lock" - assert child.poll() is None - engage_output, engage_error = engage.communicate(timeout=30) - assert json.loads(engage_output)["status"] == "applied", engage_output + engage_error - output, error = child.communicate(timeout=30) - payload = json.loads(output) - assert child.returncode == 1, output + error - assert payload["error_code"] == "legacy_coordination_writer_fenced" - assert state.read_bytes() == before - finally: - for child in children: - if child.poll() is None: - child.kill() - child.communicate(timeout=10) - - -@pytest.mark.parametrize("override_root", [False, True]) -def test_real_writer_commits_before_a_later_fence_is_published(tmp_path: Path, override_root: bool) -> None: - registry, state, root = fixture(tmp_path) - writer_code = """ -import sys -from loopx.control_plane.runtime import document_io -original = document_io.atomic_write_state_text -def paused(*args): - print('primary-write-cut', flush=True) - sys.stdin.readline() - return original(*args) -document_io.atomic_write_state_text = paused -from loopx.entrypoint import main -raise SystemExit(main(sys.argv[1:])) -""" - request = {"schema_version": "loopx_legacy_coordination_writer_fence_engage_request_v0", - "runtime_root": str(root), "goal_id": GOAL, "state_path": str(state), - "fence": {"schema_version": "loopx_legacy_coordination_writer_fence_v0", - "state": "engaged", "goal_id": GOAL, "fence_id": "later-fence", - "source_version": "source-after-write", "source_projection_sha256": "a" * 64, - "expected_shadow_provider_revision": "file:1:aaaaaaaaaaaaaaaaaaaaaaaa"}} - children = [] - try: - writer = subprocess.Popen([sys.executable, "-c", writer_code, "--registry", str(registry), - "--runtime-root", str(tmp_path / "override" if override_root else root), - "--format", "json", "todo", "add", "--goal-id", GOAL, "--role", "agent", - "--text", "Primary won the lock.", "--evidence", "ordering fixture"], - cwd=REPO, text=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - children.append(writer) - assert writer.stdout is not None - assert writer.stdout.readline().strip() == "primary-write-cut" - engage_code = "from loopx.control_plane.effect_runtime import effect_runtime_result; import sys,json; print(json.dumps(effect_runtime_result('coordination.local_authority.legacy_writer_fence.engage', json.loads(sys.argv[1]))))" - engager = subprocess.Popen([sys.executable, "-c", engage_code, json.dumps(request)], - cwd=REPO, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - children.append(engager) - from loopx.control_plane.coordination.shadow_management import shadow_maintenance_lock_target - mutex = Path(str(shadow_maintenance_lock_target(root, GOAL)) + ".ts-effect.lock") - deadline = time.monotonic() + 10 - while not mutex.exists(): - assert engager.poll() is None, engager.communicate(timeout=1) - assert time.monotonic() < deadline - time.sleep(0.01) - time.sleep(.2) - assert not legacy_coordination_writer_fence_path(runtime_root=root, goal_id=GOAL).exists() - assert engager.poll() is None - output, error = writer.communicate("continue\n", timeout=30) - assert writer.returncode == 0, output + error - assert json.loads(output)["added"] is True - output, error = engager.communicate(timeout=30) - assert json.loads(output)["status"] == "applied", output + error - assert "Primary won the lock." in state.read_text() - finally: - for child in children: - if child.poll() is None: - child.kill() - child.communicate(timeout=10) - - -def test_failed_primary_replace_never_marks_shadow_committed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - from loopx.control_plane.runtime import document_io - registry, state, root = fixture(tmp_path) - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": True, "schema_version": "loopx_coordination_runtime_shadow_config_v0", "provider": "file_v0"} - registry.write_text(json.dumps(value)) - cli(registry, "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute") - before = state.read_bytes() - original = document_io.os.replace - def fail_primary(source: object, target: object) -> None: - if Path(str(target)) == state: - raise OSError("primary replace refused") - original(source, target) - monkeypatch.setattr(document_io.os, "replace", fail_primary) - with pytest.raises(OSError, match="primary replace refused"): - add_goal_todo( - registry_path=registry, goal_id=GOAL, role="agent", - text="Must stay prepared.", - ) - assert state.read_bytes() == before - directory = root / "authority-shadow" / "outbox" / GOAL / "todos" - assert len(list(directory.glob("*.prepared.json"))) == 1 - assert list(directory.glob("*.committed.json")) == [] - - def test_prose_only_reward_remains_allowed_under_a_legacy_fence(tmp_path: Path) -> None: from loopx.feedback import append_human_reward registry, state, _index, reward = reward_fixture(tmp_path) @@ -551,23 +178,17 @@ def test_prose_only_reward_remains_allowed_under_a_legacy_fence(tmp_path: Path) def test_prose_guard_ignores_resume_evaluation_clock( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: - from loopx.control_plane.coordination.runtime_shadow_writer_adapter import ( - require_prose_state_write_allowed, - ) - registry, state, root = fixture(tmp_path) - add_goal_todo( - registry_path=registry, - goal_id=GOAL, - role="agent", - text="Resume the bounded inspection.", + seed_todo( + state, + "Resume the bounded inspection.", status="deferred", resume_when="resume_at:2099-01-01T00:00:00Z", ) original = state.read_text(encoding="utf-8") planned = original + "\nA bounded narrative observation.\n" - from loopx.control_plane.coordination.local_authority_shadow_adapter import ( + from loopx.control_plane.coordination.authority_source_capture import ( todo_partition_projector, ) from loopx.control_plane.coordination.local_authority_shadow_projection import ( @@ -625,12 +246,9 @@ def test_prose_guard_preserves_json_scalar_identity( from loopx.control_plane.coordination.local_authority_shadow_projection import ( todo_partition_projection, ) - from loopx.control_plane.coordination.runtime_shadow_writer_adapter import ( - ActiveStateAuthorityMutationError, require_prose_state_write_allowed, - ) registry, state, root = fixture(tmp_path) - add_goal_todo(registry_path=registry, goal_id=GOAL, role="agent", text="Retain result facts.") + seed_todo(state, "Retain result facts.") original = state.read_text(encoding="utf-8") planned = original + "\nA narrative observation.\n" real_projector = adapter.todo_partition_projector @@ -675,9 +293,6 @@ def test_prose_only_reward_holds_before_index_append_during_maintenance(tmp_path def test_prose_guard_rejects_a_handoff_change(tmp_path: Path) -> None: - from loopx.control_plane.coordination.runtime_shadow_writer_adapter import ( - ActiveStateAuthorityMutationError, require_prose_state_write_allowed, - ) registry, state, root = fixture(tmp_path) original = state.read_text() with pytest.raises(ActiveStateAuthorityMutationError, match="would change canonical"): @@ -694,7 +309,6 @@ def test_prose_projection_failure_preserves_source_and_reward_index( ) -> None: from loopx.feedback import append_human_reward from loopx.control_plane.coordination import authority_source_capture as source - from loopx.control_plane.coordination.runtime_shadow_writer_adapter import ActiveStateAuthorityMutationError from loopx.control_plane.coordination.shadow_management import ShadowManagementError, shadow_management_state_path registry, state, index, reward = reward_fixture(tmp_path) @@ -723,8 +337,8 @@ def test_configuration_and_prose_guard_load_without_capture_modules(tmp_path, mo from test_cold_source_import_cli import workspace _, state, _, _, runtime, receiver, env = workspace(tmp_path, monkeypatch) - for name in ("runtime_shadow.py", "runtime_shadow_writer_adapter.py", - "local_authority_shadow_adapter.py", "local_authority_shadow_outbox.py"): + for name in ("runtime_shadow.py", "local_authority_shadow_adapter.py", + "local_authority_shadow_outbox.py"): (receiver / "loopx/control_plane/coordination" / name).unlink() registry = tmp_path / "project/.loopx/registry.json" child = subprocess.run([sys.executable, "-c", """ @@ -750,127 +364,6 @@ def test_configuration_and_prose_guard_load_without_capture_modules(tmp_path, mo assert child.returncode == 0, child.stdout + child.stderr -def test_override_root_is_the_only_maintenance_authority(tmp_path: Path) -> None: - from loopx.control_plane.coordination.shadow_management import ShadowManagementError, shadow_management_state_path - registry, state, root = fixture(tmp_path) - override = tmp_path / "override" - management = shadow_management_state_path(override, GOAL) - management.parent.mkdir(parents=True) - management.write_text("{}") - with pytest.raises(ShadowManagementError): - add_goal_todo( - registry_path=registry, goal_id=GOAL, runtime_root_arg=str(override), - role="agent", text="Hold override.", - ) - assert "Hold override." not in state.read_text() - assert not (root / "authority-transition").exists() - result = add_goal_todo( - registry_path=registry, goal_id=GOAL, role="agent", - text="Default root remains writable.", - ) - assert result["added"] is True - - -@pytest.mark.parametrize("writer", ["todo", "prose"]) -def test_override_root_cannot_bypass_registry_source_maintenance(tmp_path: Path, writer: str) -> None: - from loopx.control_plane.coordination.shadow_management import ShadowManagementError, shadow_management_state_path - from loopx.feedback import append_human_reward - registry, state, index, reward = reward_fixture(tmp_path) - root = tmp_path / "runtime" - override = tmp_path / "override" - override_index = override / "goals" / GOAL / "runs" / "index.jsonl" - override_index.parent.mkdir(parents=True) - override_index.write_bytes(index.read_bytes()) - management = shadow_management_state_path(root, GOAL) - management.parent.mkdir(parents=True) - management.write_text("{}") - before = state.read_bytes() - with pytest.raises(ShadowManagementError): - if writer == "todo": - add_goal_todo( - registry_path=registry, goal_id=GOAL, runtime_root_arg=str(override), - role="agent", text="Cannot bypass source maintenance.", - ) - else: - append_human_reward(registry_path=registry, runtime_root_override=str(override), - goal_id=GOAL, run_generated_at=None, reward=reward, actor_kind="owner", - write_active_state_summary=True) - assert state.read_bytes() == before - - -def test_override_root_keeps_prose_writable_with_an_active_source_binding(tmp_path: Path) -> None: - from loopx.feedback import append_human_reward - registry, state, index, reward = reward_fixture(tmp_path) - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": True, "schema_version": "loopx_coordination_runtime_shadow_config_v0", "provider": "file_v0"} - registry.write_text(json.dumps(value)) - cli(registry, "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute") - override = tmp_path / "override" - override_index = override / "goals" / GOAL / "runs" / "index.jsonl" - override_index.parent.mkdir(parents=True) - override_index.write_bytes(index.read_bytes()) - refreshed = append_human_reward(registry_path=registry, runtime_root_override=str(override), - goal_id=GOAL, run_generated_at=None, reward=reward, actor_kind="owner", - write_active_state_summary=True) - assert refreshed["appended"] is True - assert "Review accepted." in state.read_text() - assert not (override / "authority-shadow" / "outbox" / GOAL).exists() - - -def test_waiting_override_writer_rechecks_registry_binding_inside_shared_state_lock(tmp_path: Path) -> None: - registry, state, _root = fixture(tmp_path) - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": True, "schema_version": "loopx_coordination_runtime_shadow_config_v0", "provider": "file_v0"} - registry.write_text(json.dumps(value)) - before = state.read_bytes() - waiting, proceed = tmp_path / "waiting", tmp_path / "proceed" - code = """ -import sys,time -from contextlib import contextmanager -from pathlib import Path -from loopx.control_plane.coordination import legacy_writer_fence as guard -state,waiting,proceed = map(Path, sys.argv[1:4]) -original = guard.exclusive_cross_runtime_file_lock -@contextmanager -def traced(path, **kwargs): - if path == state: - waiting.write_text('waiting for actual shared state lock') - deadline = time.monotonic()+20 - while not proceed.exists(): - if time.monotonic()>deadline: raise TimeoutError('parent did not release writer') - time.sleep(.01) - with original(path, **kwargs) as held: yield held -guard.exclusive_cross_runtime_file_lock = traced -from loopx.entrypoint import main -sys.argv = ['loopx', *sys.argv[4:]] -raise SystemExit(main()) -""" - child = subprocess.Popen([sys.executable, "-c", code, str(state), str(waiting), str(proceed), - "--registry", str(registry), "--runtime-root", str(tmp_path / "override"), "--format", "json", - "todo", "add", "--goal-id", GOAL, "--role", "agent", "--text", "Must observe the new source binding.", - "--evidence", "cross-root race"], cwd=REPO, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - try: - deadline = time.monotonic() + 10 - while not waiting.exists(): - assert child.poll() is None, child.communicate(timeout=1) - assert time.monotonic() < deadline, "writer did not reach shared state lock" - time.sleep(.01) - bootstrap = cli(registry, "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute") - assert bootstrap["bootstrap"]["status"] == "applied" - proceed.write_text("source root is now active") - output, error = child.communicate(timeout=30) - payload = json.loads(output) - assert child.returncode == 1, output + error - assert payload["error_code"] == "shadow_source_runtime_root_mismatch" - assert state.read_bytes() == before - finally: - if child.poll() is None: - child.kill() - child.communicate(timeout=10) - - def test_migration_refuses_overwriting_a_managed_target_before_copy(tmp_path: Path) -> None: from loopx.state_migration import migrate_legacy_state source = tmp_path / "source" @@ -892,104 +385,9 @@ def test_migration_refuses_overwriting_a_managed_target_before_copy(tmp_path: Pa assert (target_state.read_bytes(), target_registry.read_bytes(), source_state.read_bytes()) == before -def test_registry_missing_state_reconstruction_respects_maintenance(tmp_path: Path) -> None: - from loopx.control_plane.projects.registry import register_project_goal - from loopx.control_plane.coordination.shadow_management import ShadowManagementError, shadow_management_state_path - registry = tmp_path / "registry.json" - root = tmp_path / "runtime" - args = dict(registry_path=registry, runtime_root=root, project_id="project-a", project_kind="work", - knowledge_root=tmp_path, goal_id=GOAL, objective="Keep the source intact.", non_goals=[], - acceptance=["State exists."], unknowns=[], next_effect="Inspect state.", stop_condition="Done.", - repository_bindings=[], external_locator_bindings=[]) - created = register_project_goal(**args) - state = Path(created["state_file"]) - state.unlink() - before = registry.read_bytes() - management = shadow_management_state_path(root, GOAL) - management.parent.mkdir(parents=True, exist_ok=True) - management.write_text("{}") - with pytest.raises(ShadowManagementError): - register_project_goal(**args) - assert not state.exists() - assert registry.read_bytes() == before - - - -def test_active_capture_prepare_failure_holds_primary_before_any_transition(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox - from loopx.control_plane.coordination.shadow_management import ShadowManagementError - registry, state, root = fixture(tmp_path) - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": True, "schema_version": "loopx_coordination_runtime_shadow_config_v0", "provider": "file_v0"} - registry.write_text(json.dumps(value)) - cli(registry, "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute") - before = state.read_bytes() - original = outbox.durable_write_json - def refuse_prepare(path: Path, record: object) -> None: - if path.name.endswith(".prepared.json"): - raise OSError("prepared durability unavailable") - original(path, record) - monkeypatch.setattr(outbox, "durable_write_json", refuse_prepare) - for mode in ["soft_claim", "hard_lease"]: - with pytest.raises(ShadowManagementError) as error: - set_goal_handoff_mode(registry_path=registry, goal_id=GOAL, mode=mode) - assert error.value.reason_code == "shadow_capture_prepare_failed" - assert state.read_bytes() == before - directory = root / "authority-shadow" / "outbox" / GOAL / "todos" - assert list(directory.glob("*.committed.json")) == [] - - -@pytest.mark.parametrize("operation", ["add", "update", "complete", "supersede", "archive"]) -def test_all_todo_transaction_owners_enforce_active_preparation( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, operation: str, -) -> None: - from loopx import todos - from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox - from loopx.control_plane.coordination.shadow_management import ShadowManagementError - registry, state, root = fixture(tmp_path) - seed = todos.add_goal_todo(registry_path=registry, goal_id=GOAL, role="agent", - text="Characterize this transaction owner.", status="open", - task_class="advancement_task", action_kind="analyze", agent_id="agent-a") - if operation == "archive": - completed = todos.complete_goal_todo(registry_path=registry, goal_id=GOAL, todo_id=seed["todo_id"], - evidence="Seed completion.", next_agent_todo="Next bounded seed.", - next_task_class="advancement_task", agent_id="agent-a") - assert completed["ok"] is True - value = json.loads(registry.read_text()) - value["goals"][0]["coordination"]["runtime_shadow"] = { - "enabled": True, "schema_version": "loopx_coordination_runtime_shadow_config_v0", "provider": "file_v0"} - registry.write_text(json.dumps(value)) - cli(registry, "coordination-shadow", "bootstrap", "--goal-id", GOAL, "--execute") - original = outbox.durable_write_json - def fail_prepare(path: Path, record: object) -> None: - if path.name.endswith(".prepared.json"): - raise OSError("prepare is unavailable") - original(path, record) - monkeypatch.setattr(outbox, "durable_write_json", fail_prepare) - before = state.read_bytes() - identity = dict(registry_path=registry, goal_id=GOAL) - with pytest.raises(ShadowManagementError) as held: - if operation == "add": - todos.add_goal_todo(**identity, role="agent", text="A new obligation.", task_class="advancement_task") - elif operation == "update": - todos.update_goal_todo(**identity, todo_id=seed["todo_id"], note="Changed note.", agent_id="agent-a") - elif operation == "complete": - todos.complete_goal_todo(**identity, todo_id=seed["todo_id"], evidence="Check completed.", - next_agent_todo="Next bounded check.", next_task_class="advancement_task", agent_id="agent-a") - elif operation == "supersede": - todos.supersede_goal_todo(**identity, todo_id=seed["todo_id"], reason="Replace the approach.", - next_agent_todo="Use a better check.", next_task_class="advancement_task", agent_id="agent-a") - else: - todos.archive_completed_todos(**identity, max_active_done=0, dry_run=False) - assert held.value.reason_code == "shadow_capture_prepare_failed" - assert state.read_bytes() == before - - def test_public_refresh_step_does_not_write_owned_paragraph(tmp_path: Path) -> None: registry, state, root = fixture(tmp_path) - add_goal_todo(registry_path=registry, goal_id=GOAL, role="agent", - text="Inspect the bounded result.", task_class="advancement_task", agent_id="agent-a") + seed_todo(state, "Inspect the bounded result.") before = state.read_bytes() result = cli(registry, "refresh-state", "--goal-id", GOAL, "--agent-id", "agent-a", "--next-action", "Compare the current evidence.", "--no-global-sync") diff --git a/tests/control_plane/test_shadow_writer_variant_e2e.py b/tests/control_plane/test_shadow_writer_variant_e2e.py deleted file mode 100644 index 569ac4da71..0000000000 --- a/tests/control_plane/test_shadow_writer_variant_e2e.py +++ /dev/null @@ -1,321 +0,0 @@ -"""Public writer combinations with real persistence and scheduling-only seams.""" -from __future__ import annotations - -import json -from pathlib import Path -import select -import subprocess -import sys - -import pytest - -from shadow_e2e_fixture import ShadowWorkspace, workspace as make_workspace -from loopx.control_plane.coordination.coordination_state_contract_generated import TASK_LEASE_ACQUIRE_REQUEST_SCHEMA -from loopx.control_plane.coordination.legacy_writer_fence import legacy_coordination_writer_fence_path -from loopx.control_plane.work_items.task_lease_acquire_adapter import task_lease_acquire_authority_facts - -REPO = Path(__file__).resolve().parents[2] -pytestmark = pytest.mark.stage2c_e2e - - -def workspace(path: Path) -> ShadowWorkspace: - result = make_workspace(path, bootstrap=False) - result.state.write_text(result.state.read_text() + "\n## Progress Ledger\n\n## Next Action\n\n- Inspect.\n") - return result - - -def command(ws: ShadowWorkspace, *args: str, goal: str | None = None) -> list[str]: - return [sys.executable, "-m", "loopx.cli", "--registry", str(ws.registry), - "--runtime-root", str(ws.runtime), "--format", "json", *args, "--goal-id", goal or ws.goal] - - -def public(ws: ShadowWorkspace, *args: str, goal: str | None = None) -> dict: - result = subprocess.run(command(ws, *args, goal=goal), cwd=REPO, capture_output=True, text=True, timeout=30) - assert "Traceback" not in result.stderr, result.stderr - return json.loads(result.stdout) - - -def start(args: list[str]) -> subprocess.Popen[str]: - return subprocess.Popen(args, cwd=REPO, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - - -def barrier(child: subprocess.Popen[str], phase: str) -> None: - assert child.stdout is not None - ready, _, _ = select.select([child.stdout], [], [], 10) - assert ready, "public writer did not reach its real scheduling boundary" - line = child.stdout.readline().strip() - assert line == "BARRIER " + phase, line - - -def finish(child: subprocess.Popen[str], *, resume: bool = False) -> dict: - output, error = child.communicate("continue\n" if resume else None, timeout=30) - assert "Traceback" not in error, output + error - return json.loads(output) - - -def stop(child: subprocess.Popen[str] | None) -> None: - if child is not None: - if child.poll() is None: - child.kill() - child.communicate(timeout=10) - - -NATIVE = r""" -import fs from 'node:fs'; -import {syncBuiltinESMExports} from 'node:module'; -import {once} from 'node:events'; -const input = JSON.parse(process.argv[1]); -if (input.wait_lock) { - const actual = fs.promises.open; - let notified = false; - fs.promises.open = async (path, flags, ...args) => { - if (String(path) === input.wait_lock + '.ts-effect.lock' && flags === 'wx' && !notified) { - notified = true; process.stdout.write('BARRIER native-lock\n'); - } - return await actual(path, flags, ...args); - }; - syncBuiltinESMExports(); -} -const owner = await import(input.module); -const dependencies = input.pause_write ? {beforeWrite:async () => { - process.stdout.write('BARRIER native-write\n'); await once(process.stdin, 'data'); -}} : {}; -process.stdout.write(JSON.stringify(await owner[input.function](input.request, dependencies)) + '\n'); -""" - - -def native(module: str, function: str, request: dict, **options: object) -> list[str]: - return ["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", NATIVE, - json.dumps({"module": (REPO / module).as_uri(), "function": function, "request": request, **options})] - - -def fence_request(ws: ShadowWorkspace) -> dict: - return {"schema_version": "loopx_legacy_coordination_writer_fence_engage_request_v0", - "runtime_root": str(ws.runtime), "goal_id": ws.goal, "state_path": str(ws.state), - "fence": {"schema_version": "loopx_legacy_coordination_writer_fence_v0", "state": "engaged", - "goal_id": ws.goal, "fence_id": "variant-fence", "source_version": "variant-source", - "source_projection_sha256": "a" * 64, - "expected_shadow_provider_revision": "file:1:aaaaaaaaaaaaaaaaaaaaaaaa"}} - - -def fence_command(ws: ShadowWorkspace, **options: object) -> list[str]: - return native("loopx/control_plane/coordination/legacy_writer_fence.ts", - "engageLegacyCoordinationWriterFence", fence_request(ws), **options) - - -@pytest.mark.parametrize("authority", ["fence", "active_capture"]) -def test_other_goal_cannot_write_a_protected_goal_source_via_state_override(tmp_path: Path, authority: str) -> None: - ws = workspace(tmp_path) - other_state = tmp_path / "OTHER_GOAL_STATE.md" - other_state.write_text("---\ngoal_id: goal-other\n---\n\n## Agent Todo\n") - registry = json.loads(ws.registry.read_text()) - registry["goals"].append({"id": "goal-other", "repo": str(tmp_path), "state_file": other_state.name, - "coordination": {"registered_agents": ["agent-a"]}}) - ws.registry.write_text(json.dumps(registry)) - # Preserve the existing opt-out contract before any source authority exists. - legacy = public(ws, "todo", "add", "--state-file", str(ws.state), "--role", "agent", - "--text", "An unbound shared-state write remains supported.", - "--evidence", "Legacy compatibility control.", goal="goal-other") - assert legacy["ok"] is True and legacy["added"] is True, legacy - if authority == "active_capture": - # A registered source stays protected even without a frontmatter owner. - ws.state.write_text(ws.state.read_text().replace(f"goal_id: {ws.goal}\n", "")) - assert public(ws, "coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" - else: - result = subprocess.run(fence_command(ws), cwd=REPO, capture_output=True, text=True, check=True, timeout=20) - assert json.loads(result.stdout)["status"] == "applied" - before = ws.state.read_bytes(), other_state.read_bytes() - result = public(ws, "todo", "add", "--state-file", str(ws.state), "--role", "agent", - "--text", "Must not bypass another goal's source authority.", - "--evidence", "Cross-goal source boundary.", goal="goal-other") - assert not result.get("ok"), json.dumps(result, indent=2) - expected = "shadow_source_goal_mismatch" if authority == "active_capture" else "legacy_coordination_writer_fenced" - assert result["error_code"] == expected, result - assert (ws.state.read_bytes(), other_state.read_bytes()) == before - assert not (ws.runtime / "authority-shadow" / "outbox" / "goal-other").exists() - if authority == "active_capture": - unknown = public(ws, "todo", "add", "--state-file", str(ws.state), "--role", "agent", - "--text", "An unregistered goal cannot bypass source ownership.", - "--evidence", "Unregistered goal control.", goal="unregistered-goal") - assert not unknown.get("ok"), unknown - assert (ws.state.read_bytes(), other_state.read_bytes()) == before - - -PUBLIC_WORKER = r""" -import sys -from contextlib import contextmanager -from pathlib import Path -stage = sys.argv[1] -def pause(label): - print('BARRIER ' + label, flush=True) - sys.stdin.readline() -if stage == 'reward_plan': - from loopx import feedback - original = feedback.plan_active_state_update - def planned(*args, **kwargs): - result = original(*args, **kwargs) - pause('reward-plan') - return result - feedback.plan_active_state_update = planned -elif stage == 'reward_write': - from loopx import feedback - original = feedback.atomic_write_state_text - def write(*args, **kwargs): - pause('reward-write') - return original(*args, **kwargs) - feedback.atomic_write_state_text = write -elif stage == 'handoff_write': - from loopx.control_plane.runtime import document_io - original = document_io.atomic_write_state_text - def write(*args, **kwargs): - pause('handoff-write') - return original(*args, **kwargs) - document_io.atomic_write_state_text = write -elif stage == 'handoff_k': - from loopx import file_lock - original = file_lock.exclusive_cross_runtime_file_lock - @contextmanager - def lock(path, *args, **kwargs): - if Path(path).name == '.task-leases': - print('BARRIER handoff-lock', flush=True) - with original(path, *args, **kwargs) as held: - yield held - file_lock.exclusive_cross_runtime_file_lock = lock -from loopx.entrypoint import main -raise SystemExit(main(sys.argv[2:])) -""" - - -def paused_public(ws: ShadowWorkspace, stage: str, *args: str) -> subprocess.Popen[str]: - return start([sys.executable, "-c", PUBLIC_WORKER, stage, *command(ws, *args)[3:]]) - - -@pytest.mark.parametrize("first", ["lease", "handoff"]) -def test_handoff_and_native_acquire_serialize_quiescence_and_source_evidence(tmp_path: Path, first: str) -> None: - ws = workspace(tmp_path) - added = ws.add("Unclaimed work eligible for a lease.") - todo_id = added["todo_id"] - assert public(ws, "coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" - request = {"schema_version": TASK_LEASE_ACQUIRE_REQUEST_SCHEMA, "runtime_root": str(ws.runtime), "goal_id": ws.goal, - "todo_id": todo_id, "owner": "agent-a", "idempotency_key": "native-variant-acquire", "ttl_seconds": 120, - "write_scopes": [], "expected_version": None, - "authority": task_lease_acquire_authority_facts(registry_path=ws.registry, goal_id=ws.goal, todo_id=todo_id)} - lease_dir = ws.runtime / "goals" / ws.goal / "task-leases" - def lease_command(**options: object) -> list[str]: - return native("loopx/control_plane/work_items/task_lease_acquire.ts", - "executeTaskLeaseAcquire", request, **options) - handoff_args = ("handoff-mode", "set", "--mode", "soft_claim") - lease = handoff = None - original = ws.state.read_bytes() - try: - if first == "lease": - lease = start(lease_command(pause_write=True)) - barrier(lease, "native-write") - handoff = paused_public(ws, "handoff_k", *handoff_args) - barrier(handoff, "handoff-lock") - assert handoff.poll() is None - acquired = finish(lease, resume=True) - refused = finish(handoff) - assert acquired["acquired"] is True, acquired - assert refused["error_code"] == "handoff_mode_not_quiescent", refused - assert ws.state.read_bytes() == original - assert json.loads((lease_dir / f"{todo_id}.json").read_text())["owner"] == "agent-a" - else: - handoff = paused_public(ws, "handoff_write", *handoff_args) - barrier(handoff, "handoff-write") - lease = start(lease_command(wait_lock=str(lease_dir / ".task-leases"))) - barrier(lease, "native-lock") - assert lease.poll() is None - changed = finish(handoff, resume=True) - refused = finish(lease) - assert changed["changed"] is True, changed - assert refused["error_code"] == "authority_source_changed", refused - assert "handoff_mode: soft_claim" in ws.state.read_text() - assert not (lease_dir / f"{todo_id}.json").exists() - public(ws, "authority-shadow", "drain") - inspected = public(ws, "coordination-shadow", "inspect") - assert inspected["inspection"]["status"] == "matched", inspected - assert inspected["inspection"]["cursor"] == "2", inspected - finally: - stop(lease) - stop(handoff) - - -def prepare_rewards(ws: ShadowWorkspace) -> Path: - index = ws.runtime / "goals" / ws.goal / "runs" / "index.jsonl" - index.parent.mkdir(parents=True, exist_ok=True) - index.write_text(json.dumps({"generated_at": "2026-09-05T00:00:00Z", "json_path": "run.json", - "markdown_path": "run.md", "classification": "continue"}) + "\n") - return index - - -def reward_args(reason: str, timestamp: str) -> tuple[str, ...]: - return ("reward", "--actor-kind", "owner", "--decision", "continue", "--reward", "positive", - "--reason-summary", reason, "--recorded-at", timestamp, "--write-active-state-summary") - - -def test_concurrent_public_rewards_preserve_both_summaries_and_run_overlays(tmp_path: Path) -> None: - ws = workspace(tmp_path) - index = prepare_rewards(ws) - assert public(ws, "coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" - first = paused_public(ws, "reward_plan", *reward_args("First independent review.", "2026-09-05T00:01:00Z")) - try: - barrier(first, "reward-plan") - second = public(ws, *reward_args("Second independent review.", "2026-09-05T00:02:00Z")) - assert second["ok"] is True, second - result = finish(first, resume=True) - assert result["ok"] is True, result - text = ws.state.read_text() - assert text.count("First independent review.") == 1 - assert text.count("Second independent review.") == 1 - overlays = [json.loads(line)["human_reward"]["reason_summary"] for line in index.read_text().splitlines()[1:]] - assert sorted(overlays) == ["First independent review.", "Second independent review."] - inspection = public(ws, "coordination-shadow", "inspect")["inspection"] - assert inspection["status"] == "matched", inspection - assert inspection["cursor"] == "1", inspection - finally: - stop(first) - - -def test_native_fence_waits_for_public_prose_and_then_blocks_todo_writes(tmp_path: Path) -> None: - ws = workspace(tmp_path) - index = prepare_rewards(ws) - prose = paused_public(ws, "reward_write", *reward_args("Prose committed before the fence.", "2026-09-05T00:01:00Z")) - fence = None - try: - barrier(prose, "reward-write") - fence = start(fence_command(ws, wait_lock=str(ws.state))) - barrier(fence, "native-lock") - fence_path = legacy_coordination_writer_fence_path(runtime_root=ws.runtime, goal_id=ws.goal) - assert not fence_path.exists() - assert fence.poll() is None - assert finish(prose, resume=True)["ok"] is True - assert finish(fence)["status"] == "applied" - before = ws.state.read_bytes() - result = public( - ws, "todo", "add", "--role", "agent", "--text", "Must now be fenced.", - "--evidence", "Boundary check.", - ) - # The fence selects native create; its owner reads the complete head, - # rather than the retired Python list preflight. Missing authority must - # reject without falling through to this still-writable source document. - assert result["ok"] is False and result["added"] is False, result - assert result["error_code"] == "local_authority_todo_list_unavailable", result - assert result["error"] == "canonical Todo authority is unavailable", result - assert result["status"] == "missing", result - assert result["source_authority"] == "file_v0", result - assert result["decision_read_from_provider"] is True, result - assert result["legacy_fallback_used"] is False, result - assert result["recovery"] == { - "action": "restore_canonical_authority", - "runtime_root": str(ws.runtime.resolve()), - "goal_id": ws.goal, - "legacy_markdown_fallback_allowed": False, - "retry_after": "canonical_provider_readback_loaded", - }, result - assert ws.state.read_bytes() == before - assert "Prose committed before the fence." in ws.state.read_text() - assert len(index.read_text().splitlines()) == 2 - finally: - stop(prose) - stop(fence) diff --git a/tests/control_plane/test_shared_goal_authority_e2e.py b/tests/control_plane/test_shared_goal_authority_e2e.py index 9be2ebbe50..702d308611 100644 --- a/tests/control_plane/test_shared_goal_authority_e2e.py +++ b/tests/control_plane/test_shared_goal_authority_e2e.py @@ -8,11 +8,9 @@ from __future__ import annotations -import ast import json import os from collections.abc import Iterator -from dataclasses import replace from pathlib import Path import pytest @@ -29,48 +27,8 @@ "s2a.nokv_live_qualification", "s2b.postgresql_conformance_live", ) -PENDING_ONLY_ROW_ID = "s2c2.sustained_parity_soak" -PENDING_ROW_IDS = (PENDING_ONLY_ROW_ID,) LOCAL_FILE_ROW_ID = "s0.native_file_conformance" FULL_LADDER_VARIABLE = "LOOPX_LADDER_FULL" -# Rows whose assertions the in-repo CLI E2E suite already pins through the same -# product path. The pytest job runs close to its time budget, so the default CI -# projection keeps the rows that only the ladder exercises and defers these to -# the example runner (or LOOPX_LADDER_FULL=1). -# Rows the default CI projection skips, each named with the product-CLI E2E -# test that pins the same assertions; a guard below fails when a twin is -# renamed or deleted so the skip cannot outlive its coverage. -CLI_E2E_TWIN_FILE = Path(__file__).with_name("test_local_authority_shadow_cli_e2e.py") -CLI_E2E_COVERAGE = { - "s2c1.retired_observation_upgrade": "test_retired_setting_cannot_enable_capture_or_satisfy_bootstrap", -} -CLI_E2E_COVERED_ROW_IDS = tuple(CLI_E2E_COVERAGE) -CLI_E2E_COVERAGE_REASON = ( - "pinned by tests/control_plane/test_local_authority_shadow_cli_e2e.py; " - "run examples/shared-goal-authority-e2e/ladder.py or set LOOPX_LADDER_FULL=1" -) -# The Stage 2C parity half: every row below is executable through the public -# CLI and the shadow management interfaces. They carry the ``stage2c_e2e`` -# marker so CI runs them in the stage2c correctness job with the other real -# CLI, process-death and recovery suites instead of the pytest shards. -STAGE_2C2_ROW_IDS = ( - "s2c2.outbox_prepared_then_committed_entries", - "s2c2.drain_idempotent", - "s2c2.sigkill_between_primary_write_and_drain", - "s2c2.sigkill_mid_drain", - "s2c2.rollback_with_pending_entries", - "s2c2.parity_equal", - "s2c2.parity_divergent_detects_foreign_edit", - "s2c2.event_only_todo_source_holds", - "s2c2.migration_seeds_and_drains", - "s2c2.growth_measurement_gate", - "s2c2.archive_after_leased_completion_parity", -) -STAGE_2C2_POSIX_ONLY_ROW_IDS = ( - "s2c2.sigkill_between_primary_write_and_drain", - "s2c2.sigkill_mid_drain", - "s2c2.rollback_with_pending_entries", -) def _row_parameters() -> Iterator[object]: @@ -81,15 +39,11 @@ def _row_parameters() -> Iterator[object]: marks.append( pytest.mark.skipif(os.name == "nt", reason="requires POSIX cross-process flock and SIGKILL") ) - if row.id in CLI_E2E_COVERED_ROW_IDS and not full_ladder: - marks.append(pytest.mark.skip(reason=CLI_E2E_COVERAGE_REASON)) if row.stage == "0" and not full_ladder: marks.append(pytest.mark.skip(reason=( "native provider suites run in npm run test:control-plane; " "run the standalone ladder or set LOOPX_LADDER_FULL=1 for integrated evidence" ))) - if row.stage == "2c2": - marks.append(pytest.mark.stage2c_e2e) yield pytest.param(row, id=row.id, marks=marks) @@ -116,19 +70,11 @@ def test_ladder_row_passes_or_is_declared_unverified( def test_registry_vocabulary_and_pending_rows_are_declared_not_claimed() -> None: row_ids = [row.id for row in ladder.LADDER_ROWS] - assert set(CLI_E2E_COVERED_ROW_IDS) < set(row_ids) pending_ids = [row.id for row in ladder.PENDING_ROWS] assert len(set(row_ids)) == len(row_ids) assert set(row_ids).isdisjoint(pending_ids) - assert [row.id for row in ladder.LADDER_ROWS if row.stage == "2c2"] == list(STAGE_2C2_ROW_IDS) - assert pending_ids == list(PENDING_ROW_IDS) - assert {row.stage for row in ladder.LADDER_ROWS} == {"0", "1", "2a", "2b", "2c1", "2c2"} - assert {row.stage for row in ladder.PENDING_ROWS} == {"2c2"} - assert all("#3819" not in row.pending_until for row in ladder.PENDING_ROWS) - for row_id in STAGE_2C2_ROW_IDS: - row = ladder.row_by_id(row_id) - assert row.product_path == "real_cli" and row.gate == "deterministic" - assert row.posix_only is (row_id in STAGE_2C2_POSIX_ONLY_ROW_IDS) + assert pending_ids == [] + assert {row.stage for row in ladder.LADDER_ROWS} == {"0", "1", "2a", "2b"} assert all(row.gate in ladder.GATES for row in ladder.LADDER_ROWS) assert all(row.product_path in ladder.PRODUCT_PATHS for row in ladder.LADDER_ROWS) with pytest.raises(ValueError): @@ -238,9 +184,10 @@ def test_stage_2a_row_reports_specific_unverified_reasons_for_each_missing_input assert ladder.collect_bindings(inputs)["nokv_client_config_sha256"] is not None -@pytest.mark.stage2c_e2e -def test_stage_2c2_row_passes_when_the_ladder_root_is_reached_through_a_symlink(tmp_path: Path) -> None: - """macOS's default temp directory is a symlink; the shadow rows must not care (#4892).""" +def test_stage_1_row_passes_when_the_ladder_root_is_reached_through_a_symlink( + tmp_path: Path, +) -> None: + """macOS's default temp directory is a symlink; the CLI row must not care (#4892).""" real = tmp_path / "real-root" real.mkdir() @@ -249,7 +196,7 @@ def test_stage_2c2_row_passes_when_the_ladder_root_is_reached_through_a_symlink( link.symlink_to(real, target_is_directory=True) except OSError as exc: pytest.skip(f"directory symlinks unavailable: {exc}") - row = ladder.row_by_id("s2c2.outbox_prepared_then_committed_entries") + row = ladder.row_by_id("s1.cli_document_decodes_through_ts_store") result = ladder.run_row(row, root=link, environ=os.environ) if result.status == "unverified": pytest.skip(f"unverified: {result.reason_code}") @@ -273,21 +220,27 @@ def test_pending_rows_never_exit_green_without_allow_pending( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: + pending_row_id = "future.canonical_soak" + monkeypatch.setattr( + ladder, + "PENDING_ROWS", + (ladder.PendingRow(pending_row_id, "1", "a separately owned canonical soak"),), + ) for name in LIVE_ENVIRONMENT_VARIABLES: monkeypatch.delenv(name, raising=False) report_path = tmp_path / "report.json" # Pending-only selection: zero executions must not read as success. - assert ladder.main(["--row", PENDING_ONLY_ROW_ID, "--report-json", str(report_path)]) == 1 + assert ladder.main(["--row", pending_row_id, "--report-json", str(report_path)]) == 1 report = json.loads(report_path.read_text(encoding="utf-8")) assert report["summary"] == {"pass": 0, "fail": 0, "unverified": 0, "pending": 1, "executed": 0, "privacy_violations": 0} assert report["rows"] == [] - assert report["pending"][0]["id"] == PENDING_ONLY_ROW_ID + assert report["pending"][0]["id"] == pending_row_id assert report["pending"][0]["status"] == "pending" assert report["exit_policy"]["exit_code"] == 1 assert "pending rows (not verified):" in capsys.readouterr().err - assert ladder.main(["--row", PENDING_ONLY_ROW_ID, "--allow-pending", "--report-json", str(report_path)]) == 0 + assert ladder.main(["--row", pending_row_id, "--allow-pending", "--report-json", str(report_path)]) == 0 allowed = json.loads(report_path.read_text(encoding="utf-8")) assert allowed["exit_policy"] == { "allow_unverified": False, @@ -298,19 +251,33 @@ def test_pending_rows_never_exit_green_without_allow_pending( assert allowed["summary"]["pending"] == 1 capsys.readouterr() - # A stage whose only remaining declaration is pending behaves the same way; - # the executable 2c2 rows are removed from the registry for this check only. + # A stage whose only selected declaration is pending behaves the same way. with pytest.MonkeyPatch.context() as registry: - registry.setattr(ladder, "LADDER_ROWS", tuple(row for row in ladder.LADDER_ROWS if row.stage != "2c2")) - assert ladder.main(["--stage", "2c2", "--report-json", str(report_path)]) == 1 + registry.setattr(ladder, "LADDER_ROWS", tuple(row for row in ladder.LADDER_ROWS if row.stage != "1")) + assert ladder.main(["--stage", "1", "--report-json", str(report_path)]) == 1 capsys.readouterr() # Mixed selection: one executable pass does not excuse a pending obligation. - monkeypatch.setattr(ladder, "LADDER_ROWS", tuple( - replace(row, run=lambda _context: ladder.passed()) if row.id == LOCAL_FILE_ROW_ID else row - for row in ladder.LADDER_ROWS - )) - mixed = ["--row", LOCAL_FILE_ROW_ID, "--row", PENDING_ONLY_ROW_ID, "--report-json", str(report_path)] + local = ladder.row_by_id(LOCAL_FILE_ROW_ID) + monkeypatch.setattr( + ladder, + "LADDER_ROWS", + tuple( + ladder.LadderRow( + id=row.id, + stage=row.stage, + title=row.title, + product_path=row.product_path, + gate=row.gate, + posix_only=row.posix_only, + run=lambda _context: ladder.passed(), + ) + if row is local + else row + for row in ladder.LADDER_ROWS + ), + ) + mixed = ["--row", LOCAL_FILE_ROW_ID, "--row", pending_row_id, "--report-json", str(report_path)] assert ladder.main(mixed) == 1 report = json.loads(report_path.read_text(encoding="utf-8")) assert report["summary"] == {"pass": 1, "fail": 0, "unverified": 0, "pending": 1, "executed": 1, "privacy_violations": 0} @@ -319,11 +286,11 @@ def test_pending_rows_never_exit_green_without_allow_pending( capsys.readouterr() # List-only never claims verification: it prints the registry and exits 0. - assert ladder.main(["--list", "--row", PENDING_ONLY_ROW_ID]) == 0 + assert ladder.main(["--list", "--row", pending_row_id]) == 0 listing = json.loads(capsys.readouterr().out) assert listing["schema_version"] == ladder.LIST_SCHEMA assert listing["rows"] == [] - assert [row["id"] for row in listing["pending"]] == [PENDING_ONLY_ROW_ID] + assert [row["id"] for row in listing["pending"]] == [pending_row_id] assert "exit_policy" not in listing @@ -366,7 +333,7 @@ def test_privacy_scan_turns_leaks_into_failures(tmp_path: Path) -> None: "privacy_violations": 1, } assert report["exit_policy"]["exit_code"] == 1 - assert {row["status"] for row in report["pending"]} == {"pending"} + assert report["pending"] == [] def test_privacy_leak_confined_to_bindings_still_fails_the_run() -> None: @@ -403,14 +370,6 @@ def test_privacy_leak_confined_to_bindings_still_fails_the_run() -> None: assert ladder.exit_code_for(report["summary"], allow_unverified=True, allow_pending=True) == 1 -def test_ci_projection_skips_only_rows_whose_cli_e2e_twin_still_exists() -> None: - tree = ast.parse(CLI_E2E_TWIN_FILE.read_text(encoding="utf-8")) - defined = {node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)} - for row_id, twin in CLI_E2E_COVERAGE.items(): - ladder.row_by_id(row_id) - assert twin in defined, f"{row_id} is skipped by default but its twin {twin} no longer exists" - - def test_list_prints_rows_and_pending_declarations( capsys: pytest.CaptureFixture[str], ) -> None: @@ -420,11 +379,12 @@ def test_list_prints_rows_and_pending_declarations( assert [row["id"] for row in listing["pending"]] == [row.id for row in ladder.PENDING_ROWS] assert all(row["status"] == "pending" for row in listing["pending"]) - assert ladder.main(["--list", "--stage", "2c2"]) == 0 + assert ladder.main(["--list", "--stage", "1"]) == 0 stage_listing = json.loads(capsys.readouterr().out) - assert [row["id"] for row in stage_listing["rows"]] == list(STAGE_2C2_ROW_IDS) - assert [row["id"] for row in stage_listing["pending"]] == list(PENDING_ROW_IDS) - assert {row["stage"] for row in stage_listing["pending"]} == {"2c2"} + assert [row["id"] for row in stage_listing["rows"]] == [ + "s1.cli_document_decodes_through_ts_store" + ] + assert stage_listing["pending"] == [] @pytest.mark.parametrize("provider", ["file", "sqlite"]) diff --git a/tests/control_plane/test_split_root_todo_writeback_fence.py b/tests/control_plane/test_split_root_todo_writeback_fence.py index dd53e4546a..0e4f6e8e1c 100644 --- a/tests/control_plane/test_split_root_todo_writeback_fence.py +++ b/tests/control_plane/test_split_root_todo_writeback_fence.py @@ -1,7 +1,7 @@ -"""Split-root fence regressions for the monitor-poll and Turn writebacks. +"""Split-root authority regressions for the monitor-poll and Turn writebacks. -Each writeback uses its effective runtime root and also respects the authority -of the registered source file. An override cannot bypass either source fence. +Each writeback uses its effective runtime root. A write override must resolve +to canonical authority instead of mutating the registered Markdown projection. """ from __future__ import annotations @@ -14,17 +14,15 @@ import pytest +from canonical_authority_fixture import initialize_canonical_authority from loopx.cli import main from loopx.cli_commands.turn_todo_writeback import ( write_turn_repair_update, write_turn_validated_completion, ) -from loopx.control_plane.coordination.legacy_writer_fence import ( - LegacyCoordinationWriterFenced, - legacy_coordination_writer_fence_path, -) -from loopx.control_plane.coordination.local_authority import ( - LocalCoordinationAuthorityUnavailable, +from loopx.control_plane.coordination.local_authority import CanonicalAuthorityRequired +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, ) from loopx.control_plane.quota import monitor_poll from loopx.control_plane.scheduler.monitor_poll_writeback import ( @@ -36,7 +34,7 @@ ADVANCE_ID = "todo_splitroot_advance" MONITOR_ID = "todo_splitroot_monitor" OVERRIDE_POLL_HASH = "split-v2" -LEGACY_POLL_HASH = "split-v1" +INITIAL_POLL_HASH = "split-v1" REPOSITORY = Path(__file__).resolve().parents[2] @@ -58,7 +56,7 @@ def _write_split_root_goal(tmp_path: Path) -> tuple[Path, Path, Path, Path]: " \n", + f"result_hash={INITIAL_POLL_HASH} material_change=false -->\n", encoding="utf-8", ) runtime_registry = tmp_path / "registry-runtime" @@ -91,27 +89,47 @@ def _write_split_root_goal(tmp_path: Path) -> tuple[Path, Path, Path, Path]: return registry, state, runtime_registry, runtime_override -def _engage_fence_at(runtime_root: Path) -> None: - fence = legacy_coordination_writer_fence_path( - runtime_root=runtime_root, - goal_id=GOAL_ID, - ) - fence.parent.mkdir(parents=True, exist_ok=True) - fence.write_text( - json.dumps({"state": "present", "engaged_by": "promotion"}), - encoding="utf-8", - ) - - -def _fence_check_blocks(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr( - "loopx.control_plane.coordination.legacy_writer_fence." - "effect_runtime_result", - lambda *_args, **_kwargs: { - "status": "blocked", - "reason_code": "legacy_coordination_writer_fenced", - "authority_mode": "file_v0", - }, +def _initialize_canonical_override(runtime_root: Path, state: Path) -> None: + initialize_canonical_authority( + runtime_root, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + handoff_mode="soft_claim", + todos=[ + { + "schema_version": "todo_item_v0", + "todo_id": ADVANCE_ID, + "role": "agent", + "status": "open", + "done": False, + "text": "Advance the fenced writeback slice.", + "archive_state": "active", + "source_section": "Agent Todo", + "index": 1, + "task_class": "advancement_task", + "claimed_by": AGENT_ID, + }, + { + "schema_version": "todo_item_v0", + "todo_id": MONITOR_ID, + "role": "agent", + "status": "open", + "done": False, + "text": "Watch the fenced writeback channel.", + "archive_state": "active", + "source_section": "Agent Todo", + "index": 2, + "task_class": "continuous_monitor", + "claimed_by": AGENT_ID, + "target_key": "splitroot-review", + "cadence": "30m", + "result_hash": INITIAL_POLL_HASH, + "material_change": False, + }, + ], + ), + state_path=state, ) @@ -136,51 +154,26 @@ def _poll_kwargs( def test_monitor_poll_writeback_rejects_unavailable_canonical_override( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: registry, state, _runtime_registry, runtime_override = ( _write_split_root_goal(tmp_path) ) - _engage_fence_at(runtime_override) - _fence_check_blocks(monkeypatch) state_before = state.read_text(encoding="utf-8") - with pytest.raises(LocalCoordinationAuthorityUnavailable): + with pytest.raises(CanonicalAuthorityRequired) as error: write_monitor_poll_todo_state(**_poll_kwargs(registry, runtime_override)) - assert LEGACY_POLL_HASH in state.read_text(encoding="utf-8") + assert error.value.code == "canonical_authority_required" + assert error.value.payload["operation"] == "monitor_poll" + assert INITIAL_POLL_HASH in state.read_text(encoding="utf-8") assert OVERRIDE_POLL_HASH not in state.read_text(encoding="utf-8") assert state.read_text(encoding="utf-8") == state_before -def test_monitor_poll_writeback_blocks_when_the_registry_source_is_fenced( - tmp_path: Path, -) -> None: - """An override shares the source state, so its original fence still applies.""" - - from loopx.control_plane.effect_runtime import effect_runtime_result - - registry, state, runtime_registry, runtime_override = ( - _write_split_root_goal(tmp_path) - ) - before = state.read_bytes() - engaged = effect_runtime_result("coordination.local_authority.legacy_writer_fence.engage", { - "schema_version": "loopx_legacy_coordination_writer_fence_engage_request_v0", - "runtime_root": str(runtime_registry), "goal_id": GOAL_ID, "state_path": str(state), - "fence": {"schema_version": "loopx_legacy_coordination_writer_fence_v0", "state": "engaged", - "goal_id": GOAL_ID, "fence_id": "original-source", "source_version": "source:1", - "source_projection_sha256": "a" * 64, "expected_shadow_provider_revision": "file:1:aaaaaaaaaaaaaaaaaaaaaaaa"}, - }) - assert engaged["status"] == "applied", engaged - with pytest.raises(LegacyCoordinationWriterFenced) as error: - write_monitor_poll_todo_state(**_poll_kwargs(registry, runtime_override)) - assert error.value.code == "legacy_coordination_writer_fenced" - assert state.read_bytes() == before - - -def test_monitor_poll_writeback_allows_an_unfenced_runtime_override(tmp_path: Path) -> None: +def test_monitor_poll_writeback_uses_canonical_runtime_override(tmp_path: Path) -> None: registry, state, _runtime_registry, runtime_override = _write_split_root_goal(tmp_path) + _initialize_canonical_override(runtime_override, state) receipt = write_monitor_poll_todo_state( **_poll_kwargs(registry, runtime_override) @@ -193,7 +186,7 @@ def test_monitor_poll_writeback_allows_an_unfenced_runtime_override(tmp_path: Pa assert f"result_hash={OVERRIDE_POLL_HASH}" in active -def test_quota_monitor_poll_provider_writeback_blocked_under_override_fence( +def test_quota_monitor_poll_provider_writeback_requires_canonical_override( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: @@ -202,8 +195,6 @@ def test_quota_monitor_poll_provider_writeback_blocked_under_override_fence( registry, state, _runtime_registry, runtime_override = ( _write_split_root_goal(tmp_path) ) - _engage_fence_at(runtime_override) - _fence_check_blocks(monkeypatch) def native(_method: str, request: dict[str, Any]) -> dict[str, Any]: assert request["phase"] == "preflight" @@ -229,7 +220,7 @@ def native(_method: str, request: dict[str, Any]) -> dict[str, Any]: "agent_identity": {"agent_id": AGENT_ID}, } - with pytest.raises(LocalCoordinationAuthorityUnavailable): + with pytest.raises(CanonicalAuthorityRequired) as error: monitor_poll.record_quota_monitor_poll_for_decision( before, {"runtime_root": str(runtime_override)}, @@ -243,6 +234,7 @@ def native(_method: str, request: dict[str, Any]) -> dict[str, Any]: agent_id=AGENT_ID, ) + assert error.value.code == "canonical_authority_required" assert OVERRIDE_POLL_HASH not in state.read_text(encoding="utf-8") @@ -254,12 +246,10 @@ def test_quota_monitor_poll_cli_rejects_unavailable_canonical_before_collection( registry, state, _runtime_registry, runtime_override = ( _write_split_root_goal(tmp_path) ) - _engage_fence_at(runtime_override) - _fence_check_blocks(monkeypatch) before = state.read_bytes() def unexpected_collection(**_kwargs: Any) -> dict[str, Any]: - pytest.fail("fenced monitor write must be rejected before status collection") + pytest.fail("unmigrated monitor write must be rejected before status collection") monkeypatch.setattr("loopx.cli_commands.quota.collect_status", unexpected_collection) @@ -291,22 +281,20 @@ def unexpected_collection(**_kwargs: Any) -> dict[str, Any]: assert exit_code == 1 payload = json.loads(capsys.readouterr().out) - assert payload["error_code"] == "local_authority_todo_list_unavailable" - assert payload["source_authority"] == "file_v0" + assert payload["error_code"] == "canonical_authority_required" + assert payload["write_admitted"] is False assert payload["legacy_fallback_used"] is False assert state.read_bytes() == before @pytest.mark.parametrize("command", ["should-run", "monitor-poll"]) -def test_read_only_quota_still_collects_promoted_state( - command: str, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, +def test_read_only_quota_still_collects_unmigrated_state( + command: str, tmp_path: Path, ) -> None: from loopx.cli import build_parser from loopx.cli_commands.quota_context import prepare_quota_command_context registry, _state, _registered_root, runtime_override = _write_split_root_goal(tmp_path) - _engage_fence_at(runtime_override) - _fence_check_blocks(monkeypatch) args = build_parser().parse_args([ "quota", command, "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, *(["--todo-id", MONITOR_ID] if command == "monitor-poll" else []), @@ -326,18 +314,15 @@ def collector(**kwargs: Any) -> dict[str, object]: assert context.status_payload["runtime_root"] == str(runtime_override) -def test_turn_repair_update_blocked_when_override_root_is_fenced( - monkeypatch: pytest.MonkeyPatch, +def test_turn_repair_update_blocked_without_canonical_override( tmp_path: Path, ) -> None: registry, state, runtime_registry, runtime_override = ( _write_split_root_goal(tmp_path) ) - _engage_fence_at(runtime_override) - _fence_check_blocks(monkeypatch) before = state.read_bytes() - with pytest.raises(LocalCoordinationAuthorityUnavailable) as error: + with pytest.raises(CanonicalAuthorityRequired) as error: write_turn_repair_update( registry_path=registry, runtime_root_arg=str(runtime_override), @@ -348,40 +333,29 @@ def test_turn_repair_update_blocked_when_override_root_is_fenced( agent_id=AGENT_ID, ) - assert error.value.code == "local_authority_todo_list_unavailable" - assert str(error.value) == "canonical Todo authority is unavailable" - assert error.value.payload == { - "schema_version": "loopx_coordination_todo_update_result_v0", - "status": "missing", - "changed": False, - "source_authority": "file_v0", - "decision_read_from_provider": True, - "legacy_fallback_used": False, - "recovery": { - "action": "restore_canonical_authority", - "runtime_root": str(runtime_override.resolve()), - "goal_id": GOAL_ID, - "legacy_markdown_fallback_allowed": False, - "retry_after": "canonical_provider_readback_loaded", - }, + assert error.value.code == "canonical_authority_required" + assert error.value.payload["operation"] == "todo_update" + assert error.value.payload["write_admitted"] is False + assert error.value.payload["decision_read_from_provider"] is False + assert error.value.payload["legacy_fallback_used"] is False + assert error.value.payload["recovery"] == { + "action": "migrate_goal_authority", + "backup_required": True, } assert state.read_bytes() == before assert not (runtime_override / "authority").exists() assert not (runtime_registry / "authority").exists() -def test_turn_validated_completion_blocked_when_override_root_is_fenced( - monkeypatch: pytest.MonkeyPatch, +def test_turn_validated_completion_blocked_without_canonical_override( tmp_path: Path, ) -> None: registry, state, runtime_registry, runtime_override = ( _write_split_root_goal(tmp_path) ) - _engage_fence_at(runtime_override) - _fence_check_blocks(monkeypatch) before = state.read_bytes() - with pytest.raises(LocalCoordinationAuthorityUnavailable) as error: + with pytest.raises(CanonicalAuthorityRequired) as error: write_turn_validated_completion( registry_path=registry, runtime_root_arg=str(runtime_override), @@ -393,20 +367,14 @@ def test_turn_validated_completion_blocked_when_override_root_is_fenced( agent_id=AGENT_ID, ) - assert error.value.code == "local_authority_todo_list_unavailable" - assert error.value.payload == { - "schema_version": "loopx_local_coordination_todo_list_result_v0", - "status": "missing", - "source_authority": "file_v0", - "decision_read_from_provider": True, - "legacy_fallback_used": False, - "recovery": { - "action": "restore_canonical_authority", - "runtime_root": str(runtime_override.resolve()), - "goal_id": GOAL_ID, - "legacy_markdown_fallback_allowed": False, - "retry_after": "canonical_provider_readback_loaded", - }, + assert error.value.code == "canonical_authority_required" + assert error.value.payload["operation"] == "todo_terminal" + assert error.value.payload["write_admitted"] is False + assert error.value.payload["decision_read_from_provider"] is False + assert error.value.payload["legacy_fallback_used"] is False + assert error.value.payload["recovery"] == { + "action": "migrate_goal_authority", + "backup_required": True, } assert state.read_bytes() == before assert not (runtime_override / "authority").exists() @@ -448,10 +416,9 @@ def test_turn_validated_completion_blocked_when_override_root_is_fenced( ) assert process.returncode == 1, process.stdout + process.stderr payload = json.loads(process.stdout) - assert payload["error_code"] == "local_authority_todo_list_unavailable" - assert payload["status"] == "missing" - assert payload["source_authority"] == "file_v0" - assert payload["decision_read_from_provider"] is True + assert payload["error_code"] == "canonical_authority_required" + assert payload["write_admitted"] is False + assert payload["decision_read_from_provider"] is False assert payload["legacy_fallback_used"] is False assert payload["recovery"] == error.value.payload["recovery"] assert state.read_bytes() == before diff --git a/tests/control_plane/test_sqlite_authority_cli.py b/tests/control_plane/test_sqlite_authority_cli.py index 52205a065e..dd5ea9774b 100644 --- a/tests/control_plane/test_sqlite_authority_cli.py +++ b/tests/control_plane/test_sqlite_authority_cli.py @@ -143,8 +143,10 @@ def durable_bytes(): assert not list((runtime / "authority-transition").rglob("legacy-writer-fence-*.json")) -@pytest.mark.parametrize("old_producers_present", [True, False]) -def test_new_default_goal_settles_once_and_returns_current_writes_to_file(tmp_path, monkeypatch, old_producers_present): +@pytest.mark.parametrize("bridge_modules_present", [True, False]) +def test_new_default_goal_settles_once_and_returns_current_writes_to_file( + tmp_path, monkeypatch, bridge_modules_present, +): """Join the shipped creation, ordinary work and reversible-exit owners.""" from loopx.control_plane.effect_runtime import restart_effect_runtime @@ -156,9 +158,12 @@ def test_new_default_goal_settles_once_and_returns_current_writes_to_file(tmp_pa package = Path(os.environ.get("LOOPX_COLD_IMPORT_PACKAGE", str(Path(__file__).resolve().parents[2] / "loopx"))) receiver = tmp_path / "receiver" shutil.copytree(package, receiver / "loopx", ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) - if not old_producers_present: - for name in ("runtime_shadow.py", "runtime_shadow_writer_adapter.py", - "local_authority_shadow_adapter.py", "local_authority_shadow_outbox.py"): + assert not ( + receiver / "loopx/control_plane/coordination/runtime_shadow_writer_adapter.py" + ).exists() + if not bridge_modules_present: + for name in ("runtime_shadow.py", "local_authority_shadow_adapter.py", + "local_authority_shadow_outbox.py"): (receiver / "loopx/control_plane/coordination" / name).unlink() # Isolated interpreter, real full entrypoint, and verified copied-package # provenance on every command; no fallback to the test runner's checkout. diff --git a/tests/control_plane/test_superseded_turn_settlement_cli.py b/tests/control_plane/test_superseded_turn_settlement_cli.py index a86a66b682..1629d28929 100644 --- a/tests/control_plane/test_superseded_turn_settlement_cli.py +++ b/tests/control_plane/test_superseded_turn_settlement_cli.py @@ -5,30 +5,9 @@ import pytest import test_declared_terminal_settlement_cli as declared -import test_goal_handoff_mode as legacy import test_quota_settlement_cli as cli from loopx.control_plane.quota.settlement import read_heartbeat_settlement -from loopx.todos import supersede_goal_todo - - -@pytest.mark.parametrize("successors", [None, [], ["todo_existing_successor"]]) -def test_unpromoted_supersede_preserves_omitted_links_and_rejects_existing_links( - tmp_path: Path, successors: list[str] | None, -) -> None: - registry, state = legacy._write_workspace(tmp_path) - todo = legacy._add_todo(registry, claimed_by=legacy.AGENT_A) - before = state.read_bytes() - arguments = dict(registry_path=registry, goal_id=legacy.GOAL_ID, - todo_id=todo["todo_id"], agent_id=legacy.AGENT_A, - reason="Retire the original approach.", successor_todo_ids=successors) - if successors: - with pytest.raises(ValueError, match="requires promoted canonical Todo authority"): - supersede_goal_todo(**arguments) - assert state.read_bytes() == before - else: - result = supersede_goal_todo(**arguments) - assert result["ok"] is True and result["superseded"] is True def _future_monitor(run, lease, *, link=True): diff --git a/tests/control_plane/test_task_lease.py b/tests/control_plane/test_task_lease.py index cef9f73541..4125e8aa87 100644 --- a/tests/control_plane/test_task_lease.py +++ b/tests/control_plane/test_task_lease.py @@ -1,7 +1,5 @@ from __future__ import annotations -import json -import os from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any @@ -19,10 +17,8 @@ release_task_lease, renew_task_lease, task_lease_owner_constraint, - transfer_task_lease, write_scopes_overlap, ) -from loopx.todos import add_goal_todo @pytest.mark.parametrize("raw", [None, b'{"status":"active"}', b"{broken", b"\xff"], @@ -173,170 +169,6 @@ def test_bool_disguised_lease_integers_fail_closed() -> None: assert error.value.code == "corrupt_lease" -def test_retained_task_lease_lifecycle_preserves_versions( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - now = datetime(2026, 7, 13, tzinfo=timezone.utc) - monkeypatch.setattr(task_lease, "now_utc", lambda: now) - repo = tmp_path / "repo" - repo.mkdir() - state = repo / "ACTIVE_GOAL_STATE.md" - state.write_text( - "---\n" - "goal_id: goal-a\n" - "updated_at: 2026-07-13T00:00:00+00:00\n" - "---\n\n" - "## Agent Todo\n\n", - encoding="utf-8", - ) - registry_path = tmp_path / "registry.json" - runtime_root = tmp_path / "runtime" - registry_path.write_text( - json.dumps( - { - "common_runtime_root": str(runtime_root), - "goals": [ - { - "id": "goal-a", - "domain": "harness_self_improvement", - "status": "active", - "repo": str(repo), - "state_file": state.name, - "adapter": {"kind": "harness_self_improvement"}, - "coordination": { - "agent_model": "peer_v1", - "registered_agents": ["agent-a", "agent-b"], - }, - } - ], - } - ), - encoding="utf-8", - ) - todo = add_goal_todo( - registry_path=registry_path, - goal_id="goal-a", - role="agent", - text="Exercise the retained task-lease lifecycle.", - task_class="advancement_task", - ) - todo_id = str(todo["todo_id"]) - lease_path = task_lease.task_lease_path( - runtime_root=runtime_root, - goal_id="goal-a", - todo_id=todo_id, - ) - task_lease.write_lease( - lease_path, - task_lease.build_lease( - goal_id="goal-a", - todo_id=todo_id, - owner="agent-a", - idempotency_key="turn-1", - write_scopes=["loopx/**"], - acquire_ttl_seconds=120, - version=1, - lease_epoch=1, - acquired_at=now.isoformat().replace("+00:00", "Z"), - updated_at=now.isoformat().replace("+00:00", "Z"), - expires_at=(now + timedelta(seconds=120)) - .isoformat() - .replace("+00:00", "Z"), - ), - ) - - with pytest.raises(TaskLeaseError) as renew_without_version: - renew_task_lease( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id="goal-a", - todo_id=todo_id, - owner="agent-a", - idempotency_key="turn-1", - ) - assert renew_without_version.value.code == "version_required" - - with pytest.raises(TaskLeaseError) as transfer_without_version: - transfer_task_lease( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id="goal-a", - todo_id=todo_id, - owner="agent-a", - idempotency_key="turn-1", - new_owner="agent-b", - new_idempotency_key="turn-2", - ) - assert transfer_without_version.value.code == "version_required" - - renewed = renew_task_lease( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id="goal-a", - todo_id=todo_id, - owner="agent-a", - idempotency_key="turn-1", - expected_version=1, - ) - assert renewed["lease"]["version"] == 2 - assert renewed["lease"]["lease_epoch"] == 1 - - transferred = transfer_task_lease( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id="goal-a", - todo_id=todo_id, - owner="agent-a", - idempotency_key="turn-1", - new_owner="agent-b", - new_idempotency_key="turn-2", - expected_version=2, - ) - assert transferred["lease"]["owner"] == "agent-b" - assert transferred["lease"]["version"] == 3 - assert transferred["lease"]["lease_epoch"] == 2 - - released = release_task_lease( - runtime_root=runtime_root, - goal_id="goal-a", - todo_id=todo_id, - owner="agent-b", - idempotency_key="turn-2", - expected_version=3, - ) - assert released["released"] is True - assert released["lease"]["status"] == "released" - assert released["lease"]["released_at"] == now.isoformat().replace("+00:00", "Z") - assert released["lease"]["updated_at"] == released["lease"]["released_at"] - assert task_lease.lease_is_active(released["lease"], at=now) is False - persisted_path = Path(str(released["lease_path"])) - assert persisted_path.exists() - assert task_lease.read_lease(persisted_path) == released["lease"] - - replayed_release = release_task_lease( - runtime_root=runtime_root, - goal_id="goal-a", - todo_id=todo_id, - owner="agent-b", - idempotency_key="turn-2", - expected_version=3, - ) - assert replayed_release["released"] is True - assert replayed_release["idempotent"] is True - assert replayed_release["lease"] == released["lease"] - - with pytest.raises(TaskLeaseError) as missing_version: - release_task_lease( - runtime_root=runtime_root, - goal_id="goal-a", - todo_id=todo_id, - owner="agent-b", - idempotency_key="turn-2", - ) - assert missing_version.value.code == "version_required" - - def test_lifecycle_mutation_fails_closed_without_registry( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -380,7 +212,7 @@ def test_lifecycle_mutation_fails_closed_without_registry( expected_version=1, ) - assert error.value.code == "todo_not_found" + assert error.value.code == "goal_not_found" assert task_lease.read_lease(lease_path) == before @@ -427,167 +259,6 @@ def test_legacy_generation_and_unrelated_ttl_do_not_block_release( assert released["lease"]["lease_epoch"] == 1 -def test_holder_verify_transport_retry_reuses_one_fence_operation_id( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry_path = tmp_path / "registry.json" - registry_path.write_text("{}", encoding="utf-8") - authority = { - "handoff_mode": "hard_lease", - "registered_agent_candidates": [["agent-a"]], - "todos": [ - { - "todo_id": "todo_retry", - "status": "open", - "claimed_by": "agent-a", - "excluded_agents": [], - } - ], - "todo_projection_error": None, - "source_receipts": [], - } - monkeypatch.setattr( - task_lease_acquire_adapter, - "task_lease_acquire_authority_facts", - lambda **_kwargs: authority, - ) - seen_operation_ids: list[str] = [] - - def retrying_effect_runtime_result( - _method: str, - params: dict[str, Any], - *, - timeout: float, - ) -> dict[str, Any]: - assert timeout == 15.0 - operation_id = params.get("fence_operation_id") - assert isinstance(operation_id, str) - seen_operation_ids.append(operation_id) - if len(seen_operation_ids) == 1: - return { - "ok": False, - "schema_version": "task_lease_v0", - "action": "holder_verify", - "error": "authority changed while the response was in flight", - "error_code": "authority_source_changed", - } - return { - "ok": True, - "schema_version": "task_lease_v0", - "action": "holder_verify", - "fence": { - "schema_version": "task_lease_v0", - "checked": True, - "active": True, - "owner": "agent-a", - "version": 1, - "lease_epoch": 1, - "lock_token": "held-token", - "fence_operation_id": operation_id, - }, - } - - import loopx.control_plane.effect_runtime as effect_runtime - - monkeypatch.setattr( - effect_runtime, - "effect_runtime_result", - retrying_effect_runtime_result, - ) - - first = task_lease_acquire_adapter.execute_native_task_lease_lifecycle( - runtime_root=tmp_path / "runtime", - registry_path=registry_path, - goal_id="goal-a", - todo_id="todo_retry", - operation="holder_verify", - owner="agent-a", - ) - second = task_lease_acquire_adapter.execute_native_task_lease_lifecycle( - runtime_root=tmp_path / "runtime", - registry_path=registry_path, - goal_id="goal-a", - todo_id="todo_retry", - operation="holder_verify", - owner="agent-a", - ) - - assert len(seen_operation_ids) == 3 - assert seen_operation_ids[0] == seen_operation_ids[1] - assert seen_operation_ids[0] != seen_operation_ids[2] - assert first["fence"]["fence_operation_id"] == seen_operation_ids[0] - assert second["fence"]["fence_operation_id"] == seen_operation_ids[2] - - -def test_failed_fence_close_abandons_its_owned_cross_runtime_lock( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - runtime_root = tmp_path / "runtime" - token = "held-fence-token" - lock_target = task_lease.task_lease_lock_path( - runtime_root=runtime_root, - goal_id="goal-a", - ) - effect_lock = Path(f"{lock_target}.ts-effect.lock") - close_calls = 0 - - monkeypatch.setattr( - task_lease, - "runtime_root_from_registry", - lambda _registry_path, _override: runtime_root, - ) - - def failing_lifecycle(**kwargs: Any) -> dict[str, Any]: - nonlocal close_calls - if kwargs["operation"] == "terminal_verify": - effect_lock.parent.mkdir(parents=True, exist_ok=True) - effect_lock.write_text( - json.dumps({"pid": os.getpid(), "token": token}), - encoding="utf-8", - ) - return { - "ok": True, - "schema_version": "task_lease_v0", - "action": "terminal_verify", - "fence": { - "schema_version": "task_lease_v0", - "required": True, - "active": True, - "owner": "agent-a", - "version": 1, - "lease_epoch": 1, - "execution_instance_verified": True, - "lock_token": token, - "fence_operation_id": "a" * 64, - }, - } - close_calls += 1 - raise RuntimeError("simulated managed-runtime transport loss") - - monkeypatch.setattr( - task_lease, - "_execute_native_task_lease_lifecycle", - failing_lifecycle, - ) - - with task_lease.hold_task_lease_mutation_fence( - registry_path=tmp_path / "registry.json", - goal_id="goal-a", - todo_id="todo_target", - todo={"todo_id": "todo_target", "status": "open"}, - actor_agent_id="agent-a", - idempotency_key="lease-a", - expected_version=1, - ) as fence: - task_lease.release_verified_task_lease_fence(fence, committed=True) - assert fence["released"] is False - - assert close_calls == 1 - assert not effect_lock.exists() - - @pytest.mark.parametrize( ("expected_version", "expected_forwarded"), [("2", 2), (2.5, 2)], diff --git a/tests/control_plane/test_task_lease_argument_recovery.py b/tests/control_plane/test_task_lease_argument_recovery.py index b1912a13f3..71594020e6 100644 --- a/tests/control_plane/test_task_lease_argument_recovery.py +++ b/tests/control_plane/test_task_lease_argument_recovery.py @@ -15,7 +15,7 @@ GOAL, TODO = "lease-recovery", "todo_lease_recovery" -@pytest.fixture(params=["legacy", "file", "sqlite"]) +@pytest.fixture(params=["file", "sqlite"]) def lease_cli(tmp_path, monkeypatch, request): isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime = tmp_path / "project with spaces", tmp_path / "runtime" @@ -27,15 +27,14 @@ def lease_cli(tmp_path, monkeypatch, request): "id": GOAL, "repo": str(project), "state_file": state.name, "coordination": {"registered_agents": ["peer-a", "peer-b"]}, }]})) - if request.param != "legacy": - initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( - goal_id=GOAL, handoff_mode="hard_lease", leases=[], todos=[{ - "schema_version": "todo_item_v0", "todo_id": TODO, "role": "agent", - "status": "open", "done": False, "text": "Verify a source artifact.", - "archive_state": "active", "source_section": "Agent Todo", "index": 1, - "task_class": "advancement_task", "claimed_by": "peer-a", "required_write_scopes": ["src/**"], - }]), state_path=state, provider=request.param) - state.unlink() + initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( + goal_id=GOAL, handoff_mode="hard_lease", leases=[], todos=[{ + "schema_version": "todo_item_v0", "todo_id": TODO, "role": "agent", + "status": "open", "done": False, "text": "Verify a source artifact.", + "archive_state": "active", "source_section": "Agent Todo", "index": 1, + "task_class": "advancement_task", "claimed_by": "peer-a", "required_write_scopes": ["src/**"], + }]), state_path=state, provider=request.param) + state.unlink() prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] def run(args, code=0): @@ -149,7 +148,7 @@ def test_recovery_preserves_routing_and_repeatable_string_values(): assert recovered.new_idempotency_key is None -def test_transfer_repair_retains_explicit_claim_handover(lease_cli, request): +def test_transfer_repair_retains_explicit_claim_handover(lease_cli): run, action, _, _ = lease_cli before = run(action("inspect"))["lease"] rejected = run(action("transfer", "--owner", "peer-a", "--idempotency-key", "original", @@ -161,19 +160,13 @@ def test_transfer_repair_retains_explicit_claim_handover(lease_cli, request): assert "--transfer-claim" in repaired assert repaired[repaired.index("--expected-version") + 1] == "1" assert run(action("inspect"))["lease"] == before - if request.node.callspec.params["lease_cli"] == "legacy": - # Syntax repair cannot make atomic claim transfer available on legacy files. - denied = run(repaired, code=1) - assert denied["ok"] is False and "recovery" not in denied - assert run(action("inspect"))["lease"] == before - else: - transferred = run(repaired) - assert transferred["lease"]["owner"] == transferred["claimed_by"] == "peer-b" - assert transferred["lease"]["version"] == 2 - assert transferred["lease"]["write_scopes"] == ["src/**"] - released = run(action("release", "--owner", "peer-b", "--idempotency-key", "receiver", - "--expected-version", "2")) - assert released["lease"]["status"] == "released" + transferred = run(repaired) + assert transferred["lease"]["owner"] == transferred["claimed_by"] == "peer-b" + assert transferred["lease"]["version"] == 2 + assert transferred["lease"]["write_scopes"] == ["src/**"] + released = run(action("release", "--owner", "peer-b", "--idempotency-key", "receiver", + "--expected-version", "2")) + assert released["lease"]["status"] == "released" def test_grammar_rejection_precedes_runtime_resolution(monkeypatch): diff --git a/tests/control_plane/test_team_plan_authority.py b/tests/control_plane/test_team_plan_authority.py index 7b55e5a1da..b5dee8db29 100644 --- a/tests/control_plane/test_team_plan_authority.py +++ b/tests/control_plane/test_team_plan_authority.py @@ -1,4 +1,5 @@ """Actual Chat entrypoint against a fenced FileAuthorityStore and projection IO.""" +import base64 import hashlib import json import os @@ -20,7 +21,10 @@ from loopx.control_plane.coordination.coordination_state_contract import ( TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, TODO_DOMAIN_RECORD_FIELDS, ) -from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted +from loopx.control_plane.coordination.local_authority import ( + CanonicalAuthorityRequired, + read_canonical_todos_if_promoted, +) from loopx.control_plane.coordination.local_authority_shadow_projection import canonical_bytes from loopx.control_plane.todos import provider_projection @@ -51,6 +55,63 @@ def canonical_team(tmp_path, request, monkeypatch): return runtime, state, service, preview +def test_unpromoted_team_plan_preview_requires_canonical_authority(tmp_path): + runtime = tmp_path / "runtime" + state = tmp_path / "state.md" + state.write_text("# Goal\n\n## Agent Todo\n\n") + registry = tmp_path / "registry.json" + registry.write_text(json.dumps({ + "common_runtime_root": str(runtime), + "goals": [{ + "id": "goal-a", + "repo": str(tmp_path), + "state_file": state.name, + "coordination": { + "registered_agents": ["alpha"], + "agent_model": "peer_v1", + }, + }], + })) + service = ChatActionService( + store=ChatActionStore(runtime / "chat/actions"), + registry_path=registry, + ) + before = { + path: path.read_bytes() + for root in (tmp_path, runtime) + for path in root.rglob("*") + if path.is_file() + } + + with pytest.raises(CanonicalAuthorityRequired) as error: + service.preview({ + "action_kind": "team.plan", + "summary": "Assign work", + "context": {}, + "normalized_parameters": { + "goal_id": "goal-a", + "plan": _agent_plan(), + }, + "idempotency_key": "unpromoted-team-plan", + }) + + assert error.value.code == "canonical_authority_required" + assert error.value.payload["operation"] == "team_plan" + assert error.value.payload["write_admitted"] is False + assert error.value.payload["decision_read_from_provider"] is False + assert error.value.payload["legacy_fallback_used"] is False + assert error.value.payload["recovery"] == { + "action": "migrate_goal_authority", + "backup_required": True, + } + assert { + path: path.read_bytes() + for root in (tmp_path, runtime) + for path in root.rglob("*") + if path.is_file() + } == before + + @pytest.mark.parametrize("source", ["state", "registry"]) def test_reviewed_sources_remain_locked_through_native_commit(canonical_team, monkeypatch, source): """A compliant writer cannot replace reviewed facts after fingerprinting.""" @@ -334,7 +395,9 @@ def _capture_free_chat(team, tmp_path): package = package_root / "loopx" shutil.copytree(Path(loopx.__file__).parent, package, ignore=shutil.ignore_patterns("__pycache__")) - (package / "control_plane/coordination/runtime_shadow_writer_adapter.py").unlink() + assert not ( + package / "control_plane/coordination/runtime_shadow_writer_adapter.py" + ).exists() script = r''' import json, sys from pathlib import Path @@ -447,7 +510,7 @@ def test_capture_free_chat_recovers_committed_projection(canonical_team, capture @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_capture_free_chat_preserves_pre_promotion_receipt(tmp_path, monkeypatch, provider): """Imported history remains readable without recreating or overwriting work.""" - from loopx.todos import list_goal_todos + from loopx.control_plane.effect_runtime import effect_runtime_result from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection isolate_sqlite_runtime(tmp_path, monkeypatch) @@ -460,21 +523,47 @@ def test_capture_free_chat_preserves_pre_promotion_receipt(tmp_path, monkeypatch "coordination": {"registered_agents": ["alpha"], "agent_model": "peer_v1"}}]})) service = ChatActionService(store=ChatActionStore(runtime / "chat/actions"), registry_path=registry) plan = _agent_plan() - preview = service.preview({"action_kind": "team.plan", "summary": "Assign work", "context": {}, - "normalized_parameters": {"goal_id": "goal-a", "plan": plan}, "idempotency_key": "legacy-plan"}) - applied = service.apply(preview["proposal_id"])["proposal"] - assert applied["status"] == "applied", applied - assert "loopx:team-plan:" in state.read_text() - rows = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] - projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=rows, handoff_mode="soft_claim") + historical_basis = "historical-team-plan-basis" + preview = service.store.create_preview( + action_kind="team.plan", summary="Assign work", + normalized_parameters={"goal_id": "goal-a", "plan": plan}, context={}, + expected_state_fingerprint=historical_basis, + permission_classification="owner_confirmation_required", + validation_evidence=[], available_transitions=["apply"], + idempotency_key="legacy-plan", + ) + planned = effect_runtime_result("work_items.team_plan.plan", { + "goal_id": "goal-a", + "plan": {**plan, "proposal_id": preview["proposal_id"]}, + "actor_agent_id": None, + "registered_agents": ["alpha"], + "supported_action_kinds": ["implement"], + "observed_at": "2026-09-01T00:00:00Z", + "expected_state_fingerprint": historical_basis, + "current_state_fingerprint": historical_basis, + "todos": [], + }) + receipt = planned["receipt"] + marker_id = receipt["operation_id"].split(":", 1)[1] + encoded_receipt = base64.b64encode( + json.dumps(receipt, separators=(",", ":")).encode() + ).decode() + state.write_text( + f"# Goal\n\n\n\n" + "## Agent Todo\n\n" + ) + projection = build_todo_runtime_shadow_projection( + goal_id="goal-a", todos=planned["todos"], handoff_mode="soft_claim", + ) initialize_canonical_authority(runtime, "goal-a", projection, state_path=state, provider=provider) invoke = _capture_free_chat((runtime, state, service, preview), tmp_path) later = invoke("preview", text="New work after import") - assert invoke("apply", proposal=later["proposal_id"])["proposal"]["status"] == "applied" + later_apply = invoke("apply", proposal=later["proposal_id"]) + assert later_apply["proposal"]["status"] == "applied", later_apply["proposal"].get("failure") current = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") display = state.read_bytes() replay = invoke("historical") assert replay["action"] == "reused" - assert replay["lane_todo_ids"] == applied["receipt"]["resource_ids"]["lane_todo_ids"] + assert replay["lane_todo_ids"] == planned["result"]["lane_todo_ids"] assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == current assert state.read_bytes() == display diff --git a/tests/control_plane/test_terminal_settlement_tool_behavior.py b/tests/control_plane/test_terminal_settlement_tool_behavior.py index 6a2cd60d4d..e3dd8c423e 100644 --- a/tests/control_plane/test_terminal_settlement_tool_behavior.py +++ b/tests/control_plane/test_terminal_settlement_tool_behavior.py @@ -96,10 +96,7 @@ def action(request: Mapping[str, Any]) -> ScriptedExecToolAction: if "loopx todo complete" in item ] command = commands[index] - command = command[command.index("loopx ") :].replace( - "", - f"validated terminal slice {index + 1}; no successor remains", - ) + command = command[command.index("loopx ") :] return ScriptedExecToolAction(command=command) return action diff --git a/tests/control_plane/test_todo_authoring_scope.py b/tests/control_plane/test_todo_authoring_scope.py index 2764143aa1..be72a648a5 100644 --- a/tests/control_plane/test_todo_authoring_scope.py +++ b/tests/control_plane/test_todo_authoring_scope.py @@ -4,16 +4,21 @@ import pytest from loopx.control_plane.testing.canary_harness import run_json_cli_result, write_fixture_registry +from loopx.control_plane.coordination.local_authority import ( + LocalCoordinationAuthorityUnavailable, +) from loopx.control_plane.todos.active_state_editing import find_todo_block -from loopx.todos import add_goal_todo, add_todo_to_lines, update_goal_todo +from loopx.todos import add_goal_todo, update_goal_todo from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection GOAL = "authoring-scope" -@pytest.fixture -def authored_goal(tmp_path): +@pytest.fixture(params=["file", "sqlite"]) +def authored_goal(tmp_path, monkeypatch, request): + if request.param == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) project = tmp_path / "project" project.mkdir() state = project / "ACTIVE_GOAL_STATE.md" @@ -23,6 +28,17 @@ def authored_goal(tmp_path): registry_path=registry, goal_id=GOAL, domain="scope-test", adapter_kind="generic_project_goal_v0", registered_agents=["agent-a", "agent-b"], quota_allowed_slots=None) + initialize_canonical_authority( + tmp_path / "runtime", + GOAL, + build_todo_runtime_shadow_projection( + goal_id=GOAL, + handoff_mode="soft_claim", + todos=[], + ), + state_path=state, + provider=request.param, + ) return registry, state @@ -61,7 +77,7 @@ def test_update_rejects_explicit_scope_conflicts_without_writing(authored_goal, registry, state = authored_goal result = add(registry, state, task_class="user_gate", global_gate=True, goal_bound=True) before = state.read_bytes() - with pytest.raises(ValueError): + with pytest.raises((ValueError, LocalCoordinationAuthorityUnavailable)): update_goal_todo(registry_path=registry, goal_id=GOAL, state_file=state, todo_id=result["todo_id"], agent_id="agent-a", role="user", clear_global_gate=not intent.get("global_gate", False), **intent) @@ -100,19 +116,9 @@ def test_missing_scope_never_creates_global_gate(authored_goal): assert state.read_bytes() == before -@pytest.fixture(params=["unpromoted", "file", "sqlite"]) -def execution_exclusion_goal(authored_goal, tmp_path, monkeypatch, request): +@pytest.fixture +def execution_exclusion_goal(authored_goal): registry, state = authored_goal - if request.param != "unpromoted": - if request.param == "sqlite": - isolate_sqlite_runtime(tmp_path, monkeypatch) - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL, handoff_mode="soft_claim", todos=[] - ) - initialize_canonical_authority( - tmp_path / "runtime", GOAL, projection, - state_path=state, provider=request.param, - ) def cli(*args): return run_json_cli_result( @@ -213,18 +219,6 @@ def test_create_role_restrictions_refuse_before_source_write( assert listed["todo_count"] == 0 -@pytest.mark.parametrize("role, task_class, error", [ - ("user", None, "user todo requires explicit --task-class"), - ("agent", "user_gate", "user_action and user_gate task_class are only valid for --role user"), -]) -def test_standalone_markdown_codec_keeps_class_admission(role, task_class, error): - lines = ["# Goal", "", "## Agent Todo", "", "## User Todo", ""] - before = list(lines) - with pytest.raises(ValueError, match=error): - add_todo_to_lines(lines, role=role, task_class=task_class, text="Decide the next step") - assert lines == before - - @pytest.mark.parametrize("role, task_class, flags, error", [ ("user", None, ("--bound-agent", "agent-a"), "user todo requires explicit --task-class"), ("user", "advancement_task", ("--bound-agent", "agent-a"), "user todo requires explicit --task-class"), diff --git a/tests/control_plane/test_todo_capability_gap_cli_validation.py b/tests/control_plane/test_todo_capability_gap_cli_validation.py index 5849fcffa6..197bae1a9d 100644 --- a/tests/control_plane/test_todo_capability_gap_cli_validation.py +++ b/tests/control_plane/test_todo_capability_gap_cli_validation.py @@ -3,6 +3,10 @@ import json from pathlib import Path +from canonical_authority_fixture import initialize_canonical_authority +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.control_plane.testing.canary_harness import ( run_json_cli, write_fixture_registry, @@ -28,7 +32,10 @@ def _write_fixture(tmp_path: Path) -> Path: "## Objective\n\n" "Keep capability-gap lifecycle evidence coherent.\n\n" "## Next Action\n\n" - "- Close the validated capability gap.\n", + "- Close the validated capability gap.\n\n" + "## User Todo / Owner Review Reading Queue\n\n" + "## Agent Todo\n\n" + "## Completed Work Archive\n", encoding="utf-8", ) write_fixture_registry( @@ -45,6 +52,16 @@ def _write_fixture(tmp_path: Path) -> Path: "registered_agents": [AGENT_ID], } registry_path.write_text(json.dumps(payload), encoding="utf-8") + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + handoff_mode="soft_claim", + todos=[], + ), + state_path=state_file, + ) return registry_path @@ -96,4 +113,3 @@ def test_todo_complete_records_fixed_capability_gap_with_completion( assert completed["status"] == "done", completed assert completed["capability_gap_event"]["status"] == "fixed", completed assert completed["capability_gap_event"]["event_kind"] == "capability_gap" - diff --git a/tests/control_plane/test_todo_claim_argument_recovery.py b/tests/control_plane/test_todo_claim_argument_recovery.py index 9654394698..85f87be862 100644 --- a/tests/control_plane/test_todo_claim_argument_recovery.py +++ b/tests/control_plane/test_todo_claim_argument_recovery.py @@ -15,7 +15,7 @@ GOAL, TODO = "claim-arguments", "todo_claim_arguments" -@pytest.fixture(params=["legacy", "file", "sqlite"]) +@pytest.fixture(params=["file", "sqlite"]) def claim_cli(tmp_path, monkeypatch, request): isolate_sqlite_runtime(tmp_path, monkeypatch) provider = request.param @@ -30,21 +30,22 @@ def claim_cli(tmp_path, monkeypatch, request): "id": GOAL, "repo": str(project), "state_file": state.name, "coordination": {"registered_agents": ["peer-a", "peer-b"]}, }]})) - if provider != "legacy": - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL, handoff_mode="hard_lease", leases=[], todos=[{ - "schema_version": "todo_item_v0", "todo_id": TODO, "role": "agent", - "status": "open", "done": False, "text": "Claim one open task.", - "archive_state": "active", "source_section": "Agent Todo", "index": 1, - "task_class": "advancement_task", "required_write_scopes": ["src/**"], - }], - ) - initialize_canonical_authority(runtime, GOAL, projection, state_path=state, provider=provider) - state.unlink() + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL, handoff_mode="hard_lease", leases=[], todos=[{ + "schema_version": "todo_item_v0", "todo_id": TODO, "role": "agent", + "status": "open", "done": False, "text": "Claim one open task.", + "archive_state": "active", "source_section": "Agent Todo", "index": 1, + "task_class": "advancement_task", "required_write_scopes": ["src/**"], + }], + ) + initialize_canonical_authority( + runtime, GOAL, projection, state_path=state, provider=provider, + ) + state.unlink() prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] claim = ["todo", "claim", "--goal-id", GOAL, "--todo-id", TODO, "--agent-id", "peer-a"] - lease = [] if provider == "legacy" else [ + lease = [ "--claim-operation-id", "original-claim", "--task-lease-idempotency-key", "original-lease", "--task-lease-expected-version", "0", ] @@ -101,12 +102,11 @@ def test_one_packet_repairs_all_grammar_errors_and_preserves_route(claim_cli): applied = run([*repaired, "--claimed-by", "peer-a"]) assert applied["ok"] is True assert snapshot()[0][0]["claimed_by"] == "peer-a" - if provider != "legacy": - assert applied["lease"]["version"] == 1 - assert applied["lease"]["write_scopes"] == ["src/**"] - replay = run([*repaired, "--claimed-by", "peer-a"]) - assert replay["status"] == "replayed" - assert replay["changed"] is False + assert applied["lease"]["version"] == 1 + assert applied["lease"]["write_scopes"] == ["src/**"] + replay = run([*repaired, "--claimed-by", "peer-a"]) + assert replay["status"] == "replayed" + assert replay["changed"] is False def test_syntax_repair_does_not_authorize_a_different_executor(claim_cli): @@ -143,7 +143,7 @@ def test_repair_preserves_an_invalid_cas_instead_of_weakening_it(claim_cli): def test_lease_input_requirement_preserves_zero_cas(claim_cli): - provider, run, snapshot, prefix, claim, _, _, _ = claim_cli + _, run, snapshot, prefix, claim, _, _, _ = claim_cli before = snapshot() rejected = run([ *prefix, *claim, "--claimed-by", "peer-a", "--task-lease-expected-version", "0", @@ -154,30 +154,19 @@ def test_lease_input_requirement_preserves_zero_cas(claim_cli): repaired = recovery["cli_args"] assert repaired[repaired.index("--task-lease-expected-version") + 1] == "0" completed_args = [*repaired, "--task-lease-idempotency-key", "explicit-execution"] - if provider == "legacy": - rejected = run(completed_args, expected_exit=1) - assert "requires promoted canonical authority" in rejected["error"] - assert snapshot() == before - else: - applied = run(completed_args) - assert applied["lease"]["version"] == 1 + applied = run(completed_args) + assert applied["lease"]["version"] == 1 def test_grammar_packet_does_not_guess_authority_mode(claim_cli): - provider, run, snapshot, prefix, claim, _, _, _ = claim_cli + _, run, snapshot, prefix, claim, _, _, _ = claim_cli before = snapshot() flags = ["--claimed-by", "peer-a"] - if provider == "legacy": - flags += ["--task-lease-idempotency-key", "canonical-only"] rejected = run([*prefix, *claim, *flags], expected_exit=1) assert rejected.get("error_code") != "todo_claim_invalid_arguments" assert snapshot() == before - if provider == "legacy": - assert "requires promoted canonical authority" in rejected["error"] - assert "recovery" not in rejected - else: - assert rejected["error_code"] == "handoff_mode_requires_lease" - assert rejected["recovery"]["requires_flags"] == ["--task-lease-idempotency-key"] + assert rejected["error_code"] == "handoff_mode_requires_lease" + assert rejected["recovery"]["requires_flags"] == ["--task-lease-idempotency-key"] def test_markdown_exposes_the_same_required_input(): diff --git a/tests/control_plane/test_todo_completion_validation.py b/tests/control_plane/test_todo_completion_validation.py index 727c2a058f..eb9b685b45 100644 --- a/tests/control_plane/test_todo_completion_validation.py +++ b/tests/control_plane/test_todo_completion_validation.py @@ -10,8 +10,14 @@ import pytest -import loopx.control_plane.effect_runtime as effect_runtime_module +from canonical_authority_fixture import initialize_canonical_authority import loopx.control_plane.todos.completion_validation as completion_validation_module +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from loopx.control_plane.coordination.local_authority import ( + LocalCoordinationAuthorityUnavailable, +) from loopx.control_plane.todos.completion_validation_projection import ( completion_validation_declaration_sha256, project_completion_validation_authority, @@ -26,7 +32,7 @@ read_completion_validation_declaration, ) from loopx.status import parse_active_state_todos -from loopx.todos import add_goal_todo, complete_goal_todo, update_goal_todo +from loopx.todos import add_goal_todo, complete_goal_todo GOAL_ID = "todo-completion-validation" AGENT = "codex-author" @@ -78,15 +84,23 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path]: ), encoding="utf-8", ) + initialize_canonical_authority( + tmp_path / "runtime", + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[], + handoff_mode="soft_claim", + ), + state_path=state, + ) return registry, state def _agent_todo(state: Path, todo_id: str) -> dict: todos = parse_active_state_todos(state.read_text(encoding="utf-8")) return next( - item - for item in todos["agent_todos"]["items"] - if item["todo_id"] == todo_id + item for item in todos["agent_todos"]["items"] if item["todo_id"] == todo_id ) @@ -217,20 +231,8 @@ def _same_repository_worktrees( ) -def _record_completion_runtime_calls( - monkeypatch: pytest.MonkeyPatch, -) -> list[str]: - calls: list[str] = [] - original_request = effect_runtime_module._request_with_info - - def recording_request(*args, **kwargs): # type: ignore[no-untyped-def] - method = kwargs.get("method") - if isinstance(method, str) and method.startswith("todo.completion"): - calls.append(method) - return original_request(*args, **kwargs) - - monkeypatch.setattr(effect_runtime_module, "_request_with_info", recording_request) - return calls +def _validation_receipt(result: dict) -> dict: + return result["validation_failure"]["validation_receipt"] def test_validation_command_declared_and_passing_commits_completion( @@ -251,9 +253,9 @@ def counting_runner(*args, **kwargs): # type: ignore[no-untyped-def] calls["count"] += 1 return original_runner(*args, **kwargs) - monkeypatch.setattr(completion_validation_module, "run_caller_validation", counting_runner) - transaction_calls = _record_completion_runtime_calls(monkeypatch) - + monkeypatch.setattr( + completion_validation_module, "run_caller_validation", counting_runner + ) result = complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -262,16 +264,10 @@ def counting_runner(*args, **kwargs): # type: ignore[no-untyped-def] evidence="validated completion", ) assert calls["count"] == 1 # the gate actually ran the declared command - assert [ - method for method in transaction_calls - if method == "todo.completion.reduce" - ] == [ - "todo.completion.reduce", - "todo.completion.reduce", - ] - assert "todo.completion_policy.resolve" not in transaction_calls assert result["ok"] is True assert result["changed"] is True + assert result["provider_status"] == "applied" + assert result["validation_receipt"]["passed"] is True assert "validation_blocked_completion" not in result assert _agent_todo(state, str(todo["todo_id"]))["status"] == "done" @@ -383,8 +379,9 @@ def forbidden_runner(*args, **kwargs): # type: ignore[no-untyped-def] assert calls["count"] == 0 assert result["validation_blocked_completion"] is True - assert result["validation"]["status"] == "workspace_repository_mismatch" - assert str(tmp_path) not in json.dumps(result["validation"]) + receipt = _validation_receipt(result) + assert receipt["status"] == "workspace_repository_mismatch" + assert str(tmp_path) not in json.dumps(receipt) assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" @@ -408,7 +405,9 @@ def forbidden_runner(*args, **kwargs): # type: ignore[no-untyped-def] calls["count"] += 1 raise AssertionError("validation command must not run") - monkeypatch.setattr(completion_validation_module, "run_caller_validation", forbidden_runner) + monkeypatch.setattr( + completion_validation_module, "run_caller_validation", forbidden_runner + ) result = complete_goal_todo( registry_path=registry, @@ -421,8 +420,9 @@ def forbidden_runner(*args, **kwargs): # type: ignore[no-untyped-def] assert calls["count"] == 0 assert result["validation_blocked_completion"] is True - assert result["validation"]["status"] == "workspace_receipt_unavailable" - assert str(tmp_path) not in json.dumps(result["validation"]) + receipt = _validation_receipt(result) + assert receipt["status"] == "workspace_receipt_unavailable" + assert str(tmp_path) not in json.dumps(receipt) assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" @@ -449,7 +449,9 @@ def forbidden_runner(*args, **kwargs): # type: ignore[no-untyped-def] calls["count"] += 1 raise AssertionError("validation command must not run") - monkeypatch.setattr(completion_validation_module, "run_caller_validation", forbidden_runner) + monkeypatch.setattr( + completion_validation_module, "run_caller_validation", forbidden_runner + ) result = complete_goal_todo( registry_path=registry, @@ -463,8 +465,9 @@ def forbidden_runner(*args, **kwargs): # type: ignore[no-untyped-def] assert calls["count"] == 0 assert result["validation_blocked_completion"] is True - assert result["validation"]["status"] == "workspace_repository_mismatch" - assert str(tmp_path) not in json.dumps(result["validation"]) + receipt = _validation_receipt(result) + assert receipt["status"] == "workspace_repository_mismatch" + assert str(tmp_path) not in json.dumps(receipt) assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" @@ -489,7 +492,9 @@ def forbidden_runner(*args, **kwargs): # type: ignore[no-untyped-def] calls["count"] += 1 raise AssertionError("validation command must not run") - monkeypatch.setattr(completion_validation_module, "run_caller_validation", forbidden_runner) + monkeypatch.setattr( + completion_validation_module, "run_caller_validation", forbidden_runner + ) result = complete_goal_todo( registry_path=registry, @@ -503,8 +508,9 @@ def forbidden_runner(*args, **kwargs): # type: ignore[no-untyped-def] assert calls["count"] == 0 assert result["validation_blocked_completion"] is True - assert result["validation"]["status"] == "workspace_dirty" - assert str(tmp_path) not in json.dumps(result["validation"]) + receipt = _validation_receipt(result) + assert receipt["status"] == "workspace_dirty" + assert str(tmp_path) not in json.dumps(receipt) assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" @@ -522,7 +528,9 @@ def test_malformed_delivery_workspace_receipt_returns_typed_failure_without_runn def unexpected_validation(*_args: Any, **_kwargs: Any) -> dict[str, Any]: calls["count"] += 1 - raise AssertionError("malformed workspace receipts must block before validation") + raise AssertionError( + "malformed workspace receipts must block before validation" + ) monkeypatch.setattr( completion_validation_module, @@ -550,8 +558,9 @@ def unexpected_validation(*_args: Any, **_kwargs: Any) -> dict[str, Any]: assert calls["count"] == 0 assert result["ok"] is False assert result["validation_blocked_completion"] is True - assert result["validation"]["status"] == "workspace_receipt_invalid" - assert result["validation"]["local_path_captured"] is False + receipt = _validation_receipt(result) + assert receipt["status"] == "workspace_receipt_invalid" + assert receipt["local_path_captured"] is False assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" @@ -573,7 +582,7 @@ def test_missing_validation_executable_returns_typed_receipt( ) assert result["ok"] is False assert result["validation_blocked_completion"] is True - receipt = result["validation"] + receipt = _validation_receipt(result) assert receipt["passed"] is False assert receipt["status"] == "command_not_run" assert str(missing_executable) not in json.dumps(receipt) @@ -596,7 +605,7 @@ def test_malformed_validation_command_returns_typed_receipt( ) assert result["ok"] is False assert result["validation_blocked_completion"] is True - receipt = result["validation"] + receipt = _validation_receipt(result) assert receipt["passed"] is False assert receipt["status"] == "command_malformed" assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" @@ -623,7 +632,7 @@ def test_validation_command_declared_and_failing_blocks_completion( assert result["completed"] is False assert result["changed"] is False assert result["validation_blocked_completion"] is True - receipt = result["validation"] + receipt = _validation_receipt(result) assert receipt["passed"] is False assert receipt["exit_code"] == 1 assert receipt["command_label"] == "caller-declared smoke" @@ -637,11 +646,9 @@ def test_validation_command_declared_and_failing_blocks_completion( def test_no_validation_command_keeps_fast_path_unchanged( tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, ) -> None: registry, state = _write_fixture(tmp_path) todo = _add_todo(registry) # no validation_command declared - transaction_calls = _record_completion_runtime_calls(monkeypatch) note = "post-merge note parity" result = complete_goal_todo( registry_path=registry, @@ -653,63 +660,13 @@ def test_no_validation_command_keeps_fast_path_unchanged( ) assert result["ok"] is True assert result["changed"] is True - assert [ - method for method in transaction_calls - if method == "todo.completion.reduce" - ] == ["todo.completion.reduce"] - assert "todo.completion_policy.resolve" not in transaction_calls + assert result["provider_status"] == "applied" assert "validation_blocked_completion" not in result persisted = _agent_todo(state, str(todo["todo_id"])) assert persisted["status"] == "done" assert persisted["note"] == note -def test_validation_receipt_cannot_commit_a_changed_completion_source( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_todo( - registry, - validation_command=_PASS_COMMAND, - validation_label="caller-declared smoke", - ) - original_validation = ( - completion_validation_module._run_declared_completion_validation - ) - - def validate_then_drift(**kwargs): # type: ignore[no-untyped-def] - receipt = original_validation(**kwargs) - source = state.read_text(encoding="utf-8") - state.write_text( - source.replace( - "validation_label=caller-declared%20smoke", - "validation_label=drifted%20smoke", - ), - encoding="utf-8", - ) - return receipt - - monkeypatch.setattr( - completion_validation_module, - "_run_declared_completion_validation", - validate_then_drift, - ) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - agent_id=AGENT, - evidence="stale validation receipt", - ) - - assert result["ok"] is False - assert result["completion_source_drift"] is True - assert result["changed"] is False - assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" - - def test_validation_receipt_cannot_commit_changed_completion_policy_source( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -739,41 +696,16 @@ def validate_then_change_registry(**kwargs): # type: ignore[no-untyped-def] validate_then_change_registry, ) - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - agent_id=AGENT, - evidence="stale registry projection", - ) - - assert result["ok"] is False - assert result["completion_source_drift"] is True - assert result["changed"] is False - assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" - + with pytest.raises(LocalCoordinationAuthorityUnavailable) as caught: + complete_goal_todo( + registry_path=registry, + goal_id=GOAL_ID, + todo_id=str(todo["todo_id"]), + agent_id=AGENT, + evidence="stale registry projection", + ) -def test_validation_timeout_blocks_completion( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr( - completion_validation_module, "_COMPLETION_VALIDATION_TIMEOUT_SECONDS", 0.5 - ) - registry, state = _write_fixture(tmp_path) - todo = _add_todo(registry, validation_command=_SLEEP_COMMAND) - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - agent_id=AGENT, - evidence="claim of completion", - ) - assert result["ok"] is False - assert result["validation_blocked_completion"] is True - receipt = result["validation"] - assert receipt["passed"] is False - assert receipt["status"] == "timeout" + assert caught.value.code == "authority_source_changed" assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" @@ -791,7 +723,9 @@ def counting_runner(*args, **kwargs): # type: ignore[no-untyped-def] calls["count"] += 1 return original_runner(*args, **kwargs) - monkeypatch.setattr(completion_validation_module, "run_caller_validation", counting_runner) + monkeypatch.setattr( + completion_validation_module, "run_caller_validation", counting_runner + ) first = complete_goal_todo( registry_path=registry, @@ -833,7 +767,7 @@ def test_per_todo_validation_timeout_overrides_default(tmp_path: Path) -> None: ) assert result["ok"] is False assert result["validation_blocked_completion"] is True - receipt = result["validation"] + receipt = _validation_receipt(result) assert receipt["passed"] is False assert receipt["status"] == "timeout" assert "timed out after 1s" in receipt["summary"] @@ -893,7 +827,7 @@ def test_validation_command_json_failing_blocks_completion( ) assert result["ok"] is False assert result["validation_blocked_completion"] is True - receipt = result["validation"] + receipt = _validation_receipt(result) assert receipt["passed"] is False assert receipt["exit_code"] == 1 assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" @@ -927,62 +861,6 @@ def test_validation_command_json_must_be_nonempty_string_array( _add_todo(registry, validation_command_json=payload) -def test_validation_timeout_works_with_command_json(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - sleep_argv = json.dumps([sys.executable, "-c", "import time; time.sleep(30)"]) - todo = _add_todo( - registry, - validation_command_json=sleep_argv, - validation_timeout_seconds=1, - ) - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - agent_id=AGENT, - evidence="claim of completion", - ) - assert result["ok"] is False - receipt = result["validation"] - assert receipt["status"] == "timeout" - assert "timed out after 1s" in receipt["summary"] - assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" - - -def test_corrupted_argv_declaration_fails_closed(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_todo( - registry, - validation_command_json=json.dumps( - [sys.executable, "-c", "raise SystemExit(0)"] - ), - ) - # Corrupt the persisted argv declaration in place; completion must run the - # gate and fail closed as a malformed command, never silently skip it. - text = state.read_text(encoding="utf-8") - corrupted, substitutions = re.subn( - r"validation_command_argv=\S+", - "validation_command_argv=%5Bbroken", - text, - ) - assert substitutions == 1 - state.write_text(corrupted, encoding="utf-8") - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - agent_id=AGENT, - evidence="claim of completion", - ) - assert result["ok"] is False - assert result["validation_blocked_completion"] is True - receipt = result["validation"] - assert receipt["passed"] is False - assert receipt["status"] == "declaration_invalid" - assert "validation_command_argv" in receipt["summary"] - assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" - - def test_empty_argv_declaration_reports_neutral_message( tmp_path: Path, ) -> None: @@ -1002,159 +880,6 @@ def test_empty_argv_declaration_reports_neutral_message( assert "validation command must not be empty" in receipt["summary"] -def _user_todo(state: Path, todo_id: str) -> dict: - todos = parse_active_state_todos(state.read_text(encoding="utf-8")) - return next( - item - for item in todos["user_todos"]["items"] - if item["todo_id"] == todo_id - ) - - -def _spy_validation_runner(monkeypatch: pytest.MonkeyPatch) -> dict: - original_runner = completion_validation_module.run_caller_validation - calls = {"count": 0} - - def counting_runner(*args, **kwargs): # type: ignore[no-untyped-def] - calls["count"] += 1 - return original_runner(*args, **kwargs) - - monkeypatch.setattr( - completion_validation_module, "run_caller_validation", counting_runner - ) - return calls - - -def test_user_todo_update_done_runs_declared_validation_gate( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, state = _write_fixture(tmp_path) - todo = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="user", - text="Operator-recorded outcome.", - task_class="user_action", - validation_command=_FAIL_COMMAND, - validation_label="caller-declared smoke", - ) - calls = _spy_validation_runner(monkeypatch) - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - status="done", - evidence="claim of completion", - ) - # The declared command ran once and blocked the update: nothing committed. - assert calls["count"] == 1 - assert result["ok"] is False - assert result["changed"] is False - assert result["validation_blocked_completion"] is True - receipt = result["validation"] - assert receipt["passed"] is False - assert receipt["exit_code"] == 1 - assert receipt["stdout_captured"] is False - assert _user_todo(state, str(todo["todo_id"]))["status"] == "open" - - -def test_user_todo_update_done_without_declaration_keeps_fast_path( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, state = _write_fixture(tmp_path) - todo = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="user", - text="Operator-recorded outcome.", - task_class="user_action", - ) - calls = _spy_validation_runner(monkeypatch) - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - status="done", - ) - assert calls["count"] == 0 - assert result["ok"] is True - assert "validation_blocked_completion" not in result - assert _user_todo(state, str(todo["todo_id"]))["status"] == "done" - - -def test_repeated_done_update_does_not_rerun_validation( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, state = _write_fixture(tmp_path) - todo = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="user", - text="Operator-recorded outcome.", - task_class="user_action", - validation_command=_PASS_COMMAND, - ) - calls = _spy_validation_runner(monkeypatch) - first = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - status="done", - evidence="validated completion", - ) - assert first["ok"] is True - assert calls["count"] == 1 # the passing command ran once via the update gate - assert _user_todo(state, str(todo["todo_id"]))["status"] == "done" - - second = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - status="done", - note="re-acknowledged", - ) - # Already-completed short-circuit: the command is not re-run. - assert second["ok"] is True - assert calls["count"] == 1 - assert _user_todo(state, str(todo["todo_id"]))["status"] == "done" - - -def test_agent_todo_update_done_keeps_guard_error_without_running_gate( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_todo(registry, validation_command=_FAIL_COMMAND) - calls = _spy_validation_runner(monkeypatch) - with pytest.raises(ValueError, match="must use complete_goal_todo"): - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(todo["todo_id"]), - status="done", - ) - # The gate never runs for agent sections; the pre-existing guard fires. - assert calls["count"] == 0 - assert _agent_todo(state, str(todo["todo_id"]))["status"] == "open" - - - - - - - - -def _set_registry_event_log(registry: Path, event_log: Path) -> None: - data = json.loads(registry.read_text(encoding="utf-8")) - data["goals"][0]["event_log"] = str(event_log) - registry.write_text(json.dumps(data), encoding="utf-8") - - - - def test_private_validation_store_is_owner_only_and_detects_tampering( tmp_path: Path, ) -> None: @@ -1177,11 +902,14 @@ def test_private_validation_store_is_owner_only_and_detects_tampering( ) assert path.stat().st_mode & 0o777 == 0o600 - assert read_completion_validation_declaration( - runtime_root=tmp_path, - goal_id=GOAL_ID, - todo_id="todo_private_validation", - ) == declaration + assert ( + read_completion_validation_declaration( + runtime_root=tmp_path, + goal_id=GOAL_ID, + todo_id="todo_private_validation", + ) + == declaration + ) assert digest == completion_validation_declaration_sha256(declaration) stored = json.loads(path.read_text(encoding="utf-8")) diff --git a/tests/control_plane/test_todo_decision_scope_cli_validation.py b/tests/control_plane/test_todo_decision_scope_cli_validation.py index 8a6d9b8966..5f8191e26a 100644 --- a/tests/control_plane/test_todo_decision_scope_cli_validation.py +++ b/tests/control_plane/test_todo_decision_scope_cli_validation.py @@ -5,12 +5,15 @@ import pytest +from canonical_authority_fixture import initialize_canonical_authority +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.control_plane.testing.canary_harness import ( run_json_cli, run_json_cli_result, write_fixture_registry, ) -from loopx.rollout_event_log import load_rollout_events, rollout_event_log_path GOAL_ID = "decision-scope-cli-validation" @@ -34,7 +37,10 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path]: "## Objective\n\n" "Keep decision-scope writes valid.\n\n" "## Next Action\n\n" - "- Validate the next todo write.\n", + "- Validate the next todo write.\n\n" + "## User Todo / Owner Review Reading Queue\n\n" + "## Agent Todo\n\n" + "## Completed Work Archive\n", encoding="utf-8", ) write_fixture_registry( @@ -45,6 +51,16 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path]: domain="decision-scope-cli-validation", adapter_kind="generic_project_goal_v0", ) + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + handoff_mode="soft_claim", + todos=[], + ), + state_path=state_file, + ) return registry_path, state_file @@ -102,9 +118,21 @@ def test_todo_update_can_narrow_global_gate_without_consuming_decision( registry_path=registry_path, ) - assert updated["status"] == "open", updated - assert updated["blocks_agent"] == DELIVERY_AGENT, updated - assert updated["global_gate"] is None, updated + assert updated["status"] == "applied", updated + assert updated["source_authority"] == "file_v0", updated + assert updated["legacy_fallback_used"] is False, updated + readback = run_json_cli( + "todo", + "list", + "--goal-id", + GOAL_ID, + "--todo-id", + gate["todo_id"], + registry_path=registry_path, + )["todo"] + assert readback["status"] == "open", readback + assert readback["blocks_agent"] == DELIVERY_AGENT, readback + assert readback.get("global_gate") is None, readback state_text = state_file.read_text(encoding="utf-8") assert f"blocks_agent={DELIVERY_AGENT}" in state_text assert "global_gate=" not in state_text @@ -361,20 +389,21 @@ def test_todo_complete_cli_applies_explicit_user_gate_outcome( ) assert completed["decision_outcome"] == decision_outcome - events = load_rollout_events( - rollout_event_log_path(tmp_path / "runtime", GOAL_ID) - ) - completion_event = next( - event - for event in reversed(events) - if event.get("event_kind") == "todo_complete" - and event.get("todo_id") == gate["todo_id"] - ) - assert completion_event["details"]["task_class"] == "user_gate" - assert completion_event["details"]["decision_scope"] == ( - "private_read:project:restricted_material" - ) - assert completion_event["details"]["decision_outcome"] == decision_outcome + assert completed["source_authority"] == "file_v0" + assert completed["legacy_fallback_used"] is False + gate_readback = run_json_cli( + "todo", + "list", + "--goal-id", + GOAL_ID, + "--todo-id", + gate["todo_id"], + registry_path=registry_path, + )["todo"] + assert gate_readback["status"] == "done" + assert gate_readback["task_class"] == "user_gate" + assert gate_readback["decision_scope"]["scope_key"] == "restricted_material" + assert gate_readback["decision_outcome"] == decision_outcome target_readback = run_json_cli( "todo", "list", @@ -394,7 +423,7 @@ def test_todo_complete_cli_applies_explicit_user_gate_outcome( ) -def test_todo_complete_cli_rejects_ambiguous_user_gate_completion( +def test_todo_complete_cli_closes_user_gate_without_inventing_outcome( tmp_path: Path, ) -> None: registry_path, state_file = _write_fixture(tmp_path) @@ -413,9 +442,7 @@ def test_todo_complete_cli_rejects_ambiguous_user_gate_completion( VALID_SCOPE, registry_path=registry_path, ) - original = state_file.read_text(encoding="utf-8") - - returncode, payload = run_json_cli_result( + completed = run_json_cli( "todo", "complete", "--goal-id", @@ -429,6 +456,18 @@ def test_todo_complete_cli_rejects_ambiguous_user_gate_completion( registry_path=registry_path, ) - assert returncode != 0 - assert "user_gate completion requires decision_outcome" in payload["error"] - assert state_file.read_text(encoding="utf-8") == original + assert completed["status"] == "done" + assert "decision_outcome" not in completed + readback = run_json_cli( + "todo", + "list", + "--goal-id", + GOAL_ID, + "--todo-id", + gate["todo_id"], + registry_path=registry_path, + )["todo"] + assert readback["status"] == "done" + assert readback["decision_scope"]["scope_key"] == "release_route" + assert "decision_outcome" not in readback + assert gate["todo_id"] in state_file.read_text(encoding="utf-8") diff --git a/tests/control_plane/test_todo_decision_scope_lifecycle.py b/tests/control_plane/test_todo_decision_scope_lifecycle.py index cb590ede2d..3082e659c2 100644 --- a/tests/control_plane/test_todo_decision_scope_lifecycle.py +++ b/tests/control_plane/test_todo_decision_scope_lifecycle.py @@ -5,6 +5,14 @@ import pytest +from canonical_authority_fixture import initialize_canonical_authority +from loopx.control_plane.coordination.coordination_state_contract import ( + TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + TODO_DOMAIN_RECORD_FIELDS, +) +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.quota import build_quota_should_run from loopx.status import collect_status, parse_active_state_todos from loopx.todos import add_goal_todo, complete_goal_todo, supersede_goal_todo @@ -62,6 +70,25 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path, Path]: return repo, state, registry +def _canonical_fixture(tmp_path: Path) -> tuple[Path, Path, Path]: + repo, state, registry = _write_fixture(tmp_path) + config = json.loads(registry.read_text(encoding="utf-8")) + config["common_runtime_root"] = str(tmp_path / "runtime") + registry.write_text(json.dumps(config), encoding="utf-8") + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, handoff_mode="soft_claim", todos=[], + ) + projection["todo_read_model"] = { + **projection["todo_read_model"], + "schema_version": TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + "contract_fields": list(TODO_DOMAIN_RECORD_FIELDS), + } + initialize_canonical_authority( + tmp_path / "runtime", GOAL_ID, projection, state_path=state, + ) + return repo, state, registry + + def _todo(state: Path, todo_id: str) -> dict: todos = parse_active_state_todos(state.read_text(encoding="utf-8")) return next( @@ -105,7 +132,7 @@ def _add_target_and_gate( def test_completed_gate_consumes_scope_for_already_open_publication( tmp_path: Path, ) -> None: - repo, state, registry = _write_fixture(tmp_path) + repo, state, registry = _canonical_fixture(tmp_path) target, gate = _add_target_and_gate( registry, required_scopes=[PUBLISH_SCOPE], @@ -116,6 +143,7 @@ def test_completed_gate_consumes_scope_for_already_open_publication( goal_id=GOAL_ID, todo_id=gate["todo_id"], role="user", + agent_id=AGENT_ID, decision_outcome="approve", evidence="owner approved the exact release publication", ) @@ -164,7 +192,7 @@ def test_completed_gate_consumes_scope_for_already_open_publication( def test_completed_broad_gate_projects_standing_authority_into_quota( tmp_path: Path, ) -> None: - repo, state, registry = _write_fixture(tmp_path) + repo, state, registry = _canonical_fixture(tmp_path) target = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -219,7 +247,7 @@ def test_completed_broad_gate_projects_standing_authority_into_quota( def test_completed_gate_consumes_only_covered_scope(tmp_path: Path) -> None: - _repo, state, registry = _write_fixture(tmp_path) + _repo, state, registry = _canonical_fixture(tmp_path) target, gate = _add_target_and_gate( registry, required_scopes=[PUBLISH_SCOPE, ANNOUNCE_SCOPE], @@ -230,6 +258,7 @@ def test_completed_gate_consumes_only_covered_scope(tmp_path: Path) -> None: goal_id=GOAL_ID, todo_id=gate["todo_id"], role="user", + agent_id=AGENT_ID, decision_outcome="approve", evidence="owner approved publication only", ) @@ -250,7 +279,7 @@ def test_completed_gate_consumes_only_covered_scope(tmp_path: Path) -> None: def test_superseded_gate_does_not_consume_required_scope(tmp_path: Path) -> None: - _repo, state, registry = _write_fixture(tmp_path) + _repo, state, registry = _canonical_fixture(tmp_path) target, gate = _add_target_and_gate( registry, required_scopes=[PUBLISH_SCOPE], @@ -286,7 +315,7 @@ def test_non_approval_gate_outcome_remains_a_durable_block( decision_outcome: str, unblock_state: str, ) -> None: - repo, state, registry = _write_fixture(tmp_path) + repo, state, registry = _canonical_fixture(tmp_path) target, gate = _add_target_and_gate( registry, required_scopes=[PRIVATE_READ_SCOPE], @@ -299,6 +328,7 @@ def test_non_approval_gate_outcome_remains_a_durable_block( goal_id=GOAL_ID, todo_id=gate["todo_id"], role="user", + agent_id=AGENT_ID, decision_outcome=decision_outcome, evidence=f"owner recorded {decision_outcome} for the exact private read", ) @@ -339,29 +369,30 @@ def test_non_approval_gate_outcome_remains_a_durable_block( assert consistency is None or consistency["ok"] is True -def test_user_gate_completion_requires_explicit_decision_outcome( +def test_user_gate_completion_without_decision_closes_only_the_source( tmp_path: Path, ) -> None: - _repo, state, registry = _write_fixture(tmp_path) + _repo, state, registry = _canonical_fixture(tmp_path) target, gate = _add_target_and_gate( registry, required_scopes=[PRIVATE_READ_SCOPE], target_status="blocked", decision_scope=PRIVATE_READ_SCOPE, ) - before = state.read_text(encoding="utf-8") - - with pytest.raises( - ValueError, - match="user_gate completion requires decision_outcome", - ): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - role="user", - evidence="ambiguous completion must not grant authority", - ) + result = complete_goal_todo( + registry_path=registry, + goal_id=GOAL_ID, + todo_id=gate["todo_id"], + role="user", + agent_id=AGENT_ID, + evidence="Close the request without recording an owner decision", + ) - assert state.read_text(encoding="utf-8") == before - assert _todo(state, target["todo_id"])["status"] == "blocked" + assert result.get("decision_outcome") is None + assert "decision_scope_resolution" not in result + assert _todo(state, gate["todo_id"])["status"] == "done" + current_target = _todo(state, target["todo_id"]) + assert current_target["status"] == "blocked" + assert current_target["required_decision_scopes"][0]["scope_key"] == ( + "restricted_material" + ) diff --git a/tests/control_plane/test_todo_document_ownership.py b/tests/control_plane/test_todo_document_ownership.py index 7e737c4f5a..982c6eee78 100644 --- a/tests/control_plane/test_todo_document_ownership.py +++ b/tests/control_plane/test_todo_document_ownership.py @@ -7,6 +7,13 @@ import pytest +from canonical_authority_fixture import initialize_canonical_authority +from loopx.control_plane.coordination.local_authority import ( + read_canonical_todos_if_promoted, +) +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.control_plane.todos.active_state_editing import section_bounds, archive_section_bounds from loopx.control_plane.todos.active_state_todo_parser import parse_todo_source from loopx.control_plane.todos.machine_section_projection import render_canonical_todo_sections, inspect_todo_section_projection @@ -68,24 +75,88 @@ def test_projection_diagnostics_ignore_markers_inside_examples(): assert {row["revision"] for row in inspected["sections"]} == {"actual:1"} -def test_public_legacy_update_targets_real_section_and_preserves_example(tmp_path): +def test_canonical_update_projects_real_section_and_preserves_example(tmp_path): state, registry = tmp_path / "state.md", tmp_path / "registry.json" + runtime = tmp_path / "runtime" example = "```markdown\n" + EXAMPLE + "```\n" state.write_text(document(example, False)) - registry.write_text(json.dumps({"common_runtime_root": str(tmp_path / "runtime"), "goals": [{ + registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{ "id": "document-goal", "repo": str(tmp_path), "state_file": state.name, "coordination": {"registered_agents": ["agent-a"]}, }]})) - def cli(todo_id): - return subprocess.run([sys.executable, "-m", "loopx.cli", "--registry", str(registry), "--format", "json", - "todo", "update", "--goal-id", "document-goal", "--role", "agent", "--agent-id", "agent-a", - "--todo-id", todo_id, "--text", "Updated actual task"], capture_output=True, text=True, timeout=90) + active, _, _ = parse_todo_source(state.read_text(encoding="utf-8")) + initialize_canonical_authority( + runtime, + "document-goal", + build_todo_runtime_shadow_projection( + goal_id="document-goal", + todos=[ + { + "schema_version": "todo_item_v0", + "task_class": "advancement_task", + **record, + } + for record in active["agent"] + ], + handoff_mode="soft_claim", + ), + state_path=state, + ) + + def cli(*arguments): + return subprocess.run( + [ + sys.executable, + "-m", + "loopx.cli", + "--registry", + str(registry), + "--format", + "json", + "todo", + *arguments, + ], + capture_output=True, + text=True, + timeout=90, + ) + before = state.read_bytes() - rejected = cli("todo_example") + rejected = cli( + "update", + "--goal-id", + "document-goal", + "--role", + "agent", + "--agent-id", + "agent-a", + "--todo-id", + "todo_example", + "--text", + "Updated actual task", + ) assert rejected.returncode == 1, rejected.stdout + rejected.stderr assert state.read_bytes() == before - updated = cli("todo_real") + updated = cli( + "update", + "--goal-id", + "document-goal", + "--role", + "agent", + "--agent-id", + "agent-a", + "--todo-id", + "todo_real", + "--text", + "Updated actual task", + ) assert updated.returncode == 0, updated.stdout + updated.stderr + projection = read_canonical_todos_if_promoted( + runtime_root=runtime, + goal_id="document-goal", + ) + assert projection is not None + assert projection["todos"][0]["text"] == "Updated actual task" assert example in state.read_text() assert "Updated actual task" in state.read_text() diff --git a/tests/control_plane/test_todo_external_wait.py b/tests/control_plane/test_todo_external_wait.py index 65118e7c23..ab461e0816 100644 --- a/tests/control_plane/test_todo_external_wait.py +++ b/tests/control_plane/test_todo_external_wait.py @@ -1,23 +1,31 @@ from __future__ import annotations -from concurrent.futures import ThreadPoolExecutor from pathlib import Path -from threading import Event import pytest from loopx.control_plane.scheduler.monitor_poll_writeback import ( write_monitor_poll_todo_state, ) +from loopx.control_plane.coordination.local_authority import ( + read_canonical_todo_fields_if_promoted, + read_canonical_todos_if_promoted, +) from loopx.control_plane.testing.canary_harness import ( run_json_cli_result, write_fixture_registry, ) from loopx.control_plane.todos.active_state_todo_parser import parse_active_state_todos +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.control_plane.todos.external_wait_contract import ( TodoExternalWaitAuthoringError, ) from loopx.todos import complete_goal_todo, update_goal_todo +from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_authority, +) GOAL_ID = "external-wait-fixture" @@ -28,7 +36,11 @@ FALLBACK_ID = "todo_fallback001" -def _write_fixture(root: Path, *, dependency: bool = False) -> tuple[Path, Path]: +def _write_fixture( + root: Path, + *, + dependency: bool = False, +) -> tuple[Path, Path, Path]: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -72,22 +84,49 @@ def _write_fixture(root: Path, *, dependency: bool = False) -> tuple[Path, Path] registered_agents=[AGENT_ID], quota_allowed_slots=None, ) - return registry, state_file + todos = parse_active_state_todos(state_file.read_text(encoding="utf-8"))[ + "agent_todos" + ]["items"] + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=todos, + handoff_mode="soft_claim", + ), + state_path=state_file, + ) + return registry, runtime, state_file -def _todos(state_file: Path) -> dict[str, dict]: - summary = parse_active_state_todos(state_file.read_text(encoding="utf-8"))[ - "agent_todos" - ] - return {item["todo_id"]: item for item in summary["items"]} +def _todos(runtime: Path) -> dict[str, dict]: + source = read_canonical_todos_if_promoted( + runtime_root=runtime, + goal_id=GOAL_ID, + ) + assert source is not None + return {item["todo_id"]: item for item in source["todos"]} + + +def _summary_todos(runtime: Path) -> dict[str, dict]: + fields = read_canonical_todo_fields_if_promoted( + runtime_root=runtime, + goal_id=GOAL_ID, + ) + assert fields is not None + return { + item["todo_id"]: item + for item in fields["agent_todos"]["items"] + } def test_monitor_change_wait_binds_generation_and_resumes_only_after_increment( tmp_path: Path, ) -> None: - registry, state_file = _write_fixture(tmp_path) + registry, runtime, state_file = _write_fixture(tmp_path) - transition = update_goal_todo( + update_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=WAITING_ID, @@ -96,13 +135,12 @@ def test_monitor_change_wait_binds_generation_and_resumes_only_after_increment( agent_id=AGENT_ID, ) - receipt = transition["external_wait_transition"] - assert receipt["baseline_generation"] == 2 - assert receipt["successor_todo_ids"] == [FALLBACK_ID] - assert receipt["authoring_contract"]["waiting_todo"]["status"] == "open" - waiting = _todos(state_file)[WAITING_ID] + waiting = _todos(runtime)[WAITING_ID] + assert waiting["status"] == "open" + assert waiting["resume_when"] == f"monitor_changed:{MONITOR_ID}" assert waiting["resume_monitor_generation"] == 2 - assert waiting["resume_ready"] is False + assert waiting["successor_todo_ids"] == [FALLBACK_ID] + assert _summary_todos(runtime)[WAITING_ID]["resume_ready"] is False unchanged = write_monitor_poll_todo_state( registry_path=registry, @@ -118,7 +156,7 @@ def test_monitor_change_wait_binds_generation_and_resumes_only_after_increment( agent_id=AGENT_ID, ) assert unchanged["material_change_generation"] == 2 - assert _todos(state_file)[WAITING_ID]["resume_ready"] is False + assert _summary_todos(runtime)[WAITING_ID]["resume_ready"] is False changed = write_monitor_poll_todo_state( registry_path=registry, @@ -133,7 +171,7 @@ def test_monitor_change_wait_binds_generation_and_resumes_only_after_increment( agent_id=AGENT_ID, ) assert changed["material_change_generation"] == 3 - resumed = _todos(state_file)[WAITING_ID] + resumed = _summary_todos(runtime)[WAITING_ID] assert resumed["resume_ready"] is True assert resumed["resume_condition"]["generation_fence"] == ( "strictly_greater_than_baseline" @@ -167,7 +205,7 @@ def test_monitor_change_wait_binds_generation_and_resumes_only_after_increment( def test_monitor_wait_diagnoses_status_and_successor_faults_separately( tmp_path: Path, ) -> None: - registry, _ = _write_fixture(tmp_path) + registry, _, _ = _write_fixture(tmp_path) with pytest.raises(TodoExternalWaitAuthoringError) as status_error: update_goal_todo( @@ -206,7 +244,7 @@ def test_monitor_wait_diagnoses_status_and_successor_faults_separately( def test_todo_cli_projects_external_wait_repair_contract(tmp_path: Path) -> None: - registry, _ = _write_fixture(tmp_path) + registry, _, _ = _write_fixture(tmp_path) returncode, payload = run_json_cli_result( "todo", @@ -243,7 +281,7 @@ def test_todo_cli_projects_external_wait_repair_contract(tmp_path: Path) -> None def test_monitor_provider_effect_replay_does_not_advance_counters( tmp_path: Path, ) -> None: - registry, state_file = _write_fixture(tmp_path) + registry, runtime, state_file = _write_fixture(tmp_path) kwargs = { "registry_path": registry, "runtime_root": tmp_path / "runtime", @@ -266,7 +304,7 @@ def test_monitor_provider_effect_replay_does_not_advance_counters( assert first["monitor_effect_id"] == "quota-monitor-poll:provider-reentry" assert replayed["provider_replayed"] is True assert replayed["consecutive_no_change"] == first["consecutive_no_change"] - monitor = _todos(state_file)[MONITOR_ID] + monitor = _todos(runtime)[MONITOR_ID] assert monitor["monitor_effect_id"] == "quota-monitor-poll:provider-reentry" assert int(monitor["consecutive_no_change"]) == first["consecutive_no_change"] @@ -291,13 +329,20 @@ def test_monitor_provider_effect_replay_does_not_advance_counters( before_replay = state_file.read_bytes() historical = write_monitor_poll_todo_state(**kwargs) expected = {**first, "provider_replayed": True} - # Post-commit shadow diagnostics are outside the immutable business receipt. - expected.pop("coordination_runtime_shadow", None) + actual = dict(historical) + # Projection delivery describes the current head, not the immutable receipt. + for key in ( + "coordination_runtime_shadow", + "projection_delivery", + "projection_outbox", + ): + actual.pop(key, None) + expected.pop(key, None) expected["todo_update"] = {key: value for key, value in first["todo_update"].items() if key != "coordination_runtime_shadow"} - assert historical == expected + assert actual == expected assert state_file.read_bytes() == before_replay - monitor = _todos(state_file)[MONITOR_ID] + monitor = _todos(runtime)[MONITOR_ID] assert monitor["result_hash"] == "review-v3-newer" assert monitor["monitor_effect_id"] == "quota-monitor-poll:newer-effect" @@ -305,7 +350,7 @@ def test_monitor_provider_effect_replay_does_not_advance_counters( def test_monitor_provider_effect_replay_reuses_material_successor( tmp_path: Path, ) -> None: - registry, state_file = _write_fixture(tmp_path) + registry, _, state_file = _write_fixture(tmp_path) successor_text = "Advance the material monitor transition." kwargs = { "registry_path": registry, @@ -339,78 +384,10 @@ def test_monitor_provider_effect_replay_reuses_material_successor( assert state_file.read_text(encoding="utf-8").count(successor_text) == 1 -def test_overlapping_material_batches_recompute_generation_after_wait_baseline( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - import loopx.control_plane.scheduler.legacy_monitor_poll as batch_module - - registry, state_file = _write_fixture(tmp_path) - original_apply = batch_module.apply_legacy_monitor_poll - second_poll_ready = Event() - release_second_poll = Event() - - def delay_second_poll(**kwargs): - result_hash = kwargs["request"]["observation"]["result_hash"] - if result_hash == "review-v4-second": - second_poll_ready.set() - if not release_second_poll.wait(timeout=30): - raise RuntimeError("timed out waiting to release overlapping poll") - return original_apply(**kwargs) - - monkeypatch.setattr(batch_module, "apply_legacy_monitor_poll", delay_second_poll) - with ThreadPoolExecutor(max_workers=2) as executor: - second_poll = executor.submit( - write_monitor_poll_todo_state, - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - execute=True, - generated_at="2026-08-25T02:01:00Z", - todo_id=MONITOR_ID, - result_hash="review-v4-second", - material_change=True, - agent_id=AGENT_ID, - ) - assert second_poll_ready.wait(timeout=5) - - first_poll = write_monitor_poll_todo_state( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - execute=True, - generated_at="2026-08-25T02:00:00Z", - todo_id=MONITOR_ID, - result_hash="review-v3-first", - material_change=True, - agent_id=AGENT_ID, - ) - assert first_poll["material_change_generation"] == 3 - - wait_transition = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=WAITING_ID, - resume_when=f"monitor_changed:{MONITOR_ID}", - successor_todo_ids=[FALLBACK_ID], - agent_id=AGENT_ID, - ) - assert wait_transition["external_wait_transition"]["baseline_generation"] == 3 - - release_second_poll.set() - second_result = second_poll.result(timeout=5) - - assert second_result["material_change_generation"] == 4 - todos = _todos(state_file) - assert todos[MONITOR_ID]["result_hash"] == "review-v4-second" - assert todos[MONITOR_ID]["material_change_generation"] == 4 - assert todos[WAITING_ID]["resume_ready"] is True - - def test_dependency_completion_automatically_resumes_waiting_todo(tmp_path: Path) -> None: - registry, state_file = _write_fixture(tmp_path, dependency=True) + registry, runtime, _ = _write_fixture(tmp_path, dependency=True) - transition = update_goal_todo( + update_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=WAITING_ID, @@ -418,8 +395,10 @@ def test_dependency_completion_automatically_resumes_waiting_todo(tmp_path: Path successor_todo_ids=[FALLBACK_ID], agent_id=AGENT_ID, ) - assert transition["external_wait_transition"]["resume_kind"] == "todo_done" - assert _todos(state_file)[WAITING_ID]["resume_ready"] is False + waiting = _todos(runtime)[WAITING_ID] + assert waiting["resume_when"] == f"todo_done:{DEPENDENCY_ID}" + assert waiting["successor_todo_ids"] == [FALLBACK_ID] + assert _summary_todos(runtime)[WAITING_ID]["resume_ready"] is False complete_goal_todo( registry_path=registry, @@ -429,13 +408,13 @@ def test_dependency_completion_automatically_resumes_waiting_todo(tmp_path: Path agent_id=AGENT_ID, ) - waiting = _todos(state_file)[WAITING_ID] + waiting = _summary_todos(runtime)[WAITING_ID] assert waiting["resume_ready"] is True assert waiting["resume_condition"]["target_status"] == "done" def test_waiting_prose_without_typed_condition_remains_runnable(tmp_path: Path) -> None: - registry, state_file = _write_fixture(tmp_path) + registry, runtime, _ = _write_fixture(tmp_path) update_goal_todo( registry_path=registry, @@ -445,9 +424,12 @@ def test_waiting_prose_without_typed_condition_remains_runnable(tmp_path: Path) agent_id=AGENT_ID, ) - waiting = _todos(state_file)[WAITING_ID] + waiting = _todos(runtime)[WAITING_ID] assert "resume_when" not in waiting - executable = parse_active_state_todos( - state_file.read_text(encoding="utf-8") - )["agent_todos"]["first_executable_items"] + fields = read_canonical_todo_fields_if_promoted( + runtime_root=runtime, + goal_id=GOAL_ID, + ) + assert fields is not None + executable = fields["agent_todos"]["first_executable_items"] assert WAITING_ID in {item["todo_id"] for item in executable} diff --git a/tests/control_plane/test_todo_field_update_runtime.py b/tests/control_plane/test_todo_field_update_runtime.py deleted file mode 100644 index 141a0cdf38..0000000000 --- a/tests/control_plane/test_todo_field_update_runtime.py +++ /dev/null @@ -1,300 +0,0 @@ -"""Observable update semantics shared by the legacy lifecycle writers.""" - -from copy import deepcopy - -import pytest - -from loopx.control_plane.todos.active_state_editing import find_todo_block -from loopx.control_plane.todos.line_update import apply_todo_update_to_lines -from loopx.control_plane.todos.contract import normalize_todo_watch_only - -AT = "2026-09-01T00:00:00Z" - - -def _lines(metadata=""): - return [ - "## Agent Todo", - "", - "- [ ] Preserve this task", - " Detail stays unless text changes.", - " ", - "", - ] - - -@pytest.mark.parametrize( - "intent, initial, expected", - [ - ({"clear_claim": True}, "claimed_by=agent-a", {"claimed_by": None}), - ({"claimed_by": "agent-b"}, "claimed_by=agent-a", {"claimed_by": "agent-b"}), - ( - {"clear_user_binding": True}, - "bound_agent=agent-a", - {"bound_agent": None, "goal_bound": None}, - ), - ( - {"goal_bound": True}, - "bound_agent=agent-a", - {"bound_agent": None, "goal_bound": True}, - ), - ({"excluded_agents": []}, "excluded_agents=agent-a", {"excluded_agents": []}), - ({"clear_blocks_agent": True}, "blocks_agent=agent-a", {"blocks_agent": None}), - ({"clear_global_gate": True}, "global_gate=true", {"global_gate": None}), - ( - {"clear_resume_when": True}, - "resume_when=monitor_changed:todo_monitor resume_monitor_generation=4", - {"resume_when": None, "resume_monitor_generation": None}, - ), - ( - {"resume_when": "todo_done:todo_dependency"}, - "resume_monitor_generation=4", - { - "resume_when": "todo_done:todo_dependency", - "resume_monitor_generation": None, - }, - ), - ( - { - "resume_when": "monitor_changed:todo_monitor", - "resume_monitor_generation": 0, - }, - "", - { - "resume_when": "monitor_changed:todo_monitor", - "resume_monitor_generation": 0, - }, - ), - ( - {"status": "done", "no_followup": True}, - "", - { - "status": "done", - "no_followup": True, - "completion_continuation": "no_followup", - }, - ), - ( - {"status": "done", "successor_todo_ids": ["todo_successor"]}, - "", - {"completion_continuation": "successor"}, - ), - ( - {"status": "open"}, - "completed_at=2026-08-01T00%3A00%3A00Z", - {"status": "open"}, - ), - ( - {"note": "", "required_capabilities": []}, - "note=keep required_capabilities=network", - {"required_capabilities": []}, - ), - ( - {"monitor_metadata": {"consecutive_no_change": 0, "watch_only": False}}, - "watch_only=true", - {"watch_only": False}, - ), - ], -) -def test_field_omission_clear_and_completion_semantics(intent, initial, expected): - lines = _lines(initial) - result = apply_todo_update_to_lines( - lines, todo_id="todo_field_test", updated_at=AT, **intent - ) - for key, value in expected.items(): - # Legacy result fields retain the Markdown codec's scalar representation. - actual = ( - normalize_todo_watch_only(result[key]) - if key == "watch_only" - else result[key] - ) - assert actual == value - block = find_todo_block(lines, todo_id="todo_field_test")[4] - assert block["todo_id"] == "todo_field_test" - if intent.get("status") == "done": - assert block["completed_at"] == AT - if intent.get("status") == "open": - assert not block.get("completed_at") - if intent.get("note") == "": - assert block["note"] == "keep" # Empty note historically means omit here. - - -def test_noop_does_not_advance_updated_at_or_touch_unrelated_lines(): - lines = _lines("updated_at=2026-08-01T00%3A00%3A00Z") - # Stabilize representation once; the next call must be an actual no-op. - apply_todo_update_to_lines(lines, todo_id="todo_field_test", updated_at=AT) - before = deepcopy(lines) - result = apply_todo_update_to_lines( - lines, todo_id="todo_field_test", updated_at="2026-09-02T00:00:00Z" - ) - assert not result["changed"] - assert lines == before - - -@pytest.mark.parametrize( - "intent, initial, error", - [ - ({"status": "invalid"}, "", "todo status"), - ({"claim_only": True}, "claimed_by=agent-a", None), - ( - {"claim_only": True, "claimed_by": "agent-b"}, - "claimed_by=agent-a", - "already claimed_by", - ), - ( - {"status": "deferred", "clear_resume_when": True}, - "", - "cannot clear resume_when", - ), - ( - { - "status": "done", - "no_followup": True, - "successor_todo_ids": ["todo_successor"], - }, - "", - "both no_followup", - ), - ], -) -def test_invalid_lifecycle_field_combinations_are_rejected(intent, initial, error): - lines = _lines(initial) - if error is None: - apply_todo_update_to_lines( - lines, todo_id="todo_field_test", updated_at=AT, **intent - ) - else: - before = deepcopy(lines) - with pytest.raises(ValueError, match=error): - apply_todo_update_to_lines( - lines, todo_id="todo_field_test", updated_at=AT, **intent - ) - assert lines == before - - -def test_completion_field_plan_has_one_crossing_and_retires_metadata_rpc(monkeypatch): - from loopx.control_plane import effect_runtime - from loopx.control_plane.todos import line_update - - actual = effect_runtime.effect_runtime_result - calls = [] - - def traced(method, params): - calls.append(method) - return actual(method, params) - - monkeypatch.setattr(line_update, "effect_runtime_result", traced) - result = apply_todo_update_to_lines( - _lines(), - todo_id="todo_field_test", - updated_at=AT, - status="done", - no_followup=True, - ) - assert result["completion_continuation"] == "no_followup" - assert calls == ["todo.field_update.plan"] - with pytest.raises( - effect_runtime.EffectRuntimeRejected, match="[Uu]nknown|[Uu]nsupported" - ): - actual("todo.completion_state.metadata_updates", {}) - - -def test_rejected_plan_does_not_partially_change_text_or_checkbox(): - lines = _lines() - before = deepcopy(lines) - with pytest.raises(ValueError, match="both no_followup"): - apply_todo_update_to_lines( - lines, - todo_id="todo_field_test", - updated_at=AT, - text="Do not commit", - status="done", - no_followup=True, - successor_todo_ids=["todo_next"], - ) - assert lines == before - - -@pytest.mark.parametrize( - "resume, clear, expected, error", - [ - (None, False, "todo_done:todo_old", None), - ("", False, "todo_done:todo_old", None), - ("\u001c\t \u2003", False, "todo_done:todo_old", None), - ("\u2003TODO_DONE:todo_dependency\u2003", False, "todo_done:todo_dependency", None), - ("resume_at:2026-09-01T08:00:00+08:00", False, "resume_at:2026-09-01T00:00:00Z", None), - ("", True, None, None), - ("todo_done:todo_dependency", True, None, "either resume_when"), - ("unknown:todo_dependency", False, None, "supported"), - ("resume_at:2026-09-01T08:00:00", False, None, "supported"), - ], -) -def test_compatibility_field_update_preserves_resume_authoring(resume, clear, expected, error): - lines = _lines("resume_when=todo_done:todo_old") - before = deepcopy(lines) - kwargs = dict(todo_id="todo_field_test", updated_at=AT, - resume_when=resume, clear_resume_when=clear) - if error: - with pytest.raises(ValueError, match=error): - apply_todo_update_to_lines(lines, text="Do not commit", **kwargs) - assert lines == before - else: - result = apply_todo_update_to_lines(lines, **kwargs) - assert result["resume_when"] == expected - - -def test_compatibility_resume_update_uses_the_whole_field_owner(monkeypatch): - from loopx.control_plane.todos import line_update, resume_condition - - actual = line_update.effect_runtime_result - calls = [] - - def traced(method, params): - calls.append(method) - return actual(method, params) - - for module in (line_update, resume_condition): - monkeypatch.setattr(module, "effect_runtime_result", traced) - result = apply_todo_update_to_lines( - _lines(), todo_id="todo_field_test", updated_at=AT, - resume_when="TODO_DONE:todo_dependency", - ) - assert result["resume_when"] == "todo_done:todo_dependency" - assert calls == ["todo.field_update.plan"] - - -@pytest.mark.parametrize("exclusions", [[], ["!!!"]]) -def test_removed_policy_repair_requires_a_valid_exclusion(exclusions): - lines = _lines("continuation_policy=primary_review") - before = deepcopy(lines) - with pytest.raises(ValueError, match="repair it explicitly"): - apply_todo_update_to_lines( - lines, - todo_id="todo_field_test", - updated_at=AT, - continuation_policy="independent_handoff", - excluded_agents=exclusions, - ) - assert lines == before - - -def test_field_planner_covers_the_existing_monitor_codec_fields(): - from loopx.control_plane.effect_runtime import effect_runtime_result - from loopx.control_plane.todos.contract import TODO_MONITOR_METADATA_FIELDS - - clears = dict.fromkeys(TODO_MONITOR_METADATA_FIELDS) - plan = effect_runtime_result( - "todo.field_update.plan", - { - "schema_version": "loopx_todo_field_update_request_v0", - "todo": {"todo_id": "todo_field_test", "status": "open"}, - "intent": {"monitor_metadata": clears}, - "updated_at": AT, - }, - ) - assert plan["metadata_updates"] == { - "todo_id": "todo_field_test", - "status": "open", - **clears, - } diff --git a/tests/control_plane/test_todo_lifecycle_authority_config.py b/tests/control_plane/test_todo_lifecycle_authority_config.py index 8e55b1197c..72c7ed9852 100644 --- a/tests/control_plane/test_todo_lifecycle_authority_config.py +++ b/tests/control_plane/test_todo_lifecycle_authority_config.py @@ -5,8 +5,12 @@ import pytest +from canonical_authority_fixture import initialize_canonical_authority from loopx.cli import main from loopx.configure_goal import configure_goal +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.todos import add_goal_todo @@ -27,6 +31,7 @@ def _registry(tmp_path: Path) -> Path: registry.write_text( json.dumps( { + "common_runtime_root": str(tmp_path / "runtime"), "goals": [ { "id": GOAL_ID, @@ -45,6 +50,16 @@ def _registry(tmp_path: Path) -> Path: ), encoding="utf-8", ) + initialize_canonical_authority( + tmp_path / "runtime", + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[], + handoff_mode="soft_claim", + ), + state_path=tmp_path / "ACTIVE_GOAL_STATE.md", + ) return registry @@ -225,9 +240,8 @@ def test_todo_cli_emits_delegated_override_receipt( assert exit_code == 0 payload = json.loads(capsys.readouterr().out) - assert payload["mutation_authority"]["mode"] == ( + assert payload["mutation_authority"]["authority_mode"] == ( "delegated_orchestration_override" ) - assert payload["mutation_authority"]["authority_reason"] == ( - "Verified the result before orchestration closeout." - ) + assert payload["mutation_authority"]["code"] == "terminal_transition" + assert payload["source_authority"] == "file_v0" diff --git a/tests/control_plane/test_todo_mutation_authority.py b/tests/control_plane/test_todo_mutation_authority.py index e65263948b..011047b77b 100644 --- a/tests/control_plane/test_todo_mutation_authority.py +++ b/tests/control_plane/test_todo_mutation_authority.py @@ -5,22 +5,20 @@ import pytest -from loopx.cli import main as cli_main +from canonical_authority_fixture import initialize_canonical_authority +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from loopx.control_plane.coordination.local_authority import ( + LocalCoordinationAuthorityRejection, +) from loopx.control_plane.scheduler.monitor_poll_writeback import ( write_monitor_poll_todo_state, ) -import loopx.control_plane.todos.mutation_authority as mutation_authority_module -import loopx.control_plane.work_items.task_lease as task_lease_module -from loopx.control_plane.work_items.task_lease import ( - TaskLeaseError, - acquire_task_lease, - release_task_lease, -) from loopx.status import parse_active_state_todos from loopx.todos import ( add_goal_todo, complete_goal_todo, - supersede_goal_todo, update_goal_todo, ) @@ -36,6 +34,7 @@ def _write_fixture( *, multi_agent: bool = True, lifecycle_authority: list[dict] | None = None, + handoff_mode: str = "soft_claim", ) -> tuple[Path, Path]: repo = tmp_path / "repo" repo.mkdir() @@ -86,13 +85,19 @@ def _write_fixture( ), encoding="utf-8", ) + initialize_canonical_authority( + tmp_path / "runtime", + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[], + handoff_mode=handoff_mode, + ), + state_path=state, + ) return registry, state -def _lease_path(tmp_path: Path, todo_id: str) -> Path: - return tmp_path / "runtime" / "goals" / GOAL_ID / "task-leases" / f"{todo_id}.json" - - def _agent_todo(state: Path, todo_id: str) -> dict: todos = parse_active_state_todos(state.read_text(encoding="utf-8")) return next( @@ -196,83 +201,6 @@ def test_continuous_monitor_accepts_resume_condition_bound(tmp_path: Path) -> No assert item["resume_when"] == "pr_merged:#3083" -def test_multi_agent_update_requires_actor_and_is_atomic(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo(registry) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="requires --agent-id"): - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - note="This must not be written without an attributed actor.", - ) - - assert state.read_text(encoding="utf-8") == before - - -def test_registered_actor_can_correct_unclaimed_todo_without_claim(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo(registry, claimed_by=None) - result = update_goal_todo( - registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, text="Correct unclaimed copy", note="Correct note", - ) - assert result["mutation_authority"]["mode"] == "registered_peer_actor" - corrected = _agent_todo(state, todo["todo_id"]) - assert corrected["text"] == "Correct unclaimed copy" - assert corrected["note"] == "Correct note" - assert not corrected.get("claimed_by") - - -def test_excluded_actor_cannot_mutate_unclaimed_todo(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo( - registry, - claimed_by=None, - excluded_agents=[AUTHOR_AGENT], - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="is excluded from mutating"): - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - note="An excluded author cannot rewrite review work.", - ) - - assert state.read_text(encoding="utf-8") == before - - -def test_idempotent_add_preserves_existing_executor_exclusions( - tmp_path: Path, -) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo( - registry, - text="Independently review one exact PR head.", - claimed_by=None, - excluded_agents=[AUTHOR_AGENT], - ) - - replay = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Independently review one exact PR head.", - task_class="advancement_task", - ) - - assert replay["todo_id"] == todo["todo_id"] - assert replay["already_exists"] is True - assert replay["metadata_updated"] is False - assert replay["excluded_agents"] == [AUTHOR_AGENT] - assert _agent_todo(state, todo["todo_id"])["excluded_agents"] == [AUTHOR_AGENT] - - def test_unresolved_decision_scope_is_not_a_local_claim_gate(tmp_path: Path) -> None: """Characterize the local writer before shared revision publishers exist. @@ -314,44 +242,6 @@ def test_unresolved_decision_scope_is_not_a_local_claim_gate(tmp_path: Path) -> assert item["claimed_by"] == AUTHOR_AGENT -@pytest.mark.parametrize("command", ["update", "complete", "supersede"]) -def test_non_owner_cannot_mutate_claimed_todo( - tmp_path: Path, - command: str, -) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo(registry, claimed_by=REVIEW_AGENT) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="claimed_by='codex-review'"): - if command == "update": - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - note="unauthorized", - ) - elif command == "complete": - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - evidence="unauthorized", - ) - else: - supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - reason="unauthorized", - ) - - assert state.read_text(encoding="utf-8") == before - - def test_completion_policy_unicode_parity_through_public_facade( tmp_path: Path, ) -> None: @@ -402,13 +292,13 @@ def test_completion_policy_unicode_parity_through_public_facade( evidence="\ufeff", no_followup=True, ) - assert accepted["self_merged"] is True + assert accepted["completion_policy"]["self_merged"] is True def test_completion_policy_errors_do_not_preempt_actor_or_lease_fences( tmp_path: Path, ) -> None: - registry, state = _write_fixture(tmp_path) + registry, state = _write_fixture(tmp_path, handoff_mode="hard_lease") claimed_by_other = _add_agent_todo( registry, text="Keep actor authority ahead of policy diagnostics.", @@ -416,7 +306,7 @@ def test_completion_policy_errors_do_not_preempt_actor_or_lease_fences( ) before = state.read_text(encoding="utf-8") - with pytest.raises(ValueError, match="claimed_by='codex-review'"): + with pytest.raises(LocalCoordinationAuthorityRejection) as denied: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -426,251 +316,57 @@ def test_completion_policy_errors_do_not_preempt_actor_or_lease_fences( evidence="\u0085", no_followup=True, ) + assert denied.value.code == "claim_owner_mismatch" assert state.read_text(encoding="utf-8") == before leased = _add_agent_todo( registry, text="Keep the task lease ahead of policy diagnostics.", - ) - lease_key = "policy-priority-instance" - acquire_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=leased["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key=lease_key, - ttl_seconds=600, + claimed_by=None, ) before = state.read_text(encoding="utf-8") - with pytest.raises(TaskLeaseError) as stale_lease: + with pytest.raises(LocalCoordinationAuthorityRejection) as missing_lease: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=leased["todo_id"], agent_id=AUTHOR_AGENT, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=0, self_merged=True, evidence="\u0085", no_followup=True, ) - assert stale_lease.value.code == "version_mismatch" - assert state.read_text(encoding="utf-8") == before - - -def test_owner_actor_update_returns_typed_receipt(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo(registry) - - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - note="owner-attributed update", - ) - - assert result["mutation_authority"] == { - "schema_version": "todo_mutation_authority_v0", - "command": "update", - "mode": "registered_peer_actor", - "actor_agent_id": AUTHOR_AGENT, - "todo_id": todo["todo_id"], - "claim_owner": AUTHOR_AGENT, - "registered_agent_count": 3, - } - assert _agent_todo(state, todo["todo_id"])["note"] == "owner-attributed update" - - -@pytest.mark.parametrize("status", ["deferred", "blocked", "done"]) -def test_claim_rejection_preserves_status_suffix( - tmp_path: Path, - status: str, -) -> None: - """Claim rejections keep the legacy todo_id/status diagnostic suffix.""" - - registry, state = _write_fixture(tmp_path) - if status == "deferred": - todo = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Wait for external capacity before claim.", - status="deferred", - resume_when="capacity_available:network", - ) - else: - todo = _add_agent_todo(registry) - if status == "done": - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - evidence="finished before the contested claim", - ) - else: - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - status=status, - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError) as excinfo: - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claim_only=True, - claimed_by=AUTHOR_AGENT, - agent_id=AUTHOR_AGENT, - ) - - assert str(excinfo.value) == ( - f"todo claim requires status=open; todo_id '{todo['todo_id']}' " - f"is status='{status}'" - ) + assert missing_lease.value.code == "handoff_mode_requires_lease" assert state.read_text(encoding="utf-8") == before -def test_claim_user_todo_rejection_preserves_binding_hint( - tmp_path: Path, -) -> None: - """Claiming a user Todo keeps the legacy --bound-agent remediation hint.""" - +def test_capability_binding_cannot_be_rebound_by_duplicate_add(tmp_path: Path) -> None: registry, state = _write_fixture(tmp_path) - todo = add_goal_todo( + original = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, - role="user", - text="Owner reviews the delivered change.", - task_class="user_action", - bound_agent=AUTHOR_AGENT, - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError) as excinfo: - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claim_only=True, - claimed_by=AUTHOR_AGENT, - agent_id=AUTHOR_AGENT, - ) - - assert str(excinfo.value) == ( - "claimed_by is execution ownership for agent todos, not a user-todo " - "binding; use --bound-agent or --goal-bound" - ) - assert state.read_text(encoding="utf-8") == before - - -def test_claim_rejection_preserves_removed_policy_repair_hint( - tmp_path: Path, -) -> None: - """Claiming a removed-policy Todo keeps the legacy repair instructions.""" - - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo(registry, claimed_by=None) - text = state.read_text(encoding="utf-8") - marker = f"todo_id={todo['todo_id']} " - assert marker in text - state.write_text( - text.replace( - marker, - f"{marker}removed_continuation_policy=primary_review ", - 1, - ), - encoding="utf-8", + role="agent", + text="Advance the admitted issue-fix route.", + task_class="advancement_task", + capability_binding_ref="issue-fix:feasibility-a1b2c3d4", ) before = state.read_text(encoding="utf-8") - with pytest.raises(ValueError) as excinfo: - update_goal_todo( + with pytest.raises( + Exception, + match="different capability_binding_ref", + ): + add_goal_todo( registry_path=registry, goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claim_only=True, - claimed_by=AUTHOR_AGENT, - agent_id=AUTHOR_AGENT, + role="agent", + text="Advance the admitted issue-fix route.", + task_class="advancement_task", + capability_binding_ref="issue-fix:feasibility-e5f6a7b8", ) - assert str(excinfo.value) == ( - f"todo_id '{todo['todo_id']}' uses removed continuation_policy=" - "primary_review; repair it before claiming" - ) assert state.read_text(encoding="utf-8") == before - - -def test_claim_archived_todo_rejection_preserves_not_found_message( - tmp_path: Path, -) -> None: - """Archived-Todo claim rejections keep the legacy not-found message.""" - - registry, _state = _write_fixture(tmp_path) - - with pytest.raises(ValueError) as excinfo: - mutation_authority_module.authorize_todo_lifecycle_mutation( - registry_path=registry, - goal_id=GOAL_ID, - command="claim", - todo={ - "todo_id": "todo_archivedprobe", - "role": "agent", - "status": "open", - "archive_state": "archive", - }, - actor_agent_id=AUTHOR_AGENT, - requested_claimed_by=AUTHOR_AGENT, - ) - - assert str(excinfo.value) == ( - "todo_id 'todo_archivedprobe' was not found in active user or agent todos" - ) - - -def test_claim_role_mismatch_rejection_preserves_not_found_message( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Role-mismatch claim rejections keep the legacy not-found message.""" - - registry, _state = _write_fixture(tmp_path) - - def fake_typescript_decision(_command: str, _payload: dict) -> dict: - return { - "status": "rejected", - "reason_code": "todo_role_mismatch", - "reason": "Todo does not have the requested role", - } - - monkeypatch.setattr( - mutation_authority_module, - "effect_runtime_result", - fake_typescript_decision, - ) - - with pytest.raises(ValueError) as excinfo: - mutation_authority_module.authorize_todo_lifecycle_mutation( - registry_path=registry, - goal_id=GOAL_ID, - command="claim", - todo={ - "todo_id": "todo_rolemismatchprobe", - "role": "agent", - "status": "open", - }, - actor_agent_id=AUTHOR_AGENT, - requested_claimed_by=AUTHOR_AGENT, - ) - - assert str(excinfo.value) == ( - "todo_id 'todo_rolemismatchprobe' was not found in active user or agent todos" + assert _agent_todo(state, original["todo_id"])["capability_binding_ref"] == ( + "issue-fix:feasibility-a1b2c3d4" ) @@ -722,870 +418,75 @@ def test_capability_binding_follows_generated_agent_successor(tmp_path: Path) -> assert successor["capability_binding_ref"] == ("issue-fix:feasibility-a1b2c3d4") -def test_active_task_lease_fences_same_agent_completion_instance( +def test_duplicate_todo_add_rejects_invalid_task_domain_atomically( tmp_path: Path, ) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - lease_key = "turn-owner-instance" - acquire_task_lease( + registry, state = _write_fixture(tmp_path) + todo = add_goal_todo( registry_path=registry, - runtime_root=tmp_path / "runtime", goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key=lease_key, - ttl_seconds=600, + role="agent", + text="Validate one adaptive lane.", + task_class="advancement_task", + task_domain="validation", ) before = state.read_text(encoding="utf-8") - with pytest.raises(TaskLeaseError) as missing_fence: - complete_goal_todo( + with pytest.raises(ValueError, match="task_domain"): + add_goal_todo( registry_path=registry, goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - evidence="stale execution instance", - next_agent_todo="Create a stale successor.", + role="agent", + text="Validate one adaptive lane.", + task_class="advancement_task", + task_domain="../private", ) - assert missing_fence.value.code == "lease_fence_required" - assert state.read_text(encoding="utf-8") == before - with pytest.raises(TaskLeaseError) as mismatched_fence: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key="other-instance", - evidence="wrong execution instance", - ) - assert mismatched_fence.value.code == "lease_cas_mismatch" assert state.read_text(encoding="utf-8") == before + assert _agent_todo(state, todo["todo_id"])["task_domain"] == "validation" - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=1, - evidence="lease owner validated the result", - next_agent_todo="Create the canonical successor.", - ) - assert completed["task_lease_fence"] == { - "schema_version": "task_lease_v0", - "required": True, - "active": True, - "owner": AUTHOR_AGENT, - "version": 1, - "lease_epoch": 1, - "execution_instance_verified": True, - "released": True, - } - assert ( - json.loads(_lease_path(tmp_path, todo["todo_id"]).read_text(encoding="utf-8"))[ - "status" - ] - == "released" - ) - replayed = complete_goal_todo( +def test_todo_update_rejects_invalid_task_domain_atomically(tmp_path: Path) -> None: + registry, state = _write_fixture(tmp_path) + todo = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, - todo_id=todo["todo_id"], + role="agent", + text="Validate one adaptive lane.", + task_class="advancement_task", + task_domain="validation", claimed_by=AUTHOR_AGENT, - evidence="late result from another execution instance", - next_agent_todo="Create a duplicate stale successor.", - ) - assert replayed["idempotent_replay"] is True - assert replayed["changed"] is False - agent_todos = parse_active_state_todos(state.read_text(encoding="utf-8"))[ - "agent_todos" - ]["items"] - todo_titles = [ - str(item.get("title") or item.get("text") or "") for item in agent_todos - ] - assert todo_titles.count("Create the canonical successor.") == 1 - assert "Create a duplicate stale successor." not in todo_titles - - -def test_released_lease_generation_rejects_stale_completion_fences( - tmp_path: Path, -) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - runtime_root = tmp_path / "runtime" - first = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key="execution-one", - ttl_seconds=600, - ) - assert first["lease"]["version"] == 1 - assert first["lease"]["lease_epoch"] == 1 - - released = release_task_lease( - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key="execution-one", - expected_version=1, - registry_path=registry, ) - assert released["lease"]["status"] == "released" - assert _lease_path(tmp_path, todo["todo_id"]).exists() + before = state.read_text(encoding="utf-8") - with pytest.raises(TaskLeaseError) as reused_execution: - acquire_task_lease( + with pytest.raises(ValueError, match="task_domain"): + update_goal_todo( registry_path=registry, - runtime_root=runtime_root, goal_id=GOAL_ID, todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key="execution-one", - ttl_seconds=600, + task_domain="../private", + agent_id=AUTHOR_AGENT, ) - assert reused_execution.value.code == "idempotency_key_reuse" - second = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key="execution-two", - ttl_seconds=600, - ) - assert second["lease"]["version"] == 2 - assert second["lease"]["lease_epoch"] == 2 - before = state.read_text(encoding="utf-8") + assert state.read_text(encoding="utf-8") == before + assert _agent_todo(state, todo["todo_id"])["task_domain"] == "validation" - with pytest.raises(TaskLeaseError) as old_key: - complete_goal_todo( + +def test_monitor_schedule_fields_remain_monitor_only(tmp_path: Path) -> None: + registry, _state = _write_fixture(tmp_path) + + with pytest.raises(ValueError, match="monitor schedule metadata requires"): + add_goal_todo( registry_path=registry, goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key="execution-one", - task_lease_expected_version=1, - evidence="stale first execution", - no_followup=True, - ) - assert old_key.value.code == "lease_cas_mismatch" - assert state.read_text(encoding="utf-8") == before - - with pytest.raises(TaskLeaseError) as old_version: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key="execution-two", - task_lease_expected_version=1, - evidence="stale version from the first generation", - no_followup=True, - ) - assert old_version.value.code == "version_mismatch" - assert state.read_text(encoding="utf-8") == before - - with pytest.raises(TaskLeaseError) as missing_version: - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key="execution-two", - evidence="incomplete fence", - no_followup=True, - ) - assert missing_version.value.code == "version_required" - assert state.read_text(encoding="utf-8") == before - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key="execution-two", - task_lease_expected_version=2, - evidence="current generation completed", - no_followup=True, - ) - assert completed["task_lease_fence"]["lease_epoch"] == 2 - tombstone = json.loads( - _lease_path(tmp_path, todo["todo_id"]).read_text(encoding="utf-8") - ) - assert tombstone["status"] == "released" - assert tombstone["version"] == 2 - assert tombstone["lease_epoch"] == 2 - - -def test_completion_turn_key_rejects_cross_turn_replay(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo(registry) - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - evidence="turn A validated the selected Todo", - completion_turn_key="turn-a", - no_followup=True, - ) - assert completed["completed"] is True - completed_todo = _agent_todo(state, todo["todo_id"]) - assert completed_todo["completion_turn_key"] == "turn-a" - assert completed_todo["completion_continuation"] == "no_followup" - completed_state = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="different completion_turn_key"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - evidence="turn B must not claim turn A's completion", - completion_turn_key="turn-b", - no_followup=True, - ) - assert state.read_text(encoding="utf-8") == completed_state - - replayed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - evidence="turn A retried after its durable write", - completion_turn_key="turn-a", - no_followup=True, - ) - assert replayed["idempotent_replay"] is True - assert replayed["changed"] is False - - -def test_terminal_upgrade_cannot_replace_existing_successor(tmp_path: Path) -> None: - registry, _state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - completion_turn_key="turn-a", - evidence="ordinary completion created the successor", - next_agent_todo="Continue the durable successor.", - ) - assert completed["changed"] is True - assert completed["completion_continuation"] == "successor" - - with pytest.raises(ValueError, match="cannot replace an existing successor"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - completion_turn_key="turn-a", - evidence="terminal closeout must not erase the successor", - no_followup=True, - ) - - -def test_terminal_upgrade_requires_original_completion_turn_key(tmp_path: Path) -> None: - registry, _state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - completion_turn_key="turn-a", - evidence="ordinary completion from the original turn", - ) - assert completed["changed"] is True - assert completed["completion_continuation"] == "active_goal" - - with pytest.raises(ValueError, match="requires the original completion_turn_key"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - evidence="an unscoped caller cannot claim terminal closeout", - no_followup=True, - ) - - -def test_terminal_upgrade_rejects_untyped_legacy_completion_until_repaired( - tmp_path: Path, -) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - todo_id = todo["todo_id"] - - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo_id, - claimed_by=AUTHOR_AGENT, - completion_turn_key="turn-a", - evidence="ordinary completion from the original turn", - ) - state.write_text( - state.read_text(encoding="utf-8").replace( - " completion_continuation=active_goal", - "", - ), - encoding="utf-8", - ) - - with pytest.raises(ValueError, match="missing completion_continuation"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo_id, - claimed_by=AUTHOR_AGENT, - completion_turn_key="turn-a", - evidence="legacy ambiguity must not be inferred", - no_followup=True, - ) - - repaired = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo_id, - claimed_by=AUTHOR_AGENT, - completion_turn_key="turn-a", - evidence="repair explicit completion continuation", - note="repair explicit completion continuation", - ) - assert repaired["completion_continuation"] == "active_goal" - - upgraded = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo_id, - claimed_by=AUTHOR_AGENT, - completion_turn_key="turn-a", - evidence="same-turn terminal closeout after explicit repair", - no_followup=True, - ) - assert upgraded["completion_continuation"] == "no_followup" - assert upgraded["completion_recovery"] == "same_turn_terminal_closeout" - - - - -def test_unfenced_completion_leaves_no_lease_artifacts(tmp_path: Path) -> None: - registry, _state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - evidence="no lease was ever taken", - no_followup=True, - ) - - assert completed["ok"] is True - assert completed["task_lease_fence"] == { - "schema_version": "task_lease_v0", - "required": False, - "active": False, - } - lease_dir = _lease_path(tmp_path, todo["todo_id"]).parent - assert list(lease_dir.glob("todo_*.json")) == [] - - -def test_divergent_claim_completion_leaves_foreign_lease_untouched( - tmp_path: Path, -) -> None: - registry, _state = _write_fixture(tmp_path) - todo = _add_agent_todo(registry, claimed_by=None) - acquire_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key="author-instance", - ttl_seconds=600, - ) - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=REVIEW_AGENT, - agent_id=REVIEW_AGENT, - claim_only=True, - ) - lease_path = _lease_path(tmp_path, todo["todo_id"]) - lease_before = lease_path.read_text(encoding="utf-8") - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=REVIEW_AGENT, - agent_id=REVIEW_AGENT, - evidence="split-brain claimer completed without the lease key", - no_followup=True, - ) - - assert completed["ok"] is True - assert completed["task_lease_fence"] == { - "schema_version": "task_lease_v0", - "required": False, - "active": False, - } - assert lease_path.read_text(encoding="utf-8") == lease_before - assert json.loads(lease_before)["owner"] == AUTHOR_AGENT - - -def test_dry_run_completion_does_not_release_lease(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - lease_key = "dry-run-instance" - acquire_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key=lease_key, - ttl_seconds=600, - ) - before = state.read_text(encoding="utf-8") - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=1, - evidence="dry run must not touch the lease", - no_followup=True, - dry_run=True, - ) - - assert completed["dry_run"] is True - assert completed["task_lease_fence"]["execution_instance_verified"] is True - assert "released" not in completed["task_lease_fence"] - lease_path = _lease_path(tmp_path, todo["todo_id"]) - assert json.loads(lease_path.read_text(encoding="utf-8"))["status"] == "active" - assert state.read_text(encoding="utf-8") == before - - -def test_release_failure_after_commit_keeps_completion_ok( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, _state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - lease_key = "release-failure-instance" - acquire_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key=lease_key, - ttl_seconds=600, - ) - - native_lifecycle = task_lease_module._execute_native_task_lease_lifecycle - - def _fail_release(**kwargs: object) -> dict: - if ( - kwargs.get("operation") == "fence_close" - and kwargs.get("committed") is True - and kwargs.get("release_lease") is True - ): - native_lifecycle( - **{ - **kwargs, - "committed": False, - "release_lease": False, - } - ) - raise OSError("simulated terminal write failure") - return native_lifecycle(**kwargs) - - monkeypatch.setattr( - task_lease_module, - "_execute_native_task_lease_lifecycle", - _fail_release, - ) - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=1, - evidence="completion must survive a failed lease release", - no_followup=True, - ) - - assert completed["ok"] is True - assert completed["completed"] is True - assert completed["task_lease_fence"]["execution_instance_verified"] is True - assert completed["task_lease_fence"]["released"] is False - lease_path = _lease_path(tmp_path, todo["todo_id"]) - assert json.loads(lease_path.read_text(encoding="utf-8"))["status"] == "active" - - -def test_reopened_todo_completes_unfenced_after_lease_release( - tmp_path: Path, -) -> None: - """Pin the accepted reopen-window divergence of release-on-completion. - - The terminal tombstone retains the generation but is not time-active, so a - manually reopened todo completes unfenced. The tombstone exists only to - prevent a later acquire from reusing the old execution generation. - """ - - registry, _state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - lease_key = "reopen-window-instance" - acquire_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key=lease_key, - ttl_seconds=600, - ) - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=1, - evidence="first completion releases the verified lease", - no_followup=True, - ) - assert completed["task_lease_fence"]["released"] is True - assert ( - json.loads(_lease_path(tmp_path, todo["todo_id"]).read_text(encoding="utf-8"))[ - "status" - ] - == "released" - ) - - reopened = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - status="open", - note="manual reopen inside the old lease TTL", - ) - assert reopened["ok"] is True - - recompleted = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - evidence="reopened todo completes without a lease fence", - no_followup=True, - ) - assert recompleted["ok"] is True - assert recompleted["task_lease_fence"] == { - "schema_version": "task_lease_v0", - "required": False, - "active": False, - } - - -def test_cli_release_after_auto_release_replays_terminal_tombstone( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: - """The public CLI replays the retained terminal result, not `missing`.""" - - registry, _state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - lease_key = "double-release-instance" - acquire_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key=lease_key, - ttl_seconds=600, - ) - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=1, - evidence="completion already released the lease", - no_followup=True, - ) - lease_path = _lease_path(tmp_path, todo["todo_id"]) - tombstone = json.loads(lease_path.read_text(encoding="utf-8")) - - exit_code = cli_main( - [ - "--registry", - str(registry), - "--format", - "json", - "task-lease", - "release", - "--goal-id", - GOAL_ID, - "--todo-id", - todo["todo_id"], - "--owner", - AUTHOR_AGENT, - "--idempotency-key", - lease_key, - "--expected-version", - "1", - ] - ) - released = json.loads(capsys.readouterr().out) - - assert exit_code == 0 - assert released["ok"] is True - assert released["action"] == "release" - assert released["released"] is True - assert released["idempotent"] is True - assert released["lease"] == tombstone - assert released["lease"]["status"] == "released" - assert released["lease"]["version"] == 1 - assert released["lease"]["lease_epoch"] == 1 - assert "missing" not in released - assert json.loads(lease_path.read_text(encoding="utf-8")) == tombstone - - -def test_exception_after_verified_fence_leaves_lease_intact( - tmp_path: Path, -) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - lease_key = "post-fence-failure-instance" - acquire_task_lease( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - owner=AUTHOR_AGENT, - idempotency_key=lease_key, - ttl_seconds=600, - ) - lease_path = _lease_path(tmp_path, todo["todo_id"]) - lease_before = lease_path.read_text(encoding="utf-8") - state_before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="successor_todo_ids"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=1, - successor_todo_ids=["!!not-a-todo-id!!"], - evidence="fails after the fence verified", - no_followup=True, - ) - - assert lease_path.read_text(encoding="utf-8") == lease_before - assert json.loads(lease_before)["status"] == "active" - assert state.read_text(encoding="utf-8") == state_before - - - - - - - - -def test_capability_binding_cannot_be_rebound_by_duplicate_add(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Advance the admitted issue-fix route.", - task_class="advancement_task", - capability_binding_ref="issue-fix:feasibility-a1b2c3d4", - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="immutable once set"): - add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Advance the admitted issue-fix route.", - task_class="advancement_task", - capability_binding_ref="issue-fix:feasibility-e5f6a7b8", - ) - - assert state.read_text(encoding="utf-8") == before - - - - - - - - - - - - -def test_duplicate_todo_add_rejects_invalid_task_domain_atomically( - tmp_path: Path, -) -> None: - registry, state = _write_fixture(tmp_path) - todo = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Validate one adaptive lane.", - task_class="advancement_task", - task_domain="validation", - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="task_domain"): - add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Validate one adaptive lane.", - task_class="advancement_task", - task_domain="../private", - ) - - assert state.read_text(encoding="utf-8") == before - assert _agent_todo(state, todo["todo_id"])["task_domain"] == "validation" - - -def test_todo_update_rejects_invalid_task_domain_atomically(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - todo = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Validate one adaptive lane.", - task_class="advancement_task", - task_domain="validation", - claimed_by=AUTHOR_AGENT, - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="task_domain"): - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - task_domain="../private", - agent_id=AUTHOR_AGENT, - ) - - assert state.read_text(encoding="utf-8") == before - assert _agent_todo(state, todo["todo_id"])["task_domain"] == "validation" - - -def test_monitor_schedule_fields_remain_monitor_only(tmp_path: Path) -> None: - registry, _state = _write_fixture(tmp_path) - - with pytest.raises(ValueError, match="monitor schedule metadata requires"): - add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Do not attach cadence to advancement work.", - task_class="advancement_task", - monitor_metadata={ - "target_key": "issue-fix:owner/repo:issue_42", - "cadence": "15m", - }, - ) - - -def test_claim_actor_must_match_requested_owner(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo(registry, claimed_by=None) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="requires --claimed-by to match"): - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - agent_id=REVIEW_AGENT, - claim_only=True, - ) - assert state.read_text(encoding="utf-8") == before - - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - agent_id=AUTHOR_AGENT, - claim_only=True, - ) - assert result["mutation_authority"]["command"] == "claim" - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AUTHOR_AGENT - - -def test_monitor_writeback_propagates_multi_agent_actor(tmp_path: Path) -> None: - registry, _state = _write_fixture(tmp_path) - todo = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Poll one public monitor target.", - task_class="continuous_monitor", - claimed_by=AUTHOR_AGENT, - monitor_metadata={ - "target_key": "public-pr:42", - "cadence": "15m", - "watch_only": "true", - }, - ) - - result = write_monitor_poll_todo_state( - registry_path=registry, - runtime_root=tmp_path / "runtime", - goal_id=GOAL_ID, - generated_at="2026-07-18T00:15:00+00:00", - execute=False, - todo_id=todo["todo_id"], - result_hash="unchanged", - agent_id=AUTHOR_AGENT, - ) - - assert result is not None - authority = result["todo_update"]["mutation_authority"] - assert authority["mode"] == "registered_peer_actor" - assert authority["actor_agent_id"] == AUTHOR_AGENT + role="agent", + text="Do not attach cadence to advancement work.", + task_class="advancement_task", + monitor_metadata={ + "target_key": "issue-fix:owner/repo:issue_42", + "cadence": "15m", + }, + ) def test_monitor_successor_replay_preserves_independent_reviewer_exclusion( @@ -1643,15 +544,13 @@ def test_monitor_successor_replay_preserves_independent_reviewer_exclusion( assert result is not None assert result["successor_receipts"][0]["todo_id"] == review["todo_id"] - assert _agent_todo(state, review["todo_id"])["excluded_agents"] == [ - AUTHOR_AGENT - ] + assert _agent_todo(state, review["todo_id"])["excluded_agents"] == [AUTHOR_AGENT] def test_exact_user_gate_decision_scope_uses_controller_override( tmp_path: Path, ) -> None: - registry, _state = _write_fixture(tmp_path) + registry, state = _write_fixture(tmp_path) target = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1682,193 +581,11 @@ def test_exact_user_gate_decision_scope_uses_controller_override( ) authority = result["mutation_authority"] - assert authority["mode"] == "exact_user_gate_decision_scope_override" - assert authority["actor_agent_id"] is None - assert authority["target_todo_id"] == target["todo_id"] - assert authority["decision_scope"]["scope_key"] == "publish_release" - - -def test_exact_user_gate_ignores_unrelated_malformed_lifecycle_grant( - tmp_path: Path, -) -> None: - registry, _state = _write_fixture( - tmp_path, - lifecycle_authority=[ - { - "agent_id": ORCHESTRATION_AGENT, - "actions": [], - } - ], - ) - target = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Publish the approved release.", - task_class="advancement_task", - claimed_by=AUTHOR_AGENT, - required_decision_scopes=[DECISION_SCOPE], - ) - gate = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="user", - text="Approve the exact release publication.", - task_class="user_gate", - blocks_agent=AUTHOR_AGENT, - decision_scope=DECISION_SCOPE, - unblocks_todo_id=target["todo_id"], - ) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - role="user", - decision_outcome="approve", - evidence="owner approved the exact decision scope", - ) - - assert result["mutation_authority"]["mode"] == ( - "exact_user_gate_decision_scope_override" - ) - - -def test_non_exact_user_gate_cannot_bypass_actor_attribution(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) - target = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Announce the approved release.", - task_class="advancement_task", - claimed_by=AUTHOR_AGENT, - required_decision_scopes=["direction:action:announce_release"], - ) - gate = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="user", - text="Approve a different action.", - task_class="user_gate", - blocks_agent=AUTHOR_AGENT, - decision_scope=DECISION_SCOPE, - unblocks_todo_id=target["todo_id"], - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="requires --agent-id"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - role="user", - decision_outcome="approve", - evidence="scope mismatch", - ) - - assert state.read_text(encoding="utf-8") == before - - -def test_single_agent_goal_keeps_lifecycle_compatibility(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) - todo = _add_agent_todo(registry) - - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - note="legacy single-agent update", - ) - - assert result["mutation_authority"]["mode"] == "single_agent_compatibility" - assert _agent_todo(state, todo["todo_id"])["note"] == "legacy single-agent update" - - -def test_single_agent_ignores_unrelated_malformed_lifecycle_grant( - tmp_path: Path, -) -> None: - registry, state = _write_fixture( - tmp_path, - multi_agent=False, - lifecycle_authority=[ - { - "agent_id": REVIEW_AGENT, - "actions": ["complete"], - } - ], - ) - todo = _add_agent_todo(registry) - - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - note="legacy single-agent update", - ) - - assert result["mutation_authority"]["mode"] == "single_agent_compatibility" - assert _agent_todo(state, todo["todo_id"])["note"] == "legacy single-agent update" - - -def test_current_owner_ignores_unrelated_malformed_lifecycle_grant( - tmp_path: Path, -) -> None: - registry, state = _write_fixture( - tmp_path, - lifecycle_authority=[ - { - "agent_id": ORCHESTRATION_AGENT, - "actions": ["unsupported-action"], - } - ], - ) - todo = _add_agent_todo(registry) - - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - note="owner-attributed update", - ) - - assert result["mutation_authority"]["mode"] == "registered_peer_actor" - assert _agent_todo(state, todo["todo_id"])["note"] == "owner-attributed update" - - -@pytest.mark.parametrize("command", ["complete", "supersede"]) -def test_author_cannot_replace_explicit_independent_review( - tmp_path: Path, - command: str, -) -> None: - registry, state = _write_fixture(tmp_path) - todo = _add_agent_todo( - registry, - claimed_by=REVIEW_AGENT, - excluded_agents=[AUTHOR_AGENT], - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="is excluded from mutating"): - if command == "complete": - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - evidence="author cannot self-review", - ) - else: - supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=AUTHOR_AGENT, - reason="author cannot replace review", - ) - - assert state.read_text(encoding="utf-8") == before + assert authority["authority_mode"] == "exact_user_gate_decision_scope_override" + assert authority["code"] == "terminal_transition" + assert _agent_todo(state, target["todo_id"]).get( + "required_decision_scopes", [] + ) == [] def test_delegated_orchestrator_can_complete_claimed_todo_with_reason( @@ -1895,130 +612,10 @@ def test_delegated_orchestrator_can_complete_claimed_todo_with_reason( evidence="validation://orchestrator-closeout", ) - assert result["mutation_authority"] == { - "schema_version": "todo_mutation_authority_v0", - "command": "complete", - "mode": "delegated_orchestration_override", - "actor_agent_id": ORCHESTRATION_AGENT, - "todo_id": todo["todo_id"], - "claim_owner": REVIEW_AGENT, - "authority_action": "complete", - "authority_source": "coordination.todo_lifecycle_authority", - "authority_reason": "Verified the worker result and closed the stalled lane.", - "requires_reason": True, - "registered_agent_count": 3, - } - assert _agent_todo(state, todo["todo_id"])["status"] == "done" - - -def test_delegated_orchestrator_requires_reason_before_state_write( - tmp_path: Path, -) -> None: - registry, state = _write_fixture( - tmp_path, - lifecycle_authority=[ - { - "agent_id": ORCHESTRATION_AGENT, - "actions": ["complete"], - "requires_reason": True, - } - ], - ) - todo = _add_agent_todo(registry, claimed_by=REVIEW_AGENT) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="requires --authority-reason"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=ORCHESTRATION_AGENT, - evidence="must remain atomic", - ) - - assert state.read_text(encoding="utf-8") == before - - -def test_delegated_orchestration_authority_is_action_scoped( - tmp_path: Path, -) -> None: - registry, state = _write_fixture( - tmp_path, - lifecycle_authority=[ - { - "agent_id": ORCHESTRATION_AGENT, - "actions": ["reassign"], - "requires_reason": True, - } - ], - ) - todo = _add_agent_todo(registry, claimed_by=REVIEW_AGENT) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="does not grant action='complete'"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=ORCHESTRATION_AGENT, - authority_reason="This grant only permits reassignment.", - evidence="unauthorized-action", - ) - assert state.read_text(encoding="utf-8") == before - - with pytest.raises(ValueError, match="does not grant action='update'"): - update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=ORCHESTRATION_AGENT, - claimed_by=AUTHOR_AGENT, - status="done", - authority_reason="A reassign grant cannot carry another mutation.", - ) - assert state.read_text(encoding="utf-8") == before - - result = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=ORCHESTRATION_AGENT, - claimed_by=AUTHOR_AGENT, - authority_reason="Move the stalled work to an available peer.", - ) - - assert result["mutation_authority"]["authority_action"] == "reassign" - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AUTHOR_AGENT - - -def test_delegated_override_never_bypasses_explicit_exclusion( - tmp_path: Path, -) -> None: - registry, state = _write_fixture( - tmp_path, - lifecycle_authority=[ - { - "agent_id": ORCHESTRATION_AGENT, - "actions": ["complete", "supersede"], - "requires_reason": True, - } - ], - ) - todo = _add_agent_todo( - registry, - claimed_by=REVIEW_AGENT, - excluded_agents=[ORCHESTRATION_AGENT], + authority = result["mutation_authority"] + assert authority["schema_version"] == ( + "loopx_coordination_todo_terminal_decision_result_v0" ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError, match="is excluded from mutating"): - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - agent_id=ORCHESTRATION_AGENT, - authority_reason="An explicit exclusion remains authoritative.", - evidence="must-not-write", - ) - - assert state.read_text(encoding="utf-8") == before + assert authority["authority_mode"] == "delegated_orchestration_override" + assert authority["code"] == "terminal_transition" + assert _agent_todo(state, todo["todo_id"])["status"] == "done" diff --git a/tests/control_plane/test_todo_next_action_settlement.py b/tests/control_plane/test_todo_next_action_settlement.py index 9a97b4bfa7..f5f27a3cad 100644 --- a/tests/control_plane/test_todo_next_action_settlement.py +++ b/tests/control_plane/test_todo_next_action_settlement.py @@ -3,22 +3,41 @@ import json from pathlib import Path +from canonical_authority_fixture import initialize_canonical_authority from loopx.bootstrap import render_state_markdown -from loopx.control_plane.effect_runtime import MAX_REQUEST_BYTES +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.control_plane.todos.next_action_runtime import ( _agent_todo_snapshots, apply_added_todo_next_action, reconcile_added_todo_next_action, ) -from loopx.control_plane.todos.projection import TODO_MISSING_PRIORITY_RANK, todo_priority_rank +from loopx.control_plane.todos.projection import ( + TODO_MISSING_PRIORITY_RANK, + todo_priority_rank, +) from loopx.state_projection import active_state_next_action_entries from loopx.state_refresh import build_state_refresh_record -from loopx.todos import add_goal_todo, complete_goal_todo, supersede_goal_todo +from loopx.todos import add_goal_todo, complete_goal_todo GOAL_ID = "todo-next-action-settlement" AGENT_ID = "codex-next-action" +def _initialize_canonical(tmp_path: Path, state: Path) -> None: + initialize_canonical_authority( + tmp_path / "runtime", + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[], + handoff_mode="soft_claim", + ), + state_path=state, + ) + + def test_agent_todo_snapshot_uses_structured_prefix_priority() -> None: snapshots = _agent_todo_snapshots( [ @@ -86,6 +105,7 @@ def _write_fixture(tmp_path: Path, *, next_action: str) -> tuple[Path, Path]: ), encoding="utf-8", ) + _initialize_canonical(tmp_path, state) return registry, state @@ -164,6 +184,7 @@ def test_agent_todo_add_after_bootstrap_keeps_default_next_action( ), encoding="utf-8", ) + _initialize_canonical(tmp_path, state) added = add_goal_todo( registry_path=registry, @@ -310,24 +331,30 @@ def reject_unexpected_transition(*_args, **_kwargs): ) lines = ["## Next Action", "", "- Preserve the existing route.", ""] - assert apply_added_todo_next_action( - lines, - role="agent", - add_result={ - "changed": False, - "todo_id": "todo_existing", - "task_class": "advancement_task", - }, - ) is False - assert apply_added_todo_next_action( - lines, - role="agent", - add_result={ - "changed": True, - "todo_id": "todo_monitor", - "task_class": "continuous_monitor", - }, - ) is True + assert ( + apply_added_todo_next_action( + lines, + role="agent", + add_result={ + "changed": False, + "todo_id": "todo_existing", + "task_class": "advancement_task", + }, + ) + is False + ) + assert ( + apply_added_todo_next_action( + lines, + role="agent", + add_result={ + "changed": True, + "todo_id": "todo_monitor", + "task_class": "continuous_monitor", + }, + ) + is True + ) def test_refresh_record_preserves_a_long_wrapped_next_action_losslessly( @@ -365,152 +392,6 @@ def test_refresh_record_preserves_a_long_wrapped_next_action_losslessly( assert "…" not in record["state"]["next_action"][0] -def test_complete_reprojects_typed_next_action_to_open_successor( - tmp_path: Path, -) -> None: - completed_text = "[P1] Validate the project connection." - registry, state = _write_fixture(tmp_path, next_action=completed_text) - completed = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text=completed_text, - task_class="advancement_task", - action_kind="project_connection_review", - claimed_by=AGENT_ID, - ) - successor = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="[P0] Implement and validate the requested behavior.", - task_class="advancement_task", - action_kind="implementation", - claimed_by=AGENT_ID, - ) - state.write_text( - state.read_text(encoding="utf-8").replace( - f"- {completed_text}\n", - f"- {completed_text}\n" - "\n", - 1, - ), - encoding="utf-8", - ) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(completed["todo_id"]), - successor_todo_ids=[str(successor["todo_id"])], - agent_id=AGENT_ID, - evidence="connection preflight passed", - ) - - assert result["changed"] is True - assert active_state_next_action_entries(state.read_text(encoding="utf-8")) == [ - "[P0] Implement and validate the requested behavior." - ] - - -def test_complete_projects_next_action_when_archived_state_exceeds_runtime_limit( - tmp_path: Path, -) -> None: - completed_text = "[P1] Finish the bounded implementation." - registry, state = _write_fixture(tmp_path, next_action=completed_text) - completed = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text=completed_text, - task_class="advancement_task", - action_kind="implementation", - claimed_by=AGENT_ID, - ) - successor = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="[P1] Validate the bounded implementation.", - task_class="advancement_task", - action_kind="validation_review", - claimed_by=AGENT_ID, - ) - archived_evidence = "x" * (MAX_REQUEST_BYTES + 1024) - state_text = state.read_text(encoding="utf-8").replace( - f"- {completed_text}\n", - f"- {completed_text}\n" - "\n", - 1, - ) - state.write_text( - f"{state_text}\n## Completed Work Archive\n\n{archived_evidence}\n", - encoding="utf-8", - ) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(completed["todo_id"]), - successor_todo_ids=[str(successor["todo_id"])], - agent_id=AGENT_ID, - evidence="implementation completed", - ) - - final_text = state.read_text(encoding="utf-8") - assert result["changed"] is True - assert active_state_next_action_entries(final_text) == [ - "[P1] Validate the bounded implementation." - ] - assert archived_evidence in final_text - - -def test_supersede_reprojects_after_generated_successor_is_materialized( - tmp_path: Path, -) -> None: - completed_text = "[P1] Replace the obsolete implementation." - registry, state = _write_fixture(tmp_path, next_action=completed_text) - completed = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text=completed_text, - task_class="advancement_task", - action_kind="implementation", - claimed_by=AGENT_ID, - ) - state.write_text( - state.read_text(encoding="utf-8").replace( - f"- {completed_text}\n", - f"- {completed_text}\n" - "\n", - 1, - ), - encoding="utf-8", - ) - - result = supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(completed["todo_id"]), - reason="replace the obsolete route", - next_agent_todo="[P1] Implement the corrected route.", - next_action_kind="implementation", - next_claimed_by=AGENT_ID, - agent_id=AGENT_ID, - ) - - successor_id = result["next_todos"][0]["todo_id"] - state_text = state.read_text(encoding="utf-8") - assert active_state_next_action_entries(state_text) == [ - "[P1] Implement the corrected route." - ] - assert f"todo_id={successor_id} -->" in state_text - - def test_complete_preserves_unrelated_owner_next_action(tmp_path: Path) -> None: registry, state = _write_fixture( tmp_path, @@ -539,121 +420,3 @@ def test_complete_preserves_unrelated_owner_next_action(tmp_path: Path) -> None: assert active_state_next_action_entries(state.read_text(encoding="utf-8")) == [ "Keep the owner-approved release route unchanged." ] - - -def test_complete_waits_for_generated_successor_before_reprojecting( - tmp_path: Path, -) -> None: - completed_text = "[P0] Finish the bounded implementation." - registry, state = _write_fixture(tmp_path, next_action=completed_text) - completed = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text=completed_text, - task_class="advancement_task", - action_kind="implementation", - claimed_by=AGENT_ID, - ) - state.write_text( - state.read_text(encoding="utf-8").replace( - f"- {completed_text}\n", - f"- {completed_text}\n" - "\n", - 1, - ), - encoding="utf-8", - ) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(completed["todo_id"]), - next_agent_todo="[P1] Review and validate the implementation.", - next_action_kind="validation_review", - next_claimed_by=AGENT_ID, - agent_id=AGENT_ID, - evidence="implementation completed", - ) - - successor_id = result["next_todos"][0]["todo_id"] - state_text = state.read_text(encoding="utf-8") - assert active_state_next_action_entries(state_text) == [ - "[P1] Review and validate the implementation." - ] - assert f"todo_id={successor_id} -->" in state_text - - -def test_complete_migrates_legacy_exact_text_next_action(tmp_path: Path) -> None: - completed_text = "[P1] Validate the legacy project connection." - registry, state = _write_fixture(tmp_path, next_action=completed_text) - completed = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text=completed_text, - task_class="advancement_task", - action_kind="project_connection_review", - claimed_by=AGENT_ID, - ) - - result = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(completed["todo_id"]), - no_followup=True, - agent_id=AGENT_ID, - evidence="legacy connection preflight passed", - ) - - assert result["changed"] is True - assert active_state_next_action_entries(state.read_text(encoding="utf-8")) == [] - - -def test_complete_upgrades_legacy_open_todo_to_typed_binding( - tmp_path: Path, -) -> None: - completed_text = "[P0] Finish the bound implementation." - registry, state = _write_fixture(tmp_path, next_action=completed_text) - completed = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text=completed_text, - task_class="advancement_task", - action_kind="implementation", - claimed_by=AGENT_ID, - ) - state_text = state.read_text(encoding="utf-8") - state_text = state_text.replace( - "## Next Action", - "- [ ] [P1] Legacy open work without metadata.\n\n## Next Action", - 1, - ).replace( - f"- {completed_text}\n", - f"- {completed_text}\n" - "\n", - 1, - ) - state.write_text(state_text, encoding="utf-8") - - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(completed["todo_id"]), - no_followup=True, - agent_id=AGENT_ID, - evidence="implementation completed", - ) - - final_text = state.read_text(encoding="utf-8") - assert active_state_next_action_entries(final_text) == [ - "[P1] Legacy open work without metadata." - ] - assert ( - " + """, encoding="utf-8", ) @@ -453,6 +466,16 @@ def _fixture(tmp_path: Path) -> tuple[Path, Path]: ), encoding="utf-8", ) + todos = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim" + ), + state_path=state, + provider=provider, + ) sidecar_dir = runtime / "goals" / GOAL_ID / "post_writeback_hooks" sidecar_dir.mkdir(parents=True) sidecar = sidecar_dir / ("pwh_" + "a" * 64 + ".json") @@ -1821,14 +1844,8 @@ def test_consumption_reuses_the_exact_frozen_fact_request(tmp_path: Path) -> Non def test_consumption_derives_the_period_from_actual_report_facts( tmp_path: Path, ) -> None: - registry, runtime = _fixture(tmp_path) - state_path = registry.parent / "ACTIVE_GOAL_STATE.md" - state_path.write_text( - state_path.read_text(encoding="utf-8").replace( - "updated_at=2026-08-30T09:00:00Z", - "updated_at=2026-08-29T22:42:26+08:00", - ), - encoding="utf-8", + registry, runtime = _fixture( + tmp_path, finished_updated_at="2026-08-29T22:42:26+08:00" ) required = consume_pending_periodic_report_intent( @@ -1921,26 +1938,13 @@ def test_real_cli_editorial_fallback_reads_canonical_work_without_display( import subprocess import sys from tests.control_plane.canonical_authority_fixture import ( - initialize_canonical_authority, isolate_sqlite_runtime, ) - from loopx.control_plane.coordination.runtime_shadow import ( - build_todo_runtime_shadow_projection, - ) if provider == "sqlite": isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime = _fixture(tmp_path) + registry, runtime = _fixture(tmp_path, provider=provider) state = registry.parent / "ACTIVE_GOAL_STATE.md" - records = parse_active_state_todos(state.read_text(), item_limit=None)[ - "agent_todos" - ]["items"] - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, todos=records, handoff_mode="soft_claim" - ) - initialize_canonical_authority( - runtime, GOAL_ID, projection, state_path=state, provider=provider - ) # A runtime override must route both intent IO and the Todo snapshot. config = json.loads(registry.read_text()) config["common_runtime_root"] = str(tmp_path / "unused-runtime") diff --git a/tests/cli_commands/settlement_capability_dispatch_fixture.py b/tests/cli_commands/settlement_capability_dispatch_fixture.py index 0f7afa4848..c9a54c3373 100644 --- a/tests/cli_commands/settlement_capability_dispatch_fixture.py +++ b/tests/cli_commands/settlement_capability_dispatch_fixture.py @@ -50,6 +50,10 @@ def write_periodic_report_registry(project: Path, runtime: Path) -> Path: "id": GOAL_ID, "repo": str(project), "state_file": "goal.md", + "coordination": { + "agent_model": "peer_v1", + "registered_agents": [AGENT_ID], + }, "control_plane": { "periodic_report": { "enabled": True, @@ -191,12 +195,30 @@ def complete_todo_via_cli( journal_capabilities: list[str], write_state: Callable[[Path], None], ) -> tuple[dict[str, object], Path, Path]: + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + from loopx.todos import list_goal_todos + from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_authority, + ) + project = tmp_path / "repo" project.mkdir() runtime = tmp_path / "runtime" runtime.mkdir() write_state(project) registry_path = write_periodic_report_registry(project, runtime) + todos = list_goal_todos(registry_path=registry_path, goal_id=GOAL_ID)["todos"] + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim" + ), + state_path=project / "goal.md", + provider="file", + ) write_successor_run_history(runtime) write_turn_heartbeat_receipt(runtime) write_turn_journal(runtime, observed=journal_capabilities) @@ -243,6 +265,6 @@ def run_todo_completion_via_cli( build_periodic_report_post_writeback_projection ), ) - assert exit_code == 0 + assert exit_code == 0, captured assert captured.get("ok") is True, captured return captured diff --git a/tests/cli_commands/test_periodic_report_intent_capability_chain.py b/tests/cli_commands/test_periodic_report_intent_capability_chain.py index 9ad4ec67da..6104b62643 100644 --- a/tests/cli_commands/test_periodic_report_intent_capability_chain.py +++ b/tests/cli_commands/test_periodic_report_intent_capability_chain.py @@ -6,12 +6,11 @@ from loopx.capabilities.periodic_report.pending_intent import ( pending_periodic_report_intents, ) +from loopx.todos import add_goal_todo from settlement_capability_dispatch_fixture import ( AGENT_ID, COMPLETED_TODO, - GATED_TODO, GOAL_ID, - PLAIN_TODO, complete_todo_via_cli, ) @@ -35,24 +34,26 @@ def _write_unclaimed_frontier_state(project: Path) -> None: ) -def _claim_gated_successors(project: Path) -> None: +def _claim_gated_successors(registry: Path) -> None: """Evolve durable state after the dispatch, as the next Turn would.""" - project.joinpath("goal.md").write_text( - f"""# Goal - -## User Todo - -## Agent Todo - -- [x] Ship the localized weekly report slice. - -- [ ] Re-run the outbound channel sync after network capacity returns. - -- [ ] Outline the follow-up frontier analysis. - -""", - encoding="utf-8", + add_goal_todo( + registry_path=registry, + goal_id=GOAL_ID, + role="agent", + text="Re-run the outbound channel sync after network capacity returns.", + task_class="advancement_task", + claimed_by=AGENT_ID, + action_kind="gated_work", + resume_when="capacity_available:network", + ) + add_goal_todo( + registry_path=registry, + goal_id=GOAL_ID, + role="agent", + text="Outline the follow-up frontier analysis.", + task_class="advancement_task", + claimed_by=AGENT_ID, ) @@ -105,15 +106,6 @@ def test_later_todo_completion_without_capabilities_does_not_replay_milestone( def test_post_writeback_frontier_and_progress_share_one_canonical_snapshot( tmp_path, monkeypatch ): - from tests.control_plane.canonical_authority_fixture import ( - initialize_canonical_authority, - ) - from loopx.control_plane.coordination.runtime_shadow import ( - build_todo_runtime_shadow_projection, - ) - from loopx.control_plane.todos.active_state_todo_parser import ( - parse_active_state_todos, - ) from loopx.capabilities.periodic_report.post_writeback_hook import ( build_periodic_report_post_writeback_projection, ) @@ -124,15 +116,8 @@ def test_post_writeback_frontier_and_progress_share_one_canonical_snapshot( journal_capabilities=["network"], write_state=_write_unclaimed_frontier_state, ) - _claim_gated_successors(registry.parent) + _claim_gated_successors(registry) state = registry.parent / "goal.md" - records = parse_active_state_todos(state.read_text(), item_limit=None)[ - "agent_todos" - ]["items"] - source = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, todos=records, handoff_mode="soft_claim" - ) - initialize_canonical_authority(runtime, GOAL_ID, source, state_path=state) state.unlink() reads = [] original = todo_source.read_canonical_todos_if_promoted diff --git a/tests/control_plane/test_explore_capture_identity.py b/tests/control_plane/test_explore_capture_identity.py index c49b63d7af..49287f7506 100644 --- a/tests/control_plane/test_explore_capture_identity.py +++ b/tests/control_plane/test_explore_capture_identity.py @@ -27,14 +27,14 @@ def test_first_capture_allocates_identity_and_recovers_without_rewriting_scope( registry.write_text(json.dumps(config)) binding = ("--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", TURN_ID) - assert _run_cli(registry, runtime, "todo", "claim", "--claimed-by", AGENT_ID, - *binding[:-2], cwd=project)[0] == 0 rows = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=rows, handoff_mode="hard_lease"), state_path=project / config["goals"][0]["state_file"], provider=provider) acquire_task_lease(registry_path=registry, runtime_root=runtime, goal_id=GOAL_ID, todo_id=TODO_ID, owner=AGENT_ID, idempotency_key="capture-identity") + assert _run_cli(registry, runtime, "todo", "claim", "--claimed-by", AGENT_ID, + *binding[:-2], cwd=project)[0] == 0 rc, guard = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", *binding, "--scan-path", str(project), cwd=project) assert rc == 0, guard diff --git a/tests/control_plane/test_explore_linked_capture.py b/tests/control_plane/test_explore_linked_capture.py index c6ab8c8368..5a46a0de12 100644 --- a/tests/control_plane/test_explore_linked_capture.py +++ b/tests/control_plane/test_explore_linked_capture.py @@ -47,7 +47,7 @@ def test_scope_uses_source_registry_and_canonical_todo_not_stale_shared_projecti from tests.control_plane.test_native_todo_planning_update import fixture, records from loopx.capabilities.explore.turn_context import explore_turn_context - source, _state = fixture(tmp_path, True, provider) + source, _state = fixture(tmp_path, provider) runtime = tmp_path / "runtime" config = json.loads(source.read_text()) config["goals"][0]["explore_graph"] = {"enabled": True} @@ -98,6 +98,11 @@ def test_scope_uses_source_registry_and_canonical_todo_not_stale_shared_projecti @pytest.mark.parametrize("case", ["unknown", "unlinked", "partial"]) def test_real_cli_rejects_invalid_linked_capture_before_primary_commit_and_recovers(tmp_path, case): + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + from loopx.todos import list_goal_todos from tests.control_plane.test_quota_settlement_cli import ( _write_fixture, _run_cli, GOAL_ID, AGENT_ID, TODO_ID, TURN_ID, ) @@ -107,6 +112,16 @@ def test_real_cli_rejects_invalid_linked_capture_before_primary_commit_and_recov registry.write_text(json.dumps(config)) binding = ("--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", TURN_ID) + todos = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim" + ), + state_path=project / config["goals"][0]["state_file"], + provider="file", + ) assert _run_cli(registry, runtime, "todo", "claim", "--claimed-by", AGENT_ID, *binding[:-2], cwd=project)[0] == 0 assert _run_cli(registry, runtime, "quota", "should-run", "--codex-app", diff --git a/tests/control_plane/test_explore_result_writeback.py b/tests/control_plane/test_explore_result_writeback.py index 8358110791..040c21ace7 100644 --- a/tests/control_plane/test_explore_result_writeback.py +++ b/tests/control_plane/test_explore_result_writeback.py @@ -30,7 +30,22 @@ def fixture(tmp_path, enabled=True): + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + path = registry(tmp_path, graph=enabled, planning=enabled) + runtime = tmp_path / "runtime" + initialize_canonical_authority( + runtime, + "research", + build_todo_runtime_shadow_projection( + goal_id="research", todos=[], handoff_mode="soft_claim" + ), + state_path=tmp_path / "goal.md", + provider="file", + ) todo = add_goal_todo( registry_path=path, goal_id="research", @@ -41,7 +56,7 @@ def fixture(tmp_path, enabled=True): ) return dict( registry_path=path, - runtime_root=tmp_path / "runtime", + runtime_root=runtime, goal_id="research", agent_id="worker", todo_id=todo["todo_id"], @@ -187,6 +202,10 @@ def interrupt(**kwargs): @pytest.mark.parametrize("case", ["disabled", "null", "invalid", "conflict", "off-without-result", "path-disabled", "path-missing", "path-no-evidence", "path-overflow"]) def test_real_cli_inline_validation_precedes_primary_commit(tmp_path, case): + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) from tests.control_plane.test_quota_settlement_cli import ( _write_fixture, _run_cli, GOAL_ID, AGENT_ID, TODO_ID, TURN_ID, ) @@ -195,6 +214,16 @@ def test_real_cli_inline_validation_precedes_primary_commit(tmp_path, case): config = json.loads(path.read_text()) config["goals"][0]["explore_graph"] = {"enabled": case not in {"disabled", "off-without-result", "path-disabled"}} path.write_text(json.dumps(config)) + todos = list_goal_todos(registry_path=path, goal_id=GOAL_ID)["todos"] + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim" + ), + state_path=project / config["goals"][0]["state_file"], + provider="file", + ) rc, claimed = _run_cli(path, runtime, "todo", "claim", "--goal-id", GOAL_ID, "--todo-id", TODO_ID, "--agent-id", AGENT_ID, "--claimed-by", AGENT_ID, cwd=project) @@ -262,6 +291,11 @@ def test_real_cli_inline_validation_precedes_primary_commit(tmp_path, case): def test_real_cli_normal_writeback_persists_attachment_and_rejects_conflicting_retry( tmp_path, terminal, interrupted, source_mode, monkeypatch, capsys, ): + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + from loopx.control_plane.work_items.task_lease import acquire_task_lease from tests.control_plane.test_quota_settlement_cli import ( _write_fixture, _run_cli, @@ -279,23 +313,10 @@ def test_real_cli_normal_writeback_persists_attachment_and_rejects_conflicting_r "explore_harness": {"enabled": True}, } path.write_text(json.dumps(config)) - rc, claimed = _run_cli( - path, - runtime, - "todo", - "claim", - "--goal-id", - GOAL_ID, - "--todo-id", - TODO_ID, - "--agent-id", - AGENT_ID, - "--claimed-by", - AGENT_ID, - cwd=project, - ) - assert rc == 0, claimed + todos = list_goal_todos(registry_path=path, goal_id=GOAL_ID)["todos"] if source_mode == "linked_path_delta": + target = next(todo for todo in todos if todo["todo_id"] == TODO_ID) + target["explore_result_node_refs"] = [ATTACHMENT["node_id"]] from loopx.capabilities.explore.result_log import ( append_explore_result_event, build_explore_node_event, ) @@ -303,27 +324,19 @@ def test_real_cli_normal_writeback_persists_attachment_and_rejects_conflicting_r build_explore_node_event(goal_id=GOAL_ID, node_id=ATTACHMENT["node_id"], node_kind="question", title=ATTACHMENT["question"], summary=ATTACHMENT["applicability"], status="open")) - update_goal_todo(registry_path=path, goal_id=GOAL_ID, todo_id=TODO_ID, - agent_id=AGENT_ID, explore_result_node_refs=[ATTACHMENT["node_id"]]) + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=todos, + handoff_mode="hard_lease" if terminal else "soft_claim", + ), + state_path=project / config["goals"][0]["state_file"], + provider="file", + ) terminal_proof = {} if terminal: - from canonical_authority_fixture import initialize_canonical_authority - from loopx.control_plane.coordination.runtime_shadow import ( - build_todo_runtime_shadow_projection, - ) - from loopx.control_plane.work_items.task_lease import acquire_task_lease - from loopx.todos import complete_goal_todo - - todos = list_goal_todos(registry_path=path, goal_id=GOAL_ID)["todos"] - initialize_canonical_authority( - runtime, - GOAL_ID, - build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, todos=todos, handoff_mode="hard_lease" - ), - state_path=project / config["goals"][0]["state_file"], - provider="file", - ) lease = acquire_task_lease( registry_path=path, runtime_root=runtime, @@ -336,6 +349,25 @@ def test_real_cli_normal_writeback_persists_attachment_and_rejects_conflicting_r task_lease_idempotency_key="cli-terminal-result", task_lease_expected_version=lease["lease"]["version"], ) + rc, claimed = _run_cli( + path, + runtime, + "todo", + "claim", + "--goal-id", + GOAL_ID, + "--todo-id", + TODO_ID, + "--agent-id", + AGENT_ID, + "--claimed-by", + AGENT_ID, + cwd=project, + ) + assert rc == 0, claimed + if terminal: + from loopx.todos import complete_goal_todo + binding = ( "--goal-id", GOAL_ID, @@ -510,7 +542,7 @@ def test_canonical_delivery_keeps_existing_refs_and_receiver_reads_scoped_result records, ) - path, _state = canonical_fixture(tmp_path, True, provider) + path, _state = canonical_fixture(tmp_path, provider) config = json.loads(path.read_text()) config["goals"][0]["explore_graph"] = {"enabled": True} config["goals"][0]["spawn_policy"] = { @@ -600,7 +632,7 @@ def test_completed_work_can_attach_evidence_without_reopening_or_changing_comple tmp_path, provider, mode ): from tests.control_plane.test_native_todo_planning_update import ( - fixture as canonical_fixture, + source_fixture as canonical_source_fixture, records, ) from canonical_authority_fixture import initialize_canonical_authority @@ -613,7 +645,7 @@ def test_completed_work_can_attach_evidence_without_reopening_or_changing_comple from loopx.control_plane.work_items.task_lease import acquire_task_lease from loopx.todos import complete_goal_todo - path, state = canonical_fixture(tmp_path, False, provider) + path, state = canonical_source_fixture(tmp_path) config = json.loads(path.read_text()) config["goals"][0]["explore_graph"] = {"enabled": True} path.write_text(json.dumps(config)) @@ -719,7 +751,7 @@ def test_hard_lease_is_required_for_new_delivery_but_not_successful_readback( tmp_path, provider ): from tests.control_plane.test_native_todo_planning_update import ( - fixture as canonical_fixture, + source_fixture as canonical_source_fixture, ) from canonical_authority_fixture import initialize_canonical_authority from loopx.control_plane.coordination.runtime_shadow import ( @@ -730,7 +762,7 @@ def test_hard_lease_is_required_for_new_delivery_but_not_successful_readback( release_task_lease, ) - path, state = canonical_fixture(tmp_path, False, provider) + path, state = canonical_source_fixture(tmp_path) config = json.loads(path.read_text()) config["goals"][0]["explore_graph"] = {"enabled": True} path.write_text(json.dumps(config)) @@ -784,6 +816,7 @@ def test_real_cli_open_capture_release_order_and_readback_replay(tmp_path, provi from canonical_authority_fixture import initialize_canonical_authority from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted + from loopx.control_plane.work_items.task_lease import acquire_task_lease from tests.control_plane.test_quota_settlement_cli import ( _write_fixture, _run_cli, GOAL_ID, AGENT_ID, TODO_ID, TURN_ID, ) @@ -798,12 +831,17 @@ def run(*command): assert rc == 0, packet return packet + source_todos = list_goal_todos(registry_path=path, goal_id=GOAL_ID)["todos"] + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=source_todos, handoff_mode="hard_lease"), + state_path=project / config["goals"][0]["state_file"], provider=provider) + acquire_task_lease( + registry_path=path, runtime_root=runtime, goal_id=GOAL_ID, + todo_id=TODO_ID, owner=AGENT_ID, idempotency_key="early-release", + ) run("todo", "claim", "--goal-id", GOAL_ID, "--todo-id", TODO_ID, "--agent-id", AGENT_ID, "--claimed-by", AGENT_ID) todos = list_goal_todos(registry_path=path, goal_id=GOAL_ID)["todos"] - initialize_canonical_authority(runtime, GOAL_ID, - build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=todos, handoff_mode="hard_lease"), - state_path=project / config["goals"][0]["state_file"], provider=provider) binding = ["--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", TURN_ID] guard_command = ["quota", "should-run", "--codex-app", *binding, From c04654ab52e8c283c01b196ac38d183340ad6f7a Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 03:35:35 +0800 Subject: [PATCH 06/98] test(todos): reject retired monitor writers Signed-off-by: Duang777 --- .../test_monitor_unavailable_closeout_cli.py | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/tests/control_plane/test_monitor_unavailable_closeout_cli.py b/tests/control_plane/test_monitor_unavailable_closeout_cli.py index 406ab543b8..61c853daa6 100644 --- a/tests/control_plane/test_monitor_unavailable_closeout_cli.py +++ b/tests/control_plane/test_monitor_unavailable_closeout_cli.py @@ -114,6 +114,13 @@ def cli(*args: str) -> tuple[int, dict]: "--target-key", "synthetic-source", "--result-hash", "synthetic-observed", *lease_args, "--execute") + if provider == "markdown": + assert rc == 1 and poll["error_code"] == "canonical_authority_required", poll + assert poll["write_admitted"] is False + assert poll["legacy_fallback_used"] is False + assert _classification_count(runtime, "quota_monitor_poll") == 0 + assert _spend_run_count(runtime) == 0 + return assert rc == 0 and poll["appended"] is True, poll rc, denied = cli(*refresh) assert rc != 0 and denied["appended"] is False, denied @@ -166,15 +173,25 @@ def cli(*args: str) -> tuple[int, dict]: # A newly executable independent advancement belongs to the same recovery # host Turn after the original effect is closed, without a third identity. - if provider != "markdown": - rc, restored = cli("task-lease", "release", "--goal-id", GOAL_ID, "--todo-id", MONITOR_ID, - "--owner", AGENT_ID, "--idempotency-key", f"unavailable-{provider}", - "--expected-version", str(lease["lease"]["version"])) - assert rc == 0, restored - rc, successor = cli("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", - "--text", "Inspect an independent public source.", "--task-class", "advancement_task", - "--claimed-by", AGENT_ID, - *(("--operation-id", f"independent-{provider}") if provider != "markdown" else ())) + successor_args = ( + "todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--text", "Inspect an independent public source.", + "--task-class", "advancement_task", "--claimed-by", AGENT_ID, + ) + if provider == "markdown": + state_before_add = state.read_bytes() + rc, successor = cli(*successor_args) + assert rc == 1 and successor["error_code"] == "canonical_authority_required", successor + assert successor["write_admitted"] is False + assert successor["legacy_fallback_used"] is False + assert state.read_bytes() == state_before_add + return + + rc, restored = cli("task-lease", "release", "--goal-id", GOAL_ID, "--todo-id", MONITOR_ID, + "--owner", AGENT_ID, "--idempotency-key", f"unavailable-{provider}", + "--expected-version", str(lease["lease"]["version"])) + assert rc == 0, restored + rc, successor = cli(*successor_args, "--operation-id", f"independent-{provider}") assert rc == 0, successor rc, next_turn = cli("quota", "should-run", "--goal-id", GOAL_ID, "--codex-app", "--agent-id", AGENT_ID, "--turn-instance-id", recovery_turn, *capabilities) From e638ff4b204e2c5d5fec503f4123743fefc3d40d Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 03:58:54 +0800 Subject: [PATCH 07/98] test(todos): migrate remaining canonical fixtures Signed-off-by: Duang777 --- .../capabilities/test_research_composition_gate.py | 14 ++++++++++++++ .../test_canonical_lease_inspection.py | 8 ++++---- tests/control_plane/test_explore_durable_links.py | 3 +-- .../test_quota_authority_settlement_journey.py | 6 ++++-- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/tests/capabilities/test_research_composition_gate.py b/tests/capabilities/test_research_composition_gate.py index a60c4865fa..ca4cdcfd3e 100644 --- a/tests/capabilities/test_research_composition_gate.py +++ b/tests/capabilities/test_research_composition_gate.py @@ -12,8 +12,11 @@ from loopx.capabilities.explore.result_log import ( append_explore_result_event, build_explore_edge_event, build_explore_node_event, explore_result_log_path, ) +from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection +from loopx.control_plane.todos.list_readback import list_goal_todos from loopx.control_plane.work_items.semantic_replan_writeback import qualify_replan_writeback from loopx.capabilities.explore.research_frontier import prepare_research_replan_evidence +from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority GOAL = "research-gate-fixture" AGENT = "fixture-agent" @@ -56,6 +59,17 @@ def fixture(tmp_path: Path): append_research_observation(log, goal_id=GOAL, observation=observation("a", "b")) for name in ["a", "b"]: append_explore_result_event(log, build_explore_edge_event(goal_id=GOAL, from_node="joint", to_node=name, edge_type="depends_on")) + todos = list_goal_todos(registry_path=registry, goal_id=GOAL)["todos"] + initialize_canonical_authority( + runtime, + GOAL, + build_todo_runtime_shadow_projection( + goal_id=GOAL, + todos=todos, + handoff_mode="soft_claim", + ), + state_path=state, + ) def cli(*args: str, success: bool = True) -> dict: result = subprocess.run([sys.executable, "-m", "loopx.cli", "--format", "json", "--registry", str(registry), diff --git a/tests/control_plane/test_canonical_lease_inspection.py b/tests/control_plane/test_canonical_lease_inspection.py index ba53566605..88ca782337 100644 --- a/tests/control_plane/test_canonical_lease_inspection.py +++ b/tests/control_plane/test_canonical_lease_inspection.py @@ -186,10 +186,10 @@ def test_inactive_legacy_inspection_does_not_project_todos(tmp_path, monkeypatch state = tmp_path / "state.md" state.write_text("---\nhandoff_mode: hard_lease\n---\n# Synthetic state\n") registry.write_text(json.dumps({"goals": [{"id": GOAL, "repo": str(tmp_path), "state_file": str(state)}]})) - import loopx.todos + from loopx.control_plane.todos import list_readback def forbidden_projection(**_): raise AssertionError("inactive inspection must not parse Todo history") - monkeypatch.setattr(loopx.todos, "list_goal_todos", forbidden_projection) + monkeypatch.setattr(list_readback, "list_goal_todos", forbidden_projection) runtime = tmp_path / "runtime" missing = _inspect(registry, runtime) assert missing["active"] is False and missing["lease"] is None @@ -211,13 +211,13 @@ def test_legacy_inspection_reloads_lease_after_demanding_todo_facts(tmp_path, mo lease_path.parent.mkdir(parents=True) lease = {"schema_version": "task_lease_v0", "status": "active", "owner": "agent-a", "expires_at": "2099-01-01T00:00:00Z"} lease_path.write_text(json.dumps(lease)) - import loopx.todos + from loopx.control_plane.todos import list_readback calls = [] def release_during_projection(**_): calls.append(True) lease_path.write_text(json.dumps({**lease, "status": "released"})) return {"todos": [{"todo_id": TODO, "status": "open", "claimed_by": None, "excluded_agents": []}]} - monkeypatch.setattr(loopx.todos, "list_goal_todos", release_during_projection) + monkeypatch.setattr(list_readback, "list_goal_todos", release_during_projection) result = _inspect(registry, runtime) assert len(calls) == 1 assert result["active"] is False diff --git a/tests/control_plane/test_explore_durable_links.py b/tests/control_plane/test_explore_durable_links.py index 677aba5408..3931f1b133 100644 --- a/tests/control_plane/test_explore_durable_links.py +++ b/tests/control_plane/test_explore_durable_links.py @@ -25,9 +25,8 @@ def test_ninth_capture_keeps_history_and_survives_real_cli_replay(tmp_path, prov registry.write_text(json.dumps(config)) binding = ("--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", TURN_ID) - assert _run_cli(registry, runtime, "todo", "claim", "--claimed-by", AGENT_ID, - *binding[:-2], cwd=project)[0] == 0 rows = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] + next(row for row in rows if row["todo_id"] == TODO_ID)["claimed_by"] = AGENT_ID initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=rows, handoff_mode="hard_lease"), state_path=project / config["goals"][0]["state_file"], provider=provider) diff --git a/tests/control_plane/test_quota_authority_settlement_journey.py b/tests/control_plane/test_quota_authority_settlement_journey.py index 6a02a1ef84..4ccbcdf867 100644 --- a/tests/control_plane/test_quota_authority_settlement_journey.py +++ b/tests/control_plane/test_quota_authority_settlement_journey.py @@ -78,9 +78,11 @@ def test_stale_markdown_cannot_admit_terminal_or_blocked_work(tmp_path, provider @pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) def test_exact_selection_reaches_work_beyond_display_limits(tmp_path, provider): - project, runtime, registry, path, _ = _source(tmp_path, provider=provider) + project, runtime, registry, path, prefix = _source(tmp_path, provider=provider) if provider != "legacy": - path.unlink() # Display is not a prerequisite for promoted admission. + # Goal intent remains readable, while its bounded Todo display cannot + # supply the exact candidate from the canonical provider. + path.write_text(prefix) code, guard = _guard(project, runtime, registry) assert code == 0, guard assert guard["decision"] == "run" From 179a54d33e6872f25e47ff0c38f43f34f282d2cd Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 04:16:29 +0800 Subject: [PATCH 08/98] test(leases): retain only historical source cleanup Signed-off-by: Duang777 --- ...test_quota_authority_settlement_journey.py | 3 +- .../test_source_lease_reader_isolation.py | 97 +++++++------------ 2 files changed, 39 insertions(+), 61 deletions(-) diff --git a/tests/control_plane/test_quota_authority_settlement_journey.py b/tests/control_plane/test_quota_authority_settlement_journey.py index 4ccbcdf867..b566dd9e79 100644 --- a/tests/control_plane/test_quota_authority_settlement_journey.py +++ b/tests/control_plane/test_quota_authority_settlement_journey.py @@ -265,7 +265,8 @@ def run(*args): assert code == 0 and migrated["status"] == "applied", migrated assert Path(migrated["backup_path"]).is_file() assert migrated["execution_authority_granted"] is False - path.unlink() # The migrated journey cannot use the old Markdown writer. + goal_context = path.read_text().split("## Agent Todo")[0] + path.write_text(goal_context + "## Agent Todo\n\n") code, guard = _guard(project, runtime, registry) assert code == 0 and guard["decision"] == "run", guard diff --git a/tests/control_plane/test_source_lease_reader_isolation.py b/tests/control_plane/test_source_lease_reader_isolation.py index 95d7e4704f..32514ab51b 100644 --- a/tests/control_plane/test_source_lease_reader_isolation.py +++ b/tests/control_plane/test_source_lease_reader_isolation.py @@ -1,4 +1,4 @@ -"""Supported source leases retain readback without the historic mutation facade.""" +"""Historical source leases retain readback and exact cleanup without the facade.""" from __future__ import annotations @@ -65,7 +65,7 @@ def lease(action, *args, success=True): "task-lease", action, "--todo-id", "todo_source", *args, success=success ) - first = lease( + rejected = lease( "acquire", "--owner", "agent-a", @@ -73,43 +73,34 @@ def lease(action, *args, success=True): "source-a", "--ttl-seconds", "600", - success=mode != "soft_claim", + success=False, ) - if mode == "soft_claim": - assert not first["ok"] and first["error_code"] == "handoff_mode_forbids_lease" - assert lease("inspect")["lease"] is None - assert (w.state.read_bytes(), w.registry.read_bytes()) == original - return + assert not rejected["ok"] + assert rejected["error_code"] == "canonical_authority_required" - assert first["ok"] and first["acquired"] and first["lease"]["owner"] == "agent-a" - assert first["lease"]["version"] == first["lease"]["lease_epoch"] == 1 - assert lease("inspect")["active"] is True - retry = lease( - "acquire", - "--owner", - "agent-a", - "--idempotency-key", - "source-a", - "--ttl-seconds", - "600", - ) - assert retry["idempotent"] and retry["lease"] == first["lease"] - denied = lease( - "acquire", "--owner", "agent-b", "--idempotency-key", "other", success=False - ) - assert not denied["ok"] and denied["error_code"] == "todo_lease_conflict" - renewed = lease( - "renew", - "--owner", - "agent-a", - "--idempotency-key", - "source-a", - "--expected-version", - "1", + lease_path = ( + w.runtime / "goals" / w.goal / "task-leases" / "todo_source.json" ) - assert renewed["lease"]["version"] == 2 and renewed["lease"]["lease_epoch"] == 1 - # The exact original renewal is receipt recovery, not a stale new write. - replay = lease( + lease_path.parent.mkdir(parents=True, exist_ok=True) + historical = { + "schema_version": "task_lease_v0", + "goal_id": w.goal, + "todo_id": "todo_source", + "status": "active", + "owner": "agent-a", + "idempotency_key": "source-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T00:00:00Z", + "updated_at": "2026-09-01T00:00:00Z", + "expires_at": "2099-01-01T00:00:00Z", + } + lease_path.write_text(json.dumps(historical)) + assert lease("inspect")["lease"] == historical + + renew = lease( "renew", "--owner", "agent-a", @@ -117,47 +108,33 @@ def lease(action, *args, success=True): "source-a", "--expected-version", "1", + success=False, ) - assert replay["idempotent"] and replay["lease"] == renewed["lease"] - assert lease("inspect")["lease"] == renewed["lease"] - transferred = lease( + assert renew["error_code"] == "canonical_authority_required" + transfer = lease( "transfer", "--owner", "agent-a", "--idempotency-key", "source-a", "--expected-version", - "2", + "1", "--new-owner", "agent-b", "--new-idempotency-key", "source-b", - ) - assert ( - transferred["lease"]["version"] == 3 - and transferred["lease"]["lease_epoch"] == 2 - ) - assert transferred["lease"]["owner"] == "agent-b" - stale = lease( - "renew", - "--owner", - "agent-b", - "--idempotency-key", - "source-b", - "--expected-version", - "2", success=False, ) - assert not stale["ok"] and stale["error_code"] == "version_mismatch" - assert lease("inspect")["lease"] == transferred["lease"] + assert transfer["error_code"] == "canonical_authority_required" + assert lease("inspect")["lease"] == historical + released = lease( "release", - "--owner", - "agent-b", + "--owner", "agent-a", "--idempotency-key", - "source-b", + "source-a", "--expected-version", - "3", + "1", ) assert released["lease"]["status"] == "released" retained = lease("inspect") From 9ffe9c0c89289a0e551622c28365844c44bb6cb8 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 04:51:23 +0800 Subject: [PATCH 09/98] test(todos): align retirement fixtures with canonical authority Signed-off-by: Duang777 --- .../replan_semantic_action_behavior.py | 2 +- .../test_explore_research_evidence.py | 8 +- .../test_canonical_todo_writer_isolation.py | 22 +++-- .../test_todo_consumer_host_retirement.py | 12 +-- .../test_todo_consumer_retirement.py | 82 +++++++++++++++++-- .../test_governed_capability_execution.py | 12 +++ 6 files changed, 115 insertions(+), 23 deletions(-) diff --git a/loopx/control_plane/testing/replan_semantic_action_behavior.py b/loopx/control_plane/testing/replan_semantic_action_behavior.py index 69b49d3e13..52b0181e2c 100644 --- a/loopx/control_plane/testing/replan_semantic_action_behavior.py +++ b/loopx/control_plane/testing/replan_semantic_action_behavior.py @@ -278,10 +278,10 @@ def _build_fixture( registry_path.parent.mkdir(parents=True, exist_ok=True) registry_path.write_text(registry_text + "\n", encoding="utf-8") - from ...todos import add_goal_todo from ..coordination.goal_storage_initialization import ( initialize_goal_storage_target, ) + from ..todos.mutation_api import add_goal_todo initialized = initialize_goal_storage_target( runtime_root, diff --git a/tests/capabilities/test_explore_research_evidence.py b/tests/capabilities/test_explore_research_evidence.py index 5d55c12359..56ee6706c0 100644 --- a/tests/capabilities/test_explore_research_evidence.py +++ b/tests/capabilities/test_explore_research_evidence.py @@ -179,7 +179,11 @@ def command(*args: str) -> dict: assert view["research_frontier"]["mode"] == "read_only_shadow" -def execution_fixture(tmp_path: Path) -> tuple[Path, Path, Path, dict]: +def execution_fixture( + tmp_path: Path, + *, + provider: str = "file", +) -> tuple[Path, Path, Path, dict]: from loopx.control_plane.coordination.runtime_shadow import ( build_todo_runtime_shadow_projection, ) @@ -204,7 +208,7 @@ def execution_fixture(tmp_path: Path) -> tuple[Path, Path, Path, dict]: goal_id=GOAL, todos=[], handoff_mode="soft_claim" ), state_path=state, - provider="file", + provider=provider, ) path = explore_result_log_path(runtime, GOAL) for name in ["a", "b"]: diff --git a/tests/control_plane/test_canonical_todo_writer_isolation.py b/tests/control_plane/test_canonical_todo_writer_isolation.py index 68b88847b0..ea12517f8a 100644 --- a/tests/control_plane/test_canonical_todo_writer_isolation.py +++ b/tests/control_plane/test_canonical_todo_writer_isolation.py @@ -280,11 +280,19 @@ def bootstrap(): @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_leased_delivery_settlement_and_recovery_without_source_todo_writers(tmp_path, provider, without_source_todo_writers): - project, runtime, registry, display, _ = _source( + project, runtime, registry, display, prefix = _source( tmp_path, provider=provider, handoff_mode="hard_lease", - extra=f"claimed_by={settlement.AGENT_ID}", + extra=( + f"claimed_by={settlement.AGENT_ID} " + "required_write_scopes=tests/**" + ), ) - display.unlink() + config = json.loads(registry.read_text()) + config["goals"][0]["coordination"]["write_scope"] = ["tests/**"] + registry.write_text(json.dumps(config)) + # Goal intent remains readable while the Markdown Todo display contributes + # no candidate or lease facts after canonical promotion. + display.write_text(prefix) def run(*args, succeeds=True): result = subprocess.run( @@ -301,10 +309,6 @@ def run(*args, succeeds=True): def guard(turn): return run("quota", "should-run", *identity, "--codex-app", "--turn-instance-id", turn) - selected = guard(settlement.TURN_ID) - assert selected["decision"] == "run" - original = selected["heartbeat_receipt"]["settlement_identity"] - assert original["todo_id"] == settlement.TODO_ID update = ["todo", "update", *identity, "--note", "Canonical delivery retained"] rejected = run(*update, succeeds=False) assert rejected["error_code"] == "handoff_mode_requires_lease" @@ -315,6 +319,10 @@ def guard(turn): assert acquired["acquired"] is True proof = ["--task-lease-idempotency-key", "delivery", "--task-lease-expected-version", str(acquired["lease"]["version"])] + selected = guard(settlement.TURN_ID) + assert selected["decision"] == "run" + original = selected["heartbeat_receipt"]["settlement_identity"] + assert original["todo_id"] == settlement.TODO_ID assert run(*update, *proof)["ok"] is True before = run("todo", "list", *identity)["todo"] assert before["note"] == "Canonical delivery retained" diff --git a/tests/control_plane/test_todo_consumer_host_retirement.py b/tests/control_plane/test_todo_consumer_host_retirement.py index 09b4af8b71..bc3de04557 100644 --- a/tests/control_plane/test_todo_consumer_host_retirement.py +++ b/tests/control_plane/test_todo_consumer_host_retirement.py @@ -103,19 +103,15 @@ def forbidden_runner(*args, **kwargs): def test_fresh_research_execution_and_historical_replay_use_retained_owners( tmp_path, provider, consumer_distribution, # noqa: F811 ): - from canonical_authority_fixture import initialize_canonical_authority from tests.capabilities import test_explore_research_evidence as research - from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection - from loopx.control_plane.todos.list_readback import list_goal_todos # Seed through the existing source fixture, then use the selected copied # distribution for every CLI decision. No editable finder can restore it. - registry, runtime, log, observation = research.execution_fixture(tmp_path) + registry, runtime, log, observation = research.execution_fixture( + tmp_path, + provider=provider, + ) state = tmp_path / "project/ACTIVE_GOAL_STATE.md" - todos = list_goal_todos(registry_path=registry, goal_id=research.GOAL, role="agent")["todos"] - initialize_canonical_authority(runtime, research.GOAL, - build_todo_runtime_shadow_projection(goal_id=research.GOAL, todos=todos, handoff_mode="soft_claim"), - state_path=state, provider=provider) package, command = consumer_distribution packet = tmp_path / "observation.json" packet.write_text(json.dumps(observation)) diff --git a/tests/control_plane/test_todo_consumer_retirement.py b/tests/control_plane/test_todo_consumer_retirement.py index edc8a893c8..ca1aa2c562 100644 --- a/tests/control_plane/test_todo_consumer_retirement.py +++ b/tests/control_plane/test_todo_consumer_retirement.py @@ -65,8 +65,6 @@ def test_historic_facade_has_no_production_consumers(): @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( @@ -114,10 +112,84 @@ def test_once_only_settlement_uses_retained_owners(tmp_path, provider, consumer_ @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, + tmp_path, provider, material_change, consumer_distribution, ): - journeys.test_monitor_recovery_with_source_writer_isolation( - tmp_path, request, provider, material_change, True, consumer_distribution) + from test_leased_monitor_poll import LEASE, PROOF + from test_monitor_followthrough_contract import GOAL_ID, AGENT_ID + from test_native_monitor_poll import _canonical + + _package, command = consumer_distribution + registry, runtime, display, monitor = _canonical( + tmp_path, native=True, provider=provider, lease=LEASE, + ) + display.unlink() + + def cli(*args, succeeds=True): + result = subprocess.run( + [*command, "--format", "json", + "--registry", str(registry), "--runtime-root", str(runtime), *args], + cwd=tmp_path, capture_output=True, text=True, timeout=45, + ) + assert (result.returncode == 0) is succeeds, result.stdout + result.stderr + return json.loads(result.stdout) + + def current(): + return cli("todo", "list", "--goal-id", GOAL_ID) + + identity = [ + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--runtime-profile", "generic_cli", "--turn-instance-id", "monitor-retirement", + "--available-capability", "network", + "--available-capability", "external_evidence_poll", + ] + guard = cli("quota", "should-run", *identity) + assert guard["selected_todo"]["todo_id"] == monitor["todo_id"] + before = current() + poll = [ + "quota", "monitor-poll", *identity, "--todo-id", monitor["todo_id"], + "--result-hash", "observed-revision", "--use-current-task-lease", "--execute", + ] + if material_change: + poll += [ + "--material-change", "--next-agent-todo", "Validate the observed revision", + "--next-action-kind", "validate", + ] + + backend = runtime / "authority" / f"{provider}-v0" + offline = backend.with_name(backend.name + "-offline") + backend.rename(offline) + try: + failed = cli(*poll, succeeds=False) + assert failed["ok"] is False + assert not backend.exists() and not display.exists() + finally: + offline.rename(backend) + assert current()["todos"] == before["todos"] + + committed = cli(*poll) + assert committed["todo_writeback"]["lease_proof"] == PROOF + assert committed["todo_writeback"]["material_change_generation"] == int(material_change) + after = current() + assert len(after["todos"]) == len(before["todos"]) + int(material_change) + observed = next(todo for todo in after["todos"] if todo["todo_id"] == monitor["todo_id"]) + assert observed["result_hash"] == "observed-revision" + assert int(observed["consecutive_no_change"]) == int(not material_change) + assert display.exists() + + assert cli( + "task-lease", "release", "--goal-id", GOAL_ID, + "--todo-id", monitor["todo_id"], "--owner", AGENT_ID, + "--idempotency-key", PROOF["idempotency_key"], + "--expected-version", str(PROOF["expected_version"]), + )["released"] + assert cli(*poll)["replayed"] is True + assert current()["todos"] == after["todos"] + rows = [ + json.loads(line) + for line in (runtime / "goals" / GOAL_ID / "runs/index.jsonl").read_text().splitlines() + ] + assert sum(row.get("classification") == "quota_monitor_poll" for row in rows) == 1 + assert all(row.get("classification") != "quota_slot_spend" for row in rows) def test_public_imports_keep_same_owners(): diff --git a/tests/extensions/test_governed_capability_execution.py b/tests/extensions/test_governed_capability_execution.py index d19aff3110..2c5211377a 100644 --- a/tests/extensions/test_governed_capability_execution.py +++ b/tests/extensions/test_governed_capability_execution.py @@ -10,6 +10,7 @@ import loopx.extensions.governed_capability_execution as governed_execution from loopx import paths +from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.effect_program import SettlementIdentity from loopx.control_plane.effect_runtime import EffectRuntimeRejected from loopx.extensions.capability_admission import ( @@ -23,6 +24,7 @@ ) from loopx.extensions.runtime import install_extension from loopx.todos import list_goal_todos +from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority def test_default_governed_run_dir_follows_single_runtime_route( @@ -219,6 +221,16 @@ def _installed_material_extension( operations=["publish"], execute=True, ) + initialize_canonical_authority( + tmp_path / "runtime", + "fixture-goal", + build_todo_runtime_shadow_projection( + goal_id="fixture-goal", + todos=[], + handoff_mode="soft_claim", + ), + state_path=active_state, + ) return state_file, registry, call_log From b9ec87c9ae817f96922d6ba1d417edde2e86f2ed Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 05:15:16 +0800 Subject: [PATCH 10/98] test(chat): migrate team plan action fixtures Signed-off-by: Duang777 --- tests/test_chat_team_plan_action.py | 68 ++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/tests/test_chat_team_plan_action.py b/tests/test_chat_team_plan_action.py index ad47d09216..900192f335 100644 --- a/tests/test_chat_team_plan_action.py +++ b/tests/test_chat_team_plan_action.py @@ -9,8 +9,12 @@ import pytest +from control_plane.canonical_authority_fixture import initialize_canonical_authority from loopx.chat_action_store import ActionConflictError, ChatActionStore from loopx.chat_actions import ChatActionService +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) GOAL_ID = "team-plan-action-fixture" AGENT_ID = "agent-alpha" @@ -60,6 +64,16 @@ def _fixture(tmp_path: Path, *, agents: tuple[str, ...] = (AGENT_ID,)): ), encoding="utf-8", ) + initialize_canonical_authority( + tmp_path / "runtime", + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[], + handoff_mode="soft_claim", + ), + state_path=state_path, + ) service = ChatActionService( store=ChatActionStore(tmp_path / "runtime" / "chat" / "actions"), registry_path=registry_path, @@ -411,16 +425,29 @@ def test_an_unchanged_objective_still_confirms_and_records_its_basis( def test_failed_batch_retries_same_card_without_partial_work(tmp_path: Path, monkeypatch) -> None: from loopx.control_plane.work_items import team_plan_adapter + from loopx.control_plane.coordination.local_authority import ( + read_canonical_todos_if_promoted, + ) + project, _, service = _fixture(tmp_path) before = _todos(project) preview = _preview(service, _two_lane_plan()) - def fail(*args, **kwargs): - raise OSError("batch unavailable") - monkeypatch.setattr(team_plan_adapter, "write_captured_todo_state", fail) + original_effect = team_plan_adapter.effect_runtime_result + + def fail(method, request): + if method == "work_items.team_plan.commit": + raise OSError("batch unavailable") + return original_effect(method, request) + + monkeypatch.setattr(team_plan_adapter, "effect_runtime_result", fail) failed = service.apply(preview["proposal_id"])["proposal"] assert failed["status"] == "failed" assert failed["failure"]["error_code"] == "team_plan_commit_failed" assert _todos(project) == before + assert read_canonical_todos_if_promoted( + runtime_root=project.parent / "runtime", + goal_id=GOAL_ID, + )["todos"] == [] monkeypatch.undo() applied = service.apply(preview["proposal_id"])["proposal"] assert applied["status"] == "applied" @@ -430,24 +457,36 @@ def fail(*args, **kwargs): @pytest.mark.parametrize("has_gap", [False, True]) def test_lost_response_recovers_original_commit_after_work_changes(tmp_path: Path, monkeypatch, has_gap: bool) -> None: + from loopx.control_plane.coordination.local_authority import ( + read_canonical_todos_if_promoted, + ) from loopx.control_plane.work_items import team_plan_adapter + project, _, service = _fixture(tmp_path) plan = _two_lane_plan() if has_gap: plan["lanes"][1]["agent_id"] = "unregistered-agent" preview = _preview(service, plan) - real = team_plan_adapter.write_captured_todo_state - def lose_response(*args, **kwargs): - real(*args, **kwargs) - raise OSError("response lost after commit") - monkeypatch.setattr(team_plan_adapter, "write_captured_todo_state", lose_response) + original_effect = team_plan_adapter.effect_runtime_result + + def lose_response(method, request): + result = original_effect(method, request) + if method == "work_items.team_plan.commit": + raise OSError("response lost after commit") + return result + + monkeypatch.setattr(team_plan_adapter, "effect_runtime_result", lose_response) assert service.apply(preview["proposal_id"])["proposal"]["status"] == "failed" - assert _todos(project).count("loopx:todo ") == (1 if has_gap else 2) + committed = read_canonical_todos_if_promoted( + runtime_root=project.parent / "runtime", + goal_id=GOAL_ID, + ) + assert len(committed["todos"]) == (1 if has_gap else 2) + assert _todos(project).count("loopx:todo ") == 0 monkeypatch.undo() state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" # A receiver's later edit must survive historical commit recovery. - state.write_text(state.read_text().replace("Advance the intake contract", "Receiver revised the work")) - before = state.read_bytes() + state.write_text(state.read_text() + "\nReceiver revised the work.\n") recovered = service.apply(preview["proposal_id"])["proposal"] assert recovered["status"] == "applied" assert recovered["receipt"]["outcome"] == "team_plan_commit_recovered" @@ -458,7 +497,12 @@ def lose_response(*args, **kwargs): "agent_id": "unregistered-agent", "reason_code": "agent_not_registered", }] - assert state.read_bytes() == before + assert "Receiver revised the work." in state.read_text() + assert _todos(project).count("loopx:todo ") == (1 if has_gap else 2) + assert read_canonical_todos_if_promoted( + runtime_root=project.parent / "runtime", + goal_id=GOAL_ID, + )["provider_revision"] == committed["provider_revision"] def test_equal_text_lanes_keep_distinct_identity_and_owner(tmp_path: Path) -> None: From 11f3b2badbbb5835f0757d4e26e56ec7435834e5 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 05:39:55 +0800 Subject: [PATCH 11/98] test(hosts): migrate release fixtures to canonical authority Signed-off-by: Duang777 --- scripts/qualify-claude-goal-release.py | 24 ++++-- scripts/qualify-native-goal-release.py | 86 ++++++++++++++++--- .../test_native_goal_release_qualification.py | 15 ++-- 3 files changed, 96 insertions(+), 29 deletions(-) diff --git a/scripts/qualify-claude-goal-release.py b/scripts/qualify-claude-goal-release.py index 0f0d475a7d..20a3bcdb4f 100644 --- a/scripts/qualify-claude-goal-release.py +++ b/scripts/qualify-claude-goal-release.py @@ -28,6 +28,17 @@ MANIFEST_ASSETS = {"assets/alpha.txt": b"alpha\n", "assets/nested/beta.bin": bytes(range(256))} +def _replan_active_state() -> str: + return ( + "---\nstatus: active\n---\n\n## Objective\n\nDeliver and validate TASK.md.\n\n" + "## Next Action\n\nVerify artifact integrity, beyond the completed filename inventory.\n\n" + "## User Todo\n\n## Agent Todo\n\n" + "- [ ] [P1] Inventory the regular asset filenames in manifest.json.\n" + " \n" + ) + + def run_host(command: list[str], *, cwd: Path, env: dict, timeout: float) -> str: """Bound output memory and clean this test's process group, including on timeout.""" with tempfile.TemporaryFile() as stdout, tempfile.TemporaryFile() as stderr: @@ -157,7 +168,10 @@ def setup_replan(root: Path) -> tuple[Path, Path, Path]: """A legitimately finished inventory stage, not fabricated Goal acceptance.""" from loopx.goal_mode_mcp import GoalModeMCPConfig, GoalModeMCPControlPlane - project, runtime, launcher = shared.setup(root) + project, runtime, launcher = shared.setup( + root, + active_state_text=_replan_active_state(), + ) assets = project / "assets" for name, content in MANIFEST_ASSETS.items(): path = project / name @@ -165,14 +179,6 @@ def setup_replan(root: Path) -> tuple[Path, Path, Path]: path.write_bytes(content) (project / "manifest.json").write_text(json.dumps(["assets/alpha.txt", "assets/nested/beta.bin"])) shutil.copyfile(REPO / "tests/fixtures/host_vision_replan/TASK.md", project / "TASK.md") - (project / "ACTIVE_GOAL_STATE.md").write_text( - "---\nstatus: active\n---\n\n## Objective\n\nDeliver and validate TASK.md.\n\n" - "## Next Action\n\nVerify artifact integrity, beyond the completed filename inventory.\n\n" - "## User Todo\n\n## Agent Todo\n\n" - "- [ ] [P1] Inventory the regular asset filenames in manifest.json.\n" - " \n", - ) assert json.loads((project / "manifest.json").read_text()) == sorted( str(path.relative_to(project)) for path in assets.rglob("*") if path.is_file()) control = GoalModeMCPControlPlane(GoalModeMCPConfig(server_name="fixture", diff --git a/scripts/qualify-native-goal-release.py b/scripts/qualify-native-goal-release.py index 9af62a2ea4..c1b65664bd 100644 --- a/scripts/qualify-native-goal-release.py +++ b/scripts/qualify-native-goal-release.py @@ -22,6 +22,24 @@ TODOS = {"todo_reducer", "todo_cli"} +def _default_active_state() -> str: + return ( + '---\nstatus: active-read-only\nowner_mode: goal\n' + 'objective: "Deliver the finite local ledger specification."\n---\n\n' + '# Ledger\n\n## Objective\n\nDeliver and validate TASK.md.\n\n' + '## Next Action\n\nImplement the reducer, then the CLI.\n\n' + '## User Todo\n\n## Agent Todo\n\n' + '- [ ] [P1] Implement and validate the TASK.md reducer.\n' + ' \n' + '- [ ] [P1] Implement the CLI, verify full acceptance and close this finite Goal.\n' + ' \n' + ) + + def host_environment(root: Path, launcher: Path) -> dict[str, str]: """Explicit transport/tool environment; never clone the operator's environment.""" home, temporary = root / "home", root / "tmp" @@ -83,7 +101,11 @@ def prerequisite_failure(codex: str) -> str | None: return None -def setup(root: Path) -> tuple[Path, Path, Path]: +def setup( + root: Path, + *, + active_state_text: str | None = None, +) -> tuple[Path, Path, Path]: # Reuse the existing real CLI fixture, not an alternate registry contract. spec = importlib.util.spec_from_file_location( "heartbeat_fixture", REPO / "examples/control_plane/heartbeat-quota-flow-smoke.py", @@ -99,19 +121,7 @@ def setup(root: Path) -> tuple[Path, Path, Path]: runtime.mkdir(parents=True, exist_ok=True) (runtime / "registry.global.json").write_text(json.dumps(config)) (project / "ACTIVE_GOAL_STATE.md").write_text( - '---\nstatus: active-read-only\nowner_mode: goal\n' - 'objective: "Deliver the finite local ledger specification."\n---\n\n' - '# Ledger\n\n## Objective\n\nDeliver and validate TASK.md.\n\n' - '## Next Action\n\nImplement the reducer, then the CLI.\n\n' - '## User Todo\n\n## Agent Todo\n\n' - '- [ ] [P1] Implement and validate the TASK.md reducer.\n' - ' \n' - '- [ ] [P1] Implement the CLI, verify full acceptance and close this finite Goal.\n' - ' \n', + active_state_text if active_state_text is not None else _default_active_state(), encoding="utf-8", ) shutil.copyfile(FIXTURE / "TASK.md", project / "TASK.md") @@ -128,6 +138,54 @@ def setup(root: Path) -> tuple[Path, Path, Path]: for args in (("init", "-q"), ("remote", "add", "origin", "https://example.com/ledger.git"), ("add", "TASK.md"), ("commit", "-qm", "Initialize synthetic acceptance")): subprocess.run([*git, *args], check=True, capture_output=True, timeout=30) + from loopx.state_backup import build_state_backup_plan, execute_state_backup_plan + + backup = execute_state_backup_plan( + build_state_backup_plan( + project=project, + runtime_root=runtime, + output_dir=root / "backups", + backup_id="release-goal-source", + include_automations=False, + include_skills=False, + include_registry_projects=False, + registry_path=registry, + ) + ) + if backup.get("ok") is not True: + raise RuntimeError("release fixture source backup failed") + prepared = cli( + launcher, + "coordination-shadow", + "prepare-import", + "--goal-id", + GOAL, + "--operation-id", + "release-goal-source", + "--backup-manifest", + str(backup["manifest_path"]), + "--provider", + "file", + "--target-handoff-mode", + "soft_claim", + )["cold_import"] + if prepared.get("status") != "prepared": + raise RuntimeError("release fixture import plan failed") + applied = cli( + launcher, + "coordination-shadow", + "apply-import", + "--goal-id", + GOAL, + "--operation-id", + "release-goal-source", + "--plan-sha256", + str(prepared["plan_sha256"]), + "--writers-stopped", + "--execute", + )["cold_import"] + if applied.get("status") not in {"applied", "replayed"}: + raise RuntimeError("release fixture canonical import failed") return project, runtime, launcher diff --git a/tests/test_native_goal_release_qualification.py b/tests/test_native_goal_release_qualification.py index 6d476d3494..df2345f5c0 100644 --- a/tests/test_native_goal_release_qualification.py +++ b/tests/test_native_goal_release_qualification.py @@ -127,20 +127,23 @@ def test_synthetic_fixture_real_cli_projects_identity_reentry(tmp_path): assert len(actions) == 1 and "--turn-instance-id" in actions[0] assert "spend-slot" not in actions[0] - # Qualify the fixture's terminal shape without paying for a model run. A - # missing user section is not proof of zero user obligations. + # The Markdown file is only the display projection after migration. A + # direct edit cannot fabricate canonical completion or Goal terminality. state = project / "ACTIVE_GOAL_STATE.md" state.write_text(state.read_text().replace("- [ ]", "- [x]").replace( "status=open", "status=done no_followup=true", )) - terminal = runner.cli(launcher, "quota", "should-run", "--runtime-profile", "codex_cli", - "--goal-id", runner.GOAL, "--agent-id", runner.AGENT) - assert terminal["should_run"] is False - assert terminal["interaction_contract"]["mode"] == "terminal_no_followup" + unchanged = runner.cli(launcher, "quota", "should-run", "--runtime-profile", "codex_cli", + "--goal-id", runner.GOAL, "--agent-id", runner.AGENT) + assert unchanged["should_run"] is True + assert unchanged["selected_todo"]["todo_id"] == "todo_reducer" + assert unchanged["interaction_contract"]["mode"] != "terminal_no_followup" state.write_text(state.read_text().replace("## User Todo\n\n", "")) incomplete = runner.cli(launcher, "quota", "should-run", "--runtime-profile", "codex_cli", "--goal-id", runner.GOAL, "--agent-id", runner.AGENT) + assert incomplete["should_run"] is True + assert incomplete["selected_todo"]["todo_id"] == "todo_reducer" assert incomplete["interaction_contract"]["mode"] != "terminal_no_followup" From 665da4dfa639c3080648e905c162c7421d39a0ee Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 06:00:35 +0800 Subject: [PATCH 12/98] fix(demo): initialize canonical todo authority Signed-off-by: Duang777 --- examples/collaboration-delivery/demo.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/examples/collaboration-delivery/demo.py b/examples/collaboration-delivery/demo.py index 80dfd9ed89..53d0d6b7a9 100644 --- a/examples/collaboration-delivery/demo.py +++ b/examples/collaboration-delivery/demo.py @@ -15,6 +15,9 @@ from loopx.control_plane.collaboration.peers import returns from loopx.capabilities.manager_context.roundtrip import drain from loopx.chat_store import ChatSessionStore +from loopx.control_plane.coordination.goal_storage_initialization import ( + initialize_goal_storage_target, +) HERE = Path(__file__).resolve().parent GOAL = "allocation-demo" @@ -173,6 +176,7 @@ def prepare(root): "goals": [ { "id": GOAL, + "creation_operation_id": f"goal-create:{GOAL}", "domain": "synthetic-allocation", "status": "active", "repo": str(project), @@ -183,11 +187,27 @@ def prepare(root): "agent_model": "peer_v1", "registered_agents": list(ACTORS), "write_scope": ["**"], + "storage_target": { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "file", + "handoff_mode": "soft_claim", + }, }, } ], } - write(root / "registry.json", registry) + registry_path = root / "registry.json" + write(registry_path, registry) + initialized = initialize_goal_storage_target( + root / "runtime", + registry["goals"][0], + registry_path=registry_path, + ) + if not isinstance(initialized, dict) or ( + initialized.get("authority_initialized") is not True + or initialized.get("legacy_fallback_used") is not False + ): + raise RuntimeError("demo canonical authority initialization failed") for actor in ACTORS: workspace = worker(root, actor) workspace.parent.mkdir(exist_ok=True) From 478f32c4d7024ca429ad5e51ffbcffe0386ea13e Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 06:27:05 +0800 Subject: [PATCH 13/98] test(mcp): initialize canonical settlement fixtures Signed-off-by: Duang777 --- tests/test_goal_mode_mcp_settlement.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_goal_mode_mcp_settlement.py b/tests/test_goal_mode_mcp_settlement.py index 5a76e5e292..3a8d84284d 100644 --- a/tests/test_goal_mode_mcp_settlement.py +++ b/tests/test_goal_mode_mcp_settlement.py @@ -13,6 +13,9 @@ host_adapter_turn_instance_id, settle_host_todo_completion, ) +from loopx.control_plane.coordination.goal_storage_initialization import ( + initialize_goal_storage_target, +) from loopx.goal_mode_mcp import GoalModeMCPConfig, GoalModeMCPControlPlane from loopx.status import parse_active_state_todos from loopx.todos import add_goal_todo @@ -196,6 +199,7 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path]: "goals": [ { "id": GOAL_ID, + "creation_operation_id": f"goal-create:{GOAL_ID}", "domain": "harness_self_improvement", "status": "active", "repo": str(project), @@ -212,6 +216,11 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path]: "coordination": { "agent_model": "peer_v1", "registered_agents": [AGENT_ID], + "storage_target": { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "file", + "handoff_mode": "soft_claim", + }, }, } ], @@ -219,6 +228,14 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path]: ), encoding="utf-8", ) + goal = json.loads(registry.read_text(encoding="utf-8"))["goals"][0] + initialized = initialize_goal_storage_target( + tmp_path / "runtime", + goal, + registry_path=registry, + ) + assert initialized["authority_initialized"] is True + assert initialized["legacy_fallback_used"] is False return registry, state_file From 4662ffffbe0adda3064f3dc15fae4c336b9c2ef7 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 06:52:42 +0800 Subject: [PATCH 14/98] test(kiro): initialize canonical MCP fixtures Signed-off-by: Duang777 --- tests/test_kiro_cli_mcp.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_kiro_cli_mcp.py b/tests/test_kiro_cli_mcp.py index 93b297b6fe..af00cddd6f 100644 --- a/tests/test_kiro_cli_mcp.py +++ b/tests/test_kiro_cli_mcp.py @@ -21,6 +21,9 @@ import pytest from loopx import slash_command_install +from loopx.control_plane.coordination.goal_storage_initialization import ( + initialize_goal_storage_target, +) from loopx.goal_mode_mcp import GoalModeMCPControlPlane from loopx.kiro_cli_goal_mode import ( KIRO_CLI_MCP_CONFIG_SUBPATH, @@ -62,6 +65,7 @@ def _write_project( "goals": [ { "id": GOAL_ID, + "creation_operation_id": f"goal-create:{GOAL_ID}", "domain": "harness_self_improvement", "status": "active", "repo": str(project), @@ -80,6 +84,11 @@ def _write_project( # A different agent first: resolution must never # fall back to registry order. "registered_agents": [OTHER_AGENT_ID, AGENT_ID], + "storage_target": { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "file", + "handoff_mode": "soft_claim", + }, "thread_agent_bindings": ( bindings if bindings is not None @@ -98,6 +107,14 @@ def _write_project( ), encoding="utf-8", ) + goal = json.loads(registry.read_text(encoding="utf-8"))["goals"][0] + initialized = initialize_goal_storage_target( + tmp_path / "runtime", + goal, + registry_path=registry, + ) + assert initialized["authority_initialized"] is True + assert initialized["legacy_fallback_used"] is False return project, registry, state_file From 3c06602887ec621a892ffda883277b549b80a5dc Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 06:55:25 +0800 Subject: [PATCH 15/98] test(todos): retire legacy writer serialization coverage Signed-off-by: Duang777 --- tests/test_todo_write_serialization.py | 152 ------------------------- 1 file changed, 152 deletions(-) delete mode 100644 tests/test_todo_write_serialization.py diff --git a/tests/test_todo_write_serialization.py b/tests/test_todo_write_serialization.py deleted file mode 100644 index 602ca9e73f..0000000000 --- a/tests/test_todo_write_serialization.py +++ /dev/null @@ -1,152 +0,0 @@ -from __future__ import annotations - -from concurrent.futures import ThreadPoolExecutor -from contextlib import contextmanager -import json -from pathlib import Path -from threading import Barrier, Lock -from typing import Any, Iterator - -import pytest - -from loopx.control_plane.coordination import legacy_writer_fence -from loopx.control_plane.coordination.legacy_writer_fence import ( - legacy_coordination_todo_lock_path, -) -from loopx.status import parse_active_state_todos -from loopx.todos import add_goal_todo, update_goal_todo - - -GOAL_ID = "todo-write-serialization" -AGENT_ID = "fixture-agent" -INITIAL_TODO = "Update this Todo while another Todo is added." -CONCURRENT_TODO = "Preserve this concurrently added Todo." -UPDATE_REASON = "waiting-for-concurrent-dependency" - - -def _write_fixture(tmp_path: Path) -> tuple[Path, Path, Path]: - project = tmp_path / "project" - project.mkdir() - state_file = project / "ACTIVE_GOAL_STATE.md" - state_file.write_text( - "---\n" - f"goal_id: {GOAL_ID}\n" - "updated_at: 2026-09-07T00:00:00+00:00\n" - "---\n\n" - "## Agent Todo\n\n", - encoding="utf-8", - ) - runtime_root = tmp_path / "runtime" - registry_path = tmp_path / "registry.global.json" - registry_path.write_text( - json.dumps( - { - "common_runtime_root": str(runtime_root), - "goals": [ - { - "id": GOAL_ID, - "domain": "todo-write-serialization-fixture", - "status": "active", - "repo": str(project), - "state_file": state_file.name, - "adapter": {"kind": "fixture"}, - "coordination": { - "agent_model": "peer_v1", - "registered_agents": [AGENT_ID], - }, - } - ], - } - ), - encoding="utf-8", - ) - return registry_path, state_file, runtime_root - - -def test_concurrent_add_and_update_share_the_goal_todo_write_lock( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry_path, state_file, runtime_root = _write_fixture(tmp_path) - target = add_goal_todo( - registry_path=registry_path, - goal_id=GOAL_ID, - role="agent", - text=INITIAL_TODO, - task_class="advancement_task", - claimed_by=AGENT_ID, - ) - target_id = str(target["todo_id"]) - goal_lock_path = legacy_coordination_todo_lock_path( - runtime_root=runtime_root, - goal_id=GOAL_ID, - ) - - arrivals = Barrier(2) - records_guard = Lock() - locks_by_path: dict[Path, Lock] = {} - lock_attempts: list[tuple[Path, str | None]] = [] - - @contextmanager - def deterministic_lock( - path: Path, - **kwargs: Any, - ) -> Iterator[Path]: - with records_guard: - path_lock = locks_by_path.setdefault(path, Lock()) - lock_attempts.append((path, kwargs.get("operation"))) - if path == goal_lock_path: - arrivals.wait(timeout=5) - with path_lock: - yield path - - monkeypatch.setattr( - legacy_writer_fence, - "exclusive_cross_runtime_file_lock", - deterministic_lock, - ) - - with ThreadPoolExecutor(max_workers=2) as executor: - add_future = executor.submit( - add_goal_todo, - registry_path=registry_path, - goal_id=GOAL_ID, - role="agent", - text=CONCURRENT_TODO, - task_class="advancement_task", - claimed_by=AGENT_ID, - ) - update_future = executor.submit( - update_goal_todo, - registry_path=registry_path, - goal_id=GOAL_ID, - todo_id=target_id, - status="blocked", - reason=UPDATE_REASON, - agent_id=AGENT_ID, - ) - added = add_future.result(timeout=10) - updated = update_future.result(timeout=10) - - assert added["added"] is True, added - assert updated["changed"] is True, updated - assert [attempt for attempt in lock_attempts if attempt[0] == goal_lock_path] == [ - (goal_lock_path, "legacy_coordination_todo_write"), - (goal_lock_path, "legacy_coordination_todo_write"), - ] - assert sorted( - operation - for path, operation in lock_attempts - if path == state_file - ) == ["todo_add", "todo_update"] - - parsed = parse_active_state_todos(state_file.read_text(encoding="utf-8")) - items = parsed["agent_todos"]["items"] - todo_ids = [str(item["todo_id"]) for item in items] - assert len(todo_ids) == len(set(todo_ids)), items - - by_id = {str(item["todo_id"]): item for item in items} - assert str(added["todo_id"]) in by_id, items - assert by_id[str(added["todo_id"])]["text"] == CONCURRENT_TODO - assert by_id[target_id]["status"] == "blocked" - assert by_id[target_id]["reason"] == UPDATE_REASON From bb49ae121221c855c97411b18c96cae6c1192eec Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 06:56:41 +0800 Subject: [PATCH 16/98] test(team-plan): migrate settlement fixtures to authority Signed-off-by: Duang777 --- tests/test_steward_team_plan_apply.py | 50 ++++++++++++--------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/tests/test_steward_team_plan_apply.py b/tests/test_steward_team_plan_apply.py index 910e95817b..01a3663186 100644 --- a/tests/test_steward_team_plan_apply.py +++ b/tests/test_steward_team_plan_apply.py @@ -8,6 +8,10 @@ import pytest +from control_plane.canonical_authority_fixture import initialize_canonical_authority +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.control_plane.work_items.governed_transition_proposal import ( GovernedTransitionSettlementPhase, settle_governed_transition_proposals, @@ -71,6 +75,16 @@ def _fixture( ), encoding="utf-8", ) + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[], + handoff_mode="soft_claim", + ), + state_path=state_path, + ) return project, registry_path @@ -110,30 +124,6 @@ def _basis(registry_path: Path) -> str: ) -def test_legacy_registry_stays_locked_through_plan_and_write(tmp_path, monkeypatch): - from loopx.file_lock import exclusive_cross_runtime_file_lock, LockAcquireTimeoutError - from loopx.control_plane.work_items import team_plan_adapter - - _project, registry = _fixture(tmp_path) - original_effect = team_plan_adapter.effect_runtime_result - checked = [] - - def inspect(method, request): - if method == "work_items.team_plan.plan": - with pytest.raises(LockAcquireTimeoutError): - with exclusive_cross_runtime_file_lock(registry, timeout_seconds=0): - pass - checked.append(True) - return original_effect(method, request) - - monkeypatch.setattr(team_plan_adapter, "effect_runtime_result", inspect) - result = _settle(registry, _proposal()) - assert result[0]["status"] == "committed", result - assert checked == [True] - with exclusive_cross_runtime_file_lock(registry, timeout_seconds=0): - pass - - def _settle( registry_path: Path, proposal: dict, @@ -651,11 +641,17 @@ def test_a_failed_receipt_carries_a_typed_verdict_and_no_lane_identity() -> None def test_batch_write_failure_leaves_every_lane_unwritten(tmp_path: Path, monkeypatch) -> None: from loopx.control_plane.work_items import team_plan_adapter + project, registry_path = _fixture(tmp_path, agents=(AGENT_ID, "agent-beta")) before = _todos(project) - def fail(*args, **kwargs): - raise OSError("batch write unavailable") - monkeypatch.setattr(team_plan_adapter, "write_captured_todo_state", fail) + original_effect = team_plan_adapter.effect_runtime_result + + def fail(method, request): + if method == "work_items.team_plan.commit": + raise OSError("batch write unavailable") + return original_effect(method, request) + + monkeypatch.setattr(team_plan_adapter, "effect_runtime_result", fail) with pytest.raises(OSError, match="batch write unavailable"): _settle(registry_path, _proposal(extra_lane=_second_lane())) assert _todos(project) == before From 6a7b754762abeeba35e08b3cc2c97022ef71a24f Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 07:08:20 +0800 Subject: [PATCH 17/98] test(explore): initialize canonical turn context fixture Signed-off-by: Duang777 --- .../capabilities/test_explore_turn_context.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/capabilities/test_explore_turn_context.py b/tests/capabilities/test_explore_turn_context.py index 0bacfad5e8..e7762be039 100644 --- a/tests/capabilities/test_explore_turn_context.py +++ b/tests/capabilities/test_explore_turn_context.py @@ -23,6 +23,9 @@ effect_runtime_result, EffectRuntimeRejected, ) +from loopx.control_plane.coordination.goal_storage_initialization import ( + initialize_goal_storage_target, +) from loopx.explore_graph import compact_explore_graph_policy from loopx.todos import add_goal_todo @@ -38,10 +41,18 @@ def registry(tmp_path, *, graph=False, planning=False): "goals": [ { "id": "research", + "creation_operation_id": "goal-create:research", "status": "active", "repo": str(tmp_path), "state_file": "goal.md", - "coordination": {"registered_agents": ["worker"]}, + "coordination": { + "registered_agents": ["worker"], + "storage_target": { + "schema_version": "loopx_new_goal_storage_target_v1", + "provider": "file", + "handoff_mode": "soft_claim", + }, + }, "explore_graph": {"enabled": graph}, "spawn_policy": { "allowed": False, @@ -52,6 +63,14 @@ def registry(tmp_path, *, graph=False, planning=False): } ) ) + goal = json.loads(path.read_text(encoding="utf-8"))["goals"][0] + initialized = initialize_goal_storage_target( + tmp_path / "runtime", + goal, + registry_path=path, + ) + assert initialized["authority_initialized"] is True + assert initialized["legacy_fallback_used"] is False return path From 74a6a1bec7b7a6949d81a312857f1a553efab47a Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 07:18:47 +0800 Subject: [PATCH 18/98] fix(authority): align fail-closed qualification Retire the stale legacy writer exception, keep existing Markdown bootstrap fail-closed, and split canonical registration and update decisions to satisfy the maintainability gates. Signed-off-by: Duang777 --- loopx/canary/maintainability_ratchet.py | 7 - .../goals/goal_frontier/__init__.py | 13 +- loopx/control_plane/projects/registry.py | 136 ++++++++++-------- loopx/control_plane/todos/provider_update.py | 96 +++++++------ tests/canary/test_maintainability_ratchet.py | 4 +- .../test_goal_terminal_no_followup.py | 109 ++++++-------- 6 files changed, 177 insertions(+), 188 deletions(-) diff --git a/loopx/canary/maintainability_ratchet.py b/loopx/canary/maintainability_ratchet.py index 4ccc88748f..ebad2d40c4 100644 --- a/loopx/canary/maintainability_ratchet.py +++ b/loopx/canary/maintainability_ratchet.py @@ -49,13 +49,6 @@ def _exception( REVIEWED_MAINTAINABILITY_EXCEPTIONS: dict[str, dict[str, Any]] = { - "dependency_debt:loopx.control_plane.todos.legacy_mutation->loopx.capabilities.explore.research_frontier": _exception( - "The unpromoted legacy Todo writer was moved out of loopx.todos without changing its " - "explicit Explore completion evidence guard. The existing capability owner still " - "qualifies that evidence; omitting the guard would let legacy completion bypass M3.", - "Retire this bridge with the legacy writer, or replace it with a provider-neutral " - "injected terminal guard before removing this exact exception.", - ), "compatibility_facade:loopx.quota": _exception( "The public loopx.quota import surface remains a supported compatibility contract, " "including presentation-owned quota event renderers.", diff --git a/loopx/control_plane/goals/goal_frontier/__init__.py b/loopx/control_plane/goals/goal_frontier/__init__.py index dd25008ab5..49b77b7272 100644 --- a/loopx/control_plane/goals/goal_frontier/__init__.py +++ b/loopx/control_plane/goals/goal_frontier/__init__.py @@ -43,9 +43,7 @@ completed_todo_replan_threshold, goal_vision_repeats_advancement_until_closed, ) -from ..goal_vision_state import ( - goal_vision_state_is_closed, -) +from ..goal_vision_state import goal_vision_state_is_closed from ..goal_vision_read_model import ( VISION_ACCEPTANCE_GAP_TRIGGER as VISION_ACCEPTANCE_GAP_TRIGGER, VISION_SUCCESSOR_GAP_TRIGGER as VISION_SUCCESSOR_GAP_TRIGGER, @@ -72,20 +70,15 @@ declared_fallback_gap_from_agent_vision, parse_fallback_declarations, # noqa: F401 ) -from .long_todo_chain import ( - LONG_TODO_CHAIN_TRIGGER, - evaluate_long_todo_chain, -) +from .long_todo_chain import LONG_TODO_CHAIN_TRIGGER, evaluate_long_todo_chain from .replan_rules import ( GoalFrontierReplanFacts, GoalFrontierReplanRule, select_goal_frontier_replan_rule, ) -from .semantic_history import ( - latest_agent_vision_from_runs as latest_agent_vision_from_runs, -) from .semantic_history import ( _latest_runs_for_goal, + latest_agent_vision_from_runs as latest_agent_vision_from_runs, latest_agent_vision_from_status_payload, latest_autonomous_replan_ack_from_status_payload, latest_missing_vision_checkpoint_from_status_payload, diff --git a/loopx/control_plane/projects/registry.py b/loopx/control_plane/projects/registry.py index 746e81b7f7..1008dfb610 100644 --- a/loopx/control_plane/projects/registry.py +++ b/loopx/control_plane/projects/registry.py @@ -141,6 +141,76 @@ def _resolve_exact_project_binding( } +def _bind_project_goal_storage( + *, + registry: dict[str, Any], + registry_path: Path, + runtime_root: Path | None, + project_id: str, + goal_id: str, + goal_record: dict[str, Any], + resolve_new_goal_storage_target: Callable[[Path], dict[str, Any]], +) -> Path: + effective_root = resolve_runtime_root( + registry, + str(runtime_root) if runtime_root else None, + registry_path=registry_path, + ) + existing_goal = next( + ( + item + for item in registry.get("goals", []) + if isinstance(item, dict) and item.get("id") == goal_id + ), + None, + ) + existing_target = ( + (existing_goal.get("coordination") or {}).get("storage_target") + if existing_goal is not None + else None + ) + resumable_creation = ( + isinstance(existing_target, dict) + and existing_target.get("schema_version") + == "loopx_new_goal_storage_target_v1" + and isinstance(existing_goal.get("creation_operation_id"), str) + and existing_goal["creation_operation_id"] != "" + ) + existing_authority = ( + read_canonical_todos_if_promoted( + runtime_root=effective_root, + goal_id=goal_id, + ) + if existing_goal is None or not resumable_creation + else None + ) + if existing_goal is None and existing_authority is not None: + raise ValueError( + f"goal_id is already bound to canonical authority through another registry: {goal_id}" + ) + if existing_goal is not None and existing_authority is None and not resumable_creation: + require_canonical_authority(goal_id=goal_id, operation="goal_create") + storage_target = ( + existing_target + if existing_goal is not None + else resolve_new_goal_storage_target(effective_root) + ) + if ( + existing_goal is None + and (storage_target or {}).get("schema_version") + != "loopx_new_goal_storage_target_v1" + ): + require_canonical_authority(goal_id=goal_id, operation="goal_create") + if storage_target is not None: + goal_record.setdefault("coordination", {})["storage_target"] = storage_target + goal_record["creation_operation_id"] = ( + (existing_goal or {}).get("creation_operation_id") + or "goal-create:project-register:" + + hashlib.sha256(f"{project_id}\0{goal_id}".encode()).hexdigest()[:24] + ) + return effective_root + + def _register_project_goal_unlocked( *, registry_path: Path, @@ -266,66 +336,14 @@ def _register_project_goal_unlocked( "registry_role": "project-local", "common_runtime_root": str(runtime_root or select_default_runtime_root()), } - effective_root = resolve_runtime_root( - initial_registry, - str(runtime_root) if runtime_root else None, + effective_root = _bind_project_goal_storage( + registry=initial_registry, registry_path=registry_path, - ) - existing_goal_record = next( - ( - item - for item in initial_registry.get("goals", []) - if isinstance(item, dict) and item.get("id") == goal_id - ), - None, - ) - existing_target = ( - (existing_goal_record.get("coordination") or {}).get("storage_target") - if existing_goal_record is not None - else None - ) - resumable_creation = ( - isinstance(existing_target, dict) - and existing_target.get("schema_version") - == "loopx_new_goal_storage_target_v1" - and isinstance(existing_goal_record.get("creation_operation_id"), str) - and existing_goal_record["creation_operation_id"] != "" - ) - existing_authority = ( - read_canonical_todos_if_promoted( - runtime_root=effective_root, - goal_id=goal_id, - ) - if existing_goal_record is None or not resumable_creation - else None - ) - if existing_goal_record is None and existing_authority is not None: - raise ValueError( - f"goal_id is already bound to canonical authority through another registry: {goal_id}" - ) - if ( - existing_goal_record is not None - and existing_authority is None - and not resumable_creation - ): - require_canonical_authority(goal_id=goal_id, operation="goal_create") - storage_target = ( - existing_target - if existing_goal_record is not None - else resolve_new_goal_storage_target(effective_root) - ) - if ( - existing_goal_record is None - and (storage_target or {}).get("schema_version") - != "loopx_new_goal_storage_target_v1" - ): - require_canonical_authority(goal_id=goal_id, operation="goal_create") - if storage_target is not None: - goal_record.setdefault("coordination", {})["storage_target"] = storage_target - goal_record["creation_operation_id"] = ( - (existing_goal_record or {}).get("creation_operation_id") - or "goal-create:project-register:" - + hashlib.sha256(f"{project_id}\0{goal_id}".encode()).hexdigest()[:24] + runtime_root=runtime_root, + project_id=project_id, + goal_id=goal_id, + goal_record=goal_record, + resolve_new_goal_storage_target=resolve_new_goal_storage_target, ) changed = True diff --git a/loopx/control_plane/todos/provider_update.py b/loopx/control_plane/todos/provider_update.py index 3e67bd2af9..fa81931e38 100644 --- a/loopx/control_plane/todos/provider_update.py +++ b/loopx/control_plane/todos/provider_update.py @@ -177,6 +177,58 @@ def _publish_completion_validation_revision( ) +def _raise_public_update_rejection( + result: dict[str, Any], + *, + planning_intent: dict[str, Any] | None, +) -> None: + if result.get("status") != "failed": + return + reason_code = str(result.get("reason_code") or "") + if reason_code.startswith("external_wait_"): + resume_when = str((planning_intent or {}).get("resume_when") or "") + resume_kind, separator, dependency_id = resume_when.partition(":") + raw_successors = (planning_intent or {}).get("successor_todo_ids") + raise TodoExternalWaitAuthoringError( + str(result.get("reason") or "external-wait Todo update was rejected"), + code=reason_code, + monitor_todo_id=( + dependency_id + if separator and resume_kind == "monitor_changed" + else None + ), + successor_todo_ids=( + [str(item) for item in raw_successors] + if isinstance(raw_successors, list) + else None + ), + ) + if reason_code == "todo_not_found": + raise ValueError("Todo is missing from canonical authority") + if reason_code == "todo_role_mismatch": + raise ValueError("Todo does not have the requested role") + rejection_messages = { + "coordination_operation_identity_mismatch": ( + "monitor effect identity is already bound to a different observation" + ), + "update_owner_mismatch": ( + "Todo claimed_by does not grant this actor update authority" + ), + } + if reason_code in rejection_messages: + raise LocalCoordinationAuthorityRejection( + rejection_messages[reason_code], + code=reason_code, + payload=result, + ) + if result.get("failure_kind") == "decision_rejection": + raise LocalCoordinationAuthorityRejection( + str(result.get("reason") or "canonical Todo update was rejected"), + code=reason_code or "todo_update_rejected", + payload=result, + ) + + def update_canonical_todo_if_promoted( *, registry_path: Path, runtime_root: Path, goal_id: str, todo_id: str, actor_agent_id: str | None, role: str | None, text: str | None, @@ -279,48 +331,8 @@ def update_canonical_todo_if_promoted( if failure is not None: return failure # Keep the public lookup-error contract, without a second pre-transaction read. - if isinstance(result, dict) and result.get("status") == "failed": - reason_code = str(result.get("reason_code") or "") - if reason_code.startswith("external_wait_"): - resume_when = str((planning_intent or {}).get("resume_when") or "") - resume_kind, separator, dependency_id = resume_when.partition(":") - raw_successors = (planning_intent or {}).get("successor_todo_ids") - raise TodoExternalWaitAuthoringError( - str(result.get("reason") or "external-wait Todo update was rejected"), - code=reason_code, - monitor_todo_id=( - dependency_id - if separator and resume_kind == "monitor_changed" - else None - ), - successor_todo_ids=( - [str(item) for item in raw_successors] - if isinstance(raw_successors, list) - else None - ), - ) - if reason_code == "todo_not_found": - raise ValueError("Todo is missing from canonical authority") - if reason_code == "todo_role_mismatch": - raise ValueError("Todo does not have the requested role") - if reason_code == "coordination_operation_identity_mismatch": - raise LocalCoordinationAuthorityRejection( - "monitor effect identity is already bound to a different observation", - code=reason_code, - payload=result, - ) - if reason_code == "update_owner_mismatch": - raise LocalCoordinationAuthorityRejection( - "Todo claimed_by does not grant this actor update authority", - code=reason_code, - payload=result, - ) - if result.get("failure_kind") == "decision_rejection": - raise LocalCoordinationAuthorityRejection( - str(result.get("reason") or "canonical Todo update was rejected"), - code=reason_code or "todo_update_rejected", - payload=result, - ) + if isinstance(result, dict): + _raise_public_update_rejection(result, planning_intent=planning_intent) if isinstance(result, dict) and ( result.get("status") == "missing" and result.get("source_authority") in LOCAL_AUTHORITY_SOURCES diff --git a/tests/canary/test_maintainability_ratchet.py b/tests/canary/test_maintainability_ratchet.py index b7292e45f2..6e8f1a6a56 100644 --- a/tests/canary/test_maintainability_ratchet.py +++ b/tests/canary/test_maintainability_ratchet.py @@ -33,9 +33,7 @@ def test_current_repository_debt_is_reviewed_without_line_count_pins() -> None: ] assert report["unreviewed_count"] == 0 assert report["stale_exception_count"] == 0 - # The legacy Todo writer's Explore guard moved with that writer. Its - # exact dependency remains reviewed until the legacy path is retired. - assert report["category_counts"] == {"compatibility_facade": 2, "dependency_debt": 1} + assert report["category_counts"] == {"compatibility_facade": 2} assert report["category_counts"].get("oversized_decision_function", 0) == 0 assert report["reviewed_exception_count"] == report["finding_count"] # One scan of the immutable checkout owns both debt and metric assertions. diff --git a/tests/control_plane/test_goal_terminal_no_followup.py b/tests/control_plane/test_goal_terminal_no_followup.py index d2d77aff58..66b0988b18 100644 --- a/tests/control_plane/test_goal_terminal_no_followup.py +++ b/tests/control_plane/test_goal_terminal_no_followup.py @@ -1,10 +1,15 @@ from __future__ import annotations +import pytest + from loopx.bootstrap import ( bootstrap_project, render_state_markdown, repair_missing_todo_source_sections, ) +from loopx.control_plane.coordination.local_authority import ( + CanonicalAuthorityRequired, +) from loopx.control_plane.goals.goal_frontier import ( build_goal_frontier_projection_from_summaries, goal_frontier_is_terminal_no_followup, @@ -97,11 +102,10 @@ def test_existing_state_migration_preserves_recognized_legacy_source_heading() - assert "## Agent Todo" in migrated -def test_bootstrap_repairs_existing_state_todo_sources_in_place(tmp_path) -> None: +def test_bootstrap_rejects_existing_markdown_only_state_without_rewriting(tmp_path) -> None: state_file = tmp_path / ".codex/goals/goal-terminal-test/ACTIVE_GOAL_STATE.md" state_file.parent.mkdir(parents=True) - state_file.write_text( - """\ + original = """\ # Active Goal State ## Objective @@ -111,70 +115,41 @@ def test_bootstrap_repairs_existing_state_todo_sources_in_place(tmp_path) -> Non ## Next Action - Continue. -""", - encoding="utf-8", - ) - - result = bootstrap_project( - project=tmp_path, - registry_path=tmp_path / ".loopx/registry.json", - runtime_root=tmp_path / ".loopx-runtime", - goal_id="goal-terminal-test", - objective="Do not replace the existing objective.", - domain="issue-fix", - role="primary", - parent_goal_id=None, - state_file=state_file, - goal_doc=None, - adapter_kind="issue_fix_workflow_v0", - adapter_status="connected", - next_probe=None, - spawn_allowed=False, - max_children=0, - allowed_domains=[], - write_scope=[], - force=False, - dry_run=False, - sync_global=False, - ) - - repaired = state_file.read_text(encoding="utf-8") - assert result["state_action"] == "kept-existing" - assert result["todo_source_migration"] == { - "schema_version": "todo_source_section_migration_v0", - "added_roles": ["user", "agent"], - "applied": True, - } - assert "Preserve this existing state." in repaired - assert "Do not replace the existing objective." not in repaired - assert "## User Todo / Owner Review Reading Queue" in repaired - assert "## Agent Todo" in repaired - - second_result = bootstrap_project( - project=tmp_path, - registry_path=tmp_path / ".loopx/registry.json", - runtime_root=tmp_path / ".loopx-runtime", - goal_id="goal-terminal-test", - objective="Still do not replace the existing objective.", - domain="issue-fix", - role="primary", - parent_goal_id=None, - state_file=state_file, - goal_doc=None, - adapter_kind="issue_fix_workflow_v0", - adapter_status="connected", - next_probe=None, - spawn_allowed=False, - max_children=0, - allowed_domains=[], - write_scope=[], - force=False, - dry_run=False, - sync_global=False, - ) - - assert second_result["todo_source_migration"] is None - assert state_file.read_text(encoding="utf-8") == repaired +""" + state_file.write_text(original, encoding="utf-8") + registry_path = tmp_path / ".loopx/registry.json" + runtime_root = tmp_path / ".loopx-runtime" + + with pytest.raises(CanonicalAuthorityRequired) as caught: + bootstrap_project( + project=tmp_path, + registry_path=registry_path, + runtime_root=runtime_root, + goal_id="goal-terminal-test", + objective="Do not replace the existing objective.", + domain="issue-fix", + role="primary", + parent_goal_id=None, + state_file=state_file, + goal_doc=None, + adapter_kind="issue_fix_workflow_v0", + adapter_status="connected", + next_probe=None, + spawn_allowed=False, + max_children=0, + allowed_domains=[], + write_scope=[], + force=False, + dry_run=False, + sync_global=False, + ) + + assert caught.value.code == "canonical_authority_required" + assert caught.value.payload["operation"] == "goal_create" + assert caught.value.payload["legacy_fallback_used"] is False + assert state_file.read_text(encoding="utf-8") == original + assert not registry_path.exists() + assert not runtime_root.exists() def test_agent_only_no_followup_closes_explicit_empty_user_source() -> None: From 3c35f0f32718445fb30b0303e3ec8b3687424a37 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 07:25:23 +0800 Subject: [PATCH 19/98] test(authority): retire shadow fallback fixture Use the shared canonical Explore fixture once and remove the last test that required public Todo and lease writes to fall back to Markdown. Signed-off-by: Duang777 --- .../test_explore_result_writeback.py | 17 +-- .../test_local_authority_shadow_runtime.py | 107 ------------------ 2 files changed, 1 insertion(+), 123 deletions(-) delete mode 100644 tests/control_plane/test_local_authority_shadow_runtime.py diff --git a/tests/control_plane/test_explore_result_writeback.py b/tests/control_plane/test_explore_result_writeback.py index 040c21ace7..7aa6ea04a7 100644 --- a/tests/control_plane/test_explore_result_writeback.py +++ b/tests/control_plane/test_explore_result_writeback.py @@ -30,22 +30,7 @@ def fixture(tmp_path, enabled=True): - from canonical_authority_fixture import initialize_canonical_authority - from loopx.control_plane.coordination.runtime_shadow import ( - build_todo_runtime_shadow_projection, - ) - path = registry(tmp_path, graph=enabled, planning=enabled) - runtime = tmp_path / "runtime" - initialize_canonical_authority( - runtime, - "research", - build_todo_runtime_shadow_projection( - goal_id="research", todos=[], handoff_mode="soft_claim" - ), - state_path=tmp_path / "goal.md", - provider="file", - ) todo = add_goal_todo( registry_path=path, goal_id="research", @@ -56,7 +41,7 @@ def fixture(tmp_path, enabled=True): ) return dict( registry_path=path, - runtime_root=runtime, + runtime_root=tmp_path / "runtime", goal_id="research", agent_id="worker", todo_id=todo["todo_id"], diff --git a/tests/control_plane/test_local_authority_shadow_runtime.py b/tests/control_plane/test_local_authority_shadow_runtime.py deleted file mode 100644 index 9a5629e2af..0000000000 --- a/tests/control_plane/test_local_authority_shadow_runtime.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import annotations - -import json -from pathlib import Path - -import pytest - -from loopx.control_plane.work_items.task_lease import ( - acquire_task_lease, - release_task_lease, - renew_task_lease, -) -from loopx.todos import ( - add_goal_todo, -) - - -GOAL_ID = "goal-shadow" -AGENT_A = "agent-a" -AGENT_B = "agent-b" - - -def _fixture(tmp_path: Path, *, enabled: bool) -> tuple[Path, Path, Path]: - repo = tmp_path / "repo" - repo.mkdir() - state = repo / "ACTIVE_GOAL_STATE.md" - state.write_text( - "---\n" - f"goal_id: {GOAL_ID}\n" - "handoff_mode: hard_lease\n" - "updated_at: 2026-09-02T00:00:00+00:00\n" - "---\n\n" - "## Agent Todo\n\n", - encoding="utf-8", - ) - runtime_root = tmp_path / "runtime" - coordination: dict[str, object] = { - "agent_model": "peer_v1", - "registered_agents": [AGENT_A, AGENT_B], - } - if enabled: - coordination["authority_shadow"] = { - "schema_version": "loopx_local_authority_shadow_config_v0", - "mode": "file_one_way", - } - registry = tmp_path / "registry.json" - registry.write_text( - json.dumps( - { - "common_runtime_root": str(runtime_root), - "goals": [ - { - "id": GOAL_ID, - "domain": "harness_self_improvement", - "status": "active", - "repo": str(repo), - "state_file": state.name, - "adapter": {"kind": "harness_self_improvement"}, - "coordination": coordination, - } - ], - } - ), - encoding="utf-8", - ) - return registry, state, runtime_root - - -def _add(registry: Path) -> dict: - return add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Continue primary work with a retained observation setting.", - task_class="advancement_task", - ) - - -@pytest.mark.parametrize("enabled", [False, True]) -def test_retired_and_absent_settings_leave_public_writers_on_primary_only(tmp_path: Path, enabled: bool) -> None: - registry, state, runtime = _fixture(tmp_path, enabled=enabled) - retained = runtime / "authority-shadow" / "file" / GOAL_ID / "historical.json" - if enabled: - retained.parent.mkdir(parents=True) - retained.write_bytes(b'{"historical":true}\n') - before = retained.read_bytes() if enabled else None - added = _add(registry) - todo_id = added["todo_id"] - acquired = acquire_task_lease(registry_path=registry, runtime_root=runtime, goal_id=GOAL_ID, - todo_id=todo_id, owner=AGENT_A, idempotency_key="retired-writer", ttl_seconds=120) - renewed = renew_task_lease(registry_path=registry, runtime_root=runtime, goal_id=GOAL_ID, - todo_id=todo_id, owner=AGENT_A, idempotency_key="retired-writer", - expected_version=acquired["lease"]["version"], ttl_seconds=180) - released = release_task_lease(registry_path=registry, runtime_root=runtime, goal_id=GOAL_ID, - todo_id=todo_id, owner=AGENT_A, idempotency_key="retired-writer", - expected_version=renewed["lease"]["version"]) - for result in (added, acquired, renewed, released): - assert result["ok"] is True - assert "authority_shadow" not in result - assert todo_id in state.read_text() - assert not (runtime / "authority-shadow" / "outbox").exists() - assert not (runtime / "authority-shadow" / "file-v0").exists() - if enabled: - assert retained.read_bytes() == before - assert list(retained.parent.iterdir()) == [retained] - else: - assert not (runtime / "authority-shadow").exists() From 1ac78d26fbf6e62487c2b18d70810c87f088997d Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 07:42:41 +0800 Subject: [PATCH 20/98] test(authority): migrate projected history fixtures Seed long-chain and archived-dependency scenarios from one canonical source snapshot before exercising public Todo mutations. Signed-off-by: Duang777 --- .../test_long_chain_projected_closeout.py | 3 +- .../test_long_history_todo_reads.py | 42 ++++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/tests/control_plane/test_long_chain_projected_closeout.py b/tests/control_plane/test_long_chain_projected_closeout.py index 525f56ca2c..25a3ec98e8 100644 --- a/tests/control_plane/test_long_chain_projected_closeout.py +++ b/tests/control_plane/test_long_chain_projected_closeout.py @@ -9,7 +9,7 @@ from tests.control_plane.test_quota_settlement_cli import ( AGENT_ID, GOAL_ID, SELECTED_REPLAN_TODO_ID, TURN_ID, _bind_selected_replan_guard, _configure_selected_todo_replan_fixture, - _projected_cli_args, + _initialize_fixture_authority, _projected_cli_args, _run_cli, _spend_run_count, _write_fixture, ) @@ -80,6 +80,7 @@ def test_projected_vision_replan_settles_without_a_meta_successor(tmp_path: Path f"todo_id={last_id} status=open task_class=advancement_task action_kind=validate", ) state.write_text(source) + _initialize_fixture_authority(project, runtime) def call(*args): rc, result = _run_cli(registry, runtime, *args, cwd=project) diff --git a/tests/control_plane/test_long_history_todo_reads.py b/tests/control_plane/test_long_history_todo_reads.py index 5ac1103796..3eb17f44d1 100644 --- a/tests/control_plane/test_long_history_todo_reads.py +++ b/tests/control_plane/test_long_history_todo_reads.py @@ -3,10 +3,36 @@ import pytest +from loopx.control_plane.coordination.authority_source_capture import ( + build_runtime_shadow_source_snapshot, + todo_partition_projector, +) from loopx.control_plane.testing.canary_harness import write_fixture_registry, run_json_cli_result from loopx.control_plane.todos.active_state_todo_parser import parse_active_state_todos, parse_todo_source from loopx.control_plane.todos.goal_todo_projection import todo_summaries_from_fields from loopx.control_plane.todos.todo_summary import compact_evaluated_todo_group +from loopx.history import load_registry +from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority + + +def _initialize_source_authority( + *, + state: Path, + runtime: Path, + registry: Path, +) -> dict: + goal = next(goal for goal in load_registry(registry)["goals"] if goal["id"] == "goal-a") + projection, _ = build_runtime_shadow_source_snapshot( + goal=goal, + runtime_root=runtime, + state_path=state, + registry_path=registry, + ) + assert todo_partition_projector(goal, state_path=state)( + state.read_text(encoding="utf-8") + )["todos"] == projection["todos"] + initialize_canonical_authority(runtime, "goal-a", projection, state_path=state) + return projection @pytest.mark.parametrize("history_count", [0, 4096]) @@ -23,6 +49,7 @@ def test_public_list_preserves_completed_dependency_after_archive(tmp_path: Path write_fixture_registry(project=tmp_path, runtime_root=runtime, registry_path=registry, goal_id="goal-a", domain="long-history", adapter_kind="generic_project_goal_v0", state_file=str(state), registered_agents=["agent-a"], quota_allowed_slots=None) + _initialize_source_authority(state=state, runtime=runtime, registry=registry) code, archived = run_json_cli_result("todo", "archive-completed", "--goal-id", "goal-a", "--role", "agent", "--max-active-done", "0", "--execute", registry_path=registry, runtime_root=runtime) assert code == 0, archived @@ -57,10 +84,6 @@ def test_display_cannot_bypass_full_source_resume_evaluation(condition: dict | N def test_capture_preserves_archived_dependency_for_real_canonical_cli_read(tmp_path: Path) -> None: - from loopx.history import load_registry - from loopx.control_plane.coordination.runtime_shadow import build_runtime_shadow_source_snapshot - from loopx.control_plane.coordination.local_authority_shadow_adapter import todo_partition_projector - from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority state, runtime, registry = tmp_path / "STATE.md", tmp_path / "runtime", tmp_path / "registry.json" state.write_text("# Goal\n\n## Agent Todo\n\n- [ ] Waiting work\n" " \n\n" @@ -70,17 +93,16 @@ def test_capture_preserves_archived_dependency_for_real_canonical_cli_read(tmp_p domain="long-history", adapter_kind="generic_project_goal_v0", state_file=str(state), registered_agents=["agent-a"], quota_allowed_slots=None) original = state.read_bytes() - goal = next(goal for goal in load_registry(registry)["goals"] if goal["id"] == "goal-a") - projection, _ = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=runtime, - state_path=state, registry_path=registry) - writer_projection = todo_partition_projector(goal, state_path=state)(state.read_text()) - assert writer_projection["todos"] == projection["todos"] + projection = _initialize_source_authority( + state=state, + runtime=runtime, + registry=registry, + ) assert state.read_bytes() == original records = {item["todo_id"]: item for item in projection["todos"]} assert records["todo_history"]["archive_state"] == "archive" assert records["todo_history"]["status"] == "done" assert records["todo_history"]["role"] == "agent" - initialize_canonical_authority(runtime, "goal-a", projection, state_path=state) state.unlink() code, result = run_json_cli_result("todo", "list", "--goal-id", "goal-a", "--role", "agent", "--todo-id", "todo_waiting", registry_path=registry, runtime_root=runtime) From d44212653e71b5f7b99af247ca49be6093b14f74 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Tue, 13 Oct 2026 08:10:48 +0800 Subject: [PATCH 21/98] test(authority): align recovery fixtures Signed-off-by: Duang777 --- .../test_refresh_state_replan_gate.py | 24 +++++++++++++++++++ .../test_validated_create_recovery.py | 5 +++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/control_plane/test_refresh_state_replan_gate.py b/tests/control_plane/test_refresh_state_replan_gate.py index ccbec53bce..7c00315d69 100644 --- a/tests/control_plane/test_refresh_state_replan_gate.py +++ b/tests/control_plane/test_refresh_state_replan_gate.py @@ -1241,6 +1241,30 @@ def test_legacy_unscoped_completion_projects_executable_identity_repair( ), encoding="utf-8", ) + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.authority_source_capture import ( + build_runtime_shadow_source_snapshot, + ) + from loopx.history import load_registry + + runtime_root = tmp_path / "runtime" + goal = next( + goal + for goal in load_registry(registry_path)["goals"] + if goal["id"] == GOAL_ID + ) + projection, _ = build_runtime_shadow_source_snapshot( + goal=goal, + runtime_root=runtime_root, + state_path=state, + registry_path=registry_path, + ) + initialize_canonical_authority( + runtime_root, + GOAL_ID, + projection, + state_path=state, + ) assert cli_main( [ diff --git a/tests/control_plane/test_validated_create_recovery.py b/tests/control_plane/test_validated_create_recovery.py index 9cd184cfdb..0f71bebb69 100644 --- a/tests/control_plane/test_validated_create_recovery.py +++ b/tests/control_plane/test_validated_create_recovery.py @@ -216,11 +216,14 @@ def lose_response(method, request, **kwargs): real(method, request, **kwargs) raise EffectRuntimeResponseAmbiguous(method, timeout=1) monkeypatch.setattr(provider_update, "effect_runtime_result", lose_response) - with pytest.raises(EffectRuntimeResponseAmbiguous): + with pytest.raises(LocalCoordinationAuthorityUnavailable) as error: update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id=created["todo_id"], role="agent", agent_id="agent-a", update_operation_id="revise-after-loss", update_expected_provider_revision=read(runtime)["provider_revision"], validation_command_json=json.dumps([sys.executable, "-c", "raise SystemExit(3)"])) + assert error.value.code == "runtime_response_ambiguous" + assert error.value.payload["operation_id"] == "revise-after-loss" + assert error.value.payload["recovery_command"][-1] == "revise-after-loss" project(registry, runtime) canonical = read(runtime)["todos"][0] stored = read_completion_validation_declaration(runtime_root=runtime, goal_id="goal-a", From 4993c7deac97bef277fdc161d95a639436349f12 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Wed, 14 Oct 2026 00:13:26 +0800 Subject: [PATCH 22/98] fix(authority): stabilize canonical verification Signed-off-by: Duang777 --- .../collaboration/delegation_result_use.ts | 3 +- .../test_periodic_report_turn_writeback.py | 19 +--- .../test_reward_memory_host_isolation.py | 13 +-- tests/test_ark_managed_agent_host.py | 23 +++++ tests/test_chat_explore_results.py | 43 ++++++--- tests/test_delegation_result_use.py | 10 ++ ...est_goal_mode_mcp_completion_validation.py | 24 ++++- tests/test_lark_private_progress.py | 4 +- tests/test_loopx_turn_driver.py | 93 +++++++------------ 9 files changed, 126 insertions(+), 106 deletions(-) diff --git a/loopx/control_plane/collaboration/delegation_result_use.ts b/loopx/control_plane/collaboration/delegation_result_use.ts index 15c7a1f643..ccce6cca11 100644 --- a/loopx/control_plane/collaboration/delegation_result_use.ts +++ b/loopx/control_plane/collaboration/delegation_result_use.ts @@ -15,10 +15,11 @@ type Input = {operation_id: string; ref: string; sha256: string; input_ref: stri type Node = {operation_id: string; accepted: boolean; artifacts: {ref: string; sha256: string}[]; inputs: Input[]}; export const DELEGATION_RESULT_USE_MAX_OPERATIONS = 64; export const DELEGATION_RESULT_USE_MAX_DEPTH = 16; +export const DELEGATION_RESULT_USE_MAX_SECONDS = 30; export function delegationResultUseLimits(): JsonObject { return {max_operations: DELEGATION_RESULT_USE_MAX_OPERATIONS, - max_depth: DELEGATION_RESULT_USE_MAX_DEPTH, max_seconds: 15}; + max_depth: DELEGATION_RESULT_USE_MAX_DEPTH, max_seconds: DELEGATION_RESULT_USE_MAX_SECONDS}; } function requireThat(value: unknown, message: string): asserts value { diff --git a/tests/capabilities/test_periodic_report_turn_writeback.py b/tests/capabilities/test_periodic_report_turn_writeback.py index 138fa4d783..9ccd6c825f 100644 --- a/tests/capabilities/test_periodic_report_turn_writeback.py +++ b/tests/capabilities/test_periodic_report_turn_writeback.py @@ -18,12 +18,7 @@ ) from loopx.cli import main as cli_main from loopx.cli_commands import project_lifecycle_refresh_state, turn_post_writeback -from loopx.control_plane.coordination.runtime_shadow import ( - build_todo_runtime_shadow_projection, -) -from loopx.control_plane.todos.active_state_todo_parser import parse_active_state_todos from tests.control_plane.canonical_authority_fixture import ( - initialize_canonical_authority, isolate_sqlite_runtime, ) from tests.test_loopx_turn_driver import ( @@ -52,17 +47,9 @@ def _promoted_fixture( if provider == "sqlite": isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_live_fixture( - tmp_path, todo_metadata_extra="no_followup=true" - ) - state = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" - todos = parse_active_state_todos(state.read_text(encoding="utf-8"), item_limit=None) - projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, - handoff_mode="soft_claim", - todos=todos["agent_todos"]["items"], - ) - initialize_canonical_authority( - runtime, GOAL_ID, projection, state_path=state, provider=provider + tmp_path, + todo_metadata_extra="no_followup=true", + provider=provider, ) return project, runtime, registry diff --git a/tests/control_plane/test_reward_memory_host_isolation.py b/tests/control_plane/test_reward_memory_host_isolation.py index 58d19b1431..a93bf7e544 100644 --- a/tests/control_plane/test_reward_memory_host_isolation.py +++ b/tests/control_plane/test_reward_memory_host_isolation.py @@ -124,21 +124,12 @@ def test_old_host_reflection_is_ignored_when_ingest_is_off(ingest: bool) -> None @pytest.mark.parametrize("mode", ["unconfigured", "disabled", "stale", "both_off", "recall_only", "ingest_only", "both_on"]) def test_real_turn_cli_memory_projection_and_settlement(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, mode: str) -> None: from loopx.cli import main - from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection - from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime + from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime from tests.control_plane.reward_memory_host_fixture import enable_live_memory from test_loopx_turn_driver import _write_live_fixture isolate_sqlite_runtime(tmp_path, monkeypatch) - project, runtime, registry = _write_live_fixture(tmp_path) - state = project / ".codex/goals/loopx-turn-fixture/ACTIVE_GOAL_STATE.md" - projection = build_todo_runtime_shadow_projection(goal_id="loopx-turn-fixture", handoff_mode="soft_claim", todos=[{ - "schema_version": "todo_item_v0", "todo_id": "todo_fixture0001", "role": "agent", - "status": "open", "archive_state": "active", "source_section": "Agent Todo", - "task_class": "advancement_task", "action_kind": "fixture", "claimed_by": "codex-fixture", - "priority": "P0", "done": False, "text": "Advance one public fixture.", - }]) - initialize_canonical_authority(runtime, "loopx-turn-fixture", projection, state_path=state, provider=provider) + project, runtime, registry = _write_live_fixture(tmp_path, provider=provider) ingest = mode in {"ingest_only", "both_on"} if mode != "unconfigured": config = enable_live_memory(registry, recall=mode in {"recall_only", "both_on"}, ingest=ingest) diff --git a/tests/test_ark_managed_agent_host.py b/tests/test_ark_managed_agent_host.py index f615c8da74..0f9849e119 100644 --- a/tests/test_ark_managed_agent_host.py +++ b/tests/test_ark_managed_agent_host.py @@ -14,6 +14,12 @@ run_json_cli, write_fixture_registry, ) +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from loopx.control_plane.todos.active_state_todo_parser import ( + parse_active_state_todos, +) from loopx.doctor import collect_doctor from loopx.heartbeat_prompt import build_heartbeat_prompt from loopx.host_loop_activation import ( @@ -28,6 +34,9 @@ write_skill_install_readback, ) from loopx.slash_command_install import materialize_loopx_entry_skill +from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_authority, +) REPO_ROOT = Path(__file__).resolve().parents[1] @@ -410,6 +419,20 @@ def _write_continuity_fixture(tmp_path: Path) -> tuple[Path, Path, Path]: registered_agents=[CONTINUITY_AGENT_ID], quota_allowed_slots=None, ) + todos = parse_active_state_todos( + state.read_text(encoding="utf-8"), + item_limit=None, + )["agent_todos"]["items"] + initialize_canonical_authority( + runtime, + CONTINUITY_GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=CONTINUITY_GOAL_ID, + todos=todos, + handoff_mode="soft_claim", + ), + state_path=state, + ) return project, runtime, registry diff --git a/tests/test_chat_explore_results.py b/tests/test_chat_explore_results.py index b8067596bc..5d69b25a52 100644 --- a/tests/test_chat_explore_results.py +++ b/tests/test_chat_explore_results.py @@ -12,6 +12,10 @@ explore_result_log_path, ) from loopx.chat_server import ChatHTTPServer, ChatRequestHandler +from loopx.control_plane.todos.contract import ( + encode_metadata_value, + format_todo_metadata_line, +) @pytest.mark.parametrize("canonical", [False, True]) @@ -36,25 +40,36 @@ def test_ninth_link_readback_pagination_scope_failure_and_recovery(tmp_path, mon summary="Counterexample at input revision A " + "c" * 1700 + "; do not transfer beyond the tested scope.", evidence_refs=["artifact:counterexample"], agent_id="worker", )) - from loopx.todos import add_goal_todo, list_goal_todos - from loopx.control_plane.todos.contract import encode_metadata_value + from loopx.todos import list_goal_todos state = tmp_path / "active.md" - state.write_text("# Synthetic Goal\n\n## Agent Todo\n") - linked = add_goal_todo( - registry_path=registry, goal_id="evidence-goal", role="agent", - text="Test a uniform bound", agent_id="worker", claimed_by="worker", + linked = "todo_linked0001" + linked_metadata = format_todo_metadata_line( + todo_id=linked, + status="open", + claimed_by="worker", explore_result_node_refs=[*[f"prior-{i}" for i in range(8)], "question"], - )["todo_id"] - add_goal_todo( - registry_path=registry, goal_id="evidence-goal", role="agent", - text="Unrelated task", agent_id="worker", claimed_by="worker", + ) + unrelated_metadata = format_todo_metadata_line( + todo_id="todo_unrelated0001", + status="open", + claimed_by="worker", explore_result_node_refs=["elsewhere"], ) - state.write_text(state.read_text() + ( - "\n## Completed Work Archive\n- [x] Retain the counterexample\n" + state.write_text( + "# Synthetic Goal\n\n" + "## Agent Todo\n\n" + "- [ ] Test a uniform bound\n" + f"{linked_metadata}\n" + "- [ ] Unrelated task\n" + f"{unrelated_metadata}\n\n" + "## Completed Work Archive\n" + "- [x] Retain the counterexample\n" " \n" - )) + "claimed_by=worker explore_result_node_refs=" + + encode_metadata_value("question") + + " -->\n", + encoding="utf-8", + ) if canonical: from pathlib import Path monkeypatch.syspath_prepend(str(Path(__file__).parent / "control_plane")) diff --git a/tests/test_delegation_result_use.py b/tests/test_delegation_result_use.py index 2995ff785e..0aaa6337a0 100644 --- a/tests/test_delegation_result_use.py +++ b/tests/test_delegation_result_use.py @@ -238,6 +238,16 @@ def expire_after_link(self, roots): return result monkeypatch.setattr(module._ResultUseRead, "qualify", expire_after_link) links, aggregate = module._dependency_observation(runner, {}, request) + assert aggregate["state"] == "current" + assert links[0]["state"] == "current" + + def exceed_budget_after_link(self, roots): + result = qualify(self, roots) + elapsed[0] = 31 + return result + monkeypatch.setattr(module._ResultUseRead, "qualify", exceed_budget_after_link) + elapsed[0] = 0 + links, aggregate = module._dependency_observation(runner, {}, request) assert aggregate["reason"] == "verification_budget_exhausted" assert links[0]["state"] == "unavailable" elapsed[0] = 0 diff --git a/tests/test_goal_mode_mcp_completion_validation.py b/tests/test_goal_mode_mcp_completion_validation.py index 2da1e2e47c..9ab079898b 100644 --- a/tests/test_goal_mode_mcp_completion_validation.py +++ b/tests/test_goal_mode_mcp_completion_validation.py @@ -7,9 +7,15 @@ import pytest +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.goal_mode_mcp import GoalModeMCPConfig, GoalModeMCPControlPlane from loopx.status import parse_active_state_todos from loopx.todos import add_goal_todo +from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_authority, +) GOAL_ID = "mcp-completion-validation" AGENT = "claude" @@ -20,6 +26,7 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path]: repo = tmp_path / "repo" repo.mkdir() + runtime = tmp_path / "runtime" state = repo / "ACTIVE_GOAL_STATE.md" state.write_text( "\n".join( @@ -40,7 +47,7 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path]: registry.write_text( json.dumps( { - "common_runtime_root": str(tmp_path / "runtime"), + "common_runtime_root": str(runtime), "goals": [ { "id": GOAL_ID, @@ -62,6 +69,16 @@ def _write_fixture(tmp_path: Path) -> tuple[Path, Path]: ), encoding="utf-8", ) + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + todos=[], + handoff_mode="soft_claim", + ), + state_path=state, + ) return registry, state @@ -123,7 +140,10 @@ def test_mcp_complete_task_fails_closed_on_failing_declared_validation( assert payload["ok"] is False assert payload["completed"] is False assert payload["validation_blocked_completion"] is True - assert payload["validation"]["passed"] is False + assert ( + payload["validation_failure"]["validation_receipt"]["passed"] + is False + ) assert "spend-slot" not in output assert "refresh-state" not in output assert _agent_todo_status(state, todo_id) != "done" diff --git a/tests/test_lark_private_progress.py b/tests/test_lark_private_progress.py index 68c55b07c7..c7c9c4a618 100644 --- a/tests/test_lark_private_progress.py +++ b/tests/test_lark_private_progress.py @@ -339,7 +339,7 @@ def test_lost_edit_ack_recovered_by_readback_then_exact_stop_closes_draft(ordina def test_literal_mentions_use_exact_wire_proof_through_update_replay_and_final(ordinary): # noqa: F811 fake = ordinary[-2] final = "完整结果:@Fixture 与 @Later 都是普通文字。" - fake.write_text(fake.read_text().replace('"message": "Steered response.",', f'"message": {final!r},')) + fake.write_text(fake.read_text().replace('"message": "Steered response.",', f'"message": {ascii(final)},')) store, runtime, provider, transport, row = start(ordinary, first_text="阅读 @Fixture。\n") try: sid, tid = row["session_id"], row["turn_id"] @@ -373,7 +373,7 @@ def test_literal_mentions_use_exact_wire_proof_through_update_replay_and_final(o def test_full_canonical_final_replaces_bounded_draft_without_truncation(ordinary): # noqa: F811 final = "**完整结果**\n\n" + "保留每一段证据。\n" * 850 fake = ordinary[-2] - fake.write_text(fake.read_text().replace('"message": "Steered response.",', f'"message": {final!r},')) + fake.write_text(fake.read_text().replace('"message": "Steered response.",', f'"message": {ascii(final)},')) store, runtime, provider, transport, row = start(ordinary) try: sid, tid = row["session_id"], row["turn_id"] diff --git a/tests/test_loopx_turn_driver.py b/tests/test_loopx_turn_driver.py index 7c93bdff98..b357d83fc1 100644 --- a/tests/test_loopx_turn_driver.py +++ b/tests/test_loopx_turn_driver.py @@ -39,7 +39,6 @@ ) from loopx.control_plane.turn_driver.executor import BuiltInHostError from loopx.control_plane.quota.live_decision import bind_scheduler_followup_cli_routes -from loopx.todos import complete_goal_todo def _envelope( @@ -1323,6 +1322,7 @@ def _write_live_fixture( *, todo_metadata_extra: str = "", extra_agent_todo_lines: tuple[str, ...] = (), + provider: str = "file", ) -> tuple[Path, Path, Path]: project = root / "project" runtime = root / "runtime" @@ -1397,44 +1397,23 @@ def _write_live_fixture( + "\n", encoding="utf-8", ) - return project, runtime, registry - - -def _promote_turn_fixture(project: Path, runtime: Path) -> None: - state = ( - project - / ".codex" - / "goals" - / "loopx-turn-fixture" - / "ACTIVE_GOAL_STATE.md" + parsed = parse_active_state_todos( + state.read_text(encoding="utf-8"), + item_limit=None, ) projection = build_todo_runtime_shadow_projection( goal_id="loopx-turn-fixture", handoff_mode="soft_claim", - todos=[ - { - "schema_version": "todo_item_v0", - "index": 1, - "done": False, - "text": "[P0] Advance one public fixture.", - "todo_id": "todo_fixture0001", - "role": "agent", - "status": "open", - "archive_state": "active", - "source_section": "Agent Todo", - "task_class": "advancement_task", - "action_kind": "fixture", - "claimed_by": "codex-fixture", - "priority": "P0", - } - ], + todos=parsed["agent_todos"]["items"], ) initialize_canonical_authority( runtime, "loopx-turn-fixture", projection, state_path=state, + provider=provider, ) + return project, runtime, registry def test_quota_cli_projects_outer_controller_without_codex_app_action( @@ -2286,17 +2265,10 @@ def test_turn_run_once_cli_dsh_preserves_material_path_delta( tmp_path: Path, malformed_packet: bool, ) -> None: project, runtime, registry = _write_live_fixture( - tmp_path, todo_metadata_extra="no_followup=true" + tmp_path, + todo_metadata_extra="no_followup=true", ) state_path = project / ".codex/goals/loopx-turn-fixture/ACTIVE_GOAL_STATE.md" - parsed = parse_active_state_todos(state_path.read_text(encoding="utf-8"), item_limit=None) - projection = build_todo_runtime_shadow_projection( - goal_id="loopx-turn-fixture", handoff_mode="soft_claim", - todos=parsed["agent_todos"]["items"], - ) - initialize_canonical_authority( - runtime, "loopx-turn-fixture", projection, state_path=state_path - ) host_project = project / "isolated-dsh-workspace" host_project.mkdir() vision = { @@ -2530,17 +2502,6 @@ def test_turn_run_once_cli_completes_selected_todo_after_validation( next_obligation = next_plan["turn_envelope"]["replan_action_packet"] assert next_obligation["decision"] == "replan_required" - recovered_completion = complete_goal_todo( - registry_path=registry, - goal_id="loopx-turn-fixture", - todo_id="todo_fixture0001", - role="agent", - agent_id="codex-fixture", - completion_turn_key=payload["resume_turn_key"], - ) - assert recovered_completion["idempotent_replay"] is True - assert recovered_completion["changed"] is False - replayed_output = io.StringIO() with contextlib.redirect_stdout(replayed_output): replayed_exit_code = cli_main( @@ -2562,7 +2523,6 @@ def test_promoted_turn_completion_replays_after_commit_before_journal_crash( monkeypatch: pytest.MonkeyPatch, ) -> None: project, runtime, registry = _write_live_fixture(tmp_path) - _promote_turn_fixture(project, runtime) host_project = project / "isolated-host-workspace" host_project.mkdir() host_script, validation_script = _completion_host_and_validation_scripts() @@ -2891,13 +2851,13 @@ def test_turn_run_once_cli_projects_durable_no_followup_continuation( @pytest.mark.parametrize( - "durable_completion_turn_key", + "display_completion_turn_key", [None, "sha256:different-turn"], ids=["missing-turn-key", "different-turn-key"], ) -def test_turn_run_once_cli_terminal_recovery_rejects_unowned_completion( +def test_turn_run_once_cli_terminal_recovery_ignores_stale_display_identity( tmp_path: Path, - durable_completion_turn_key: str | None, + display_completion_turn_key: str | None, ) -> None: project, runtime, registry = _write_live_fixture( tmp_path, @@ -2932,8 +2892,8 @@ def test_turn_run_once_cli_terminal_recovery_rejects_unowned_completion( assert current_turn_field in state replacement = ( "" - if durable_completion_turn_key is None - else f" completion_turn_key={durable_completion_turn_key}" + if display_completion_turn_key is None + else f" completion_turn_key={display_completion_turn_key}" ) state_path.write_text( state.replace(current_turn_field, replacement), @@ -3002,19 +2962,32 @@ def test_turn_run_once_cli_terminal_recovery_rejects_unowned_completion( ) resumed = json.loads(resumed_output.getvalue()) - assert resumed_exit_code == 1, resumed - assert resumed["settlement_result"]["failure"] == { - "kind": "effect_outcome_unknown", - "step_kind": "terminal_closeout", - "reason": "closeout readback failed", + assert resumed_exit_code == 0, resumed + assert resumed["status"] == "committed" + assert resumed["settlement_result"]["failure"] is None + assert not any(resumed["effects"].values()) + assert resumed["todo_completion"] == { + "todo_id": "todo_fixture0001", + "continuation": "no_followup", } recovered_events = [ json.loads(line) for line in event_path.read_text(encoding="utf-8").splitlines() ] + terminal_events = [ + event + for event in recovered_events + if ( + event.get("event_kind") == "todo_complete" + and event.get("run_id") == turn_key + ) + ] + assert len(terminal_events) == 1 + assert terminal_events[0]["status"] == "terminal_no_followup" + assert terminal_events[0]["details"]["command"] == "turn recovery probe" assert not any( event.get("event_kind") == "todo_complete" - and event.get("run_id") == turn_key + and event.get("run_id") != turn_key for event in recovered_events ) From 8d7a885e52201688895eda12dbd19f19bbca958a Mon Sep 17 00:00:00 2001 From: Duang777 Date: Wed, 14 Oct 2026 12:02:04 +0800 Subject: [PATCH 23/98] test(authority): restore canonical monitor fixtures Signed-off-by: Duang777 --- .../monitor_poll_canonical_fixture.py | 200 ++++++++++++++++++ .../test_monitor_poll_unavailable_recovery.py | 34 ++- .../test_monitor_target_key_replay.py | 66 +++--- 3 files changed, 260 insertions(+), 40 deletions(-) create mode 100644 tests/control_plane/monitor_poll_canonical_fixture.py diff --git a/tests/control_plane/monitor_poll_canonical_fixture.py b/tests/control_plane/monitor_poll_canonical_fixture.py new file mode 100644 index 0000000000..20de2f7ed3 --- /dev/null +++ b/tests/control_plane/monitor_poll_canonical_fixture.py @@ -0,0 +1,200 @@ +"""Canonical Monitor poll fixtures shared by recovery tests.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess +import sys + +from canonical_authority_fixture import initialize_canonical_authority +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from loopx.control_plane.testing.canary_harness import ( + read_run_index, + write_fixture_registry, +) + + +GOAL = "synthetic-monitor-recovery" +AGENT = "codex-quality-qualification" +MONITOR_TARGET = "synthetic-target" +AGENT_TASK = "Validate observed synthetic head" +USER_TASK = "Review observed synthetic head" +REPO = Path(__file__).resolve().parents[2] + +_QUOTA_COMMIT_FAULT_WRAPPER = """ +import os, runpy, sys +import loopx.control_plane.quota.monitor_poll as monitor +original = monitor._native_result +def interrupted_commit(request): + if request.get('phase') == 'commit' and request.get('provider_receipt'): + os._exit(73) + return original(request) +monitor._native_result = interrupted_commit +sys.argv = ['loopx.cli', *sys.argv[1:]] +runpy.run_module('loopx.cli', run_name='__main__') +""" + + +def _cli( + registry: Path, + runtime: Path, + *args: str, + fault: str | None = None, +) -> tuple[int, dict | None]: + launcher = ( + [sys.executable, "-c", _QUOTA_COMMIT_FAULT_WRAPPER] + if fault == "quota_commit" + else [sys.executable, "-m", "loopx.cli"] + ) + result = subprocess.run( + [ + *launcher, + "--registry", + str(registry), + "--format", + "json", + "--runtime-root", + str(runtime), + *args, + ], + cwd=REPO, + env={**os.environ, "LOOPX_USAGE_PING": "0"}, + capture_output=True, + text=True, + check=False, + timeout=60, + ) + return ( + result.returncode, + json.loads(result.stdout) if result.stdout.strip() else None, + ) + + +def _ok(registry: Path, runtime: Path, *args: str) -> dict: + code, result = _cli(registry, runtime, *args) + assert code == 0, result + assert isinstance(result, dict) + return result + + +def _fixture( + tmp_path: Path, + *, + provider: str, +) -> tuple[Path, Path, Path, str, tuple[str, ...]]: + project = tmp_path / "project" + runtime = tmp_path / "runtime" + state = project / ".codex" / "goals" / GOAL / "ACTIVE_GOAL_STATE.md" + registry = project / ".loopx" / "registry.json" + state.parent.mkdir(parents=True) + state.write_text( + "---\nstatus: active\n---\n\n# Active Goal State\n\n## Agent Todo\n", + encoding="utf-8", + ) + write_fixture_registry( + project=project, + runtime_root=runtime, + registry_path=registry, + goal_id=GOAL, + domain="loopx-platform", + adapter_kind="harness_self_improvement", + registered_agents=[AGENT], + quota_allowed_slots=None, + ) + monitor = { + "schema_version": "todo_item_v0", + "todo_id": "todo_synthetic_monitor", + "role": "agent", + "status": "open", + "done": False, + "text": "Observe public synthetic target", + "archive_state": "active", + "source_section": "Agent Todo", + "index": 1, + "task_class": "continuous_monitor", + "claimed_by": AGENT, + "target_key": MONITOR_TARGET, + "cadence": "1h", + "next_due_at": "2000-01-01T00:00:00+00:00", + "watch_only": "true", + } + initialize_canonical_authority( + runtime, + GOAL, + build_todo_runtime_shadow_projection( + goal_id=GOAL, + todos=[monitor], + handoff_mode="soft_claim", + ), + state_path=state, + provider=provider, + ) + turn = "synthetic-monitor-turn-1" + _ok( + registry, + runtime, + "quota", + "should-run", + "--goal-id", + GOAL, + "--agent-id", + AGENT, + "--runtime-profile", + "generic_cli", + "--turn-instance-id", + turn, + ) + poll = ( + "quota", + "monitor-poll", + "--goal-id", + GOAL, + "--agent-id", + AGENT, + "--runtime-profile", + "generic_cli", + "--turn-instance-id", + turn, + "--todo-id", + monitor["todo_id"], + "--target-key", + MONITOR_TARGET, + "--result-hash", + "synthetic-head-1", + "--material-change", + "--next-agent-todo", + AGENT_TASK, + "--next-action-kind", + "validate", + "--next-user-todo", + USER_TASK, + "--next-user-task-class", + "user_action", + "--execute", + ) + return registry, runtime, state, monitor["todo_id"], poll + + +def _poll_events(runtime: Path) -> list[dict]: + return [ + row + for row in read_run_index(runtime, GOAL) + if row.get("classification") == "quota_monitor_poll" + ] + + +def _quota_receipt(runtime: Path) -> Path: + directory = ( + runtime + / "goals" + / GOAL + / "runs" + / ".transactions" + / "quota-monitor-poll" + ) + (receipt,) = directory.glob("*.json") + return receipt diff --git a/tests/control_plane/test_monitor_poll_unavailable_recovery.py b/tests/control_plane/test_monitor_poll_unavailable_recovery.py index 64aab772fe..6c60e2c1ec 100644 --- a/tests/control_plane/test_monitor_poll_unavailable_recovery.py +++ b/tests/control_plane/test_monitor_poll_unavailable_recovery.py @@ -7,12 +7,19 @@ from pathlib import Path import pytest -from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime +from canonical_authority_fixture import isolate_sqlite_runtime from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted -from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.testing.canary_harness import read_run_index -from loopx.todos import list_goal_todos -from test_monitor_poll_transaction_recovery import _fixture, _cli, _poll_events, _quota_receipt, GOAL, AGENT, REPO +from loopx.todos import update_goal_todo +from monitor_poll_canonical_fixture import ( + AGENT, + GOAL, + REPO, + _cli, + _fixture, + _poll_events, + _quota_receipt, +) _ACTOR_POLL_WRAPPER = """ import os, runpy, sys @@ -48,17 +55,22 @@ def test_pending_poll_cannot_be_closed_as_unavailable( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, material: bool, actor: str | None, ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state, monitor, poll = _fixture(tmp_path) - todos = list_goal_todos(registry_path=registry, goal_id=GOAL)['todos'] + registry, runtime, _state, monitor, poll = _fixture( + tmp_path, + provider=provider, + ) if actor is not None: data = json.loads(registry.read_text()) data['goals'][0]['coordination']['registered_agents'].append(actor) registry.write_text(json.dumps(data)) - for todo in todos: - if todo['todo_id'] == monitor: - todo['claimed_by'] = None - initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=todos, handoff_mode='soft_claim'), state_path=state, provider=provider) + update_goal_todo( + registry_path=registry, + goal_id=GOAL, + todo_id=monitor, + clear_claim=True, + agent_id=AGENT, + update_operation_id="synthetic-monitor-clear-claim", + ) if not material: args = list(poll) args.remove('--material-change') diff --git a/tests/control_plane/test_monitor_target_key_replay.py b/tests/control_plane/test_monitor_target_key_replay.py index 2335d07c14..29fe588fcc 100644 --- a/tests/control_plane/test_monitor_target_key_replay.py +++ b/tests/control_plane/test_monitor_target_key_replay.py @@ -9,19 +9,26 @@ import pytest -from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime +from canonical_authority_fixture import isolate_sqlite_runtime from loopx.cli import main as cli_main from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted -from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.scheduler.monitor_todo import monitor_todo_is_due from loopx.control_plane.scheduler.monitor_poll_writeback import write_monitor_poll_todo_state from loopx.control_plane.scheduler.provider_monitor_poll import poll_canonical_monitor_if_promoted from loopx.control_plane.quota import monitor_poll as monitor_runtime from loopx.control_plane.quota.accounting_admission import quota_accounting_admission from loopx.control_plane.testing.canary_harness import read_run_index -from loopx.todos import add_goal_todo, list_goal_todos, update_goal_todo -from test_monitor_poll_transaction_recovery import AGENT, GOAL, MONITOR_TARGET, REPO, _cli, _fixture, _poll_events +from loopx.todos import add_goal_todo, update_goal_todo +from monitor_poll_canonical_fixture import ( + AGENT, + GOAL, + MONITOR_TARGET, + REPO, + _cli, + _fixture, + _poll_events, +) _NULL_TODO_WRAPPER = """ import hashlib, os, runpy, sys @@ -88,14 +95,15 @@ def test_target_only_monitor_poll_replays_accepted_identity( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, completion: str, material: bool, ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state, monitor, poll = _fixture(tmp_path) + registry, runtime, _state, monitor, poll = _fixture( + tmp_path, + provider=provider, + ) other = add_goal_todo(registry_path=registry, goal_id=GOAL, role='agent', text='Observe a different public synthetic target', task_class='continuous_monitor', claimed_by=AGENT, monitor_metadata={'target_key': 'synthetic-other-target', 'cadence': '1h', - 'next_due_at': '2000-01-01T00:00:00+00:00', 'watch_only': 'true'}) - todos = list_goal_todos(registry_path=registry, goal_id=GOAL)['todos'] - initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=todos, handoff_mode='soft_claim'), state_path=state, provider=provider) + 'next_due_at': '2000-01-01T00:00:00+00:00', 'watch_only': 'true'}, + operation_id='synthetic-other-monitor') target_poll = _target_poll(poll, material=material) rc, first = _cli(registry, runtime, *target_poll, fault='quota_commit' if completion == 'interrupted' else None) @@ -149,14 +157,14 @@ def test_target_only_retry_rejects_changed_provider_actor( provider: str, completion: str, ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state, monitor, poll = _fixture(tmp_path) + registry, runtime, _state, monitor, poll = _fixture( + tmp_path, + provider=provider, + ) peer = 'synthetic-registered-observer' data = json.loads(registry.read_text()) data['goals'][0]['coordination']['registered_agents'].append(peer) registry.write_text(json.dumps(data)) - todos = list_goal_todos(registry_path=registry, goal_id=GOAL)['todos'] - initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=todos, handoff_mode='soft_claim'), state_path=state, provider=provider) target_poll = _target_poll(poll, material=False) rc, original = _cli(registry, runtime, *target_poll, fault='quota_commit' if completion == 'interrupted' else None) @@ -214,10 +222,10 @@ def test_target_only_concurrent_pending_retries_keep_one_observation( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, competitor: str, ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state, monitor, poll = _fixture(tmp_path) - todos = list_goal_todos(registry_path=registry, goal_id=GOAL)['todos'] - initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=todos, handoff_mode='soft_claim'), state_path=state, provider=provider) + registry, runtime, _state, monitor, poll = _fixture( + tmp_path, + provider=provider, + ) target_poll = _target_poll(poll, material=False) rc, interrupted = _cli(registry, runtime, *target_poll, fault='quota_commit') assert (rc, interrupted) == (73, None) @@ -279,10 +287,10 @@ def test_target_only_new_observation_remains_rejected_when_not_due( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state, monitor, poll = _fixture(tmp_path) - todos = list_goal_todos(registry_path=registry, goal_id=GOAL)['todos'] - initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=todos, handoff_mode='soft_claim'), state_path=state, provider=provider) + registry, runtime, _state, monitor, poll = _fixture( + tmp_path, + provider=provider, + ) # Another legitimate business observation advances cadence without this # Turn accepting a quota poll. No historical quota receipt may be inferred. write_monitor_poll_todo_state(registry_path=registry, runtime_root=runtime, goal_id=GOAL, @@ -307,10 +315,10 @@ def test_target_only_auxiliary_recovery_preserves_advancement_binding( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, historical_null_todo: bool, ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state, monitor, poll = _fixture(tmp_path) - todos = list_goal_todos(registry_path=registry, goal_id=GOAL)['todos'] - initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=todos, handoff_mode='soft_claim'), state_path=state, provider=provider) + registry, runtime, _state, monitor, poll = _fixture( + tmp_path, + provider=provider, + ) rc, original = _cli(registry, runtime, *_target_poll(poll, material=False)) assert rc == 0, original rc, advancement = _cli(registry, runtime, 'todo', 'add', '--goal-id', GOAL, '--role', 'agent', @@ -357,14 +365,14 @@ def test_target_recovery_hint_cannot_create_poll_after_no_effect_abort( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], provider: str, ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state, monitor, poll = _fixture(tmp_path) + registry, runtime, _state, monitor, poll = _fixture( + tmp_path, + provider=provider, + ) peer = 'synthetic-registered-peer' data = json.loads(registry.read_text()) data['goals'][0]['coordination']['registered_agents'].append(peer) registry.write_text(json.dumps(data)) - todos = list_goal_todos(registry_path=registry, goal_id=GOAL)['todos'] - initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( - goal_id=GOAL, todos=todos, handoff_mode='soft_claim'), state_path=state, provider=provider) target_poll = _target_poll(poll, material=False) captured = {} native = monitor_runtime._native_result From b199d0bc37b14265135e225f5ea78df8a6534ec7 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:24:33 +0800 Subject: [PATCH 24/98] fix(authority): commit imported untyped completion repairs Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../coordination/todo_terminal_lifecycle.ts | 11 +++- .../terminal_reentry_conformance.ts | 51 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index 7ec816e2b5..3fae90019a 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -1506,6 +1506,12 @@ export async function executeCoordinationTodoTerminalLifecycle( ); } + // Imported completed records can lack the continuation metadata owned by + // the completion fence. Honor its explicit repair without admitting fresh + // successor intent or changing ordinary terminal replay. + const untypedCompletionRepair = completion?.decision === "commit" && + completion.fence.reason === "untyped_completion_repair"; + if (authority.outcome === "no_change" && (edit === null || !edit.changed) && terminalUpgradeReceipt === null) { const existingSuccessorIds = Array.isArray(todo.successor_todo_ids) ? todo.successor_todo_ids.map((value, index) => @@ -1520,7 +1526,7 @@ export async function executeCoordinationTodoTerminalLifecycle( "decision_rejection", ); } - return commitTerminalResult(store, input, requestSha, head, { + if (!untypedCompletionRepair) return commitTerminalResult(store, input, requestSha, head, { todo_id: input.todo_id, command: input.command, changed: false, @@ -1662,7 +1668,8 @@ export async function executeCoordinationTodoTerminalLifecycle( ? planUserCompletion(todo, [...projection.todos.values()], input.decision_outcome) : null; const dependent = followthrough && Object.keys(followthrough.updates).length ? projection.todos.get(String(todo.unblocks_todo_id)) : undefined; - const changed = authority.outcome === "apply" || edit?.changed === true || terminalUpgradeReceipt !== null; + const changed = authority.outcome === "apply" || edit?.changed === true || + terminalUpgradeReceipt !== null || untypedCompletionRepair; const result: JsonObject = { todo_id: input.todo_id, command: input.command, diff --git a/tests/control_plane_ts/terminal_reentry_conformance.ts b/tests/control_plane_ts/terminal_reentry_conformance.ts index 52566d662e..e6f97ae9f7 100644 --- a/tests/control_plane_ts/terminal_reentry_conformance.ts +++ b/tests/control_plane_ts/terminal_reentry_conformance.ts @@ -10,6 +10,57 @@ import type {AuthorityStoreConformanceFactory} from "./authority_store_conforman export function registerTerminalReentryConformance(provider: string, factory: AuthorityStoreConformanceFactory): void { for (const schema of ["legacy", "native"] as const) { + test(`${provider}: imported untyped completion repairs once before terminal closeout (${schema})`, async t => { + const {store, contender} = await factory(t); + const todo: JsonObject = {schema_version: schema === "native" ? "todo_domain_item_v0" : "todo_item_v0", + todo_id: "todo_untyped", role: "agent", status: "done", done: true, archive_state: "active", + text: "Repair imported terminal metadata", claimed_by: "agent-a", completion_turn_key: "original-turn", + completed_at: "2026-09-01T00:00:00Z", ...(schema === "legacy" ? {source_section: "Agent Todo"} : {})}; + await store.commitAuthority({operation_id: "import-untyped", expected_provider_revision: null, + next_projection: authorityProjectionFixture("goal-a", [todo], [], schema, {handoff_mode: "soft_claim"}), + events: [], receipts: [{original_import: true}]}); + const request: CoordinationTodoTerminalLifecycleInput = { + goal_id: "goal-a", todo_id: "todo_untyped", expected_role: "agent", command: "complete", + operation_identity: {kind: "completion_turn"}, + actor_agent_id: "agent-a", registered_agents: ["agent-a", "agent-b"], lifecycle_grants: [], + authority_reason: null, decision_outcome: null, lease_idempotency_key: null, lease_expected_version: null, + allow_user_gate_auto_acquire: false, requested_no_followup: false, + requested_completion_turn_key: "original-turn", requested_completion_identity_source: "turn_settlement", + linked_successor_todo_ids: [], successor_intents: [], note: null, evidence: null, reason: null, + clear_claim: false, validation_declaration: null, validation_receipt: null, + completion_policy_request: null, dry_run: false, now: new Date("2026-09-07T07:00:00Z"), + }; + const before = await store.loadAuthority(), importedReceipt = await store.readReceipt("import-untyped"); + for (const change of [{requested_no_followup: true}, {actor_agent_id: "agent-b"}, + {requested_completion_turn_key: "other-turn"}, + {successor_intents: [{role: "agent", text: "Cannot add work during repair"}]}]) { + assert.equal((await execute(store, {...request, ...change})).status, "failed", JSON.stringify(change)); + assert.deepEqual(await store.loadAuthority(), before); + } + const preview = await execute(store, {...request, dry_run: true}); + assert.equal(preview.status, "planned", JSON.stringify(preview)); + assert.equal(preview.changed, true); + assert.deepEqual(await store.loadAuthority(), before); + const repair = await execute(store, request); + assert.equal(repair.status, "applied", JSON.stringify(repair)); + assert.equal(repair.changed, true); + assert.equal(repair.completion_continuation, "active_goal"); + const repaired = await store.loadAuthority(); + assert.equal(repaired.status, "loaded"); + if (repaired.status !== "loaded") throw new Error("repair head missing"); + const item = (repaired.head.todos as JsonObject[])[0]; + assert.equal(item.completion_continuation, "active_goal"); + assert.equal(item.completion_turn_key, "original-turn"); + assert.equal(item.completed_at, todo.completed_at); + assert.deepEqual(await store.readReceipt("import-untyped"), importedReceipt); + assert.equal((await execute(contender, request)).status, "replayed"); + assert.deepEqual(await store.loadAuthority(), repaired); + const closed = await execute(store, {...request, requested_no_followup: true, + operation_identity: {kind: "completion_turn"}}); + assert.equal(closed.status, "applied", JSON.stringify(closed)); + assert.equal(closed.completion_continuation, "no_followup"); + assert.equal(closed.completion_recovery, "same_turn_terminal_closeout"); + }); for (const validationRequired of [false, true]) { test(`${provider}: migrated terminal reentry preserves completion and closes only continuation (${schema}, validation=${validationRequired})`, async t => { const {store, contender} = await factory(t); From 3fa639f64107e49ff21fc482890ee467c7a583fd Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:24:34 +0800 Subject: [PATCH 25/98] build: identify the canonical-only cutoff as 2.0 alpha Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/loopx/__init__.py b/loopx/__init__.py index eb16acc782..13aa310863 100644 --- a/loopx/__init__.py +++ b/loopx/__init__.py @@ -2,4 +2,4 @@ __all__ = ["__version__"] -__version__ = "1.4.0" +__version__ = "2.0.0a1" diff --git a/pyproject.toml b/pyproject.toml index 6df0b458b5..8823f0759c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "loopx" -version = "1.4.0" +version = "2.0.0a1" description = "A Loop Engineering control plane for long-running agent goals (requires Node.js 22.22.3+)." readme = "README.md" requires-python = ">=3.11" From 548b0f37d6294efaefe0f581663f05f82ad3332c Mon Sep 17 00:00:00 2001 From: Duang777 Date: Wed, 14 Oct 2026 12:46:34 +0800 Subject: [PATCH 26/98] test(authority): promote browser fixtures before writes Signed-off-by: Duang777 --- .github/workflows/python-tests.yml | 1 - .../attention-user-action.mjs | 12 ++++++------ .../goal-workspace-todo-backend.py | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 4b413eb057..fdf5cebdcc 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -848,7 +848,6 @@ jobs: tests/test_file_lock.py tests/test_file_lock_cross_process.py tests/control_plane/test_effect_runtime_integration.py - tests/control_plane/test_local_authority_shadow_outbox.py tests/test_self_update_runtime_activation.py tests/test_windows_atomic_writes.py tests/test_windows_install.py diff --git a/examples/personal-workspace-browser/attention-user-action.mjs b/examples/personal-workspace-browser/attention-user-action.mjs index 73cce19142..8ebefb6cd2 100644 --- a/examples/personal-workspace-browser/attention-user-action.mjs +++ b/examples/personal-workspace-browser/attention-user-action.mjs @@ -25,19 +25,19 @@ from loopx.chat_server import ChatHTTPServer, ChatRequestHandler from loopx.chat_actions import ChatActionService from loopx.chat_action_store import ChatActionStore from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection -from loopx.todos import add_goal_todo, list_goal_todos +from loopx.todos import add_goal_todo r=pathlib.Path(sys.argv[1]); runtime=r/'runtime'; registry=r/'registry.json'; state=r/'state.md' state.write_text('---\\ngoal_id: product-release\\n---\\n\\n## Agent Todo\\n') agents=['codex-review','codex-delivery'] + (['codex'] if sys.argv[2]=='true' else []) registry.write_text(json.dumps({'common_runtime_root':str(runtime),'goals':[{'id':'product-release','repo':str(r),'state_file':'state.md','coordination':{'agent_model':'peer_v1','registered_agents':agents}}]})) +config=json.loads(registry.read_text()); config['goals'][0]['coordination']['handoff_mode']='hard_lease'; registry.write_text(json.dumps(config)) +projection=build_todo_runtime_shadow_projection(goal_id='product-release',handoff_mode='hard_lease',todos=[]) +initialize_canonical_authority(runtime,'product-release',projection,state_path=state,provider='file') ids={} for operation in ['complete','cancel']: - target=add_goal_todo(registry_path=registry,goal_id='product-release',role='agent',text='Dependent '+operation,status='blocked',task_class='advancement_task',claimed_by='codex-delivery') - action=add_goal_todo(registry_path=registry,goal_id='product-release',role='user',text='Handle '+operation,task_class='user_action',bound_agent='codex-delivery',unblocks_todo_id=target['todo_id']) + target=add_goal_todo(registry_path=registry,goal_id='product-release',role='agent',text='Dependent '+operation,status='blocked',task_class='advancement_task',claimed_by='codex-delivery',operation_id='attention-user-action:'+operation+':target') + action=add_goal_todo(registry_path=registry,goal_id='product-release',role='user',text='Handle '+operation,task_class='user_action',bound_agent='codex-delivery',unblocks_todo_id=target['todo_id'],operation_id='attention-user-action:'+operation+':request') ids[operation]=[action['todo_id'],target['todo_id']] -projection=build_todo_runtime_shadow_projection(goal_id='product-release',handoff_mode='hard_lease',todos=list_goal_todos(registry_path=registry,goal_id='product-release')['todos']) -config=json.loads(registry.read_text()); config['goals'][0]['coordination']['handoff_mode']='hard_lease'; registry.write_text(json.dumps(config)) -initialize_canonical_authority(runtime,'product-release',projection,state_path=state,provider='file') s=ChatHTTPServer(('127.0.0.1',0),ChatRequestHandler) s.registry_path,s.runtime_root,s.runtime_root_override,s.verbose=registry,runtime,str(runtime),False s.selected_goal_id,s.scan_roots,s.limit='product-release',[],10 diff --git a/examples/personal-workspace-browser/goal-workspace-todo-backend.py b/examples/personal-workspace-browser/goal-workspace-todo-backend.py index 508bdf46cf..22bf1a9256 100644 --- a/examples/personal-workspace-browser/goal-workspace-todo-backend.py +++ b/examples/personal-workspace-browser/goal-workspace-todo-backend.py @@ -10,6 +10,9 @@ from loopx.chat_action_store import ChatActionStore from loopx.chat_actions import ChatActionService from loopx.chat_server import ChatHTTPServer, ChatRequestHandler +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) from loopx.control_plane.effect_runtime import restart_effect_runtime root = Path(sys.argv[1]).resolve() @@ -20,8 +23,23 @@ registry, state = fixtures["write_registry_fixture"](root) data = json.loads(registry.read_text()) data["goals"][0].update(id="progress-projection", display_name="Synthetic workspace") +data["goals"][0]["coordination"]["handoff_mode"] = "soft_claim" registry.write_text(json.dumps(data), encoding="utf-8") runtime = Path(data["common_runtime_root"]) +canonical_fixtures = runpy.run_path( + str(Path(__file__).resolve().parents[2] / "tests/control_plane/canonical_authority_fixture.py") +) +canonical_fixtures["initialize_canonical_authority"]( + runtime, + "progress-projection", + build_todo_runtime_shadow_projection( + goal_id="progress-projection", + handoff_mode="soft_claim", + todos=[], + ), + state_path=state, + provider="file", +) server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler) server.registry_path, server.runtime_root, server.runtime_root_override = registry, runtime, str(runtime) server.selected_goal_id, server.scan_roots, server.limit = "progress-projection", [], 20 From 8880b20d244e0327e08e900352ec957aa6cd12a7 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:46:48 +0800 Subject: [PATCH 27/98] fix(project): preserve canonical registration entry contracts Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../coordination/goal_storage_initialization.py | 3 +-- loopx/control_plane/projects/registry.py | 4 ++++ loopx/semantics/project_registry_io_manifest_v1.json | 8 ++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/loopx/control_plane/coordination/goal_storage_initialization.py b/loopx/control_plane/coordination/goal_storage_initialization.py index 81495b2864..fae44b1f12 100644 --- a/loopx/control_plane/coordination/goal_storage_initialization.py +++ b/loopx/control_plane/coordination/goal_storage_initialization.py @@ -13,8 +13,7 @@ authority_registry_source, build_runtime_shadow_source_snapshot, ) - -NEW_GOAL_STORAGE_METHOD = "coordination.local_authority.new_goal_storage" +from .new_goal_storage_transport import NEW_GOAL_STORAGE_METHOD def initialize_goal_storage_target( diff --git a/loopx/control_plane/projects/registry.py b/loopx/control_plane/projects/registry.py index 81d2f51590..85a93aa295 100644 --- a/loopx/control_plane/projects/registry.py +++ b/loopx/control_plane/projects/registry.py @@ -481,6 +481,8 @@ def register_project_goal( resolve_new_goal_storage_target: Callable[[Path], dict[str, Any]], goal_instance_profile: str | None = None, operation_id: str | None = None, + registered_agents: list[str] | None = None, + canonical_storage: str | None = None, ) -> dict[str, Any]: normalized_goal_id = _identifier(goal_id, field="goal_id") expanded_registry = registry_path.expanduser() @@ -526,6 +528,8 @@ def register_project_goal( resolve_new_goal_storage_target=resolve_new_goal_storage_target, goal_instance_profile=goal_instance_profile, operation_id=operation_id, + registered_agents=registered_agents, + canonical_storage=canonical_storage, ) diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index 91e9fe2100..dc6550c07a 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -1615,7 +1615,7 @@ }, { "site": "loopx/control_plane/projects/registry.py::.bind_session::codec_write:mutate_project_registry#1", - "line": 612, + "line": 616, "column": 12, "kind": "codec_write", "api": "mutate_project_registry", @@ -1623,7 +1623,7 @@ }, { "site": "loopx/control_plane/projects/registry.py::.register_project_goal::codec_read:load_project_registry#1", - "line": 488, + "line": 490, "column": 9, "kind": "codec_read", "api": "load_project_registry", @@ -1631,7 +1631,7 @@ }, { "site": "loopx/control_plane/projects/registry.py::.resolve_project::codec_read:load_project_registry#1", - "line": 735, + "line": 739, "column": 16, "kind": "codec_read", "api": "load_project_registry", @@ -1639,7 +1639,7 @@ }, { "site": "loopx/control_plane/projects/registry.py::.unbind_session::codec_write:mutate_project_registry#1", - "line": 695, + "line": 699, "column": 12, "kind": "codec_write", "api": "mutate_project_registry", From eebb6bfedb18706edf4f0438582eaf27e57c2374 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:46:48 +0800 Subject: [PATCH 28/98] ci: validate retained migration observations on Windows Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .github/workflows/python-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 4b413eb057..31107df33e 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -848,7 +848,7 @@ jobs: tests/test_file_lock.py tests/test_file_lock_cross_process.py tests/control_plane/test_effect_runtime_integration.py - tests/control_plane/test_local_authority_shadow_outbox.py + tests/control_plane/test_shadow_observation_retirement.py tests/test_self_update_runtime_activation.py tests/test_windows_atomic_writes.py tests/test_windows_install.py From e720933301d35c58c3542651413d49c64791854b Mon Sep 17 00:00:00 2001 From: Duang777 Date: Wed, 14 Oct 2026 13:58:06 +0800 Subject: [PATCH 29/98] fix(settings): await goal configuration readback Signed-off-by: Duang777 --- .../goal-capability-settings.tsx | 20 ++++++++-------- .../replan-cadence.mjs | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx index 764128ed9b..af7f15dc16 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx @@ -24,7 +24,7 @@ type CapabilityCatalogProps = Readonly<{ callbacks: PersonalWorkspaceCallbacks; catalog: CapabilityConfigurationCatalog; goalId: string; - onApplied: () => void; + onApplied: () => Promise; notification?: WorkspaceGoalNotification; onNotificationChanged: () => void; }>; @@ -62,7 +62,7 @@ function goalWriteDraft(selected: CapabilityConfigurationCatalog["capabilities"] function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ goalId: string; - onApplied: () => void; + onApplied: () => Promise; selected: CapabilityConfigurationCatalog["capabilities"][number] | undefined; t: ReturnType["t"]; }>) { @@ -134,7 +134,7 @@ function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ partialWrite: result.status === "partial_write" ? result : null, preview: null, })); - if (result.status !== "partial_write") onApplied(); + if (result.status !== "partial_write") await onApplied(); } catch (reason) { setMutation((current) => ({ ...current, preview: null })); setError(reason instanceof Error ? reason.message : t("capabilities.applyFailed")); @@ -191,7 +191,7 @@ function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ function CapabilityMutationFeedback({ mutationError, onApplied, partialWrite, preview }: Readonly<{ mutationError: string | null; - onApplied: () => void; + onApplied: () => Promise; partialWrite: GoalConfigurationPartialWrite | null; preview: GoalConfigurationPreview | null; }>) { @@ -211,7 +211,7 @@ function CapabilityMutationFeedback({ mutationError, onApplied, partialWrite, pr : "capabilities.partialWriteDescription")}

{partialWrite.recommended_action} - + ) : null} {preview ? ( @@ -298,7 +298,7 @@ function CapabilityCatalog({ callbacks, catalog, goalId, notification, onApplied {selected.capability_id === "progress_review" && selected.observation ?

{locale === "zh-CN" ? "审查证据读回" : "Review evidence readback"}

- +

{selected.observation.read_state === "unavailable" ? (locale === "zh-CN" ? "审查存储不可读,当前判断未知。" : "Review storage unavailable; current judgment unknown.") : selected.observation.read_state === "missing" ? (locale === "zh-CN" ? "尚无审查回执。" : "No review receipts yet.") : `${selected.observation.receipt_count} ${locale === "zh-CN" ? "条已读取回执" : "loaded receipts"}`}

@@ -418,15 +418,15 @@ export function GoalCapabilitySettings({ callbacks, goalId, notification, onChan const [loading, setLoading] = useState(false); const readGeneration = useRef(0); - function load() { - if (!goalId) return; + function load(): Promise { + if (!goalId) return Promise.resolve(); const generation = ++readGeneration.current; setLoading(true); setError(null); // Withdraw the previous observation during a recheck; preserve navigation. setInspection(current => current ? {...current, capability_catalog: {...current.capability_catalog, capabilities: current.capability_catalog.capabilities.map(item => item.observation ? {...item, observation: undefined} : item)}} : null); - void fetchGoalConfiguration(goalId) + return fetchGoalConfiguration(goalId) .then(value => {if (generation === readGeneration.current) setInspection(value);}) .catch((reason: unknown) => {if (generation === readGeneration.current) setError(reason instanceof Error ? reason.message : t("capabilities.loadFailed"));}) .finally(() => {if (generation === readGeneration.current) setLoading(false);}); @@ -445,7 +445,7 @@ export function GoalCapabilitySettings({ callbacks, goalId, notification, onChan
{t("capabilities.loadFailed")}{error} - +
); } diff --git a/examples/personal-workspace-browser/replan-cadence.mjs b/examples/personal-workspace-browser/replan-cadence.mjs index ac158e594f..272a39dfcd 100644 --- a/examples/personal-workspace-browser/replan-cadence.mjs +++ b/examples/personal-workspace-browser/replan-cadence.mjs @@ -21,6 +21,15 @@ export const replanCadenceScenario = { const legacy = { namespace: "todo_replan_cadence", namespace_configuration: { schema_version: "todo_replan_cadence_machine_defaults_v0", completed_todos: 2, } }; + let nextGoalReadGate = null; + function gateNextGoalRead() { + let markStarted; + let release; + const started = new Promise((resolveStarted) => { markStarted = resolveStarted; }); + const blocked = new Promise((resolveBlocked) => { release = resolveBlocked; }); + nextGoalReadGate = { blocked, markStarted }; + return { release, started }; + } let context; try { context = await openWorkspacePage(browser, url, { collectCoverage, @@ -28,6 +37,12 @@ export const replanCadenceScenario = { for (const endpoint of ["machine-configuration", "goal-configuration"]) { await page.route(`**/api/chat/${endpoint}**`, async (route) => { const parsed = new URL(route.request().url()); + if (endpoint === "goal-configuration" && route.request().method() === "GET" && nextGoalReadGate) { + const gate = nextGoalReadGate; + nextGoalReadGate = null; + gate.markStarted(); + await gate.blocked; + } await route.fulfill({ response: await route.fetch({ url: authority.url + parsed.pathname + parsed.search }) }); }); } @@ -156,7 +171,16 @@ export const replanCadenceScenario = { await waitForInputValue(count, "3"); await strategy.selectOption("off"); await page.getByRole("button", { name: "预览变更", exact: true }).click(); + const revisionBeforeApply = await page.locator(".personal-capability-settings.is-goal-scoped").getAttribute("data-revision"); + const goalReadGate = gateNextGoalRead(); await applyGoalPreview(); + await goalReadGate.started; + const editableBeforeReadback = await count.isEnabled(); + goalReadGate.release(); + await page.waitForFunction((revision) => ( + document.querySelector(".personal-capability-settings.is-goal-scoped")?.getAttribute("data-revision") !== revision + ), revisionBeforeApply); + if (editableBeforeReadback) throw new Error("Goal editor accepted a new draft before applied configuration readback"); let goalCadence = await readGoalCadence(); if (JSON.stringify(goalCadence.current) !== JSON.stringify({ stride_strategy: "off" }) || goalCadence.effective_configuration.configuration.count !== 3) { From 24e23ec2bf7ba54a8243a12c7a65a3ee60a3e300 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:24:55 +0800 Subject: [PATCH 30/98] test(todos): retain delegated actor coverage on canonical providers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_native_todo_planning_update.py | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/tests/control_plane/test_native_todo_planning_update.py b/tests/control_plane/test_native_todo_planning_update.py index 7f0782bc24..59420ecc52 100644 --- a/tests/control_plane/test_native_todo_planning_update.py +++ b/tests/control_plane/test_native_todo_planning_update.py @@ -53,10 +53,10 @@ def fixture(tmp_path: Path, provider: str = "file") -> tuple[Path, Path]: return registry, state -def update(registry: Path, *args: str, ok: bool = True) -> dict: +def update(registry: Path, *args: str, ok: bool = True, actor: str = "agent-a") -> dict: process = subprocess.run([ sys.executable, "-m", "loopx.cli", "--format", "json", "--registry", str(registry), - "todo", "update", "--goal-id", "goal-a", "--todo-id", "todo_target", "--agent-id", "agent-a", *args, + "todo", "update", "--goal-id", "goal-a", "--todo-id", "todo_target", "--agent-id", actor, *args, ], capture_output=True, text=True, timeout=45) result = json.loads(process.stdout) assert (process.returncode == 0) is ok, (result, process.stderr) @@ -67,6 +67,53 @@ def records(registry: Path) -> dict[str, dict]: return {item["todo_id"]: item for item in list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"]} +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("edit,expected_actor", [ + (["--evidence", "Synthetic planning validation"], "agent-a"), + (["--priority", "P2"], "agent-a"), + (["--clear-priority"], "agent-a"), + (["--note", "Owner context", "--clear-priority"], "agent-a"), + (["--note", "Peer correction", "--evidence", "Synthetic mixed validation"], "agent-b"), + (["--text", "Corrected task", "--evidence", "Synthetic mixed validation"], "agent-b"), +]) +def test_delegated_cli_planning_preserves_actor_until_copy_changes( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, + edit: list[str], expected_actor: str, +) -> None: + # Preserve the supported audit contract on the canonical path; the retired + # Markdown writer and its mutation-authority fixture are not dependencies. + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, state = fixture(tmp_path, provider) + registration = json.loads(registry.read_text()) + registration["goals"][0]["coordination"]["todo_lifecycle_authority"] = [{ + "agent_id": "agent-b", "actions": ["update"], "requires_reason": True, + }] + registry.write_text(json.dumps(registration)) + update(registry, "--note", "Owner context", "--priority", "P1", + "--update-operation-id", "owner-audit") + before = records(registry) + assert before["todo_target"]["last_actor_agent_id"] == "agent-a" + display = state.read_bytes() + attempt = [*edit, "--update-operation-id", "delegated-audit"] + rejected = update(registry, *attempt, actor="agent-b", ok=False) + assert rejected["error_code"] == "delegation_reason_required" + assert records(registry) == before + assert state.read_bytes() == display + attempt += ["--authority-reason", "Review the existing task"] + assert update(registry, *attempt, "--dry-run", actor="agent-b")["status"] == "planned" + assert records(registry) == before + assert state.read_bytes() == display + assert update(registry, *attempt, actor="agent-b")["status"] == "applied" + after = records(registry) + assert after["todo_target"]["last_actor_agent_id"] == expected_actor + assert after["todo_target"]["claimed_by"] == "agent-a" + assert after["todo_other"] == before["todo_other"] + display = state.read_bytes() + assert update(registry, *attempt, actor="agent-b")["status"] == "replayed" + assert records(registry) == after + assert state.read_bytes() == display + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_work_requirements_update_preserves_authority_and_supports_explicit_empty( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, From 542bd8073b2f25e0c1c6fb3dd495d075db2497ed Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:52:33 +0800 Subject: [PATCH 31/98] test(app): restore disposable retirement receiver isolation Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_todo_consumer_host_retirement.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/control_plane/test_todo_consumer_host_retirement.py b/tests/control_plane/test_todo_consumer_host_retirement.py index f860f83993..43b792f062 100644 --- a/tests/control_plane/test_todo_consumer_host_retirement.py +++ b/tests/control_plane/test_todo_consumer_host_retirement.py @@ -28,6 +28,14 @@ def cold_api(tmp_path, monkeypatch, consumer_distribution): # noqa: F811 def isolated_workspace(*args): result = list(workspace(*args)) + # The shared cold-import fixture retains history/drain adapters. This + # stronger isolation oracle removes them only from its disposable copy; + # keep the pre-start assertion so a borrowed source cannot pass unnoticed. + receiver = result[-2] / "loopx" + for name in ("control_plane/coordination/runtime_shadow.py", + "control_plane/coordination/local_authority_shadow_adapter.py", + "control_plane/coordination/local_authority_shadow_outbox.py"): + (receiver / name).unlink(missing_ok=True) # The copied receiver is first, with only the selected dependencies; # -S prevents an editable finder restoring the removed modules. env = result[-1] From c486804bbc3c4c35b9038186a25f7852250aee6c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:56:37 +0800 Subject: [PATCH 32/98] fix(team-plan): recover original receipts before fresh admission Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../canonical-todo-completion-update.md | 14 ++- .../work_items/team_plan_adapter.py | 11 ++- .../control_plane/test_team_plan_authority.py | 91 ++++++++++++++----- 3 files changed, 86 insertions(+), 30 deletions(-) diff --git a/docs/reference/canonical-todo-completion-update.md b/docs/reference/canonical-todo-completion-update.md index 06390853f9..8adff406fd 100644 --- a/docs/reference/canonical-todo-completion-update.md +++ b/docs/reference/canonical-todo-completion-update.md @@ -598,8 +598,12 @@ canonical 写入;provider 丢失时明确拒绝,不重建空库。 Canonical team-plan preview, confirmed batch commit and original-operation replay use the TypeScript batch transaction and canonical projection outbox. -An unpromoted request returns `canonical_authority_required`; it cannot load a -capture producer or Markdown editor. +An unpromoted fresh request returns `canonical_authority_required`; it cannot +load a capture producer or Markdown editor. An exact pre-cutoff receipt may be +read back before import through the same typed identity validator, without +creating tasks, capture evidence or execution authority. The locked source +binding is revalidated first; a selected but unavailable provider still fails +rather than replaying from Markdown as fallback. The real File/SQLite Chat oracle physically removes the capture adapter from the package before preview and confirmation. It checks provider outage without @@ -613,8 +617,10 @@ Projection IO and historical receipt readers remain necessary. Assignment still grants neither receiver adoption nor execution authority. canonical team-plan 的预览、确认后整批提交与原操作重放复用 TS batch 事务及 -canonical 投影 outbox。未迁移请求返回 `canonical_authority_required`,不能加载 -capture producer 或 Markdown editor。 +canonical 投影 outbox。未迁移的新请求返回 `canonical_authority_required`,不能加载 +capture producer 或 Markdown editor。原 pre-cutoff 回执在导入前也可通过同一 TS +身份校验读回,不创建 Todo、capture 或执行授权。先在原锁内核验来源绑定;已选定 +provider 不可用时仍明确拒绝,不能将 Markdown 历史读取当作 fallback。 真实 File/SQLite Chat 检查在预览及确认前物理移除该模块,验证 provider 失效无 Markdown fallback、恢复、后续写入后的原操作重放、审核依据过期及提交后投影失败 恢复;导入前的历史回执重放保留后来写入与当前 revision。正常 wheel 复用同一 diff --git a/loopx/control_plane/work_items/team_plan_adapter.py b/loopx/control_plane/work_items/team_plan_adapter.py index 7b4b833dfc..764e8a3323 100644 --- a/loopx/control_plane/work_items/team_plan_adapter.py +++ b/loopx/control_plane/work_items/team_plan_adapter.py @@ -176,9 +176,6 @@ def previous_receipt(original: str) -> dict[str, Any] | None: raise ValueError("duplicate team plan operation receipts") return json.loads(base64.b64decode(matches[0], validate=True)) if matches else None - canonical = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=goal_id) - if canonical is None: - require_canonical_authority(goal_id=goal_id, operation="team_plan") targets = ( shadow_maintenance_lock_target(runtime, goal_id), legacy_coordination_todo_lock_path(runtime_root=runtime, goal_id=goal_id), @@ -196,12 +193,16 @@ def previous_receipt(original: str) -> dict[str, Any] | None: raise TeamPlanCommitError("team_plan_commit_failed", "team plan registry is busy; retry the original confirmation") from None read_source_request() - # Pre-promotion receipts remain historical facts, but only after the - # canonical route has been admitted and its provider is readable. + # Observe the selected provider under the transition/source locks: + # provider loss never authorizes a fallback to historical source. + canonical = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=goal_id) + # An exact pre-cutoff receipt is read-only recovery, not fresh work. previous = previous_receipt(state.read_text(encoding="utf-8")) if previous is not None: return dict(effect_runtime_result("work_items.team_plan.plan", { **request, "previous_receipt": previous})["result"]) + if canonical is None: + require_canonical_authority(goal_id=goal_id, operation="team_plan") response = effect_runtime_result("work_items.team_plan.commit", { **request, "runtime_root": str(runtime.resolve()), "expected_provider_revision": canonical["provider_revision"], diff --git a/tests/control_plane/test_team_plan_authority.py b/tests/control_plane/test_team_plan_authority.py index a2a2d1c4b6..98cba4ba9f 100644 --- a/tests/control_plane/test_team_plan_authority.py +++ b/tests/control_plane/test_team_plan_authority.py @@ -507,11 +507,10 @@ def test_capture_free_chat_recovers_committed_projection(canonical_team, capture assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a")["provider_revision"] == committed["provider_revision"] -@pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_capture_free_chat_preserves_pre_promotion_receipt(tmp_path, monkeypatch, provider): - """Imported history remains readable without recreating or overwriting work.""" +@pytest.fixture +def historical_team(tmp_path, monkeypatch): + """Persist the original typed receipt without calling a retired writer.""" from loopx.control_plane.effect_runtime import effect_runtime_result - from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection isolate_sqlite_runtime(tmp_path, monkeypatch) runtime = tmp_path / "runtime" @@ -552,11 +551,29 @@ def test_capture_free_chat_preserves_pre_promotion_receipt(tmp_path, monkeypatch f"# Goal\n\n\n\n" "## Agent Todo\n\n" ) + return runtime, state, registry, service, preview, planned + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_capture_free_chat_preserves_pre_promotion_receipt( + historical_team, tmp_path, provider, +): + """Original history is read-only before import and after newer writes.""" + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + + runtime, state, registry, service, preview, planned = historical_team + invoke = _capture_free_chat((runtime, state, service, preview), tmp_path) + before_import = state.read_bytes() + recovered = invoke("historical") + assert recovered["action"] == "reused" + assert recovered["lane_todo_ids"] == planned["result"]["lane_todo_ids"] + assert state.read_bytes() == before_import + assert not (runtime / "authority").exists() + projection = build_todo_runtime_shadow_projection( goal_id="goal-a", todos=planned["todos"], handoff_mode="soft_claim", ) initialize_canonical_authority(runtime, "goal-a", projection, state_path=state, provider=provider) - invoke = _capture_free_chat((runtime, state, service, preview), tmp_path) later = invoke("preview", text="New work after import") later_apply = invoke("apply", proposal=later["proposal_id"]) assert later_apply["proposal"]["status"] == "applied", later_apply["proposal"].get("failure") @@ -568,29 +585,61 @@ def test_capture_free_chat_preserves_pre_promotion_receipt(tmp_path, monkeypatch assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == current assert state.read_bytes() == display - # A missing canonical observation fails before source locks or a fallback - # read. Historical receipts remain readable only through canonical state. + # A selected but missing provider still refuses historical readback. + # Use the actual failed store, not a mocked unpromoted observation. + from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable from loopx.control_plane.work_items import team_plan_adapter - observed = [] - - def promotion_race(**kwargs): - observed.append(True) - return None backend = runtime / "authority" / current["source_authority"].replace("_", "-") offline = backend.with_name(backend.name + "-offline") backend.rename(offline) try: - with monkeypatch.context() as race: - race.setattr(team_plan_adapter, "read_canonical_todos_if_promoted", promotion_race) - saved = service.store.load(preview["proposal_id"]) - with pytest.raises(CanonicalAuthorityRequired): - team_plan_adapter.settle_team_plan( - registry_path=registry, goal_id="goal-a", agent_id=None, - proposal={**saved["normalized_parameters"]["plan"], "proposal_id": preview["proposal_id"]}, - expected_state_fingerprint=saved["expected_state_fingerprint"], basis_agent_id="alpha") - assert len(observed) == 1 + saved = service.store.load(preview["proposal_id"]) + with pytest.raises(LocalCoordinationAuthorityUnavailable): + team_plan_adapter.settle_team_plan( + registry_path=registry, goal_id="goal-a", agent_id=None, + proposal={**saved["normalized_parameters"]["plan"], "proposal_id": preview["proposal_id"]}, + expected_state_fingerprint=saved["expected_state_fingerprint"], basis_agent_id="alpha") assert state.read_bytes() == display and not backend.exists() finally: offline.rename(backend) assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == current + + +@pytest.mark.parametrize("conflict", ["actor", "intent", "basis"]) +def test_pre_cutoff_team_receipt_rejects_changed_identity(historical_team, conflict): + from loopx.control_plane.effect_runtime import EffectRuntimeRejected + from loopx.control_plane.work_items.team_plan_adapter import apply_team_plan + + runtime, state, registry, _service, preview, _planned = historical_team + plan = {**preview["normalized_parameters"]["plan"], "proposal_id": preview["proposal_id"]} + actor = "alpha" if conflict == "actor" else None + if conflict == "intent": + plan = {**plan, "objective": "Changed original intent"} + basis = "changed-basis" if conflict == "basis" else preview["expected_state_fingerprint"] + before = state.read_bytes() + with pytest.raises(EffectRuntimeRejected, match="identity mismatch"): + apply_team_plan(registry_path=registry, goal_id="goal-a", agent_id=actor, + proposal=plan, expected_state_fingerprint=basis) + assert state.read_bytes() == before + assert not (runtime / "authority").exists() + + +@pytest.mark.parametrize("history", ["missing", "malformed", "duplicate"]) +def test_pre_cutoff_team_receipt_never_recreates_work(historical_team, history): + from loopx.control_plane.work_items.team_plan_adapter import apply_team_plan + + runtime, state, registry, _service, preview, _planned = historical_team + original = state.read_text() + marker = next(line for line in original.splitlines() if line.startswith("\n") + registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{ + "id": goal, "repo": str(root), "state_file": state.name, + "coordination": {"registered_agents": ["agent-a", "agent-b"]}}]})) + projection = build_todo_runtime_shadow_projection(goal_id=goal, handoff_mode="soft_claim", todos=[{ + "schema_version": "todo_item_v0", "done": False, + "archive_state": "active", "source_section": "Agent Todo", + "todo_id": target, "text": "Imported work.", "role": "agent", "status": "open", + "task_class": "advancement_task", "removed_continuation_policy": policy, + }]) + initialize_canonical_authority(runtime, goal, projection, state_path=state, provider=provider) + before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=goal) + document = state.read_bytes() + prefix = ("todo", "update", "--goal-id", goal, "--todo-id", target, "--agent-id", "agent-a") + for edit in (("--text", "Unreviewed replacement."), ("--note", "Unreviewed annotation.")): + for preview in (("--dry-run",), ()): + code, rejected = run_json_cli_result(*prefix, *edit, *preview, registry_path=registry) + assert code == 1 and "repair it explicitly" in rejected["error"], rejected + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=goal) == before + assert state.read_bytes() == document + code, repaired = run_json_cli_result( + *prefix, "--continuation-policy", "independent_handoff", "--excluded-agent", "agent-b", + "--update-operation-id", "explicit-repair", registry_path=registry) + assert code == 0 and repaired["status"] == "applied", repaired + readback = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=goal) + assert readback["source_authority"] == f"{provider}_v0" + todo = readback["todos"][0] + assert todo["continuation_policy"] == "independent_handoff" + assert todo["excluded_agents"] == ["agent-b"] + assert not todo.get("removed_continuation_policy") + assert f"continuation_policy={policy}" not in state.read_text() + assert repaired["projection_delivery"] != "pending", repaired + code, edited = run_json_cli_result(*prefix, "--note", "After repair.", registry_path=registry) + assert code == 0 and edited["status"] == "applied", edited + later = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=goal) + code, replay = run_json_cli_result( + *prefix, "--continuation-policy", "independent_handoff", "--excluded-agent", "agent-b", + "--update-operation-id", "explicit-repair", registry_path=registry) + assert code == 0 and replay["status"] == "replayed", replay + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=goal)["todos"] == later["todos"] + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("operation_id", [None, "caller-owned-update"], ids=["generated", "explicit"]) @pytest.mark.parametrize("commit", [True, False], ids=["committed", "no-receipt"]) diff --git a/tests/control_plane_ts/todo_update.test.ts b/tests/control_plane_ts/todo_update.test.ts index 1cd0f103af..f8877d6cbc 100644 --- a/tests/control_plane_ts/todo_update.test.ts +++ b/tests/control_plane_ts/todo_update.test.ts @@ -253,6 +253,41 @@ async function seeded(overrides: Record = {}) { return {store, request}; } +test("imported removed policies reject copy edits and clear only after explicit repair", async () => { + for (const removed_continuation_policy of ["primary_review", "review_handoff"]) { + const {store, request} = await seeded({removed_continuation_policy}); + const before = await store.loadAuthority(); + for (const edit of [{patch: {text: "Unreviewed replacement"}, clear_fields: []}, + {patch: {note: "Unreviewed annotation"}, clear_fields: []}, + {patch: {}, clear_fields: ["note"]}]) { + for (const dry_run of [true, false]) { + const rejected = await executeCoordinationTodoUpdate(store, {...request, ...edit, dry_run}); + assert.equal(rejected.status, "failed", JSON.stringify(rejected)); + assert.match(String(rejected.reason), /repair it explicitly/); + assert.deepEqual(await store.loadAuthority(), before); + assert.equal((await store.readReceipt(request.operation_id)).status, "missing"); + } + } + const repair = {...request, patch: {}, clear_fields: [], planning_intent: { + continuation_policy: "independent_handoff", excluded_agents: ["agent-b"], + }}; + assert.equal((await executeCoordinationTodoUpdate(store, {...repair, dry_run: true})).status, "planned"); + assert.deepEqual(await store.loadAuthority(), before); + assert.equal((await executeCoordinationTodoUpdate(store, repair)).status, "applied"); + const repaired = await store.loadAuthority(); + assert.equal(repaired.status, "loaded"); + if (repaired.status !== "loaded") return; + const record = (repaired.head.todos as Record[])[0]!; + assert.equal(record.removed_continuation_policy, undefined); + assert.equal(record.continuation_policy, "independent_handoff"); + assert.deepEqual(record.excluded_agents, ["agent-b"]); + assert.equal((await executeCoordinationTodoUpdate(store, {...request, operation_id: "after-repair"})).status, "applied"); + const later = await store.loadAuthority(); + assert.equal((await executeCoordinationTodoUpdate(store, repair)).status, "replayed"); + assert.deepEqual(await store.loadAuthority(), later); + } +}); + test("provider-first update preserves typed external-wait rejection codes", async () => { const {store, request} = await seeded({ todo_id: "todo_waiting", From 32c2a85e8674ec3bb6e1c668c4876dead95e99cd Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:41:28 +0800 Subject: [PATCH 34/98] test: migrate promised Todo smokes to canonical transactions Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...esearch-role-successor-completion-smoke.py | 8 +- .../todo-claim-visibility-lanes-smoke.py | 78 ++--- .../todo-continuation-policy-smoke.py | 4 +- .../todo-write-correctness-smoke.py | 279 +++++------------- loopx/control_plane/testing/canary_harness.py | 52 ++++ 5 files changed, 168 insertions(+), 253 deletions(-) diff --git a/examples/control_plane/auto-research-role-successor-completion-smoke.py b/examples/control_plane/auto-research-role-successor-completion-smoke.py index b5931322ab..7270dea817 100644 --- a/examples/control_plane/auto-research-role-successor-completion-smoke.py +++ b/examples/control_plane/auto-research-role-successor-completion-smoke.py @@ -13,6 +13,7 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.control_plane.testing.canary_harness import ( # noqa: E402 + import_fixture_authority, run_json_cli, run_json_cli_result, write_fixture_registry, @@ -71,6 +72,7 @@ def write_fixture(root: Path, *, auto_research: bool) -> tuple[Path, Path, Path] registered_agents=[CURATOR, PROPOSER, EXECUTOR], peer_independent_worktree_required=False, ) + import_fixture_authority(registry_path, GOAL_ID) return project, runtime, registry_path @@ -122,7 +124,7 @@ def assert_actor_attribution_fails_closed() -> None: actor_agent_id=None, ) assert missing_code == 1, missing - assert "requires --agent-id" in missing["error"], missing + assert missing["error_code"] == "actor_required", missing assert state_path.read_text(encoding="utf-8") == before wrong_code, wrong = complete_proposer( @@ -131,7 +133,7 @@ def assert_actor_attribution_fails_closed() -> None: actor_agent_id=CURATOR, ) assert wrong_code == 1, wrong - assert f"claimed_by={PROPOSER!r}" in wrong["error"], wrong + assert wrong["error_code"] == "claim_owner_mismatch", wrong assert state_path.read_text(encoding="utf-8") == before @@ -142,7 +144,6 @@ def assert_auto_research_role_successor_allowed() -> None: assert returncode == 0, completed assert completed["ok"] is True, completed assert completed["status"] == "done", completed - assert completed["linked_successor_id"] == EXECUTOR_TODO, completed assert completed["successor_todo_ids"] == [EXECUTOR_TODO], completed assert completed["next_todos"] == [], completed @@ -168,7 +169,6 @@ def assert_generic_peer_route_links_existing_successor() -> None: returncode, completed = complete_proposer(registry_path, runtime) assert returncode == 0, completed assert completed["ok"] is True, completed - assert completed["linked_successor_id"] == EXECUTOR_TODO, completed assert completed["successor_todo_ids"] == [EXECUTOR_TODO], completed diff --git a/examples/control_plane/todo-claim-visibility-lanes-smoke.py b/examples/control_plane/todo-claim-visibility-lanes-smoke.py index 5b757ae8f4..6290287a5e 100644 --- a/examples/control_plane/todo-claim-visibility-lanes-smoke.py +++ b/examples/control_plane/todo-claim-visibility-lanes-smoke.py @@ -4,6 +4,7 @@ from __future__ import annotations import sys +import tempfile from pathlib import Path @@ -17,7 +18,9 @@ from loopx.control_plane.todos.quota_selection import project_quota_planning # noqa: E402 from loopx.control_plane.todos.contract import parse_todo_metadata_line # noqa: E402 from loopx.quota import build_quota_should_run # noqa: E402 -from loopx.todos import apply_todo_update_to_lines # noqa: E402 +from loopx.control_plane.testing.canary_harness import ( # noqa: E402 + import_fixture_authority, run_json_cli, run_json_cli_result, write_fixture_registry, +) from work_lane_contract_fixtures import GOAL_ID, status_payload # noqa: E402 @@ -423,40 +426,45 @@ def assert_legacy_review_handoff_write_requires_explicit_repair() -> None: "continuation_policy=review_handoff -->", ] - for updates in ( - {"claimed_by": CURRENT_AGENT, "claim_only": True}, - {"note": "Keep this legacy review blocked."}, - ): - lines = list(original_lines) - try: - apply_todo_update_to_lines( - lines, - todo_id="todo_legacy_write", - role="agent", - updated_at="2026-07-11T06:00:00+08:00", - **updates, - ) - except ValueError as exc: - assert "repair it" in str(exc), exc - else: - raise AssertionError("legacy review writes must fail closed") - assert lines == original_lines - - repaired_lines = list(original_lines) - repaired = apply_todo_update_to_lines( - repaired_lines, - todo_id="todo_legacy_write", - role="agent", - continuation_policy="independent_handoff", - excluded_agents=[CURRENT_AGENT], - updated_at="2026-07-11T06:00:00+08:00", - ) - assert repaired["continuation_policy"] == "independent_handoff", repaired - assert repaired["excluded_agents"] == [CURRENT_AGENT], repaired - repaired_text = "\n".join(repaired_lines) - assert "continuation_policy=review_handoff" not in repaired_text - assert "continuation_policy=independent_handoff" in repaired_text - assert f"excluded_agents={CURRENT_AGENT}" in repaired_text + with tempfile.TemporaryDirectory(prefix="loopx-claim-repair-") as tmp: + root = Path(tmp).resolve() + state = root / "ACTIVE_GOAL_STATE.md" + state.write_text("\n".join(original_lines) + "\n", encoding="utf-8") + registry = root / ".loopx/registry.json" + write_fixture_registry( + project=root, runtime_root=root / "runtime", registry_path=registry, + goal_id=GOAL_ID, domain="claim-repair-fixture", + adapter_kind="generic_project_goal_v0", state_file=state.name, + registered_agents=[CURRENT_AGENT, OTHER_AGENT], + ) + import_fixture_authority(registry, GOAL_ID) + before = state.read_bytes() + prefix = ("todo", "update", "--goal-id", GOAL_ID, + "--todo-id", "todo_legacy_write", "--agent-id", OTHER_AGENT) + revision = run_json_cli("todo", "list", "--goal-id", GOAL_ID, + registry_path=registry)["authority_read"]["provider_revision"] + claim = ("todo", "claim", "--goal-id", GOAL_ID, + "--todo-id", "todo_legacy_write", "--agent-id", CURRENT_AGENT, + "--claimed-by", CURRENT_AGENT) + for request in (claim, (*prefix, "--note", "Keep this legacy review blocked.")): + code, rejected = run_json_cli_result(*request, registry_path=registry) + assert code == 1, rejected + if request is claim: + assert rejected["error_code"] == "removed_continuation_policy", rejected + else: + assert "repair it" in rejected["error"], rejected + assert state.read_bytes() == before + assert run_json_cli("todo", "list", "--goal-id", GOAL_ID, + registry_path=registry)["authority_read"]["provider_revision"] == revision + repaired = run_json_cli(*prefix, "--continuation-policy", "independent_handoff", + "--excluded-agent", CURRENT_AGENT, registry_path=registry) + assert repaired["source_authority"] == "file_v0", repaired + current = run_json_cli("todo", "list", "--goal-id", GOAL_ID, + "--todo-id", "todo_legacy_write", registry_path=registry)["todo"] + assert current["continuation_policy"] == "independent_handoff", current + assert current["excluded_agents"] == [CURRENT_AGENT], current + assert not current.get("removed_continuation_policy"), current + assert "continuation_policy=review_handoff" not in state.read_text() def assert_claim_visibility_lanes_split_current_other_and_task_class() -> None: diff --git a/examples/control_plane/todo-continuation-policy-smoke.py b/examples/control_plane/todo-continuation-policy-smoke.py index 2a5098a5ff..08f13c93e9 100644 --- a/examples/control_plane/todo-continuation-policy-smoke.py +++ b/examples/control_plane/todo-continuation-policy-smoke.py @@ -13,6 +13,7 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.control_plane.testing.canary_harness import ( # noqa: E402 + import_fixture_authority, run_json_cli, run_json_cli_result, write_fixture_registry, @@ -50,6 +51,7 @@ def write_fixture(root: Path) -> tuple[Path, Path]: registered_agents=[PEER_ALPHA, PEER_BETA], quota_allowed_slots=None, ) + import_fixture_authority(registry_path, GOAL_ID) return registry_path, state_file @@ -165,7 +167,7 @@ def main() -> int: registry_path=registry_path, ) assert returncode == 1, rejected - assert "cannot also appear in next_excluded_agents" in rejected["error"] + assert "cannot also appear in excluded_agents" in rejected["error"] assert agent_todo(state_file, conflict["todo_id"])["status"] == "open" missing_evidence = add_todo(registry_path, "Require self-merge evidence.") diff --git a/examples/control_plane/todo-write-correctness-smoke.py b/examples/control_plane/todo-write-correctness-smoke.py index 9dceb28031..fe36b4d3f2 100644 --- a/examples/control_plane/todo-write-correctness-smoke.py +++ b/examples/control_plane/todo-write-correctness-smoke.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 -"""Smoke-test todo add/update dry-run write correctness packets.""" +"""Qualify canonical Todo previews, once-only commit, actor checks and CAS.""" from __future__ import annotations -import hashlib import json import subprocess import sys @@ -15,11 +14,10 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) -from loopx.control_plane.runtime.local_state_write_correctness import ( # noqa: E402 - shadow_validate_local_state_write_correctness_packet, -) from loopx.control_plane.testing.canary_harness import ( # noqa: E402 + import_fixture_authority, run_json_cli, + run_json_cli_result, write_fixture_registry, ) @@ -55,6 +53,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: registered_agents=("codex-main-control", AGENT_ID), quota_allowed_slots=None, ) + import_fixture_authority(registry_path, GOAL_ID) return registry_path, state_file, project @@ -83,216 +82,70 @@ def run_cli(registry_path: Path, *args: str, as_json: bool = True) -> dict | str return result.stdout -def assert_packet( - payload: dict, - *, - state_file: Path, - project: Path, - write_class: str, - todo_id: str, - claimed_by: str | None, -) -> dict: - packet = payload.get("local_state_write_correctness") - assert isinstance(packet, dict), payload - assert packet["schema_version"] == "local_state_write_correctness_v0", packet - assert str(project) not in json.dumps(packet, ensure_ascii=False), packet - - intent = packet["write_intent"] - assert intent["goal_id"] == GOAL_ID, packet - assert intent["writer_id"] == "loopx.todo", packet - assert intent["write_class"] == write_class, packet - assert intent["target_refs"]["state_file_ref"] == "registry.goal.state_file", packet - assert intent["target_refs"]["todo_id"] == todo_id, packet - assert intent["expected_revision"]["value"] == "sha256:" + hashlib.sha256( - state_file.read_text(encoding="utf-8").encode("utf-8") - ).hexdigest(), packet - assert intent["idempotency_key"].startswith(f"{GOAL_ID}:{write_class}:"), packet - - assert packet["lock_boundary"] == { - "kind": "per_goal", - "lock_key": f"goal:{GOAL_ID}", - "narrower_lock_allowed": "per_todo_when_patch_is_single_todo_and_order_independent", - }, packet - assert packet["preview"]["mode"] == "dry_run", packet - assert packet["preview"]["non_destructive"] is True, packet - assert packet["preview"]["expected_write_scopes"] == ["active_state"], packet - assert packet["apply_result"]["status"] == "preview_only", packet - boundary = packet["projection"]["public_boundary"] - assert boundary == { - "raw_logs_copied": False, - "private_paths_copied": False, - "credentials_copied": False, - "production_action_authorized": False, - }, packet - - if claimed_by: - assert intent["lease_ref"]["todo_id"] == todo_id, packet - assert intent["lease_ref"]["claimed_by"] == claimed_by, packet - assert packet["projection"]["lease_projection"] == { - "todo_id": todo_id, - "claimed_by": claimed_by, - "lease_state": "preview_only", - }, packet - else: - assert intent["lease_ref"] is None, packet - assert packet["projection"]["lease_projection"] is None, packet - return packet - - def main() -> int: with tempfile.TemporaryDirectory(prefix="loopx-todo-write-correctness-") as tmp: - registry_path, state_file, project = write_fixture(Path(tmp)) - original = state_file.read_text(encoding="utf-8") - - add_dry_run = run_cli( - registry_path, - "todo", - "add", - "--goal-id", - GOAL_ID, - "--role", - "agent", - "--text", - TODO_TEXT, - "--claimed-by", - AGENT_ID, - "--dry-run", - ) - add_dry_run_repeat = run_cli( - registry_path, - "todo", - "add", - "--goal-id", - GOAL_ID, - "--role", - "agent", - "--text", - TODO_TEXT, - "--claimed-by", - AGENT_ID, - "--dry-run", - ) - assert add_dry_run["dry_run"] is True, add_dry_run - assert add_dry_run["added"] is True, add_dry_run - assert state_file.read_text(encoding="utf-8") == original - add_packet = assert_packet( - add_dry_run, - state_file=state_file, - project=project, - write_class="todo_add", - todo_id=add_dry_run["todo_id"], - claimed_by=AGENT_ID, - ) - clean_shadow = shadow_validate_local_state_write_correctness_packet( - add_packet, - current_state_text=state_file.read_text(encoding="utf-8"), - observed_lease_ref=add_packet["write_intent"]["lease_ref"], - ) - assert clean_shadow["apply_result"]["status"] == "preview_only", clean_shadow - assert clean_shadow["apply_result"]["conflict"] is None, clean_shadow - - revision_conflict = shadow_validate_local_state_write_correctness_packet( - add_packet, - current_state_text=( - state_file.read_text(encoding="utf-8") - + "\n\n" - ), - ) - assert ( - revision_conflict["apply_result"]["status"] == "revision_conflict" - ), revision_conflict - revision_conflict_detail = revision_conflict["apply_result"]["conflict"] - assert revision_conflict_detail["kind"] == "revision_conflict", revision_conflict - assert revision_conflict_detail["expected_revision"] == add_packet["write_intent"][ - "expected_revision" - ], revision_conflict - assert revision_conflict_detail["current_revision"] != add_packet["write_intent"][ - "expected_revision" - ], revision_conflict - assert state_file.read_text(encoding="utf-8") == original - - foreign_lease = dict(add_packet["write_intent"]["lease_ref"]) - foreign_lease["claimed_by"] = "codex-main-control" - foreign_lease["lease_id"] = f"lease_{add_dry_run['todo_id']}_codex_main_control" - lease_conflict = shadow_validate_local_state_write_correctness_packet( - add_packet, - current_state_text=state_file.read_text(encoding="utf-8"), - observed_lease_ref=foreign_lease, - ) - assert lease_conflict["apply_result"]["status"] == "lease_conflict", lease_conflict - lease_conflict_detail = lease_conflict["apply_result"]["conflict"] - assert lease_conflict_detail["kind"] == "lease_conflict", lease_conflict - assert lease_conflict_detail["expected_lease_ref"] == add_packet["write_intent"][ - "lease_ref" - ], lease_conflict - assert lease_conflict_detail["observed_lease_ref"] == foreign_lease, lease_conflict - assert state_file.read_text(encoding="utf-8") == original - - repeat_packet = add_dry_run_repeat["local_state_write_correctness"] - assert ( - add_packet["write_intent"]["idempotency_key"] - == repeat_packet["write_intent"]["idempotency_key"] - ) - - add_markdown = run_cli( - registry_path, - "todo", - "add", - "--goal-id", - GOAL_ID, - "--role", - "agent", - "--text", - TODO_TEXT, - "--dry-run", - as_json=False, - ) - assert "## Local State Write Correctness" in add_markdown, add_markdown - assert "status: `preview_only`" in add_markdown, add_markdown - - real_add = run_cli( - registry_path, - "todo", - "add", - "--goal-id", - GOAL_ID, - "--role", - "agent", - "--text", - TODO_TEXT, - ) - assert real_add["dry_run"] is False, real_add - assert "local_state_write_correctness" not in real_add, real_add - - update_dry_run = run_cli( - registry_path, - "todo", - "update", - "--goal-id", - GOAL_ID, - "--todo-id", - "todo_existing", - "--agent-id", - AGENT_ID, - "--claimed-by", - AGENT_ID, - "--note", - "Preview update without writing state.", - "--dry-run", - ) - assert update_dry_run["dry_run"] is True, update_dry_run - assert update_dry_run["changed"] is True, update_dry_run - assert_packet( - update_dry_run, - state_file=state_file, - project=project, - write_class="todo_update", - todo_id="todo_existing", - claimed_by=AGENT_ID, - ) - assert "Preview update without writing state." not in state_file.read_text( - encoding="utf-8" - ) + registry, state, project = write_fixture(Path(tmp)) + + def readback() -> dict: + return run_cli(registry, "todo", "list", "--goal-id", GOAL_ID) + + def unchanged(before: dict, document: bytes) -> None: + after = readback() + assert after["todos"] == before["todos"], after + assert after["authority_read"] == before["authority_read"], after + assert state.read_bytes() == document + + before, document = readback(), state.read_bytes() + create = ("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--text", TODO_TEXT, "--claimed-by", AGENT_ID, + "--operation-id", "preview-create") + first = run_cli(registry, *create, "--dry-run") + repeat = run_cli(registry, *create, "--dry-run") + for planned in (first, repeat): + assert planned["status"] == "planned" and planned["dry_run"] is True, planned + assert planned["source_authority"] == "file_v0", planned + assert planned["decision_read_from_provider"] is True, planned + assert planned["legacy_fallback_used"] is False, planned + assert str(project) not in json.dumps(planned), planned + assert first["todo_id"] == repeat["todo_id"] + assert first["operation_id"] == repeat["operation_id"] == "preview-create" + unchanged(before, document) + # A preview must not consume the original operation or mint its receipt. + committed = run_cli(registry, *create) + assert committed["todo_id"] == first["todo_id"] and committed["status"] == "applied", committed + replay = run_cli(registry, *create) + assert replay["provider_revision"] == committed["provider_revision"], replay + assert len([t for t in readback()["todos"] if t["todo_id"] == first["todo_id"]]) == 1 + + before, document = readback(), state.read_bytes() + update = ("todo", "update", "--goal-id", GOAL_ID, "--todo-id", first["todo_id"], + "--agent-id", AGENT_ID, "--note", "Preview update without writing state.") + planned = run_cli(registry, *update, "--dry-run") + assert planned["status"] == "planned", planned + unchanged(before, document) + + # The provider revision, rather than editable Markdown bytes, owns CAS. + revision = before["authority_read"]["provider_revision"] + run_cli(registry, "todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--text", "A later independently committed Todo.") + before, document = readback(), state.read_bytes() + for dry_run in (("--dry-run",), ()): + code, stale = run_json_cli_result( + *update, "--update-operation-id", "stale-preview", + "--update-expected-provider-revision", revision, *dry_run, + registry_path=registry) + assert code == 1 and stale["error_code"] == "provider_revision_mismatch", stale + unchanged(before, document) + code, foreign = run_json_cli_result( + "todo", "update", "--goal-id", GOAL_ID, "--todo-id", first["todo_id"], + "--agent-id", "codex-main-control", "--note", "Foreign actor edit.", + *dry_run, registry_path=registry) + assert code == 1 and "claimed_by" in foreign["error"], foreign + unchanged(before, document) + applied = run_cli(registry, *update) + assert applied["status"] == "applied", applied + todo = next(t for t in readback()["todos"] if t["todo_id"] == first["todo_id"]) + assert todo["note"] == "Preview update without writing state.", todo print("todo-write-correctness-smoke ok") return 0 diff --git a/loopx/control_plane/testing/canary_harness.py b/loopx/control_plane/testing/canary_harness.py index c280b13f09..724a19227c 100644 --- a/loopx/control_plane/testing/canary_harness.py +++ b/loopx/control_plane/testing/canary_harness.py @@ -182,3 +182,55 @@ def read_run_index(runtime_root: Path, goal_id: str) -> list[dict[str, Any]]: for line in index_path.read_text(encoding="utf-8").splitlines() if line.strip() ] + + +def import_fixture_authority(registry_path: Path, goal_id: str) -> None: + """Import a disposable smoke's complete source through the real saved plan. + + Fixture writers have finished before this call. Historical metadata stays + intact; normal writes afterwards use the canonical File/soft-claim owner. + Existing unpromoted/refusal fixtures opt in explicitly, never by default. + """ + from ...state_backup import build_state_backup_plan, execute_state_backup_plan + + registry_path = registry_path.resolve() + registry = json.loads(registry_path.read_text(encoding="utf-8")) + goal = next(item for item in registry["goals"] if item["id"] == goal_id) + project = Path(goal["repo"]).resolve() + runtime = Path(registry["common_runtime_root"]).resolve() + # Bind the physical source consistently on hosts whose temp path is a link. + goal["repo"] = str(project) + registry_path.write_text(json.dumps(registry), encoding="utf-8") + runtime.mkdir(parents=True, exist_ok=True) + backup = execute_state_backup_plan(build_state_backup_plan( + project=project, runtime_root=runtime, + output_dir=runtime.parent / "fixture-backups", backup_id=goal_id, + include_automations=False, include_skills=False, + include_registry_projects=False, registry_path=registry_path, + )) + assert backup["ok"], backup + prefix = ("coordination-shadow",) + identity = ("--goal-id", goal_id, "--operation-id", f"fixture-import:{goal_id}") + prepared = run_json_cli( + *prefix, "prepare-import", *identity, + "--backup-manifest", backup["manifest_path"], + "--provider", "file", "--target-handoff-mode", "soft_claim", + registry_path=registry_path, runtime_root=runtime, + )["cold_import"] + assert prepared["status"] == "prepared", prepared + # Read the saved carrier rather than manufacturing a digest/target. + plan = json.loads(Path(prepared["plan_path"]).read_text(encoding="utf-8")) + assert plan["plan_sha256"] == prepared["plan_sha256"], plan + applied = run_json_cli( + *prefix, "apply-import", *identity, + "--plan-sha256", prepared["plan_sha256"], "--writers-stopped", "--execute", + registry_path=registry_path, runtime_root=runtime, + )["cold_import"] + assert applied["status"] in {"applied", "recovered"}, applied + readback = run_json_cli( + "todo", "list", "--goal-id", goal_id, + registry_path=registry_path, runtime_root=runtime, + )["authority_read"] + assert readback["source_authority"] == "file_v0", readback + assert readback["decision_read_from_provider"] is True, readback + assert readback["legacy_fallback_used"] is False, readback From dc084ba58ac4ac99725496a2875d510ba0e9bfdb Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:36:40 +0800 Subject: [PATCH 35/98] fix: allow same-title successors in terminal transactions Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../coordination/todo_terminal_lifecycle.ts | 17 ++-- .../test_native_todo_planning_update.py | 50 ++++++++++++ .../terminal_source_conformance.ts | 77 ++++++++++++++++++- 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index 3fae90019a..3ede74fa7c 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -1023,12 +1023,16 @@ function terminalTarget( } function semanticDuplicate( - todos: ReadonlyMap, + todos: Iterable, successor: JsonObject, + retiringTodoId: string | null = null, ): JsonObject | undefined { - return [...todos.values()].find((todo) => - todo.role === successor.role && todo.archive_state === "active" && - todo.status !== "done" && todo.status !== "deferred" && todo.text === successor.text); + for (const todo of todos) { + if (todo.todo_id !== retiringTodoId && todo.role === successor.role && + todo.archive_state === "active" && todo.status !== "done" && + todo.status !== "deferred" && todo.text === successor.text) return todo; + } + return undefined; } /** @@ -1636,7 +1640,10 @@ export async function executeCoordinationTodoTerminalLifecycle( todo_id: successorId, }); } - const duplicate = semanticDuplicate(projection.todos, successor); + // This CAS retires the source and creates the successors together. Only + // that source stops competing for its title. Peers still participate in + // the unchanged rule; successor admission already permits one per role. + const duplicate = semanticDuplicate(projection.todos.values(), successor, input.todo_id); if (duplicate !== undefined) { return terminalFailure( "todo_semantic_duplicate_conflict", diff --git a/tests/control_plane/test_native_todo_planning_update.py b/tests/control_plane/test_native_todo_planning_update.py index 59420ecc52..b9857da4a5 100644 --- a/tests/control_plane/test_native_todo_planning_update.py +++ b/tests/control_plane/test_native_todo_planning_update.py @@ -67,6 +67,56 @@ def records(registry: Path) -> dict[str, dict]: return {item["todo_id"]: item for item in list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"]} +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("command", ["complete", "supersede"]) +def test_cli_same_title_terminal_successor_preserves_peers_and_replay(tmp_path, monkeypatch, provider, command): + from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted + from loopx.control_plane.testing.canary_harness import run_json_cli, run_json_cli_result + + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, state = fixture(tmp_path, provider) + runtime = tmp_path / "runtime" + before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + prefix = ("todo", command, "--goal-id", "goal-a", "--todo-id", "todo_target") + flags = ("--evidence", "Synthetic accepted result", "--self-merged") if command == "complete" else ( + "--reason", "Synthetic replacement of the accepted work") + for actor, title, code in (("agent-b", "Synthetic task", "claim_owner_mismatch"), + ("agent-a", "Independent task", "todo_semantic_duplicate_conflict")): + result_code, rejected = run_json_cli_result( + *prefix, "--agent-id", actor, *flags, "--next-agent-todo", title, + "--next-claimed-by", "agent-a", registry_path=registry) + assert result_code == 1 and rejected["error_code"] == code, rejected + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == before + assert not state.exists() + terminal = (*prefix, "--agent-id", "agent-a", *flags, + "--next-agent-todo", "Synthetic task", "--next-claimed-by", "agent-a") + preview = run_json_cli(*terminal, "--dry-run", registry_path=registry) + assert preview["dry_run"] is True, preview + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == before + assert not state.exists() + applied = run_json_cli(*terminal, registry_path=registry) + assert applied["changed"] is True and applied["projection_delivery"] != "pending", applied + current = records(registry) + origin = current["todo_target"] + successor_id = origin["successor_todo_ids"][0] + assert successor_id not in {"todo_target", "todo_other"} + assert origin["status"] == "done" + assert current[successor_id]["text"] == "Synthetic task" + assert current[successor_id]["status"] == "open" + assert current["todo_other"]["status"] == "open" + if command == "complete": + assert current[successor_id]["unblocks_todo_id"] == "todo_target" + else: + assert origin["superseded_by"] == successor_id + run_json_cli("todo", "add", "--goal-id", "goal-a", "--role", "agent", + "--text", "A later independently admitted Todo", registry_path=registry) + later = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + replay = run_json_cli(*terminal, registry_path=registry) + assert replay["changed"] is False, replay + after = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + assert after["todos"] == later["todos"] and after["provider_revision"] == later["provider_revision"] + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("edit,expected_actor", [ (["--evidence", "Synthetic planning validation"], "agent-a"), diff --git a/tests/control_plane_ts/terminal_source_conformance.ts b/tests/control_plane_ts/terminal_source_conformance.ts index 3208b5e74e..62fdd6eb2e 100644 --- a/tests/control_plane_ts/terminal_source_conformance.ts +++ b/tests/control_plane_ts/terminal_source_conformance.ts @@ -10,7 +10,7 @@ import {executeCoordinationTodoTerminalLifecycle as execute, import {prepareCoordinationProjectionCommit} from "../../loopx/control_plane/coordination/coordination_projection.ts"; import {canonicalAuthoritySha256} from "../../loopx/control_plane/coordination/authority_store_codec.ts"; import type {AuthorityStoreConformanceFactory} from "./authority_store_conformance.ts"; -import {projectionFixtureAsSchema} from "./authority_projection_fixture.ts"; +import {authorityProjectionFixture, projectionFixtureAsSchema} from "./authority_projection_fixture.ts"; import {productionScaleCoordinationFixture, PRODUCTION_SCALE_VALIDATION_DECLARATION as declaration} from "./production_scale_coordination_fixture.ts"; @@ -27,6 +27,81 @@ const validation = {schema_version: "issue_fix_validation_command_v0", command_l export function registerTerminalSourceConformance(provider: string, factory: AuthorityStoreConformanceFactory): void { for (const schema of ["legacy", "native"] as const) { + for (const command of ["complete", "supersede"] as const) { + test(`${provider}: ${command} replaces its own title without admitting duplicate work (${schema})`, async t => { + const {store, contender} = await factory(t); + const goal = "terminal-same-title", title = "Continue the next verified slice"; + const origin: JsonObject = {todo_id: "todo_origin", role: "agent", status: "open", done: false, + archive_state: "active", text: title, task_class: "advancement_task", claimed_by: "agent-a"}; + await store.commitAuthority({operation_id: "seed", expected_provider_revision: null, + next_projection: authorityProjectionFixture(goal, [origin], [], schema, {handoff_mode: "soft_claim"}), + events: [], receipts: []}); + const policy = {schema_version: "loopx_todo_completion_policy_request_v0", goal_id: goal, + agent_model: "peer_v1", registered_agents: ["agent-a", "agent-b"], claimed_by: "agent-a", + next_claimed_by: "agent-a", next_agent_todo: title, next_continuation_policy: null, + next_excluded_agents: [], self_merged: true, evidence: "Synthetic verified completion"}; + const request: CoordinationTodoTerminalLifecycleInput = { + goal_id: goal, todo_id: "todo_origin", expected_role: "agent", command, + actor_agent_id: "agent-a", registered_agents: ["agent-a", "agent-b"], lifecycle_grants: [], + authority_reason: null, decision_outcome: null, + operation_identity: {kind: "explicit", operation_id: "replace-origin"}, + lease_idempotency_key: null, lease_expected_version: null, allow_user_gate_auto_acquire: false, + requested_no_followup: false, requested_completion_turn_key: null, + requested_completion_identity_source: null, linked_successor_todo_ids: [], + successor_intents: [{role: "agent", text: title, task_class: "advancement_task", claimed_by: "agent-a"}], + note: null, evidence: "Synthetic verified completion", reason: null, clear_claim: false, + validation_declaration: null, validation_receipt: null, + completion_policy_request: command === "complete" ? policy : null, + dry_run: false, now: new Date("2026-09-05T23:00:00Z"), + }; + const before = await loaded(store); + const foreign = await execute(store, {...request, actor_agent_id: "agent-b"}); + assert.equal(foreign.reason_code, "claim_owner_mismatch"); + assert.deepEqual(await loaded(store), before); + assert.equal((await store.readReceipt("replace-origin")).status, "missing"); + const duplicateUser = {role: "user", text: "Verify the same outcome", task_class: "user_action", bound_agent: "agent-a"}; + const siblings = await execute(store, {...request, successor_intents: [duplicateUser, duplicateUser], + completion_policy_request: command === "complete" ? {...policy, next_agent_todo: null, next_claimed_by: null} : null}); + assert.equal(siblings.reason_code, "invalid_todo_successor_derivation", JSON.stringify(siblings)); + assert.match(String(siblings.reason), /at most one generated user successor/); + assert.deepEqual(await loaded(store), before); + assert.equal((await store.readReceipt("replace-origin")).status, "missing"); + const preview = await execute(store, {...request, dry_run: true}); + assert.equal(preview.status, "planned", JSON.stringify(preview)); + assert.deepEqual(await loaded(store), before); + assert.equal((await store.readReceipt("replace-origin")).status, "missing"); + const result = await execute(store, request); + assert.equal(result.status, "applied", JSON.stringify(result)); + const after = await loaded(store), records = after.head.todos as JsonObject[]; + const completed = records.find(todo => todo.todo_id === "todo_origin")!; + const successor = records.find(todo => todo.todo_id !== "todo_origin")!; + assert.equal(records.length, 2); + assert.equal(completed.status, "done"); + assert.equal(successor.status, "open"); + assert.equal(successor.text, title); + if (command === "complete") assert.equal(successor.unblocks_todo_id, "todo_origin"); + else { + assert.equal(completed.superseded_by, successor.todo_id); + assert.equal(successor.unblocks_todo_id, origin.unblocks_todo_id); + } + assert.deepEqual(completed.successor_todo_ids, [successor.todo_id]); + // A still-open peer with this title remains a conflict; completing the + // new successor cannot create a third copy while that peer is active. + const peer = {...successor, todo_id: "todo_peer", claimed_by: "agent-b"}; + await contender.commitAuthority(prepareCoordinationProjectionCommit({goal_id: goal, + operation_id: "later-peer", expected_provider_revision: after.provider_revision, + projection: after.head, mutations: [{kind: "todo_upsert", todo: peer}]})); + const later = await loaded(store); + const duplicate = await execute(store, {...request, todo_id: String(successor.todo_id), + operation_identity: {kind: "explicit", operation_id: "must-not-duplicate"}}); + assert.equal(duplicate.reason_code, "todo_semantic_duplicate_conflict", JSON.stringify(duplicate)); + assert.equal(duplicate.todo_id, "todo_peer"); + assert.deepEqual(await loaded(store), later); + assert.equal((await store.readReceipt("must-not-duplicate")).status, "missing"); + assert.equal((await execute(contender, request)).status, "replayed"); + assert.deepEqual(await loaded(store), later); + }); + } test(`${provider}: completion resolves linked work from its canonical head (${schema})`, async t => { // Caller snapshots may lag a peer's completion, reopening or role change. // The declared link order, rather than caller-supplied facts or store order, From ffab1e0a683739041afb82299fe1429d46388f30 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:36:56 +0800 Subject: [PATCH 36/98] fix: replace complete folded Todo blocks during projection Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/todos/machine_region.py | 15 ++++++++++- .../control_plane/test_todo_machine_region.py | 25 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/loopx/control_plane/todos/machine_region.py b/loopx/control_plane/todos/machine_region.py index 381daf74b2..75b50c99dc 100644 --- a/loopx/control_plane/todos/machine_region.py +++ b/loopx/control_plane/todos/machine_region.py @@ -94,22 +94,35 @@ def find_todo_regions(lines: list[str], *, visible: frozenset[int] | None = None marked = index < len(lines) and lines[index].rstrip("\r\n") == todo_region_marker(role, "begin") if marked: index += 1 + legacy_task_open = False while index < len(lines): content = lines[index].rstrip("\r\n") if content == todo_region_marker(role, "end") and marked: break if TODO_REGION_PREFIX in content: raise ValueError("nested, mismatched, or malformed Todo region marker") + task = TODO_TASK_PATTERN.match(content) is not None + legacy_continuation = ( + not marked and legacy_task_open and content.startswith((" ", "\t")) + ) generated = index in visible and ( not content.strip() - or TODO_TASK_PATTERN.match(content) is not None + or task or parse_todo_metadata_line(content) is not None or content.startswith("\n") + tail = " Indented narrative after the task block.\n\n```markdown\n## Agent Todo\n- [ ] Example only.\n```\n" + source = (_source() + task + "\n" + tail).replace("\n", newline) + record = {"schema_version": "todo_item_v0", "role": "agent", "index": 1, + "source_section": "Agent Todo", "archive_state": "active", + "status": "open", "done": False, "task_class": "advancement_task", + "todo_id": "todo_folded", "text": "A folded task with a retained identity."} + projected = render_canonical_todo_sections(source, [record], provider_revision="rev-1") + assert projected.markdown.endswith(tail.replace("\n", newline)) + assert projected.markdown.count("todo_id=todo_folded") == 1 + assert projected.markdown.count("A folded task with a retained identity.") == 1 + readback, _, _ = parse_todo_source(projected.markdown) + assert readback["agent"][0]["text"] == "A folded task with a retained identity." + assert readback["agent"][0]["todo_id"] == "todo_folded" + assert render_canonical_todo_sections( + projected.markdown, [record], provider_revision="rev-1" + ).markdown == projected.markdown + + @pytest.mark.parametrize("example", [ "```markdown\n## Agent Todo\n- [ ] Example only.\n```\n", "~~~~markdown\n## Agent Todo\n- [ ] Example only.\n~~~~\n", From 81c81a62f1fc3cad4cf3e600b97a4d72cc975ec0 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:36:56 +0800 Subject: [PATCH 37/98] test: migrate Todo CLI lifecycle coverage to canonical authority Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- examples/control_plane/todo-cli-smoke.py | 186 +++++++++++++----- .../control_plane/todo-lifecycle-cli-smoke.py | 75 ++++--- .../control_plane/todo_lifecycle_fixtures.py | 13 +- 3 files changed, 176 insertions(+), 98 deletions(-) diff --git a/examples/control_plane/todo-cli-smoke.py b/examples/control_plane/todo-cli-smoke.py index 693129e18f..316289b410 100644 --- a/examples/control_plane/todo-cli-smoke.py +++ b/examples/control_plane/todo-cli-smoke.py @@ -13,6 +13,7 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.control_plane.testing.canary_harness import ( # noqa: E402 + import_fixture_authority, run_json_cli, run_json_cli_result, write_fixture_registry, @@ -34,7 +35,7 @@ MONITOR_EXPIRES_AT = "2026-01-04T00:00:00+00:00" -def write_fixture(root: Path, *, register_agents: bool = True) -> tuple[Path, Path]: +def write_fixture(root: Path, *, register_agents: bool = True, folded_source: bool = False) -> tuple[Path, Path]: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -48,10 +49,20 @@ def write_fixture(root: Path, *, register_agents: bool = True) -> tuple[Path, Pa "# Active Goal State\n\n" "## Objective\n\n" "Keep this fixture small.\n\n" + "## User Todo / Owner Review Reading Queue\n\n" + "## Agent Todo\n\n" + "## Completed Work Archive\n\n" "## Next Action\n\n" "- Choose the next bounded step.\n", encoding="utf-8", ) + if folded_source: + text = state_file.read_text(encoding="utf-8") + state_file.write_text(text.replace("## Agent Todo\n\n", + "## Agent Todo\n\n- [ ] Summarize the read-only evidence after the user\n" + " checklist is done.\n" + " \n\n"), + encoding="utf-8") write_fixture_registry( project=project, runtime_root=runtime, @@ -64,6 +75,7 @@ def write_fixture(root: Path, *, register_agents: bool = True) -> tuple[Path, Pa else None, quota_allowed_slots=None, ) + import_fixture_authority(registry_path, GOAL_ID) return registry_path, state_file @@ -82,11 +94,27 @@ def run_cli_error(registry_path: Path, *args: str) -> dict: return payload +def assert_folded_source_import_preserves_identity() -> None: + with tempfile.TemporaryDirectory(prefix="loopx-folded-source-") as tmp: + registry, state = write_fixture(Path(tmp), folded_source=True) + imported = run_cli(registry, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", "todo_folded")["todo"] + assert imported["text"] == AGENT_TODO, imported + duplicate = run_cli(registry, "todo", "add", "--goal-id", GOAL_ID, + "--role", "agent", "--text", AGENT_TODO, "--task-class", "advancement_task") + assert duplicate["already_exists"] is True and duplicate["todo_id"] == "todo_folded", duplicate + listing = run_cli(registry, "todo", "list", "--goal-id", GOAL_ID) + assert len(listing["todos"]) == 1, listing + assert listing["authority_read"]["legacy_fallback_used"] is False, listing + assert state.read_text(encoding="utf-8").count("todo_id=todo_folded") == 1 + + def main() -> int: + assert_folded_source_import_preserves_identity() with tempfile.TemporaryDirectory(prefix="loopx-todo-cli-smoke-") as tmp: root = Path(tmp) registry_path, state_file = write_fixture(root) - legacy_registry_path, _ = write_fixture(root / "legacy", register_agents=False) + legacy_registry_path, legacy_state_file = write_fixture(root / "legacy", register_agents=False) claim_state_registry_path, claim_state_file = write_fixture(root / "claim-state") original = state_file.read_text(encoding="utf-8") @@ -119,10 +147,8 @@ def main() -> int: "--agent-id", "codex-main-control", ) - assert blocked_claim["error"] == ( - f"todo claim requires status=open; todo_id '{blocked_todo['todo_id']}' " - "is status='blocked'" - ), blocked_claim + assert blocked_claim["error_code"] == "todo_not_open", blocked_claim + assert blocked_claim["todo_status"] == "blocked", blocked_claim assert claim_state_file.read_text(encoding="utf-8") == before_rejected_claim bare_user_error = run_cli_error( @@ -213,7 +239,8 @@ def main() -> int: assert "## User Todo / Owner Review Reading Queue" in after_user, after_user assert f"- [ ] {USER_TODO}" in after_user, after_user assert after_user.index("## User Todo / Owner Review Reading Queue") < after_user.index("## Next Action") - assert "updated_at: 2026-01-01T00:00:00+00:00" not in after_user + # Todo provider revisions leave the Goal narrative timestamp intact. + assert "updated_at: 2026-01-01T00:00:00+00:00" in after_user duplicate = run_cli( registry_path, @@ -251,8 +278,8 @@ def main() -> int: "codex-side-bypass", ) assert scoped_gate["added"] is True, scoped_gate - assert scoped_gate["agent_id"] == "codex-side-bypass", scoped_gate - assert scoped_gate["blocks_agent"] == "codex-side-bypass", scoped_gate + assert scoped_gate["todo"]["bound_agent"] == "codex-side-bypass", scoped_gate + assert scoped_gate["todo"]["blocks_agent"] == "codex-side-bypass", scoped_gate agent_payload = run_cli( registry_path, @@ -268,7 +295,7 @@ def main() -> int: AGENT_TODO_NOTE, ) assert agent_payload["added"] is True, agent_payload - assert agent_payload["note"] == AGENT_TODO_NOTE, agent_payload + assert agent_payload["todo"]["note"] == AGENT_TODO_NOTE, agent_payload added_fields = parse_active_state_todos(state_file.read_text(encoding="utf-8")) assert added_fields["agent_todos"]["items"][0]["note"] == AGENT_TODO_NOTE, added_fields legacy_agent = run_cli( @@ -284,7 +311,8 @@ def main() -> int: ) # Omit-note negative (GH-C97): an add without --note must not invent a note. assert legacy_agent["added"] is True, legacy_agent - assert not legacy_agent.get("note"), legacy_agent + assert not legacy_agent["todo"].get("note"), legacy_agent + before_legacy_claim = legacy_state_file.read_bytes() legacy_claim = run_cli_error( legacy_registry_path, "todo", @@ -296,15 +324,10 @@ def main() -> int: "--claimed-by", "codex-main-control", ) - assert "has no coordination.registered_agents" in legacy_claim["error"], legacy_claim - assert "Register this peer identity first" in legacy_claim["error"], legacy_claim - assert "--registered-agent codex-main-control" in legacy_claim["error"], legacy_claim - wrapped_agent_todo = "- [ ] Summarize the read-only evidence after the user\n checklist is done." - state_file.write_text( - state_file.read_text(encoding="utf-8").replace(f"- [ ] {AGENT_TODO}", wrapped_agent_todo), - encoding="utf-8", - ) - metadata_payload = run_cli( + assert legacy_claim["error_code"] == "actor_not_registered", legacy_claim + assert legacy_state_file.read_bytes() == before_legacy_claim + before_metadata = state_file.read_text(encoding="utf-8") + metadata_conflict = run_cli_error( registry_path, "todo", "add", @@ -331,18 +354,48 @@ def main() -> int: "--required-decision-scope", "direction:action:read_only_evidence_review", ) - assert metadata_payload["added"] is False, metadata_payload - assert metadata_payload["already_exists"] is True, metadata_payload - assert metadata_payload["metadata_updated"] is True, metadata_payload - assert metadata_payload["task_class"] == "advancement_task", metadata_payload - assert metadata_payload["action_kind"] == "run_eval", metadata_payload - assert metadata_payload["note"] == UPDATED_AGENT_TODO_NOTE, metadata_payload + assert metadata_conflict["error_code"] == "todo_semantic_duplicate_conflict", metadata_conflict + assert "use Todo update" in metadata_conflict["error"], metadata_conflict + assert state_file.read_text(encoding="utf-8") == before_metadata + metadata_payload = run_cli( + registry_path, + "todo", + "update", + "--goal-id", + GOAL_ID, + "--todo-id", + agent_payload["todo_id"], + "--agent-id", + "codex-main-control", + "--task-class", + "advancement_task", + "--action-kind", + "run_eval", + "--note", + UPDATED_AGENT_TODO_NOTE, + "--continuation-policy", + "independent_handoff", + "--required-capability", + "shell", + "--required-capability", + "benchmark_runner", + "--target-capability", + "benchmark_runner", + "--required-decision-scope", + "direction:action:read_only_evidence_review", + ) + assert metadata_payload["status"] == "applied", metadata_payload + metadata_todo = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", agent_payload["todo_id"])["todo"] + assert metadata_todo["task_class"] == "advancement_task", metadata_payload + assert metadata_todo["action_kind"] == "run_eval", metadata_payload + assert metadata_todo["note"] == UPDATED_AGENT_TODO_NOTE, metadata_payload assert ( - metadata_payload["continuation_policy"] == "independent_handoff" + metadata_todo["continuation_policy"] == "independent_handoff" ), metadata_payload - assert metadata_payload["required_capabilities"] == ["shell", "benchmark_runner"], metadata_payload - assert metadata_payload["target_capabilities"] == ["benchmark_runner"], metadata_payload - assert metadata_payload["required_decision_scopes"] == [ + assert metadata_todo["required_capabilities"] == ["shell", "benchmark_runner"], metadata_payload + assert metadata_todo["target_capabilities"] == ["benchmark_runner"], metadata_payload + assert metadata_todo["required_decision_scopes"] == [ { "schema_version": "decision_scope_v0", "kind": "direction", @@ -352,10 +405,7 @@ def main() -> int: ], metadata_payload after_metadata = state_file.read_text(encoding="utf-8") assert after_metadata.count("- [ ] Summarize the read-only evidence after the user") == 1, after_metadata - agent_block_start = after_metadata.index("- [ ] Summarize the read-only evidence after the user") - agent_metadata_start = after_metadata.index("\n" ) @@ -80,7 +82,7 @@ def user_role_state_text() -> str: ) -def write_fixture(root: Path, *, text: str | None = None) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, text: str | None = None, import_authority: bool = True) -> tuple[Path, Path, Path]: project = root / "project" runtime = root / "runtime" state_file = ".codex/goals/todo-archive-completed-goal/ACTIVE_GOAL_STATE.md" @@ -119,6 +121,8 @@ def write_fixture(root: Path, *, text: str | None = None) -> tuple[Path, Path, P + "\n", encoding="utf-8", ) + if import_authority: + import_fixture_authority(registry_path, GOAL_ID) return registry_path, runtime, state_path @@ -312,7 +316,7 @@ def main() -> int: assert updated.count("[P1] Completed implementation lane 1.") == 1 assert updated.count("[P1] Completed implementation lane 2.") == 1 assert "excluded_agents=codex-reviewer" in updated - assert "updated_at: 2026-01-01T00:00:00+00:00" not in updated + assert "updated_at: 2026-01-01T00:00:00+00:00" in updated status_after = run_cli(registry_path, runtime, "status") assert status_after["ok"] is True, status_after @@ -438,8 +442,9 @@ def main() -> int: assert lineage_status["ok"] is True, lineage_status assert lineage_status["contract_errors"] == [], lineage_status - state_path.write_text( - archived_lineage + before_authority = run_cli(registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID) + invalid_rows = ( + "" + "- [ ] [P1] Contradictory archived work item.\n" + " \n" @@ -448,11 +453,24 @@ def main() -> int: + "task_class=advancement_task excluded_agents=codex-reviewer -->\n" + "- [x] [P1] Empty-status archived work item.\n" + " \n", - encoding="utf-8", + + "task_class=advancement_task excluded_agents=codex-reviewer -->\n" ) + state_path.write_text(archived_lineage + invalid_rows, encoding="utf-8") invalid_status = run_cli(registry_path, runtime, "status", check=False) + assert invalid_status["ok"] is True, invalid_status + after_authority = run_cli(registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID) + assert after_authority["todos"] == before_authority["todos"] + assert after_authority["authority_read"]["provider_revision"] == before_authority["authority_read"]["provider_revision"] + # These diagnostics still belong to explicit legacy source inspection, + # before import. Display text outside canonical regions grants no work. + source_registry, source_runtime, source_state = write_fixture( + Path(tmp) / "invalid-source", text=state_text() + invalid_rows, + import_authority=False, + ) + before_source = source_state.read_bytes() + invalid_status = run_cli(source_registry, source_runtime, "status", check=False) assert invalid_status["ok"] is False, invalid_status + assert source_state.read_bytes() == before_source assert any( "todo_archived_open" in item and "executor exclusions" in item for item in invalid_status["contract_errors"] diff --git a/examples/control_plane/todo-concurrent-write-lock-smoke.py b/examples/control_plane/todo-concurrent-write-lock-smoke.py index ee88eb10eb..62938adca3 100644 --- a/examples/control_plane/todo-concurrent-write-lock-smoke.py +++ b/examples/control_plane/todo-concurrent-write-lock-smoke.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Smoke-test active-state todo writers under a contended file lock.""" +"""Smoke-test concurrent canonical writes while readable projection is locked.""" from __future__ import annotations @@ -16,14 +16,16 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.file_lock import exclusive_file_lock # noqa: E402 +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 from loopx.status import parse_active_state_todos # noqa: E402 GOAL_ID = "todo-concurrent-write-lock-goal" -CHILD_ADD_TODO = "Child add should preserve the parent todo written under lock." -PARENT_ADD_TODO = "Parent todo written while child add waits on the lock." +CHILD_ADD_TODO = "Child add should preserve the concurrently committed parent todo." +PARENT_ADD_TODO = "Parent todo committed while child add awaits readable projection." UPDATE_TARGET_TODO = "Update target todo should keep unrelated concurrent additions." -PARENT_UPDATE_TODO = "Parent todo written while child update waits on the lock." +PARENT_UPDATE_TODO = "Parent todo committed while child update awaits readable projection." def write_fixture(root: Path) -> tuple[Path, Path]: @@ -73,6 +75,7 @@ def write_fixture(root: Path) -> tuple[Path, Path]: + "\n", encoding="utf-8", ) + import_fixture_authority(registry_path, GOAL_ID) return registry_path, state_file @@ -118,14 +121,33 @@ def wait_json(process: subprocess.Popen[str]) -> dict[str, Any]: def assert_waiting_on_lock(process: subprocess.Popen[str], label: str) -> None: time.sleep(0.35) - assert process.poll() is None, ( - f"{label} process finished before the parent released the lock" + if process.poll() is not None: + stdout, stderr = process.communicate() + raise AssertionError(f"{label} finished before display delivery: {stdout} {stderr}") + + +def authority(registry_path: Path) -> dict[str, Any]: + registry = json.loads(registry_path.read_text(encoding="utf-8")) + result = read_canonical_todos_if_promoted( + runtime_root=Path(registry["common_runtime_root"]), goal_id=GOAL_ID, ) + assert result is not None and result["legacy_fallback_used"] is False, result + return result -def append_agent_todo(state_file: Path, text: str) -> None: - with state_file.open("a", encoding="utf-8") as file: - file.write(f"- [ ] {text}\n") +def wait_for_committed_texts( + registry_path: Path, texts: set[str], *, blocked_todo_id: str | None = None, +) -> None: + # The five-second display-lock deadline is unchanged. Authority commits + # must be observable independently while both CLI callers await projection. + deadline = time.monotonic() + 3 + while time.monotonic() < deadline: + rows = authority(registry_path)["todos"] + if texts <= {row["text"] for row in rows} and (blocked_todo_id is None or + any(row["todo_id"] == blocked_todo_id and row["status"] == "blocked" for row in rows)): + return + time.sleep(0.05) + raise AssertionError("canonical writes did not commit before display delivery") def agent_items(state_file: Path) -> list[dict[str, Any]]: @@ -141,24 +163,24 @@ def main() -> int: with tempfile.TemporaryDirectory(prefix="loopx-todo-lock-smoke-") as tmp: registry_path, state_file = write_fixture(Path(tmp)) + child_add = ("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--text", CHILD_ADD_TODO, "--operation-id", "child-add") with exclusive_file_lock(state_file): - add_process = start_cli( - registry_path, - "todo", - "add", - "--goal-id", - GOAL_ID, - "--role", - "agent", - "--text", - CHILD_ADD_TODO, - ) + add_process = start_cli(registry_path, *child_add) + parent_add = start_cli(registry_path, "todo", "add", "--goal-id", GOAL_ID, + "--role", "agent", "--text", PARENT_ADD_TODO, + "--operation-id", "parent-add") assert_waiting_on_lock(add_process, "todo add") - append_agent_todo(state_file, PARENT_ADD_TODO) - add_result = wait_json(add_process) - assert add_result["added"] is True, add_result - assert find_item(state_file, CHILD_ADD_TODO), state_file.read_text(encoding="utf-8") - assert find_item(state_file, PARENT_ADD_TODO), state_file.read_text(encoding="utf-8") + wait_for_committed_texts(registry_path, {CHILD_ADD_TODO, PARENT_ADD_TODO}) + assert wait_json(add_process)["added"] is True + assert wait_json(parent_add)["added"] is True + assert find_item(state_file, CHILD_ADD_TODO) + assert find_item(state_file, PARENT_ADD_TODO) + before_replay = authority(registry_path) + run_cli(registry_path, *child_add) + after_replay = authority(registry_path) + assert after_replay["provider_revision"] == before_replay["provider_revision"] + assert after_replay["todos"] == before_replay["todos"] target = run_cli( registry_path, @@ -173,34 +195,34 @@ def main() -> int: ) target_id = target["todo_id"] + child_update = ("todo", "update", "--goal-id", GOAL_ID, "--todo-id", target_id, + "--agent-id", "codex-side-bypass", "--status", "blocked", + "--reason", "waiting-on-concurrent-state", "--claimed-by", + "codex-side-bypass", "--update-operation-id", "child-update") with exclusive_file_lock(state_file): - update_process = start_cli( - registry_path, - "todo", - "update", - "--goal-id", - GOAL_ID, - "--todo-id", - target_id, - "--agent-id", - "codex-side-bypass", - "--status", - "blocked", - "--reason", - "waiting-on-concurrent-state", - "--claimed-by", - "codex-side-bypass", - ) + update_process = start_cli(registry_path, *child_update) + parent_update = start_cli(registry_path, "todo", "add", "--goal-id", GOAL_ID, + "--role", "agent", "--text", PARENT_UPDATE_TODO, + "--operation-id", "parent-during-update") assert_waiting_on_lock(update_process, "todo update") - append_agent_todo(state_file, PARENT_UPDATE_TODO) - update_result = wait_json(update_process) - assert update_result["changed"] is True, update_result + wait_for_committed_texts(registry_path, {PARENT_UPDATE_TODO}, blocked_todo_id=target_id) + assert next(row for row in authority(registry_path)["todos"] + if row["todo_id"] == target_id)["status"] == "blocked" + assert wait_json(update_process)["changed"] is True + assert wait_json(parent_update)["added"] is True updated_target = find_item(state_file, UPDATE_TARGET_TODO) assert updated_target["status"] == "blocked", updated_target assert updated_target["reason"] == "waiting-on-concurrent-state", updated_target assert updated_target["claimed_by"] == "codex-side-bypass", updated_target assert find_item(state_file, PARENT_UPDATE_TODO), state_file.read_text(encoding="utf-8") + before_replay = authority(registry_path) + run_cli(registry_path, *child_update) + after_replay = authority(registry_path) + assert after_replay["provider_revision"] == before_replay["provider_revision"] + assert after_replay["todos"] == before_replay["todos"] + assert len([row for row in after_replay["todos"] if row["text"] == PARENT_UPDATE_TODO]) == 1 + print("todo-concurrent-write-lock-smoke ok") return 0 diff --git a/examples/control_plane/todo-deferred-capacity-cli-smoke.py b/examples/control_plane/todo-deferred-capacity-cli-smoke.py index 5a54de1d64..874e929e17 100644 --- a/examples/control_plane/todo-deferred-capacity-cli-smoke.py +++ b/examples/control_plane/todo-deferred-capacity-cli-smoke.py @@ -87,7 +87,7 @@ def assert_deferred_add_and_capacity_resume() -> None: "--resume-when", CAPACITY_RESUME, ) - assert added["status"] == "deferred", added + assert added["todo"]["status"] == "deferred", added todo_id = added["todo_id"] item = next(item for item in parsed_items(state_file) if item["todo_id"] == todo_id) assert item["status"] == "deferred" and item["done"] is True, item @@ -187,7 +187,7 @@ def assert_deferred_add_and_capacity_resume() -> None: "capacity condition was satisfied", ) assert ( - reopened["status"] == "open" and reopened["resume_when"] is None + reopened["status"] == "applied" and reopened["changed"] is True ), reopened item = next(item for item in parsed_items(state_file) if item["todo_id"] == todo_id) assert item["status"] == "open" and item.get("resume_when") is None, item @@ -260,7 +260,7 @@ def assert_open_to_deferred_update() -> None: "--reason", "short worker capacity is not currently available", ) - assert updated["status"] == "deferred" and updated["status_changed"] is True, updated + assert updated["status"] == "applied" and updated["changed"] is True, updated item = next(item for item in parsed_items(state_file) if item["todo_id"] == todo_id) assert item["status"] == "deferred" and item["resume_when"] == CAPACITY_RESUME, item From d359b59810eba8394ffff933c4198ea4403fe5d1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:54:20 +0800 Subject: [PATCH 40/98] fix: admit readonly completion facts for imported metadata repair Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../coordination/todo_terminal_lifecycle.ts | 25 +++++++++- .../test_native_todo_planning_update.py | 50 +++++++++++++++++++ .../terminal_reentry_conformance.ts | 14 +++++- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index d6840ea74d..3ee8a057b6 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -60,6 +60,7 @@ import { selectTodoCompletionState, TODO_COMPLETION_STATE_REQUEST_SCHEMA, } from "../todos/completion_state.ts"; +import {resolveTodoCompletionPolicy} from "../todos/completion_policy.ts"; import { TASK_LEASE_SCHEMA_VERSION, leaseEpoch, @@ -1031,6 +1032,24 @@ interface UntypedCompletionRepairPlan { readonly successorTodoIds: readonly string[]; } +function completionPolicyFactsOnly( + todo: JsonObject, input: ResolvedCoordinationTodoTerminalLifecycleInput, +): boolean { + const request = input.completion_policy_request; + if (request === null) return true; + // The ordinary CLI always supplies this existing source-fact envelope. + // Decode through its owner, but never apply a policy during imported repair. + const policy = resolveTodoCompletionPolicy(request); + return request.goal_id === input.goal_id && request.next_agent_todo == null && + request.next_action_kind == null && request.next_continuation_policy == null && + request.evidence == null && !policy.self_merged && + policy.effective_next_claimed_by === null && policy.effective_next_excluded_agents.length === 0 && + Array.isArray(request.linked_successors) && request.linked_successors.length === 0 && + (policy.effective_claimed_by === null || policy.effective_claimed_by === todo.claimed_by) && + canonicalAuthoritySha256([...policy.registered_agents].sort()) === + canonicalAuthoritySha256([...input.registered_agents].sort()); +} + function planUntypedCompletionRepair( todo: JsonObject, input: ResolvedCoordinationTodoTerminalLifecycleInput, @@ -1057,9 +1076,9 @@ function planUntypedCompletionRepair( } if (input.user_update !== undefined || input.note !== null || input.evidence !== null || input.reason !== null || input.decision_outcome !== null || input.clear_claim || - input.allow_user_gate_auto_acquire || input.linked_successor_todo_ids.length > 0 || + input.linked_successor_todo_ids.length > 0 || input.successor_intents.length > 0 || input.completion_result != null || - input.completion_policy_request !== null || input.validation_declaration !== null || + !completionPolicyFactsOnly(todo, input) || input.validation_declaration !== null || input.validation_receipt !== null || input.validation_source_provider_revision != null || input.validation_declaration_sha256 != null || input.goal_acceptance_source_binding != null || input.goal_acceptance_validation_receipts != null) { @@ -1337,6 +1356,8 @@ export async function executeCoordinationTodoTerminalLifecycle( } if (untypedCompletionRepair !== null) { const target = untypedCompletionRepair.todo; + // A public auto-acquire option is not an acquisition: this branch commits + // only the existing Todo's completion metadata and never mutates leases. return commitTerminalResult(store, input, requestSha, head, { todo_id: input.todo_id, command: input.command, diff --git a/tests/control_plane/test_native_todo_planning_update.py b/tests/control_plane/test_native_todo_planning_update.py index b9857da4a5..a7ab8ab0fa 100644 --- a/tests/control_plane/test_native_todo_planning_update.py +++ b/tests/control_plane/test_native_todo_planning_update.py @@ -67,6 +67,56 @@ def records(registry: Path) -> dict[str, dict]: return {item["todo_id"]: item for item in list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"]} +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("claim_flag", [False, True]) +def test_cli_imported_completion_repairs_only_metadata(tmp_path, monkeypatch, provider, claim_flag): + from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted + from loopx.control_plane.testing.canary_harness import run_json_cli, run_json_cli_result + + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, state = source_fixture(tmp_path) + rows = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] + imported = next(row for row in rows if row["todo_id"] == "todo_target") + imported.update(status="done", done=True, evidence="Original evidence", reason="Original reason", + completed_at="2026-01-02T00:00:00Z", updated_at="2026-01-02T01:00:00Z", + last_actor_agent_id="agent-b", completion_receipt_id="tcw_original") + projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=rows, handoff_mode="soft_claim") + runtime = tmp_path / "runtime" + initialize_canonical_authority(runtime, "goal-a", projection, state_path=state, provider=provider) + before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + source_before = state.read_bytes() + prefix = ("todo", "complete", "--goal-id", "goal-a", "--todo-id", "todo_target") + for actor, extra, code in [ + ("agent-b", (), "claim_owner_mismatch"), + ("agent-a", ("--note", "A fresh annotation"), "invalid_todo_completion_transaction"), + ("agent-a", ("--evidence", "A fresh completion result"), "invalid_todo_completion_transaction"), + ("agent-a", ("--next-agent-todo", "Fresh successor work"), "invalid_todo_completion_transaction"), + ]: + returncode, rejected = run_json_cli_result(*prefix, "--agent-id", actor, *extra, registry_path=registry) + assert returncode == 1 and rejected["error_code"] == code, rejected + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == before + assert state.read_bytes() == source_before + repair = (*prefix, "--agent-id", "agent-a", *(("--claimed-by", "agent-a") if claim_flag else ())) + preview = run_json_cli(*repair, "--dry-run", registry_path=registry) + assert preview["dry_run"] is True, preview + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == before + assert state.read_bytes() == source_before + applied = run_json_cli(*repair, registry_path=registry) + assert applied["changed"] is True and applied["completion_policy"] is None, applied + original = next(row for row in before["todos"] if row["todo_id"] == "todo_target") + current = records(registry)["todo_target"] + assert {k: v for k, v in current.items() if k != "content_revision"} == { + **{k: v for k, v in original.items() if k != "content_revision"}, "completion_continuation": "active_goal", + } + run_json_cli("todo", "add", "--goal-id", "goal-a", "--role", "agent", + "--text", "A later independent operation", registry_path=registry) + later = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + replay = run_json_cli(*repair, registry_path=registry) + assert replay["changed"] is False, replay + after = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + assert after["todos"] == later["todos"] and after["provider_revision"] == later["provider_revision"] + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("command", ["complete", "supersede"]) def test_cli_same_title_terminal_successor_preserves_peers_and_replay(tmp_path, monkeypatch, provider, command): diff --git a/tests/control_plane_ts/terminal_reentry_conformance.ts b/tests/control_plane_ts/terminal_reentry_conformance.ts index 3b598dbe5e..4bfac3a6ea 100644 --- a/tests/control_plane_ts/terminal_reentry_conformance.ts +++ b/tests/control_plane_ts/terminal_reentry_conformance.ts @@ -35,11 +35,15 @@ export function registerTerminalReentryConformance(provider: string, factory: Au operation_identity: {kind: "completion_turn"}, actor_agent_id: "agent-a", registered_agents: ["agent-a", "agent-b"], lifecycle_grants: [], authority_reason: null, decision_outcome: null, lease_idempotency_key: null, lease_expected_version: null, - allow_user_gate_auto_acquire: false, requested_no_followup: false, + allow_user_gate_auto_acquire: true, requested_no_followup: false, requested_completion_turn_key: "original-turn", requested_completion_identity_source: "turn_settlement", linked_successor_todo_ids: [], successor_intents: [], note: null, evidence: null, reason: null, clear_claim: false, validation_declaration: null, validation_receipt: null, - completion_policy_request: null, dry_run: false, now: new Date("2026-09-07T07:00:00Z"), + completion_policy_request: {schema_version: "loopx_todo_completion_policy_request_v0", + goal_id: "goal-a", agent_model: "peer_v1", claimed_by: "agent-a", + registered_agents: ["agent-a", "agent-b"], next_claimed_by: null, next_agent_todo: null, + next_continuation_policy: null, next_excluded_agents: [], self_merged: false, + evidence: null, linked_successors: []}, dry_run: false, now: new Date("2026-09-07T07:00:00Z"), }; const before = await store.loadAuthority(), importedReceipt = await store.readReceipt("import-untyped"); const rejectedChanges = [{requested_no_followup: true}, {actor_agent_id: "agent-b"}, @@ -47,6 +51,11 @@ export function registerTerminalReentryConformance(provider: string, factory: Au {note: "Replace original note"}, {evidence: "Replace original evidence"}, {reason: "Replace original reason"}, {clear_claim: true}, {decision_outcome: "approve"}, {completion_policy_request: {claimed_by: "agent-a"}}, + {completion_policy_request: {...request.completion_policy_request, claimed_by: "agent-b"}}, + {completion_policy_request: {...request.completion_policy_request, goal_id: "other-goal"}}, + {completion_policy_request: {...request.completion_policy_request, registered_agents: ["agent-a"]}}, + {completion_policy_request: {...request.completion_policy_request, evidence: "Fresh policy evidence"}}, + {completion_policy_request: {...request.completion_policy_request, next_agent_todo: "Fresh policy successor"}}, {successor_intents: [{role: "agent", text: "Cannot add work during repair"}]}] satisfies readonly Partial[]; for (const change of rejectedChanges) { @@ -67,6 +76,7 @@ export function registerTerminalReentryConformance(provider: string, factory: Au assert.equal(before.status, "loaded"); if (before.status !== "loaded") throw new Error("imported head missing"); const repairedRows = repaired.head.todos, importedRows = before.head.todos; + assert.deepEqual(repaired.head.leases, before.head.leases); if (!Array.isArray(repairedRows) || repairedRows.length !== 1 || !Array.isArray(importedRows) || importedRows.length !== 1) { throw new Error("imported repair Todo missing"); From 422b0173a7b5c494b0d2dcc1f0994978bf2306f2 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:54:20 +0800 Subject: [PATCH 41/98] docs: explain original-identity completion metadata repair Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../canonical-todo-completion-update.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/reference/canonical-todo-completion-update.md b/docs/reference/canonical-todo-completion-update.md index 8adff406fd..d3aaa1a021 100644 --- a/docs/reference/canonical-todo-completion-update.md +++ b/docs/reference/canonical-todo-completion-update.md @@ -530,6 +530,34 @@ Markdown 投影、备份读取及活跃 Host IO 保留。真实 File/SQLite CLI wheel 路径验证完成与结算恢复;Markdown writer 和 shadow capture producer 已 退役。代码回滚不需要转换数据或重写回执。 +### Repairing imported completion metadata / 修复导入的完成元数据 + +An imported completed Todo may lack `completion_continuation`. Use its original +Actor and completion identity to preview and apply the ordinary completion +command without new note, evidence, successor, policy or validation intent. +For a record without a stored Turn identity: + +```sh +loopx todo complete --goal-id example --todo-id todo_imported --agent-id agent-a --dry-run +loopx todo complete --goal-id example --todo-id todo_imported --agent-id agent-a +loopx todo list --goal-id example --todo-id todo_imported +``` + +A stored identity requires the original Turn tuple and any required execution +proof; do not replace it with a new identity. The TypeScript owner accepts the +ordinary transport's read-only policy facts, but applies no policy and acquires +no lease during this repair. It preserves identity, completion time, content, +claim and original receipt. Preview/refusal has no effects; retrying the same +operation preserves newer writes. Fresh edits require their own authorized +operation after repair. + +导入的已完成 Todo 若缺少 `completion_continuation`,使用原 Actor、完成身份和所需 +执行证明,通过普通完成命令预览并修复;不夹带新的 note、evidence、successor、 +policy 或验证意图。上例适用于未存储 Turn 身份的记录;已有身份必须提供原 tuple, +不能替换身份。TS owner 接受普通入口携带的只读 policy 事实,但修复不应用 policy、 +不领取租约,并保留原 ID、完成时间、内容、claim 与回执。预览和拒绝无副作用; +原操作重试保留后写。后续编辑仍须单独通过原授权入口。 + ## 2.0 canonical mutation boundary Create, update, complete, supersede and archive share From 9e6d860399a327e8bc0f02d13194b9d7b1862153 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:22:13 +0800 Subject: [PATCH 42/98] test: migrate user gate and monitor smokes to canonical authority Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../monitor-poll-writeback-smoke.py | 21 + .../todo-user-gate-scope-smoke.py | 395 +++++++++--------- 2 files changed, 207 insertions(+), 209 deletions(-) diff --git a/examples/control_plane/monitor-poll-writeback-smoke.py b/examples/control_plane/monitor-poll-writeback-smoke.py index a3ebdb1b14..f3134cb430 100644 --- a/examples/control_plane/monitor-poll-writeback-smoke.py +++ b/examples/control_plane/monitor-poll-writeback-smoke.py @@ -29,6 +29,7 @@ write_monitor_poll_todo_state, ) from loopx.control_plane.testing.canary_harness import ( # noqa: E402 + import_fixture_authority, read_run_index, run_json_cli, run_json_cli_result, @@ -238,6 +239,7 @@ def write_fixture( registered_agents=[AGENT_ID], quota_allowed_slots=None, ) + import_fixture_authority(registry_path, GOAL_ID) return registry_path, state_file @@ -1167,6 +1169,8 @@ def assert_writeback_helper_preview_contract() -> None: assert resolved["todo_id"] == TODO_ID, resolved assert resolved["target_key"] == TARGET_KEY, resolved generated_at = "2026-01-01T00:05:00+00:00" + before = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_ID) + source = _state_file.read_bytes() preview = write_monitor_poll_todo_state( registry_path=registry_path, runtime_root=runtime_root_from_registry(registry_path), @@ -1175,6 +1179,7 @@ def assert_writeback_helper_preview_contract() -> None: execute=False, todo_id=TODO_ID, result_hash="old", + agent_id=AGENT_ID, ) assert preview["schema_version"] == "monitor_poll_todo_writeback_v0", preview assert preview["dry_run"] is True, preview @@ -1187,6 +1192,22 @@ def assert_writeback_helper_preview_contract() -> None: assert preview["last_checked_at"] == generated_at, preview assert preview["next_due_at"] != "2026-01-01T00:00:00+00:00", preview assert preview["cadence"] == "15m", preview + for actor in (None, "codex-unregistered"): + try: + write_monitor_poll_todo_state( + registry_path=registry_path, + runtime_root=runtime_root_from_registry(registry_path), + goal_id=GOAL_ID, generated_at=generated_at, + execute=False, todo_id=TODO_ID, result_hash="old", agent_id=actor, + ) + except ValueError as exc: + assert "registered actor" in str(exc), str(exc) + else: + raise AssertionError("Monitor preview admitted an undeclared actor") + after = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_ID) + assert after["todo"] == before["todo"], after + assert after["authority_read"]["provider_revision"] == before["authority_read"]["provider_revision"], after + assert _state_file.read_bytes() == source def assert_cli_monitor_poll_uses_should_run_lookback() -> None: diff --git a/examples/control_plane/todo-user-gate-scope-smoke.py b/examples/control_plane/todo-user-gate-scope-smoke.py index c3e022c8b0..f2cf7ff69b 100644 --- a/examples/control_plane/todo-user-gate-scope-smoke.py +++ b/examples/control_plane/todo-user-gate-scope-smoke.py @@ -15,10 +15,13 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.contract import check_contract # noqa: E402 +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 from loopx.control_plane.todos.todo_summary import active_state_todo_attention_item # noqa: E402 from loopx.quota import build_quota_should_run # noqa: E402 from loopx.status import collect_status, parse_active_state_todos # noqa: E402 -from loopx.todos import add_goal_todo, complete_goal_todo, update_goal_todo # noqa: E402 +from loopx.todos import ( # noqa: E402 + add_goal_todo, complete_goal_todo, list_goal_todos, update_goal_todo, +) GOAL_ID = "user-gate-scope-smoke" @@ -26,7 +29,7 @@ SIDE_AGENT = "codex-product-capability" -def write_fixture(root: Path) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, import_authority: bool = True) -> tuple[Path, Path, Path]: repo = root / "repo" repo.mkdir() state = repo / "ACTIVE_GOAL_STATE.md" @@ -38,6 +41,8 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: "updated_at: 2026-06-26T00:00:00+00:00", "---", "", + "## User Todo", + "", "## Agent Todo", "", ] @@ -68,6 +73,8 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: ), encoding="utf-8", ) + if import_authority: + import_fixture_authority(registry, GOAL_ID) return repo, state, registry @@ -164,6 +171,128 @@ def assert_user_action_does_not_drive_active_state_attention() -> None: assert gate_item["waiting_on"] == "controller", gate_item + +def read_todo(registry: Path, todo_id: str) -> dict: + view = list_goal_todos(registry_path=registry, goal_id=GOAL_ID, todo_id=todo_id) + assert view["authority_read"]["source_authority"] == "file_v0", view + assert view["authority_read"]["legacy_fallback_used"] is False, view + return view["todo"] + + +def assert_legacy_scope_diagnostics() -> None: + # Historical source validation remains readable before import. It cannot + # serve as a normal writer or alter an already canonical authority. + with tempfile.TemporaryDirectory(prefix="loopx-legacy-gate-diagnostics-") as tmp: + root = Path(tmp) + repo, state, registry = write_fixture(root, import_authority=False) + insert_before_agent_section( + state, + "- [ ] Approve an intentionally unscoped gate.\n" + " \n" + "- [ ] Track an unbound non-blocking user action.\n" + " \n" + "- [ ] Resolve a mismatched gate binding.\n" + f" \n" + "- [ ] Track an intentionally untyped user todo.\n" + " \n" + "- [ ] Approve a gate with an unregistered blocked agent.\n" + " \n" + "- [ ] Approve a gate with conflicting scope.\n" + f" ", + ) + state.write_text( + state.read_text(encoding="utf-8") + + "\n- [ ] Repair a removed continuation value.\n" + + " \n" + + "- [ ] Repair removed agent gate routing.\n" + + f" \n" + + "- [ ] Repair an unknown executor exclusion.\n" + + " \n" + + "- [ ] Repair malformed executor exclusion metadata.\n" + + " \n", + encoding="utf-8", + ) + original = state.read_bytes() + check = check_contract( + registry_path=registry, runtime_root_override=str(root / "runtime"), + scan_roots=[repo], limit=1, + ) + assert check["ok"] is False, check + for todo_id, diagnostic in ( + ("todo_unscoped_gate", "requires bound_agent"), + ("todo_unbound_action", "requires bound_agent"), + ("todo_mismatched_gate_binding", "must bind to blocks_agent"), + ("todo_untyped_user", "requires task_class"), + ("todo_bad_agent", "not registered"), + ("todo_both_scopes", "cannot set both"), + ("todo_removed_continuation", "removed continuation_policy"), + ("todo_removed_agent_gate", "removed blocks_agent routing"), + ("todo_unknown_exclusion", "unregistered agents"), + ("todo_malformed_exclusion", "malformed excluded_agents"), + ): + assert any(todo_id in error and diagnostic in error for error in check["errors"]), check + assert state.read_bytes() == original + + +def assert_imported_scope_repairs() -> None: + with tempfile.TemporaryDirectory(prefix="loopx-imported-gate-repair-") as tmp: + root = Path(tmp) + repo, state, registry = write_fixture(root, import_authority=False) + insert_before_agent_section( + state, + "- [ ] Approve an intentionally unscoped gate.\n" + " \n" + "- [ ] Track an unbound non-blocking user action.\n" + " \n" + "- [ ] Resolve a mismatched gate binding.\n" + f" \n" + "- [ ] Track an intentionally untyped user todo.\n" + " ", + ) + state.write_text( + state.read_text(encoding="utf-8") + + "\n- [ ] Repair removed agent gate routing.\n" + + f" \n", + encoding="utf-8", + ) + import_fixture_authority(registry, GOAL_ID) + for todo_id, role, changes, expected in ( + ("todo_unscoped_gate", "user", {"status": "done"}, {"status": "done"}), + ("todo_unbound_action", "user", {"bound_agent": SIDE_AGENT}, {"bound_agent": SIDE_AGENT}), + ("todo_mismatched_gate_binding", "user", {"bound_agent": SIDE_AGENT}, {"bound_agent": SIDE_AGENT}), + # The caller now declares the missing type/binding explicitly; + # closing old source data does not infer an approval or audience. + ("todo_untyped_user", "user", {"status": "done", "task_class": "user_action", "goal_bound": True}, + {"status": "done", "task_class": "user_action", "goal_bound": True}), + ("todo_removed_agent_gate", "agent", {"clear_blocks_agent": True}, {"blocks_agent": None}), + ): + original = read_todo(registry, todo_id) + source = state.read_bytes() + preview = update_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo_id, + role=role, agent_id=PRIMARY_AGENT, dry_run=True, **changes, + ) + assert preview["changed"] is True, preview + assert read_todo(registry, todo_id) == original + assert state.read_bytes() == source + changed = update_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo_id, + role=role, agent_id=PRIMARY_AGENT, **changes, + ) + assert changed["changed"] is True, changed + current = read_todo(registry, todo_id) + assert current["text"] == original["text"], current + for key, value in expected.items(): + assert current.get(key) == value, current + repaired_check = check_contract( + registry_path=registry, runtime_root_override=str(root / "runtime"), + scan_roots=[repo], limit=1, + ) + assert repaired_check["ok"] is True, repaired_check + + def main() -> int: assert_user_action_does_not_drive_active_state_attention() @@ -214,9 +343,10 @@ def main() -> int: task_class="user_action", agent_id=SIDE_AGENT, ) + user_action = {**user_action, **read_todo(registry, user_action["todo_id"])} assert user_action["task_class"] == "user_action", user_action assert user_action["bound_agent"] == SIDE_AGENT, user_action - assert user_action["claimed_by"] is None, user_action + assert user_action.get("claimed_by") is None, user_action goal_bound_action = add_goal_todo( registry_path=registry, @@ -227,7 +357,9 @@ def main() -> int: goal_bound=True, dry_run=True, ) - assert goal_bound_action["goal_bound"] is True, goal_bound_action + assert goal_bound_action["status"] == "planned", goal_bound_action + assert goal_bound_action["changed"] is True, goal_bound_action + assert len(list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"]) == 1 other_lane_only_status = collect_status( registry_path=registry, @@ -281,6 +413,7 @@ def main() -> int: task_class="advancement_task", claimed_by=SIDE_AGENT, ) + side_agent_todo = {**side_agent_todo, **read_todo(registry, side_agent_todo["todo_id"])} assert side_agent_todo["claimed_by"] == SIDE_AGENT, side_agent_todo blocked_agent_todo = add_goal_todo( @@ -301,6 +434,9 @@ def main() -> int: bound_agent=SIDE_AGENT, unblocks_todo_id=blocked_agent_todo["todo_id"], ) + approval_before = read_todo(registry, approval["todo_id"]) + target_before = read_todo(registry, blocked_agent_todo["todo_id"]) + source_before = state.read_bytes() assert_raises_message( lambda: complete_goal_todo( registry_path=registry, @@ -311,8 +447,11 @@ def main() -> int: evidence="wrong-lane response attempt", dry_run=True, ), - "response continuation is bound", + "bound_agent_mismatch", ) + assert read_todo(registry, approval["todo_id"]) == approval_before + assert read_todo(registry, blocked_agent_todo["todo_id"]) == target_before + assert state.read_bytes() == source_before approval_completed = complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -441,6 +580,7 @@ def main() -> int: task_class="user_gate", agent_id=SIDE_AGENT, ) + scoped = {**scoped, **read_todo(registry, scoped["todo_id"])} assert scoped["blocks_agent"] == SIDE_AGENT, scoped assert scoped["bound_agent"] == SIDE_AGENT, scoped assert f"blocks_agent={SIDE_AGENT}" in state.read_text(encoding="utf-8") @@ -454,6 +594,7 @@ def main() -> int: task_class="user_gate", global_gate=True, ) + global_gate = {**global_gate, **read_todo(registry, global_gate["todo_id"])} assert global_gate["global_gate"] is True, global_gate assert global_gate["goal_bound"] is True, global_gate assert "global_gate=true" in state.read_text(encoding="utf-8") @@ -467,22 +608,6 @@ def main() -> int: task_class="advancement_task", claimed_by=PRIMARY_AGENT, ) - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(agent_todo["todo_id"]), - role="agent", - claimed_by=PRIMARY_AGENT, - agent_id=PRIMARY_AGENT, - evidence="fixture validation", - next_user_todo="Approve publishing the release notes.", - next_user_task_class="user_gate", - ) - state_text = state.read_text(encoding="utf-8") - assert "Approve publishing the release notes." in state_text, state_text - assert f"blocks_agent={PRIMARY_AGENT}" in state_text, state_text - assert f"bound_agent={PRIMARY_AGENT}" in state_text, state_text - assert_raises_message( lambda: update_goal_todo( registry_path=registry, @@ -496,210 +621,62 @@ def main() -> int: "global_gate is only valid for user_gate todos", ) - clean_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, - ) - assert clean_check["ok"] is True, clean_check - - insert_before_agent_section( - state, - "- [ ] Approve an intentionally unscoped gate.\n" - " ", - ) - bad_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, - ) - assert bad_check["ok"] is False, bad_check - assert any("todo_unscoped_gate" in item for item in bad_check["errors"]), bad_check - - closed = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id="todo_unscoped_gate", - role="user", - agent_id=PRIMARY_AGENT, - status="done", - ) - assert closed["changed"] is True, closed - repaired_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, - ) - assert repaired_check["ok"] is True, repaired_check - - insert_before_agent_section( - state, - "- [ ] Track an unbound non-blocking user action.\n" - " ", - ) - unbound_action_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, - ) - assert unbound_action_check["ok"] is False, unbound_action_check - assert any( - "todo_unbound_action" in item and "requires bound_agent" in item - for item in unbound_action_check["errors"] - ), unbound_action_check - rebound_action = update_goal_todo( + complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, - todo_id="todo_unbound_action", - role="user", + todo_id=str(agent_todo["todo_id"]), + role="agent", + claimed_by=PRIMARY_AGENT, agent_id=PRIMARY_AGENT, - bound_agent=SIDE_AGENT, - ) - assert rebound_action["bound_agent"] == SIDE_AGENT, rebound_action - rebound_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, + evidence="fixture validation", + next_user_todo="Approve publishing the release notes.", + next_user_task_class="user_gate", ) - assert rebound_check["ok"] is True, rebound_check + state_text = state.read_text(encoding="utf-8") + assert "Approve publishing the release notes." in state_text, state_text + assert f"blocks_agent={PRIMARY_AGENT}" in state_text, state_text + assert f"bound_agent={PRIMARY_AGENT}" in state_text, state_text - insert_before_agent_section( - state, - "- [ ] Resolve a mismatched gate binding.\n" - f" ", - ) - mismatched_binding_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, - ) - assert mismatched_binding_check["ok"] is False, mismatched_binding_check - assert any( - "todo_mismatched_gate_binding" in item - and "must bind to blocks_agent" in item - for item in mismatched_binding_check["errors"] - ), mismatched_binding_check - repaired_gate_binding = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id="todo_mismatched_gate_binding", - role="user", - agent_id=PRIMARY_AGENT, - bound_agent=SIDE_AGENT, + goal_bound_action = add_goal_todo( + registry_path=registry, goal_id=GOAL_ID, role="user", + text="Track a goal-wide owner note.", task_class="user_action", + goal_bound=True, ) - assert repaired_gate_binding["bound_agent"] == SIDE_AGENT, repaired_gate_binding - repaired_binding_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, + goal_bound_readback = read_todo(registry, goal_bound_action["todo_id"]) + assert goal_bound_readback["goal_bound"] is True, goal_bound_readback + assert goal_bound_readback.get("bound_agent") is None, goal_bound_readback + assert goal_bound_readback.get("global_gate") is not True, goal_bound_readback + complete_goal_todo( + registry_path=registry, goal_id=GOAL_ID, + todo_id=goal_bound_action["todo_id"], role="user", agent_id=PRIMARY_AGENT, + evidence="fixture owner note acknowledged", ) - assert repaired_binding_check["ok"] is True, repaired_binding_check + assert read_todo(registry, goal_bound_action["todo_id"])["status"] == "done" - insert_before_agent_section( - state, - "- [ ] Track an intentionally untyped user todo.\n" - " ", - ) - untyped_check = check_contract( + clean_check = check_contract( registry_path=registry, runtime_root_override=str(root / "runtime"), scan_roots=[repo], limit=1, ) - assert untyped_check["ok"] is False, untyped_check - assert any("todo_untyped_user" in item and "requires task_class" in item for item in untyped_check["errors"]), untyped_check - - closed_untyped = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id="todo_untyped_user", - role="user", - agent_id=PRIMARY_AGENT, - status="done", - ) - assert closed_untyped["changed"] is True, closed_untyped + assert clean_check["ok"] is True, clean_check + original = state.read_bytes() + before = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) insert_before_agent_section( state, - "- [ ] Approve a gate with an unregistered blocked agent.\n" - " \n" - "- [ ] Approve a gate with conflicting scope.\n" - f" ", - ) - invalid_scope_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, - ) - assert invalid_scope_check["ok"] is False, invalid_scope_check - assert any("todo_bad_agent" in item and "not registered" in item for item in invalid_scope_check["errors"]), invalid_scope_check - assert any("todo_both_scopes" in item and "cannot set both" in item for item in invalid_scope_check["errors"]), invalid_scope_check - - state.write_text( - state.read_text(encoding="utf-8") - + "\n- [ ] Repair a removed continuation value.\n" - + " \n" - + "- [ ] Repair removed agent gate routing.\n" - + f" \n" - + "- [ ] Repair an unknown executor exclusion.\n" - + " \n" - + "- [ ] Repair malformed executor exclusion metadata.\n" - + " \n", - encoding="utf-8", - ) - hard_cut_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, - ) - assert hard_cut_check["ok"] is False, hard_cut_check - assert any( - "todo_removed_continuation" in item and "removed continuation_policy" in item - for item in hard_cut_check["errors"] - ), hard_cut_check - assert any( - "todo_removed_agent_gate" in item and "removed blocks_agent routing" in item - for item in hard_cut_check["errors"] - ), hard_cut_check - assert any( - "todo_unknown_exclusion" in item and "unregistered agents" in item - for item in hard_cut_check["errors"] - ), hard_cut_check - assert any( - "todo_malformed_exclusion" in item and "malformed excluded_agents" in item - for item in hard_cut_check["errors"] - ), hard_cut_check - - repaired_agent_gate = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id="todo_removed_agent_gate", - role="agent", - agent_id=PRIMARY_AGENT, - clear_blocks_agent=True, - ) - assert repaired_agent_gate["blocks_agent"] is None, repaired_agent_gate - repaired_hard_cut_check = check_contract( - registry_path=registry, - runtime_root_override=str(root / "runtime"), - scan_roots=[repo], - limit=1, - ) - assert not any( - "todo_removed_agent_gate" in item - for item in repaired_hard_cut_check["errors"] - ), repaired_hard_cut_check + "- [ ] An injected projection cannot add a gate.\n" + f" ", + ) + after = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) + assert after["todos"] == before["todos"], after + assert after["authority_read"]["provider_revision"] == before["authority_read"]["provider_revision"], after + assert state.read_bytes() != original + state.write_bytes(original) + + assert_legacy_scope_diagnostics() + assert_imported_scope_repairs() print("todo-user-gate-scope-smoke ok") return 0 From 58cf90fd2c7382698973220d9835436a947c9e9f Mon Sep 17 00:00:00 2001 From: Duang777 Date: Wed, 14 Oct 2026 19:24:03 +0800 Subject: [PATCH 43/98] fix(authority): admit neutral imported repair envelopes Signed-off-by: Duang777 --- .../coordination/todo_terminal_lifecycle.ts | 71 +++++++- .../test_imported_completion_repair.py | 171 ++++++++++++++++++ .../terminal_reentry_conformance.ts | 27 ++- 3 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 tests/control_plane/test_imported_completion_repair.py diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index d6840ea74d..d95283c812 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -55,6 +55,7 @@ import { reduceTodoCompletionTransaction, TODO_COMPLETION_TRANSACTION_REQUEST_SCHEMA, } from "../todos/completion_transaction.ts"; +import {resolveTodoCompletionPolicy} from "../todos/completion_policy.ts"; import {evaluateTodoCompletionFence, TODO_COMPLETION_FENCE_REQUEST_SCHEMA} from "../todos/completion_fence.ts"; import { selectTodoCompletionState, @@ -1031,9 +1032,54 @@ interface UntypedCompletionRepairPlan { readonly successorTodoIds: readonly string[]; } +function validateUntypedCompletionRepairPolicy( + request: JsonObject, + input: ResolvedCoordinationTodoTerminalLifecycleInput, +): void { + const allowedFields = new Set([ + "schema_version", + "goal_id", + "agent_model", + "claimed_by", + "registered_agents", + "next_claimed_by", + "next_agent_todo", + "next_continuation_policy", + "next_excluded_agents", + "self_merged", + "evidence", + "linked_successors", + ]); + if (Object.keys(request).some((field) => !allowedFields.has(field))) { + throw new AuthorityStoreProtocolError( + "imported completion repair must not carry completion policy extensions", + ); + } + const policy = resolveTodoCompletionPolicy(request); + if (request.goal_id !== input.goal_id || + canonicalAuthoritySha256(policy.registered_agents) !== + canonicalAuthoritySha256(input.registered_agents) || + policy.effective_claimed_by !== null || + policy.effective_next_claimed_by !== null || + policy.effective_next_excluded_agents.length > 0 || + policy.self_merged || + policy.linked_successor_id !== null || + (request.next_agent_todo != null && request.next_agent_todo !== "") || + (request.next_continuation_policy != null && + request.next_continuation_policy !== "") || + (Array.isArray(request.linked_successors) && + request.linked_successors.length > 0) || + (request.evidence != null && request.evidence !== "")) { + throw new AuthorityStoreProtocolError( + "imported completion repair must not carry completion policy edits", + ); + } +} + function planUntypedCompletionRepair( todo: JsonObject, input: ResolvedCoordinationTodoTerminalLifecycleInput, + authority: CoordinationTodoTerminalDecisionResult, ): UntypedCompletionRepairPlan | null { if (input.command !== "complete" || input.requested_no_followup || todo.status !== "done") return null; const fence = evaluateTodoCompletionFence({ @@ -1055,18 +1101,35 @@ function planUntypedCompletionRepair( "imported completion repair must preserve the stored completion_turn_key", ); } + if (authority.outcome !== "no_change") { + throw new AuthorityStoreProtocolError( + "imported completion repair cannot apply a fresh authority transition", + ); + } if (input.user_update !== undefined || input.note !== null || input.evidence !== null || input.reason !== null || input.decision_outcome !== null || input.clear_claim || - input.allow_user_gate_auto_acquire || input.linked_successor_todo_ids.length > 0 || + input.linked_successor_todo_ids.length > 0 || input.successor_intents.length > 0 || input.completion_result != null || - input.completion_policy_request !== null || input.validation_declaration !== null || + input.validation_declaration !== null || input.validation_receipt !== null || input.validation_source_provider_revision != null || - input.validation_declaration_sha256 != null || input.goal_acceptance_source_binding != null || + input.goal_acceptance_source_binding != null || input.goal_acceptance_validation_receipts != null) { throw new AuthorityStoreProtocolError( "imported completion repair must not carry fresh completion content, policy, validation, or ownership edits", ); } + if (input.completion_policy_request !== null) { + validateUntypedCompletionRepairPolicy( + input.completion_policy_request, + input, + ); + } + if (input.validation_declaration_sha256 != null && + input.validation_declaration_sha256 !== todo.completion_validation_sha256) { + throw new AuthorityStoreProtocolError( + "imported completion repair must preserve the stored validation commitment", + ); + } const successorTodoIds = Array.isArray(todo.successor_todo_ids) ? todo.successor_todo_ids.map((value, index) => requireAuthorityStoreId(value, `todo.successor_todo_ids[${index}]`)) @@ -1326,7 +1389,7 @@ export async function executeCoordinationTodoTerminalLifecycle( } let untypedCompletionRepair: UntypedCompletionRepairPlan | null; try { - untypedCompletionRepair = planUntypedCompletionRepair(todo, input); + untypedCompletionRepair = planUntypedCompletionRepair(todo, input, authority); } catch (error) { return terminalFailure( "invalid_todo_completion_transaction", diff --git a/tests/control_plane/test_imported_completion_repair.py b/tests/control_plane/test_imported_completion_repair.py new file mode 100644 index 0000000000..95abd9ab93 --- /dev/null +++ b/tests/control_plane/test_imported_completion_repair.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest +from canonical_authority_fixture import initialize_canonical_authority + +from loopx.control_plane.coordination.local_authority import ( + read_canonical_todos_if_promoted, +) +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from loopx.control_plane.todos import provider_terminal_lifecycle +from loopx.todos import complete_goal_todo + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize( + "validation_digest", + [None, "a" * 64], + ids=["plain", "validated"], +) +def test_public_completion_repairs_imported_metadata_once( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + provider: str, + validation_digest: str | None, +) -> None: + runtime_root = tmp_path / "runtime" + project = tmp_path / "project" + project.mkdir() + state_file = project / "ACTIVE_GOAL_STATE.md" + state_file.write_text( + "# Goal\n\n## User Todo\n\n## Agent Todo\n\n## Completed Work Archive\n", + encoding="utf-8", + ) + registry = tmp_path / "registry.json" + registry.write_text( + json.dumps( + { + "schema_version": 1, + "common_runtime_root": str(runtime_root), + "goals": [ + { + "id": "goal-a", + "status": "active", + "repo": str(project), + "state_file": state_file.name, + "coordination": {"registered_agents": ["agent-a"]}, + } + ], + } + ), + encoding="utf-8", + ) + todo = { + "schema_version": "todo_item_v0", + "todo_id": "todo-imported", + "role": "agent", + "source_section": "Agent Todo", + "status": "done", + "done": True, + "archive_state": "active", + "text": "Preserve the imported completion", + "task_class": "advancement_task", + "claimed_by": "agent-a", + "note": "Original note", + "evidence": "Original evidence", + "reason": "Original reason", + "completion_turn_key": "original-turn", + "completion_receipt_id": "tcw_original", + "completed_at": "2026-09-01T00:00:00Z", + "updated_at": "2026-09-01T01:00:00Z", + "last_actor_agent_id": "agent-before", + **( + { + "completion_validation_required": True, + "completion_validation_sha256": validation_digest, + } + if validation_digest is not None + else {} + ), + } + initialize_canonical_authority( + runtime_root, + "goal-a", + build_todo_runtime_shadow_projection( + goal_id="goal-a", + todos=[todo], + handoff_mode="soft_claim", + ), + state_path=state_file, + provider=provider, + ) + + terminal_requests: list[dict[str, Any]] = [] + execute = provider_terminal_lifecycle.effect_runtime_result + + def capture_request( + method: str, + params: dict[str, Any], + **kwargs: Any, + ) -> Any: + if method == "coordination.local_authority.todo_terminal": + terminal_requests.append(dict(params)) + return execute(method, params, **kwargs) + + monkeypatch.setattr( + provider_terminal_lifecycle, + "effect_runtime_result", + capture_request, + ) + request = { + "registry_path": registry, + "goal_id": "goal-a", + "todo_id": "todo-imported", + "agent_id": "agent-a", + "completion_turn_key": "original-turn", + "completion_identity_source": "turn_settlement", + } + before = read_canonical_todos_if_promoted( + runtime_root=runtime_root, + goal_id="goal-a", + ) + assert before is not None + + preview = complete_goal_todo(**request, dry_run=True) + assert preview["provider_status"] == "planned" + assert preview["changed"] is True + assert read_canonical_todos_if_promoted( + runtime_root=runtime_root, + goal_id="goal-a", + ) == before + + repaired = complete_goal_todo(**request) + assert repaired["provider_status"] == "applied" + assert repaired["completion_continuation"] == "active_goal" + after = read_canonical_todos_if_promoted( + runtime_root=runtime_root, + goal_id="goal-a", + ) + assert after is not None + repaired_todo = after["todos"][0] + original_todo = before["todos"][0] + assert repaired_todo == { + **original_todo, + "completion_continuation": "active_goal", + } + + producer_request = terminal_requests[0] + assert producer_request["allow_user_gate_auto_acquire"] is True + assert producer_request["completion_policy_request"] is not None + assert producer_request["validation_declaration_sha256"] == validation_digest + + replay = complete_goal_todo(**request) + assert replay["provider_status"] == "replayed" + assert read_canonical_todos_if_promoted( + runtime_root=runtime_root, + goal_id="goal-a", + ) == after + + if validation_digest is not None: + return + + closed = complete_goal_todo(**request, no_followup=True) + assert closed["provider_status"] == "applied" + assert closed["completion_continuation"] == "no_followup" + assert closed["completion_recovery"] == "same_turn_terminal_closeout" diff --git a/tests/control_plane_ts/terminal_reentry_conformance.ts b/tests/control_plane_ts/terminal_reentry_conformance.ts index 3b598dbe5e..70dd5a273e 100644 --- a/tests/control_plane_ts/terminal_reentry_conformance.ts +++ b/tests/control_plane_ts/terminal_reentry_conformance.ts @@ -30,23 +30,32 @@ export function registerTerminalReentryConformance(provider: string, factory: Au await store.commitAuthority({operation_id: "import-untyped", expected_provider_revision: null, next_projection: authorityProjectionFixture("goal-a", [todo], [], schema, {handoff_mode: "soft_claim"}), events: [], receipts: [{original_import: true}]}); + const producerPolicy = { + schema_version: "loopx_todo_completion_policy_request_v0", + goal_id: "goal-a", agent_model: "peer_v1", claimed_by: null, + registered_agents: ["agent-a", "agent-b"], next_claimed_by: null, + next_agent_todo: null, next_continuation_policy: null, + next_excluded_agents: [], self_merged: false, evidence: null, + linked_successors: [], + }; const request: CoordinationTodoTerminalLifecycleInput = { goal_id: "goal-a", todo_id: "todo_untyped", expected_role: "agent", command: "complete", operation_identity: {kind: "completion_turn"}, actor_agent_id: "agent-a", registered_agents: ["agent-a", "agent-b"], lifecycle_grants: [], authority_reason: null, decision_outcome: null, lease_idempotency_key: null, lease_expected_version: null, - allow_user_gate_auto_acquire: false, requested_no_followup: false, + allow_user_gate_auto_acquire: true, requested_no_followup: false, requested_completion_turn_key: "original-turn", requested_completion_identity_source: "turn_settlement", linked_successor_todo_ids: [], successor_intents: [], note: null, evidence: null, reason: null, clear_claim: false, validation_declaration: null, validation_receipt: null, - completion_policy_request: null, dry_run: false, now: new Date("2026-09-07T07:00:00Z"), + completion_policy_request: producerPolicy, + dry_run: false, now: new Date("2026-09-07T07:00:00Z"), }; const before = await store.loadAuthority(), importedReceipt = await store.readReceipt("import-untyped"); const rejectedChanges = [{requested_no_followup: true}, {actor_agent_id: "agent-b"}, {requested_completion_turn_key: "other-turn"}, {note: "Replace original note"}, {evidence: "Replace original evidence"}, {reason: "Replace original reason"}, {clear_claim: true}, {decision_outcome: "approve"}, - {completion_policy_request: {claimed_by: "agent-a"}}, + {completion_policy_request: {...producerPolicy, claimed_by: "agent-a"}}, {successor_intents: [{role: "agent", text: "Cannot add work during repair"}]}] satisfies readonly Partial[]; for (const change of rejectedChanges) { @@ -114,11 +123,19 @@ export function registerTerminalReentryConformance(provider: string, factory: Au goal_id: "goal-a", todo_id: "todo_untyped_validated", expected_role: "agent", command: "complete", operation_identity: {kind: "completion_turn"}, actor_agent_id: "agent-a", registered_agents: ["agent-a"], lifecycle_grants: [], authority_reason: null, decision_outcome: null, - lease_idempotency_key: null, lease_expected_version: null, allow_user_gate_auto_acquire: false, + lease_idempotency_key: null, lease_expected_version: null, allow_user_gate_auto_acquire: true, requested_no_followup: false, requested_completion_turn_key: "validated-turn", requested_completion_identity_source: "turn_settlement", linked_successor_todo_ids: [], successor_intents: [], note: null, evidence: null, reason: null, clear_claim: false, - validation_declaration: null, validation_receipt: null, completion_policy_request: null, + validation_declaration: null, validation_declaration_sha256: canonicalAuthoritySha256(declaration), + validation_receipt: null, completion_policy_request: { + schema_version: "loopx_todo_completion_policy_request_v0", + goal_id: "goal-a", agent_model: "peer_v1", claimed_by: null, + registered_agents: ["agent-a"], next_claimed_by: null, + next_agent_todo: null, next_continuation_policy: null, + next_excluded_agents: [], self_merged: false, evidence: null, + linked_successors: [], + }, dry_run: false, now: new Date("2026-09-07T07:00:00Z"), }; const repaired = await execute(store, request); From 5ec722d03c7266787f6c4b480434c97cd7b2a020 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Wed, 14 Oct 2026 19:24:08 +0800 Subject: [PATCH 44/98] fix(release): qualify prerelease candidate builds Signed-off-by: Duang777 --- docs/book/chapters/00-reading-guide.md | 2 +- docs/book/en/chapters/00-reading-guide.md | 4 +- docs/book/en/index.md | 2 +- docs/book/index.md | 2 +- scripts/chat_bundle.py | 16 ++++++- tests/presentation/test_chat_bundle.py | 52 +++++++++++++++++++++++ 6 files changed, 72 insertions(+), 6 deletions(-) diff --git a/docs/book/chapters/00-reading-guide.md b/docs/book/chapters/00-reading-guide.md index 96a65b06ed..3136f7cb60 100644 --- a/docs/book/chapters/00-reading-guide.md +++ b/docs/book/chapters/00-reading-guide.md @@ -180,7 +180,7 @@ lifecycle 选择,不是所有贡献的默认终点。 ## 版本基线 -当前内容面向 LoopX release `v1.4.0`;本地命令示例已按 `loopx 1.4.0` 的 +当前内容面向 LoopX release `v2.0.0a1`;本地命令示例已按 `loopx 2.0.0a1` 的 公开 CLI 与协议表面复核。该版本要求 Python 3.11+ 与 Node.js 22.22.3+。LoopX 会自动管理 空闲后退出的 TypeScript Effect runtime,用户不需要手工维护 daemon。 diff --git a/docs/book/en/chapters/00-reading-guide.md b/docs/book/en/chapters/00-reading-guide.md index 4c37a259a3..48616f2b33 100644 --- a/docs/book/en/chapters/00-reading-guide.md +++ b/docs/book/en/chapters/00-reading-guide.md @@ -199,8 +199,8 @@ Do not bypass a newer permission or lifecycle check just to make an older exampl ## Version baseline -This book targets LoopX release `v1.4.0`. Local command examples were checked against -the public `loopx 1.4.0` CLI and protocol surface. It requires Python 3.11+ and Node.js 22.22.3+. +This book targets LoopX release `v2.0.0a1`. Local command examples were checked against +the public `loopx 2.0.0a1` CLI and protocol surface. It requires Python 3.11+ and Node.js 22.22.3+. LoopX starts and reuses its managed, idle-exiting TypeScript Effect runtime automatically; users do not operate that runtime as a manual daemon. diff --git a/docs/book/en/index.md b/docs/book/en/index.md index b63b7a8a47..82c59e4846 100644 --- a/docs/book/en/index.md +++ b/docs/book/en/index.md @@ -79,7 +79,7 @@ versioned or optional functionality, not the default shape of every contribution - Source format: Markdown - Site generator: MkDocs Material - Hosting: GitHub Pages -- LoopX release anchor: `v1.4.0` +- LoopX release anchor: `v2.0.0a1` - Runtime prerequisites: Python 3.11+ and Node.js 22.22.3+ The official public protocols remain authoritative for protocol facts. Commands that change across diff --git a/docs/book/index.md b/docs/book/index.md index bb358d3d96..abd9ec2fa7 100644 --- a/docs/book/index.md +++ b/docs/book/index.md @@ -70,7 +70,7 @@ Extension 只是可独立版本化和交付的一种路径。 - 正文格式:Markdown; - 站点生成器:MkDocs Material; - 在线发布:GitHub Pages; -- LoopX 发布锚点:`v1.4.0`; +- LoopX 发布锚点:`v2.0.0a1`; - 运行时前提:Python 3.11+ 与 Node.js 22.22.3+。 协议解释以 LoopX 官方公开合同为事实源。易变化的命令仍以对应发布物、官方文档和当前 diff --git a/scripts/chat_bundle.py b/scripts/chat_bundle.py index 1ca7895f53..33ad0b8afa 100644 --- a/scripts/chat_bundle.py +++ b/scripts/chat_bundle.py @@ -239,7 +239,21 @@ def release_build(tag: str, repository: str) -> None: ] def number(value: str) -> tuple[int, ...]: - return tuple(map(int, value.removeprefix("v").split("."))) + match = re.fullmatch( + r"v(\d+)\.(\d+)\.(\d+)(?:(a|b|rc)(\d+))?", + value, + ) + if match is None: + raise ValueError(f"invalid LoopX release tag: {value}") + major, minor, patch, prerelease, prerelease_number = match.groups() + prerelease_rank = {None: 3, "a": 0, "b": 1, "rc": 2}[prerelease] + return ( + int(major), + int(minor), + int(patch), + prerelease_rank, + int(prerelease_number or 0), + ) predecessors = [r for r in versions if number(r["tag_name"]) < number(tag)] if not predecessors: diff --git a/tests/presentation/test_chat_bundle.py b/tests/presentation/test_chat_bundle.py index de8f4cfc09..07f83fec3b 100644 --- a/tests/presentation/test_chat_bundle.py +++ b/tests/presentation/test_chat_bundle.py @@ -299,6 +299,58 @@ def download(command, **kwargs): assert downloads == ["v1.0.5"] +def test_release_predecessor_supports_prerelease_target(monkeypatch): + releases = [[ + { + "tag_name": "v1.4.0", + "draft": False, + "prerelease": False, + "assets": [{"name": "loopx-1.4.0-py3-none-any.whl"}], + }, + { + "tag_name": "v2.0.0", + "draft": False, + "prerelease": False, + "assets": [{"name": "loopx-2.0.0-py3-none-any.whl"}], + }, + ]] + monkeypatch.setattr( + builder.subprocess, + "check_output", + lambda *args, **kwargs: json.dumps(releases), + ) + downloads = [] + + def download(command, **kwargs): + downloads.append(command[3]) + directory = Path(command[command.index("--dir") + 1]) + wheel = directory / "loopx-1.4.0-py3-none-any.whl" + with zipfile.ZipFile(wheel, "w") as archive: + archive.writestr( + "loopx/web/chat/index.html", + '', + ) + archive.writestr("loopx/web/chat/assets/old.js", "old code") + (directory / "SHA256SUMS").write_text( + f"{builder.contract.digest(wheel)} {wheel.name}\n" + ) + + monkeypatch.setattr(builder.subprocess, "run", download) + builds = [] + monkeypatch.setattr( + builder, + "build", + lambda previous, **kwargs: builds.append( + (previous / "assets/old.js").read_text() + ), + ) + + builder.release_build("v2.0.0a1", "fixture/repository") + + assert downloads == ["v1.4.0"] + assert builds == ["old code"] + + def test_windows_drive_paths_are_not_portable_bundle_names(): assert not builder.contract.safe_relative("C:/escape.js") assert not builder.contract.safe_relative("C:escape.js") From 3fa2fd2325ca821a611edd3825bf98286bf604e8 Mon Sep 17 00:00:00 2001 From: Duang777 Date: Wed, 14 Oct 2026 19:24:14 +0800 Subject: [PATCH 45/98] test(semantics): register direction authority consumer Signed-off-by: Duang777 --- tests/control_plane_ts/content_digest_single_owner.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/control_plane_ts/content_digest_single_owner.test.ts b/tests/control_plane_ts/content_digest_single_owner.test.ts index 8d049af41f..c05ec973e3 100644 --- a/tests/control_plane_ts/content_digest_single_owner.test.ts +++ b/tests/control_plane_ts/content_digest_single_owner.test.ts @@ -123,6 +123,7 @@ const CANONICAL_CONSUMERS = [ "control_plane/effect_runtime_snapshot.ts", "control_plane/goals/acceptance_authority.ts", "control_plane/goals/acceptance_contract.ts", + "control_plane/goals/direction_authority.ts", "control_plane/goals/goal_amendment_authority.ts", "control_plane/goals/goal_amendment_proposal.ts", "control_plane/goals/operator_actions.ts", From 6a600eef979167e733de764e3affabf7a8514d4b Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:56:53 +0800 Subject: [PATCH 46/98] docs: clarify authorized imported completion repair claims Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/canonical-todo-completion-update.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/reference/canonical-todo-completion-update.md b/docs/reference/canonical-todo-completion-update.md index d3aaa1a021..334a5b985d 100644 --- a/docs/reference/canonical-todo-completion-update.md +++ b/docs/reference/canonical-todo-completion-update.md @@ -532,8 +532,8 @@ wheel 路径验证完成与结算恢复;Markdown writer 和 shadow capture pro ### Repairing imported completion metadata / 修复导入的完成元数据 -An imported completed Todo may lack `completion_continuation`. Use its original -Actor and completion identity to preview and apply the ordinary completion +An imported completed Todo may lack `completion_continuation`. Use an authorized +Actor and its original completion identity to preview and apply the ordinary completion command without new note, evidence, successor, policy or validation intent. For a record without a stored Turn identity: @@ -545,16 +545,18 @@ loopx todo list --goal-id example --todo-id todo_imported A stored identity requires the original Turn tuple and any required execution proof; do not replace it with a new identity. The TypeScript owner accepts the -ordinary transport's read-only policy facts, but applies no policy and acquires -no lease during this repair. It preserves identity, completion time, content, +ordinary transport's read-only policy facts, including an explicit `--claimed-by` +that matches the stored claim, but applies no policy and acquires no lease during +this repair. A different claim is rejected. It preserves identity, completion time, content, claim and original receipt. Preview/refusal has no effects; retrying the same operation preserves newer writes. Fresh edits require their own authorized operation after repair. -导入的已完成 Todo 若缺少 `completion_continuation`,使用原 Actor、完成身份和所需 +导入的已完成 Todo 若缺少 `completion_continuation`,使用已授权 Actor、原完成身份和所需 执行证明,通过普通完成命令预览并修复;不夹带新的 note、evidence、successor、 policy 或验证意图。上例适用于未存储 Turn 身份的记录;已有身份必须提供原 tuple, -不能替换身份。TS owner 接受普通入口携带的只读 policy 事实,但修复不应用 policy、 +不能替换身份。TS owner 接受普通入口携带的只读 policy 事实,包括与已存 claim 一致的 +显式 `--claimed-by`;不同 claim 会被拒绝。修复不应用 policy、 不领取租约,并保留原 ID、完成时间、内容、claim 与回执。预览和拒绝无副作用; 原操作重试保留后写。后续编辑仍须单独通过原授权入口。 From 89966f7ebd0d4dda930f11e59e75f29cc4cc9856 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:20:31 +0800 Subject: [PATCH 47/98] test: migrate status and quota smokes to canonical authority Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../quota-no-git-status-smoke.py | 15 +++++++- .../status-projection-cache-smoke.py | 7 +++- .../status-quota-perf-budget-smoke.py | 38 ++++++++++++++++--- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/examples/control_plane/quota-no-git-status-smoke.py b/examples/control_plane/quota-no-git-status-smoke.py index 6cf51ef55d..ab47e98154 100644 --- a/examples/control_plane/quota-no-git-status-smoke.py +++ b/examples/control_plane/quota-no-git-status-smoke.py @@ -12,6 +12,11 @@ REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 + GOAL_ID = "no-git-quota-fixture" AGENT_ID = "codex-benchmark-agent" @@ -30,7 +35,8 @@ def write_fixture(root: Path) -> Path: "## Objective\n\n" "Solve this no-git fixture.\n\n" "## Next Action\n\n" - "- Pick the seeded todo.\n", + "- Pick the seeded todo.\n\n" + "## Agent Todo\n\n", encoding="utf-8", ) registry_path.parent.mkdir(parents=True) @@ -60,6 +66,7 @@ def write_fixture(root: Path) -> Path: + "\n", encoding="utf-8", ) + import_fixture_authority(registry_path, GOAL_ID) return registry_path @@ -100,6 +107,7 @@ def main() -> int: "Solve the no-git quota fixture.", ) assert todo["ok"] is True, todo + before = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID) no_git_bin = root / "no-git-bin" no_git_bin.mkdir() @@ -119,6 +127,11 @@ def main() -> int: assert quota.get("status") != "quota_collection_failed", quota assert quota["should_run"] is True, quota assert quota["recommended_action"] == "Solve the no-git quota fixture.", quota + assert quota["selected_todo"]["todo_id"] == todo["todo_id"], quota + after = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID, env=env) + assert after["todos"] == before["todos"], after + assert after["authority_read"]["provider_revision"] == before["authority_read"]["provider_revision"], after + assert after["authority_read"]["legacy_fallback_used"] is False, after return 0 diff --git a/examples/control_plane/status-projection-cache-smoke.py b/examples/control_plane/status-projection-cache-smoke.py index 1b81901b4e..dc3eedc8ff 100644 --- a/examples/control_plane/status-projection-cache-smoke.py +++ b/examples/control_plane/status-projection-cache-smoke.py @@ -17,6 +17,7 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.control_plane.runtime.status_projection_cache import write_status_projection_cache # noqa: E402 +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 GOAL_ID = "status-projection-cache-fixture" AGENT_ID = "codex-main-control" @@ -41,7 +42,8 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: "## Objective\n\n" "Validate opt-in status projection cache behavior.\n\n" "## Next Action\n\n" - "- Exercise the status/quota projection cache.\n", + "- Exercise the status/quota projection cache.\n\n" + "## Agent Todo\n\n", encoding="utf-8", ) public_doc.write_text("Public smoke fixture for status projection cache.\n", encoding="utf-8") @@ -81,6 +83,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: + "\n", encoding="utf-8", ) + import_fixture_authority(registry_path, GOAL_ID) return registry_path, runtime, public_doc @@ -258,6 +261,8 @@ def main() -> int: "agent", "--todo-id", initial_todo_id, + "--agent-id", + AGENT_ID, "--claimed-by", AGENT_ID, "--evidence", diff --git a/examples/control_plane/status-quota-perf-budget-smoke.py b/examples/control_plane/status-quota-perf-budget-smoke.py index 0e07ba7eb8..e5d7c7af2e 100644 --- a/examples/control_plane/status-quota-perf-budget-smoke.py +++ b/examples/control_plane/status-quota-perf-budget-smoke.py @@ -14,6 +14,11 @@ REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 + GOAL_ID = "status-quota-perf-budget-fixture" AGENT_ID = "codex-main-control" IGNORED_FILE_COUNT = 1_200 @@ -22,7 +27,7 @@ PRIVATE_DOC_MARKER = "https://" + "la" + "rk" + "office.example/doc" -def write_fixture(root: Path) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, import_authority: bool = True) -> tuple[Path, Path, Path]: project = root / "project" runtime = root / "runtime" state_rel = f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" @@ -40,7 +45,12 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: "## Objective\n\n" "Keep status and quota hot paths fast without skipping public scans.\n\n" "## Next Action\n\n" - "- Validate the selected hot-path budget todo.\n", + "- Validate the selected hot-path budget todo.\n\n" + "## Agent Todo\n\n" + + ("" if import_authority else + "- [ ] Inspect the historical status budget fixture.\n" + f" \n"), encoding="utf-8", ) public_doc.write_text("Public smoke fixture for status/quota perf budgets.\n", encoding="utf-8") @@ -90,6 +100,8 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: + "\n", encoding="utf-8", ) + if import_authority: + import_fixture_authority(registry_path, GOAL_ID) return registry_path, project, public_doc @@ -230,10 +242,23 @@ def main() -> int: ) state_text = state_file.read_text(encoding="utf-8") assert " status=open " in state_text, state_text - state_file.write_text( - state_text.replace(" status=open ", " status=stuck ", 1), - encoding="utf-8", - ) + before, _ = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID) + tampered = state_text.replace(" status=open ", " status=stuck ", 1) + state_file.write_text(tampered, encoding="utf-8") + after, _ = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID) + assert after["todos"] == before["todos"], after + assert after["authority_read"]["provider_revision"] == before["authority_read"]["provider_revision"], after + assert after["authority_read"]["legacy_fallback_used"] is False, after + assert state_file.read_text(encoding="utf-8") == tampered + + # Malformed historical source still has a read-only diagnostic/budget + # contract. It cannot act as the authority of the imported fixture. + registry_path, project, public_doc = write_fixture(root / "legacy-inspection", import_authority=False) + state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" + state_text = state_file.read_text(encoding="utf-8") + assert " status=open " in state_text, state_text + state_file.write_text(state_text.replace(" status=open ", " status=stuck ", 1), encoding="utf-8") + legacy_source = state_file.read_bytes() ( malformed_status_payload, @@ -308,6 +333,7 @@ def main() -> int: QUOTA_BUDGET_SECONDS, "malformed_state", ) + assert state_file.read_bytes() == legacy_source print( "status_quota_perf_budget: " From 0cadb66e9e6eeff2f0e6bf56617b1e620e9c4a23 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:04:22 +0800 Subject: [PATCH 48/98] test: migrate peer workspace and lease lifecycle smokes to canonical Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...-agent-continuation-state-machine-smoke.py | 9 +- .../peer-agent-workspace-guard-smoke.py | 68 ++++++--- .../control_plane/task-lease-runtime-smoke.py | 142 +++++++++++------- 3 files changed, 138 insertions(+), 81 deletions(-) diff --git a/examples/control_plane/peer-agent-continuation-state-machine-smoke.py b/examples/control_plane/peer-agent-continuation-state-machine-smoke.py index 034f1f0b36..db5d0ab413 100644 --- a/examples/control_plane/peer-agent-continuation-state-machine-smoke.py +++ b/examples/control_plane/peer-agent-continuation-state-machine-smoke.py @@ -13,6 +13,7 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.control_plane.testing.canary_harness import ( # noqa: E402 + import_fixture_authority, run_json_cli, write_fixture_registry, ) @@ -80,6 +81,7 @@ def write_fixture(root: Path, *, include_review: bool = False) -> tuple[Path, Pa quota_allowed_slots=5, peer_independent_worktree_required=False, ) + import_fixture_authority(registry_path, GOAL_ID) return project, runtime, registry_path @@ -93,6 +95,7 @@ def should_run(project: Path, runtime: Path, registry_path: Path, agent_id: str) agent_id, "--scan-path", str(project), + "--host-surface", "generic_cli", "--scheduler-owner", "none", "--execution-mode", "interactive", registry_path=registry_path, runtime=runtime, cwd=project, @@ -137,7 +140,7 @@ def assert_same_peer_continuation() -> None: runtime=runtime, cwd=project, ) - assert completed["self_merged"] is True, completed + assert completed["completion_policy"]["self_merged"] is True, completed successor = completed["next_todos"][0] assert successor["claimed_by"] == PEER_ALPHA, successor second = should_run(project, runtime, registry_path, PEER_ALPHA) @@ -168,8 +171,8 @@ def assert_review_is_action_semantics_over_generic_handoff() -> None: runtime=runtime, cwd=project, ) - assert completed["self_merged"] is False, completed - assert completed["linked_successor_id"] == REVIEW_TODO_ID, completed + assert completed["completion_policy"]["self_merged"] is False, completed + assert completed["completion_policy"]["linked_successor_id"] == REVIEW_TODO_ID, completed beta = should_run(project, runtime, registry_path, PEER_BETA) assert beta["decision"] == "run", beta diff --git a/examples/control_plane/peer-agent-workspace-guard-smoke.py b/examples/control_plane/peer-agent-workspace-guard-smoke.py index 4488459a4f..765108a3fd 100644 --- a/examples/control_plane/peer-agent-workspace-guard-smoke.py +++ b/examples/control_plane/peer-agent-workspace-guard-smoke.py @@ -12,6 +12,10 @@ REPO_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO_ROOT)) + +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 + GOAL_ID = "workspace-guard-canary" PEER_ALPHA = "codex-alpha" PEER_BETA = "codex-beta" @@ -47,11 +51,12 @@ def run_cli(cwd: Path, *args: str, registry_path: Path, runtime: Path) -> dict: *args, ], cwd=cwd, - check=True, + check=False, capture_output=True, text=True, env=env, ) + assert result.returncode == 0, result.stdout + result.stderr return json.loads(result.stdout) @@ -155,6 +160,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path, Path, Path, Path]: + "\n", encoding="utf-8", ) + import_fixture_authority(registry_path, GOAL_ID) return project, independent, task_project, task_independent, runtime, registry_path @@ -176,7 +182,13 @@ def set_task_repository(project: Path, runtime: Path, registry_path: Path) -> No registry_path=registry_path, runtime=runtime, ) - assert updated["task_repository"] == TASK_REPOSITORY, updated + assert updated["ok"] is True, updated + current = run_cli( + project, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", "todo_workspace_guard", registry_path=registry_path, runtime=runtime, + ) + assert current["todo"]["task_repository"] == TASK_REPOSITORY, current + assert current["authority_read"]["legacy_fallback_used"] is False, current def should_run( @@ -195,6 +207,7 @@ def should_run( PEER_ALPHA, "--scan-path", str(project), + "--host-surface", "generic_cli", "--scheduler-owner", "none", "--execution-mode", "interactive", registry_path=registry_path, runtime=runtime, ) @@ -271,6 +284,18 @@ def main() -> None: assert goal_worktree_guarded["workspace_guard"]["task_repository"] == ( TASK_REPOSITORY ), goal_worktree_guarded + state_path = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" + original_display = state_path.read_text(encoding="utf-8") + before = run_cli(project, "todo", "list", "--goal-id", GOAL_ID, + registry_path=registry_path, runtime=runtime) + state_path.write_text(original_display.replace(TASK_REPOSITORY, PROJECT_REPOSITORY), encoding="utf-8") + spoofed = should_run(independent, project, runtime, registry_path) + assert spoofed["workspace_guard"]["task_repository"] == TASK_REPOSITORY, spoofed + after = run_cli(project, "todo", "list", "--goal-id", GOAL_ID, + registry_path=registry_path, runtime=runtime) + assert after["authority_read"]["provider_revision"] == before["authority_read"]["provider_revision"] + assert after["todos"] == before["todos"] + state_path.write_text(original_display, encoding="utf-8") registry = json.loads(registry_path.read_text(encoding="utf-8")) registry["goals"][0].pop("workspace_guard_policy", None) @@ -278,25 +303,24 @@ def main() -> None: json.dumps(registry, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) - state_path = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" - state_path.write_text( - "---\nstatus: active\nowner_mode: goal\n" - 'objective: "Exercise selected-todo workspace routing."\n' - "updated_at: 2026-01-01T00:00:00+00:00\n---\n\n" - "# Selected Todo Workspace Guard Canary\n\n## Agent Todo\n\n" - "- [ ] [P0] Monitor one due repository lifecycle without repository writes.\n" - f" \n" - "- [ ] [P1] Repair a separate repository-writing control-plane task.\n" - f" \n", - encoding="utf-8", + monitor = run_cli( + project, "todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--operation-id", "due-monitor", + "--text", "[P0] Monitor one due repository lifecycle without repository writes.", + "--task-class", "continuous_monitor", "--action-kind", "monitor_repository_lifecycle", + "--task-repository", PROJECT_REPOSITORY, "--claimed-by", PEER_ALPHA, + "--target-key", "repository-lifecycle", "--cadence", "30m", + "--next-due-at", "2020-01-01T00:00:00+00:00", "--expires-at", "2099-01-01T00:00:00+00:00", + registry_path=registry_path, runtime=runtime, + ) + run_cli( + project, "todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--operation-id", "unrelated-repair", + "--text", "[P1] Repair a separate repository-writing control-plane task.", + "--task-class", "advancement_task", "--action-kind", "repair_workspace_route", + "--task-repository", TASK_REPOSITORY, "--required-write-scope", "loopx/**", + "--claimed-by", PEER_ALPHA, + registry_path=registry_path, runtime=runtime, ) monitor_selected = should_run(project, project, runtime, registry_path) @@ -304,7 +328,7 @@ def main() -> None: assert "workspace_guard" not in monitor_selected, monitor_selected assert "boundary_projection_gap" not in monitor_selected, monitor_selected assert monitor_selected["selected_todo"]["todo_id"] == ( - "todo_due_monitor" + monitor["todo_id"] ), monitor_selected assert monitor_selected["selected_todo"]["task_repository"] == ( PROJECT_REPOSITORY diff --git a/examples/control_plane/task-lease-runtime-smoke.py b/examples/control_plane/task-lease-runtime-smoke.py index 1b0ac95ac7..d15d2aac86 100644 --- a/examples/control_plane/task-lease-runtime-smoke.py +++ b/examples/control_plane/task-lease-runtime-smoke.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -"""Smoke-test the task_lease_v0 runtime and CLI contract.""" +"""Exercise canonical lease, claim handover and completion through the real CLI.""" from __future__ import annotations import json -import re +import os import subprocess import sys import tempfile @@ -14,6 +14,10 @@ REPO_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(REPO_ROOT)) +sys.path.insert(0, str(REPO_ROOT / "tests" / "control_plane")) + +from canonical_authority_fixture import initialize_canonical_authority # noqa: E402 +from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection # noqa: E402 GOAL_ID = "task-lease-runtime-goal" TODO_A = "todo_taskleasea" @@ -21,7 +25,7 @@ TODO_C = "todo_taskleasec" -def write_fixture(root: Path) -> tuple[Path, Path]: +def write_fixture(root: Path, provider: str) -> tuple[Path, Path]: project = root / "project" runtime = root / "runtime" registry_path = project / ".loopx" / "registry.json" @@ -32,14 +36,7 @@ def write_fixture(root: Path) -> tuple[Path, Path]: "status: active\n" "updated_at: 2026-01-01T00:00:00+00:00\n" "---\n\n" - "# Active Goal State\n\n" - "## Agent Todo\n\n" - f"- [ ] First independently claimable todo.\n" - f" \n" - f"- [ ] Second independently claimable todo.\n" - f" \n" - f"- [ ] Conflicting write-scope todo.\n" - f" \n", + "# Active Goal State\n\n## Agent Todo\n\n", encoding="utf-8", ) registry_path.parent.mkdir(parents=True) @@ -76,28 +73,43 @@ def write_fixture(root: Path) -> tuple[Path, Path]: + "\n", encoding="utf-8", ) + worker = project.parent / "worker" + for args in [ + ("init", "--initial-branch", "main"), + ("remote", "add", "origin", "https://example.invalid/loopx/lease-fixture.git"), + ("-c", "user.name=LoopX Fixture", "-c", "user.email=fixture@example.invalid", + "commit", "--allow-empty", "-m", "fixture"), + ("worktree", "add", "-b", "worker", str(worker)), + ]: + subprocess.run(["git", "-C", str(project), *args], check=True, capture_output=True, text=True) + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, handoff_mode="hard_lease", leases=[], todos=[ + { + "schema_version": "todo_item_v0", "todo_id": todo_id, "role": "agent", + "status": "open", "done": False, "text": text, "archive_state": "active", + "source_section": "Agent Todo", "index": index, "task_class": "advancement_task", + "action_kind": "validate", "task_domain": "validation", "claimed_by": owner, + "task_repository": "git:example.invalid/loopx/lease-fixture", + "required_write_scopes": [scope], + } + for index, (todo_id, text, owner, scope) in enumerate([ + (TODO_A, "First independently claimable todo.", "codex-main-control", "loopx/**"), + (TODO_B, "Second independently claimable todo.", None, "docs/**"), + (TODO_C, "Conflicting write-scope todo.", None, "loopx/cli_commands/todo.py"), + ], 1) + ], + ) + initialize_canonical_authority(runtime, GOAL_ID, projection, state_path=state_file, provider=provider) return registry_path, state_file -def set_claimed_by(state_file: Path, *, todo_id: str, owner: str | None) -> None: - lines = state_file.read_text(encoding="utf-8").splitlines() - for index, line in enumerate(lines): - if f"todo_id={todo_id}" not in line: - continue - line = re.sub(r"\s+claimed_by=[^\s<>]+", "", line) - if owner: - line = line.replace(" -->", f" claimed_by={owner} -->") - lines[index] = line - state_file.write_text("\n".join(lines) + "\n", encoding="utf-8") - return - raise AssertionError(f"todo metadata not found: {todo_id}") - - def command( registry_path: Path, *args: str, check: bool = True, ) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + env["PYTHONPATH"] = str(REPO_ROOT) return subprocess.run( [ sys.executable, @@ -109,7 +121,8 @@ def command( "json", *args, ], - cwd=REPO_ROOT, + cwd=registry_path.parents[2] / "worker", + env=env, check=check, text=True, capture_output=True, @@ -119,6 +132,8 @@ def command( def cli( registry_path: Path, *args: str, check: bool = True ) -> subprocess.CompletedProcess[str]: + if args[0] == "acquire": + args = (*args, "--write-worktree", str(registry_path.parents[2] / "worker")) return command(registry_path, "task-lease", *args, check=check) @@ -158,15 +173,9 @@ def inspected_lease(registry_path: Path, todo_id: str) -> dict[str, Any]: ) -def main() -> int: +def exercise_provider(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-task-lease-smoke-") as tmp: - registry_path, state_file = write_fixture(Path(tmp)) - - set_claimed_by( - state_file, - todo_id=TODO_A, - owner="codex-main-control", - ) + registry_path, _state_file = write_fixture(Path(tmp), provider) first = payload( cli( registry_path, @@ -190,6 +199,10 @@ def main() -> int: assert first["lease"]["version"] == 1, first assert first["lease"]["lease_epoch"] == 1, first assert first["lease"]["acquire_ttl_seconds"] == 120, first + assert first["source_authority"] == provider + "_v0", first + assert first["lease"]["write_workspace"]["repository"] == ( + "git:example.invalid/loopx/lease-fixture" + ), first idempotent = payload( cli( @@ -296,30 +309,21 @@ def main() -> int: assert payload(claim_blocked_transfer)["error_code"] == ( "owner_conflicts_with_claim" ) - set_claimed_by(state_file, todo_id=TODO_A, owner=None) - transferred = payload( - cli( - registry_path, - "transfer", - "--goal-id", - GOAL_ID, - "--todo-id", - TODO_A, - "--owner", - "codex-main-control", - "--idempotency-key", - "turn-1", - "--new-owner", - "codex-side-bypass", - "--new-idempotency-key", - "side-transfer", - "--expected-version", - "2", - ) + assert inspected_lease(registry_path, TODO_A)["lease"] == renewed["lease"] + transfer_args = ( + "transfer", "--goal-id", GOAL_ID, "--todo-id", TODO_A, + "--owner", "codex-main-control", "--idempotency-key", "turn-1", + "--new-owner", "codex-side-bypass", "--new-idempotency-key", "side-transfer", + "--expected-version", "2", "--transfer-claim", ) + transferred = payload(cli(registry_path, *transfer_args)) assert transferred["lease"]["owner"] == "codex-side-bypass", transferred assert transferred["lease"]["version"] == 3, transferred assert transferred["lease"]["lease_epoch"] == 2, transferred + assert transferred["claimed_by"] == "codex-side-bypass", transferred + current = payload(command(registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_A)) + assert current["todo"]["claimed_by"] == "codex-side-bypass", current + assert current["authority_read"]["legacy_fallback_used"] is False, current stale_owner = lifecycle_cli( registry_path, @@ -330,9 +334,16 @@ def main() -> int: check=False, ) assert stale_owner.returncode == 1, stale_owner.stdout - assert payload(stale_owner)["error_code"] == "lease_cas_mismatch" + # Atomic claim handover rejects the previous actor before testing its lease key. + assert payload(stale_owner)["error_code"] == "owner_conflicts_with_claim", payload(stale_owner) after_stale_owner = inspected_lease(registry_path, TODO_A) assert after_stale_owner["lease"] == transferred["lease"], after_stale_owner + stale_key = lifecycle_cli( + registry_path, "renew", "codex-side-bypass", "turn-1", 3, check=False, + ) + assert stale_key.returncode == 1, stale_key.stdout + assert payload(stale_key)["error_code"] == "lease_cas_mismatch", payload(stale_key) + assert inspected_lease(registry_path, TODO_A)["lease"] == transferred["lease"] renewed_after_transfer = payload( lifecycle_cli( @@ -458,6 +469,9 @@ def main() -> int: ) assert stale_completion.returncode == 1, stale_completion.stdout assert payload(stale_completion)["error_code"] == "lease_cas_mismatch" + assert inspected_lease(registry_path, TODO_A)["lease"] == next_generation["lease"] + pending = payload(command(registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_A)) + assert pending["todo"]["status"] == "open", pending completed = payload( command( @@ -480,7 +494,23 @@ def main() -> int: ) ) assert completed["ok"] is True, completed - assert completed["task_lease_fence"]["lease_epoch"] == 3, completed + current = payload(command(registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_A)) + assert current["todo"]["status"] == "done", current + final_lease = inspected_lease(registry_path, TODO_A) + assert final_lease["lease"]["lease_epoch"] == 3, final_lease + assert final_lease["lease"]["status"] == "released" and not final_lease["active"], final_lease + historical = payload(cli(registry_path, *transfer_args)) + assert historical["original_receipt"] == transferred["original_receipt"], historical + assert historical["lease"]["status"] == "active", historical + assert inspected_lease(registry_path, TODO_A)["lease"] == final_lease["lease"] + replayed = payload(command(registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_A)) + assert replayed["todo"] == current["todo"], replayed + + +def main() -> int: + os.environ["NODE_OPTIONS"] = os.environ.get("NODE_OPTIONS", "") + " --experimental-sqlite" + for provider in ("file", "sqlite"): + exercise_provider(provider) print("task-lease-runtime-smoke ok") return 0 From 842003de2a4d49c04a8f3917dc0c3e5051a20316 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:33:49 +0800 Subject: [PATCH 49/98] test: qualify canonical Chat actions and display recovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- examples/loopx-chat-actions-smoke.py | 14 +++++- examples/loopx-chat-contract-smoke.py | 61 +++++++++++++++++++++++++-- examples/loopx-chat-server-smoke.py | 6 +++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/examples/loopx-chat-actions-smoke.py b/examples/loopx-chat-actions-smoke.py index 14d35d35ae..10ecdb045d 100644 --- a/examples/loopx-chat-actions-smoke.py +++ b/examples/loopx-chat-actions-smoke.py @@ -23,6 +23,8 @@ from loopx.chat_server import ChatHTTPServer, ChatRequestHandler # noqa: E402 from loopx.chat_store import ChatSessionStore # noqa: E402 from loopx.history import collect_history # noqa: E402 +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 def preview_request( @@ -192,6 +194,7 @@ def write_registry_fixture(root: Path) -> tuple[Path, Path]: + "\n", encoding="utf-8", ) + import_fixture_authority(registry, "goal-one") return registry, state @@ -963,6 +966,8 @@ def assert_http_action_api(root: Path) -> None: # A decision outcome is only valid on a User gate; the canonical owner # rejects it before any proposal is stored. + before_wrong_role = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id="goal-one") + before_wrong_role_display = state_path.read_bytes() code, misdirected_decision = request_json( f"{base_url}/api/actions/preview", method="POST", @@ -975,7 +980,12 @@ def assert_http_action_api(root: Path) -> None: }, ) assert code == 400, misdirected_decision - assert "decision_outcome is only valid" in misdirected_decision["error"], misdirected_decision + assert misdirected_decision["error_code"] == "invalid_action_preview", misdirected_decision + assert misdirected_decision["error"] == "Todo does not have the requested role", misdirected_decision + assert not any(proposal.get("idempotency_key") == "http-misdirected-decision" + for proposal in service.store.list()) + assert state_path.read_bytes() == before_wrong_role_display + assert read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id="goal-one") == before_wrong_role for index, (action_kind, params, gate_kind) in enumerate( [ @@ -1010,7 +1020,7 @@ def assert_http_action_api(root: Path) -> None: persisted_payload = action_store.path.read_text(encoding="utf-8") assert str(root) not in persisted_payload, persisted_payload - assert str(project := registry_path.parent.parent) not in persisted_payload, persisted_payload + assert str(registry_path.parent.parent) not in persisted_payload, persisted_payload cancellable_code, cancellable = request_json( f"{base_url}/api/actions/preview", diff --git a/examples/loopx-chat-contract-smoke.py b/examples/loopx-chat-contract-smoke.py index 24a95d5a2b..766e8cfa75 100644 --- a/examples/loopx-chat-contract-smoke.py +++ b/examples/loopx-chat-contract-smoke.py @@ -19,13 +19,16 @@ build_todo_review_preview, parse_agent_response, ) +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 +from loopx.control_plane.todos.provider_projection import project_current_canonical_todos # noqa: E402 GOAL_ID = "loopx-chat-contract-smoke" TODO_TEXT = "[P0] Add a reviewable Goal Studio interaction." -def write_fixture(root: Path) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, todo_anchor: bool = True) -> tuple[Path, Path, Path]: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -39,7 +42,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: "## Objective\n\n" "Keep LoopX Chat reviewable.\n\n" "## Next Action\n\n" - "- Review the next bounded proposal.\n", + "- Review the next bounded proposal.\n" + ("\n## Agent Todo\n" if todo_anchor else ""), encoding="utf-8", ) registry_path = project / ".loopx" / "registry.json" @@ -69,6 +72,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: + "\n", encoding="utf-8", ) + import_fixture_authority(registry_path, GOAL_ID) return registry_path, state_file, project @@ -174,6 +178,7 @@ def main() -> None: assert "tool narration" not in malformed["message"], malformed original = state_file.read_text(encoding="utf-8") + authority_before = read_canonical_todos_if_promoted(runtime_root=root / "runtime", goal_id=GOAL_ID) preview = build_todo_review_preview( registry_path=registry_path, goal_id=GOAL_ID, @@ -191,6 +196,7 @@ def main() -> None: "action_kind": "loopx_chat_proposal", }, preview assert state_file.read_text(encoding="utf-8") == original + assert read_canonical_todos_if_promoted(runtime_root=root / "runtime", goal_id=GOAL_ID) == authority_before assert "state_file" not in json.dumps(preview), preview assert "project" not in json.dumps(preview), preview @@ -213,6 +219,8 @@ def main() -> None: assert str(project) not in json.dumps(no_write_receipt), no_write_receipt else: raise AssertionError("stale preview must be rejected") + assert state_file.read_text(encoding="utf-8") == original + assert read_canonical_todos_if_promoted(runtime_root=root / "runtime", goal_id=GOAL_ID) == authority_before applied = apply_todo_review_preview( registry_path=registry_path, @@ -222,7 +230,15 @@ def main() -> None: ) assert applied["ok"] is True, applied assert applied["applied"] is True, applied - assert applied["todo"]["todo_id"] == preview["todo"]["todo_id"], applied + # Canonical creation allocates its id at commit; preview locks the + # intent and provider revision, not a dry-run's provisional id. + for field in ("goal_id", "text", "status", "task_class", "action_kind"): + assert applied["todo"][field] == preview["todo"][field], applied + committed = read_canonical_todos_if_promoted(runtime_root=root / "runtime", goal_id=GOAL_ID) + assert committed["decision_read_from_provider"] is True and committed["legacy_fallback_used"] is False + assert len(committed["todos"]) == 1, committed + assert committed["todos"][0]["todo_id"] == applied["todo"]["todo_id"], committed + assert committed["todos"][0]["text"] == TODO_TEXT, committed receipt = applied["receipt"] assert receipt["schema_version"] == "loopx_chat_todo_receipt_v0", receipt assert receipt["receipt_id"], receipt @@ -236,6 +252,14 @@ def main() -> None: assert str(project) not in json.dumps(receipt), receipt assert state_file.read_text(encoding="utf-8").count(TODO_TEXT) == 1 + # Display edits cannot replace the committed record or create a + # duplicate. Restore the document before another explicit write. + display = state_file.read_text(encoding="utf-8") + state_file.write_text(display.replace(TODO_TEXT, "Spoofed display proposal"), encoding="utf-8") + assert read_canonical_todos_if_promoted(runtime_root=root / "runtime", goal_id=GOAL_ID) == committed + + state_file.write_text(display, encoding="utf-8") + retry_preview = build_todo_review_preview( registry_path=registry_path, goal_id=GOAL_ID, @@ -254,6 +278,37 @@ def main() -> None: assert retry_receipt["already_exists"] is True, retry_receipt assert retry_receipt["todo_id"] == receipt["todo_id"], retry_receipt assert state_file.read_text(encoding="utf-8").count(TODO_TEXT) == 1 + assert read_canonical_todos_if_promoted(runtime_root=root / "runtime", goal_id=GOAL_ID) == committed + + # An anchorless historical document is retained as a separate display + # recovery case. A successful canonical commit cannot repair its prose + # implicitly or turn display failure into a second business write. + recovery_root = root / "display-recovery" + recovery_registry, recovery_state, _ = write_fixture(recovery_root, todo_anchor=False) + original_narrative = recovery_state.read_text(encoding="utf-8") + recovery_preview = build_todo_review_preview(registry_path=recovery_registry, goal_id=GOAL_ID, text=TODO_TEXT) + recovery_applied = apply_todo_review_preview(registry_path=recovery_registry, goal_id=GOAL_ID, + text=TODO_TEXT, preview_id=recovery_preview["preview_id"]) + assert recovery_applied["applied"] is True, recovery_applied + assert recovery_state.read_text(encoding="utf-8") == original_narrative + accepted = read_canonical_todos_if_promoted(runtime_root=recovery_root / "runtime", goal_id=GOAL_ID) + assert len(accepted["todos"]) == 1, accepted + try: + project_current_canonical_todos(registry_path=recovery_registry, + runtime_root=recovery_root / "runtime", goal_id=GOAL_ID) + except ValueError as error: + assert "no projection anchor" in str(error), error + else: + raise AssertionError("an anchorless document must refuse implicit projection") + assert recovery_state.read_text(encoding="utf-8") == original_narrative + recovery_state.write_text(original_narrative + "\n## Agent Todo\n", encoding="utf-8") + delivered = project_current_canonical_todos(registry_path=recovery_registry, + runtime_root=recovery_root / "runtime", goal_id=GOAL_ID, + expected_provider_revision=accepted["provider_revision"]) + assert delivered["status"] == "delivered" and delivered["narrative_preserved"] is True, delivered + assert recovery_state.read_text(encoding="utf-8").startswith(original_narrative) + assert recovery_state.read_text(encoding="utf-8").count(TODO_TEXT) == 1 + assert read_canonical_todos_if_promoted(runtime_root=recovery_root / "runtime", goal_id=GOAL_ID) == accepted print("loopx-chat-contract-smoke: ok") diff --git a/examples/loopx-chat-server-smoke.py b/examples/loopx-chat-server-smoke.py index 7fdd559c5d..ddae9b7077 100644 --- a/examples/loopx-chat-server-smoke.py +++ b/examples/loopx-chat-server-smoke.py @@ -18,6 +18,10 @@ REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 + GOAL_ID = "loopx-chat-server-smoke" SECOND_GOAL_ID = "loopx-chat-server-second-goal" TODO_TEXT = "[P0] Implement the approved Goal Studio review card." @@ -168,6 +172,8 @@ def write_fixture(root: Path) -> tuple[Path, Path]: + "\n", encoding="utf-8", ) + for goal_id in (GOAL_ID, SECOND_GOAL_ID): + import_fixture_authority(registry, goal_id) return registry, state_file From 7c57373fb4cca494baf19fcc47d5f5444c2cd5d6 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:11:46 +0800 Subject: [PATCH 50/98] test: qualify canonical integrated advancement and role successors Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../control-plane-integrated-canary-smoke.py | 100 ++++++++++-------- .../multi-agent-role-successor-todos-smoke.py | 30 +++++- loopx/control_plane/testing/canary_harness.py | 12 ++- 3 files changed, 93 insertions(+), 49 deletions(-) diff --git a/examples/control_plane/control-plane-integrated-canary-smoke.py b/examples/control_plane/control-plane-integrated-canary-smoke.py index 63049aa45b..9bb2b2cd11 100644 --- a/examples/control_plane/control-plane-integrated-canary-smoke.py +++ b/examples/control_plane/control-plane-integrated-canary-smoke.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Bounded canary for the status -> quota -> review-packet Todo read path.""" +"""Bounded File/SQLite canary for status, quota, successor and monitor readback.""" from __future__ import annotations @@ -17,21 +17,23 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.control_plane.testing.canary_harness import ( # noqa: E402 + import_fixture_authority, read_run_index, run_json_cli, write_fixture_registry, ) +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 GOAL_ID = "control-plane-integrated-canary" MONITOR_GOAL_ID = "control-plane-integrated-monitor-canary" -MARKDOWN_GOAL_ID = "control-plane-markdown-continuation-canary" +CONTINUATION_GOAL_ID = "control-plane-continuation-canary" AGENT_ID = "codex-product-capability" PRIMARY_AGENT_ID = "codex-main-control" CANARY_TODO_ID = "todo_integrated_canary" CANARY_TODO_TITLE = "Design bounded status/quota/review-packet/Todo/read-path canary" -SUCCESSOR_TODO_TITLE = "Continue integrated Markdown successor routing canary" -MARKDOWN_CONTINUATION_TITLE = "Continue same-agent markdown continuation through status and quota." +SUCCESSOR_TODO_TITLE = "Continue integrated canonical successor routing canary" +CONTINUATION_TITLE = "Continue same-agent canonical continuation through status and quota." MONITOR_TODO_ID = "todo_integrated_due_monitor" MONITOR_TARGET_KEY = "integrated-due-monitor-watch" VALIDATED_PROGRESS_CLASSIFICATION = "integrated_canary_validated_progress" @@ -73,7 +75,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: return registry_path, state_file, runtime -def write_monitor_fixture(root: Path) -> tuple[Path, Path]: +def write_monitor_fixture(root: Path, *, provider: str) -> tuple[Path, Path]: project = root / "monitor-project" runtime = root / "monitor-runtime" state_file = project / ".codex" / "goals" / MONITOR_GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -103,13 +105,14 @@ def write_monitor_fixture(root: Path) -> tuple[Path, Path]: registered_agents=[AGENT_ID], quota_allowed_slots=10, ) + import_fixture_authority(registry_path, MONITOR_GOAL_ID, provider=provider) return registry_path, runtime -def write_markdown_continuation_fixture(root: Path) -> tuple[Path, Path, Path]: - project = root / "markdown-project" - runtime = root / "markdown-runtime" - state_file = project / ".codex" / "goals" / MARKDOWN_GOAL_ID / "ACTIVE_GOAL_STATE.md" +def write_continuation_fixture(root: Path, *, provider: str) -> tuple[Path, Path, Path]: + project = root / "continuation-project" + runtime = root / "continuation-runtime" + state_file = project / ".codex" / "goals" / CONTINUATION_GOAL_ID / "ACTIVE_GOAL_STATE.md" registry_path = project / ".loopx" / "registry.json" state_file.parent.mkdir(parents=True) state_file.write_text( @@ -117,9 +120,9 @@ def write_markdown_continuation_fixture(root: Path) -> tuple[Path, Path, Path]: "status: active\n" "updated_at: 2026-06-27T00:00:00+00:00\n" "---\n\n" - "# Control Plane Markdown Continuation Canary\n\n" + "# Control Plane Canonical Continuation Canary\n\n" "## Next Action\n\n" - "- Preserve same-agent markdown successor routing across status and quota.\n\n" + "- Preserve same-agent canonical successor routing across status and quota.\n\n" "## Agent Todo\n\n" "- [ ] [P2] Markdown placeholder that should lose to the claimed P1 todo.\n" " ", + "- [ ] [P0] Run source action.", + " ", "", ] ), @@ -92,6 +95,9 @@ def main() -> int: runtime_root = root / "runtime" write_minimal_state(state_file) write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + import_fixture_authority(registry, GOAL_ID) + before = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) + display_before = state_file.read_bytes() preview = apply_role_successor_todos( registry_path=registry, @@ -139,6 +145,8 @@ def main() -> int: ) assert skipped["needed"] is False, skipped assert skipped["reason"] == "no_second_candidate", skipped + assert read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) == before + assert state_file.read_bytes() == display_before executed = apply_role_successor_todos( registry_path=registry, @@ -158,6 +166,24 @@ def main() -> int: state_text = state_file.read_text(encoding="utf-8") assert "claimed_by=agent-b" in state_text, state_text assert f"unblocks_todo_id={SOURCE_TODO_ID}" in state_text, state_text + after = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) + assert after["source_authority"] == "file_v0", after + records = {todo["todo_id"]: todo for todo in after["todos"]} + assert len(records) == 2, records + assert records[added["todo_id"]]["claimed_by"] == "agent-b", records + assert records[added["todo_id"]]["unblocks_todo_id"] == SOURCE_TODO_ID, records + original_source = next(todo for todo in before["todos"] if todo["todo_id"] == SOURCE_TODO_ID) + assert records[SOURCE_TODO_ID] == original_source, records + replay = apply_role_successor_todos( + registry_path=registry, goal_id=GOAL_ID, source_todo_id=SOURCE_TODO_ID, + current_agent_id="agent-a", role_id="evidence_runner", action="run_dev_eval", + successor_specs=[successor_spec()], decision_summary={"dev_promotion_candidate_count": 1}, + execute=True, + ) + assert replay["successors"][0]["already_exists"] is True, replay + assert replay["successors"][0]["todo_id"] == added["todo_id"], replay + assert read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) == after + display_after = state_file.read_bytes() try: apply_role_successor_todos( @@ -175,6 +201,8 @@ def main() -> int: assert "successor target_agent_id 'missing-agent' is not registered" in str(exc), exc else: raise AssertionError("missing successor target agent should fail closed") + assert read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) == after + assert state_file.read_bytes() == display_after print("multi-agent-role-successor-todos-smoke ok") return 0 diff --git a/loopx/control_plane/testing/canary_harness.py b/loopx/control_plane/testing/canary_harness.py index 724a19227c..3fb37301d4 100644 --- a/loopx/control_plane/testing/canary_harness.py +++ b/loopx/control_plane/testing/canary_harness.py @@ -184,11 +184,11 @@ def read_run_index(runtime_root: Path, goal_id: str) -> list[dict[str, Any]]: ] -def import_fixture_authority(registry_path: Path, goal_id: str) -> None: +def import_fixture_authority(registry_path: Path, goal_id: str, *, provider: str = "file") -> None: """Import a disposable smoke's complete source through the real saved plan. Fixture writers have finished before this call. Historical metadata stays - intact; normal writes afterwards use the canonical File/soft-claim owner. + intact; normal writes afterwards use the selected canonical/soft-claim owner. Existing unpromoted/refusal fixtures opt in explicitly, never by default. """ from ...state_backup import build_state_backup_plan, execute_state_backup_plan @@ -200,6 +200,10 @@ def import_fixture_authority(registry_path: Path, goal_id: str) -> None: runtime = Path(registry["common_runtime_root"]).resolve() # Bind the physical source consistently on hosts whose temp path is a link. goal["repo"] = str(project) + state_file = Path(goal["state_file"]).expanduser() + if not state_file.is_absolute(): + state_file = project / state_file + goal["state_file"] = str(state_file.resolve()) registry_path.write_text(json.dumps(registry), encoding="utf-8") runtime.mkdir(parents=True, exist_ok=True) backup = execute_state_backup_plan(build_state_backup_plan( @@ -214,7 +218,7 @@ def import_fixture_authority(registry_path: Path, goal_id: str) -> None: prepared = run_json_cli( *prefix, "prepare-import", *identity, "--backup-manifest", backup["manifest_path"], - "--provider", "file", "--target-handoff-mode", "soft_claim", + "--provider", provider, "--target-handoff-mode", "soft_claim", registry_path=registry_path, runtime_root=runtime, )["cold_import"] assert prepared["status"] == "prepared", prepared @@ -231,6 +235,6 @@ def import_fixture_authority(registry_path: Path, goal_id: str) -> None: "todo", "list", "--goal-id", goal_id, registry_path=registry_path, runtime_root=runtime, )["authority_read"] - assert readback["source_authority"] == "file_v0", readback + assert readback["source_authority"] == f"{provider}_v0", readback assert readback["decision_read_from_provider"] is True, readback assert readback["legacy_fallback_used"] is False, readback From e1304f5ae6feb6b5c6eeb901e636501ef97af258 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:50:24 +0800 Subject: [PATCH 51/98] fix: read maintainer correction ownership from canonical Todo Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../issue-fix-maintainer-correction-smoke.py | 24 ++++++++++++++++--- loopx/capabilities/issue_fix/cli.py | 5 ++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/examples/issue-fix-maintainer-correction-smoke.py b/examples/issue-fix-maintainer-correction-smoke.py index 9ef6bb1079..19a53ab67b 100644 --- a/examples/issue-fix-maintainer-correction-smoke.py +++ b/examples/issue-fix-maintainer-correction-smoke.py @@ -18,6 +18,9 @@ normalise_issue_fix_maintainer_correction_input, ) +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 +from loopx.todos import list_goal_todos # noqa: E402 + GOAL_ID = "issue-fix-maintainer-correction-smoke" AGENT_ID = "issue-fix-agent" @@ -43,7 +46,7 @@ def run_cli(registry: Path, args: list[str]) -> dict[str, object]: return json.loads(result.stdout) -def write_fixture(root: Path) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, provider: str) -> tuple[Path, Path, Path]: project = root / "project" project.mkdir() state_file = project / "ACTIVE_GOAL_STATE.md" @@ -57,6 +60,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: registry.write_text( json.dumps( { + "common_runtime_root": str(root / "runtime"), "goals": [ { "id": GOAL_ID, @@ -72,6 +76,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: ), encoding="utf-8", ) + import_fixture_authority(registry, GOAL_ID, provider=provider) metadata = root / "pr.json" metadata.write_text( json.dumps( @@ -107,10 +112,10 @@ def base_correction(**overrides: object) -> dict[str, object]: return payload -def main() -> int: +def check_corrections(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-maintainer-correction-") as tmpdir: root = Path(tmpdir) - project, registry, metadata = write_fixture(root) + project, registry, metadata = write_fixture(root, provider) correction = correction_file(root, "actionable.json", base_correction()) common = [ "issue-fix", @@ -237,6 +242,14 @@ def main() -> int: assert quiet["todo_write"]["skip_reason"] == "unchanged_monitor_quiet", quiet assert quiet["todo_write_performed"] is False, quiet + native = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) + assert native["authority_read"]["decision_read_from_provider"] is True, native + assert native["authority_read"]["legacy_fallback_used"] is False, native + patches = [item for item in native["todos"] + if item.get("action_kind") == "issue_fix_maintainer_correction_patch"] + assert len(patches) == 1, patches + assert patches[0]["claimed_by"] == AGENT_ID, patches + for unsafe in ( base_correction(raw_comment_body="raw body must not enter the contract"), base_correction(source_ref="https://localhost/reviews/42"), @@ -250,6 +263,11 @@ def main() -> int: else: raise AssertionError(f"unsafe correction input was accepted: {unsafe}") + + +def main() -> int: + for provider in ("file", "sqlite"): + check_corrections(provider) print("issue-fix-maintainer-correction-smoke: ok") return 0 diff --git a/loopx/capabilities/issue_fix/cli.py b/loopx/capabilities/issue_fix/cli.py index 65d8ad868a..69c9caa772 100644 --- a/loopx/capabilities/issue_fix/cli.py +++ b/loopx/capabilities/issue_fix/cli.py @@ -1356,14 +1356,15 @@ def handle_issue_fix_command( or todo_write.get("metadata_updated") or todo_write.get("status_changed") ) + created_todo = todo_write["todo"] payload["todo_write"] = { "schema_version": "issue_fix_maintainer_correction_todo_write_v0", "write_performed": todo_changed, "already_exists": bool(todo_write.get("already_exists")), "todo_id": todo_write.get("todo_id"), "role": todo_write.get("role"), - "claimed_by": todo_write.get("claimed_by"), - "blocks_agent": todo_write.get("blocks_agent"), + "claimed_by": created_todo.get("claimed_by"), + "blocks_agent": created_todo.get("blocks_agent"), "path_recorded": False, } payload["todo_write_performed"] = todo_changed From 5dff84771f8a95f01259b5f2492f1a1d3aa62a52 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:50:24 +0800 Subject: [PATCH 52/98] test: migrate Explore and PR reconciliation callers to canonical Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../explore-todo-result-node-linkage-smoke.py | 24 ++++++++++++++----- examples/issue-fix-pr-lifecycle-smoke.py | 21 +++++++++++++--- .../issue-fix-pr-review-reconcile-smoke.py | 18 ++++++++++++-- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/examples/explore-todo-result-node-linkage-smoke.py b/examples/explore-todo-result-node-linkage-smoke.py index 1df43c0a6d..e7812529e3 100644 --- a/examples/explore-todo-result-node-linkage-smoke.py +++ b/examples/explore-todo-result-node-linkage-smoke.py @@ -18,6 +18,7 @@ from loopx.capabilities.explore.todo_branch_plan import ( # noqa: E402 build_explore_todo_branch_plan, ) +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 from loopx.control_plane.todos.contract import format_todo_metadata_line # noqa: E402 @@ -51,7 +52,7 @@ def run_loopx(registry: Path, *args: str) -> dict[str, object]: return json.loads(completed.stdout) -def write_fixture(root: Path) -> tuple[Path, Path]: +def write_fixture(root: Path, provider: str) -> tuple[Path, Path]: project = root / "project" project.mkdir() state_file = project / "ACTIVE_GOAL_STATE.md" @@ -84,10 +85,11 @@ def write_fixture(root: Path) -> tuple[Path, Path]: ), encoding="utf-8", ) + import_fixture_authority(registry, GOAL_ID, provider=provider) return registry, state_file -def check_todo_lifecycle() -> None: +def check_todo_lifecycle(provider: str) -> None: try: format_todo_metadata_line( todo_id="todo_invalid123", @@ -99,7 +101,7 @@ def check_todo_lifecycle() -> None: raise AssertionError("private path-shaped Explore node refs must fail closed") with tempfile.TemporaryDirectory(prefix="loopx-explore-todo-link-") as tmp: - registry, state_file = write_fixture(Path(tmp)) + registry, state_file = write_fixture(Path(tmp), provider) added = run_loopx( registry, "todo", @@ -118,7 +120,7 @@ def check_todo_lifecycle() -> None: "node_dead", ) todo_id = str(added["todo_id"]) - assert added["explore_result_node_refs"] == ["node_live", "node_dead"], added + assert added["todo"]["explore_result_node_refs"] == ["node_live", "node_dead"], added listed = run_loopx( registry, @@ -148,6 +150,10 @@ def check_todo_lifecycle() -> None: "--no-follow-up", ) assert completed["status"] == "done", completed + terminal = run_loopx(registry, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", todo_id)["todo"] + assert terminal["status"] == "done" + assert terminal["explore_result_node_refs"] == ["node_live", "node_dead"] state_text = state_file.read_text(encoding="utf-8") assert "explore_result_node_refs=node_live%2Cnode_dead" in state_text, state_text @@ -165,6 +171,11 @@ def check_todo_lifecycle() -> None: assert "Completed Work Archive" in archived, archived assert "explore_result_node_refs=node_live%2Cnode_dead" in archived, archived + archived_item = run_loopx(registry, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", todo_id)["todo"] + assert archived_item["archive_state"] == "archive", archived_item + assert archived_item["explore_result_node_refs"] == ["node_live", "node_dead"] + second = run_loopx( registry, "todo", @@ -197,7 +208,7 @@ def check_todo_lifecycle() -> None: "--todo-id", str(second["todo_id"]), )["todo"] - assert "explore_result_node_refs" not in cleared, cleared + assert cleared["explore_result_node_refs"] == [], cleared def check_planner_diagnostics() -> None: @@ -283,7 +294,8 @@ def check_planner_diagnostics() -> None: def main() -> int: - check_todo_lifecycle() + for provider in ("file", "sqlite"): + check_todo_lifecycle(provider) check_planner_diagnostics() print("explore-todo-result-node-linkage-smoke ok") return 0 diff --git a/examples/issue-fix-pr-lifecycle-smoke.py b/examples/issue-fix-pr-lifecycle-smoke.py index 854faf692b..e47bf3feae 100644 --- a/examples/issue-fix-pr-lifecycle-smoke.py +++ b/examples/issue-fix-pr-lifecycle-smoke.py @@ -21,6 +21,8 @@ ISSUE_FIX_PR_LIFECYCLE_MONITOR_SCHEMA_VERSION, build_issue_fix_pr_lifecycle_monitor_packet, ) +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 +from loopx.todos import list_goal_todos # noqa: E402 from loopx.domain_packs.issue_fix import ( # noqa: E402 default_issue_fix_domain_state_ledger_path, issue_fix_pr_lifecycle_ledger_key, @@ -122,7 +124,7 @@ def assert_packet_shape(packet: dict[str, Any]) -> None: assert_public_safe(packet) -def main() -> int: +def check_lifecycle(provider: str) -> None: merged = build_issue_fix_pr_lifecycle_monitor_packet( url="https://github.com/huangruiteng/loopx/pull/1715", provider_payload={ @@ -350,6 +352,8 @@ def main() -> int: "## Agent Todo\n\n", encoding="utf-8", ) + import_fixture_authority(registry_path, "example-goal", provider=provider) + ledger = default_issue_fix_domain_state_ledger_path( project=project, goal_id="example-goal", @@ -552,8 +556,7 @@ def main() -> int: mutation_authority = reconciled_gate["todo_completion"][ "mutation_authority" ] - assert mutation_authority["mode"] == "registered_peer_actor", reconciled_gate - assert mutation_authority["actor_agent_id"] == "codex-test", reconciled_gate + assert mutation_authority["authority_mode"] == "registered_peer_actor", reconciled_gate assert reconciled_gate["rollout_event"]["appended"] is True, reconciled_gate assert "status=done" in state_file.read_text(encoding="utf-8") gate_projection = parse_active_state_todos( @@ -786,6 +789,18 @@ def main() -> int: material_rows ) + native = list_goal_todos(registry_path=registry_path, goal_id="example-goal") + assert native["authority_read"]["decision_read_from_provider"] is True, native + assert native["authority_read"]["legacy_fallback_used"] is False, native + rows = {item["todo_id"]: item for item in native["todos"]} + for todo_id in ("todo_merge_gate_1716", "todo_review_action_1716"): + assert rows[todo_id]["status"] == "done", rows[todo_id] + assert rows[todo_id]["last_actor_agent_id"] == "codex-test", rows[todo_id] + + +def main() -> int: + for provider in ("file", "sqlite"): + check_lifecycle(provider) print("issue-fix-pr-lifecycle-smoke: ok") return 0 diff --git a/examples/issue-fix-pr-review-reconcile-smoke.py b/examples/issue-fix-pr-review-reconcile-smoke.py index 040d4fc599..36075c0fcb 100644 --- a/examples/issue-fix-pr-review-reconcile-smoke.py +++ b/examples/issue-fix-pr-review-reconcile-smoke.py @@ -24,9 +24,10 @@ from loopx.capabilities.issue_fix.pr_review_ack import ( # noqa: E402 record_issue_fix_pr_review_ack, ) +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 from loopx.heartbeat_prequota import run_heartbeat_pre_quota # noqa: E402 from loopx.status import parse_active_state_todos # noqa: E402 -from loopx.todos import update_goal_todo # noqa: E402 +from loopx.todos import list_goal_todos, update_goal_todo # noqa: E402 PR_URL = "https://github.com/huangruiteng/loopx/pull/1716" @@ -89,7 +90,7 @@ def reconcile_without_provider( ) -def main() -> int: +def check_reconciliation(provider: str) -> None: with tempfile.TemporaryDirectory( prefix="loopx-issue-fix-pr-review-reconcile-" ) as tmpdir: @@ -142,6 +143,8 @@ def main() -> int: encoding="utf-8", ) + import_fixture_authority(registry_path, "example-goal", provider=provider) + missing = reconcile_without_provider( registry_path=registry_path, runtime_root=runtime_root, @@ -330,7 +333,18 @@ def main() -> int: assert cli_payload["quota_spend_required"] is False, cli_payload assert todo(state_file, "todo_other_provider")["status"] == "open" + native = list_goal_todos(registry_path=registry_path, goal_id="example-goal") + assert native["authority_read"]["decision_read_from_provider"] is True, native + assert native["authority_read"]["legacy_fallback_used"] is False, native + rows = {item["todo_id"]: item for item in native["todos"]} + assert rows["todo_github_review"]["status"] == "done" + assert rows["todo_other_provider"]["status"] == "open" + + +def main() -> int: + for provider in ("file", "sqlite"): + check_reconciliation(provider) print("issue-fix-pr-review-reconcile-smoke: ok") return 0 From f63966e936635ac7e15bc1aa1ba2199541210b6f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:06:36 +0800 Subject: [PATCH 53/98] test: qualify canonical project reconnect and adoption callers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../bootstrap-force-preserve-todos-smoke.py | 132 +++++++------ .../project/goal-rollout-event-log-smoke.py | 184 ++++++++++-------- .../project/project-agent-adoption-smoke.py | 54 +++-- 3 files changed, 206 insertions(+), 164 deletions(-) diff --git a/examples/project/bootstrap-force-preserve-todos-smoke.py b/examples/project/bootstrap-force-preserve-todos-smoke.py index 9a28448ded..110b28e261 100644 --- a/examples/project/bootstrap-force-preserve-todos-smoke.py +++ b/examples/project/bootstrap-force-preserve-todos-smoke.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Smoke-test canonical force refusal and explicit File-preserving reconnects.""" +"""Smoke-test canonical reconnect preservation and refusal of retired writes.""" from __future__ import annotations @@ -56,9 +56,11 @@ def main() -> int: assert canonical_state.read_bytes() == state_before assert (canonical_project / ".loopx/registry.json").read_bytes() == registry_before - # Existing force/Markdown migration assertions belong to the supported - # explicit File creation path, not the canonical default writer. - runtime = Path(tmp) / "file-runtime" + for provider in ("file", "sqlite"): + assert_canonical_reconnect(Path(tmp) / provider, provider) + + # The old opt-out no longer authorizes new Markdown Goal writers. + runtime = Path(tmp) / "unpromoted-runtime" configuration = runtime / "machine/configuration.json" configuration.parent.mkdir(parents=True) configuration.write_text(json.dumps({ @@ -71,81 +73,77 @@ def main() -> int: project = Path(tmp) / "project" project.mkdir() - initial = run_cli( - "--runtime-root", str(runtime), "bootstrap", - "--project", - str(project), - "--goal-id", - GOAL_ID, - "--objective", - "Exercise force reconnect preservation.", - "--no-global-sync", - ) - assert initial["ok"] is True, initial - assert initial["state_action"] == "created", initial - assert initial["storage_target"] == { - "schema_version": "loopx_new_goal_storage_target_v0", "provider": "file", - }, initial - state_file = Path(initial["state_file"]) - assert state_file == (project / ".loopx/goals/bootstrap-preserve-fixture/ACTIVE_GOAL_STATE.md").resolve() - - todo_state = "# Active Goal State\n\n## Agent Todo\n\n- [ ] Preserve this during reconnect.\n" - state_file.write_text(todo_state, encoding="utf-8") - - preserved = run_cli( - "--runtime-root", str(runtime), "bootstrap", - "--project", - str(project), - "--goal-id", - GOAL_ID, - "--objective", - "Exercise force reconnect preservation.", - "--write-scope", - "src/**", - "--force", - "--preserve-todos", - "--no-global-sync", - ) - assert preserved["ok"] is True, preserved - assert preserved["state_action"] == "kept-existing-preserve-todos", preserved - warning = preserved["force_bootstrap_warning"] - assert warning["will_replace_active_state"] is False, warning - assert warning["preserve_todos_requested"] is True, warning - assert "configure-goal --write-scope" in warning["recommended_scope_migration"], warning - migrated_state = state_file.read_text(encoding="utf-8") - assert "- [ ] Preserve this during reconnect." in migrated_state - assert "## User Todo / Owner Review Reading Queue" in migrated_state - assert preserved["todo_source_migration"] == { - "schema_version": "todo_source_section_migration_v0", - "added_roles": ["user"], - "applied": True, - }, preserved - assert goal_from_registry(project)["coordination"]["write_scope"] == ["src/**"] - - replaced = run_cli( + refused = run_cli( "--runtime-root", str(runtime), "bootstrap", "--project", str(project), "--goal-id", GOAL_ID, "--objective", - "Exercise force reconnect replacement warning.", - "--write-scope", - "docs/**", - "--force", + "Do not revive the retired Markdown writer.", "--no-global-sync", + check=False, ) - assert replaced["ok"] is True, replaced - assert replaced["state_action"] == "replaced", replaced - warning = replaced["force_bootstrap_warning"] - assert warning["will_replace_active_state"] is True, warning - assert warning["preserve_todos_requested"] is False, warning - assert "Preserve this during reconnect." not in state_file.read_text(encoding="utf-8") - assert goal_from_registry(project)["coordination"]["write_scope"] == ["docs/**"] + assert refused["ok"] is False, refused + assert refused["error_code"] == "canonical_authority_required", refused + assert not (project / ".loopx/registry.json").exists() + assert not (project / f".loopx/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md").exists() print("bootstrap-force-preserve-todos-smoke ok") return 0 +def assert_canonical_reconnect(root: Path, provider: str) -> None: + runtime = root / "runtime" + configuration = runtime / "machine/configuration.json" + configuration.parent.mkdir(parents=True) + configuration.write_text(json.dumps({ + "schema_version": "loopx_machine_configuration_v0", "namespaces": { + "goal_storage": {"schema_version": "loopx_goal_storage_defaults_v1", + "new_goal_provider": provider, "canonical_creation": True, + "new_goal_handoff_mode": "soft_claim"}, + }, + }), encoding="utf-8") + project = root / "project" + project.mkdir() + bootstrap = ( + "--runtime-root", str(runtime), "bootstrap", "--project", str(project), + "--goal-id", GOAL_ID, "--objective", "Preserve canonical Todos on reconnect.", + "--no-global-sync", + ) + initial = run_cli(*bootstrap) + assert initial["ok"] and initial["state_action"] == "created", initial + assert initial["storage_target"]["provider"] == provider, initial + state = Path(initial["state_file"]) + registry = project / ".loopx/registry.json" + prefix = ("--runtime-root", str(runtime), "--registry", str(registry)) + text = "[P1] Preserve canonical work — 保留任务与原始归属。" + added = run_cli(*prefix, "todo", "add", "--goal-id", GOAL_ID, + "--role", "agent", "--task-class", "advancement_task", + "--action-kind", "smoke", "--text", text) + assert added["added"] is True, added + read = ("todo", "list", "--goal-id", GOAL_ID) + before = run_cli(*prefix, *read) + assert before["authority_read"]["source_authority"] == f"{provider}_v0", before + assert before["authority_read"]["decision_read_from_provider"] is True, before + assert before["authority_read"]["legacy_fallback_used"] is False, before + stored = run_cli(*prefix, *read, "--todo-id", added["todo_id"])["todo"] + assert stored["text"] == text and stored["status"] == "open", stored + for force_args in (("--force",), ("--force", "--preserve-todos")): + state_before, registry_before = state.read_bytes(), registry.read_bytes() + refused = run_cli(*bootstrap, *force_args, check=False) + assert refused["ok"] is False, refused + assert "cannot rebuild existing state" in json.dumps(refused), refused + assert state.read_bytes() == state_before + assert registry.read_bytes() == registry_before + assert run_cli(*prefix, *read) == before + reconnected = run_cli(*bootstrap) + assert reconnected["ok"] is True, reconnected + assert reconnected["state_action"] == "kept-existing", reconnected + assert run_cli(*prefix, *read) == before + assert text in state.read_text(encoding="utf-8") + assert goal_from_registry(project)["coordination"]["storage_target"]["provider"] == provider + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/examples/project/goal-rollout-event-log-smoke.py b/examples/project/goal-rollout-event-log-smoke.py index 08323d4549..d5748768dc 100644 --- a/examples/project/goal-rollout-event-log-smoke.py +++ b/examples/project/goal-rollout-event-log-smoke.py @@ -21,6 +21,7 @@ rollout_event_log_path, summarize_rollout_events, ) +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 def assert_public_safe_text(text: str) -> None: @@ -347,86 +348,113 @@ def main() -> None: assert_public_safe_text(log_path.read_text(encoding="utf-8")) - registry_path, cli_runtime_root, project, cli_goal_id = write_smoke_project(tmp_root / "auto") - claim_payload = run_loopx_cli( - "--registry", - str(registry_path), - "--runtime-root", - str(cli_runtime_root), - "--format", - "json", - "todo", - "claim", - "--goal-id", - cli_goal_id, - "--todo-id", - "todo_auto_rollout", - "--claimed-by", - "codex-main-control", - ) - assert claim_payload["rollout_event"]["event_kind"] == "todo_claim", claim_payload - - refresh_payload = run_loopx_cli( - "--registry", - str(registry_path), - "--runtime-root", - str(cli_runtime_root), - "--format", - "json", - "refresh-state", - "--goal-id", - cli_goal_id, - "--classification", - "automatic_rollout_logging_smoke", - "--delivery-batch-scale", - "implementation", - "--delivery-outcome", - "outcome_progress", - "--agent-id", - "codex-main-control", - "--agent-lane", - "smoke", - "--no-global-sync", - ) - assert refresh_payload["rollout_event"]["event_kind"] == "refresh_state", refresh_payload + for provider in ("file", "sqlite"): + assert_cli_events(tmp_root / "auto" / provider, provider) - should_run_payload = run_loopx_cli( - "--registry", - str(registry_path), - "--runtime-root", - str(cli_runtime_root), - "--format", - "json", - "quota", - "should-run", - "--goal-id", - cli_goal_id, - "--agent-id", - "codex-main-control", - "--scan-root", - str(project), - "--limit", - "1", - allow_failure=True, - ) - assert should_run_payload["rollout_event"]["event_kind"] == ( - "quota_should_run" - ), should_run_payload - - auto_events = load_rollout_events(rollout_event_log_path(cli_runtime_root, cli_goal_id)) - auto_kinds = [event["event_kind"] for event in auto_events] - assert auto_kinds == [ - "todo_claim", - "refresh_state", - "quota_should_run", - ], auto_kinds - latest_event = auto_events[-1] - assert_boundary(latest_event) - auto_log_text = rollout_event_log_path(cli_runtime_root, cli_goal_id).read_text( - encoding="utf-8" - ) - assert_public_safe_text(auto_log_text) +def assert_cli_events(root: Path, provider: str) -> None: + registry_path, cli_runtime_root, project, cli_goal_id = write_smoke_project(root) + claim_args = ( + "--registry", str(registry_path), "--runtime-root", str(cli_runtime_root), + "--format", "json", "todo", "claim", "--goal-id", cli_goal_id, + "--todo-id", "todo_auto_rollout", "--claimed-by", "codex-main-control", + ) + state = project / "ACTIVE_GOAL_STATE.md" + before_state, before_registry = state.read_bytes(), registry_path.read_bytes() + refused = run_loopx_cli(*claim_args, allow_failure=True) + assert refused["ok"] is False, refused + assert refused["error_code"] == "canonical_authority_required", refused + assert state.read_bytes() == before_state + assert registry_path.read_bytes() == before_registry + import_fixture_authority(registry_path, cli_goal_id, provider=provider) + events_before = load_rollout_events(rollout_event_log_path(cli_runtime_root, cli_goal_id)) + claim_payload = run_loopx_cli( + "--registry", + str(registry_path), + "--runtime-root", + str(cli_runtime_root), + "--format", + "json", + "todo", + "claim", + "--goal-id", + cli_goal_id, + "--todo-id", + "todo_auto_rollout", + "--claimed-by", + "codex-main-control", + ) + assert claim_payload["rollout_event"]["event_kind"] == "todo_claim", claim_payload + readback = run_loopx_cli( + "--registry", str(registry_path), "--runtime-root", str(cli_runtime_root), + "--format", "json", "todo", "list", "--goal-id", cli_goal_id, + "--todo-id", "todo_auto_rollout", + ) + assert readback["authority_read"]["source_authority"] == f"{provider}_v0", readback + assert readback["authority_read"]["legacy_fallback_used"] is False, readback + assert readback["todo"]["claimed_by"] == "codex-main-control", readback + + refresh_payload = run_loopx_cli( + "--registry", + str(registry_path), + "--runtime-root", + str(cli_runtime_root), + "--format", + "json", + "refresh-state", + "--goal-id", + cli_goal_id, + "--classification", + "automatic_rollout_logging_smoke", + "--delivery-batch-scale", + "implementation", + "--delivery-outcome", + "outcome_progress", + "--agent-id", + "codex-main-control", + "--agent-lane", + "smoke", + "--no-global-sync", + ) + assert refresh_payload["rollout_event"]["event_kind"] == "refresh_state", refresh_payload + + should_run_payload = run_loopx_cli( + "--registry", + str(registry_path), + "--runtime-root", + str(cli_runtime_root), + "--format", + "json", + "quota", + "should-run", + "--goal-id", + cli_goal_id, + "--agent-id", + "codex-main-control", + "--scan-root", + str(project), + "--limit", + "1", + allow_failure=True, + ) + assert should_run_payload["rollout_event"]["event_kind"] == ( + "quota_should_run" + ), should_run_payload + + auto_events = load_rollout_events(rollout_event_log_path(cli_runtime_root, cli_goal_id)) + assert auto_events[:len(events_before)] == events_before + auto_kinds = [event["event_kind"] for event in auto_events[len(events_before):]] + assert auto_kinds == [ + "todo_claim", + "refresh_state", + "quota_should_run", + ], auto_kinds + latest_event = auto_events[-1] + assert_boundary(latest_event) + auto_log_text = rollout_event_log_path(cli_runtime_root, cli_goal_id).read_text( + encoding="utf-8" + ) + assert_public_safe_text(auto_log_text) if __name__ == "__main__": main() diff --git a/examples/project/project-agent-adoption-smoke.py b/examples/project/project-agent-adoption-smoke.py index c5316418e2..482a144802 100644 --- a/examples/project/project-agent-adoption-smoke.py +++ b/examples/project/project-agent-adoption-smoke.py @@ -20,6 +20,11 @@ REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 + GOAL_ID = "adoption-main-control" USER_TODO = "Review the P0 owner-risk checklist before approving delivery." AGENT_TODO = "Run the read-only map dry-run after the operator approval is recorded." @@ -86,7 +91,7 @@ def write_planned_fixture(root: Path) -> Path: return registry_path -def run_cli(root: Path, registry_path: Path, *args: str) -> dict: +def run_cli(root: Path, registry_path: Path, *args: str, check: bool = True) -> dict: result = subprocess.run( [ sys.executable, @@ -101,7 +106,7 @@ def run_cli(root: Path, registry_path: Path, *args: str) -> dict: *args, ], cwd=REPO_ROOT, - check=True, + check=check, text=True, capture_output=True, ) @@ -135,8 +140,8 @@ def attention_item(status_payload: dict) -> dict: return items[0] -def main() -> int: - with tempfile.TemporaryDirectory(prefix="loopx-project-agent-adoption-") as tmp: +def assert_adoption(provider: str) -> None: + with tempfile.TemporaryDirectory(prefix=f"loopx-project-agent-adoption-{provider}-") as tmp: root = Path(tmp) registry_path = write_planned_fixture(root) project = root / "project" @@ -179,24 +184,31 @@ def main() -> int: assert first_quota["agent_todo_summary"]["open_count"] == 1, first_quota assert first_quota["selected_todo"]["text"] == AGENT_TODO, first_quota - todo_payload = run_cli( - root, - registry_path, - "todo", - "add", - "--goal-id", - GOAL_ID, - "--role", - "user", - "--task-class", - "user_gate", - "--global-gate", - "--text", - USER_TODO, + add_gate = ( + "todo", "add", "--goal-id", GOAL_ID, "--role", "user", + "--task-class", "user_gate", "--global-gate", "--text", USER_TODO, ) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + before_state, before_registry = state.read_bytes(), registry_path.read_bytes() + refused = run_cli(root, registry_path, *add_gate, check=False) + assert refused["ok"] is False, refused + assert refused["error_code"] == "canonical_authority_required", refused + assert state.read_bytes() == before_state + assert registry_path.read_bytes() == before_registry + import_fixture_authority(registry_path, GOAL_ID, provider=provider) + imported = run_cli(root, registry_path, "todo", "list", "--goal-id", GOAL_ID) + assert imported["authority_read"]["source_authority"] == f"{provider}_v0", imported + assert imported["authority_read"]["legacy_fallback_used"] is False, imported + assert len(imported["todos"]) == 1, imported + original = imported["todos"][0] + assert original["text"] == AGENT_TODO and original["status"] == "open", original + todo_payload = run_cli(root, registry_path, *add_gate) assert todo_payload["ok"] is True, todo_payload assert todo_payload["added"] is True, todo_payload - assert todo_payload["section"] == "User Todo / Owner Review Reading Queue", todo_payload + gate_id = todo_payload["todo_id"] + gate = run_cli(root, registry_path, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", gate_id)["todo"] + assert gate["text"] == USER_TODO and gate["global_gate"] is True, gate status_after_todo = run_cli(root, registry_path, "status", "--scan-root", str(project)) item_after_todo = attention_item(status_after_todo) @@ -267,6 +279,10 @@ def main() -> int: assert "不要从旧聊天或旧 packet 拼当前状态" in packet["project_agent_handoff"], packet assert "【用户本地 Gate 记录草稿】" not in packet["packet"], packet + +def main() -> int: + for provider in ("file", "sqlite"): + assert_adoption(provider) print("project-agent-adoption-smoke ok") return 0 From d7a988580a133b7c58fcb6d411f90ffc1466be42 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:18:49 +0800 Subject: [PATCH 54/98] fix: project governance leases from canonical authority Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- examples/visible-governance-slice-smoke.py | 52 +++++++++++++++++----- loopx/visible_governance.py | 34 +++++++------- 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/examples/visible-governance-slice-smoke.py b/examples/visible-governance-slice-smoke.py index d37c32b13d..27b80bc4af 100644 --- a/examples/visible-governance-slice-smoke.py +++ b/examples/visible-governance-slice-smoke.py @@ -18,9 +18,12 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.control_plane.testing.canary_harness import ( # noqa: E402 + import_fixture_authority, run_json_cli, write_fixture_registry, ) +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 +from loopx.control_plane.work_items.task_lease import task_lease_dir # noqa: E402 from loopx.quota import build_quota_should_run # noqa: E402 from loopx.status import collect_status # noqa: E402 from loopx.todos import ( # noqa: E402 @@ -152,7 +155,7 @@ def _add_user_action( return add_goal_todo(**kwargs) -def main() -> None: +def assert_governance_slice(provider: str) -> None: import tempfile print("=== Visible Governance Slice Smoke ===\n") @@ -160,6 +163,15 @@ def main() -> None: with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) registry, runtime, state = _write_fixture(tmp_path) + import_fixture_authority(registry, GOAL_ID, provider=provider) + # Select the existing lease policy while the imported tasks are still + # unclaimed. A soft claim never grants permission to acquire a lease. + mode = run_json_cli( + "handoff-mode", "set", "--goal-id", GOAL_ID, "--mode", "hard_lease", + "--operation-id", "governance-fixture-lease-policy", + registry_path=registry, runtime_root=runtime, + ) + assert mode["ok"] is True, mode # ── 1. Create agent todos with claims ── alpha_todo = _add_agent_todo( @@ -216,6 +228,16 @@ def main() -> None: ) assert lease_result["ok"] is True, f"lease acquire failed: {lease_result}" assert lease_result["acquired"] is True + canonical_before = read_canonical_todos_if_promoted( + runtime_root=runtime, goal_id=GOAL_ID, include_leases=True, + ) + assert canonical_before["leases"] == [lease_result["lease"]], canonical_before + # A stale legacy lease file must not inject a second canonical lease. + obsolete_dir = task_lease_dir(runtime_root=runtime, goal_id=GOAL_ID) + obsolete_dir.mkdir(parents=True, exist_ok=True) + (obsolete_dir / "todo_forged.json").write_text(json.dumps({ + **lease_result["lease"], "todo_id": "todo_forged", "owner": AGENT_BETA, + }), encoding="utf-8") print(f" [OK] Task lease acquired for alpha on {alpha_todo['todo_id']}") # ── 5. Collect status ── @@ -271,9 +293,13 @@ def main() -> None: # leases leases = gov_slice["leases"] assert isinstance(leases, dict) - assert leases["active_leases"] >= 1 - assert leases["current_agent_active_leases"] >= 1 - assert leases["nature"] == "optional runtime fence, not authority" + assert leases["active_leases"] == 1, leases + assert leases["current_agent_active_leases"] == 1, leases + assert leases["leases"][0]["todo_id"] == alpha_todo["todo_id"], leases + assert read_canonical_todos_if_promoted( + runtime_root=runtime, goal_id=GOAL_ID, include_leases=True, + ) == canonical_before + assert leases["nature"] == "policy-selected runtime fence, not write authority" for lease_entry in leases["leases"]: assert "todo_id" in lease_entry assert "owner" in lease_entry @@ -353,11 +379,11 @@ def main() -> None: # ── Negative: active lease is never write authority ── assert leases["active_leases"] >= 1 - assert leases["nature"] == "optional runtime fence, not authority" + assert leases["nature"] == "policy-selected runtime fence, not write authority" lease_blob = json.dumps(leases, sort_keys=True).lower() for forbidden in ( - "write authority", - "runtime authority", + "grants write authority", + "grants runtime authority", "browser write", "write api", ): @@ -366,7 +392,7 @@ def main() -> None: ) # The coverage-only claim_work reference must not imply lease authority. assert claim_work["shipped"] is False - assert "optional runtime fence" in leases["nature"] + assert "policy-selected runtime fence" in leases["nature"] print( " [OK] Negative: active leases remain fence-only " "(not write authority / no browser write API)" @@ -408,12 +434,12 @@ def main() -> None: assert GOAL_ID in markdown assert AGENT_ALPHA in markdown assert "Claims (Soft Ownership)" in markdown - assert "Task Leases (Optional Runtime Fence)" in markdown + assert "Task Leases (Policy-Selected Runtime Fence)" in markdown assert "Quota Decision" in markdown assert "Scheduler Hints" in markdown assert "Decision Scopes" in markdown assert "Authority Boundary" in markdown - assert "optional runtime fence, not authority" in markdown + assert "policy-selected runtime fence, not write authority" in markdown assert "Proposals shipped" in markdown assert "Stage-2" in markdown or "Stage 2" in markdown or "additive" in markdown assert "not write authority" in markdown.lower() or ( @@ -432,6 +458,7 @@ def main() -> None: registry_path=registry, goal_id=GOAL_ID, todo_id=gate["todo_id"], + role="user", agent_id=AGENT_ALPHA, decision_outcome="approve", ) @@ -473,5 +500,10 @@ def main() -> None: print("\n=== All smoke checks passed ===") +def main() -> None: + for provider in ("file", "sqlite"): + assert_governance_slice(provider) + + if __name__ == "__main__": main() diff --git a/loopx/visible_governance.py b/loopx/visible_governance.py index ca89e6c912..24a6ec52ac 100644 --- a/loopx/visible_governance.py +++ b/loopx/visible_governance.py @@ -23,7 +23,7 @@ # ── schema constants ──────────────────────────────────────────────────────── GOVERNANCE_SLICE_SCHEMA = "visible_governance_slice_v0" -LEASE_NATURE = "optional runtime fence, not authority" +LEASE_NATURE = "policy-selected runtime fence, not write authority" # ── authority-boundary static table ───────────────────────────────────────── @@ -41,7 +41,8 @@ def _build_authority_boundary_table() -> list[dict[str, Any]]: "shipped": True, "shipped_equivalent": ( "Promoted Goals use native TS commands and AuthorityStore transactions. " - "Unpromoted Goals retain the Markdown/lease adapters." + "Unpromoted sources remain readable for inspection and import; " + "normal mutations require canonical authority." ), "gap": "Implementation availability does not select the default provider or migrate this Goal.", }, @@ -205,6 +206,7 @@ def _build_leases_projection( read_lease, task_lease_dir, ) + from .control_plane.coordination.local_authority import read_canonical_todos_if_promoted runtime_root_raw = status_payload.get("runtime_root") if not runtime_root_raw: @@ -212,16 +214,18 @@ def _build_leases_projection( from pathlib import Path - lease_dir = task_lease_dir( - runtime_root=Path(str(runtime_root_raw)), - goal_id=goal_id, + runtime_root = Path(str(runtime_root_raw)) + canonical = read_canonical_todos_if_promoted( + runtime_root=runtime_root, goal_id=goal_id, include_leases=True, ) - if not lease_dir.exists(): - return {"active_leases": 0, "leases": [], "nature": LEASE_NATURE} + if canonical is not None: + leases = canonical["leases"] + else: + lease_dir = task_lease_dir(runtime_root=runtime_root, goal_id=goal_id) + leases = [read_lease(path) for path in sorted(lease_dir.glob("todo_*.json"))] active_leases: list[dict[str, Any]] = [] - for path in sorted(lease_dir.glob("todo_*.json")): - lease = read_lease(path) + for lease in leases: if not lease_is_active(lease): continue active_leases.append( @@ -478,7 +482,7 @@ def render_visible_governance_markdown(slice_payload: dict[str, Any]) -> str: # ── Leases ── leases = slice_payload.get("leases") if isinstance(leases, dict): - lines.append("## Task Leases (Optional Runtime Fence)") + lines.append("## Task Leases (Policy-Selected Runtime Fence)") lines.append("") lines.append(f"- Nature: *{leases.get('nature', LEASE_NATURE)}*") lines.append( @@ -600,11 +604,11 @@ def render_visible_governance_markdown(slice_payload: dict[str, Any]) -> str: f"{len(boundary) if boundary else 0}" ) lines.append( - "- **Key takeaway**: Stage-2 ships an additive coordination head, " - "file provider, and claim_work executor plus handoff_mode; default " - "runtime still uses Markdown soft claims, file-backed task leases " - "(optional fence, not write authority), and turn-scoped settlement. " - "Shared-mode promotion remains Stage 3." + "- **Key takeaway**: The original Stage-2 prototype is retired. " + "Canonical providers own normal Todo mutations and task leases. " + "soft_claim forbids lease acquisition; hard_lease enforces the lease " + "fence. Neither policy grants general write authority. " + "Installed adoption and cross-host qualification remain separate." ) lines.append("") From beecaeac1540e83d9fd1b63c3009c3ce1e66d4db Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:18:49 +0800 Subject: [PATCH 55/98] test: migrate custom CLI and Lark gate callers to canonical Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../custom-runtime-minimal-cli-turn-smoke.py | 55 +++++++++++++++++-- ...-goal-channel-human-gate-delivery-smoke.py | 16 +++++- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/examples/custom-runtime-minimal-cli-turn-smoke.py b/examples/custom-runtime-minimal-cli-turn-smoke.py index 76c73ba995..26b42791e2 100755 --- a/examples/custom-runtime-minimal-cli-turn-smoke.py +++ b/examples/custom-runtime-minimal-cli-turn-smoke.py @@ -24,6 +24,7 @@ GOAL_ID = "custom-runtime-min" AGENT_ID = "custom-host-agent" +TURN_ID = "custom-runtime-fixture-turn" MARKER_NAME = "public-marker.txt" MARKER_BODY = "ok-from-custom-runtime\n" ADVANCED_TURN_SMOKE = ( @@ -106,8 +107,10 @@ def write_project_fixture(root: Path) -> tuple[Path, Path, Path]: return project, registry_path, state_file -def assert_mainstream_cli_turn() -> None: +def assert_mainstream_cli_turn(provider: str) -> None: from loopx.status import parse_active_state_todos + from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted + from loopx.control_plane.testing.canary_harness import import_fixture_authority assert WAKE_TRIGGER, "host must own a wake trigger" assert "quota should-run" in REENTRY_INSTRUCTION @@ -116,6 +119,7 @@ def assert_mainstream_cli_turn() -> None: with tempfile.TemporaryDirectory(prefix="loopx-custom-runtime-min-") as tmp: project, registry_path, state_file = write_project_fixture(Path(tmp)) + import_fixture_authority(registry_path, GOAL_ID, provider=provider) status = run_cli(registry_path, "status", "--scan-root", str(project)) assert status.get("ok") is True, status @@ -160,6 +164,8 @@ def assert_mainstream_cli_turn() -> None: AGENT_ID, "--runtime-profile", "generic_cli", + "--todo-id", todo_id, + "--turn-instance-id", TURN_ID, "--scan-path", str(project), ) @@ -184,12 +190,13 @@ def assert_mainstream_cli_turn() -> None: AGENT_ID, "--agent-id", AGENT_ID, + "--turn-instance-id", TURN_ID, "--evidence", "public marker written under project root", - "--no-follow-up", ) assert completed.get("changed") is True, completed - assert completed.get("no_followup") is True, completed + assert completed["completion_continuation"] == "active_goal", completed + assert completed["successor_todo_ids"] == [], completed summary = parse_active_state_todos(state_file.read_text(encoding="utf-8")) item = next( @@ -198,12 +205,33 @@ def assert_mainstream_cli_turn() -> None: if row.get("todo_id") == todo_id ) assert item.get("done") is True, item + canonical = read_canonical_todos_if_promoted( + runtime_root=Path(tmp) / "runtime", goal_id=GOAL_ID, + ) + stored = next(row for row in canonical["todos"] if row["todo_id"] == todo_id) + assert stored["status"] == "done" and stored["claimed_by"] == AGENT_ID, stored refreshed = run_cli( registry_path, "refresh-state", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, + "--agent-lane", AGENT_ID, + "--progress-scope", "agent_lane", + "--todo-id", todo_id, + "--turn-instance-id", TURN_ID, + "--classification", "validated_progress", + "--delivery-batch-scale", "multi_surface", + "--delivery-outcome", "outcome_progress", + "--delivery-boundary", "in_flight_continuation", + "--progress-result-class", "advanced", + "--progress-surface-id", "custom-runtime-cli", + "--progress-work-item-id", todo_id, + "--progress-hypothesis-id", "marker-terminal-single-debit", + "--progress-probe-kind", "real-cli-artifact-readback", + "--progress-coverage-scope-id", "canonical-cli-turn", + "--progress-evidence-id", "artifact:public-marker", "--no-global-sync", ) assert refreshed.get("ok") is True, refreshed @@ -217,10 +245,12 @@ def assert_mainstream_cli_turn() -> None: GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", todo_id, + "--turn-instance-id", TURN_ID, "--slots", "1", "--source", - "controller", + "heartbeat", "--execute", "--scan-path", str(project), @@ -230,10 +260,25 @@ def assert_mainstream_cli_turn() -> None: after = spent["quota_event"]["after"] assert after["spent_slots"] == 1, spent assert after["allowed_slots"] == 5, spent + closed = run_cli( + registry_path, "todo", "complete", "--goal-id", GOAL_ID, + "--todo-id", todo_id, "--claimed-by", AGENT_ID, "--agent-id", AGENT_ID, + "--turn-instance-id", TURN_ID, + "--evidence", "public marker written under project root", "--no-follow-up", + ) + assert closed["completion_continuation"] == "no_followup", closed + replay = run_cli( + registry_path, "quota", "spend-slot", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", todo_id, + "--turn-instance-id", TURN_ID, "--slots", "1", "--source", "heartbeat", + "--execute", "--scan-path", str(project), + ) + assert replay["appended"] is False and replay["idempotent_replay"] is True, replay def main() -> int: - assert_mainstream_cli_turn() + for provider in ("file", "sqlite"): + assert_mainstream_cli_turn(provider) print("custom-runtime-minimal-cli-turn-smoke ok") return 0 diff --git a/examples/lark-goal-channel-human-gate-delivery-smoke.py b/examples/lark-goal-channel-human-gate-delivery-smoke.py index d2f4a344eb..3faffa02e5 100644 --- a/examples/lark-goal-channel-human-gate-delivery-smoke.py +++ b/examples/lark-goal-channel-human-gate-delivery-smoke.py @@ -13,6 +13,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from loopx.extensions.bundled import bundled_extension_manifest +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted +from loopx.control_plane.testing.canary_harness import import_fixture_authority from loopx.extensions.lark.goal_channel_contracts import ( GOAL_CHANNEL_BINDING_SCHEMA_VERSION, read_goal_channel_binding, @@ -296,12 +298,14 @@ def write_project(root: Path) -> tuple[Path, Path]: return registry_path, binding_path -def main() -> None: +def assert_gate_delivery(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-goal-channel-human-gate-") as raw: registry_path, binding_path = write_project(Path(raw)) bin_dir, fake_state_path = write_fake_lark_cli(Path(raw)) project = registry_path.parent.parent runtime_root = Path(raw) / "runtime" + import_fixture_authority(registry_path, GOAL_ID, provider=provider) + before = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) first_refresh = run_refresh( registry_path=registry_path, runtime_root=runtime_root, @@ -350,6 +354,11 @@ def main() -> None: == send_count ) assert gate_update["changed"] is True, gate_update + after = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) + gate = next(row for row in after["todos"] if row["todo_id"] == GATE_TODO_ID) + assert "after reviewing the diff" in gate["text"], gate + assert gate["status"] == "open" and gate["task_class"] == "user_gate", gate + assert after["provider_revision"] != before["provider_revision"] assert third["status"] == "sent_verified", third third_key = third["notification"]["idempotency_key"] assert third_key != first_key @@ -375,5 +384,10 @@ def main() -> None: print("lark-goal-channel-human-gate-delivery-smoke ok") +def main() -> None: + for provider in ("file", "sqlite"): + assert_gate_delivery(provider) + + if __name__ == "__main__": main() From c9be1562b927a8adc1dbdcc05d6f35669dad41f3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:43:49 +0800 Subject: [PATCH 56/98] refactor: retire isolated visible governance prototype Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/development/contributor-tasks-history.md | 2 +- examples/visible-governance-slice-smoke.py | 509 --------------- loopx/visible_governance.py | 615 ------------------ ...est_canonical_goal_channel_coordination.py | 70 ++ 4 files changed, 71 insertions(+), 1125 deletions(-) delete mode 100644 examples/visible-governance-slice-smoke.py delete mode 100644 loopx/visible_governance.py diff --git a/docs/development/contributor-tasks-history.md b/docs/development/contributor-tasks-history.md index 195bec1394..387e6cfe0f 100644 --- a/docs/development/contributor-tasks-history.md +++ b/docs/development/contributor-tasks-history.md @@ -58,7 +58,7 @@ points: | Review quality | PR review now requires scope-fit evidence for production surface changes (#3090), the execution contract carries four self-dev review lenses (#3123), example-only PRs need durable smoke-value evidence (#3134), age-fair exact-head scheduling persists across restarts (#3317), durable projection ACK is required (#3744), and disproportionate changes are gated (#3731). | Add synthetic conformance and negative cases around scope-fit evidence, causal chains, exact-head review packets, durable projection ACKs, and durable-smoke-value claims. | | Task leases | Typed task-lease CLI with preserved legacy error codes landed (#3095); on-disk hard leases surface in goal-channel projection (#3039); Turn fencing uses lease fences plus an append-only journal, and the OpenCode 2 goal worker fences its own live worker lease. A task-lease generation ABA fix shipped (#3393), a Pi `loopx_task_lease` facade over the shipped `task_lease_v0` CLI merged (#3559, closing #3549), task-lease settlement cut over to TypeScript (#3674), and native task-lease acquire cut over too (#3702). | Adopt the same facade in one more real host integration (for example TraeX) or add a transfer/overlap-write-scope fixture. Keep soft-claim routing and undeclared-lease authority unchanged. | | Status, quota, monitors | Replan context is host-projected from the evidence ledger; two equivalent typed progress observations create an obligation, maintenance writes fail closed against the same full goal-frontier reducer used by quota, and an exact runnable-successor Todo carries the obligation-bound semantic receipt and turn boundary. Heartbeat todos survive capability reentry (#3321), Todo identity filtering and UTC ordering are corrected (#3311), unbound `/loopx` sessions inherit existing agent identity (#3315), declared validation gates run on user-role done updates (#3291, #3293), quota guards follow the selected Todo (#3506), and a bounded fallback action portfolio keeps quota decisions actionable (#3514). Guided start is bound to one turn (#3572), stale generated Next Actions rebind to the current todo (#3524), unknown workspace causality is repairable (#3519), malformed runtime recovery and settlement payloads fail closed (#3525), managed and queued turn creation are serialized (#3542, #3562), and guided selection packet consumption is explicit (#3707). Post-v0.5.3, `todo list` gained a bounded thin projection with an output-budget cap (#3679), the dashboard projects completed Todos with done-count run progress (#3689), guided-todo onboarding delta coverage shipped (#3716), agent-lane Next Action selection prefers the selected Todo over shared prose (#3693), typed blocker outcomes settle (#3734), Turn receipt priority wins in compact output (#3726), and CLI common command owners load lazily (#3717). Manual evidence reads, prose ACKs, and historical repair-delta claims are diagnostic only. Compact scheduler-hint and heartbeat-prompt budgets plus todo-detail cold paths remain the reference. | Extend one measured performance, detail-readback, lock-timeout, malformed-state, typed progress, or obligation-bound semantic-transition case. Keep default output bounded and cold-path detail available. | -| Governance and productization | A synthetic visible-governance slice landed (#3086) and its Stage-2 proposal-vs-shipped refresh keeps additive `claim_work` / coordination-head truth plus a lease-as-fence negative explicit; decision-context evidence cursors settle (#3079); the React homepage rebuild (#3098) landed; a deterministic project registry (#3170) serializes global sync; per-goal handoff mode gates claim/lease authority (#3164) and hard-lease gates auto-acquire completion keys (#3198); a repository change-window gate and pending ledger ship (#3319); goal channels default to human-gate auto-notify on new channels (#3523); coordination state rules are centralized (#3410) and the Stage 2 slice ships an aggregate head, file provider, and `claim_work` executor (#3529); project repository delivery lands through capability hooks (#3570) with gitless delivery workspaces settled (#3574) and CPA/provider-routing qualification recorded (#3576, #3573, #3563); a read-only stride shadow observation M1 (#3207), a synthetic stride-boundary shadow fixture (#3290), and the hierarchical stride RFC (#3204) open the next boundary; goal-artifact lifecycle projection (#3136) proposes a read-model boundary and post-outcome memory utility attribution from RFC #3215 is implemented (#3280). Post-v0.5.3, periodic-report post-writeback hooks ship (#3691) with terminal Todo closeout dispatch (#3748), pending-intent consumption (#3749), Chinese-analysis editorial requirement (#3750), rejected-draft reopen (#3751), actual work interval (#3754), approved delivery bound to the Goal Channel Bot (#3755), and normalized card readback (#3757); the dashboard retains startup error context (#3745), keeps the default status source same-origin (#3732), and avoids an unbound lark-cli variable (#3738); doctor validates deep checks by install kind (#3736); Frontstage Pages PR validation is isolated (#3728); Luna ships as a bounded account-ring extension (#3737); and structured outbound mentions are verified (#3741). | Add one synthetic lifecycle or stride-boundary fixture, extend the visible-governance walkthrough with a missing negative case, or characterize the shipped `claim_work` executor with a file-backed parity fixture. Keep activation explicit and leave source bodies, draft bodies, review text, provider payloads, private locators, cursor state, and apply/publish authority outside public fixtures. | +| Governance and productization | A synthetic visible-governance slice landed (#3086) and received a Stage-2 refresh (#3657); the isolated prototype and its sole smoke are now retired in the 2.0 candidate, while canonical Goal Channel/status ownership and lease coverage remain with their existing typed owner; decision-context evidence cursors settle (#3079); the React homepage rebuild (#3098) landed; a deterministic project registry (#3170) serializes global sync; per-goal handoff mode gates claim/lease authority (#3164) and hard-lease gates auto-acquire completion keys (#3198); a repository change-window gate and pending ledger ship (#3319); goal channels default to human-gate auto-notify on new channels (#3523); coordination state rules are centralized (#3410) and the Stage 2 slice ships an aggregate head, file provider, and `claim_work` executor (#3529); project repository delivery lands through capability hooks (#3570) with gitless delivery workspaces settled (#3574) and CPA/provider-routing qualification recorded (#3576, #3573, #3563); a read-only stride shadow observation M1 (#3207), a synthetic stride-boundary shadow fixture (#3290), and the hierarchical stride RFC (#3204) open the next boundary; goal-artifact lifecycle projection (#3136) proposes a read-model boundary and post-outcome memory utility attribution from RFC #3215 is implemented (#3280). Post-v0.5.3, periodic-report post-writeback hooks ship (#3691) with terminal Todo closeout dispatch (#3748), pending-intent consumption (#3749), Chinese-analysis editorial requirement (#3750), rejected-draft reopen (#3751), actual work interval (#3754), approved delivery bound to the Goal Channel Bot (#3755), and normalized card readback (#3757); the dashboard retains startup error context (#3745), keeps the default status source same-origin (#3732), and avoids an unbound lark-cli variable (#3738); doctor validates deep checks by install kind (#3736); Frontstage Pages PR validation is isolated (#3728); Luna ships as a bounded account-ring extension (#3737); and structured outbound mentions are verified (#3741). | Extend the existing canonical Goal Channel/status owner only for demonstrated gaps; retain lease, unavailable-provider and recovery coverage in the existing canonical coordination and task-lease tests. Do not restore the retired visible-governance or `claim_work` prototype. Keep activation explicit and leave source bodies, draft bodies, review text, provider payloads, private locators, cursor state, and apply/publish authority outside public fixtures. | | Security boundaries | Four merged hardening fixes contain state-file override writes (#3140), reject shell metacharacters in launcher worker commands (#3139), validate `goal_id` in reward routes (#3138), and stop ACAO:* on unauthenticated status reads (#3137). Loopback CORS, worker-command charset, and state-file symlink containment are pinned as regression tests (#3340). GH-C90 audited the four shipped negative fixtures and added one durable mutation per boundary: path-prefix sibling containment, worker-command input redirect, absolute `goal_id` path segments, and `file://localhost` ACAO rejection (`tests/test_state_file_containment.py`, `tests/test_worker_command_validation.py`, `tests/test_feedback_goal_id_validation.py`, `tests/test_status_server_cors.py`), completing the GH-C90 slice (#3655, closing #3636). | Keep credentials, private reproduction details, and advisory coordination out of public fixtures; extend only when a new shipped boundary lacks a distinct fail-closed mutation. | | Runtime connectors and content workflows | DeepSeek Harness Turn adapter with real e2e smoke landed (#3188); OpenCode 1 and OpenCode 2 continuous goal loops ship (#3151); content-ops gained a layout template library and dense-cover defaults (#3222, #3223); provider-neutral PR program snapshots ship (#2814); `computer_use_runtime_v0` is now a machine-checkable protocol contract (#3279); a `loopx-community-discussion` public source provider (#3299) and a `loopx-repo-health` provider (#3272) extend public source coverage; goal-channel botmux runtime integration lands with terminal dispatches preserved and uncertain dispatches persisted, desktop chat routes through a loopback service, chat-wide routing and contextual inbox replies land (#3555), and managed and queued turn creation are serialized (#3542, #3562); the desktop added workspace language settings, localized write previews, and preserved heartbeat schedule and off-hours semantics (#3594); host parity, skill-delivery, and observable-handle thin pytest now cover the expanded host list. Post-v0.5.3, bot document comment scopes read from the published app (#3722), turn-start reaction replay is bounded (#3733), and the DSH plugin is one-step ready (#3725). | Add one provider-neutral parity slice for DeepSeek Harness, OpenCode 1/2, goal-channel botmux, content-ops delivery/readback, or desktop locale parity; keep raw transcripts, provider payloads, credentials, and host-local paths out of fixtures. | | Benchmark boundary | Benchmark research was reset around native runners (#3267): native Codex Goal connects to the real runtime (#3271), workers are isolated from the host (#3277), provider env binds to installed Goal profiles (#3276) with formally verified treatments (#3275) and live continuations (#3273), runtime evidence binds to the exact container (#3303), a post-run case analyst brief ships (#3289), an integrity qualification toolkit landed (#3241), source-env usage is distinguished from credential probes (#3298, #3278), and accountable closeout requires Todo validation (#3229). A provider-neutral four-arm study contract ships (#3516), alongside restricted task-source access (#3504), non-http git clones classified as network with loopback integrity probes allowed (#3510), namespaced public case ids (#3532), locked git-clone integrity boundaries (#3515), orchestrator runtime provenance, and fail-closed runtime closeout drift (#3503). Deterministic fixtures already cover GH-C99 without live scoring or uploads: `tests/capabilities/test_benchmark_four_arm_contract.py`, `tests/capabilities/test_benchmark_experiment_board.py` (orchestrator runtime provenance negatives), `tests/capabilities/test_benchmark_runtime_continuity.py` (fail-closed closeout drift), and integrity/network/task-source cases in `tests/capabilities/test_benchmark_toolkit.py`; the covered GH-C99 fixture task was retired (#3653). Public native Goal trajectory summaries now derive from compact lifecycle facts without retaining raw artifacts (#3327), completing the GH-C16 slice. Post-v0.5.3, an external agent phase ships (#3565), restricted-access suspicion adjudicates by causal use (#3747), the plan-role fidelity gate is removed (#3753), and retired fidelity references are cleaned up (#3756). Shared lifecycle, readiness, ledger, and reducer contracts remain the public seam; generic Effect Program conformance and replay tests harden settlement infrastructure but do not change benchmark scoring or authorize live runs. Live scored comparisons stay held until a fresh task-free runner lifecycle receipt proves readiness. | Extend synthetic setup/termination attribution, derive the public trajectory summary for a second non-SkillsBench adapter, or add a SWE adapter only when a second SWE route needs shared launch/observe/ingest behavior. Do not launch scoring, duplicate the controller, or expose raw task text, logs, trajectories, verifier tails, credentials, uploads, or local paths. | diff --git a/examples/visible-governance-slice-smoke.py b/examples/visible-governance-slice-smoke.py deleted file mode 100644 index 27b80bc4af..0000000000 --- a/examples/visible-governance-slice-smoke.py +++ /dev/null @@ -1,509 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke-test the ``visible_governance_slice_v0`` governance projection. - -Builds a synthetic multi-agent fixture with soft claims, a task lease, -decision scopes, and a user gate, then projects the full governance slice -and validates every section. All assertions are public-safe -- no raw -paths, credentials, or private payloads. -""" - -from __future__ import annotations - -import json -import sys -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[1] -if str(REPO_ROOT) not in sys.path: - sys.path.insert(0, str(REPO_ROOT)) - -from loopx.control_plane.testing.canary_harness import ( # noqa: E402 - import_fixture_authority, - run_json_cli, - write_fixture_registry, -) -from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 -from loopx.control_plane.work_items.task_lease import task_lease_dir # noqa: E402 -from loopx.quota import build_quota_should_run # noqa: E402 -from loopx.status import collect_status # noqa: E402 -from loopx.todos import ( # noqa: E402 - add_goal_todo, - complete_goal_todo, - list_goal_todos, -) -from loopx.visible_governance import ( # noqa: E402 - build_visible_governance_slice, - render_visible_governance_markdown, -) - - -GOAL_ID = "governance-slice-smoke" -AGENT_ALPHA = "codex-alpha" -AGENT_BETA = "codex-beta" - - -def _assert_public_safe_payload(payload: dict, *, label: str) -> None: - """Verify a payload contains no raw paths, URLs, or credential markers.""" - text = json.dumps(payload, sort_keys=True, ensure_ascii=False) - assert "/Users/" not in text, f"{label}: absolute path leak" - assert "https://" not in text, f"{label}: URL leak" - assert "pass" + "word" not in text.lower(), f"{label}: credential leak" - assert "api_" + "key" not in text.lower(), f"{label}: API key leak" - assert "C:\\" not in text, f"{label}: Windows path leak" - - -def _write_fixture(tmp_path: Path) -> tuple[Path, Path, Path]: - """Create a multi-agent fixture with claims, gates, and a task lease.""" - project = tmp_path / "project" - runtime = tmp_path / "runtime" - registry = project / ".loopx" / "registry.json" - state = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" - state.parent.mkdir(parents=True) - state.write_text( - "---\nstatus: active\n---\n\n" - "# Active Goal State\n\n" - "## Agent Todo\n\n" - "- [ ] Alpha advancement task\n" - "- [ ] Beta advancement task\n\n" - "## User Todo\n\n" - "- [ ] Review and approve the release scope\n", - encoding="utf-8", - ) - write_fixture_registry( - project=project, - runtime_root=runtime, - registry_path=registry, - goal_id=GOAL_ID, - domain="loopx-platform", - adapter_kind="harness_self_improvement", - registered_agents=[AGENT_ALPHA, AGENT_BETA], - quota_allowed_slots=10, - ) - return registry, runtime, state - - -def _add_agent_todo( - registry: Path, - *, - text: str, - claimed_by: str, - task_class: str = "advancement_task", - required_decision_scopes: list[str] | None = None, -) -> dict: - """Add an agent todo, optionally with required decision scopes. - - Scopes use the ``kind:granularity:scope_key`` string format - (e.g. ``"release:action:governance-bridge"``). - """ - kwargs: dict = { - "registry_path": registry, - "goal_id": GOAL_ID, - "role": "agent", - "text": text, - "task_class": task_class, - "claimed_by": claimed_by, - } - if required_decision_scopes: - kwargs["required_decision_scopes"] = required_decision_scopes - return add_goal_todo(**kwargs) - - -def _add_user_gate( - registry: Path, - *, - text: str, - blocks_agent: str | None = None, - unblocks_todo_id: str | None = None, - decision_scope: str | None = None, -) -> dict: - """Add a user gate, optionally linked to an agent todo or scope. - - ``decision_scope`` uses the ``kind:granularity:scope_key`` string format. - """ - kwargs: dict = { - "registry_path": registry, - "goal_id": GOAL_ID, - "role": "user", - "text": text, - "task_class": "user_gate", - } - if blocks_agent: - kwargs["blocks_agent"] = blocks_agent - if unblocks_todo_id: - kwargs["unblocks_todo_id"] = unblocks_todo_id - if decision_scope: - kwargs["decision_scope"] = decision_scope - return add_goal_todo(**kwargs) - - -def _add_user_action( - registry: Path, - *, - text: str, - bound_agent: str | None = None, -) -> dict: - """Add a non-blocking user action.""" - kwargs: dict = { - "registry_path": registry, - "goal_id": GOAL_ID, - "role": "user", - "text": text, - "task_class": "user_action", - } - if bound_agent: - kwargs["bound_agent"] = bound_agent - return add_goal_todo(**kwargs) - - -def assert_governance_slice(provider: str) -> None: - import tempfile - - print("=== Visible Governance Slice Smoke ===\n") - - with tempfile.TemporaryDirectory() as tmp: - tmp_path = Path(tmp) - registry, runtime, state = _write_fixture(tmp_path) - import_fixture_authority(registry, GOAL_ID, provider=provider) - # Select the existing lease policy while the imported tasks are still - # unclaimed. A soft claim never grants permission to acquire a lease. - mode = run_json_cli( - "handoff-mode", "set", "--goal-id", GOAL_ID, "--mode", "hard_lease", - "--operation-id", "governance-fixture-lease-policy", - registry_path=registry, runtime_root=runtime, - ) - assert mode["ok"] is True, mode - - # ── 1. Create agent todos with claims ── - alpha_todo = _add_agent_todo( - registry, - text="Alpha builds the core governance bridge.", - claimed_by=AGENT_ALPHA, - required_decision_scopes=["direction:action:governance-bridge"], - ) - beta_todo = _add_agent_todo( - registry, - text="Beta reviews the integration surface.", - claimed_by=AGENT_BETA, - ) - print( - f" [OK] Agent todos: alpha={alpha_todo['todo_id']}, " - f"beta={beta_todo['todo_id']}" - ) - - # ── 2. Create a user gate that covers alpha's required scope ── - gate = _add_user_gate( - registry, - text="Approve the governance bridge release scope.", - blocks_agent=AGENT_ALPHA, - decision_scope="direction:goal:*", - ) - print(f" [OK] User gate: {gate['todo_id']} (goal-scoped, blocks alpha)") - - # ── 3. Create a non-blocking user action ── - action = _add_user_action( - registry, - text="Review the dashboard budget projection.", - bound_agent=AGENT_ALPHA, - ) - print(f" [OK] User action: {action['todo_id']}") - - # ── 4. Acquire a task lease for alpha ── - lease_result = run_json_cli( - "task-lease", - "acquire", - "--goal-id", - GOAL_ID, - "--todo-id", - alpha_todo["todo_id"], - "--owner", - AGENT_ALPHA, - "--idempotency-key", - "smoke-lease-key-001", - "--ttl-seconds", - "2700", - "--write-scope", - "governance-bridge/*", - registry_path=registry, - runtime_root=runtime, - ) - assert lease_result["ok"] is True, f"lease acquire failed: {lease_result}" - assert lease_result["acquired"] is True - canonical_before = read_canonical_todos_if_promoted( - runtime_root=runtime, goal_id=GOAL_ID, include_leases=True, - ) - assert canonical_before["leases"] == [lease_result["lease"]], canonical_before - # A stale legacy lease file must not inject a second canonical lease. - obsolete_dir = task_lease_dir(runtime_root=runtime, goal_id=GOAL_ID) - obsolete_dir.mkdir(parents=True, exist_ok=True) - (obsolete_dir / "todo_forged.json").write_text(json.dumps({ - **lease_result["lease"], "todo_id": "todo_forged", "owner": AGENT_BETA, - }), encoding="utf-8") - print(f" [OK] Task lease acquired for alpha on {alpha_todo['todo_id']}") - - # ── 5. Collect status ── - status_payload = collect_status( - registry_path=registry, - runtime_root_override=str(runtime), - scan_roots=[Path(__file__).resolve()], - limit=20, - ) - assert isinstance(status_payload, dict) - print(" [OK] Status collected") - - # ── 6. Build quota should_run for alpha ── - decision = build_quota_should_run( - status_payload, - goal_id=GOAL_ID, - agent_id=AGENT_ALPHA, - ) - assert isinstance(decision, dict) - assert "effective_action" in decision - print(f" [OK] Quota should_run built: effective={decision['effective_action']}") - - # ── 7. Build visible governance slice ── - gov_slice = build_visible_governance_slice( - status_payload, - goal_id=GOAL_ID, - agent_id=AGENT_ALPHA, - include_task_leases=True, - ) - assert gov_slice["ok"] is True, f"slice not ok: {gov_slice}" - assert gov_slice["schema_version"] == "visible_governance_slice_v0" - assert gov_slice["goal_id"] == GOAL_ID - assert gov_slice["agent_id"] == AGENT_ALPHA - - # ── 8. Validate every section ── - - # claims - claims = gov_slice["claims"] - assert isinstance(claims, dict) - assert claims["total_claims"] >= 2, f"expected >= 2 claims, got {claims}" - assert claims["claiming_agent_count"] >= 2 - assert claims["current_agent_claims"] >= 1 - assert AGENT_ALPHA in claims["by_agent"] - assert AGENT_BETA in claims["by_agent"] - for c in claims["claims"]: - assert "todo_id" in c - assert "claimed_by" in c - print( - f" [OK] Claims: {claims['total_claims']} claims " - f"across {claims['claiming_agent_count']} agents" - ) - - # leases - leases = gov_slice["leases"] - assert isinstance(leases, dict) - assert leases["active_leases"] == 1, leases - assert leases["current_agent_active_leases"] == 1, leases - assert leases["leases"][0]["todo_id"] == alpha_todo["todo_id"], leases - assert read_canonical_todos_if_promoted( - runtime_root=runtime, goal_id=GOAL_ID, include_leases=True, - ) == canonical_before - assert leases["nature"] == "policy-selected runtime fence, not write authority" - for lease_entry in leases["leases"]: - assert "todo_id" in lease_entry - assert "owner" in lease_entry - assert "expires_at" in lease_entry - print( - f" [OK] Leases: {leases['active_leases']} active, " - f"nature='{leases['nature']}'" - ) - - # quota - quota = gov_slice["quota"] - assert isinstance(quota, dict) - for key in ("should_run", "effective_action", "state"): - assert key in quota, f"quota missing {key}" - print(f" [OK] Quota: should_run={quota['should_run']}, action={quota['effective_action']}") - - # scheduler hints - sched = gov_slice["scheduler_hints"] - if sched is not None: - assert isinstance(sched, dict) - for key in ("action", "cadence_class", "arbitration"): - assert key in sched, f"scheduler_hints missing {key}" - print(f" [OK] Scheduler hints: cadence={sched['cadence_class']}") - else: - print(" [OK] Scheduler hints: None (no agent-lane context)") - - # decision scopes - scopes = gov_slice["decision_scopes"] - assert isinstance(scopes, dict) - consistency = scopes["required_scope_consistency"] - assert isinstance(consistency, dict) - assert "status" in consistency - print( - f" [OK] Decision scopes: consistency={consistency['status']}, " - f"checked={consistency['checked_agent_todo_count']} todos" - ) - - # authority boundary (implementation availability, not per-Goal promotion) - boundary = gov_slice["authority_boundary"] - assert isinstance(boundary, list) - assert len(boundary) >= 5, f"expected >= 5 RFC entries, got {len(boundary)}" - expected_shipped = { - "3 -- State Classification": True, - "4 -- Coordination Ledger Shape": False, - "5.1 -- claim_work command": False, - "7 -- Receipt Retention": True, - "8 -- Local vs Shared Mode": True, - "Appendix B -- handoff_mode": True, - "9 -- Offline/Local Boundaries": False, - } - by_section = {entry["rfc_section"]: entry for entry in boundary} - for section, expect_shipped in expected_shipped.items(): - assert section in by_section, f"missing RFC section: {section}" - entry = by_section[section] - assert "proposal" in entry - assert "shipped_equivalent" in entry - assert "gap" in entry - assert entry["shipped"] is expect_shipped, ( - f"{section}: shipped={entry['shipped']!r}, " - f"expected {expect_shipped} after native prototype retirement" - ) - claim_work = by_section["5.1 -- claim_work command"] - claim_text = ( - f"{claim_work['shipped_equivalent']} {claim_work['gap']}".lower() - ) - assert "retired" in claim_text and "todo_claim.ts" in claim_text - assert "not current execution authority" in claim_text - shipped_count = sum(1 for e in boundary if e["shipped"]) - assert shipped_count == sum( - 1 for value in expected_shipped.values() if value - ), f"unexpected shipped count: {shipped_count}" - print( - f" [OK] Authority boundary: {len(boundary)} RFC sections, " - f"{shipped_count} implemented, " - f"{len(boundary) - shipped_count} original proposals not shipped" - ) - - # ── Negative: active lease is never write authority ── - assert leases["active_leases"] >= 1 - assert leases["nature"] == "policy-selected runtime fence, not write authority" - lease_blob = json.dumps(leases, sort_keys=True).lower() - for forbidden in ( - "grants write authority", - "grants runtime authority", - "browser write", - "write api", - ): - assert forbidden not in lease_blob, ( - f"leases projection must not present '{forbidden}'" - ) - # The coverage-only claim_work reference must not imply lease authority. - assert claim_work["shipped"] is False - assert "policy-selected runtime fence" in leases["nature"] - print( - " [OK] Negative: active leases remain fence-only " - "(not write authority / no browser write API)" - ) - - # ── 9. Public safety scan ── - _assert_public_safe_payload(gov_slice, label="governance-slice") - print(" [OK] Public safety scan passes") - - # ── 10. include_task_leases=False ── - no_lease_slice = build_visible_governance_slice( - status_payload, - goal_id=GOAL_ID, - agent_id=AGENT_ALPHA, - include_task_leases=False, - ) - assert no_lease_slice["leases"] is None - print(" [OK] include_task_leases=False omits leases section") - - # ── 11. Missing goal_id ── - bad_slice = build_visible_governance_slice(status_payload, goal_id="") - assert bad_slice["ok"] is False - assert "error" in bad_slice - print(" [OK] Empty goal_id returns error") - - # ── 12. agent_id=None works ── - no_agent_slice = build_visible_governance_slice( - status_payload, - goal_id=GOAL_ID, - agent_id=None, - ) - assert no_agent_slice["ok"] is True - assert no_agent_slice["agent_id"] is None - print(" [OK] agent_id=None produces valid slice") - - # ── 13. Markdown rendering ── - markdown = render_visible_governance_markdown(gov_slice) - assert "Visible Governance Slice" in markdown - assert GOAL_ID in markdown - assert AGENT_ALPHA in markdown - assert "Claims (Soft Ownership)" in markdown - assert "Task Leases (Policy-Selected Runtime Fence)" in markdown - assert "Quota Decision" in markdown - assert "Scheduler Hints" in markdown - assert "Decision Scopes" in markdown - assert "Authority Boundary" in markdown - assert "policy-selected runtime fence, not write authority" in markdown - assert "Proposals shipped" in markdown - assert "Stage-2" in markdown or "Stage 2" in markdown or "additive" in markdown - assert "not write authority" in markdown.lower() or ( - "optional fence, not write authority" in markdown.lower() - ) - # Verify no private markers in rendered markdown - assert "/Users/" not in markdown - assert "https://" not in markdown - assert "C:\\" not in markdown - print(" [OK] Markdown render includes all sections") - print(" [OK] Markdown is public-safe") - print(" [OK] Markdown keeps Stage-2 / lease-authority boundary explicit") - - # ── 14. Complete the gate and verify standing authority ── - complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=gate["todo_id"], - role="user", - agent_id=AGENT_ALPHA, - decision_outcome="approve", - ) - status2 = collect_status( - registry_path=registry, - runtime_root_override=str(runtime), - scan_roots=[Path(__file__).resolve()], - limit=20, - ) - gov_slice2 = build_visible_governance_slice( - status2, - goal_id=GOAL_ID, - agent_id=AGENT_ALPHA, - ) - standing = gov_slice2["decision_scopes"]["standing_decision_authority"] - if standing is not None: - assert standing["active_count"] >= 1 - print( - f" [OK] Standing authority after gate approval: " - f"{standing['active_count']} active receipts" - ) - else: - print(" [OK] Standing authority: None (gate scoped to different items)") - - # ── 15. Verify the slice is read-only (no mutations in builder) ── - todos_before = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) - _recheck = build_visible_governance_slice( - status2, - goal_id=GOAL_ID, - agent_id=AGENT_BETA, - include_task_leases=True, - ) - todos_after = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) - assert todos_before == todos_after, ( - "build_visible_governance_slice must not mutate todos" - ) - print(" [OK] Slice builder is read-only (no mutations)") - - print("\n=== All smoke checks passed ===") - - -def main() -> None: - for provider in ("file", "sqlite"): - assert_governance_slice(provider) - - -if __name__ == "__main__": - main() diff --git a/loopx/visible_governance.py b/loopx/visible_governance.py deleted file mode 100644 index 24a6ec52ac..0000000000 --- a/loopx/visible_governance.py +++ /dev/null @@ -1,615 +0,0 @@ -"""Synthetic visible-governance slice for GH-C62. - -Projects per-goal/per-agent claims, optional task leases, quota, scheduler -hints, decision scopes, and the shared-goal RFC's proposed authority/state- -provider boundary into one read-only projection. Every section makes -proposal-versus-shipped-truth explicit; no write API, no inference from -prose, no presentation of provider observations or leases as authority. -""" - -from __future__ import annotations - -from typing import Any - -from .control_plane.quota.decision_summary import compact_quota_decision -from .control_plane.todos.decision_scope import ( - build_required_decision_scope_consistency, -) -from .control_plane.todos.contract import ( - normalize_todo_claimed_by, - normalize_todo_id, -) - - -# ── schema constants ──────────────────────────────────────────────────────── -GOVERNANCE_SLICE_SCHEMA = "visible_governance_slice_v0" -LEASE_NATURE = "policy-selected runtime fence, not write authority" - - -# ── authority-boundary static table ───────────────────────────────────────── -def _build_authority_boundary_table() -> list[dict[str, Any]]: - """Implementation availability, not a claim that this Goal was promoted. - - Keep the original RFC section identifiers for report consumers. The original - prototype commands/formats are distinct from their shipped native successors. - Per-Goal authority must still be read from its actual binding and writer fence. - """ - return [ - { - "rfc_section": "3 -- State Classification", - "proposal": "Shared canonical Todo lifecycle, claims, leases and operation receipts", - "shipped": True, - "shipped_equivalent": ( - "Promoted Goals use native TS commands and AuthorityStore transactions. " - "Unpromoted sources remain readable for inspection and import; " - "normal mutations require canonical authority." - ), - "gap": "Implementation availability does not select the default provider or migrate this Goal.", - }, - { - "rfc_section": "4 -- Coordination Ledger Shape", - "proposal": "Original coordination head aggregate with an embedded receipt index", - "shipped": False, - "shipped_equivalent": ( - "The Python prototype is retired. Native File/SQLite journals retain " - "head, committed history and receipts through one AuthorityStore contract. " - "PostgreSQL and NoKV have separate candidate qualification." - ), - "gap": "The retired prototype head is not a supported Goal journal or migration source.", - }, - { - "rfc_section": "5.1 -- claim_work command", - "proposal": "Atomic claim, lease and receipt publication", - "shipped": False, - "shipped_equivalent": ( - "The experimental claim_work executor is retired. Native todo_claim.ts " - "owns atomic claim/lease acquisition on canonical providers." - ), - "gap": ( - "A historical receipt is not current execution authority. Lease/effect " - "checks and the actual Goal binding remain required; claim_work is not a public command." - ), - }, - { - "rfc_section": "7 -- Receipt Retention", - "proposal": "Retain original receipts and fail closed on unproved outcomes", - "shipped": True, - "shipped_equivalent": "Native AuthorityStore readReceipt and committed history preserve replay evidence.", - "gap": "Receipt correctness does not qualify unbounded capacity or the SQLite D2 profile.", - }, - { - "rfc_section": "8 -- Local vs Shared Mode", - "proposal": "Explicit provider binding, writer fencing and migration parity", - "shipped": True, - "shipped_equivalent": "Reviewed per-Goal promotion and fenced rollback are implemented.", - "gap": "Whole-Goal cohort qualification and default onboarding remain separate from per-Goal opt-in.", - }, - { - "rfc_section": "Appendix B -- handoff_mode", - "proposal": "Explicit legacy, soft_claim or hard_lease coordination mode", - "shipped": True, - "shipped_equivalent": "The native handoff-mode command owns transition admission and quiescence checks.", - "gap": "Provider selection does not silently replace the Goal's chosen handoff mode.", - }, - { - "rfc_section": "9 -- Offline/Local Boundaries", - "proposal": "Unavailable shared authority must not silently fall back to local writers", - "shipped": False, - "shipped_equivalent": "Promoted local providers fail closed when canonical authority cannot be read.", - "gap": "Deployed cross-host service availability and execution-interval fencing need separate qualification.", - }, - ] - - -# ── claims projection ─────────────────────────────────────────────────────── -def _build_claims_projection( - status_payload: dict[str, Any], - *, - goal_id: str, - agent_id: str | None, -) -> dict[str, Any]: - """Extract per-todo soft claims from the status attention queue. - - Looks inside ``agent_todos`` summary lists (``first_open_items``, - ``first_executable_items``) within each goal-level attention queue - item to find todos with a ``claimed_by`` field. - """ - attention = ( - status_payload.get("attention_queue") - if isinstance(status_payload.get("attention_queue"), dict) - else {} - ) - items = attention.get("items") if isinstance(attention.get("items"), list) else [] - - claims: list[dict[str, Any]] = [] - by_agent: dict[str, list[dict[str, Any]]] = {} - seen: set[str] = set() - - _SUMMARY_LIST_KEYS = ( - "first_open_items", - "first_executable_items", - "executable_backlog_items", - "backlog_items", - ) - - for item in items: - if not isinstance(item, dict): - continue - item_goal = str(item.get("goal_id") or "") - if item_goal and item_goal != goal_id: - continue - - agent_todos = ( - item.get("agent_todos") - if isinstance(item.get("agent_todos"), dict) - else {} - ) - for list_key in _SUMMARY_LIST_KEYS: - todo_list = agent_todos.get(list_key) - if not isinstance(todo_list, list): - continue - for td in todo_list: - if not isinstance(td, dict): - continue - todo_id = normalize_todo_id(td.get("todo_id")) - claimed_by = normalize_todo_claimed_by(td.get("claimed_by")) - if not claimed_by: - continue - if todo_id in seen: - continue - seen.add(todo_id) - claim_entry: dict[str, Any] = { - "todo_id": todo_id, - "claimed_by": claimed_by, - "task_class": td.get("task_class"), - "status": td.get("status") or ( - "done" if td.get("done") else "open" - ), - } - for key in ("text", "target_key", "action_kind"): - value = td.get(key) - if value: - claim_entry[key] = str(value)[:200] - claims.append(claim_entry) - by_agent.setdefault(claimed_by, []).append(claim_entry) - - normalized_agent = normalize_todo_claimed_by(agent_id) - return { - "total_claims": len(claims), - "claiming_agent_count": len(by_agent), - "current_agent_claims": len(by_agent.get(normalized_agent, [])) - if normalized_agent - else 0, - "by_agent": { - agent: len(agent_claims) - for agent, agent_claims in sorted(by_agent.items()) - }, - "claims": claims, - } - - -# ── leases projection ─────────────────────────────────────────────────────── -def _build_leases_projection( - status_payload: dict[str, Any], - *, - goal_id: str, - agent_id: str | None, - include_task_leases: bool, -) -> dict[str, Any] | None: - """Read active task leases for the goal (read-only, never writes).""" - if not include_task_leases: - return None - - from .control_plane.work_items.task_lease import ( - lease_epoch, - lease_is_active, - read_lease, - task_lease_dir, - ) - from .control_plane.coordination.local_authority import read_canonical_todos_if_promoted - - runtime_root_raw = status_payload.get("runtime_root") - if not runtime_root_raw: - return {"active_leases": 0, "leases": [], "nature": LEASE_NATURE} - - from pathlib import Path - - runtime_root = Path(str(runtime_root_raw)) - canonical = read_canonical_todos_if_promoted( - runtime_root=runtime_root, goal_id=goal_id, include_leases=True, - ) - if canonical is not None: - leases = canonical["leases"] - else: - lease_dir = task_lease_dir(runtime_root=runtime_root, goal_id=goal_id) - leases = [read_lease(path) for path in sorted(lease_dir.glob("todo_*.json"))] - - active_leases: list[dict[str, Any]] = [] - for lease in leases: - if not lease_is_active(lease): - continue - active_leases.append( - { - "todo_id": lease.get("todo_id"), - "owner": lease.get("owner"), - "expires_at": lease.get("expires_at"), - "version": lease.get("version"), - "lease_epoch": lease_epoch(lease), - "write_scopes": lease.get("write_scopes") or [], - "acquire_ttl_seconds": lease.get("acquire_ttl_seconds"), - } - ) - - normalized_agent = normalize_todo_claimed_by(agent_id) - return { - "active_leases": len(active_leases), - "current_agent_active_leases": sum( - 1 - for lease in active_leases - if lease.get("owner") == normalized_agent - ), - "leases": active_leases, - "nature": LEASE_NATURE, - } - - -# ── quota projection ──────────────────────────────────────────────────────── -def _build_quota_projection( - status_payload: dict[str, Any], - *, - goal_id: str, - agent_id: str | None, -) -> dict[str, Any] | None: - """Compact quota decision via ``build_quota_should_run``.""" - from .quota import build_quota_should_run - - try: - decision = build_quota_should_run( - status_payload, - goal_id=goal_id, - agent_id=agent_id, - ) - except (ValueError, KeyError, OSError): - return None - return compact_quota_decision(decision) - - -# ── scheduler hints projection ────────────────────────────────────────────── -def _build_scheduler_hints_projection( - status_payload: dict[str, Any], - *, - goal_id: str, - agent_id: str | None, -) -> dict[str, Any] | None: - """Extract scheduler hints from the quota should-run packet.""" - from .quota import build_quota_should_run - - try: - decision = build_quota_should_run( - status_payload, - goal_id=goal_id, - agent_id=agent_id, - include_scheduler_detail=True, - ) - except (ValueError, KeyError, OSError): - return None - - scheduler = decision.get("scheduler_hint") - if not isinstance(scheduler, dict): - return None - - return { - "action": scheduler.get("action"), - "cadence_class": scheduler.get("cadence_class"), - "recommended_interval_minutes": scheduler.get( - "recommended_interval_minutes" - ), - "max_interval_minutes": scheduler.get("max_interval_minutes"), - "reset_policy": scheduler.get("reset_policy"), - "unchanged_poll": { - k: scheduler.get("unchanged_poll", {}).get(k) - for k in ("backoff_multiplier", "max_interval_minutes") - if k in scheduler.get("unchanged_poll", {}) - } - if isinstance(scheduler.get("unchanged_poll"), dict) - else None, - "arbitration": scheduler.get("arbitration_disposition"), - } - - -# ── decision scopes projection ────────────────────────────────────────────── -def _build_decision_scopes_projection( - status_payload: dict[str, Any], - *, - goal_id: str, - agent_id: str | None, -) -> dict[str, Any]: - """Project decision scopes from quota should-run summaries. - - Uses ``build_quota_should_run`` to obtain agent/user todo summaries - and standing decision authority, then runs the consistency check. - """ - from .quota import build_quota_should_run - - try: - decision = build_quota_should_run( - status_payload, - goal_id=goal_id, - agent_id=agent_id, - ) - except (ValueError, KeyError, OSError): - return { - "standing_decision_authority": None, - "required_scope_consistency": { - "ok": None, - "status": "unavailable", - }, - } - - # standing authority already computed by the quota guard - standing = decision.get("standing_decision_authority") - - # Build consistency from the summaries in the should_run payload - agent_summary = decision.get("agent_todo_summary") - user_summary = decision.get("user_todo_summary") - consistency = build_required_decision_scope_consistency( - agent_todo_summary=agent_summary if isinstance(agent_summary, dict) else None, - user_todo_summary=user_summary if isinstance(user_summary, dict) else None, - agent_id=agent_id, - registered_agent_ids=None, - standing_decision_authority=standing, - ) - - return { - "standing_decision_authority": standing, - "required_scope_consistency": { - "ok": consistency.get("ok"), - "status": consistency.get("status"), - "checked_agent_todo_count": consistency.get( - "checked_agent_todo_count" - ), - "checked_required_scope_count": consistency.get( - "checked_required_scope_count" - ), - "standing_authority_match_count": consistency.get( - "standing_authority_match_count" - ), - "terminal_outcome_count": consistency.get("terminal_outcome_count"), - }, - } - - -# ── main builder ──────────────────────────────────────────────────────────── -def build_visible_governance_slice( - status_payload: dict[str, Any], - *, - goal_id: str, - agent_id: str | None = None, - include_task_leases: bool = True, -) -> dict[str, Any]: - """Synthesize a read-only visible-governance slice for one goal. - - Args: - status_payload: Full ``collect_status(...)`` result. - goal_id: Goal to project governance for. - agent_id: Optional agent lane to scope per-agent projections. - include_task_leases: When True, read active lease files (read-only). - - Returns: - ``{"schema_version": "visible_governance_slice_v0", "ok": True, ...}`` - with sections: claims, leases, quota, scheduler_hints, - decision_scopes, authority_boundary. - """ - safe_goal_id = str(goal_id or "").strip() - if not safe_goal_id: - return { - "schema_version": GOVERNANCE_SLICE_SCHEMA, - "ok": False, - "error": "goal_id is required", - } - - claims = _build_claims_projection( - status_payload, goal_id=safe_goal_id, agent_id=agent_id - ) - leases = _build_leases_projection( - status_payload, - goal_id=safe_goal_id, - agent_id=agent_id, - include_task_leases=include_task_leases, - ) - quota = _build_quota_projection( - status_payload, goal_id=safe_goal_id, agent_id=agent_id - ) - scheduler_hints = _build_scheduler_hints_projection( - status_payload, goal_id=safe_goal_id, agent_id=agent_id - ) - decision_scopes = _build_decision_scopes_projection( - status_payload, goal_id=safe_goal_id, agent_id=agent_id - ) - authority_boundary = _build_authority_boundary_table() - - return { - "schema_version": GOVERNANCE_SLICE_SCHEMA, - "ok": True, - "goal_id": safe_goal_id, - "agent_id": normalize_todo_claimed_by(agent_id), - "claims": claims, - "leases": leases, - "quota": quota, - "scheduler_hints": scheduler_hints, - "decision_scopes": decision_scopes, - "authority_boundary": authority_boundary, - } - - -# ── markdown renderer ─────────────────────────────────────────────────────── -def render_visible_governance_markdown(slice_payload: dict[str, Any]) -> str: - """Render a visible-governance slice as public-safe markdown.""" - - goal_id = str(slice_payload.get("goal_id") or "unknown") - agent_id = str(slice_payload.get("agent_id") or "") - - lines: list[str] = [] - lines.append("# Visible Governance Slice") - lines.append("") - lines.append(f"- **Goal**: `{goal_id}`") - if agent_id: - lines.append(f"- **Agent**: `{agent_id}`") - lines.append(f"- **Schema**: `{slice_payload.get('schema_version')}`") - lines.append("") - - # ── Claims ── - claims = slice_payload.get("claims") - if isinstance(claims, dict): - lines.append("## Claims (Soft Ownership)") - lines.append("") - lines.append( - f"- Total claims: **{claims.get('total_claims', 0)}** " - f"across {claims.get('claiming_agent_count', 0)} agent(s)" - ) - current = claims.get("current_agent_claims", 0) - if current: - lines.append(f"- Current agent claims: **{current}**") - by_agent = claims.get("by_agent") - if isinstance(by_agent, dict) and by_agent: - lines.append("") - lines.append("| Agent | Claim Count |") - lines.append("|-------|-------------|") - for agent, count in sorted(by_agent.items()): - lines.append(f"| `{agent}` | {count} |") - lines.append("") - - # ── Leases ── - leases = slice_payload.get("leases") - if isinstance(leases, dict): - lines.append("## Task Leases (Policy-Selected Runtime Fence)") - lines.append("") - lines.append(f"- Nature: *{leases.get('nature', LEASE_NATURE)}*") - lines.append( - f"- Active leases: **{leases.get('active_leases', 0)}**" - ) - lease_list = leases.get("leases") - if isinstance(lease_list, list) and lease_list: - lines.append("") - lines.append( - "| Todo ID | Owner | Expires | Version | TTL (s) |" - ) - lines.append( - "|---------|-------|---------|---------|---------|" - ) - for lease in lease_list: - lines.append( - f"| `{lease.get('todo_id', '')}` " - f"| `{lease.get('owner', '')}` " - f"| {lease.get('expires_at', '')} " - f"| {lease.get('version', '')} " - f"| {lease.get('acquire_ttl_seconds', '')} |" - ) - lines.append("") - - # ── Quota ── - quota = slice_payload.get("quota") - if isinstance(quota, dict): - lines.append("## Quota Decision") - lines.append("") - lines.append(f"- Should run: `{quota.get('should_run')}`") - lines.append( - f"- Effective action: `{quota.get('effective_action', '')}`" - ) - lines.append(f"- State: `{quota.get('state', '')}`") - compute = quota.get("compute") - if compute is not None: - lines.append(f"- Compute: {compute}") - slots_info = [] - for key in ("spent_slots", "allowed_slots", "slot_minutes"): - value = quota.get(key) - if value is not None: - slots_info.append(f"{key}: {value}") - if slots_info: - lines.append(f"- Slots: {', '.join(slots_info)}") - lines.append("") - - # ── Scheduler Hints ── - sched = slice_payload.get("scheduler_hints") - if isinstance(sched, dict): - lines.append("## Scheduler Hints") - lines.append("") - lines.append(f"- Action: `{sched.get('action', '')}`") - lines.append( - f"- Cadence class: `{sched.get('cadence_class', '')}`" - ) - interval = sched.get("recommended_interval_minutes") - if interval is not None: - lines.append(f"- Recommended interval: {interval} min") - lines.append( - f"- Arbitration: `{sched.get('arbitration', '')}`" - ) - lines.append("") - - # ── Decision Scopes ── - scopes = slice_payload.get("decision_scopes") - if isinstance(scopes, dict): - lines.append("## Decision Scopes") - lines.append("") - standing = scopes.get("standing_decision_authority") - if isinstance(standing, dict): - lines.append( - f"- Standing authority receipts: " - f"**{standing.get('active_count', 0)}** active, " - f"{standing.get('inactive_count', 0)} inactive" - ) - consistency = scopes.get("required_scope_consistency") - if isinstance(consistency, dict): - lines.append( - f"- Scope consistency: " - f"`{consistency.get('status', 'unknown')}` " - f"(checked {consistency.get('checked_agent_todo_count', 0)} " - f"todos, {consistency.get('checked_required_scope_count', 0)} " - f"scopes)" - ) - matches = consistency.get("standing_authority_match_count", 0) - if matches: - lines.append( - f"- Standing authority matches: **{matches}**" - ) - lines.append("") - - # ── Authority Boundary ── - boundary = slice_payload.get("authority_boundary") - if isinstance(boundary, list): - lines.append("## Authority Boundary: Proposal vs Shipped") - lines.append("") - lines.append( - "| RFC Section | Shipped | Gap |" - ) - lines.append( - "|-------------|---------|-----|" - ) - for entry in boundary: - shipped = "Yes" if entry.get("shipped") else "**No**" - gap = str(entry.get("gap") or "")[:120] - lines.append( - f"| {entry.get('rfc_section', '')} " - f"| {shipped} " - f"| {gap} |" - ) - lines.append("") - - # ── Summary ── - lines.append("## Summary") - lines.append("") - lines.append( - f"- **Proposals shipped**: " - f"{sum(1 for e in (boundary or []) if e.get('shipped'))} / " - f"{len(boundary) if boundary else 0}" - ) - lines.append( - "- **Key takeaway**: The original Stage-2 prototype is retired. " - "Canonical providers own normal Todo mutations and task leases. " - "soft_claim forbids lease acquisition; hard_lease enforces the lease " - "fence. Neither policy grants general write authority. " - "Installed adoption and cross-host qualification remain separate." - ) - lines.append("") - - return "\n".join(lines) diff --git a/tests/control_plane/test_canonical_goal_channel_coordination.py b/tests/control_plane/test_canonical_goal_channel_coordination.py index 6de461b1d7..7359432f40 100644 --- a/tests/control_plane/test_canonical_goal_channel_coordination.py +++ b/tests/control_plane/test_canonical_goal_channel_coordination.py @@ -100,3 +100,73 @@ def channels(value): assert all(item['active_leases'] == [] for item in found) assert all(item['coordination_observation']['source_authority'] == provider + '_v0' for item in found) assert state.read_bytes() == before + + +@pytest.mark.parametrize('provider', ['file', 'sqlite']) +def test_real_cli_reports_native_lease_and_recovers_provider_without_legacy_fallback(tmp_path, provider): + from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted + from loopx.control_plane.testing.canary_harness import import_fixture_authority, run_json_cli, run_json_cli_result + from test_goal_channel_hard_lease_visibility import _write_status_workspace + + registry, repo, runtime = _write_status_workspace(tmp_path) + state = repo / 'ACTIVE_GOAL_STATE.md' + import_fixture_authority(registry, GOAL_ID, provider=provider) + + def cli(*args): + return run_json_cli(*args, registry_path=registry, runtime_root=runtime) + + cli('handoff-mode', 'set', '--goal-id', GOAL_ID, '--mode', 'hard_lease', + '--operation-id', 'channel-lease-fixture-policy') + task = cli('todo', 'add', '--goal-id', GOAL_ID, '--role', 'agent', + '--text', 'Observe the current canonical lease.', '--task-class', 'advancement_task', + '--claimed-by', 'agent-a') + lease = cli('task-lease', 'acquire', '--goal-id', GOAL_ID, '--todo-id', task['todo_id'], + '--owner', 'agent-a', '--idempotency-key', 'channel-lease-fixture', '--ttl-seconds', '600') + assert lease['acquired'] is True + original = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID, include_leases=True) + source, registry_bytes = state.read_bytes(), registry.read_bytes() + _write_active_lease(runtime, owner='stale-agent', todo_id=task['todo_id']) + + def channel(): + status = cli('status', '--goal-id', GOAL_ID) + items = status['attention_queue']['items'] + item = next(row for row in items if row['goal_id'] == GOAL_ID) + return item['goal_channel_projection'] + + def assert_current(view): + assert view['coordination_observation']['source_authority'] == provider + '_v0' + assert view['coordination_authority'] == {'state': 'promoted'} + hard = [row for row in view['active_leases'] if row['status'] == 'hard_lease'] + assert len(hard) == 1, view + assert hard[0]['todo_id'] == task['todo_id'] and hard[0]['owner_agent'] == 'agent-a', hard + assert hard[0]['lease_version'] == lease['lease']['version'] + assert all(row.get('owner_agent') != 'stale-agent' for row in view['active_leases']) + assert view['truth_contract']['write_authority'] == 'none' + + assert_current(channel()) + state.unlink() + assert_current(channel()) + assert not state.exists(), 'status must not reconstruct the display' + state.write_bytes(source) + + authority, quarantine = runtime / 'authority', runtime / 'provider-quarantine' + authority.rename(quarantine) + try: + unavailable = build_goal_channel_projection( + goal_id=GOAL_ID, status_item=_status_item(claimed_by='stale-agent'), runtime_root=runtime, + ) + assert unavailable['coordination_observation']['status'] == 'unavailable' + assert unavailable['coordination_authority'] == {'state': 'unavailable'} + assert unavailable['active_leases'] == [] + assert unavailable['truth_contract']['write_authority'] == 'none' + code, rejected = run_json_cli_result('status', '--goal-id', GOAL_ID, + registry_path=registry, runtime_root=runtime) + assert code == 1 and rejected['ok'] is False, rejected + assert rejected['attention_queue']['items'][0]['status'] == 'status_collection_failed' + assert 'canonical Todo authority is unavailable' in rejected['error'] + assert 'stale-agent' not in json.dumps(rejected) + finally: + quarantine.rename(authority) + assert_current(channel()) + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID, include_leases=True) == original + assert state.read_bytes() == source and registry.read_bytes() == registry_bytes From 3df099234da8b802d1a2affea657cf3c9c161a9e Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:40:35 +0800 Subject: [PATCH 57/98] fix: preserve canonical failure successor lineage and historical parents Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- demo/auto_research/worker_runtime.py | 53 ++++--- demo/multi_agent/role_successor.py | 11 +- .../auto-research-role-profile-v0.md | 14 ++ examples/auto-research-worker-turn-smoke.py | 138 +++++++++++++++--- 4 files changed, 172 insertions(+), 44 deletions(-) diff --git a/demo/auto_research/worker_runtime.py b/demo/auto_research/worker_runtime.py index 9937201e44..fe90a20eb5 100644 --- a/demo/auto_research/worker_runtime.py +++ b/demo/auto_research/worker_runtime.py @@ -34,6 +34,9 @@ normalize_todo_id, ) from loopx.control_plane.todos.handoff_mode import HANDOFF_MODE_HARD_LEASE +from loopx.control_plane.coordination.local_authority import ( + LocalCoordinationAuthorityUnavailable, read_canonical_todos_if_promoted, +) from loopx.control_plane.work_items.task_lease import ( TaskLeaseError, acquire_task_lease, @@ -388,28 +391,38 @@ def _maybe_add_role_successor_todos( action=action, ) failure_continuation = decision_summary.get("failure_continuation") - if isinstance(failure_continuation, dict): - next_outcome = str(failure_continuation.get("next_outcome") or "") - successor_specs = [ - spec - for spec in successor_specs - if spec.get("action_kind") == next_outcome - ] text_source_todo_id = None if isinstance(failure_continuation, dict): - text_source_todo_id = str(failure_continuation.get("source_todo_id") or "") or None - return apply_role_successor_todos( - registry_path=registry_path, - goal_id=goal_id, - source_todo_id=source_todo_id, - current_agent_id=agent_id, - role_id=role_id, - action=action, - successor_specs=successor_specs, - decision_summary=decision_summary, - execute=execute, - text_source_todo_id=text_source_todo_id, - ) + next_outcome = str(failure_continuation.get("next_outcome") or "") + successor_specs = [spec for spec in successor_specs if spec.get("action_kind") == next_outcome] + source_todo_id = str(failure_continuation.get("source_todo_id") or "") or source_todo_id + text_source_todo_id = source_todo_id + def apply_binding(parent_id: str, *, revision: str | None = None) -> dict[str, object]: + return apply_role_successor_todos(registry_path=registry_path, goal_id=goal_id, + source_todo_id=parent_id, current_agent_id=agent_id, role_id=role_id, + action=action, successor_specs=successor_specs, decision_summary=decision_summary, + execute=execute, text_source_todo_id=text_source_todo_id, + expected_provider_revision=revision) + + try: + return apply_binding(source_todo_id) + except LocalCoordinationAuthorityUnavailable as error: + if error.code != 'todo_semantic_duplicate_conflict' or text_source_todo_id is None: + raise + # Native create owns normalized title/intent matching. Qualify only its + # exact conflicting record, without rewriting a historical parent. + snapshot = read_canonical_todos_if_promoted( + runtime_root=runtime_root_from_registry(registry_path, None), goal_id=goal_id, + ) + todos = {row['todo_id']: row for row in (snapshot or {}).get('todos', [])} + existing = todos.get(error.payload.get('todo_id')) + parent_id = existing.get('unblocks_todo_id') if existing else None + parent = todos.get(parent_id) + if not (parent and parent['role'] == 'agent' and parent.get('action_kind') in SUMMARY_ACTIONS + and parent.get('unblocks_todo_id') == text_source_todo_id): + raise + return apply_binding(parent_id, revision=snapshot['provider_revision']) + def _executed_successor_todo_ids(successor_todos: dict[str, object]) -> list[str]: diff --git a/demo/multi_agent/role_successor.py b/demo/multi_agent/role_successor.py index 5dd13cefac..4a054582a8 100644 --- a/demo/multi_agent/role_successor.py +++ b/demo/multi_agent/role_successor.py @@ -173,13 +173,13 @@ def apply_role_successor_todos( decision_summary: Mapping[str, object], execute: bool, text_source_todo_id: str | None = None, + expected_provider_revision: str | None = None, ) -> dict[str, object]: - """Preview or create role-declared successor todos through LoopX state. + """Create successors, retaining qualified historical source bindings. - When text_source_todo_id is provided, it is used for rendering the - successor text (so the title is stable across different summary todos - that review the same failure lineage). source_todo_id is still used - for the unblocks_todo_id binding. + Normally one source supplies both title and lineage. A caller recovering + an existing successor may preserve its original parent while rendering + the same title from the independently verified evidence lineage. """ specs = [dict(spec) for spec in successor_specs if isinstance(spec, Mapping)] @@ -257,6 +257,7 @@ def apply_role_successor_todos( action_kind=action_kind, claimed_by=claimed_by, unblocks_todo_id=source_todo_id, + expected_provider_revision=expected_provider_revision, dry_run=False, ) successors.append( diff --git a/docs/reference/protocols/auto-research-role-profile-v0.md b/docs/reference/protocols/auto-research-role-profile-v0.md index 4d17f3d005..38566eb70e 100644 --- a/docs/reference/protocols/auto-research-role-profile-v0.md +++ b/docs/reference/protocols/auto-research-role-profile-v0.md @@ -105,6 +105,20 @@ public/private boundary checks before another agent can run it. target is not met and a role-declared successor condition is satisfied, the worker must create or link that successor instead of closing the lane. +For failure continuation, the successor's title and `unblocks_todo_id` share +the failed evidence Todo from `research_failure_continuation_v0.source_todo_id`. +Several summaries of that lineage therefore create or reuse one identical +successor without rebinding its original parent. A historical successor may +retain its original summary parent when the complete canonical records prove +that summary belongs to the same failed evidence lineage; all other intent +fields still pass the native duplicate check. Each completed summary keeps +its own `successor_todo_ids` link to that successor. Other role handoffs still +bind the selected source Todo. These links grant no execution or claim rights; +the canonical owner continues to reject same-title requests with different +lineage or other intent. Historical-parent reuse binds create to the inspected +provider revision; a concurrent change refuses without rebinding and requires +a fresh read before retry. + ## Resolution Order Every visible auto-research worker should resolve its instructions in this diff --git a/examples/auto-research-worker-turn-smoke.py b/examples/auto-research-worker-turn-smoke.py index b6bf588022..650631c766 100644 --- a/examples/auto-research-worker-turn-smoke.py +++ b/examples/auto-research-worker-turn-smoke.py @@ -33,6 +33,11 @@ apply_role_successor_todos, ) from loopx.todos import add_goal_todo, complete_goal_todo # noqa: E402 +from loopx.control_plane.coordination.local_authority import ( # noqa: E402 + LocalCoordinationAuthorityUnavailable, + read_canonical_todos_if_promoted, +) +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 from loopx.control_plane.work_items.task_lease import ( # noqa: E402 TaskLeaseError, acquire_task_lease, @@ -173,7 +178,9 @@ def write_summary_state(path: Path) -> None: ) -def write_registry(path: Path, *, project: Path, state_file: Path, runtime_root: Path) -> None: +def write_registry( + path: Path, *, project: Path, state_file: Path, runtime_root: Path, provider: str, +) -> None: path.write_text( json.dumps( { @@ -207,6 +214,7 @@ def write_registry(path: Path, *, project: Path, state_file: Path, runtime_root: + "\n", encoding="utf-8", ) + import_fixture_authority(path, GOAL_ID, provider=provider) def append_real_fixture_evidence(*, registry: Path, runtime_root: Path, temp: Path) -> None: @@ -317,7 +325,7 @@ def append_negative_fixture_evidence( assert_public_safe(appended) -def main() -> int: +def assert_seeded_worker_turns() -> None: with tempfile.TemporaryDirectory() as temp_dir: temp = Path(temp_dir) supervisor = build_auto_research_demo_supervisor_plan( @@ -533,7 +541,7 @@ def main() -> int: successors = seeded_failure_turn["successor_todos"]["successors"] assert len(successors) == 1, seeded_failure_turn assert successors[0]["action_kind"] == "propose_failure_successor", seeded_failure_turn - assert successors[0]["unblocks_todo_id"] == evaluator_seed["todo_id"], seeded_failure_turn + assert successors[0]["unblocks_todo_id"] == executor_seed["todo_id"], seeded_failure_turn assert seeded_failure_turn["completion"]["successor_todo_ids"] == [ successors[0]["todo_id"] ], seeded_failure_turn @@ -562,6 +570,9 @@ def main() -> int: ) assert manual_lease["lease"] is None, manual_lease + + +def assert_evidence_lineage(provider: str) -> None: with tempfile.TemporaryDirectory() as temp_dir: temp = Path(temp_dir) project = temp / "project" @@ -570,7 +581,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) summary_todo = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -618,6 +629,12 @@ def main() -> int: for successor_id in successor_ids: assert successor_id in state_text, state_text assert_public_safe(evaluator_summary) + canonical = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) + by_id = {row["todo_id"]: row for row in canonical["todos"]} + assert by_id[summary_todo_id]["status"] == "done", canonical + assert by_id[summary_todo_id]["claimed_by"] == EVALUATOR_AGENT_ID + assert by_id[successor_ids[0]]["claimed_by"] == CURATOR_AGENT_ID + assert by_id[successor_ids[0]]["unblocks_todo_id"] == summary_todo_id with tempfile.TemporaryDirectory() as temp_dir: temp = Path(temp_dir) @@ -627,7 +644,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) promotion_todo = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -732,7 +749,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) first_failure_evidence = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -808,7 +825,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) retry_evidence = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -892,7 +909,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) self_referential_summary = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -948,7 +965,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) positive_evidence = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1069,7 +1086,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) failure_evidence = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1122,7 +1139,7 @@ def main() -> int: failure_turn["frontier"]["frontier"]["selected"]["unblocks_todo_id"] == failure_evidence["todo_id"] ), failure_turn - assert successors[0]["unblocks_todo_id"] == failure_review["todo_id"], failure_turn + assert successors[0]["unblocks_todo_id"] == failure_evidence["todo_id"], failure_turn assert failure_turn["completion"]["successor_todo_ids"] == [successors[0]["todo_id"]], failure_turn state_text = state_file.read_text(encoding="utf-8") assert f"todo_id={failure_review['todo_id']} status=done" in state_text, state_text @@ -1153,7 +1170,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) data_gap_evidence = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1213,7 +1230,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) retry_evidence = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1268,7 +1285,7 @@ def main() -> int: retry_turn["evaluation_summary"]["failure_continuation"]["source_todo_id"] == retry_evidence["todo_id"] ), retry_turn - assert successors[0]["unblocks_todo_id"] == retry_review["todo_id"], retry_turn + assert successors[0]["unblocks_todo_id"] == retry_evidence["todo_id"], retry_turn assert retry_turn["completion"]["successor_todo_ids"] == [successors[0]["todo_id"]], retry_turn state_text = state_file.read_text(encoding="utf-8") assert f"todo_id={retry_review['todo_id']} status=done" in state_text, state_text @@ -1282,7 +1299,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) generic_todo = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1324,7 +1341,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) append_fixture_evidence( registry=registry, runtime_root=runtime_root, @@ -1366,6 +1383,11 @@ def main() -> int: assert target_reached["frontier"]["frontier"]["selected"] is None, target_reached assert_public_safe(target_reached) + for historical_parent in (False, True): + assert_failure_successor_lineage(provider, historical_parent) + + +def assert_failure_successor_lineage(provider: str, historical_parent: bool) -> None: with tempfile.TemporaryDirectory() as temp_dir: temp = Path(temp_dir) project = temp / "project" @@ -1374,7 +1396,7 @@ def main() -> int: registry = temp / "registry.json" runtime_root = temp / "runtime" write_summary_state(state_file) - write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root) + write_registry(registry, project=project, state_file=state_file, runtime_root=runtime_root, provider=provider) failure_evidence = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1385,7 +1407,7 @@ def main() -> int: claimed_by=EXECUTOR_AGENT_ID, dry_run=False, ) - add_goal_todo( + first_summary = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, role="agent", @@ -1396,7 +1418,7 @@ def main() -> int: unblocks_todo_id=failure_evidence["todo_id"], dry_run=False, ) - add_goal_todo( + second_summary = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, role="agent", @@ -1417,6 +1439,21 @@ def main() -> int: workspace = project / "visible-workspace" workspace.mkdir() + if historical_parent: + # Seed the published pre-cutoff producer intent through native create; + # its parent must survive upgrade even when another summary retries. + historical = apply_role_successor_todos(registry_path=registry, goal_id=GOAL_ID, + source_todo_id=first_summary['todo_id'], text_source_todo_id=failure_evidence['todo_id'], + current_agent_id=EVALUATOR_AGENT_ID, role_id='evaluator_promoter', + action='summarize_evidence', execute=True, + successor_specs=[spec for spec in auto_research_successor_specs_for_action( + role_id='evaluator_promoter', action='summarize_evidence') + if spec['action_kind'] == 'propose_failure_successor'], + decision_summary={'failure_continuation': {'next_outcome': 'propose_failure_successor'}}) + assert historical['successors'][0]['added'] is True + original = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) + original_source = next(row for row in original['todos'] if row['todo_id'] == failure_evidence['todo_id']) + first_turn = run_worker_turn( registry=registry, runtime_root=runtime_root, @@ -1432,6 +1469,56 @@ def main() -> int: first_successor_text = first_successors[0]["todo_command"] assert ".." not in first_successor_text, f"successor text contains ..: {first_successor_text!r}" assert_public_safe(first_turn) + first_snapshot = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) + successor_id = first_successors[0]['todo_id'] + accepted = next(row for row in first_snapshot['todos'] if row['todo_id'] == successor_id) + assert accepted['unblocks_todo_id'] == (first_summary['todo_id'] if historical_parent else failure_evidence['todo_id']) + + # An explicit attempt to rebind this existing successor remains illegal. + # The producer fix must not weaken the canonical duplicate-intent fence. + try: + add_goal_todo(registry_path=registry, goal_id=GOAL_ID, role='agent', + text=accepted['text'], task_class='advancement_task', + action_kind='propose_failure_successor', claimed_by=HYPOTHESIS_AGENT_ID, + unblocks_todo_id=second_summary['todo_id'], dry_run=False) + except LocalCoordinationAuthorityUnavailable as error: + assert error.code == 'todo_semantic_duplicate_conflict', error + else: + raise AssertionError('same-title successor must not acquire another source binding') + assert read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) == first_snapshot + if historical_parent: + from loopx.control_plane.testing.canary_harness import run_json_cli + run_json_cli('todo', 'archive-completed', '--goal-id', GOAL_ID, '--role', 'agent', + '--max-active-done', '0', '--execute', + registry_path=registry, runtime_root=runtime_root) + + if historical_parent: + from unittest.mock import patch + from demo.auto_research import worker_runtime + observed_after_race = None + + def concurrent_read(**kwargs): + nonlocal observed_after_race + snapshot = read_canonical_todos_if_promoted(**kwargs) + add_goal_todo(registry_path=registry, goal_id=GOAL_ID, role='agent', + text='[P2] Independent concurrent planning work.', task_class='advancement_task', + claimed_by=CURATOR_AGENT_ID, dry_run=False) + observed_after_race = read_canonical_todos_if_promoted(**kwargs) + return snapshot + + # Intervene after a real read; both the update and CAS refusal use + # the real provider. No mock supplies the claimed postcondition. + with patch.object(worker_runtime, 'read_canonical_todos_if_promoted', concurrent_read): + try: + run_auto_research_worker_turn(registry_path=registry, + runtime_root_arg=str(runtime_root), goal_id=GOAL_ID, + agent_id=EVALUATOR_AGENT_ID, workspace=workspace, execute=True, + complete_selected_todo=True, lane_count=4, visible_lanes_accepted=True) + except LocalCoordinationAuthorityUnavailable as error: + assert error.code == 'provider_revision_mismatch', error + else: + raise AssertionError('stale historical-lineage proof must refuse') + assert read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) == observed_after_race second_turn = run_worker_turn( registry=registry, @@ -1451,7 +1538,20 @@ def main() -> int: f"second successor should have the same todo_id, got: {second_successor['todo_id']}" ) assert_public_safe(second_turn) + final = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=GOAL_ID) + rows = {row['todo_id']: row for row in final['todos']} + assert len(final['todos']) == len(original['todos']) + 1 + assert rows[successor_id] == accepted + assert rows[failure_evidence['todo_id']] == original_source + for summary in (first_summary, second_summary): + stored = rows[summary['todo_id']] + assert stored['status'] == 'done' and stored['claimed_by'] == EVALUATOR_AGENT_ID + assert stored['successor_todo_ids'] == [successor_id] +def main() -> int: + assert_seeded_worker_turns() + for provider in ("file", "sqlite"): + assert_evidence_lineage(provider) return 0 From 58277fb486bfe0689682f56d8a6e28f68f68cb79 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:40:35 +0800 Subject: [PATCH 58/98] test: migrate dreaming and durable lifecycle callers to canonical authority Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../todo-durability-fixture-smoke.py | 71 ++++++++++++------- examples/dreaming-proposal-decision-smoke.py | 52 ++++++++------ 2 files changed, 74 insertions(+), 49 deletions(-) diff --git a/examples/control_plane/todo-durability-fixture-smoke.py b/examples/control_plane/todo-durability-fixture-smoke.py index 6fc1c3cdbd..7dc2f5ffd8 100644 --- a/examples/control_plane/todo-durability-fixture-smoke.py +++ b/examples/control_plane/todo-durability-fixture-smoke.py @@ -4,7 +4,6 @@ from __future__ import annotations import json -import subprocess import sys import tempfile from pathlib import Path @@ -15,6 +14,8 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.status import parse_active_state_todos # noqa: E402 +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 +from loopx.control_plane.testing.canary_harness import import_fixture_authority, run_json_cli # noqa: E402 from loopx.rollout_event_log import ( # noqa: E402 append_rollout_event, build_rollout_event, @@ -35,30 +36,14 @@ def run_cli(*args: str, registry_path: Path, runtime: Path) -> dict: - result = subprocess.run( - [ - sys.executable, - "-m", - "loopx.cli", - "--registry", - str(registry_path), - "--runtime-root", - str(runtime), - "--format", - "json", - *args, - ], - cwd=REPO_ROOT, - check=True, - capture_output=True, - text=True, - ) - return json.loads(result.stdout) + return run_json_cli(*args, registry_path=registry_path, runtime_root=runtime, + cwd=REPO_ROOT, include_returncode=False) def state_text() -> str: archived_lines = "".join( f"- [x] [P1] Archived completed work item {index}.\n" + f" \n" for index in range(1, ARCHIVED_DONE_TODOS + 1) ) return ( @@ -273,11 +258,7 @@ def assert_pr_resume_repository_binding() -> None: ], ambiguous -def main() -> int: - assert_pr_resume_repository_binding() - parsed = parse_active_state_todos(state_text(), rollout_events=[pr_merged_event()]) - assert_parseable_agent_todos(parsed["agent_todos"]) - +def assert_lifecycle(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-todo-durability-") as tmp: registry_path, runtime = write_fixture(Path(tmp)) status_payload = run_cli("status", registry_path=registry_path, runtime=runtime) @@ -329,6 +310,26 @@ def main() -> int: assert "completed_todo_archive_warning" not in guard, guard first_todo_id = guard["agent_todo_summary"]["first_executable_items"][0]["todo_id"] + # Historical inspection above is read-only. Declare the fixture's + # existing claim owners before the reviewed import and normal writes. + registry = json.loads(registry_path.read_text(encoding='utf-8')) + registry['goals'][0]['coordination'] = { + 'agent_model': 'peer_v1', + 'registered_agents': ['fixture-agent', 'codex-product-capability', + 'codex-side-bypass', 'codex-reviewer', 'codex-builder'], + } + registry_path.write_text(json.dumps(registry), encoding='utf-8') + import_fixture_authority(registry_path, GOAL_ID, provider=provider) + original = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert original['source_authority'] == provider + '_v0' + assert original['decision_read_from_provider'] is True and original['legacy_fallback_used'] is False + assert len(original['todos']) == 5 + ARCHIVED_DONE_TODOS + assert sum(row['archive_state'] == 'archive' for row in original['todos']) == ARCHIVED_DONE_TODOS + native_guard = run_cli('quota', 'should-run', '--goal-id', GOAL_ID, + '--agent-id', 'fixture-agent', '--host-surface', 'generic_cli', + '--scheduler-owner', 'none', '--execution-mode', 'interactive', + registry_path=registry_path, runtime=runtime) + assert native_guard['selected_todo']['todo_id'] == first_todo_id lifecycle_result = run_cli( "todo", "complete", @@ -338,6 +339,7 @@ def main() -> int: "agent", "--todo-id", first_todo_id, + '--agent-id', 'fixture-agent', "--evidence", "fixture completed", "--next-agent-todo", @@ -350,18 +352,35 @@ def main() -> int: runtime=runtime, ) next_todo = lifecycle_result["next_todos"][0] - assert next_todo["todo"].startswith("[P1] Continue lifecycle"), lifecycle_result + canonical = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + rows = {row['todo_id']: row for row in canonical['todos']} + assert rows[next_todo['todo_id']]['text'].startswith('[P1] Continue lifecycle') + assert rows[first_todo_id]['status'] == 'done' + assert rows[first_todo_id]['successor_todo_ids'] == [next_todo['todo_id']] + assert len(canonical['todos']) == len(original['todos']) + 1 + for row in original['todos']: + if row['todo_id'] != first_todo_id: + assert rows[row['todo_id']] == row post_lifecycle_status = run_cli("status", registry_path=registry_path, runtime=runtime) post_lifecycle_item = attention_item(post_lifecycle_status) post_agent_todos = post_lifecycle_item["agent_todos"] assert post_agent_todos["open_count"] == 2, post_agent_todos + # The summary retains its documented non-open count, including deferred. assert post_agent_todos["done_count"] == 4, post_agent_todos assert post_agent_todos["deferred_count"] == 2, post_agent_todos assert post_agent_todos["first_open_items"][0]["priority"] == "P1", post_agent_todos assert post_agent_todos["first_open_items"][0]["todo_id"] == next_todo["todo_id"], post_agent_todos assert post_agent_todos["first_open_items"][0]["action_kind"] == "fixture_follow_up", post_agent_todos + + +def main() -> int: + assert_pr_resume_repository_binding() + parsed = parse_active_state_todos(state_text(), rollout_events=[pr_merged_event()]) + assert_parseable_agent_todos(parsed["agent_todos"]) + for provider in ('file', 'sqlite'): + assert_lifecycle(provider) print("todo-durability-fixture-smoke ok") return 0 diff --git a/examples/dreaming-proposal-decision-smoke.py b/examples/dreaming-proposal-decision-smoke.py index f368457487..f12326dd49 100644 --- a/examples/dreaming-proposal-decision-smoke.py +++ b/examples/dreaming-proposal-decision-smoke.py @@ -4,36 +4,24 @@ from __future__ import annotations import json -import subprocess import sys import tempfile from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 +from loopx.control_plane.testing.canary_harness import import_fixture_authority, run_json_cli # noqa: E402 + GOAL_ID = "dreaming-decision-fixture" def run_cli(*args: str, registry_path: Path, runtime: Path) -> dict: - result = subprocess.run( - [ - sys.executable, - "-m", - "loopx.cli", - "--registry", - str(registry_path), - "--runtime-root", - str(runtime), - "--format", - "json", - *args, - ], - cwd=REPO_ROOT, - check=True, - capture_output=True, - text=True, - ) - return json.loads(result.stdout) + return run_json_cli(*args, registry_path=registry_path, runtime_root=runtime, + cwd=REPO_ROOT, include_returncode=False) def append_recorded_proposal( @@ -86,7 +74,7 @@ def append_recorded_proposal( f.write(json.dumps(record, sort_keys=True) + "\n") -def write_fixture(root: Path) -> tuple[Path, Path, Path, Path]: +def write_fixture(root: Path, provider: str) -> tuple[Path, Path, Path, Path]: project = root / "project" runtime = root / "runtime" state_file = f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" @@ -139,6 +127,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path, Path]: + "\n", encoding="utf-8", ) + import_fixture_authority(registry_path, GOAL_ID, provider=provider) append_recorded_proposal( runs_dir, generated_at="2026-01-01T00:00:00+00:00", @@ -162,9 +151,10 @@ def assert_no_quota_spend(runtime: Path) -> None: assert "quota_slot_spent" not in text, text -def main() -> int: +def assert_decisions(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-dreaming-decision-") as tmp: - registry_path, runtime, state_path, index_path = write_fixture(Path(tmp)) + registry_path, runtime, state_path, index_path = write_fixture(Path(tmp), provider) + original = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) approve = run_cli( "dreaming", @@ -200,6 +190,15 @@ def main() -> int: assert approve["side_effects"]["runtime_history_appended"] is True, approve assert approve["side_effects"]["quota_spent"] is False, approve assert_no_quota_spend(runtime) + approved = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert approved['source_authority'] == provider + '_v0' + assert approved['decision_read_from_provider'] is True and approved['legacy_fallback_used'] is False + assert len(approved['todos']) == len(original['todos']) + 1 + promoted = next(row for row in approved['todos'] if row['todo_id'] == todo_id) + assert promoted['text'] == '[P1] Implement the approved dreaming follow-up as normal delivery.' + assert promoted['status'] == 'open' and promoted['task_class'] == 'advancement_task' + original_ids = {row['todo_id'] for row in original['todos']} + assert [row for row in approved['todos'] if row['todo_id'] in original_ids] == original['todos'] before_defer_state = state_path.read_text(encoding="utf-8") before_defer_index = index_path.read_text(encoding="utf-8") @@ -228,6 +227,7 @@ def main() -> int: assert defer["side_effects"]["runtime_history_appended"] is True, defer assert defer["side_effects"]["quota_spent"] is False, defer assert state_path.read_text(encoding="utf-8") == before_defer_state + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) == approved assert index_path.read_text(encoding="utf-8") != before_defer_index assert_no_quota_spend(runtime) @@ -257,6 +257,12 @@ def main() -> int: assert reject["side_effects"]["quota_spent"] is False, reject assert state_path.read_text(encoding="utf-8") == before_reject_state assert index_path.read_text(encoding="utf-8") == before_reject_index + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) == approved + + +def main() -> int: + for provider in ('file', 'sqlite'): + assert_decisions(provider) print("dreaming-proposal-decision-smoke ok") return 0 From e74a1287c39be2b62a7adc58f4b31233b9e9e74a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 03:24:45 +0800 Subject: [PATCH 59/98] fix: recover cached material replan without repeating the Host Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../loopx-turn-path-delta-acceptance-smoke.py | 70 +++++++++++++++++-- loopx/control_plane/turn_driver/executor.py | 28 +++++++- tests/test_loopx_turn_executor.py | 39 ++++++++++- 3 files changed, 129 insertions(+), 8 deletions(-) diff --git a/examples/loopx-turn-path-delta-acceptance-smoke.py b/examples/loopx-turn-path-delta-acceptance-smoke.py index e0863556ea..3431108f53 100644 --- a/examples/loopx-turn-path-delta-acceptance-smoke.py +++ b/examples/loopx-turn-path-delta-acceptance-smoke.py @@ -16,6 +16,10 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.cli import main as cli_main # noqa: E402 +from loopx.control_plane.coordination.local_authority import ( # noqa: E402 + read_canonical_todos_if_promoted, +) +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 GOAL_ID = "loopx-turn-path-delta-fixture" @@ -112,6 +116,8 @@ def _write_host(root: Path) -> Path: mode = sys.argv[1] marker = pathlib.Path({MARKER!r}) marker.write_text(mode, encoding="utf-8") +with marker.with_suffix(".calls").open("a", encoding="utf-8") as calls: + calls.write(mode + "\\n") material_packet = {{ "agent_id": {AGENT_ID!r}, "state": "active", @@ -181,6 +187,8 @@ def _run_turn( workspace: Path, host: Path, mode: str, + expect_canonical_refusal: bool = False, + resume_turn_key: str | None = None, ) -> dict[str, Any]: output = io.StringIO() with contextlib.redirect_stdout(output): @@ -200,8 +208,10 @@ def _run_turn( GOAL_ID, "--agent-id", AGENT_ID, - "--turn-instance-id", - f"path-delta-{mode}", + *( + ["--resume-turn-key", resume_turn_key] + if resume_turn_key else ["--turn-instance-id", f"path-delta-{mode}"] + ), "--project", str(workspace), "--host-adapter-command-json", @@ -215,6 +225,14 @@ def _run_turn( ] ) payload = json.loads(output.getvalue()) + if expect_canonical_refusal: + assert exit_code != 0, payload + assert payload["reason_code"] == "canonical_authority_required", payload + assert payload["write_admitted"] is False, payload + assert payload["journal_observation"]["recorded_effects"]["host_invoked"] is True, payload + assert payload["journal_observation"]["recorded_effects"]["quota_spent"] is False, payload + assert payload["journal_observation"]["recovery_decision"]["reinvoke_host"] is False, payload + return payload assert exit_code == 0, payload assert payload["status"] == "committed", payload assert payload["validation"]["status"] == "passed", payload @@ -226,19 +244,44 @@ def _run_records(runtime: Path) -> list[dict[str, Any]]: return [json.loads(line) for line in index.read_text(encoding="utf-8").splitlines()] -def main() -> int: - with tempfile.TemporaryDirectory(prefix="loopx-turn-path-delta-") as directory: +def assert_canonical_turn_path_delta(provider: str) -> None: + with tempfile.TemporaryDirectory(prefix=f"loopx-turn-path-delta-{provider}-") as directory: root = Path(directory) project, runtime, workspace, registry = _write_fixture(root) host = _write_host(root) - _run_turn( + state = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" + source_bytes = state.read_bytes() + refused = _run_turn( + registry=registry, + runtime=runtime, + project=project, + workspace=workspace, + host=host, + mode="material", + expect_canonical_refusal=True, + ) + # The Host has returned, but no Todo write/spend is admitted. Import + # the complete stopped fixture and recover that original Turn. + assert state.read_bytes() == source_bytes + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) is None + calls = (project / MARKER).with_suffix(".calls") + assert calls.read_text(encoding="utf-8").splitlines() == ["material"] + import_fixture_authority(registry, GOAL_ID, provider=provider) + original = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert original is not None + assert original["source_authority"] == f"{provider}_v0", original + assert {item["todo_id"] for item in original["todos"]} == {TODO_ID}, original + recovered = _run_turn( registry=registry, runtime=runtime, project=project, workspace=workspace, host=host, mode="material", + resume_turn_key=refused["resume_turn_key"], ) + assert recovered["resume_turn_key"] == refused["resume_turn_key"], recovered + assert calls.read_text(encoding="utf-8").splitlines() == ["material"] _run_turn( registry=registry, runtime=runtime, @@ -247,6 +290,19 @@ def main() -> int: host=host, mode="routine", ) + assert calls.read_text(encoding="utf-8").splitlines() == ["material", "routine"] + canonical = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert canonical is not None + assert canonical["source_authority"] == f"{provider}_v0", canonical + assert {item["todo_id"] for item in canonical["todos"]} == {TODO_ID}, canonical + before, after = original["todos"][0], canonical["todos"][0] + for field in ("todo_id", "text", "role", "status", "claimed_by", + "task_class", "action_kind", "priority", "successor_todo_ids"): + assert after.get(field) == before.get(field), (field, before, after) + mutable_result_fields = {"note", "evidence", "updated_at", "last_actor_agent_id"} + assert {k: v for k, v in after.items() if k not in mutable_result_fields} == { + k: v for k, v in before.items() if k not in mutable_result_fields + }, (before, after) records = _run_records(runtime) material = next( @@ -266,6 +322,10 @@ def main() -> int: assert "agent_vision" not in routine, routine assert routine["vision_checkpoint"]["decision"] == "unchanged_with_reason", routine + +def main() -> int: + for provider in ("file", "sqlite"): + assert_canonical_turn_path_delta(provider) print("loopx-turn-path-delta-acceptance-smoke ok") return 0 diff --git a/loopx/control_plane/turn_driver/executor.py b/loopx/control_plane/turn_driver/executor.py index d615697435..bdb7e09a79 100644 --- a/loopx/control_plane/turn_driver/executor.py +++ b/loopx/control_plane/turn_driver/executor.py @@ -239,6 +239,7 @@ def _normalize_host_path_delta( *, unchanged_reason: str, errors: list[str], + from_journal: bool = False, ) -> tuple[str, dict[str, Any] | None]: raw_path_delta_mode = result.get("path_delta_mode") path_delta_mode = "" @@ -268,7 +269,25 @@ def _normalize_host_path_delta( errors.append("path_delta_mode must be unchanged or material_replan") agent_vision: dict[str, Any] | None = None - if agent_vision_json: + if from_journal and "agent_vision" in result: + # The journal stores the typed owner's decoded packet. Revalidate it + # through that same owner, without treating derived budget diagnostics + # as a new Host wire string or widening the untrusted Host protocol. + try: + if "agent_vision_json" in result: + raise ValueError("cached vision cannot carry both packet forms") + packet = result["agent_vision"] + if not isinstance(packet, dict): + raise TypeError("cached agent_vision must be a JSON object") + envelope = plan.get("turn_envelope") or {} + agent_vision = normalize_goal_vision_packet( + packet, + goal_id=str(envelope.get("goal_id") or ""), + agent_id=str(envelope.get("agent_id") or "") or None, + ) + except (TypeError, ValueError) as exc: + errors.append(f"invalid cached agent_vision: {exc}") + elif agent_vision_json: if len(agent_vision_json) > HOST_AGENT_VISION_JSON_MAX_CHARS: errors.append( "agent_vision_json exceeds " @@ -319,10 +338,13 @@ def validate_loopx_turn_host_result( value: Mapping[str, Any], *, completion_lifecycle_configured: bool = False, + from_journal: bool = False, ) -> dict[str, Any]: result = dict(value) errors: list[str] = [] allowed_fields = HOST_RESULT_FIELDS | subagent.subagent_host_result_fields(plan) + if from_journal: + allowed_fields = allowed_fields | {"agent_vision"} unknown = sorted(set(result) - allowed_fields) if unknown: errors.append("unsupported host result fields: " + ", ".join(unknown)) @@ -413,6 +435,7 @@ def validate_loopx_turn_host_result( result, unchanged_reason=unchanged_reason, errors=errors, + from_journal=from_journal, ) if ( path_delta_mode == "material_replan" @@ -428,6 +451,7 @@ def validate_loopx_turn_host_result( elif ( str(result.get("path_delta_mode") or "").strip() or str(result.get("agent_vision_json") or "").strip() + or (from_journal and "agent_vision" in result) ): errors.append( "non-material host results cannot declare path_delta_mode or agent_vision_json" @@ -849,6 +873,7 @@ def _host_result_stage( usage_goal_id: str = "", ) -> tuple[dict[str, Any] | None, list[str], dict[str, Any] | None]: completed_phases = list(journal.get("completed_phases") or []) + from_journal = "typed_result" in completed_phases result = ( journal.get("host_result") if isinstance(journal.get("host_result"), dict) @@ -927,6 +952,7 @@ def _host_result_stage( plan, result or {}, completion_lifecycle_configured=completion_lifecycle_configured, + from_journal=from_journal, ) if not validation.get("ok"): failure = _host_failure( diff --git a/tests/test_loopx_turn_executor.py b/tests/test_loopx_turn_executor.py index a8c4a1485d..a602dda4c0 100644 --- a/tests/test_loopx_turn_executor.py +++ b/tests/test_loopx_turn_executor.py @@ -712,10 +712,11 @@ def write_source_journal() -> None: assert calls == {"writeback": 0, "spend": 0, "scheduler": 0} -def test_host_result_requires_bounded_public_material_fields() -> None: +@pytest.mark.parametrize("field", ["raw_trajectory", "agent_vision"]) +def test_host_result_requires_bounded_public_material_fields(field: str) -> None: plan = _plan() result = _host_result(plan) - result["raw_trajectory"] = "not allowed" + result[field] = "not allowed" validation = validate_loopx_turn_host_result(plan, result) @@ -723,6 +724,40 @@ def test_host_result_requires_bounded_public_material_fields() -> None: assert "unsupported host result fields" in " ".join(validation["errors"]) +@pytest.mark.parametrize("packet, expected_error", [ + (None, "cached agent_vision must be a JSON object"), + ([], "cached agent_vision must be a JSON object"), + ("not an object", "cached agent_vision must be a JSON object"), + ({"vision_patch": {"acceptance_summary": "x" * 421}}, "acceptance_summary"), +]) +def test_cached_host_result_revalidates_decoded_vision( + packet: object, expected_error: str, +) -> None: + plan = _plan() + result = _host_result(plan) + result.update(result_kind="replan_required", path_delta_mode="material_replan", + agent_vision=packet) + result.pop("vision_unchanged_reason", None) + + validation = validate_loopx_turn_host_result(plan, result, from_journal=True) + + assert validation["ok"] is False + assert expected_error in " ".join(validation["errors"]) + + +def test_cached_host_result_rejects_conflicting_vision_forms() -> None: + plan = _plan() + result = _host_result(plan) + result.update(result_kind="replan_required", path_delta_mode="material_replan", + agent_vision={}, agent_vision_json="{}") + result.pop("vision_unchanged_reason", None) + + validation = validate_loopx_turn_host_result(plan, result, from_journal=True) + + assert validation["ok"] is False + assert "cached vision cannot carry both packet forms" in " ".join(validation["errors"]) + + def test_child_receipt_schema_excludes_registered_peer_authority() -> None: schema = child_execution_receipts_json_schema() item_schema = schema["items"] From 52fbb56762e292f398938c21df9737f5468506eb Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 03:24:45 +0800 Subject: [PATCH 60/98] test: qualify explicit quota selection and routed user notices Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- examples/capability-gate-smoke.py | 33 +++++++++++++++--- .../quota-agent-scoped-user-gate-smoke.py | 34 ++++++++++++++++--- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/examples/capability-gate-smoke.py b/examples/capability-gate-smoke.py index ba4178d1ab..3c17fdd8b4 100644 --- a/examples/capability-gate-smoke.py +++ b/examples/capability-gate-smoke.py @@ -376,15 +376,38 @@ def main() -> int: "provide or authorize the missing owner-held capability: credentials " "for todo_capability_9" ], owner_with_fallback + # A runnable recommendation is not a Turn binding. Keep the owner notice, + # but require explicit selection before projecting write/spend commands. + unbound = owner_with_fallback["interaction_contract"] + assert unbound["agent_channel"]["delivery_allowed"] is False, unbound + assert unbound["cli_channel"]["selection_required"] is True, unbound + assert unbound["cli_channel"]["next_cli_actions"] == [], unbound + assert unbound["cli_channel"]["spend_after_validation"] is False, unbound + assert "--todo-id '{todo_id}'" in unbound["cli_channel"][ + "selection_command" + ]["command_args_template"], unbound + + # This pure projection receives an already-bound receipt as input; real + # receipt acquisition and canonical recovery are covered by CLI smokes. + bound_fallback = build_quota_should_run( + status_payload([credentials_todo, p1_docs], registered_agent=AGENT_ID), + goal_id=GOAL_ID, + agent_id=AGENT_ID, + available_capabilities=["shell", "filesystem_write"], + scheduler_execution_context=SCHEDULER_CONTEXT, + receipt_bound_todo_id=p1_docs["todo_id"], + ) + bound = bound_fallback["interaction_contract"] + assert bound["agent_channel"]["delivery_allowed"] is True, bound + assert bound["cli_channel"]["spend_after_validation"] is True, bound + assert bound["user_channel"] == unbound["user_channel"], bound assert any( "--task-class user_gate" in action and "--target-capability credentials" in action and f"--blocks-agent {AGENT_ID}" in action and "--unblocks-todo-id todo_capability_9" in action - for action in owner_with_fallback["interaction_contract"]["cli_channel"][ - "next_cli_actions" - ] - ), owner_with_fallback + for action in bound["cli_channel"]["next_cli_actions"] + ), bound_fallback mixed_capability_todo = todo( 8, @@ -396,6 +419,7 @@ def main() -> int: status_payload([mixed_capability_todo]), goal_id=GOAL_ID, available_capabilities=["shell", "filesystem_write"], + scheduler_execution_context=SCHEDULER_CONTEXT, ) mixed_gate = mixed_owner_gate["capability_gate"] assert mixed_gate["action"] == "ask_owner", mixed_owner_gate @@ -426,6 +450,7 @@ def main() -> int: "credentials", "external_evidence_poll", ], + scheduler_execution_context=SCHEDULER_CONTEXT, ) assert mixed_ready["capability_gate"]["action"] == "run", mixed_ready assert mixed_ready["interaction_contract"]["user_channel"]["action_required"] is False diff --git a/examples/control_plane/quota-agent-scoped-user-gate-smoke.py b/examples/control_plane/quota-agent-scoped-user-gate-smoke.py index 53525bd1c5..88c1c56a61 100644 --- a/examples/control_plane/quota-agent-scoped-user-gate-smoke.py +++ b/examples/control_plane/quota-agent-scoped-user-gate-smoke.py @@ -13,6 +13,7 @@ from loopx.control_plane.todos.decision_scope import todo_gate_relation # noqa: E402 from loopx.control_plane.scheduler.execution_context import ( # noqa: E402 + GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, scheduler_execution_context_for_runtime_profile, ) from loopx.control_plane.testing.quota_fixtures import ( # noqa: E402 @@ -46,6 +47,7 @@ def todo_item( role: str = "agent", task_class: str = "advancement_task", claimed_by: str | None = None, + bound_agent: str | None = None, action_kind: str | None = None, blocks_agent: str | None = None, global_gate: bool = False, @@ -61,6 +63,7 @@ def todo_item( "next_due_at": next_due_at, "target_key": target_key, "global_gate": True if global_gate else None, + "bound_agent": bound_agent, }.items() if value is not None } @@ -245,7 +248,7 @@ def other_agent_gate_with_current_user_action_payload() -> dict: role="user", task_class="user_action", action_kind="review_handoff_material", - claimed_by="codex-value-explorer", + bound_agent=VALUE_AGENT, priority="P1", ) value_agent_todo = todo_item( @@ -276,6 +279,7 @@ def assert_other_agent_user_gate_does_not_block_current_agent() -> None: status_payload(), goal_id=GOAL_ID, agent_id="codex-main-control", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["should_run"] is True, payload assert payload["normal_delivery_allowed"] is True, payload @@ -307,13 +311,16 @@ def assert_current_user_action_does_not_preserve_other_agent_operator_gate() -> other_agent_gate_with_current_user_action_payload(), goal_id=GOAL_ID, agent_id=VALUE_AGENT, + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["decision"] == "run", payload assert payload["state"] == "eligible", payload assert payload["should_run"] is True, payload assert payload["normal_delivery_allowed"] is True, payload - assert payload["requires_user_action"] is True, payload - assert payload["notify_user_on_open_todo"] is True, payload + # A routed user_action is a non-blocking notice while live work proceeds; + # the foreign user_gate must not turn that notice into an execution gate. + assert payload["requires_user_action"] is False, payload + assert "notify_user_on_open_todo" not in payload, payload assert "notify_user_on_gate" not in payload, payload summary = payload["user_todo_summary"] @@ -331,9 +338,13 @@ def assert_current_user_action_does_not_preserve_other_agent_operator_gate() -> assert "scoped_user_gate_fallback" not in payload, payload contract = payload["interaction_contract"] - assert contract["mode"] == "bounded_delivery_with_user_notice", contract - assert contract["user_channel"]["action_required"] is True, contract + assert contract["mode"] == "bounded_delivery", contract + assert contract["user_channel"]["action_required"] is False, contract + assert contract["user_channel"]["non_blocking"] is True, contract assert contract["user_channel"]["notify"] == "NOTIFY", contract + assert contract["user_channel"]["actions"] == [ + "Review the value-lane handoff material when convenient." + ], contract assert contract["agent_channel"]["must_attempt"] is True, contract assert contract["agent_channel"]["delivery_allowed"] is True, contract @@ -343,6 +354,7 @@ def assert_other_agent_user_gate_does_not_notify_non_due_monitor_lane() -> None: other_agent_gate_with_non_due_monitor_payload(), goal_id=GOAL_ID, agent_id="codex-value-explorer", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["decision"] == "skip", payload assert payload["effective_action"] == "monitor_quiet_skip", payload @@ -369,6 +381,7 @@ def assert_target_agent_still_blocks_on_its_user_gate() -> None: status_payload(), goal_id=GOAL_ID, agent_id="codex-product-capability", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["should_run"] is False, payload assert payload["normal_delivery_allowed"] is False, payload @@ -389,6 +402,7 @@ def assert_unscoped_user_gate_requires_projection_repair() -> None: status_payload(blocks_agent=None), goal_id=GOAL_ID, agent_id="codex-main-control", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["decision"] == "self_repair", payload assert payload["should_run"] is True, payload @@ -412,6 +426,7 @@ def assert_explicit_global_gate_blocks_goal() -> None: status_payload(blocks_agent=None, global_gate=True), goal_id=GOAL_ID, agent_id="codex-main-control", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["should_run"] is False, payload assert payload["requires_user_action"] is True, payload @@ -463,6 +478,7 @@ def assert_unrelated_user_gate_allows_feishu_fallback() -> None: unrelated_gate_status_payload(), goal_id=GOAL_ID, agent_id="codex-main-control", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["should_run"] is True, payload assert payload["decision"] == "safe_bypass_user_gate_fallback", payload @@ -715,6 +731,7 @@ def assert_exact_todo_gate_only_blocks_target_todo() -> None: exact_todo_gate_status_payload(), goal_id=GOAL_ID, agent_id="codex-main-control", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["should_run"] is True, payload assert payload["decision"] == "safe_bypass_user_gate_fallback", payload @@ -742,6 +759,7 @@ def assert_exact_todo_gate_only_blocks_target_todo() -> None: exact_todo_gate_status_payload(include_fallback=False), goal_id=GOAL_ID, agent_id="codex-main-control", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert blocked_payload["should_run"] is False, blocked_payload assert blocked_payload["interaction_contract"]["mode"] == "user_gate", blocked_payload @@ -791,6 +809,7 @@ def assert_exact_todo_gate_survives_decision_scope_migration() -> None: exact_todo_gate_with_decision_scope_migration_payload(), goal_id=GOAL_ID, agent_id="codex-main-control", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["should_run"] is True, payload assert payload["decision"] == "safe_bypass_user_gate_fallback", payload @@ -819,6 +838,7 @@ def assert_exact_todo_gate_allows_independent_due_monitor() -> None: goal_id=GOAL_ID, agent_id="codex-main-control", available_capabilities=["network", "external_write"], + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["should_run"] is True, payload assert payload["decision"] == "safe_bypass_user_gate_fallback", payload @@ -842,6 +862,7 @@ def assert_exact_todo_gate_allows_independent_due_monitor() -> None: goal_id=GOAL_ID, agent_id="codex-main-control", available_capabilities=["network"], + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert blocked_payload["should_run"] is False, blocked_payload assert "scoped_user_gate_fallback" not in blocked_payload, blocked_payload @@ -888,6 +909,7 @@ def assert_monitor_debt_yields_to_equal_priority_advancement() -> None: goal_id=GOAL_ID, agent_id="codex-main-control", available_capabilities=["network", "external_write"], + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) arbitration = payload["monitor_debt_arbitration"] assert arbitration["active"] is True, arbitration @@ -909,6 +931,7 @@ def assert_monitor_debt_yields_to_equal_priority_advancement() -> None: goal_id=GOAL_ID, agent_id="codex-main-control", available_capabilities=["network", "external_write"], + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert "monitor_debt_arbitration" not in terminal_transition, terminal_transition terminal_fallback = terminal_transition["scoped_user_gate_fallback"] @@ -959,6 +982,7 @@ def assert_decision_scope_overrides_shared_action_kind_tokens() -> None: decision_scope_status_payload(), goal_id=GOAL_ID, agent_id="codex-main-control", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, ) assert payload["should_run"] is True, payload assert payload["decision"] == "safe_bypass_user_gate_fallback", payload From a21153e4f49cc37046b3bcae02f8a298a97d4ab8 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:23:10 +0800 Subject: [PATCH 61/98] test: qualify project vision and next-step canonical callers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../goal-vision-refresh-state-budget-smoke.py | 46 ++++++++++++++-- .../next-action-projection-contract-smoke.py | 55 ++++++++++++++++--- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/examples/project/goal-vision-refresh-state-budget-smoke.py b/examples/project/goal-vision-refresh-state-budget-smoke.py index 5a3af07ffe..73de3068c9 100644 --- a/examples/project/goal-vision-refresh-state-budget-smoke.py +++ b/examples/project/goal-vision-refresh-state-budget-smoke.py @@ -11,6 +11,16 @@ REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from loopx.control_plane.coordination.local_authority import ( # noqa: E402 + LocalCoordinationAuthorityUnavailable, + read_canonical_todos_if_promoted, +) +from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402 +from loopx.control_plane.todos.mutation_api import add_goal_todo # noqa: E402 + GOAL_ID = "goal-vision-budget-fixture" AGENT_ID = "research-curator" @@ -184,10 +194,27 @@ def write_json(path: Path, value: dict) -> None: path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") -def main() -> int: +def assert_vision_budget(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-goal-vision-budget-") as tmp: root = Path(tmp) registry_path, runtime, project = write_fixture(root) + state_path = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" + # A source fixture must be imported before any normal Todo writer. + source_before = state_path.read_bytes() + create_args = dict(registry_path=registry_path, goal_id=GOAL_ID, role="agent", + text="Validate the scoped delivery evidence.", task_class="advancement_task", + claimed_by=AGENT_ID, agent_id=AGENT_ID) + try: + add_goal_todo(**create_args) + except LocalCoordinationAuthorityUnavailable as error: + assert error.code == "canonical_authority_required", error + else: + raise AssertionError("unmigrated fixture must not admit a normal Todo write") + assert state_path.read_bytes() == source_before + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) is None + import_fixture_authority(registry_path, GOAL_ID, provider=provider) + original = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert original["source_authority"] == provider + "_v0" valid_path = root / "valid-vision.json" invalid_path = root / "invalid-vision.json" drift_path = root / "drift-vision.json" @@ -603,10 +630,14 @@ def main() -> int: assert "limit is 80" in long_todo["error"], long_todo assert "suggested compact value" in long_todo["error"], long_todo - from loopx.todos import add_goal_todo - add_goal_todo(registry_path=registry_path, goal_id=GOAL_ID, role="agent", - text="Validate the scoped delivery evidence.", task_class="advancement_task", - claimed_by=AGENT_ID) + created = add_goal_todo(**create_args) + canonical = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert len(canonical["todos"]) == len(original["todos"]) + 1 + rows = {row["todo_id"]: row for row in canonical["todos"]} + assert rows[created["todo_id"]]["claimed_by"] == AGENT_ID + assert rows[created["todo_id"]]["text"] == create_args["text"] + for row in original["todos"]: + assert rows[row["todo_id"]] == row local_next_action = "/Users/example/private/raw-task-note" private_next_action_result = payload( run_cli( @@ -689,6 +720,11 @@ def main() -> int: assert "vision_summary" in invalid["error"], invalid assert "suggested compact value" in invalid["error"], invalid + + +def main() -> int: + for provider in ("file", "sqlite"): + assert_vision_budget(provider) print("goal-vision-refresh-state-budget-smoke ok") return 0 diff --git a/examples/project/next-action-projection-contract-smoke.py b/examples/project/next-action-projection-contract-smoke.py index 314987dce5..75223875a1 100644 --- a/examples/project/next-action-projection-contract-smoke.py +++ b/examples/project/next-action-projection-contract-smoke.py @@ -8,10 +8,17 @@ import sys import tempfile -from loopx.control_plane.scheduler.execution_context import GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT -from loopx.quota import build_quota_should_run -from loopx.state_refresh import refresh_state_run -from loopx.status import collect_status +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 +from loopx.control_plane.scheduler.execution_context import GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT # noqa: E402 +from loopx.control_plane.testing.canary_harness import import_fixture_authority, run_json_cli # noqa: E402 +from loopx.control_plane.todos.mutation_api import complete_goal_todo # noqa: E402 +from loopx.quota import build_quota_should_run # noqa: E402 +from loopx.state_refresh import refresh_state_run # noqa: E402 +from loopx.status import collect_status # noqa: E402 GOAL_ID = "next-action-projection-goal" ACTIVE_NEXT_ACTION = "Shared compatibility guidance." @@ -93,9 +100,18 @@ def assert_state_next_action(path: Path, expected: str) -> None: -def main() -> None: +def assert_bound_projection(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-bound-recommendation-") as directory: registry, runtime, project, state = write_fixture(Path(directory)) + import_fixture_authority(registry, GOAL_ID, provider=provider) + original = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert original["source_authority"] == provider + "_v0" + assert {row["todo_id"] for row in original["todos"]} == {"todo_primary", "todo_side"} + # Finish the import's display delivery before checking that a task + # step does not change the already-current Markdown projection. + run_json_cli("todo", "project-markdown", "--goal-id", GOAL_ID, + "--provider-revision", original["provider_revision"], "--execute", + registry_path=registry, runtime_root=runtime, cwd=REPO_ROOT) before = state.read_bytes() common = dict(registry_path=registry, runtime_root_override=str(runtime), goal_id=GOAL_ID, project=project, state_file=None, classification="state_refreshed", @@ -133,7 +149,7 @@ def main() -> None: command = [sys.executable, "-m", "loopx.cli", "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", "status", "--goal-id", GOAL_ID, "--agent-id", "codex-side-bypass"] - readback = subprocess.run(command, capture_output=True, text=True, timeout=30) + readback = subprocess.run(command, cwd=REPO_ROOT, capture_output=True, text=True, timeout=30) assert readback.returncode == 0, readback.stdout + readback.stderr item = next(item for item in json.loads(readback.stdout)["attention_queue"]["items"] if item["goal_id"] == GOAL_ID) projected = item.get("agent_lane_next_action") or item["project_asset"]["agent_lane_next_action"] @@ -141,14 +157,37 @@ def main() -> None: assert projected["next_action_basis"] == lane["next_action_basis"] assert "next_action_basis" not in item # the selected route owns the basis markdown = subprocess.run([*command[:command.index("--format")], "--format", "markdown", - *command[command.index("--format") + 2:]], capture_output=True, text=True, timeout=30) + *command[command.index("--format") + 2:]], cwd=REPO_ROOT, capture_output=True, text=True, timeout=30) assert markdown.returncode == 0 and "next_step: " + step in markdown.stdout - # A different selected task does not inherit the old experiment. + # Display metadata cannot complete the authoritative selected task. state.write_text(state.read_text().replace("todo_side status=open", "todo_side status=done")) + assert state.read_bytes() != before + spoofed = collect_status(registry_path=registry, runtime_root_override=str(runtime), scan_roots=[project], limit=2) + spoofed_decision = build_quota_should_run(spoofed, goal_id=GOAL_ID, agent_id="codex-side-bypass", + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT) + assert spoofed_decision["agent_lane_next_action"]["todo_id"] == "todo_side" + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) == original + state.write_bytes(before) + + # Complete through the real owner: another selected task must not + # inherit the original task's retained recommendation receipt. + complete_goal_todo(registry_path=registry, goal_id=GOAL_ID, + todo_id="todo_side", agent_id="codex-side-bypass", evidence="Scoped fixture work verified.") + completed = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + before_rows = {row["todo_id"]: row for row in original["todos"]} + after_rows = {row["todo_id"]: row for row in completed["todos"]} + assert after_rows["todo_side"]["status"] == "done" + assert after_rows["todo_side"]["claimed_by"] == before_rows["todo_side"]["claimed_by"] + assert after_rows["todo_primary"] == before_rows["todo_primary"] changed = collect_status(registry_path=registry, runtime_root_override=str(runtime), scan_roots=[project], limit=2) next_decision = build_quota_should_run(changed, goal_id=GOAL_ID, agent_id="codex-side-bypass", scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT) assert step not in next_decision["interaction_contract"]["agent_channel"]["primary_action"] + + +def main() -> None: + for provider in ("file", "sqlite"): + assert_bound_projection(provider) print("next-action-projection-contract-smoke ok") From 604482f0cbd8ddc08ebae158a3c5d2effaaea63d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:14:02 +0800 Subject: [PATCH 62/98] fix: classify existing CLI help and sync candidate manual version Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/help_surface.py | 2 ++ man/loopx.1 | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/loopx/help_surface.py b/loopx/help_surface.py index 4e0324396d..899b848941 100644 --- a/loopx/help_surface.py +++ b/loopx/help_surface.py @@ -383,6 +383,7 @@ def register_command_reference(subparsers: argparse._SubParsersAction) -> None: "global-todos", "goal-actions", "goal-alignment", + "goal-direction", "amendment-proposal", "goal-amendment-proposal", "handoff-mode", @@ -410,6 +411,7 @@ def register_command_reference(subparsers: argparse._SubParsersAction) -> None: "value-connectors", "version", "worker-bridge", + "zcode-goal", } ) diff --git a/man/loopx.1 b/man/loopx.1 index 156ebef2a2..6ac065d1ca 100644 --- a/man/loopx.1 +++ b/man/loopx.1 @@ -1,4 +1,4 @@ -.TH LOOPX 1 "" "LoopX 1.4.0" "User Commands" +.TH LOOPX 1 "" "LoopX 2.0.0a1" "User Commands" .SH NAME loopx \- control-plane helper for long-running agent work .SH SYNOPSIS From 20c61653295016865fc10b83887e4b7dfcaa8d66 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:14:02 +0800 Subject: [PATCH 63/98] test: qualify public smoke prerequisites and structured contracts Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .github/workflows/full-public-smokes.yml | 5 ++- examples/frontstage-pages-workflow-smoke.py | 25 ++++++++++++++- examples/full-public-smokes-workflow-smoke.py | 2 +- ...acos-dashboard-launchagent-status-smoke.py | 12 +++++-- ...alue-connectors-finance-probe-doc-smoke.py | 32 +++++++++++++++---- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/.github/workflows/full-public-smokes.yml b/.github/workflows/full-public-smokes.yml index 587d151042..0f5d34e08c 100644 --- a/.github/workflows/full-public-smokes.yml +++ b/.github/workflows/full-public-smokes.yml @@ -95,7 +95,10 @@ jobs: run: sudo apt-get update && sudo apt-get install -y zsh - name: Install smoke runtime dependencies - run: python -m pip install --disable-pip-version-check "jsonschema>=4.23,<5" + # Budget probes import pytest. LaunchAgent qualification resolves the + # selected distribution outside the checkout, so install this source + # explicitly rather than relying on an unrelated runner installation. + run: python -m pip install --disable-pip-version-check "jsonschema>=4.23,<5" "pytest>=9.0.3,<10" --editable . - name: Install locked TypeScript parser for semantic production checks run: npm ci --ignore-scripts diff --git a/examples/frontstage-pages-workflow-smoke.py b/examples/frontstage-pages-workflow-smoke.py index 3795c23598..1eb78867ef 100644 --- a/examples/frontstage-pages-workflow-smoke.py +++ b/examples/frontstage-pages-workflow-smoke.py @@ -20,6 +20,12 @@ def assert_absent(text: str, needle: str) -> None: raise AssertionError(f"workflow must not reference {needle!r}") +def step_source(text: str, name: str) -> str: + marker = f" - name: {name}\n" + assert text.count(marker) == 1, name + return text.split(marker, 1)[1].split("\n - ", 1)[0] + + def _extract_paths_block(text: str, event: str) -> str: """Extract the paths list under a given event (pull_request or push).""" marker = f"\n {event}:" @@ -129,7 +135,6 @@ def main() -> int: ".codex/goals", ".goal-" + "harness/", "registry.global.json", - "GH_TOKEN: ${{ github.token }}", "enable-reward-write-api", "npm run dev", "npm run preview", @@ -144,6 +149,24 @@ def main() -> int: ]: assert_absent(text, forbidden) + # Release-asset resolution and complete stargazer history use distinct + # credentials. The ordinary token must never replace the stargazer PAT. + release_steps = ( + "Validate public-safe frontstage bundle", + "Export Pages frontstage artifact", + ) + for name in release_steps: + step = step_source(text, name) + assert_contains(step, "GH_TOKEN: ${{ github.token }}") + assert_contains(step, 'LOOPX_SITE_RESOLVE_DESKTOP_RELEASE: "1"') + assert_absent(step, "scripts/fetch-github-stargazers.sh") + assert text.count("GH_TOKEN: ${{ github.token }}") == len(release_steps) + stargazers = step_source(text, "Fetch complete GitHub stargazer history") + assert_contains(stargazers, "if: github.event_name != 'pull_request'") + assert_contains(stargazers, "GH_TOKEN: ${{ secrets.STAR_HISTORY_READ_TOKEN }}") + assert_contains(stargazers, "scripts/fetch-github-stargazers.sh") + assert_absent(stargazers, "GH_TOKEN: ${{ github.token }}") + # Detaching a non-published product must preserve its own qualification lane. python_tests = (WORKFLOW.parent / "python-tests.yml").read_text(encoding="utf-8") for required in [ diff --git a/examples/full-public-smokes-workflow-smoke.py b/examples/full-public-smokes-workflow-smoke.py index 486a68bf21..3f874bd781 100644 --- a/examples/full-public-smokes-workflow-smoke.py +++ b/examples/full-public-smokes-workflow-smoke.py @@ -49,7 +49,7 @@ def main() -> int: "--jobs \"${SMOKE_JOBS}\"", "--no-execute", "--json", - 'python -m pip install --disable-pip-version-check "jsonschema>=4.23,<5"', + 'python -m pip install --disable-pip-version-check "jsonschema>=4.23,<5" "pytest>=9.0.3,<10" --editable .', "actions/upload-artifact@", "$RUNNER_TEMP/loopx-smoke-results/full-public-shard-${{ matrix.shard }}.json", "${{ runner.temp }}/loopx-smoke-results/full-public-shard-${{ matrix.shard }}.json", diff --git a/examples/macos-dashboard-launchagent-status-smoke.py b/examples/macos-dashboard-launchagent-status-smoke.py index a005124053..32ebd20062 100644 --- a/examples/macos-dashboard-launchagent-status-smoke.py +++ b/examples/macos-dashboard-launchagent-status-smoke.py @@ -388,10 +388,12 @@ def main() -> int: "#!/usr/bin/env bash\n" "if [[ \"$*\" == *\"--format json doctor\"* ]]; then\n" " if [[ -n \"${FAKE_RUNTIME_IDENTITY:-}\" ]]; then\n" - " printf '{\"service_runtime_identity\":%s}\\n' \"$FAKE_RUNTIME_IDENTITY\"\n" + " printf '{\"service_runtime_identity\":%s,\"python\":{\"executable\":%s}}\\n' \"$FAKE_RUNTIME_IDENTITY\" " + + shlex.quote(json.dumps(sys.executable)) + "\n" " exit 0\n" " fi\n" - " printf '%s\\n' '{\"release_manifest\":{\"manifest\":{\"release_id\":\"current-release\",\"package\":{\"version\":\"0.5.3\"},\"source\":{\"git_commit\":\"current-revision\"}}}}'\n" + " printf '{\"release_manifest\":{\"manifest\":{\"release_id\":\"current-release\",\"package\":{\"version\":\"0.5.3\"},\"source\":{\"git_commit\":\"current-revision\"}}},\"python\":{\"executable\":%s}}\\n' " + + shlex.quote(json.dumps(sys.executable)) + "\n" " exit 0\n" "fi\n" "echo loopx \"$@\"\n", @@ -456,6 +458,12 @@ def main() -> int: assert f"export CODEX_HOME={(home / '.codex').resolve()};" in default_chat_plist, default_chat_plist assert "export LOOPX_PYTHON=" in default_plist, default_plist assert "export LOOPX_PYTHON=" in default_chat_plist, default_chat_plist + # The selected console distribution owns its interpreter; another + # Python version on PATH must not replace this current doctor fact. + for plist in (status_plist, chat_plist): + shell = plistlib.loads(plist.read_bytes())["ProgramArguments"][2] + words = [word.removesuffix(";") for word in shlex.split(shell)] + assert f"LOOPX_PYTHON={sys.executable}" in words, shell assert "/loopx --registry" in default_plist, default_plist assert "/loopx-canary" not in default_plist, default_plist assert not (home / "Library" / "LaunchAgents" / "com.loopx.dashboard.plist").exists(), "retired dashboard LaunchAgent should not be installed" diff --git a/examples/value-connectors-finance-probe-doc-smoke.py b/examples/value-connectors-finance-probe-doc-smoke.py index 921132a7ba..e5bf0f992e 100644 --- a/examples/value-connectors-finance-probe-doc-smoke.py +++ b/examples/value-connectors-finance-probe-doc-smoke.py @@ -3,6 +3,7 @@ from __future__ import annotations +import json from pathlib import Path import subprocess import sys @@ -43,13 +44,30 @@ def main() -> int: "finance_market_snapshot", ) assert completed.returncode == 0, completed.stderr - assert '"status": "migrated_to_extension"' in completed.stdout - assert ( - '"replacement_extension_id": "loopx-finance-value-discovery"' - in completed.stdout - ) - assert '"replacement_capability_id": null' in completed.stdout - assert '"legacy_connector_executes_finance": false' in completed.stdout + packet = json.loads(completed.stdout) + assert packet["ok"] is True, packet + if command == "source-map": + profiles = packet["source_profiles"] + elif command == "install-check": + profiles = packet["checks"] + else: + profiles = None + if profiles is not None: + assert len(profiles) == 1, profiles + profile = profiles[0] + assert profile["connector_id"] == "finance_market_snapshot", profile + assert profile["status"] == "migrated_to_extension", profile + migration = profile["migration"] + else: + migration = packet + assert migration["schema_version"] == "value_connector_extension_migration_v0", migration + assert migration["status"] == "migrated_to_extension", migration + assert migration["replacement_extension_id"] == "loopx-finance-value-discovery", migration + assert migration["replacement_capability_id"] is None, migration + truth = migration["truth_contract"] + assert truth["legacy_connector_executes_finance"] is False, truth + assert truth["external_reads_performed"] is False, truth + assert truth["external_writes_performed"] is False, truth doc = " ".join(DOC.read_text(encoding="utf-8").lower().split()) for marker in ( From 131203faee7b707e3f3bc6b3cfb27057a29341b5 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:04:30 +0800 Subject: [PATCH 64/98] fix: preserve settings readback in contributor integration Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../goal-capability-settings.tsx | 53 +++++++----- .../replan-cadence.mjs | 83 +++++++++++++------ .../scenario-context.mjs | 13 +++ 3 files changed, 100 insertions(+), 49 deletions(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx index af7f15dc16..cce3e0aa60 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx @@ -24,6 +24,7 @@ type CapabilityCatalogProps = Readonly<{ callbacks: PersonalWorkspaceCallbacks; catalog: CapabilityConfigurationCatalog; goalId: string; + loading: boolean; onApplied: () => Promise; notification?: WorkspaceGoalNotification; onNotificationChanged: () => void; @@ -60,8 +61,9 @@ function goalWriteDraft(selected: CapabilityConfigurationCatalog["capabilities"] return writable; } -function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ +function useCapabilityMutation({ goalId, loading, onApplied, selected, t }: Readonly<{ goalId: string; + loading: boolean; onApplied: () => Promise; selected: CapabilityConfigurationCatalog["capabilities"][number] | undefined; t: ReturnType["t"]; @@ -104,7 +106,7 @@ function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ }, [selected]); async function preview(configuration: Record | null) { - if (!selected || mutation.busy || !jsonValid) return; + if (!selected || loading || mutation.busy || !jsonValid) return; setMutation((current) => ({ ...current, busy: "preview", partialWrite: null })); setError(null); try { @@ -118,7 +120,7 @@ function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ } async function apply() { - if (!selected || !mutation.preview || mutation.busy || !jsonValid) return; + if (!selected || loading || !mutation.preview || mutation.busy || !jsonValid) return; setMutation((current) => ({ ...current, busy: "apply" })); setError(null); try { @@ -144,6 +146,7 @@ function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ } function changeDraft(key: string, value: unknown) { + if (loading || mutation.busy) return; setMutation((current) => ({ ...current, draft: selected?.capability_id === "periodic_report" @@ -156,7 +159,7 @@ function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ } function changeJson(text: string) { - if (!selected || mutation.busy) return; + if (!selected || loading || mutation.busy) return; setJsonDraft(text); const parsed = parseEditableCapabilityJson(selected.configuration_editor, text); setMutation((current) => ({ ...current, preview: null, draft: parsed @@ -170,7 +173,7 @@ function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ } function changeMode() { - if (mutation.busy || !jsonValid) return; + if (loading || mutation.busy || !jsonValid) return; if (editorMode === "guided" && selected) { const jsonValue = selected.capability_id === "todo_replan_cadence" ? goalWriteDraft(selected, mutation.draft, editorMode, mutation.cadenceOverride) @@ -191,7 +194,7 @@ function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{ function CapabilityMutationFeedback({ mutationError, onApplied, partialWrite, preview }: Readonly<{ mutationError: string | null; - onApplied: () => Promise; + onApplied: () => void; partialWrite: GoalConfigurationPartialWrite | null; preview: GoalConfigurationPreview | null; }>) { @@ -211,7 +214,7 @@ function CapabilityMutationFeedback({ mutationError, onApplied, partialWrite, pr : "capabilities.partialWriteDescription")}

{partialWrite.recommended_action} - +
) : null} {preview ? ( @@ -234,7 +237,7 @@ function CapabilityMutationFeedback({ mutationError, onApplied, partialWrite, pr ); } -function CapabilityCatalog({ callbacks, catalog, goalId, notification, onApplied, onNotificationChanged }: CapabilityCatalogProps) { +function CapabilityCatalog({ callbacks, catalog, goalId, loading, notification, onApplied, onNotificationChanged }: CapabilityCatalogProps) { const { locale, t } = useWorkspaceI18n(); const orderedCapabilities = useMemo( () => orderCapabilitiesForPresentation( @@ -255,9 +258,12 @@ function CapabilityCatalog({ callbacks, catalog, goalId, notification, onApplied () => selected ? localizeCapability(selected, locale) : undefined, [locale, selected], ); - const capabilityMutation = useCapabilityMutation({ goalId, onApplied, selected: localizedSelected, t }); + const capabilityMutation = useCapabilityMutation({ goalId, loading, onApplied, selected: localizedSelected, t }); const { apply, changeDraft, changeJson, changeMode, editorMode, jsonDraft, jsonValid, error: mutationError, mutation, preview: requestPreview } = capabilityMutation; const { busy, draft, partialWrite, preview } = mutation; + // A refreshed observation can replace `selected` and reset local mutation + // state. The existing catalog read still owns admission until it settles. + const editorBusy = loading || Boolean(busy); if (!selected || !localizedSelected) { return

{t("capabilities.empty")}

; @@ -269,12 +275,12 @@ function CapabilityCatalog({ callbacks, catalog, goalId, notification, onApplied ?? (!supportsGoal ? t("capabilities.machineOnly") : t("capabilities.previewOnly")); async function createPreview() { - if (!localizedSelected || !editorAvailable || busy || !jsonValid) return; + if (!localizedSelected || !editorAvailable || editorBusy || !jsonValid) return; await requestPreview(draft); } async function createClearPreview() { - if (!editorAvailable || busy) return; + if (!editorAvailable || editorBusy) return; await requestPreview(null); } @@ -298,7 +304,7 @@ function CapabilityCatalog({ callbacks, catalog, goalId, notification, onApplied {selected.capability_id === "progress_review" && selected.observation ?

{locale === "zh-CN" ? "审查证据读回" : "Review evidence readback"}

- +

{selected.observation.read_state === "unavailable" ? (locale === "zh-CN" ? "审查存储不可读,当前判断未知。" : "Review storage unavailable; current judgment unknown.") : selected.observation.read_state === "missing" ? (locale === "zh-CN" ? "尚无审查回执。" : "No review receipts yet.") : `${selected.observation.receipt_count} ${locale === "zh-CN" ? "条已读取回执" : "loaded receipts"}`}

@@ -357,7 +363,7 @@ function CapabilityCatalog({ callbacks, catalog, goalId, notification, onApplied {editorAvailable ? <> {editorMode === "json" || !localizedSelected.configuration_editor.fields.some((field) => field.key === "enabled" && field.input_kind === "boolean") ? (
- +
) : null} {editorMode === "guided" ?
@@ -367,16 +373,16 @@ function CapabilityCatalog({ callbacks, catalog, goalId, notification, onApplied : `Review interval follows ${selected.machine_current ? "device" : "product"} defaults; this Goal sets the stride strategy.`}

: null} {t("machine.editJson")}} + enabledAction={} />
: