From d2deb568e9fc37cdcea3ae4c4d0286d7766a1324 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:51:39 +0800 Subject: [PATCH 01/54] feat: require canonical authority for 2.0 normal writes Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../capability-localization.ts | 4 +- .../machine-configuration-settings.tsx | 4 +- loopx/__init__.py | 2 +- loopx/bootstrap.py | 56 +- .../coordination/legacy_writer_fence.py | 20 +- .../coordination/legacy_writer_fence.ts | 16 + .../coordination/local_authority_defaults.ts | 4 +- .../runtime_shadow_writer_adapter.py | 143 +- .../coordination/task_lease_acquire.ts | 4 +- .../coordination/task_lease_lifecycle.ts | 4 +- .../control_plane/effect_runtime_handlers.ts | 1 + .../scheduler/legacy_monitor_poll.py | 229 --- .../scheduler/monitor_poll_writeback.py | 6 +- loopx/control_plane/todos/handoff_mode.py | 69 +- loopx/control_plane/todos/legacy_mutation.py | 1533 ----------------- loopx/control_plane/todos/line_update.py | 364 ---- loopx/control_plane/todos/mutation_api.py | 48 +- .../work_items/task_lease_acquire.ts | 52 +- .../work_items/task_lease_acquire_decision.ts | 3 + .../work_items/task_lease_lifecycle.ts | 87 +- .../task_lease_lifecycle_decision.ts | 3 + .../work_items/team_plan_adapter.py | 76 +- loopx/todos.py | 8 - pyproject.toml | 2 +- 24 files changed, 114 insertions(+), 2624 deletions(-) delete mode 100644 loopx/control_plane/scheduler/legacy_monitor_poll.py delete mode 100644 loopx/control_plane/todos/legacy_mutation.py delete mode 100644 loopx/control_plane/todos/line_update.py diff --git a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts index 97d2250861..d5091ffcfb 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts +++ b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts @@ -150,7 +150,7 @@ const capabilityCopy: Record> = { const fieldCopy: Record = { en: { new_goal_provider: { label: "New Goal storage provider", description: "File or SQLite; frozen at creation, independently of execution policy." }, - canonical_creation: { label: "Create canonical authority", description: "Future Goals only. Disabled retains the post-promotion target; failures require the original creation retry." }, + canonical_creation: { label: "Create canonical authority", description: "Required for new Goals in 2.0. An older disabled setting remains visible until you enable it; existing Goals require import." }, new_goal_handoff_mode: { label: "New Goal execution policy", description: "soft_claim or hard_lease, used with canonical creation. Agents inherit the Goal policy; tool authority is unchanged." }, runtime_profile: { label: "Runtime profile", description: "Restricted keeps scoped LoopX reads only. Trusted owner enables normal host tools while protected operations retain separate checks." }, selection_policy: { label: "Selection policy", description: "Preferred allows an explicit user choice; pinned rejects another executor; flexible permits fallback only inside the eligible pool." }, @@ -185,7 +185,7 @@ const fieldCopy: Record = { }, "zh-CN": { new_goal_provider: { label: "新 Goal 的存储 provider", description: "选择 File 或 SQLite;创建时固定,与执行策略分别选择。" }, - canonical_creation: { label: "建立 canonical 权威存储", description: "仅影响此后新建的 Goal。关闭时仅固定晋升后的目标;失败须重试原创建操作。" }, + canonical_creation: { label: "建立 canonical 权威存储", description: "2.0 新建 Goal 必须启用。旧的关闭设置会保留显示,需自行启用;已有 Goal 需导入。" }, new_goal_handoff_mode: { label: "新 Goal 的执行策略", description: "canonical 创建使用 soft_claim 或 hard_lease。Agent 继承 Goal 策略;不授予工具权限。" }, runtime_profile: { label: "运行模式", description: "restricted 仅使用受限 LoopX 读取;trusted_owner 开放常规宿主工具,但受保护操作仍单独校验。" }, selection_policy: { label: "选择策略", description: "preferred 允许用户显式改选;pinned 拒绝其他执行器;flexible 只在已授权资源池内回退。" }, diff --git a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx index 283ee318bd..9018c9711b 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx @@ -382,8 +382,8 @@ export function MachineConfigurationSettings({ section, onChanged }: { section:
{locale === "zh-CN" ? "仅影响此后创建的 Goal" : "Future Goals only"}

{locale === "zh-CN" - ? "启用 canonical 创建后,新 Goal 直接建立权威存储,并采用所选执行策略;失败时须重试原创建操作。关闭时仅固定晋升后的目标存储。已有 Goal 的升级仍需备份、停止写入和结算租约;这里不授予工具权限。" - : "With canonical creation enabled, new Goals establish authority with the selected execution policy; retry the original operation after failure. Disabled only freezes the post-promotion target. Existing Goals still require backup, stopped writers and settled leases for upgrade; this setting grants no tool permissions."}

+ ? "2.0 新建 Goal 必须启用 canonical 创建,并采用所选执行策略。旧的关闭设置不会自动改变;启用后再重试创建。已有 Goal 需备份、停止写入并导入;这里不迁移已有数据,也不授予工具权限。" + : "2.0 requires canonical creation with the selected execution policy. An older disabled setting is not changed automatically; enable it before retrying creation. Back up, stop writers and import existing Goals separately. This setting neither migrates existing data nor grants tool permissions."}

) : null} 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/loopx/bootstrap.py b/loopx/bootstrap.py index 1ae08d3a67..8e318c9970 100644 --- a/loopx/bootstrap.py +++ b/loopx/bootstrap.py @@ -393,6 +393,9 @@ 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: + from .control_plane.coordination.legacy_writer_fence import reject_legacy_coordination_execution + reject_legacy_coordination_execution(goal_id) 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: @@ -415,24 +418,7 @@ def bootstrap_project( elif state_exists and force: state_action = "replaced" - repaired_state_text: str | None = None - repaired_todo_source_roles: list[str] = [] - if not canonical_creation and not canonical_existing and state_exists and state_action in { - "kept-existing", - "kept-existing-preserve-todos", - }: - repaired_state_text, repaired_todo_source_roles = repair_missing_todo_source_sections( - state_file.read_text(encoding="utf-8") - ) - todo_source_migration = ( - { - "schema_version": "todo_source_section_migration_v0", - "added_roles": repaired_todo_source_roles, - "applied": False, - } - if repaired_todo_source_roles - else None - ) + todo_source_migration = None dry_state_actions = { "created": "would-create", @@ -530,8 +516,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 # reads or rebuild checks. It alone decides whether unfinished creation @@ -540,7 +524,6 @@ def bootstrap_project( storage_selection = initialize_goal_storage_target(runtime_root, {**previous_goal, "state_file": str(state_file)}, registry_path=registry_path) canonical_reconnect = canonical_existing or (storage_selection is not None and storage_selection.get("authority_initialized") is True) - canonical_bootstrap_transport = canonical_creation with project_registry_transaction( registry_path, operation="bootstrap_registry", @@ -565,6 +548,9 @@ def bootstrap_project( goal_entry["coordination"]["storage_target"] = frozen frozen_target = goal_entry.get("coordination", {}).get("storage_target") or {} canonical_creation = frozen_target.get("schema_version") == "loopx_new_goal_storage_target_v1" + if not canonical_creation and not canonical_existing: + from .control_plane.coordination.legacy_writer_fence import reject_legacy_coordination_execution + reject_legacy_coordination_execution(goal_id) if canonical_creation and force and (current_goal is not None or state_file.exists()): raise ValueError("Canonical creation cannot rebuild existing state; restore or migrate through its owning operation") @@ -601,28 +587,10 @@ def bootstrap_project( execution_profile=execution_profile, handoff_mode=declared_handoff_mode, ) - elif not canonical_creation and not canonical_reconnect: - planned, repaired_todo_source_roles = repair_missing_todo_source_sections(original) if planned != original: - 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) + require_registry_source_write_allowed(registry_path=registry_path, runtime_root=runtime_root, + goal_id=goal_id, state_file=state_file) 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") current_registry["updated_at"] = updated_at.split("T")[0] current_registry["common_runtime_root"] = str(runtime_root) @@ -633,11 +601,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, @@ -648,7 +611,6 @@ def bootstrap_project( ) return { - **shadow_evidence, **({"authority_read": {key: existing_authority[key] for key in ( "provider_revision", "cursor", "source_authority", "decision_read_from_provider", "legacy_fallback_used", )}} if existing_authority is not None else {}), diff --git a/loopx/control_plane/coordination/legacy_writer_fence.py b/loopx/control_plane/coordination/legacy_writer_fence.py index f540942769..d8cf743c42 100644 --- a/loopx/control_plane/coordination/legacy_writer_fence.py +++ b/loopx/control_plane/coordination/legacy_writer_fence.py @@ -1,9 +1,9 @@ """Cheap Python entry guard for the TypeScript-owned coordination fence. -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 legacy execution is rejected by the shared TypeScript admission rule. +Historical receipt recovery and lease release retain the durable fence check; +this adapter skips that lookup only when no fence exists and delegates every +present-fence decision to the canonical handler. """ from __future__ import annotations @@ -13,7 +13,7 @@ import hashlib import re from pathlib import Path -from typing import Any, Iterator +from typing import Any, Iterator, NoReturn from ..effect_runtime import effect_runtime_result from ...file_lock import exclusive_cross_runtime_file_lock @@ -78,6 +78,16 @@ def __init__(self, message: str, *, code: str, payload: dict[str, Any]) -> None: self.payload = payload +def reject_legacy_coordination_execution(goal_id: str) -> NoReturn: + """Transport the shared 2.0 cutoff; receipt recovery must run first.""" + result = effect_runtime_result("coordination.local_authority.require_canonical", {"goal_id": goal_id}) + if (not isinstance(result, dict) or result.get("schema_version") != LEGACY_COORDINATION_WRITE_CHECK_RESULT_SCHEMA + or result.get("status") != "blocked"): + raise RuntimeError("canonical admission returned an invalid decision") + raise LegacyCoordinationWriterFenced(str(result["reason"]), code=str(result["reason_code"]), + payload={"write_check": result}) + + def legacy_coordination_writer_fence_path( *, runtime_root: Path, goal_id: str ) -> Path: diff --git a/loopx/control_plane/coordination/legacy_writer_fence.ts b/loopx/control_plane/coordination/legacy_writer_fence.ts index d28826f7ff..bb50a05a0c 100644 --- a/loopx/control_plane/coordination/legacy_writer_fence.ts +++ b/loopx/control_plane/coordination/legacy_writer_fence.ts @@ -80,6 +80,22 @@ export class LegacyCoordinationWriteError extends Error { } } +/** 2.0 fresh execution has no Markdown authority. Recovery uses the original + * fence/receipt owners below; calling this must never replace those checks. */ +export function legacyCoordinationExecutionRefusal(goalId: string): JsonObject { + requireAuthorityStoreId(goalId, "goal id"); + return { + schema_version: LEGACY_COORDINATION_WRITE_CHECK_RESULT_SCHEMA, + status: "blocked", reason_code: "canonical_authority_required", goal_id: goalId, + reason: "Normal writes require canonical authority; inspect and import the existing Goal with coordination-shadow prepare-import/apply-import, then review its ownership policy. Original receipts and lease release remain available; no source data was changed.", + }; +} + +export function rejectLegacyCoordinationExecution(goalId: string): never { + const refusal = legacyCoordinationExecutionRefusal(goalId); + throw new LegacyCoordinationWriteError(String(refusal.reason_code), refusal, String(refusal.reason)); +} + /** Call under the existing primary lock, before receipts, capture or bytes. */ export async function requireLegacyCoordinationPrimaryWriteAllowed(root: string, goalId: string): Promise { await requireShadowPrimaryWriteAllowed(root, goalId); diff --git a/loopx/control_plane/coordination/local_authority_defaults.ts b/loopx/control_plane/coordination/local_authority_defaults.ts index 42e0897acb..2d7e33c8ac 100644 --- a/loopx/control_plane/coordination/local_authority_defaults.ts +++ b/loopx/control_plane/coordination/local_authority_defaults.ts @@ -1,4 +1,5 @@ /** New-Goal target selection; never live inheritance or authority promotion. */ +import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; import type {JsonObject} from "../effect_program.ts"; import {requireJsonObject} from "../runtime_decode.ts"; import {requireAuthorityStoreId} from "./authority_store_codec.ts"; @@ -31,7 +32,8 @@ export async function manageNewGoalStorage(request: JsonObject): Promise New Goal authority, enable canonical_creation with soft_claim or hard_lease; existing Goals require coordination-shadow prepare-import/apply-import."); } if (request.action !== "initialize") throw new Error("Unknown new Goal storage action"); const target = requireJsonObject(request.target, "New Goal storage target"); diff --git a/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py b/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py index 167407d60b..1c22fb162e 100644 --- a/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py +++ b/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py @@ -1,4 +1,4 @@ -"""Transaction-capture orchestration for legacy Todo and lease writers.""" +"""Retained lease-release capture settlement and prose write compatibility.""" from __future__ import annotations @@ -12,145 +12,6 @@ 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( @@ -200,7 +61,5 @@ def settle_lease_runtime_shadow_capture( __all__ = [ - "begin_todo_runtime_shadow_capture", "settle_lease_runtime_shadow_capture", - "settle_todo_runtime_shadow_capture", ] diff --git a/loopx/control_plane/coordination/task_lease_acquire.ts b/loopx/control_plane/coordination/task_lease_acquire.ts index f2335e7150..792f38c165 100644 --- a/loopx/control_plane/coordination/task_lease_acquire.ts +++ b/loopx/control_plane/coordination/task_lease_acquire.ts @@ -80,7 +80,9 @@ export async function executeCanonicalTaskLeaseAcquire(store: AuthorityStore, ra const decision = decideTaskLeaseAcquire({handoff_mode: mode!, registered_agents: input.registered_agents, ...facts, command: input}); if (decision.outcome === "rejected" || decision.outcome === "conflict") { - return failed(decision.code, `canonical task lease acquire rejected: ${decision.code}`, { + return failed(decision.code, decision.code === "handoff_mode_retired" + ? "Legacy ownership cannot grant new execution; release retained leases and review handoff-mode plan-migration/migrate before acquiring again." + : `canonical task lease acquire rejected: ${decision.code}`, { ...(decision.code === "write_scope_conflict" ? {recommended_action: "Coordinate shared writes with the listed holders. For isolated code edits, use your own separate Git worktree and acquire with --write-worktree; file overlaps become integration advisories. A known same-checkout collision remains rejected. Never take over a foreign lease or use code-edit mode for shared runtime state."} : {}), handoff_mode: mode, expected_version: input.expected_version, actual_version: leaseVersion(facts.current), diff --git a/loopx/control_plane/coordination/task_lease_lifecycle.ts b/loopx/control_plane/coordination/task_lease_lifecycle.ts index f9c1f600bc..07e27cdcba 100644 --- a/loopx/control_plane/coordination/task_lease_lifecycle.ts +++ b/loopx/control_plane/coordination/task_lease_lifecycle.ts @@ -151,7 +151,9 @@ export async function executeCanonicalTaskLeaseLifecycle(store: AuthorityStore, ...(lease.write_repository == null ? {} : {write_repository: String(lease.write_repository)}), acquire_ttl_seconds: null} : null, command}); if (decision.outcome === "rejected" || decision.outcome === "conflict") { - return {...failed(decision.code, `canonical task lease ${input.operation} rejected: ${decision.code}`), + return {...failed(decision.code, decision.code === "handoff_mode_retired" + ? "Legacy ownership cannot renew or transfer execution; release retained leases and review handoff-mode plan-migration/migrate." + : `canonical task lease ${input.operation} rejected: ${decision.code}`), handoff_mode: mode, expected_version: input.expected_version, actual_version: leaseVersion(lease), ...(todo ? {todo_status: todo.status, claimed_by: todo.claimed_by ?? null, excluded_agents: excluded} : {})}; } diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index d07e0c1b2f..906ed4bc65 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -412,6 +412,7 @@ export function createEffectRuntimeHandlers( ["coordination.authority_archive.manage", lazyHandler(() => import("./coordination/local_authority_archive.ts"), ({manageLocalAuthorityArchive}) => manageLocalAuthorityArchive)], ["configuration.backup", lazyHandler(() => import("./configuration_backup.ts"), ({configurationBackupOperation}) => configurationBackupOperation)], ["coordination.sqlite_backup.snapshot", lazyHandler(() => import("./coordination/sqlite_backup.ts"), ({snapshotSqliteBackup}) => snapshotSqliteBackup)], + ["coordination.local_authority.require_canonical", lazyHandler(() => import("./coordination/legacy_writer_fence.ts"), ({legacyCoordinationExecutionRefusal}) => request => legacyCoordinationExecutionRefusal(requiredString(request.goal_id, "goal_id")))], ["coordination.local_authority.new_goal_storage", lazyHandler(() => import("./coordination/local_authority_defaults.ts"), ({manageNewGoalStorage}) => manageNewGoalStorage)], ["coordination.local_authority.promotion_review", lazyHandler(() => Promise.all([import("./coordination/local_authority_runtime.ts"), import("./coordination/source_transfer.ts")]), ([{reviewLocalCoordinationAuthorityPromotion}, {withCoordinationSourceTransfer}]) => withCoordinationSourceTransfer("coordination.local_authority.promotion_review", reviewLocalCoordinationAuthorityPromotion))], ["coordination.local_authority.promotion_reviewed", lazyHandler(() => import("./coordination/local_authority_runtime.ts"), ({executeReviewedCoordinationPromotion}) => executeReviewedCoordinationPromotion)], 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_poll_writeback.py b/loopx/control_plane/scheduler/monitor_poll_writeback.py index a686881282..a0ebbc4829 100644 --- a/loopx/control_plane/scheduler/monitor_poll_writeback.py +++ b/loopx/control_plane/scheduler/monitor_poll_writeback.py @@ -127,7 +127,5 @@ def write_monitor_poll_todo_state( 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) + from ..coordination.legacy_writer_fence import reject_legacy_coordination_execution + reject_legacy_coordination_execution(goal_id) diff --git a/loopx/control_plane/todos/handoff_mode.py b/loopx/control_plane/todos/handoff_mode.py index dd92a3558d..4d32b3c468 100644 --- a/loopx/control_plane/todos/handoff_mode.py +++ b/loopx/control_plane/todos/handoff_mode.py @@ -39,7 +39,7 @@ OwnershipGate, ownership_gate_requirement, ) -from ..goals.active_state_metadata import parse_state_frontmatter, split_state_frontmatter +from ..goals.active_state_metadata import parse_state_frontmatter from .contract import normalize_todo_claimed_by HANDOFF_MODE_SCHEMA_VERSION = "goal_handoff_mode_v0" @@ -353,68 +353,5 @@ def set_goal_handoff_mode( 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, - 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, - ) + from ..coordination.legacy_writer_fence import reject_legacy_coordination_execution + reject_legacy_coordination_execution(goal_id) diff --git a/loopx/control_plane/todos/legacy_mutation.py b/loopx/control_plane/todos/legacy_mutation.py deleted file mode 100644 index 8feeccbe3f..0000000000 --- a/loopx/control_plane/todos/legacy_mutation.py +++ /dev/null @@ -1,1533 +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") - from ...capabilities.explore.research_frontier import hold_research_completion_evidence - research_evidence = lease_fence_stack.enter_context(hold_research_completion_evidence( - registry_path=registry_path, runtime_root=shadow_runtime_root, goal_id=goal_id, - todo=completion_todo, state_text=original, - actor_agent_id=mutation_authority.get("actor_agent_id"), - )) - 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, - **({"capability_completion_evidence": research_evidence} if research_evidence is not None else {}), - "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, - ) - from ...capabilities.explore.research_frontier import hold_research_completion_evidence - research_completion_evidence = lease_fence_stack.enter_context(hold_research_completion_evidence( - registry_path=registry_path, runtime_root=shadow_runtime_root, goal_id=goal_id, - todo=authority_todo, state_text=original, actor_agent_id=mutation_authority.get("actor_agent_id"), - )) - 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, - **({"capability_completion_evidence": research_completion_evidence} if research_completion_evidence else {}), - "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..e6c1f360af 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. +Unpromoted sources remain readable for import; normal mutation requires authority. """ from __future__ import annotations @@ -17,6 +17,7 @@ normalize_todo_status, require_todo_excluded_agents, ) +from ..coordination.legacy_writer_fence import reject_legacy_coordination_execution from .active_state_editing import TODO_SECTION_HEADINGS from .addition import require_replan_successor_scope from . import completion_validation as completion_validation_module @@ -87,8 +88,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") @@ -211,25 +210,7 @@ def add_goal_todo( 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, - }) + reject_legacy_coordination_execution(goal_id) def _update_completion_validation_declaration( @@ -348,7 +329,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) @@ -490,12 +470,7 @@ def update_goal_todo( 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, - }) + reject_legacy_coordination_execution(goal_id) @provider_first_terminal_lifecycle("complete") @@ -537,10 +512,7 @@ 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) + reject_legacy_coordination_execution(goal_id) @provider_first_terminal_lifecycle("supersede") @@ -571,10 +543,7 @@ 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 _supersede_goal_todo_legacy(**call) + reject_legacy_coordination_execution(goal_id) @provider_first_terminal_lifecycle("archive") @@ -589,7 +558,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) + reject_legacy_coordination_execution(goal_id) diff --git a/loopx/control_plane/work_items/task_lease_acquire.ts b/loopx/control_plane/work_items/task_lease_acquire.ts index d66904913a..c6676ba9a5 100644 --- a/loopx/control_plane/work_items/task_lease_acquire.ts +++ b/loopx/control_plane/work_items/task_lease_acquire.ts @@ -2,11 +2,11 @@ import {observeLeaseWorktree, type LeaseWorkspace} from "./task_lease_workspace. import {isCanonicalTodoId} from "../todos/identity.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 {evaluateTaskLeaseWriteScopesOverlap, evaluateTaskLeaseAcquireDecision, 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 { ShadowManagementError } from "../coordination/shadow_management.ts"; import { parseIsoTimestamp } from "../runtime_timestamp.ts"; -import { LegacyCoordinationWriteError, requireLegacyCoordinationPrimaryWriteAllowed } from "../coordination/legacy_writer_fence.ts"; +import { LegacyCoordinationWriteError, rejectLegacyCoordinationExecution, requireLegacyCoordinationPrimaryWriteAllowed } from "../coordination/legacy_writer_fence.ts"; import { createHash } from "node:crypto"; import { readdir, readFile } from "node:fs/promises"; import { join } from "node:path"; @@ -15,7 +15,7 @@ import { EffectRuntimeLockTimeoutError, EffectRuntimeRequestError, } from "../effect_runtime_errors.ts"; -import { atomicWriteJson, withFileMutationLock } from "../effect_runtime_io.ts"; +import { withFileMutationLock } from "../effect_runtime_io.ts"; import { legacyCoordinationLeaseLockPath, taskLeaseLockPath, @@ -26,7 +26,6 @@ import { } from "../effect_program.ts"; import { requireJsonObject } from "../runtime_decode.ts"; import { - beginLeaseOutboxEntry, decodeLocalAuthorityShadowBinding, type LocalAuthorityShadowBinding, } from "../coordination/local_authority_shadow_outbox.ts"; @@ -972,48 +971,7 @@ async function commitAcquire( 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; + rejectLegacyCoordinationExecution(request.goal_id); } export async function executeTaskLeaseAcquire( diff --git a/loopx/control_plane/work_items/task_lease_acquire_decision.ts b/loopx/control_plane/work_items/task_lease_acquire_decision.ts index 478d57e9ef..02626dea63 100644 --- a/loopx/control_plane/work_items/task_lease_acquire_decision.ts +++ b/loopx/control_plane/work_items/task_lease_acquire_decision.ts @@ -374,6 +374,9 @@ export function decideTaskLeaseAcquire(input: AcquireDecisionInput): AcquireDeci ) { return acquireDecisionResult("rejected", "idempotency_key_reuse"); } + if (input.handoff_mode === "legacy") { + return acquireDecisionResult("rejected", "handoff_mode_retired"); + } const repository = normalizeTodoRepository(input.todo?.task_repository); const workspace = leaseWorkspace(command.write_workspace); if (workspace && workspace.repository.toLowerCase() !== repository?.toLowerCase()) { diff --git a/loopx/control_plane/work_items/task_lease_lifecycle.ts b/loopx/control_plane/work_items/task_lease_lifecycle.ts index 86f3c48cdb..58ce30b2ec 100644 --- a/loopx/control_plane/work_items/task_lease_lifecycle.ts +++ b/loopx/control_plane/work_items/task_lease_lifecycle.ts @@ -4,7 +4,7 @@ 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 { LegacyCoordinationWriteError, requireLegacyCoordinationPrimaryWriteAllowed } from "../coordination/legacy_writer_fence.ts"; +import { LegacyCoordinationWriteError, rejectLegacyCoordinationExecution, requireLegacyCoordinationPrimaryWriteAllowed } from "../coordination/legacy_writer_fence.ts"; import { createHash } from "node:crypto"; import { readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; @@ -1220,13 +1220,11 @@ async function ordinaryOperation( return replay; } - let todo: TodoFact | null = null; - if (request.operation !== "release") { - const authority = request.authority; - if (!authority) throw new TaskLeaseLifecycleError("authority is required", "authority_required"); - await revalidateAuthoritySources(authority.source_receipts); - todo = authorityTodo(request); - } + // Prepared-with-matching-bytes and committed receipts above are recovery. + // A missing/ambiguous effect cannot be renewed or transferred after cutoff. + if (request.operation !== "release") rejectLegacyCoordinationExecution(request.goal_id); + + const todo: TodoFact | null = null; const decision = ordinaryDecision(request, existing, todo, at); if (decision.outcome === "conflict" || decision.outcome === "rejected") { if ( @@ -1236,70 +1234,38 @@ async function ordinaryOperation( decision.code === "owner_excluded_from_todo" || decision.code === "owner_conflicts_with_claim" ) { - const rejectedOwner = request.operation === "transfer" && - decision.code === "owner_not_registered" && - !request.authority?.registered_agents.includes(request.owner!) - ? request.owner - : request.operation === "transfer" ? request.new_owner : request.owner; + const rejectedOwner = request.owner; throw ownerError(request, decision.code, todo, rejectedOwner); } throw transitionError(request, decision.code, existing, leasePath); } - if (request.operation === "release") { - if (decision.code === "lease_missing") { - return { - ok: true, - schema_version: "task_lease_v0", - action: "release", - released: false, - missing: true, - lease_path: leasePath, - ...(request.authority?.handoff_mode - ? { handoff_mode: request.authority.handoff_mode } - : {}), - settlement: lifecycleSettlement(request, "committed"), - }; - } - if (decision.code === "lease_release_replay" && existing !== null) { - return responseForOrdinary(request, leasePath, existing, true, { released: true }); - } - if (existing === null || decision.next_lease === null) { - throw transitionError(request, "invalid_lease_snapshot", existing, leasePath); - } - const next = releasedLease(existing, at); - 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); + if (decision.code === "lease_missing") { + return { + ok: true, + schema_version: "task_lease_v0", + action: "release", + released: false, + missing: true, + lease_path: leasePath, + ...(request.authority?.handoff_mode + ? { handoff_mode: request.authority.handoff_mode } + : {}), + settlement: lifecycleSettlement(request, "committed"), + }; + } + if (decision.code === "lease_release_replay" && existing !== null) { + return responseForOrdinary(request, leasePath, existing, true, { released: true }); } if (existing === null || decision.next_lease === null) { throw transitionError(request, "invalid_lease_snapshot", existing, leasePath); } - const next = materializeTaskLeaseLifecycle(existing, { - operation: request.operation as "renew" | "transfer", owner: request.owner!, - idempotency_key: request.idempotency_key!, expected_version: request.expected_version, - ttl_seconds: request.ttl_seconds, new_owner: request.new_owner, - new_idempotency_key: request.new_idempotency_key, - }, decision, at); - const response = responseForOrdinary( - request, - leasePath, - next, - false, - request.operation === "renew" ? { renewed: true } : { transferred: true }, - ); + const next = releasedLease(existing, at); + const response = responseForOrdinary(request, leasePath, next, false, { released: true }); 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}`, + request, existing, next, "task_lease_release", ); await atomicWriteJson(leasePath, next); await shadowCapture?.commit(); @@ -1533,6 +1499,7 @@ async function fenceVerify( ); } } + if (receipt === null) rejectLegacyCoordinationExecution(request.goal_id); const adoptedLock = await adoptFenceReceiptLock(request, receipt); const acquiredFreshLock = adoptedLock === null; lock = adoptedLock ?? await acquireFileMutationLock( diff --git a/loopx/control_plane/work_items/task_lease_lifecycle_decision.ts b/loopx/control_plane/work_items/task_lease_lifecycle_decision.ts index bc9eb64ced..c688eab6f6 100644 --- a/loopx/control_plane/work_items/task_lease_lifecycle_decision.ts +++ b/loopx/control_plane/work_items/task_lease_lifecycle_decision.ts @@ -266,6 +266,9 @@ export function decideTaskLeaseLifecycle( nextLease: { ...lease, active: false, status: "released" }, }); } + if (input.handoff_mode === "legacy") { + return result("rejected", "handoff_mode_retired"); + } if (lease === null || !lease.present || !lease.active) { return result("rejected", "lease_not_active"); } diff --git a/loopx/control_plane/work_items/team_plan_adapter.py b/loopx/control_plane/work_items/team_plan_adapter.py index 2316450d33..7c30b29896 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. +Normal writes use the existing AuthorityStore transaction and projection outbox. +Historical Markdown operation receipts remain readable after writer retirement. Source locks and historical receipt readers remain shared IO. Neither adapter grants receiver adoption or execution authority. """ @@ -24,7 +23,7 @@ ) 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 ...paths import effective_runtime_root @@ -42,18 +41,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: @@ -231,10 +218,9 @@ def previous_receipt(original: str) -> dict[str, Any] | None: result["projection_delivery"] = projected.get("projection_delivery") return result - # Lock the original source before consulting its receipt. A management hold - # stops fresh writes, not readback of an already-published operation. Keep - # receipt identity validation in the shared typed planner. - from ..coordination.legacy_writer_fence import require_legacy_todo_write_allowed + # Original receipts remain recoverable after fresh Markdown writes retire. + # R/T/S matches existing registry writers. No new capture or source write. + from ..coordination.legacy_writer_fence import reject_legacy_coordination_execution from ..runtime.document_io import verify_state_text_durable with exclusive_cross_runtime_file_lock(registry_path, operation="team_plan_commit"), \ @@ -254,52 +240,4 @@ def previous_receipt(original: str) -> dict[str, Any] | None: **request, "previous_receipt": previous}) verify_state_text_durable(state, original) return dict(replay["result"]) - require_legacy_todo_write_allowed( - registry_path=registry_path, runtime_root=runtime, goal_id=goal_id, state_file=state) - # Recovery above needs neither the source producer nor Markdown editing. - from ..coordination.runtime_shadow_writer_adapter import ( - begin_todo_runtime_shadow_capture, settle_todo_runtime_shadow_capture, - ) - from ..todos.active_state_editing import ( - insert_into_existing_section, insert_new_section, section_bounds, - replace_updated_at, - ) - from ..todos.contract import format_todo_metadata_line, parse_todo_metadata_line - - 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 - try: - planned = effect_runtime_result("work_items.team_plan.plan", { - **request, "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"]) - 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) + reject_legacy_coordination_execution(goal_id) diff --git a/loopx/todos.py b/loopx/todos.py index a9d37b9376..cc647b1ff0 100644 --- a/loopx/todos.py +++ b/loopx/todos.py @@ -130,14 +130,6 @@ "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 f31725bc9d..5eeaa7ed42 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 b95fe76cd1b2eb3e3ad72cb6a19ed535dc15685d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:51:39 +0800 Subject: [PATCH 02/54] test: qualify canonical cutoff and retained recovery journeys Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...-agent-continuation-state-machine-smoke.py | 22 +- .../todo-claim-visibility-lanes-smoke.py | 48 -- .../test_explore_research_evidence.py | 7 +- .../control_plane/fixtures/team_plan_1x.json | 80 +++ tests/control_plane/shadow_e2e_fixture.py | 30 ++ .../test_canonical_todo_writer_isolation.py | 29 +- .../test_canonical_update_dispatch.py | 14 +- .../test_canonical_writer_cutoff.py | 50 ++ .../test_cold_source_import_cli.py | 48 +- .../test_local_authority_shadow_runtime.py | 15 +- .../control_plane/test_native_monitor_poll.py | 10 +- .../test_new_goal_storage_defaults.py | 105 ++-- .../test_shadow_goal_instance.py | 81 +-- .../test_shadow_writer_boundaries.py | 462 ++---------------- tests/control_plane/test_task_lease.py | 67 +-- .../control_plane/test_team_plan_authority.py | 40 +- .../test_todo_authoring_scope.py | 40 +- .../test_todo_consumer_host_retirement.py | 13 +- .../test_todo_field_update_runtime.py | 278 +---------- .../canonical_writer_cutoff.test.ts | 95 ++++ tests/test_chat_team_plan_action.py | 24 +- tests/test_steward_team_plan_apply.py | 16 +- 22 files changed, 543 insertions(+), 1031 deletions(-) create mode 100644 tests/control_plane/fixtures/team_plan_1x.json create mode 100644 tests/control_plane/test_canonical_writer_cutoff.py create mode 100644 tests/control_plane_ts/canonical_writer_cutoff.test.ts 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..daf136ed54 100644 --- a/examples/control_plane/peer-agent-continuation-state-machine-smoke.py +++ b/examples/control_plane/peer-agent-continuation-state-machine-smoke.py @@ -80,6 +80,22 @@ def write_fixture(root: Path, *, include_review: bool = False) -> tuple[Path, Pa quota_allowed_slots=5, peer_independent_worktree_required=False, ) + # Import the synthetic historical source through the shipped migration + # entrypoint before exercising fresh peer continuation writes. + 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 / "backup", backup_id="peer-original", + include_automations=False, include_skills=False, include_registry_projects=False, + registry_path=registry_path)) + assert backup["ok"], backup + prepared = run_cli("coordination-shadow", "prepare-import", "--goal-id", GOAL_ID, + "--operation-id", "peer-original", "--backup-manifest", backup["manifest_path"], + "--provider", "file", "--target-handoff-mode", "soft_claim", + registry_path=registry_path, runtime=runtime, cwd=project)["cold_import"] + applied = run_cli("coordination-shadow", "apply-import", "--goal-id", GOAL_ID, + "--operation-id", "peer-original", "--plan-sha256", prepared["plan_sha256"], + "--writers-stopped", "--execute", registry_path=registry_path, runtime=runtime, cwd=project) + assert applied["ok"], applied return project, runtime, registry_path @@ -137,7 +153,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 +184,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/todo-claim-visibility-lanes-smoke.py b/examples/control_plane/todo-claim-visibility-lanes-smoke.py index 5b757ae8f4..5069516a2f 100644 --- a/examples/control_plane/todo-claim-visibility-lanes-smoke.py +++ b/examples/control_plane/todo-claim-visibility-lanes-smoke.py @@ -17,7 +17,6 @@ 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 work_lane_contract_fixtures import GOAL_ID, status_payload # noqa: E402 @@ -413,52 +412,6 @@ def assert_legacy_review_handoff_fails_closed_until_repaired() -> None: assert peer_scope["removed_continuation_blocked_count"] == 1 -def assert_legacy_review_handoff_write_requires_explicit_repair() -> None: - original_lines = [ - "## Agent Todo", - "", - "- [ ] [P0] Independently review the peer implementation.", - " ", - ] - - 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 - - def assert_claim_visibility_lanes_split_current_other_and_task_class() -> None: open_items = [ todo( @@ -520,7 +473,6 @@ def main() -> int: assert_executor_exclusion_filters_only_the_named_peer() assert_quota_routes_unclaimed_handoff_to_an_eligible_peer() assert_legacy_review_handoff_fails_closed_until_repaired() - assert_legacy_review_handoff_write_requires_explicit_repair() assert_claim_visibility_lanes_split_current_other_and_task_class() print("todo-claim-visibility-lanes-smoke ok") return 0 diff --git a/tests/capabilities/test_explore_research_evidence.py b/tests/capabilities/test_explore_research_evidence.py index 6941a6a78e..223f003919 100644 --- a/tests/capabilities/test_explore_research_evidence.py +++ b/tests/capabilities/test_explore_research_evidence.py @@ -179,7 +179,7 @@ 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]: project = tmp_path / "project" project.mkdir() state = project / "ACTIVE_GOAL_STATE.md" @@ -199,6 +199,11 @@ def execution_fixture(tmp_path: Path) -> tuple[Path, Path, Path, dict]: for name in ["a", "b"]: append_explore_result_event(path, build_explore_edge_event( goal_id=GOAL, from_node="joint", to_node=name, edge_type="depends_on")) + from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + initialize_canonical_authority(runtime, GOAL, + build_todo_runtime_shadow_projection(goal_id=GOAL, todos=[], handoff_mode="soft_claim"), + state_path=state, provider=provider) todo = add_goal_todo( registry_path=registry, goal_id=GOAL, role="agent", text="Run the bounded joint experiment.", task_class="advancement_task", action_kind="joint_probe", claimed_by="fixture-agent", diff --git a/tests/control_plane/fixtures/team_plan_1x.json b/tests/control_plane/fixtures/team_plan_1x.json new file mode 100644 index 0000000000..2679da2cce --- /dev/null +++ b/tests/control_plane/fixtures/team_plan_1x.json @@ -0,0 +1,80 @@ +{ + "source_revision": "60d833b13c85135587b725013b247e827663ffb9", + "plan": { + "schema_version": "steward_team_plan_preview_v0", + "kind": "steward_team_plan_preview", + "goal_id": "goal-a", + "proposal_id": "historical-plan", + "objective": "Independent lane outcomes", + "quota_envelope": { + "slots": 2 + }, + "stop_condition": "Owner ends request", + "lanes": [ + { + "lane_id": "lane-alpha", + "agent_id": "alpha", + "acceptance": "Return independent evidence", + "first_todo": { + "text": "Own work", + "priority": "P1", + "task_class": "advancement_task", + "action_kind": "implement" + } + } + ] + }, + "basis": "e56dfe4695a07f36678032267de2d3aca064a7ce1cdf69db4cec2367a11da105", + "source": "\n\n# Goal\n\n## Agent Todo\n\n- [ ] [P1] Own work\n \n\n", + "result": { + "action": "created", + "todo_id": "todo_0257c02b0bf76ba6c7c3ed4e", + "target_key": null, + "created_todo_ids": [ + "todo_0257c02b0bf76ba6c7c3ed4e" + ], + "lane_todo_ids": [ + "todo_0257c02b0bf76ba6c7c3ed4e" + ], + "lane_settlements": [ + { + "lane_id": "lane-alpha", + "agent_id": "alpha", + "priority": "P1", + "acceptance": "Return independent evidence", + "disposition": "created", + "todo_id": "todo_0257c02b0bf76ba6c7c3ed4e" + } + ], + "gap_count": 0, + "gap_lanes": [], + "reused_lane_count": 0, + "intent_basis": "sha256:0960f2bef565df76a1c073b25ca20c31db825c5b1b30ac66e03dc721b33b4c8e", + "lane_failure": null, + "coordination_runtime_shadow": { + "schema_version": "loopx_local_authority_shadow_evidence_v1", + "outcome": "no_transaction", + "reason_code": "shadow_disabled", + "goal_id": "goal-a", + "entry": { + "entry_id": null, + "partition": null, + "seq": null, + "partition_digest": null, + "source_bytes_digest": null + }, + "drain": null, + "capture_kind": "source_transaction_outbox", + "source_transaction_correlated": false, + "durable_source_outbox": false, + "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": null + } + } +} diff --git a/tests/control_plane/shadow_e2e_fixture.py b/tests/control_plane/shadow_e2e_fixture.py index 0469688896..bbf3b465ff 100644 --- a/tests/control_plane/shadow_e2e_fixture.py +++ b/tests/control_plane/shadow_e2e_fixture.py @@ -165,3 +165,33 @@ def workspace(path: Path, *, bootstrap: bool = True) -> ShadowWorkspace: boot = result.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"] assert boot["status"] == "applied", boot return result + + +def seed_legacy_todo_capture(workspace: ShadowWorkspace, text: str, todo_id: str) -> str: + """Seed a synthetic 1.x persistence boundary for recovery tests, not a writer API. + + The source/prepare/commit order is the historical compatibility contract. + Tests exercise the retained decoder and real CLI drain after this fixture. + """ + from loopx.control_plane.coordination.local_authority_shadow_outbox import TodoPartitionCapture + from loopx.control_plane.coordination.authority_source_capture import todo_partition_projector + from loopx.control_plane.coordination.shadow_goal_scope import shadow_goal_scope + from loopx.control_plane.todos.contract import format_todo_metadata_line + + original = workspace.state.read_text() + block = "- [ ] " + text + "\n" + format_todo_metadata_line( + todo_id=todo_id, status="open", task_class="advancement_task", action_kind="implement") + "\n" + assert "## Agent Todo\n" in original + updated = original.replace("## Agent Todo\n", "## Agent Todo\n\n" + block, 1) + with shadow_goal_scope(workspace.registry, goal_id=workspace.goal) as scope: + capture = TodoPartitionCapture.begin(enabled=True, registry_path=workspace.registry, + runtime_root=workspace.runtime, goal_id=workspace.goal, state_path=workspace.state, + write_class="todo_add", original_text=original, + projector=todo_partition_projector(scope.goal, state_path=workspace.state), goal_ref=scope.goal_ref) + capture.prepare(updated) + assert capture.outcome.recorded, capture.outcome + workspace.state.write_text(updated) + capture.committed() + assert capture.outcome.failure is None + assert capture.outcome.entry_id is not None + return capture.outcome.entry_id diff --git a/tests/control_plane/test_canonical_todo_writer_isolation.py b/tests/control_plane/test_canonical_todo_writer_isolation.py index 5fc6cd690a..07ff44aea1 100644 --- a/tests/control_plane/test_canonical_todo_writer_isolation.py +++ b/tests/control_plane/test_canonical_todo_writer_isolation.py @@ -18,7 +18,7 @@ import test_quota_settlement_cli as settlement -@pytest.mark.parametrize("module", ["mutation_api", "legacy_mutation"]) +@pytest.mark.parametrize("module", ["mutation_api"]) def test_todo_mutation_import_does_not_load_status(module): result = subprocess.run( [sys.executable, "-c", @@ -88,7 +88,7 @@ def without_source_todo_writers(isolated_todo_distribution): def _remove_source_todo_writers(package): - (package / "control_plane/todos/line_update.py").unlink() + (package / "control_plane/todos/line_update.py").unlink(missing_ok=True) (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() @@ -97,14 +97,14 @@ def _remove_source_todo_writers(package): "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 + 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() + (package / "control_plane/scheduler/legacy_monitor_poll.py").unlink(missing_ok=True) 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() @@ -115,14 +115,17 @@ def _remove_monitor_source_modules(package): def test_pending_source_outbox_survives_producer_retirement( tmp_path, isolated_todo_distribution, window, corrupt_cursor, ): - from shadow_e2e_fixture import workspace + from shadow_e2e_fixture import workspace, seed_legacy_todo_capture 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") + original = "todo_original" + seed_legacy_todo_capture(w, "Already delivered source work", original) + assert w.drain()["ok"] is True + seed_legacy_todo_capture(w, "Pending source work", "todo_pending") + w.crash(window, "authority-shadow", "drain") def history(): result = subprocess.run( @@ -454,7 +457,7 @@ 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_exports_are_absent(tmp_path): result = subprocess.run( [sys.executable, "-c", """ import sys @@ -462,11 +465,11 @@ def test_explicit_source_writer_import_preserves_the_existing_seam(tmp_path): 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) +from importlib.util import find_spec +assert find_spec('loopx.control_plane.todos.line_update') is None +assert find_spec('loopx.control_plane.todos.legacy_mutation') is None +for name in ('apply_todo_update_to_lines', 'link_generated_successor_todo_ids', 'link_superseding_todo_id', 'upsert_todo_metadata'): + assert not hasattr(todos, name), name try: getattr(todos, 'unknown_writer_export') except AttributeError: diff --git a/tests/control_plane/test_canonical_update_dispatch.py b/tests/control_plane/test_canonical_update_dispatch.py index 050f56b62f..e3b06c2303 100644 --- a/tests/control_plane/test_canonical_update_dispatch.py +++ b/tests/control_plane/test_canonical_update_dispatch.py @@ -147,7 +147,7 @@ def cli_update(tmp_path, monkeypatch, request, update_workspace): 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() + (package / "control_plane/todos/line_update.py").unlink(missing_ok=True) provenance = tmp_path / "cli-origin.txt" (package_root / "sitecustomize.py").write_text( "import importlib.util\nfrom pathlib import Path\n" @@ -299,7 +299,7 @@ def dispatch(method, request, **kwargs): assert caught.value.payload["recovery_command"][-1] == calls[0]["operation_id"] -def test_unpromoted_facade_preserves_empty_edit_no_change(tmp_path): +def test_unpromoted_facade_requires_import_even_for_empty_edit(tmp_path): project = tmp_path / "project" project.mkdir() state = project / "state.md" @@ -308,11 +308,11 @@ 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") + from loopx.control_plane.coordination.legacy_writer_fence import LegacyCoordinationWriterFenced + state.write_text(state.read_text() + "- [ ] Retain source work\n \n") 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 + with pytest.raises(LegacyCoordinationWriterFenced, match="canonical authority"): + update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id="todo_old", + agent_id="agent-a", **intent) assert state.read_bytes() == before diff --git a/tests/control_plane/test_canonical_writer_cutoff.py b/tests/control_plane/test_canonical_writer_cutoff.py new file mode 100644 index 0000000000..65a4d71189 --- /dev/null +++ b/tests/control_plane/test_canonical_writer_cutoff.py @@ -0,0 +1,50 @@ +"""2.0 normal writes refuse a cold source without changing its history.""" +import json + +import pytest + +from loopx.control_plane.coordination.legacy_writer_fence import LegacyCoordinationWriterFenced +from loopx.control_plane.todos.mutation_api import ( + add_goal_todo, update_goal_todo, complete_goal_todo, + supersede_goal_todo, archive_completed_todos, +) +from loopx.control_plane.todos.contract import format_todo_metadata_line +from loopx.control_plane.todos.list_readback import list_goal_todos +from loopx.control_plane.todos.handoff_mode import set_goal_handoff_mode + + +@pytest.fixture +def cold_goal(tmp_path): + runtime = tmp_path / "runtime" + state = tmp_path / "state.md" + state.write_text('---\ngoal_id: old-goal\nhandoff_mode: soft_claim\n---\n# 原始目标\n\n## Agent Todo\n\n' + '- [ ] 保留原内容\n' + format_todo_metadata_line( + todo_id="todo_original", status="open", task_class="advancement_task", + action_kind="implement") + '\n', encoding="utf-8") + registry = tmp_path / "registry.json" + registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{ + "id": "old-goal", "repo": str(tmp_path), "state_file": str(state), + "coordination": {"registered_agents": ["alpha"], "agent_model": "peer_v1"}}]})) + return registry, runtime, state + + +@pytest.mark.parametrize("operation", ["add", "update", "claim", "complete", "supersede", "archive", "handoff"]) +def test_cold_source_normal_writes_require_import_before_effect(cold_goal, operation): + registry, runtime, state = cold_goal + before = state.read_bytes(), registry.read_bytes() + common = dict(registry_path=registry, goal_id="old-goal", runtime_root_arg=str(runtime)) + calls = { + "add": lambda: add_goal_todo(**common, role="agent", text="New work", agent_id="alpha"), + "update": lambda: update_goal_todo(**common, todo_id="todo_original", text="Changed", agent_id="alpha"), + "claim": lambda: update_goal_todo(**common, todo_id="todo_original", claimed_by="alpha", agent_id="alpha"), + "complete": lambda: complete_goal_todo(**common, todo_id="todo_original", agent_id="alpha"), + "supersede": lambda: supersede_goal_todo(**common, todo_id="todo_original", agent_id="alpha", reason="replacement"), + "archive": lambda: archive_completed_todos(**common, dry_run=False), + "handoff": lambda: set_goal_handoff_mode(**common, mode="hard_lease"), + } + with pytest.raises(LegacyCoordinationWriterFenced, match="canonical authority.*import"): + calls[operation]() + assert (state.read_bytes(), registry.read_bytes()) == before + assert not list(runtime.rglob("*outbox*")) + # Inspection is still an import/recovery entrypoint. + assert list_goal_todos(**common, role="agent")["todos"][0]["text"] == "保留原内容" diff --git a/tests/control_plane/test_cold_source_import_cli.py b/tests/control_plane/test_cold_source_import_cli.py index 982dc2b3d9..cd645215f8 100644 --- a/tests/control_plane/test_cold_source_import_cli.py +++ b/tests/control_plane/test_cold_source_import_cli.py @@ -7,6 +7,7 @@ from __future__ import annotations import json +from datetime import datetime, timedelta, timezone import os from pathlib import Path import shutil @@ -84,7 +85,7 @@ def cli(action, *args, success=True, output_format="json", runtime_override=None @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( +def test_unpromoted_cutoff_retains_only_original_lease_replay_and_release( tmp_path, monkeypatch, old_producers_present, configuration, ): _, state, _, _, runtime, receiver, env = workspace(tmp_path, monkeypatch) @@ -130,33 +131,36 @@ def lease(action, *args, owner="agent-a", key="original", success=True): 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"]) + rejected = lease("acquire", "--ttl-seconds", 120, "--write-scope", "docs/**", success=False) + assert rejected["error_code"] == "canonical_authority_required" + path = runtime / "goals/cold/task-leases/todo_current.json" + assert not path.exists() + # Retained 1.x persistence contract, not a fresh acquisition through a retired writer. + now = datetime.now(timezone.utc) + original = {"schema_version": "task_lease_v0", "goal_id": "cold", "todo_id": "todo_current", + "owner": "agent-a", "idempotency_key": "original", "version": 1, "lease_epoch": 1, + "status": "active", "write_scopes": ["docs/**"], "acquire_ttl_seconds": 120, + "acquired_at": now.isoformat(), "updated_at": now.isoformat(), + "expires_at": (now + timedelta(seconds=120)).isoformat()} + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(original)) 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 replay["idempotent"] is True and replay["lease"] == original assert path.read_bytes() == original_lease - rejected = lease("renew", "--expected-version", version + 1, "--ttl-seconds", 180, success=False) + for action, extra in [("renew", ()), ("transfer", ("--new-owner", "agent-b", "--new-idempotency-key", "receiver"))]: + rejected = lease(action, "--expected-version", 1, "--ttl-seconds", 180, *extra, success=False) + assert rejected["error_code"] == "canonical_authority_required" + assert path.read_bytes() == original_lease + rejected = lease("release", "--expected-version", 2, 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") + released = lease("release", "--expected-version", 1) 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"] + assert released["lease"]["lease_epoch"] == 1 retained = path.read_bytes() - assert lease("release", "--expected-version", version + 2, - owner="agent-b", key="receiver")["idempotent"] is True + assert lease("release", "--expected-version", 1)["idempotent"] is True assert path.read_bytes() == retained @@ -294,7 +298,7 @@ def test_cold_cli_import_and_source_free_original_recovery( "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"): - (receiver / "loopx" / path).unlink() + (receiver / "loopx" / path).unlink(missing_ok=True) original = state.read_bytes() prepared = cli("prepare-import", "--backup-manifest", backup["manifest_path"], "--provider", provider, "--target-handoff-mode", "hard_lease")["cold_import"] @@ -338,7 +342,7 @@ def test_cold_import_recovery_preserves_later_canonical_writes( "control_plane/coordination/runtime_shadow_writer_adapter.py", "control_plane/coordination/local_authority_shadow_adapter.py", "control_plane/coordination/local_authority_shadow_outbox.py"): - (receiver / "loopx" / path).unlink() + (receiver / "loopx" / path).unlink(missing_ok=True) prepared = cli("prepare-import", "--backup-manifest", backup["manifest_path"], "--provider", provider, "--target-handoff-mode", "hard_lease")["cold_import"] digest = prepared["plan_sha256"] diff --git a/tests/control_plane/test_local_authority_shadow_runtime.py b/tests/control_plane/test_local_authority_shadow_runtime.py index 9a5629e2af..4c83f9fc51 100644 --- a/tests/control_plane/test_local_authority_shadow_runtime.py +++ b/tests/control_plane/test_local_authority_shadow_runtime.py @@ -5,6 +5,10 @@ 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 loopx.control_plane.coordination.legacy_writer_fence import LegacyCoordinationWriterFenced + from loopx.control_plane.work_items.task_lease import ( acquire_task_lease, release_task_lease, @@ -77,13 +81,22 @@ def _add(registry: Path) -> dict: @pytest.mark.parametrize("enabled", [False, True]) -def test_retired_and_absent_settings_leave_public_writers_on_primary_only(tmp_path: Path, enabled: bool) -> None: +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_retired_settings_preserve_history_and_require_canonical_writes(tmp_path: Path, monkeypatch, enabled: bool, provider: str) -> 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 + source_before = state.read_bytes(), registry.read_bytes() + with pytest.raises(LegacyCoordinationWriterFenced, match="canonical authority"): + _add(registry) + assert (state.read_bytes(), registry.read_bytes()) == source_before + isolate_sqlite_runtime(tmp_path, monkeypatch) + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, handoff_mode="hard_lease", todos=[]), + state_path=state, provider=provider) added = _add(registry) todo_id = added["todo_id"] acquired = acquire_task_lease(registry_path=registry, runtime_root=runtime, goal_id=GOAL_ID, diff --git a/tests/control_plane/test_native_monitor_poll.py b/tests/control_plane/test_native_monitor_poll.py index 8f8fd64e2d..553727631d 100644 --- a/tests/control_plane/test_native_monitor_poll.py +++ b/tests/control_plane/test_native_monitor_poll.py @@ -6,7 +6,7 @@ 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 @@ -16,8 +16,12 @@ 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") + # Import fixture: historical bytes are input, never produced by a retired writer. + from loopx.control_plane.todos.contract import format_todo_metadata_line + monitor = {"todo_id": "todo_imported_monitor"} + state.write_text(state.read_text() + "- [ ] Observe a public target\n" + format_todo_metadata_line( + todo_id=monitor["todo_id"], status="open", task_class="continuous_monitor", claimed_by=AGENT_ID, + target_key="public-watch", cadence="1h", next_due_at="2000-01-01T00:00:00Z", watch_only="true") + "\n") items = list_goal_todos(registry_path=registry, goal_id=GOAL_ID, role="agent")["todos"] leases = [] if lease is not None: diff --git a/tests/control_plane/test_new_goal_storage_defaults.py b/tests/control_plane/test_new_goal_storage_defaults.py index 744a47ce4a..d9dafee837 100644 --- a/tests/control_plane/test_new_goal_storage_defaults.py +++ b/tests/control_plane/test_new_goal_storage_defaults.py @@ -43,34 +43,33 @@ def marker(goal="first"): restart_effect_runtime() -def test_creation_freezes_target_and_reconnect_does_not_follow_changed_defaults(environment): +def test_canonical_creation_freezes_target_and_reconnect_does_not_follow_changed_defaults(environment): configure, bootstrap, marker, _, _ = environment - configure("sqlite") + configure("sqlite", mode="hard_lease") preview = bootstrap("first", "--dry-run") assert preview["storage_target"]["provider"] == "sqlite" assert not marker().exists() actual = bootstrap() - assert actual["storage_selection"]["promotion_performed"] is False - assert json.loads(marker().read_text())["provider"] == "sqlite" - configure("file") + assert actual["storage_selection"]["authority_initialized"] is True + configure("file", mode="soft_claim") assert bootstrap()["storage_selection"]["provider"] == "sqlite" assert bootstrap("second")["storage_selection"]["provider"] == "file" - assert not marker("second").exists() # An explicit File selector requires a committed head. -def test_existing_implicit_file_goal_is_not_retargeted(environment): - configure, bootstrap, marker, project, _ = environment - # A supported historical Goal has no creation target, independently of the - # current release's default for a new Goal. - configure("file") - bootstrap() +def test_existing_implicit_file_goal_requires_import_without_retargeting(environment): + configure, bootstrap, marker, project, runtime = environment + state = project / "state.md" + state.write_text("---\ngoal_id: first\n---\n## Agent Todo\n\n- [ ] Keep original work\n") registry = project / ".loopx/registry.json" - data = json.loads(registry.read_text()) - data["goals"][0]["coordination"].pop("storage_target") - registry.write_text(json.dumps(data)) - configure("sqlite") - assert bootstrap("first", "--dry-run")["storage_target"] is None - assert bootstrap()["storage_selection"] is None + registry.parent.mkdir() + registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{ + "id": "first", "repo": str(project), "state_file": str(state)}]})) + original = registry.read_bytes(), state.read_bytes() + configure("sqlite", mode="hard_lease") + for flags in [("--dry-run",), ()]: + rejected = bootstrap("first", *flags, expected_code=1) + assert "import" in rejected["error"] + assert (registry.read_bytes(), state.read_bytes()) == original assert not marker().exists() @@ -107,16 +106,16 @@ 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): +def test_explicit_canonical_creation_disabled_requires_settings_update(environment, provider): _, bootstrap, _, _, 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 "requires canonical creation" in rejected["error"] + assert "Settings" in rejected["error"] + assert not (runtime / "authority-transition").exists() @pytest.mark.parametrize("invalid_configuration", ["invalid_json", "invalid_namespace", "directory", "broken_symlink"]) @@ -233,19 +232,24 @@ 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. - configure("file") - bootstrap() - registry = project / ".loopx/registry.json" - data = json.loads(registry.read_text()) - data["goals"][0].setdefault("coordination", {})["storage_target"] = { +def test_pending_legacy_creation_requires_import_without_retargeting(environment): + from tests.control_plane.shadow_e2e_fixture import workspace + configure, bootstrap, _, project, runtime = environment + w = workspace(project, bootstrap=False) + data = json.loads(w.registry.read_text()) + data["common_runtime_root"] = str(runtime) + data["goals"][0]["id"] = "first" + data["goals"][0]["coordination"]["storage_target"] = { "schema_version": "loopx_new_goal_storage_target_v0", "provider": "sqlite"} + registry = project / ".loopx/registry.json" + registry.parent.mkdir() registry.write_text(json.dumps(data)) - configure("file") - assert bootstrap()["storage_selection"]["provider"] == "sqlite" - assert json.loads(marker().read_text())["provider"] == "sqlite" + before = registry.read_bytes(), w.state.read_bytes() + configure("file", mode="soft_claim") + rejected = bootstrap(expected_code=1) + assert "import" in rejected["error"] + assert (registry.read_bytes(), w.state.read_bytes()) == before + assert not (runtime / "authority").exists() @pytest.fixture @@ -629,7 +633,7 @@ 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.control_plane.coordination import new_goal_storage_transport as goal_storage from loopx.todos import add_goal_todo @@ -756,7 +760,7 @@ def test_app_creation_cannot_adopt_a_competing_goal(environment, app, monkeypatc def create_competitor(): # Real CLI registration, then model its durable pre-initialization # boundary independently. No fake success receipt or provider is used. - configure("file") + configure("file", mode="hard_lease") bootstrap("competing", "--project", str(other_project)) data = json.loads(registry.read_text()) goal = next(g for g in data["goals"] if g["id"] == "competing") @@ -803,21 +807,20 @@ 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): - from tests.control_plane.shadow_e2e_fixture import ShadowWorkspace - configure, bootstrap, _, project, runtime = environment - configure(provider) - bootstrap() - registry = project / ".loopx/registry.json" - state = project / ".codex/goals/first/ACTIVE_GOAL_STATE.md" - ws = ShadowWorkspace(registry, runtime, state, "first") - configured = ws.cli("configure-goal", "--coordination-runtime-shadow-file", "--execute") - assert configured["ok"] is True +def test_historical_target_is_used_by_reviewed_promotion(environment, provider): + from tests.control_plane.shadow_e2e_fixture import workspace, seed_legacy_todo_capture + from loopx.capabilities.machine_configuration.goal_storage import initialize_goal_storage_target + _, _, _, project, _ = environment + ws = workspace(project, bootstrap=False) + data = json.loads(ws.registry.read_text()) + data["goals"][0]["coordination"]["storage_target"] = { + "schema_version": "loopx_new_goal_storage_target_v0", "provider": provider} + ws.registry.write_text(json.dumps(data)) + assert initialize_goal_storage_target(ws.runtime, data["goals"][0])["promotion_performed"] is False 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 + seed_legacy_todo_capture(ws, f"Historical target work {n}", f"todo_history_{n}") + assert ws.drain(budget_seconds="60")["ok"] is True preview = ws.cli("coordination-shadow", "promote", "--handoff-mode-migration", "preserve") assert preview["promotion"]["status"] == "preview_ready", preview plan = project / "reviewed.json" @@ -829,5 +832,5 @@ def test_created_target_is_used_by_reviewed_promotion(environment, provider): # The initializer must not override a promoted selection. The separate legacy # bootstrap command still rejects a fenced Goal before reaching this helper. from loopx.control_plane.coordination.new_goal_storage_transport import initialize_goal_storage_target - goal = json.loads(registry.read_text())["goals"][0] - assert initialize_goal_storage_target(runtime, goal)["status"] == "existing_authority_preserved" + goal = json.loads(ws.registry.read_text())["goals"][0] + assert initialize_goal_storage_target(ws.runtime, goal)["status"] == "existing_authority_preserved" diff --git a/tests/control_plane/test_shadow_goal_instance.py b/tests/control_plane/test_shadow_goal_instance.py index ff979fbd17..a8188377d8 100644 --- a/tests/control_plane/test_shadow_goal_instance.py +++ b/tests/control_plane/test_shadow_goal_instance.py @@ -14,12 +14,9 @@ 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 shadow_e2e_fixture import ShadowWorkspace, seed_legacy_todo_capture +from loopx.control_plane.todos.mutation_api import add_goal_todo 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 @@ -31,7 +28,7 @@ FreshSourceSessionRegistration, register_fresh_source_session_project, ) -from loopx.control_plane.projects.registry_codec import load_project_registry +from loopx.control_plane.projects.registry_codec import load_project_registry, ProjectRegistryProtocolError from loopx.registry import find_registry_goal @@ -125,33 +122,9 @@ def _add_todo( 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 + # Historical persisted input only; the 2.0 public producer is absent. + return seed_legacy_todo_capture( + ShadowWorkspace(registry_path, runtime_root, state_path, goal=GOAL_ID), text, todo_id) def _outbox_bytes(runtime_root: Path) -> dict[str, bytes]: @@ -260,15 +233,6 @@ def test_recreated_goal_cannot_capture_or_drain_the_retired_shadow( ) 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, @@ -280,17 +244,9 @@ def test_recreated_goal_cannot_capture_or_drain_the_retired_shadow( 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", - ), - ) + with pytest.raises(ProjectRegistryProtocolError, match="lifecycle-only profile"): + add_goal_todo(registry_path=registry_path, runtime_root_arg=str(runtime_root), + goal_id=GOAL_ID, role="agent", text="Continue retired source work", agent_id=AGENT_ID) assert state_path.read_text(encoding="utf-8") == in_flight_original stale = adapter.drain_local_authority_shadow_outbox( @@ -306,25 +262,6 @@ def test_recreated_goal_cannot_capture_or_drain_the_retired_shadow( 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( diff --git a/tests/control_plane/test_shadow_writer_boundaries.py b/tests/control_plane/test_shadow_writer_boundaries.py index 71be3c41b1..eb8eaa3fad 100644 --- a/tests/control_plane/test_shadow_writer_boundaries.py +++ b/tests/control_plane/test_shadow_writer_boundaries.py @@ -4,7 +4,6 @@ import json import subprocess import sys -import time from pathlib import Path import pytest @@ -42,6 +41,38 @@ def fixture(tmp_path: Path) -> tuple[Path, Path, Path]: return registry, state, root +def seed_historical_todo(state: Path, *, text: str, **metadata) -> None: + """Persist a synthetic pre-cutoff source for unchanged prose/recovery checks.""" + from loopx.control_plane.todos.contract import format_todo_metadata_line + block = "- [ ] " + text + "\n" + format_todo_metadata_line(todo_id="todo_retained", **metadata) + "\n" + state.write_text(state.read_text().replace("## Agent Todo\n", "## Agent Todo\n\n" + block, 1)) + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_cutoff_preserves_active_shadow_history_when_configuration_changes(tmp_path, enabled): + from shadow_e2e_fixture import ShadowWorkspace, seed_legacy_todo_capture + registry, state, runtime = 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") + seed_legacy_todo_capture(ShadowWorkspace(registry, runtime, state, goal=GOAL), "Original pending work", "todo_pending") + value["goals"][0]["coordination"]["runtime_shadow"]["enabled"] = enabled + registry.write_text(json.dumps(value)) + original = state.read_bytes(), registry.read_bytes() + def history(): + return {str(p.relative_to(runtime)): p.read_bytes() for p in runtime.rglob("*.json")} + retained = history() + for override in [None, str(tmp_path / "override")]: + with pytest.raises(LegacyCoordinationWriterFenced, match="canonical authority"): + add_goal_todo(registry_path=registry, goal_id=GOAL, runtime_root_arg=override, + role="agent", text="Must import first", agent_id="agent-a") + assert (state.read_bytes(), registry.read_bytes()) == original + assert history() == retained + assert ShadowWorkspace(registry, runtime, state, goal=GOAL).drain()["outcome"] == "drained" + + @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( @@ -230,128 +261,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" @@ -383,154 +292,6 @@ def replace(source, target): 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) @@ -556,10 +317,7 @@ def test_prose_guard_ignores_resume_evaluation_clock( ) registry, state, root = fixture(tmp_path) - add_goal_todo( - registry_path=registry, - goal_id=GOAL, - role="agent", + seed_historical_todo(state, text="Resume the bounded inspection.", status="deferred", resume_when="resume_at:2099-01-01T00:00:00Z", @@ -614,7 +372,6 @@ def project_at_distinct_time(text): ) - @pytest.mark.parametrize("original_value,planned_value", [(True, 1), (False, 0)]) def test_prose_guard_preserves_json_scalar_identity( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -630,7 +387,7 @@ def test_prose_guard_preserves_json_scalar_identity( ) registry, state, root = fixture(tmp_path) - add_goal_todo(registry_path=registry, goal_id=GOAL, role="agent", text="Retain result facts.") + seed_historical_todo(state, text="Retain result facts.") original = state.read_text(encoding="utf-8") planned = original + "\nA narrative observation.\n" real_projector = adapter.todo_partition_projector @@ -750,27 +507,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 @@ -785,7 +521,7 @@ def test_override_root_cannot_bypass_registry_source_maintenance(tmp_path: Path, management.parent.mkdir(parents=True) management.write_text("{}") before = state.read_bytes() - with pytest.raises(ShadowManagementError): + with pytest.raises(LegacyCoordinationWriterFenced if writer == "todo" else ShadowManagementError): if writer == "todo": add_goal_todo( registry_path=registry, goal_id=GOAL, runtime_root_arg=str(override), @@ -818,59 +554,6 @@ def test_override_root_keeps_prose_writable_with_an_active_source_binding(tmp_pa 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" @@ -914,82 +597,9 @@ def test_registry_missing_state_reconstruction_respects_maintenance(tmp_path: Pa 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_historical_todo(state, text="Inspect the bounded result.", task_class="advancement_task", claimed_by="agent-a") 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_task_lease.py b/tests/control_plane/test_task_lease.py index cef9f73541..43b0fbb3c7 100644 --- a/tests/control_plane/test_task_lease.py +++ b/tests/control_plane/test_task_lease.py @@ -22,7 +22,7 @@ transfer_task_lease, write_scopes_overlap, ) -from loopx.todos import add_goal_todo +from loopx.control_plane.todos.contract import format_todo_metadata_line @pytest.mark.parametrize("raw", [None, b'{"status":"active"}', b"{broken", b"\xff"], @@ -173,7 +173,7 @@ def test_bool_disguised_lease_integers_fail_closed() -> None: assert error.value.code == "corrupt_lease" -def test_retained_task_lease_lifecycle_preserves_versions( +def test_cold_source_refuses_renew_transfer_but_preserves_release_cas( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -214,14 +214,9 @@ def test_retained_task_lease_lifecycle_preserves_versions( ), 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"]) + todo_id = "todo_retained_lease" + state.write_text(state.read_text() + "- [ ] Historical work\n" + + format_todo_metadata_line(todo_id=todo_id, status="open", task_class="advancement_task") + "\n") lease_path = task_lease.task_lease_path( runtime_root=runtime_root, goal_id="goal-a", @@ -270,40 +265,24 @@ def test_retained_task_lease_lifecycle_preserves_versions( ) 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 + before = lease_path.read_bytes(), state.read_bytes(), registry_path.read_bytes() + for operation in (renew_task_lease, transfer_task_lease): + extra = ({"new_owner": "agent-b", "new_idempotency_key": "turn-2"} + if operation is transfer_task_lease else {}) + with pytest.raises(TaskLeaseError) as refusal: + operation(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, **extra) + assert refusal.value.code == "canonical_authority_required" + assert (lease_path.read_bytes(), state.read_bytes(), registry_path.read_bytes()) == before - transferred = transfer_task_lease( - registry_path=registry_path, + released = release_task_lease( 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, + expected_version=1, ) assert released["released"] is True assert released["lease"]["status"] == "released" @@ -318,9 +297,9 @@ def test_retained_task_lease_lifecycle_preserves_versions( runtime_root=runtime_root, goal_id="goal-a", todo_id=todo_id, - owner="agent-b", - idempotency_key="turn-2", - expected_version=3, + owner="agent-a", + idempotency_key="turn-1", + expected_version=1, ) assert replayed_release["released"] is True assert replayed_release["idempotent"] is True @@ -331,8 +310,8 @@ def test_retained_task_lease_lifecycle_preserves_versions( runtime_root=runtime_root, goal_id="goal-a", todo_id=todo_id, - owner="agent-b", - idempotency_key="turn-2", + owner="agent-a", + idempotency_key="turn-1", ) assert missing_version.value.code == "version_required" @@ -380,7 +359,7 @@ def test_lifecycle_mutation_fails_closed_without_registry( expected_version=1, ) - assert error.value.code == "todo_not_found" + assert error.value.code == "canonical_authority_required" assert task_lease.read_lease(lease_path) == before diff --git a/tests/control_plane/test_team_plan_authority.py b/tests/control_plane/test_team_plan_authority.py index 1d9b369166..03442395a9 100644 --- a/tests/control_plane/test_team_plan_authority.py +++ b/tests/control_plane/test_team_plan_authority.py @@ -446,36 +446,42 @@ 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.""" + """Frozen 1.x source is recovered before/after import; 2.0 never recreates it.""" from loopx.todos import list_goal_todos from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + from loopx.control_plane.work_items.team_plan_adapter import apply_team_plan isolate_sqlite_runtime(tmp_path, monkeypatch) - runtime = tmp_path / "runtime" - state = tmp_path / "state.md" - state.write_text("# Goal\n\n## Agent Todo\n\n") - registry = tmp_path / "registry.json" + history = json.loads((Path(__file__).parent / "fixtures/team_plan_1x.json").read_text()) + runtime, state, registry = tmp_path / "runtime", tmp_path / "state.md", tmp_path / "registry.json" + state.write_text(history["source"]) registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{ "id": "goal-a", "repo": str(tmp_path), "state_file": "state.md", "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() + def replay(plan=None): + return apply_team_plan(registry_path=registry, goal_id="goal-a", agent_id=None, + proposal=plan or history["plan"], expected_state_fingerprint=history["basis"]) + original = state.read_bytes() + assert replay()["action"] == "reused" + assert state.read_bytes() == original + with pytest.raises(Exception, match="identity mismatch"): + replay({**history["plan"], "objective": "Different confirmed request"}) + assert state.read_bytes() == original rows = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] + assert len(rows) == 1 and rows[0]["text"] == "[P1] Own work" projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=rows, handoff_mode="soft_claim") initialize_canonical_authority(runtime, "goal-a", projection, state_path=state, provider=provider) + service = ChatActionService(store=ChatActionStore(runtime / "chat/actions"), registry_path=registry) + preview = service.preview({"action_kind": "team.plan", "summary": "New work after import", "context": {}, + "normalized_parameters": {"goal_id": "goal-a", "plan": _agent_plan()}, "idempotency_key": "new-plan"}) 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" + assert invoke("apply", proposal=preview["proposal_id"])["proposal"]["status"] == "applied" current = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + assert len(current["todos"]) == 2 display = state.read_bytes() - replay = invoke("historical") - assert replay["action"] == "reused" - assert replay["lane_todo_ids"] == applied["receipt"]["resource_ids"]["lane_todo_ids"] + recovered = replay() + assert recovered["action"] == "reused" + assert recovered["lane_todo_ids"] == history["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_todo_authoring_scope.py b/tests/control_plane/test_todo_authoring_scope.py index 2764143aa1..c51de815fe 100644 --- a/tests/control_plane/test_todo_authoring_scope.py +++ b/tests/control_plane/test_todo_authoring_scope.py @@ -5,15 +5,16 @@ from loopx.control_plane.testing.canary_harness import run_json_cli_result, write_fixture_registry 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 loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable 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): project = tmp_path / "project" project.mkdir() state = project / "ACTIVE_GOAL_STATE.md" @@ -23,6 +24,10 @@ 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) + isolate_sqlite_runtime(tmp_path, monkeypatch) + 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 +66,8 @@ 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(LocalCoordinationAuthorityUnavailable, match="global|scope|bind"): + 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 +106,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 +209,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_consumer_host_retirement.py b/tests/control_plane/test_todo_consumer_host_retirement.py index 7cd5ea8a73..8a4dcb0d6e 100644 --- a/tests/control_plane/test_todo_consumer_host_retirement.py +++ b/tests/control_plane/test_todo_consumer_host_retirement.py @@ -119,19 +119,12 @@ 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) + # Seed a real canonical provider; every subsequent CLI decision runs in + # the selected copied distribution without an editable finder. + 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_field_update_runtime.py b/tests/control_plane/test_todo_field_update_runtime.py index 141a0cdf38..89a13fc027 100644 --- a/tests/control_plane/test_todo_field_update_runtime.py +++ b/tests/control_plane/test_todo_field_update_runtime.py @@ -1,284 +1,8 @@ -"""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 +"""The shared typed field owner retains the supported monitor codec contract.""" 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 diff --git a/tests/control_plane_ts/canonical_writer_cutoff.test.ts b/tests/control_plane_ts/canonical_writer_cutoff.test.ts new file mode 100644 index 0000000000..bcc5ac0667 --- /dev/null +++ b/tests/control_plane_ts/canonical_writer_cutoff.test.ts @@ -0,0 +1,95 @@ +import {FileAuthorityStore} from "../../loopx/control_plane/coordination/file_authority_store.ts"; +import {SqliteAuthorityStore} from "../../loopx/control_plane/coordination/sqlite_authority_store.ts"; +import {executeCanonicalTaskLeaseAcquire} from "../../loopx/control_plane/coordination/task_lease_acquire.ts"; +import {executeCanonicalTaskLeaseLifecycle} from "../../loopx/control_plane/coordination/task_lease_lifecycle.ts"; +import {authorityProjectionFixture} from "./authority_projection_fixture.ts"; +import assert from "node:assert/strict"; +import {createHash} from "node:crypto"; +import {mkdir, mkdtemp, readFile, rm, writeFile} from "node:fs/promises"; +import {tmpdir} from "node:os"; +import {join} from "node:path"; +import test from "node:test"; +import {executeTaskLeaseAcquire, TASK_LEASE_ACQUIRE_REQUEST_SCHEMA_VERSION} from "../../loopx/control_plane/work_items/task_lease_acquire.ts"; +import {executeTaskLeaseLifecycle, TASK_LEASE_LIFECYCLE_REQUEST_SCHEMA_VERSION} from "../../loopx/control_plane/work_items/task_lease_lifecycle.ts"; +import {manageNewGoalStorage} from "../../loopx/control_plane/coordination/local_authority_defaults.ts"; + +for (const provider of ["file", "sqlite"]) { + for (const legacy of [true, false]) test(`2.0 rejects ${provider} target-only creation (${legacy ? "v0" : "disabled"})`, async () => { + const configuration = legacy ? {schema_version:"loopx_goal_storage_defaults_v0", new_goal_provider:provider} + : {schema_version:"loopx_goal_storage_defaults_v1", new_goal_provider:provider, canonical_creation:false, new_goal_handoff_mode:"hard_lease"}; + await assert.rejects(manageNewGoalStorage({action:"resolve", configuration}), /requires canonical creation/); + }); +} + +test("cold source refuses new grants but releases an original lease with CAS", async t => { + const root = await mkdtemp(join(tmpdir(), "cutoff-lease-")); + t.after(() => rm(root, {recursive:true, force:true})); + const source = join(root, "state.md"); + await writeFile(source, "original source"); + const authority = {handoff_mode:"hard_lease", registered_agent_candidates:[["agent-a", "agent-b"]], + todos:[{todo_id:"todo_target",status:"open",claimed_by:null,excluded_agents:[]}],todo_projection_error:null, + source_receipts:[{source_id:"authority",path:source,state:"file",sha256:createHash("sha256").update("original source").digest("hex")}]}; + const acquire = {schema_version:TASK_LEASE_ACQUIRE_REQUEST_SCHEMA_VERSION, runtime_root:join(root,"runtime"), + goal_id:"goal-a",todo_id:"todo_target",owner:"agent-a",idempotency_key:"lease-a",ttl_seconds:600, + write_scopes:[],expected_version:null,authority}; + const now = () => new Date("2026-09-01T03:01:00Z"); + const refused = await executeTaskLeaseAcquire(acquire, {now}); + assert.equal(refused.ok,false); assert.equal(refused.error_code,"canonical_authority_required"); + const directory = join(root,"runtime/goals/goal-a/task-leases"); + await mkdir(directory,{recursive:true}); + const path = join(directory,"todo_target.json"); + // Retained 1.x lease, including original identity, generation and expiry. + await writeFile(path, JSON.stringify({schema_version:"task_lease_v0",goal_id:"goal-a",todo_id:"todo_target", + owner:"agent-a",idempotency_key:"lease-a",write_scopes:[],acquire_ttl_seconds:600,version:1,lease_epoch:1, + acquired_at:"2026-09-01T03:00:00Z",updated_at:"2026-09-01T03:00:00Z",expires_at:"2026-09-01T03:10:00Z",status:"active"})); + const before = await readFile(path,"utf8"); + const replay = await executeTaskLeaseAcquire(acquire,{now}); + assert.equal(replay.ok,true); assert.equal(replay.idempotent,true); + const base = {schema_version:TASK_LEASE_LIFECYCLE_REQUEST_SCHEMA_VERSION,runtime_root:acquire.runtime_root, + goal_id:"goal-a",todo_id:"todo_target",owner:"agent-a",idempotency_key:"lease-a",expected_version:1, + ttl_seconds:600,new_owner:null,new_idempotency_key:null,lock_token:null,committed:false,release_lease:false, + fence_owner:null,fence_idempotency_key:null,fence_expected_version:null,fence_expected_lease_epoch:1,authority}; + for (const operation of ["renew","transfer"]) { + const result = await executeTaskLeaseLifecycle({...base,operation, + ...(operation === "transfer" ? {new_owner:"agent-b",new_idempotency_key:"lease-b"} : {})},{now}); + assert.equal(result.ok,false); assert.equal(result.error_code,"canonical_authority_required"); + assert.equal(await readFile(path,"utf8"),before); + } + const invalid = await executeTaskLeaseLifecycle({...base,operation:"release",expected_version:0},{now}); + assert.equal(invalid.ok,false); assert.equal(await readFile(path,"utf8"),before); + const released = await executeTaskLeaseLifecycle({...base,operation:"release"},{now}); + assert.equal(released.ok,true); assert.equal(released.released,true); + assert.equal(JSON.parse(await readFile(path,"utf8")).status,"released"); + assert.equal((await executeTaskLeaseLifecycle({...base,operation:"release"},{now})).ok,true); + assert.equal(await readFile(source,"utf8"),"original source"); +}); + + +for (const provider of ["file", "sqlite"] as const) test(`${provider} imported legacy policy requires upgrade before new execution`, async t => { + const root = await mkdtemp(join(tmpdir(), "cutoff-policy-")); + t.after(() => rm(root, {recursive:true, force:true})); + const store = provider === "file" ? new FileAuthorityStore(root, "goal-a") : new SqliteAuthorityStore(root, "goal-a"); + const todo = {todo_id:"todo_work",role:"agent",status:"open",done:false,text:"Imported work", + archive_state:"active",claimed_by:null,task_class:"advancement_task"}; + const lease = {schema_version:"task_lease_v0",goal_id:"goal-a",todo_id:"todo_work",owner:"agent-a", + idempotency_key:"old",version:1,lease_epoch:1,status:"active",write_scopes:[],acquire_ttl_seconds:600, + acquired_at:"2026-09-01T03:00:00Z",updated_at:"2026-09-01T03:00:00Z",expires_at:"2026-09-01T03:10:00Z"}; + assert.equal((await store.commitAuthority({expected_provider_revision:null, operation_id:"seed", + next_projection:authorityProjectionFixture("goal-a",[todo],[lease],"native",{handoff_mode:"legacy"}), + events:[],receipts:[]})).status,"applied"); + const command = {goal_id:"goal-a",todo_id:"todo_work",owner:"agent-a",idempotency_key:"old", + expected_version:1,ttl_seconds:600,write_scopes:[],registered_agents:["agent-a","agent-b"],now:new Date("2026-09-01T03:01:00Z")}; + const before = await store.loadAuthority(); + for (const operation of ["renew","transfer"] as const) { + const refused = await executeCanonicalTaskLeaseLifecycle(store,{...command,operation, + ...(operation === "transfer" ? {new_owner:"agent-b",new_idempotency_key:"new"} : {})}); + assert.equal(refused.reason_code,"handoff_mode_retired"); + assert.match(String(refused.reason),/handoff-mode plan-migration/); + assert.deepEqual(await store.loadAuthority(),before); + } + assert.equal((await executeCanonicalTaskLeaseLifecycle(store,{...command,operation:"release",ttl_seconds:null})).status,"applied"); + const released = await store.loadAuthority(); + const denied = await executeCanonicalTaskLeaseAcquire(store,{...command,idempotency_key:"new",expected_version:1}); + assert.equal(denied.reason_code,"handoff_mode_retired"); + assert.deepEqual(await store.loadAuthority(),released); +}); diff --git a/tests/test_chat_team_plan_action.py b/tests/test_chat_team_plan_action.py index ad47d09216..fcabb72ff4 100644 --- a/tests/test_chat_team_plan_action.py +++ b/tests/test_chat_team_plan_action.py @@ -60,6 +60,11 @@ def _fixture(tmp_path: Path, *, agents: tuple[str, ...] = (AGENT_ID,)): ), encoding="utf-8", ) + from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + 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, @@ -416,7 +421,12 @@ def test_failed_batch_retries_same_card_without_partial_work(tmp_path: Path, mon preview = _preview(service, _two_lane_plan()) def fail(*args, **kwargs): raise OSError("batch unavailable") - monkeypatch.setattr(team_plan_adapter, "write_captured_todo_state", fail) + real_effect = team_plan_adapter.effect_runtime_result + def fail_commit(method, request): + if method == "work_items.team_plan.commit": + return fail() + return real_effect(method, request) + monkeypatch.setattr(team_plan_adapter, "effect_runtime_result", fail_commit) failed = service.apply(preview["proposal_id"])["proposal"] assert failed["status"] == "failed" assert failed["failure"]["error_code"] == "team_plan_commit_failed" @@ -431,22 +441,26 @@ 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.work_items import team_plan_adapter - project, _, service = _fixture(tmp_path) + project, registry, 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 + real = team_plan_adapter.settle_canonical_todo_projection 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) + monkeypatch.setattr(team_plan_adapter, "settle_canonical_todo_projection", lose_response) assert service.apply(preview["proposal_id"])["proposal"]["status"] == "failed" assert _todos(project).count("loopx:todo ") == (1 if has_gap else 2) 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")) + from loopx.control_plane.todos.list_readback import list_goal_todos + from loopx.control_plane.todos.mutation_api import update_goal_todo + todo = list_goal_todos(registry_path=registry, goal_id=GOAL_ID, role="agent")["todos"][0] + update_goal_todo(registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], + agent_id=todo["claimed_by"], text="Receiver revised the work") before = state.read_bytes() recovered = service.apply(preview["proposal_id"])["proposal"] assert recovered["status"] == "applied" diff --git a/tests/test_steward_team_plan_apply.py b/tests/test_steward_team_plan_apply.py index 910e95817b..3a5aa43fbc 100644 --- a/tests/test_steward_team_plan_apply.py +++ b/tests/test_steward_team_plan_apply.py @@ -71,6 +71,11 @@ def _fixture( ), encoding="utf-8", ) + from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + 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,7 +115,7 @@ def _basis(registry_path: Path) -> str: ) -def test_legacy_registry_stays_locked_through_plan_and_write(tmp_path, monkeypatch): +def test_canonical_registry_stays_locked_through_plan_and_commit(tmp_path, monkeypatch): from loopx.file_lock import exclusive_cross_runtime_file_lock, LockAcquireTimeoutError from loopx.control_plane.work_items import team_plan_adapter @@ -119,7 +124,7 @@ def test_legacy_registry_stays_locked_through_plan_and_write(tmp_path, monkeypat checked = [] def inspect(method, request): - if method == "work_items.team_plan.plan": + if method == "work_items.team_plan.commit": with pytest.raises(LockAcquireTimeoutError): with exclusive_cross_runtime_file_lock(registry, timeout_seconds=0): pass @@ -655,7 +660,12 @@ def test_batch_write_failure_leaves_every_lane_unwritten(tmp_path: Path, monkeyp before = _todos(project) def fail(*args, **kwargs): raise OSError("batch write unavailable") - monkeypatch.setattr(team_plan_adapter, "write_captured_todo_state", fail) + real_effect = team_plan_adapter.effect_runtime_result + def fail_commit(method, request): + if method == "work_items.team_plan.commit": + return fail() + return real_effect(method, request) + monkeypatch.setattr(team_plan_adapter, "effect_runtime_result", fail_commit) with pytest.raises(OSError, match="batch write unavailable"): _settle(registry_path, _proposal(extra_lane=_second_lane())) assert _todos(project) == before From 46805d4a8debb44f71871c59a720492d2730f0d1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:51:39 +0800 Subject: [PATCH 03/54] docs: describe 2.0 cutoff and remaining qualification Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 13 +++- .../local-authority-provider-selection.md | 70 +++++++++++++++---- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 1ba703beeb..5a14f359ba 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -102,7 +102,18 @@ unpromoted callers remain in ordinary mutation, bootstrap/creation opt-out, team-plan, Monitor, ownership/source lease and active capture/binding. Reuse their typed admission/transaction owners; treat retained source capture, historical receipts and permanent document IO as recovery obligations. -The existing delivery Todos should carry these concrete exits: +The 2.0 alpha implementation now removes the normal Todo/Monitor source writers, +unused line-update helpers and four Todo capture producers, sharing typed refusal across cold-source +entrypoints while keeping original lease release, team-plan receipts and outbox +drain. This candidate changes v0/disabled creation admission and fresh legacy +ownership grants intentionally; it does not change the compatible 1.x line. +The normal-wheel import/consumer matrix, packaged Settings enable/refusal/readback, +real PostgreSQL conformance and risk canary have focused passing evidence. The +native source-lease regression fixtures still need to preserve historical recovery +without recreating retired writers; the backend configuration catalog description +also needs alignment. Author integration and exact-head review remain open. Retained outbox recovery and source +lease release are concrete remaining callers, not grounds for keeping normal +Todo producers. The existing delivery Todos should carry these concrete exits: - Qualify the installed import/backup/recovery journey for the declared source matrix; importer implementation is already present and is not a missing-code diff --git a/docs/reference/local-authority-provider-selection.md b/docs/reference/local-authority-provider-selection.md index b062eddcc9..719cd26231 100644 --- a/docs/reference/local-authority-provider-selection.md +++ b/docs/reference/local-authority-provider-selection.md @@ -65,13 +65,14 @@ See [reviewed promotion and recovery](reviewed-coordination-promotion.md) for th File or SQLite independently of the execution policy. In the new-Goal default candidate, a device without a `goal_storage` namespace creates canonical SQLite with `hard_lease`; CLI bootstrap and App creation reuse the same configuration -and typed initialization owner. **Create canonical authority** can be disabled -explicitly, or used with File and either `soft_claim` or `hard_lease`. Agents +and typed initialization owner. The **2.0 alpha candidate requires Create canonical +authority** with File or SQLite and either `soft_claim` or `hard_lease`. Agents inherit the Goal policy; this grants no tool, repository, scheduler, account or network permission and does not migrate existing data. -The candidate changes only unconfigured new creation. Released v0 settings and -v1 with `canonical_creation=false` retain the post-promotion target behavior. +The 2.0 candidate intentionally ends fresh target-only creation. Released v0 +settings and v1 with `canonical_creation=false` remain readable, but creation +refuses with a Settings remediation before publishing a registry or provider. Opening v0 in the guided editor previews a v1 envelope upgrade with creation still disabled; switching editor modes cannot enable it through the new default. The CLI continues to accept v0. Existing Goals keep their recorded target, @@ -135,8 +136,8 @@ prevent duplicate writes or first-Turn submission. A started Turn is read back from its original Session, not inferred from a creation checkpoint. This recovery does not certify model execution or Goal completion. A missing original workspace, conflicting identity or unavailable authority still stops recovery. -Target-only historical creation retains its adapter; it does not gain the -canonical transaction's response-loss guarantee. +Unfinished target-only historical creation must use reviewed import. Its +recorded target remains migration input, not permission to resume source writes. If the Host dies after Session acceptance but before upstream startup, the local queued Turn is still the original request. Session restoration replays it through @@ -155,7 +156,7 @@ Agent 和首批任务。使用“重试原操作”在冻结的服务端工作 工作区同样适用。原生 File/SQLite 复用 Todo 操作身份与既有 Session/Turn 接纳规则;已启动的 Turn 必须从原 Session 读回。创建步骤不代表模型运行或 Goal 验收完成;原工作区、操作身份或 authority 不可用时仍须停止恢复。 -历史 target-only 创建保留其适配器,不获得原生事务的响应丢失恢复保证。 +历史 target-only 创建须走审核导入;原目标仍是迁移输入,不授权恢复旧源写入。 Host 在 Session 接纳后、上游启动前退出时,本地排队 Turn 仍是原请求。 Session 恢复须先通过既有 Turn 接纳 owner 重放该请求,再准备适配器。 @@ -171,7 +172,8 @@ Turn 恢复支持仍待实测;CLI authority 创建与其他支持范围分别 Synthetic workspace data on the candidate's packaged App and installed wheel; model execution is disabled in this isolated creation/configuration rehearsal. -Released v0 preferences remain target-only when opened in the candidate editor. +Historical screenshot: v0 preferences remain visibly disabled in the editor. +In 2.0 that state blocks new creation until explicitly enabled. ![Existing File preference stays disabled](images/new-goal-authority/v0-retained.png) @@ -186,8 +188,8 @@ layout or responsive navigation is introduced. -To disable future canonical creation, preview and apply the same v1 document -with `canonical_creation=false`. Removing the preference restores the candidate +A retained `canonical_creation=false` preference blocks new creation in 2.0; +it no longer enables a Markdown writer. Removing the preference restores the candidate SQLite/hard-lease default for future new Goals; removal is **not** opt-out. To remove the whole preference: @@ -231,8 +233,9 @@ default admission remain separate; the existing RFC acceptance is unchanged. 在“设置 → 能力中心 → 此设备默认 → 新 Goal 的权威存储”中,分别选择 File/SQLite 和 `soft_claim`/`hard_lease`。候选实现让未配置此命名空间的新 Goal 默认建立 canonical SQLite、使用 `hard_lease`,CLI 与 App 共用配置和类型化创建归属。 -旧 v0 或显式关闭状态仍只固定晋升后的目标;表单以关闭状态预览 v1 升级, -切换编辑模式也不会自动启用。CLI 继续接受旧格式,已有 Goal 保留原路径。 +2.0 候选明确停止旧源新建:旧 v0 或显式关闭状态仍可读取,但新建会在写入前拒绝; +表单保留关闭状态,需明确启用后再重试,不会自动开启。 +CLI 配置读取继续接受旧格式,已有 Goal 保留原路径。 Registry 中的相对 runtime 路径按实际文件位置比较;等价路径不会误触发权威迁移检查。 Agent 继承 Goal 策略;此设置不授予工具、仓库、账户或网络权限。 @@ -247,7 +250,7 @@ CLI 使用上面的完整 JSON 和 preview/apply/inspect/bootstrap 命令;App 后的存储丢失须恢复完整备份,不能重新创建空库;通用 force 不能重建。原回执在 后续写入后仍可读回,重试不得丢失或重复 Todo。 -显式设置 `canonical_creation=false` 才关闭之后的新建;remove 删除偏好会恢复候选 +2.0 中 `canonical_creation=false` 会阻止新建,不再启用 Markdown writer;remove 删除偏好会恢复候选 SQLite/hard-lease 默认,不能用于关闭。两者均不会迁回已有数据、 删除 fence 或重新开放旧 writer。既有 Goal 升级仍需备份、停止写入、结算租约和 审核计划;反向迁移须保留新增写入。受支持的旧备份、格式和原回执恢复能力保留。 @@ -266,6 +269,43 @@ 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. +## 2.0 normal-write cutoff candidate + +This is a breaking alpha implementation, not an upgrade to the compatible 1.x +line or a declaration of release qualification. Ordinary Todo add/update/claim, +complete/supersede/archive, source bootstrap and handoff changes, fresh team-plan +application and Monitor writeback require canonical authority. Fresh source lease +acquire/renew/transfer and fence admission are refused. Canonical Goals retaining +`legacy` ownership cannot grant, renew or transfer a new execution; inspect and +review `handoff-mode plan-migration` / `handoff-mode migrate` first. + +Use the existing [cold-source import journey](reviewed-coordination-promotion.md) +before ordinary writes. Settle retained operations by their original identity; +inspect and drain prepared/committed captures before import. Original lease +release and closed-operation recovery remain available with their original CAS +and identity checks. A changed identity, unavailable provider, changed source +or unproved cursor remains a failure, never permission to construct empty state. +Markdown document projections, backup readers, import and historical receipts +are retained independently of normal source writers. + +The candidate removes the Todo mutation implementation and line-update helpers, +Monitor source writer, and four Todo capture producers after their normal callers +exit. Their Python facade exports and writer-only tests retire with them; the +shared typed field, claim, visibility and recovery rules retain their tests. The lease-release +capture adapter and shared outbox recovery remain live. File/SQLite source CLI, +canonical Chat, original team-plan replay and interrupted outbox readback have +focused tests. The normal-wheel import/consumer matrix, packaged Settings journey, +real PostgreSQL conformance and risk canary pass their focused checks. Historical +native source-lease fixtures and the backend configuration description still need +integration; exact-head review remains open. These checks +do not certify natural multi-developer adoption, sustained D2 or a public release. + +2.0 alpha 候选把正常写入统一收口到 canonical authority,明确与兼容的 1.x 线区分。 +旧 Goal 先结算原操作并审核导入;保留原租约释放、历史回执和 outbox 恢复,不能因 +关闭配置而抹掉未结算责任。导入后仍为 legacy ownership 的 Goal 须审核策略迁移, +不能新授予、续期或转移执行权。删除正常 writer 不等于删除 Markdown 文档显示, +也不代表完整安装资格、自然试用、D2 或发布已完成。 + ## Retirement boundaries before changing the release default SQLite adoption and Python retirement need separate evidence. A canonical Goal @@ -276,8 +316,8 @@ to qualify each last caller family independently. | Retained boundary | Current value and owner | Evidence needed before removal | | --- | --- | --- | -| `todos/mutation_api.py` → `todos/legacy_mutation.py`; `bootstrap.py`, `work_items/team_plan_adapter.py`, `scheduler/legacy_monitor_poll.py` | These are still reachable unpromoted mutation families. Production callers have left the historic `todos.py` re-export; canonical transactions use their concrete typed owners. | Qualify the import-first cutoff for every family below, then delete its normal source-write dispatch and implementation together. Keep Markdown display, historical imports where supported, and import/export recovery. | -| `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/mutation_api.py`; `bootstrap.py`, `work_items/team_plan_adapter.py`, `scheduler/monitor_poll_writeback.py` | The 2.0 candidate refuses fresh unpromoted mutations and removes the legacy Todo/Monitor implementations; 1.x retains its compatibility contract. Canonical transactions use their concrete typed owners. | Qualify the import-first cutoff for every family below, then delete its normal source-write dispatch and implementation together. Keep Markdown display, historical imports where supported, and import/export recovery. | +| `runtime_shadow_writer_adapter.py`, `local_authority_shadow_outbox.py` | The 2.0 candidate removes four Todo producers. Retained lease release still prepares capture before its source write; 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. | | `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`; retained `configuration.py`, `authority_source_capture.py`, `local_authority_shadow_history.py`, `local_authority_shadow_readback.py` | Configuration, cold-source capture and read-only diagnostics now have concrete owners independent of producers. Explicit shadow bootstrap/drain/promotion and recovery still have live callers. | Retire obsolete producer aliases only after their supported callers exit. Preserve the retained import/history/readback owners and outbox recovery; inspect or cold import succeeding without producers does not make a live drain 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. | From 6161bb2a07a93531807ec46ef312736258cce37e Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:39:24 +0800 Subject: [PATCH 04/54] fix: preserve original lease fence recovery after writer cutoff Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../work_items/task_lease_lifecycle.ts | 84 +- .../control_plane/fixtures/task_lease_1x.json | 1132 ++++++++++ .../fixtures/task_lease_parity_1x.json | 72 + .../deferred_hard_lease_lifecycle.test.ts | 4 +- .../goal_acceptance_runtime.test.ts | 21 +- ...gacy_writer_fence_caller_parity_support.ts | 26 +- .../local_authority_defaults.test.ts | 6 +- .../new_goal_initialization.test.ts | 7 +- .../shadow_native_writer_boundary.test.ts | 35 +- .../task_lease_acquire.test.ts | 151 +- .../task_lease_acquire_cli.test.ts | 40 +- .../task_lease_lifecycle.test.ts | 1852 +++-------------- tests/control_plane_ts/todo_update.test.ts | 4 +- .../legacy_writer_fence_caller_parity_v0.json | 93 +- 14 files changed, 1661 insertions(+), 1866 deletions(-) create mode 100644 tests/control_plane/fixtures/task_lease_1x.json create mode 100644 tests/control_plane/fixtures/task_lease_parity_1x.json diff --git a/loopx/control_plane/work_items/task_lease_lifecycle.ts b/loopx/control_plane/work_items/task_lease_lifecycle.ts index 58ce30b2ec..be453e1d19 100644 --- a/loopx/control_plane/work_items/task_lease_lifecycle.ts +++ b/loopx/control_plane/work_items/task_lease_lifecycle.ts @@ -50,7 +50,6 @@ import { taskLeaseDirectory, taskLeaseLockPath, taskLeasePath, - utcIsoformat, type AuthorityFacts, type LeaseRecord, type TodoFact, @@ -58,7 +57,6 @@ import { } from "./task_lease_acquire.ts"; import { decideTaskLeaseLifecycle, - materializeTaskLeaseLifecycle, releasedTaskLeaseRecord as releasedLease, type TaskLeaseLifecycleDecision, type TaskLeaseLifecycleDecisionInput, @@ -411,10 +409,10 @@ async function persistFenceReceipt( ): T | null => value !== undefined ? value : fallback ?? null; const verifyResponse = options.verifyResponse !== undefined ? options.verifyResponse - : base?.verify_response ?? ( - state === "held" - ? options.response - : state === "closed" && + : state === "held" + ? options.response + : base?.verify_response ?? ( + state === "closed" && base?.response !== null && base?.response !== undefined && base.response.action !== "fence_close" @@ -1469,11 +1467,11 @@ async function fenceVerify( // 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 { + // Retain the original receipt as recovery authority; the locked + // re-read below revalidates this exact active generation. + if (currentLease === null || + currentLease.status !== "active" || + !tupleMatches) { throw new TaskLeaseLifecycleError( "closed task lease fence receipt is stale or has no retryable lease generation", "fence_state_invalid", @@ -1499,7 +1497,10 @@ async function fenceVerify( ); } } - if (receipt === null) rejectLegacyCoordinationExecution(request.goal_id); + if (receipt === null) { + await requireLegacyCoordinationPrimaryWriteAllowed(request.runtime_root, request.goal_id); + rejectLegacyCoordinationExecution(request.goal_id); + } const adoptedLock = await adoptFenceReceiptLock(request, receipt); const acquiredFreshLock = adoptedLock === null; lock = adoptedLock ?? await acquireFileMutationLock( @@ -1707,6 +1708,11 @@ async function fenceVerify( } return replayResponse; } + throw new TaskLeaseLifecycleError( + "held task lease fence receipt does not match the persisted lease", + "fence_state_invalid", + { goal_id: request.goal_id, todo_id: request.todo_id, lease_path: leasePath }, + ); } if (request.operation === "holder_verify") { if (request.authority.handoff_mode !== "hard_lease") { @@ -1757,60 +1763,14 @@ async function fenceVerify( 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); - } + // An acquired receipt records intent, not a committed grant. After the + // cutoff it can adopt an existing effect, but cannot create a missing one. + if (autoAcquire && !effective && !active) rejectLegacyCoordinationExecution(request.goal_id); // 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" && + const recoveredAutoAcquire = request.operation === "terminal_verify" && effective && receipt?.state !== "closed" && receipt?.fence_idempotency_key === `auto-${request.todo_id}` && receipt?.fence_owner !== null && diff --git a/tests/control_plane/fixtures/task_lease_1x.json b/tests/control_plane/fixtures/task_lease_1x.json new file mode 100644 index 0000000000..ef37db6027 --- /dev/null +++ b/tests/control_plane/fixtures/task_lease_1x.json @@ -0,0 +1,1132 @@ +{ + "source_commit": "60d833b13c85135587b725013b247e827663ffb9", + "scenarios": { + "original": { + "request": { + "schema_version": "loopx_task_lease_acquire_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": null, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [] + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + } + }, + "files": { + "goals/goal-a/task-leases/todo_target.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:00:00Z", + "expires_at": "2026-09-01T03:10:00Z", + "status": "active" + } + } + }, + "renew": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": 1, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [] + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "renew", + "new_owner": null, + "new_idempotency_key": null, + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1 + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-operations/83642ecca0b109ad9c67ef641c43a3cf84dfcb1a156bfc012e7b975475926323.json": { + "schema_version": "task_lease_lifecycle_receipt_v0", + "operation_id": "83642ecca0b109ad9c67ef641c43a3cf84dfcb1a156bfc012e7b975475926323", + "request_digest": "3c064882fc370f6dbc79e8526f25162e4c893830cc9be7ae3e455067f5d1a2dc", + "state": "committed", + "planned_lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 2, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:01:00Z", + "expires_at": "2026-09-01T03:11:00Z", + "status": "active" + }, + "response": { + "ok": true, + "schema_version": "task_lease_v0", + "action": "renew", + "renewed": true, + "lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 2, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:01:00Z", + "expires_at": "2026-09-01T03:11:00Z", + "status": "active" + }, + "lease_path": "$ROOT/runtime/goals/goal-a/task-leases/todo_target.json", + "handoff_mode": "hard_lease", + "idempotent": false, + "settlement": { + "effect_id": "goal-a:agent-a:todo_target:lease-a", + "receipts": [ + { + "step": "validation", + "status": "committed", + "effect_id": "goal-a:agent-a:todo_target:lease-a" + }, + { + "step": "durable_writeback", + "status": "committed", + "effect_id": "goal-a:agent-a:todo_target:lease-a" + } + ] + } + } + }, + "goals/goal-a/task-leases/todo_target.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 2, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:01:00Z", + "expires_at": "2026-09-01T03:11:00Z", + "status": "active" + } + } + }, + "transfer": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": 1, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [] + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "transfer", + "new_owner": "agent-b", + "new_idempotency_key": "lease-b", + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1 + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-operations/993e3b79909ee83f0dfcb96ee676b54b7435f12f5dfb14b4a0c7151216e3a5c5.json": { + "schema_version": "task_lease_lifecycle_receipt_v0", + "operation_id": "993e3b79909ee83f0dfcb96ee676b54b7435f12f5dfb14b4a0c7151216e3a5c5", + "request_digest": "3e7bf5cde7d9499d77ecb7b29d003f935ea2a868c99e5703ede7e54513864219", + "state": "committed", + "planned_lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-b", + "idempotency_key": "lease-b", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 2, + "lease_epoch": 2, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:01:00Z", + "expires_at": "2026-09-01T03:11:00Z", + "status": "active" + }, + "response": { + "ok": true, + "schema_version": "task_lease_v0", + "action": "transfer", + "transferred": true, + "lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-b", + "idempotency_key": "lease-b", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 2, + "lease_epoch": 2, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:01:00Z", + "expires_at": "2026-09-01T03:11:00Z", + "status": "active" + }, + "lease_path": "$ROOT/runtime/goals/goal-a/task-leases/todo_target.json", + "handoff_mode": "hard_lease", + "idempotent": false, + "settlement": { + "effect_id": "goal-a:agent-a:todo_target:lease-a", + "receipts": [ + { + "step": "validation", + "status": "committed", + "effect_id": "goal-a:agent-a:todo_target:lease-a" + }, + { + "step": "durable_writeback", + "status": "committed", + "effect_id": "goal-a:agent-a:todo_target:lease-a" + } + ] + } + } + }, + "goals/goal-a/task-leases/todo_target.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-b", + "idempotency_key": "lease-b", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 2, + "lease_epoch": 2, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:01:00Z", + "expires_at": "2026-09-01T03:11:00Z", + "status": "active" + } + } + }, + "terminal-held": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": 1, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [] + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "terminal_verify", + "new_owner": null, + "new_idempotency_key": null, + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1 + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-fences/ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a.json": { + "schema_version": "task_lease_fence_receipt_v0", + "operation_id": "ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a", + "request_digest": "0e4b9170ebc4555e5c8aeb5d7195db3e5ad2c61750f1a29597c5ea61a2433f5d", + "close_request_digest": null, + "state": "held", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner_pid": 54181, + "lock_token": "8ff0637e-8406-4215-b76c-0041b84d235e", + "fence_owner": "agent-a", + "fence_idempotency_key": "lease-a", + "fence_expected_version": 1, + "fence_expected_lease_epoch": 1, + "lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:00:00Z", + "expires_at": "2026-09-01T03:10:00Z", + "status": "active" + }, + "response": { + "schema_version": "task_lease_v0", + "required": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "execution_instance_verified": true, + "lock_token": "8ff0637e-8406-4215-b76c-0041b84d235e", + "fence_operation_id": "ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a" + }, + "verify_response": { + "schema_version": "task_lease_v0", + "required": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "execution_instance_verified": true, + "lock_token": "8ff0637e-8406-4215-b76c-0041b84d235e", + "fence_operation_id": "ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a" + } + }, + "goals/goal-a/task-leases/todo_target.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:00:00Z", + "expires_at": "2026-09-01T03:10:00Z", + "status": "active" + } + } + }, + "holder-held": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": null, + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": null, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [] + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "holder_verify", + "new_owner": null, + "new_idempotency_key": null, + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1, + "fence_operation_id": "d4ca3c2a1f291b383a47a5454f444f614f9b889ec3545ceb99cde5c6b5617746" + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-fences/d4ca3c2a1f291b383a47a5454f444f614f9b889ec3545ceb99cde5c6b5617746.json": { + "schema_version": "task_lease_fence_receipt_v0", + "operation_id": "d4ca3c2a1f291b383a47a5454f444f614f9b889ec3545ceb99cde5c6b5617746", + "request_digest": "95c9295b1e378e149dbe6fb5b5102560c2c8c23ea67662b690bf4a6eb59719c3", + "close_request_digest": null, + "state": "held", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner_pid": 54181, + "lock_token": "c62562d7-1549-492d-a235-139d8a30c92d", + "fence_owner": "agent-a", + "fence_idempotency_key": null, + "fence_expected_version": 1, + "fence_expected_lease_epoch": 1, + "lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:00:00Z", + "expires_at": "2026-09-01T03:10:00Z", + "status": "active" + }, + "response": { + "schema_version": "task_lease_v0", + "checked": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "lock_token": "c62562d7-1549-492d-a235-139d8a30c92d", + "fence_operation_id": "d4ca3c2a1f291b383a47a5454f444f614f9b889ec3545ceb99cde5c6b5617746" + }, + "verify_response": { + "schema_version": "task_lease_v0", + "checked": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "lock_token": "c62562d7-1549-492d-a235-139d8a30c92d", + "fence_operation_id": "d4ca3c2a1f291b383a47a5454f444f614f9b889ec3545ceb99cde5c6b5617746" + } + }, + "goals/goal-a/task-leases/todo_target.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:00:00Z", + "expires_at": "2026-09-01T03:10:00Z", + "status": "active" + } + } + }, + "terminal-closed": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": 1, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [] + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "terminal_verify", + "new_owner": null, + "new_idempotency_key": null, + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1 + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-fences/ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a.json": { + "schema_version": "task_lease_fence_receipt_v0", + "operation_id": "ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a", + "request_digest": "0e4b9170ebc4555e5c8aeb5d7195db3e5ad2c61750f1a29597c5ea61a2433f5d", + "close_request_digest": "ca27183a8d1792fd75c8d3e2102eca358ccc981209722700052d017af23d6eb0", + "state": "closed", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner_pid": 54181, + "lock_token": "6c7cc0c6-2d55-4af8-82cc-e9ff5211cf12", + "fence_owner": "agent-a", + "fence_idempotency_key": "lease-a", + "fence_expected_version": 1, + "fence_expected_lease_epoch": 1, + "lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:02:00Z", + "expires_at": "2026-09-01T03:10:00Z", + "status": "released", + "released_at": "2026-09-01T03:02:00Z" + }, + "response": { + "ok": true, + "schema_version": "task_lease_v0", + "action": "fence_close", + "released": true + }, + "verify_response": { + "schema_version": "task_lease_v0", + "required": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "execution_instance_verified": true, + "lock_token": "6c7cc0c6-2d55-4af8-82cc-e9ff5211cf12", + "fence_operation_id": "ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a" + } + }, + "goals/goal-a/task-leases/todo_target.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:02:00Z", + "expires_at": "2026-09-01T03:10:00Z", + "status": "released", + "released_at": "2026-09-01T03:02:00Z" + } + } + }, + "terminal-aborted": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": 1, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [] + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "terminal_verify", + "new_owner": null, + "new_idempotency_key": null, + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1 + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-fences/ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a.json": { + "schema_version": "task_lease_fence_receipt_v0", + "operation_id": "ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a", + "request_digest": "0e4b9170ebc4555e5c8aeb5d7195db3e5ad2c61750f1a29597c5ea61a2433f5d", + "close_request_digest": "7dcfa909d2cce1bb41c5d3567f8e19af386743795c08528b95537c83dd3830c7", + "state": "closed", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner_pid": 54181, + "lock_token": "3b2489d8-c76b-42a5-a116-919851935b87", + "fence_owner": "agent-a", + "fence_idempotency_key": "lease-a", + "fence_expected_version": 1, + "fence_expected_lease_epoch": 1, + "lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:00:00Z", + "expires_at": "2026-09-01T03:10:00Z", + "status": "active" + }, + "response": { + "ok": true, + "schema_version": "task_lease_v0", + "action": "fence_close", + "released": false + }, + "verify_response": { + "schema_version": "task_lease_v0", + "required": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "execution_instance_verified": true, + "lock_token": "3b2489d8-c76b-42a5-a116-919851935b87", + "fence_operation_id": "ee58a5ed5a4df4e8c8657e727f47f305250bacddc13bf50d27819573e9ed080a" + } + }, + "goals/goal-a/task-leases/todo_target.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "lease-a", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:00:00Z", + "updated_at": "2026-09-01T03:00:00Z", + "expires_at": "2026-09-01T03:10:00Z", + "status": "active" + } + } + }, + "noop-closed": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": null, + "idempotency_key": null, + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": null, + "authority": { + "handoff_mode": "legacy", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [] + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "terminal_verify", + "new_owner": null, + "new_idempotency_key": null, + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1 + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-fences/eeabc3e47ad0e1432ef39d62a0058afbebdce7feaa67d7efbff40962501918b8.json": { + "schema_version": "task_lease_fence_receipt_v0", + "operation_id": "eeabc3e47ad0e1432ef39d62a0058afbebdce7feaa67d7efbff40962501918b8", + "request_digest": "ff89bc5ce0e7df3e0bfb68cd124e399dd68d085e85c6307befb901ac62d0a843", + "close_request_digest": null, + "state": "closed", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner_pid": 54181, + "lock_token": "8abcfadf-17e6-4d2f-a214-76f2afe07fb9", + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": null, + "lease": null, + "response": { + "schema_version": "task_lease_v0", + "required": false, + "active": false, + "fence_operation_id": "eeabc3e47ad0e1432ef39d62a0058afbebdce7feaa67d7efbff40962501918b8" + }, + "verify_response": null + } + } + }, + "auto-held": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": null, + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": null, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [], + "role": "user", + "task_class": "user_gate" + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "terminal_verify", + "new_owner": null, + "new_idempotency_key": null, + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1, + "allow_user_gate_auto_acquire": true + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-fences/6ffd6291c01402856dd97ad9ccfbf47f5e84c52797ed8b0be1ac0e33c76149cc.json": { + "schema_version": "task_lease_fence_receipt_v0", + "operation_id": "6ffd6291c01402856dd97ad9ccfbf47f5e84c52797ed8b0be1ac0e33c76149cc", + "request_digest": "ffe0fc2a270897bc1f3250278c1a871fd7c04cfb196f52f675e8322eeb2b77ac", + "close_request_digest": null, + "state": "held", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner_pid": 54181, + "lock_token": "bb695187-1239-49b3-bcb6-37391c708011", + "fence_owner": "agent-a", + "fence_idempotency_key": "auto-todo_target", + "fence_expected_version": 1, + "fence_expected_lease_epoch": 1, + "lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "auto-todo_target", + "write_scopes": [], + "acquire_ttl_seconds": 2700, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:01:00Z", + "updated_at": "2026-09-01T03:01:00Z", + "expires_at": "2026-09-01T03:46:00Z", + "status": "active" + }, + "response": { + "schema_version": "task_lease_v0", + "required": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "execution_instance_verified": true, + "lock_token": "bb695187-1239-49b3-bcb6-37391c708011", + "fence_operation_id": "6ffd6291c01402856dd97ad9ccfbf47f5e84c52797ed8b0be1ac0e33c76149cc", + "auto_acquired": true + }, + "verify_response": { + "schema_version": "task_lease_v0", + "required": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "execution_instance_verified": true, + "lock_token": "bb695187-1239-49b3-bcb6-37391c708011", + "fence_operation_id": "6ffd6291c01402856dd97ad9ccfbf47f5e84c52797ed8b0be1ac0e33c76149cc", + "auto_acquired": true + } + }, + "goals/goal-a/task-leases/todo_target.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "auto-todo_target", + "write_scopes": [], + "acquire_ttl_seconds": 2700, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:01:00Z", + "updated_at": "2026-09-01T03:01:00Z", + "expires_at": "2026-09-01T03:46:00Z", + "status": "active" + } + } + }, + "auto-prewrite": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": null, + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": null, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [], + "role": "user", + "task_class": "user_gate" + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "terminal_verify", + "new_owner": null, + "new_idempotency_key": null, + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1, + "allow_user_gate_auto_acquire": true + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-fences/6ffd6291c01402856dd97ad9ccfbf47f5e84c52797ed8b0be1ac0e33c76149cc.json": { + "schema_version": "task_lease_fence_receipt_v0", + "operation_id": "6ffd6291c01402856dd97ad9ccfbf47f5e84c52797ed8b0be1ac0e33c76149cc", + "request_digest": "ffe0fc2a270897bc1f3250278c1a871fd7c04cfb196f52f675e8322eeb2b77ac", + "close_request_digest": null, + "state": "acquired", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner_pid": 54181, + "lock_token": "17c7067e-dbe7-4881-ac2a-c98c4b9bd3b3", + "fence_owner": "agent-a", + "fence_idempotency_key": "auto-todo_target", + "fence_expected_version": null, + "fence_expected_lease_epoch": null, + "lease": null, + "response": null, + "verify_response": null + } + } + }, + "auto-postwrite": { + "request": { + "schema_version": "loopx_task_lease_lifecycle_native_v0", + "runtime_root": "$ROOT/runtime", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": null, + "ttl_seconds": 600, + "write_scopes": [], + "expected_version": null, + "authority": { + "handoff_mode": "hard_lease", + "registered_agent_candidates": [ + [ + "agent-a", + "agent-b" + ] + ], + "todos": [ + { + "todo_id": "todo_target", + "status": "open", + "claimed_by": null, + "excluded_agents": [], + "role": "user", + "task_class": "user_gate" + } + ], + "todo_projection_error": null, + "source_receipts": [ + { + "source_id": "authority", + "path": "$ROOT/authority.json", + "state": "file", + "sha256": "408c3ee0aef794f423d49ba8141f3492961aa68b354d03c37c455d072d7e32c3" + } + ] + }, + "operation": "terminal_verify", + "new_owner": null, + "new_idempotency_key": null, + "lock_token": null, + "committed": false, + "release_lease": false, + "fence_owner": null, + "fence_idempotency_key": null, + "fence_expected_version": null, + "fence_expected_lease_epoch": 1, + "allow_user_gate_auto_acquire": true + }, + "files": { + "goals/goal-a/task-leases/.lifecycle-fences/6ffd6291c01402856dd97ad9ccfbf47f5e84c52797ed8b0be1ac0e33c76149cc.json": { + "schema_version": "task_lease_fence_receipt_v0", + "operation_id": "6ffd6291c01402856dd97ad9ccfbf47f5e84c52797ed8b0be1ac0e33c76149cc", + "request_digest": "ffe0fc2a270897bc1f3250278c1a871fd7c04cfb196f52f675e8322eeb2b77ac", + "close_request_digest": null, + "state": "acquired", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner_pid": 54181, + "lock_token": "c744a049-b1e0-4a9d-a3af-1507d6403b7a", + "fence_owner": "agent-a", + "fence_idempotency_key": "auto-todo_target", + "fence_expected_version": null, + "fence_expected_lease_epoch": null, + "lease": null, + "response": null, + "verify_response": null + }, + "goals/goal-a/task-leases/todo_target.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_target", + "owner": "agent-a", + "idempotency_key": "auto-todo_target", + "write_scopes": [], + "acquire_ttl_seconds": 2700, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-01T03:01:00Z", + "updated_at": "2026-09-01T03:01:00Z", + "expires_at": "2026-09-01T03:46:00Z", + "status": "active" + } + } + } + } +} diff --git a/tests/control_plane/fixtures/task_lease_parity_1x.json b/tests/control_plane/fixtures/task_lease_parity_1x.json new file mode 100644 index 0000000000..b3ce0c7964 --- /dev/null +++ b/tests/control_plane/fixtures/task_lease_parity_1x.json @@ -0,0 +1,72 @@ +{ + "source_commit": "60d833b13c85135587b725013b247e827663ffb9", + "files": { + "goals/goal-a/task-leases/.lifecycle-fences/e098754f2dbe127f48549cf2b18c8311ff011f63920fbfa6a4374e3bbd571005.json": { + "schema_version": "task_lease_fence_receipt_v0", + "operation_id": "e098754f2dbe127f48549cf2b18c8311ff011f63920fbfa6a4374e3bbd571005", + "request_digest": "92ce14fc6767c991a946c5e9703fa8ca247c889941efe10fc880d88abca3ccae", + "close_request_digest": null, + "state": "held", + "goal_id": "goal-a", + "todo_id": "todo_abc", + "owner_pid": 74800, + "lock_token": "e7e1dc9a-88fd-443d-8ef0-bd78ade8d9da", + "fence_owner": "agent-a", + "fence_idempotency_key": "lease:parity", + "fence_expected_version": 1, + "fence_expected_lease_epoch": 1, + "lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_abc", + "owner": "agent-a", + "idempotency_key": "lease:parity", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-07T00:00:00Z", + "updated_at": "2026-09-07T00:00:00Z", + "expires_at": "2026-09-07T00:10:00Z", + "status": "active" + }, + "response": { + "schema_version": "task_lease_v0", + "required": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "execution_instance_verified": true, + "lock_token": "e7e1dc9a-88fd-443d-8ef0-bd78ade8d9da", + "fence_operation_id": "e098754f2dbe127f48549cf2b18c8311ff011f63920fbfa6a4374e3bbd571005" + }, + "verify_response": { + "schema_version": "task_lease_v0", + "required": true, + "active": true, + "owner": "agent-a", + "version": 1, + "lease_epoch": 1, + "execution_instance_verified": true, + "lock_token": "e7e1dc9a-88fd-443d-8ef0-bd78ade8d9da", + "fence_operation_id": "e098754f2dbe127f48549cf2b18c8311ff011f63920fbfa6a4374e3bbd571005" + } + }, + "goals/goal-a/task-leases/todo_abc.json": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_abc", + "owner": "agent-a", + "idempotency_key": "lease:parity", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-07T00:00:00Z", + "updated_at": "2026-09-07T00:00:00Z", + "expires_at": "2026-09-07T00:10:00Z", + "status": "active" + } + } +} diff --git a/tests/control_plane_ts/deferred_hard_lease_lifecycle.test.ts b/tests/control_plane_ts/deferred_hard_lease_lifecycle.test.ts index 7393a5773b..d60aab4086 100644 --- a/tests/control_plane_ts/deferred_hard_lease_lifecycle.test.ts +++ b/tests/control_plane_ts/deferred_hard_lease_lifecycle.test.ts @@ -198,7 +198,9 @@ for (const provider of ["file", "sqlite"] as const) { assert.equal((await executeCoordinationTodoUpdate(store, resume("resume-after-wait"))).status, "applied"); const retry = await executeCoordinationTodoUpdate(store, {...input, operation_id: "defer-again"}); assert.equal(retry.status, "failed"); - assert.equal((retry.recovery as JsonObject).action, "acquire_fresh_lease"); + assert.equal((retry.recovery as JsonObject).action, + mode === "legacy" ? "resolve_acquire_rejection" : "acquire_fresh_lease"); + if (mode === "legacy") assert.equal((retry.recovery as JsonObject).reason_code, "handoff_mode_retired"); assert.equal((await executeCoordinationTodoUpdate(store, {...resume("old-proof"), patch: {note: "Unauthorized execution"}, planning_intent: {}, lease_idempotency_key: "old-execution", lease_expected_version: 3})).status, "failed"); diff --git a/tests/control_plane_ts/goal_acceptance_runtime.test.ts b/tests/control_plane_ts/goal_acceptance_runtime.test.ts index eefa2d3a09..6ce98e568c 100644 --- a/tests/control_plane_ts/goal_acceptance_runtime.test.ts +++ b/tests/control_plane_ts/goal_acceptance_runtime.test.ts @@ -93,18 +93,19 @@ for (const provider of ["file", ...(process.env.LOOPX_TEST_POSTGRES_URL ? ["post validation_label: "ordinary-check", validation_timeout_seconds: 5}; const records = [todo({completion_validation_required: true, completion_validation_sha256: canonicalAuthoritySha256(declaration)}), todo({todo_id: "todo_probe"})]; - await replace(store, projection(records, head.goal_acceptance as JsonObject), "add-probe"); + await replace(store, {...projection(records, head.goal_acceptance as JsonObject), handoff_mode: "hard_lease"}, "add-probe"); const basis = await loaded(store); const doc = {...(head.goal_acceptance as JsonObject).document as JsonObject, scope: {kind: "selected_work", todo_ids: ["todo_probe"]}}; assert.equal((await configureGoalAcceptance(store, {goal_id: "goal-a", operation_id: "scope-correction", actor_agent_id: null, expected_provider_revision: basis.provider_revision, document: doc})).status, "applied"); - assert.equal((await executeCoordinationTodoClaim(store, claim)).status, "applied"); - const plan = await executeCoordinationTodoTerminalLifecycle(store, {...terminal, validation_declaration: declaration}); + assert.equal((await executeCoordinationTodoClaim(store, {...claim, lease_request: {idempotency_key: "execution-a", expected_version: null, ttl_seconds: 60}})).status, "applied"); + assert.equal((await executeCanonicalTaskLeaseAcquire(store, acquire)).status, "no_change"); + const plan = await executeCoordinationTodoTerminalLifecycle(store, {...terminal, validation_declaration: declaration, + lease_idempotency_key: "execution-a", lease_expected_version: 1}); assert.equal(plan.status, "execute_validation"); assert.equal((plan.validation_effect as JsonObject).validation_label, "ordinary-check"); assert.deepEqual(plan.goal_acceptance_validation_effects ?? [], []); - assert.equal((await executeCanonicalTaskLeaseAcquire(store, acquire)).status, "applied"); assert.equal(acceptanceWorkGuard((await loaded(store)).head, "goal-a", "todo_probe")?.allowed, false); }); test(`${provider}: absent acceptance preserves ordinary completion, receipts, and unrelated fields`, async t => { @@ -118,14 +119,15 @@ for (const provider of ["file", ...(process.env.LOOPX_TEST_POSTGRES_URL ? ["post assert.equal((await executeCoordinationTodoTerminalLifecycle(store, terminal)).status, "replayed"); }); - test(`${provider}: unbound work rejects claim/lease/completion with actor precedence and no writes`, async t => { - const {store} = await seeded(t, provider, "unbound"); + test(`${provider}: unbound work preserves actor and lease precedence without writes`, async t => { + const {store, head} = await seeded(t, provider, "unbound"); + await replace(store, {...head, handoff_mode: "hard_lease"}, "supported-policy"); const before = await loaded(store); assert.equal((await executeCoordinationTodoClaim(store, {...claim, actor_agent_id: "intruder"})).reason_code, "actor_not_registered"); - assert.equal((await executeCoordinationTodoClaim(store, claim)).reason_code, "goal_acceptance_unbound"); + assert.equal((await executeCoordinationTodoClaim(store, {...claim, lease_request: {idempotency_key: "execution-a", expected_version: null, ttl_seconds: 60}})).reason_code, "goal_acceptance_unbound"); assert.equal((await executeCanonicalTaskLeaseAcquire(store, {...acquire, owner: "intruder"})).reason_code, "owner_not_registered"); assert.equal((await executeCanonicalTaskLeaseAcquire(store, acquire)).reason_code, "goal_acceptance_unbound"); - assert.equal((await executeCoordinationTodoTerminalLifecycle(store, terminal)).reason_code, "goal_acceptance_unbound"); + assert.equal((await executeCoordinationTodoTerminalLifecycle(store, terminal)).reason_code, "handoff_mode_requires_lease"); assert.deepEqual(await loaded(store), before); assert.equal((await store.readReceipt("complete")).status, "missing"); }); @@ -164,7 +166,8 @@ for (const provider of ["file", ...(process.env.LOOPX_TEST_POSTGRES_URL ? ["post }); test(`${provider}: acquisition replay checks current acceptance after current lease proof`, async t => { - const {store} = await seeded(t, provider, "bound"); + const {store, head} = await seeded(t, provider, "bound"); + await replace(store, {...head, handoff_mode: "hard_lease"}, "supported-policy"); assert.equal((await executeCanonicalTaskLeaseAcquire(store, acquire)).status, "applied"); const current = await loaded(store); await replace(store, {...current.head, goal_acceptance: acceptance(current.head.todos as JsonObject[], false)}, "unbind"); diff --git a/tests/control_plane_ts/legacy_writer_fence_caller_parity_support.ts b/tests/control_plane_ts/legacy_writer_fence_caller_parity_support.ts index 8022a71462..7c5e92d2ad 100644 --- a/tests/control_plane_ts/legacy_writer_fence_caller_parity_support.ts +++ b/tests/control_plane_ts/legacy_writer_fence_caller_parity_support.ts @@ -87,7 +87,7 @@ export const TS_ROWS: ParityRow[] = [ })) ), { id: "ts-terminal_verify_auto_acquire_keyed-engaged", caller: "terminal_verify_auto_acquire_keyed", fence_state: "engaged" }, - { id: "ts-acquire-absent", caller: "acquire", fence_state: "absent" }, + { id: "ts-acquire-absent-cutoff", caller: "acquire", fence_state: "absent" }, { id: "ts-release-absent", caller: "release", fence_state: "absent" }, ]; @@ -177,22 +177,14 @@ function lifecycleRequest(ws: Workspace, operation: string, extra: JsonObject): } async function acquireParityLease(ws: Workspace): Promise { - const acquired = await executeTaskLeaseAcquire({ - schema_version: "loopx_task_lease_acquire_native_v0", - runtime_root: ws.runtimeRoot, - goal_id: GOAL, - todo_id: "todo_abc", - owner: "agent-a", - idempotency_key: PARITY_KEY, - write_scopes: [], - ttl_seconds: 600, - expected_version: null, - authority: ws.authority, - }, CLOCK); - if (acquired.ok !== true) throw new Error(`parity fixture acquire failed: ${JSON.stringify(acquired)}`); - const lease = acquired.lease as JsonObject; - ws.leaseVersion = lease.version as number; - ws.leaseEpoch = lease.lease_epoch as number; + const historical = JSON.parse(await readFile(new URL( + "../control_plane/fixtures/task_lease_parity_1x.json", import.meta.url), "utf8").then( + text => text.replaceAll("{runtime_root}", ws.runtimeRoot))) as {files: Record}; + for (const [path, record] of Object.entries(historical.files)) { + await atomicWriteJson(join(ws.runtimeRoot, path), record); + } + ws.leaseVersion = 1; + ws.leaseEpoch = 1; } async function holdParityFence(ws: Workspace): Promise { diff --git a/tests/control_plane_ts/local_authority_defaults.test.ts b/tests/control_plane_ts/local_authority_defaults.test.ts index 3699dccd79..c6646511e8 100644 --- a/tests/control_plane_ts/local_authority_defaults.test.ts +++ b/tests/control_plane_ts/local_authority_defaults.test.ts @@ -7,16 +7,16 @@ import {manageNewGoalStorage as manage} from "../../loopx/control_plane/coordina import {openLocalAuthorityStoreHandle as selected} from "../../loopx/control_plane/coordination/local_authority_provider.ts"; const configuration=(provider: string)=>({schema_version:"loopx_goal_storage_defaults_v0",new_goal_provider:provider}); for(const provider of ["file","sqlite"]){ - test(`new Goal target ${provider} is durable, idempotent and not a promotion`,async t=>{ + test(`historical ${provider} target initialization remains durable for import recovery`,async t=>{ const root=await mkdtemp(join(tmpdir(),"goal-storage-"));t.after(()=>rm(root,{recursive:true,force:true})); - const target=await manage({action:"resolve",configuration:configuration(provider)}); + const target={schema_version:"loopx_new_goal_storage_target_v0",provider}; const request={action:"initialize",runtime_root:root,goal_id:"example",target}; const first=await manage(request);assert.equal(first.changed,provider === "sqlite");assert.equal(first.promotion_performed,false); const handle=await selected(root,"example");assert.equal(handle.provider,provider); assert.equal((await handle.store.loadAuthority()).status,"missing"); assert.equal((await manage(request)).changed,false); if (provider === "file") return; - const other=await manage({action:"resolve",configuration:configuration(provider==="file"?"sqlite":"file")}); + const other={schema_version:"loopx_new_goal_storage_target_v0",provider:provider==="file"?"sqlite":"file"}; await assert.rejects(manage({...request,target:other}),/reviewed migration/); assert.equal((await selected(root,"example")).provider,provider); }); diff --git a/tests/control_plane_ts/new_goal_initialization.test.ts b/tests/control_plane_ts/new_goal_initialization.test.ts index 8cd1d1ff56..5a656f6c91 100644 --- a/tests/control_plane_ts/new_goal_initialization.test.ts +++ b/tests/control_plane_ts/new_goal_initialization.test.ts @@ -80,12 +80,11 @@ for (const provider of ["file", "sqlite"] as const) { }); } -test("creation opt-in reuses execution policy vocabulary; legacy is only a compatibility target", async () => { +test("2.0 creation requires canonical authority and a supported execution policy", async () => { await assert.rejects(manageNewGoalStorage({action: "resolve", configuration: {schema_version: "loopx_goal_storage_defaults_v1", new_goal_provider: "sqlite", canonical_creation: true, new_goal_handoff_mode: "legacy"}}), /soft_claim or hard_lease/); - assert.deepEqual(await manageNewGoalStorage({action: "resolve", configuration: {schema_version: "loopx_goal_storage_defaults_v1", - new_goal_provider: "sqlite", canonical_creation: false, new_goal_handoff_mode: "hard_lease"}}), - {schema_version: "loopx_new_goal_storage_target_v0", provider: "sqlite"}); + await assert.rejects(manageNewGoalStorage({action: "resolve", configuration: {schema_version: "loopx_goal_storage_defaults_v1", + new_goal_provider: "sqlite", canonical_creation: false, new_goal_handoff_mode: "hard_lease"}}), /requires canonical creation/); }); test("completed File creation cannot recreate a lost authority document", async () => { diff --git a/tests/control_plane_ts/shadow_native_writer_boundary.test.ts b/tests/control_plane_ts/shadow_native_writer_boundary.test.ts index a19304c787..b4a31c6b11 100644 --- a/tests/control_plane_ts/shadow_native_writer_boundary.test.ts +++ b/tests/control_plane_ts/shadow_native_writer_boundary.test.ts @@ -200,10 +200,9 @@ test("managed fence engagement rejects a different existing source file", async await assert.rejects(access(legacyCoordinationWriterFencePath(f.root, "goal-a"))); }); -test("active native lease requires durable prepare even without a caller capture hint", async (t) => { +test("original lease release requires durable prepare without a caller capture hint", async (t) => { const f = await bootstrapFixture(t); const part = join(f.root, "authority-shadow", "outbox", "goal-a", "leases"); - await writeFile(part, "prepare unavailable"); const authority = {handoff_mode: "hard_lease", registered_agent_candidates: [["agent-a"]], todos: [{todo_id: "todo_one", status: "open", claimed_by: null, excluded_agents: []}], todo_projection_error: null, source_receipts: [{source_id: "state", path: f.statePath, state: "file", @@ -211,21 +210,29 @@ test("active native lease requires durable prepare even without a caller capture const request = {schema_version: TASK_LEASE_ACQUIRE_REQUEST_SCHEMA_VERSION, runtime_root: f.root, goal_id: "goal-a", todo_id: "todo_one", owner: "agent-a", idempotency_key: "lease-a", ttl_seconds: 600, write_scopes: [], expected_version: null, authority}; - const held = await executeTaskLeaseAcquire(request); - assert.equal(held.ok, false); - assert.equal(held.error_code, "shadow_capture_prepare_failed", JSON.stringify(held)); + assert.equal((await executeTaskLeaseAcquire(request)).error_code, "canonical_authority_required"); const leasePath = join(f.root, "goals", "goal-a", "task-leases", "todo_one.json"); await assert.rejects(access(leasePath)); - await rm(part); - const applied = await executeTaskLeaseAcquire(request); - assert.equal(applied.ok, true, JSON.stringify(applied)); - assert.equal(typeof (applied.coordination_runtime_shadow_capture as Record).entry_id, "string"); + await atomicWriteJson(leasePath, {schema_version: "task_lease_v0", goal_id: "goal-a", todo_id: "todo_one", + owner: "agent-a", idempotency_key: "lease-a", acquire_ttl_seconds: 600, write_scopes: [], + version: 1, lease_epoch: 1, acquired_at: "2026-09-01T03:00:00Z", updated_at: "2026-09-01T03:00:00Z", + expires_at: "2026-09-01T03:10:00Z", status: "active"}); const before = await readFile(leasePath, "utf8"); - await atomicWriteJson(join(part, "drain-cursor.json"), {}); - const released = await executeTaskLeaseLifecycle({schema_version: TASK_LEASE_LIFECYCLE_REQUEST_SCHEMA_VERSION, + const release = {schema_version: TASK_LEASE_LIFECYCLE_REQUEST_SCHEMA_VERSION, operation: "release", runtime_root: f.root, goal_id: "goal-a", todo_id: "todo_one", - owner: "agent-a", idempotency_key: "lease-a", expected_version: 1, authority}); - assert.equal(released.ok, false); - assert.equal(released.error_code, "shadow_capture_prepare_failed", JSON.stringify(released)); + owner: "agent-a", idempotency_key: "lease-a", expected_version: 1, authority}; + await writeFile(part, "prepare unavailable"); + const unavailable = await executeTaskLeaseLifecycle(release); + assert.equal(unavailable.error_code, "shadow_capture_prepare_failed", JSON.stringify(unavailable)); + assert.equal(await readFile(leasePath, "utf8"), before); + await rm(part); + await atomicWriteJson(join(part, "drain-cursor.json"), {}); + const corrupt = await executeTaskLeaseLifecycle(release); + assert.equal(corrupt.error_code, "shadow_capture_prepare_failed", JSON.stringify(corrupt)); assert.equal(await readFile(leasePath, "utf8"), before); + await rm(join(part, "drain-cursor.json")); + const released = await executeTaskLeaseLifecycle(release); + assert.equal(released.released, true, JSON.stringify(released)); + assert.equal(typeof (released.coordination_runtime_shadow_capture as Record).entry_id, "string"); + assert.equal(JSON.parse(await readFile(leasePath, "utf8")).status, "released"); }); diff --git a/tests/control_plane_ts/task_lease_acquire.test.ts b/tests/control_plane_ts/task_lease_acquire.test.ts index bb94af08bb..60939253e6 100644 --- a/tests/control_plane_ts/task_lease_acquire.test.ts +++ b/tests/control_plane_ts/task_lease_acquire.test.ts @@ -45,7 +45,7 @@ async function request( overrides: Record = {}, ): Promise> { const authority = { - handoff_mode: "legacy", + handoff_mode: "hard_lease", registered_agent_candidates: [["agent-a", "agent-b"]], todos: [ { @@ -91,72 +91,49 @@ async function persistedLease( return JSON.parse(await readFile(leasePath(root, todoId), "utf8")); } -test("native acquire owns validation, generation, persistence, and canonical receipts", async (t) => { +// Frozen 1.x persisted shape, formerly asserted by the fresh-acquire test. +// Recovery setup must not depend on a writer that the candidate has retired. +async function seedOriginalLease(root: string): Promise { + await mkdir(join(root, "runtime", "goals", "goal-a", "task-leases"), { recursive: true }); + await writeFile(leasePath(root), JSON.stringify({ + schema_version: "task_lease_v0", goal_id: "goal-a", todo_id: "todo_target", + owner: "agent-a", idempotency_key: "turn-1", write_scopes: ["loopx/**"], + acquire_ttl_seconds: 120, version: 1, lease_epoch: 1, + acquired_at: "2026-08-27T03:00:00Z", updated_at: "2026-08-27T03:00:00Z", + expires_at: "2026-08-27T03:02:00Z", status: "active", + })); +} + +test("fresh source acquisition requires import before creating an execution grant", async (t) => { const root = await workspace(t); const result = await executeTaskLeaseAcquire(await request(root), { now: () => FIXED_NOW }); - - assert.equal(result.ok, true); - assert.equal(result.acquired, true); - assert.equal(result.idempotent, false); - assert.equal("handoff_mode" in result, false); - assert.deepEqual(result.settlement, { - effect_id: "goal-a:agent-a:todo_target:turn-1", - receipts: [ - { step: "validation", status: "committed", effect_id: "goal-a:agent-a:todo_target:turn-1" }, - { - step: "durable_writeback", - status: "committed", - effect_id: "goal-a:agent-a:todo_target:turn-1", - }, - ], - }); - assert.deepEqual(await persistedLease(root), { - schema_version: "task_lease_v0", - goal_id: "goal-a", - todo_id: "todo_target", - owner: "agent-a", - idempotency_key: "turn-1", - write_scopes: ["loopx/**"], - acquire_ttl_seconds: 120, - version: 1, - lease_epoch: 1, - acquired_at: "2026-08-27T03:00:00Z", - updated_at: "2026-08-27T03:00:00Z", - expires_at: "2026-08-27T03:02:00Z", - status: "active", - }); + assert.equal(result.ok, false); + assert.equal(result.error_code, "canonical_authority_required"); + assert.match(String(result.error), /import/u); + await assert.rejects(readFile(leasePath(root)), { code: "ENOENT" }); }); -test("exact replay is idempotent and does not rewrite the lease", async (t) => { +test("original acquisition replay is idempotent without rewriting its lease", async (t) => { const root = await workspace(t); + await seedOriginalLease(root); + const before = await readFile(leasePath(root), "utf8"); let writes = 0; - const first = await executeTaskLeaseAcquire(await request(root), { - now: () => FIXED_NOW, - beforeWrite: () => { - writes += 1; - }, - }); - const second = await executeTaskLeaseAcquire(await request(root), { + const replay = await executeTaskLeaseAcquire(await request(root), { now: () => new Date("2026-08-27T03:00:30.000Z"), - beforeWrite: () => { - writes += 1; - }, + beforeWrite: () => { writes += 1; }, }); - - assert.equal(first.acquired, true); - assert.equal(second.acquired, false); - assert.equal(second.idempotent, true); - assert.equal(writes, 1); - assert.equal( - (second.settlement as Record).effect_id, - "goal-a:agent-a:todo_target:turn-1", - ); - assert.equal((await persistedLease(root)).version, 1); + assert.equal(replay.ok, true); + assert.equal(replay.acquired, false); + assert.equal(replay.idempotent, true); + assert.equal(writes, 0); + assert.equal((replay.settlement as Record).effect_id, + "goal-a:agent-a:todo_target:turn-1"); + assert.equal(await readFile(leasePath(root), "utf8"), before); }); test("replay key reuse, CAS mismatch, and active same-todo contention stay typed", async (t) => { const root = await workspace(t); - await executeTaskLeaseAcquire(await request(root), { now: () => FIXED_NOW }); + await seedOriginalLease(root); const reuse = await executeTaskLeaseAcquire( await request(root, { ttl_seconds: 300 }), @@ -183,7 +160,7 @@ test("replay key reuse, CAS mismatch, and active same-todo contention stay typed assert.equal(contention.error_code, "todo_lease_conflict"); }); -test("retired generations reject key reuse and mint monotonic version and epoch", async (t) => { +test("retired generations reject key reuse and refuse a new source execution", async (t) => { const root = await workspace(t); await mkdir(join(root, "runtime", "goals", "goal-a", "task-leases"), { recursive: true }); await writeFile( @@ -210,13 +187,13 @@ test("retired generations reject key reuse and mint monotonic version and epoch" assert.equal(reuse.error_code, "idempotency_key_reuse"); assert.match(String(reuse.error), /expired or released/u); + const before = await readFile(leasePath(root), "utf8"); const acquired = await executeTaskLeaseAcquire( await request(root, { idempotency_key: "new-key", expected_version: 7 }), { now: () => FIXED_NOW }, ); - assert.equal(acquired.ok, true); - assert.equal((await persistedLease(root)).version, 8); - assert.equal((await persistedLease(root)).lease_epoch, 2); + assert.equal(acquired.error_code, "canonical_authority_required"); + assert.equal(await readFile(leasePath(root), "utf8"), before); }); test("timezone-less legacy expiration remains UTC", async (t) => { @@ -279,7 +256,8 @@ test("effective overlapping leases conflict while ineligible leases self-disarm" await request(root, { authority }), { now: () => FIXED_NOW }, ); - assert.equal(acquired.ok, true); + assert.equal(acquired.error_code, "canonical_authority_required"); + await assert.rejects(readFile(leasePath(root)), { code: "ENOENT" }); }); test("legacy scope strings retain Python splitting and fnmatch behavior", async (t) => { @@ -465,43 +443,21 @@ test("source CAS rejects stale authority facts before lease read or write", asyn await assert.rejects(() => readFile(leasePath(root), "utf8"), { code: "ENOENT" }); }); -test("source CAS is rechecked after the decision and immediately before write", async (t) => { +test("source cutoff precedes write hooks and repeated attempts remain effect-free", async (t) => { const root = await workspace(t); const input = await request(root); - - const result = await executeTaskLeaseAcquire(input, { - now: () => FIXED_NOW, - beforeWrite: () => writeFile( - join(root, "authority-source.json"), - "authority-changed-after-decision", - "utf8", - ), - }); - - assert.equal(result.error_code, "authority_source_changed"); - await assert.rejects(() => readFile(leasePath(root), "utf8"), { code: "ENOENT" }); -}); - -test("failed pre-write attempt leaves no partial lease and a retry succeeds", async (t) => { - const root = await workspace(t); - const input = await request(root); - await assert.rejects( - () => executeTaskLeaseAcquire(input, { + for (let attempt = 0; attempt < 2; attempt += 1) { + const result = await executeTaskLeaseAcquire(input, { now: () => FIXED_NOW, - beforeWrite: () => { - throw new Error("simulated crash"); - }, - }), - /simulated crash/u, - ); - await assert.rejects(() => readFile(leasePath(root), "utf8"), { code: "ENOENT" }); - - const retry = await executeTaskLeaseAcquire(input, { now: () => FIXED_NOW }); - assert.equal(retry.ok, true); - assert.equal((await persistedLease(root)).version, 1); + beforeWrite: () => { assert.fail("retired writer hook must never run"); }, + }); + assert.equal(result.error_code, "canonical_authority_required"); + await assert.rejects(readFile(leasePath(root)), { code: "ENOENT" }); + assert.equal(await readFile(join(root, "authority-source.json"), "utf8"), "authority-v1"); + } }); -test("corrupt bool integers fail closed and legacy epoch advances", async (t) => { +test("corrupt bool integers fail closed and valid retired records stay unchanged", async (t) => { const root = await workspace(t); await mkdir(join(root, "runtime", "goals", "goal-a", "task-leases"), { recursive: true }); await writeFile( @@ -531,9 +487,10 @@ test("corrupt bool integers fail closed and legacy epoch advances", async (t) => }), "utf8", ); + const before = await readFile(leasePath(root), "utf8"); const migrated = await executeTaskLeaseAcquire(await request(root), { now: () => FIXED_NOW }); - assert.equal(migrated.ok, true); - assert.equal((await persistedLease(root)).lease_epoch, 2); + assert.equal(migrated.error_code, "canonical_authority_required"); + assert.equal(await readFile(leasePath(root), "utf8"), before); }); for (const acquireTtlSeconds of [0, -1]) { @@ -680,8 +637,8 @@ for (const { expiresAt, active } of [ assert.equal(result.error_code, "todo_lease_conflict"); assert.deepEqual(await persistedLease(root), existing); } else { - assert.equal(result.ok, true); - assert.equal((await persistedLease(root)).owner, "agent-a"); + assert.equal(result.error_code, "canonical_authority_required"); + assert.deepEqual(await persistedLease(root), existing); } }); } @@ -690,7 +647,7 @@ for (const field of ["version", "lease_epoch"] as const) { test(`legacy acquisition rejects exhausted ${field} before durable writeback`, async t => { const root = await workspace(t); const command = await request(root); - assert.equal((await executeTaskLeaseAcquire(command, {now: () => FIXED_NOW})).ok, true); + await seedOriginalLease(root); const lease = {...await persistedLease(root), status: "released", [field]: Number.MAX_SAFE_INTEGER}; await writeFile(leasePath(root), JSON.stringify(lease)); const before = await readFile(leasePath(root), "utf8"); diff --git a/tests/control_plane_ts/task_lease_acquire_cli.test.ts b/tests/control_plane_ts/task_lease_acquire_cli.test.ts index 517c9d7657..9898172739 100644 --- a/tests/control_plane_ts/task_lease_acquire_cli.test.ts +++ b/tests/control_plane_ts/task_lease_acquire_cli.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -50,7 +50,7 @@ async function tempRequest(t: TestContext): Promise> { write_scopes: ["docs/**"], expected_version: null, authority: { - handoff_mode: "legacy", + handoff_mode: "hard_lease", registered_agent_candidates: [["agent-cli"]], todos: [{ todo_id: "todo_cli_native", @@ -69,26 +69,34 @@ async function tempRequest(t: TestContext): Promise> { }; } -test("native task-lease command writes and replays one JSON envelope", async (t) => { +test("native CLI refuses fresh source execution and replays an original lease", async (t) => { const input = await tempRequest(t); + const leaseDirectory = join(String(input.runtime_root), "goals", "goal-cli", "task-leases"); + const leasePath = join(leaseDirectory, "todo_cli_native.json"); const first = runCli(input); - assert.equal(first.status, 0); - assert.equal(first.value.ok, true); - assert.equal(first.value.acquired, true); + assert.notEqual(first.status, 0); + assert.equal(first.value.ok, false); + assert.equal(first.value.error_code, "canonical_authority_required"); + await assert.rejects(readFile(leasePath), { code: "ENOENT" }); + // Historical record; future expiry makes the CLI test independent of wall-clock timing. + await mkdir(leaseDirectory, { recursive: true }); + const original = JSON.stringify({ + schema_version: "task_lease_v0", goal_id: "goal-cli", todo_id: "todo_cli_native", + owner: "agent-cli", idempotency_key: "turn-cli", write_scopes: ["docs/**"], + acquire_ttl_seconds: 120, version: 1, lease_epoch: 1, status: "active", + acquired_at: "2026-08-27T03:00:00Z", updated_at: "2026-08-27T03:00:00Z", + expires_at: "2099-01-01T00:00:00Z", + }); + await writeFile(leasePath, original); const replay = runCli(input); assert.equal(replay.status, 0); + assert.equal(replay.value.ok, true); + assert.equal(replay.value.acquired, false); assert.equal(replay.value.idempotent, true); - assert.equal( - (replay.value.settlement as Record).effect_id, - (first.value.settlement as Record).effect_id, - ); - const leasePath = String(first.value.lease_path); - assert.equal( - (JSON.parse(await readFile(leasePath, "utf8")) as Record) - .version, - 1, - ); + assert.equal((replay.value.settlement as Record).effect_id, + "goal-cli:agent-cli:todo_cli_native:turn-cli"); + assert.equal(await readFile(leasePath, "utf8"), original); }); test("native task-lease command returns one typed malformed-input envelope", () => { diff --git a/tests/control_plane_ts/task_lease_lifecycle.test.ts b/tests/control_plane_ts/task_lease_lifecycle.test.ts index b10feb2205..07925bf279 100644 --- a/tests/control_plane_ts/task_lease_lifecycle.test.ts +++ b/tests/control_plane_ts/task_lease_lifecycle.test.ts @@ -1,40 +1,14 @@ import assert from "node:assert/strict"; -import { createHash } from "node:crypto"; -import { atomicWriteJson } from "../../loopx/control_plane/effect_runtime_io.ts"; -import { mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import test, { type TestContext } from "node:test"; - -import { - executeTaskLeaseAcquire, - TASK_LEASE_ACQUIRE_REQUEST_SCHEMA_VERSION, -} from "../../loopx/control_plane/work_items/task_lease_acquire.ts"; -import { - executeTaskLeaseLifecycle, - TASK_LEASE_LIFECYCLE_REQUEST_SCHEMA_VERSION, -} from "../../loopx/control_plane/work_items/task_lease_lifecycle.ts"; +import { atomicWriteJson } from "../../loopx/control_plane/effect_runtime_io.ts"; +import { executeTaskLeaseAcquire } from "../../loopx/control_plane/work_items/task_lease_acquire.ts"; +import { executeTaskLeaseLifecycle } from "../../loopx/control_plane/work_items/task_lease_lifecycle.ts"; import { evaluateTaskLeaseLifecycleDecision } from "../../loopx/control_plane/work_items/task_lease_lifecycle_decision.ts"; import { shadowManagementStatePath } from "../../loopx/control_plane/coordination/shadow_management.ts"; - -const ACQUIRE_NOW = new Date("2026-09-01T03:00:00.000Z"); - -test("maintenance corruption blocks native acquire and release before lease mutation", async (t) => { - const root = await workspace(t); - const acquired = await executeTaskLeaseAcquire(await acquireRequest(root)); - assert.equal(acquired.ok, true); - const path = join(root, "runtime", "goals", "goal-a", "task-leases", "todo_target.json"); - const before = await readFile(path, "utf8"); - await atomicWriteJson(shadowManagementStatePath(join(root, "runtime"), "goal-a"), {}); - const released = await executeTaskLeaseLifecycle(await lifecycleRequest(root, "release")); - assert.equal(released.ok, false); - assert.equal(released.error_code, "shadow_management_state_invalid"); - assert.equal(await readFile(path, "utf8"), before); - const retry = await executeTaskLeaseAcquire(await acquireRequest(root, {idempotency_key: "new-key"})); - assert.equal(retry.ok, false); - assert.equal(retry.error_code, "shadow_management_state_invalid"); - assert.equal(await readFile(path, "utf8"), before); -}); +import type { JsonObject } from "../../loopx/control_plane/effect_program.ts"; function lifecycleDecision( operation: "renew" | "transfer" | "release", @@ -150,1604 +124,270 @@ test("soft-claim rejects renew and transfer before missing version; release keep assert.equal(cleanup.next_lease?.lease_epoch, 7); }); -async function workspace(t: TestContext): Promise { - const root = await mkdtemp(join(tmpdir(), "loopx-task-lease-lifecycle-")); - t.after(() => rm(root, { recursive: true, force: true })); - return root; -} - -async function authoritySource(root: string, content = "authority-v1") { - const path = join(root, "authority-source.json"); - await writeFile(path, content, "utf8"); - return { - source_id: "authority", - path, - state: "file", - sha256: createHash("sha256").update(content).digest("hex"), - }; +// Independent 1.x artifacts from the last pre-cutoff base. Only $ROOT paths +// relocate; original operation IDs, digests, generation tuples and tokens do not. +// No old acquire/renew/fence producer is executed to prepare candidate tests. +interface HistoricalScenario { + request: JsonObject; + files: Record; } - -type RequestOverrides = Record; - -async function authority( - root: string, - overrides: RequestOverrides = {}, -): Promise> { - return { - handoff_mode: "hard_lease", - registered_agent_candidates: [["agent-a", "agent-b"]], - todos: [ - { - todo_id: "todo_target", - status: "open", - claimed_by: null, - excluded_agents: [], - }, - ], - todo_projection_error: null, - source_receipts: [await authoritySource(root)], - ...overrides, +const historical = JSON.parse(await readFile(new URL( + "../control_plane/fixtures/task_lease_1x.json", import.meta.url), "utf8")) as { + source_commit: string; scenarios: Record; }; +const now = () => new Date("2026-09-01T03:03:00Z"); +async function restore(t: TestContext, name = "original") { + const root = await mkdtemp(join(tmpdir(), "loopx-lease-recovery-")); + t.after(() => rm(root, {recursive: true, force: true})); + const fixture = JSON.parse(JSON.stringify(historical.scenarios[name]).replaceAll("$ROOT", root)) as HistoricalScenario; + await writeFile(join(root, "authority.json"), "authority-v1"); + for (const [relative, value] of Object.entries(fixture.files)) { + const path = join(root, "runtime", relative); + await mkdir(dirname(path), {recursive: true}); + await writeFile(path, JSON.stringify(value)); + } + const path = join(root, "runtime/goals/goal-a/task-leases/todo_target.json"); + const record = async () => JSON.parse(await readFile(path, "utf8")) as JsonObject; + const receiptPath = Object.keys(fixture.files).find(p => p.includes("/.lifecycle")); + return {root, ...fixture, path, record, + receiptPath: receiptPath ? join(root, "runtime", receiptPath) : null}; } - -async function acquireRequest( - root: string, - overrides: RequestOverrides = {}, -): Promise> { - return { - schema_version: TASK_LEASE_ACQUIRE_REQUEST_SCHEMA_VERSION, - runtime_root: join(root, "runtime"), - goal_id: "goal-a", - todo_id: "todo_target", - owner: "agent-a", - idempotency_key: "lease-a", - ttl_seconds: 600, - write_scopes: [], - expected_version: null, - authority: await authority(root), - ...overrides, - }; +function lifecycle(f: {request: JsonObject}, operation: string, overrides: JsonObject = {}): JsonObject { + return {...f.request, schema_version: "loopx_task_lease_lifecycle_native_v0", operation, + expected_version: 1, new_owner: null, new_idempotency_key: null, lock_token: null, + committed: false, release_lease: false, fence_owner: null, fence_idempotency_key: null, + fence_expected_version: null, fence_expected_lease_epoch: 1, ...overrides}; } - -async function lifecycleRequest( - root: string, - operation: string, - overrides: RequestOverrides = {}, -): Promise> { - return { - schema_version: TASK_LEASE_LIFECYCLE_REQUEST_SCHEMA_VERSION, - operation, - runtime_root: join(root, "runtime"), - goal_id: "goal-a", - todo_id: "todo_target", - owner: "agent-a", - idempotency_key: "lease-a", - expected_version: 1, - ttl_seconds: 600, - new_owner: null, - new_idempotency_key: null, - lock_token: null, - committed: false, - release_lease: false, - fence_owner: null, - fence_idempotency_key: null, - fence_expected_version: null, - fence_expected_lease_epoch: 1, - authority: await authority(root), - ...overrides, - }; +function closeRequest(f: {request: JsonObject}, fence: JsonObject, overrides: JsonObject = {}): JsonObject { + return lifecycle(f, "fence_close", {authority: null, owner: null, idempotency_key: null, + expected_version: null, lock_token: fence.lock_token, fence_operation_id: fence.fence_operation_id, + committed: true, release_lease: true, fence_owner: fence.owner ?? null, + fence_idempotency_key: fence.idempotency_key ?? null, fence_expected_version: fence.version ?? null, + fence_expected_lease_epoch: fence.lease_epoch ?? null, ...overrides}); } - -async function lease(root: string): Promise> { - return JSON.parse( - await readFile( - join(root, "runtime", "goals", "goal-a", "task-leases", "todo_target.json"), - "utf8", - ), - ) as Record; +async function receipt(f: {receiptPath: string | null}): Promise { + assert.ok(f.receiptPath); + return JSON.parse(await readFile(f.receiptPath, "utf8")); } -test("native lifecycle renew, transfer, and release preserve CAS generations", async (t) => { - const root = await workspace(t); - const acquired = await executeTaskLeaseAcquire(await acquireRequest(root), { - now: () => ACQUIRE_NOW, - }); - assert.equal(acquired.ok, true); - - const renewed = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "renew"), - { now: () => new Date("2026-09-01T03:01:00.000Z") }, - ); - assert.equal(renewed.ok, true); - assert.equal(renewed.renewed, true); - assert.equal((renewed.lease as Record).version, 2); - - const transferred = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "transfer", { - owner: "agent-a", - idempotency_key: "lease-a", - expected_version: 2, - new_owner: "agent-b", - new_idempotency_key: "lease-b", - }), - { now: () => new Date("2026-09-01T03:02:00.000Z") }, - ); - assert.equal(transferred.ok, true); - assert.equal(transferred.transferred, true); - assert.equal((transferred.lease as Record).owner, "agent-b"); - assert.equal((transferred.lease as Record).lease_epoch, 2); - assert.equal((transferred.lease as Record).version, 3); - - const released = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "release", { - owner: "agent-b", - idempotency_key: "lease-b", - expected_version: 3, - authority: null, - }), - { now: () => new Date("2026-09-01T03:03:00.000Z") }, - ); - assert.equal(released.ok, true); - assert.equal(released.released, true); - assert.equal((await lease(root)).status, "released"); -}); - -for (const scenario of [ - { - name: "unregistered", - errorCode: "owner_not_registered", - newOwner: "agent-c", - authority: {}, - }, - { - name: "excluded", - errorCode: "owner_excluded_from_todo", - newOwner: "agent-b", - authority: { - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: null, - excluded_agents: ["agent-b"], - }], - }, - }, - { - name: "claim conflict", - errorCode: "owner_conflicts_with_claim", - newOwner: "agent-b", - authority: { - registered_agent_candidates: [["agent-a", "agent-b", "agent-c"]], - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: "agent-c", - excluded_agents: [], - }], - }, - }, - { - name: "closed todo", - errorCode: "todo_not_open", - newOwner: "agent-b", - authority: { - todos: [{ - todo_id: "todo_target", - status: "done", - claimed_by: null, - excluded_agents: [], - }], - }, - }, - { - name: "missing todo", - errorCode: "todo_not_found", - newOwner: "agent-b", - authority: { todos: [] }, - }, -] as const) { - test(`transfer rejection identifies the requested new owner: ${scenario.name}`, async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const rejected = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "transfer", { - new_owner: scenario.newOwner, - new_idempotency_key: "lease-target", - authority: await authority(root, scenario.authority), - }), - { now: () => new Date("2026-09-01T03:01:00.000Z") }, - ); - assert.equal(rejected.ok, false); - assert.equal(rejected.error_code, scenario.errorCode); - assert.equal(rejected.owner, scenario.newOwner); - assert.equal((await lease(root)).owner, "agent-a"); - assert.equal((await lease(root)).version, 1); +for (const operation of ["renew", "transfer", "terminal_verify", "holder_verify"]) { + test(`2.0 refuses fresh source ${operation} without touching an original lease`, async t => { + const f = await restore(t); const before = await readFile(f.path, "utf8"); + const result = await executeTaskLeaseLifecycle(lifecycle(f, operation, + operation === "transfer" ? {new_owner: "agent-b", new_idempotency_key: "lease-b"} : {}), {now}); + assert.equal(result.error_code, "canonical_authority_required"); + assert.equal(await readFile(f.path, "utf8"), before); }); } -test("lifecycle decode failures preserve the requested action", async (t) => { - const root = await workspace(t); - const rejected = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "renew", { expected_version: "1" }), - ); - - assert.equal(rejected.ok, false); - assert.equal(rejected.action, "renew"); - assert.equal(rejected.error_code, "invalid_request"); -}); - -test("lifecycle replay is idempotent and parameter reuse fails without mutation", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const request = await lifecycleRequest(root, "renew"); - const first = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - const afterFirst = await lease(root); - const replay = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:30:00.000Z"), - }); - assert.equal(first.ok, true); - assert.equal(replay.ok, true); - assert.equal(replay.idempotent, true); - assert.deepEqual(await lease(root), afterFirst); - - const reuse = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "renew", { ttl_seconds: 300 }), - { now: () => new Date("2026-09-01T03:02:00.000Z") }, - ); - assert.equal(reuse.ok, false); - assert.equal(reuse.error_code, "idempotency_key_reuse"); - assert.deepEqual(await lease(root), afterFirst); -}); - -test("lifecycle rejects stale versions and expired leases before writeback", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root, { ttl_seconds: 30 }), { - now: () => ACQUIRE_NOW, - }); - - const stale = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "renew", { expected_version: 0 }), - { now: () => new Date("2026-09-01T03:01:00.000Z") }, - ); - assert.equal(stale.ok, false); - assert.equal(stale.error_code, "version_mismatch"); - assert.equal((await lease(root)).version, 1); - - const expired = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "renew", { expected_version: 1 }), - { now: () => new Date("2026-09-01T03:01:00.001Z") }, - ); - assert.equal(expired.ok, false); - assert.equal(expired.error_code, "lease_not_active"); - assert.equal((await lease(root)).version, 1); -}); - -test("lifecycle accepts active leases with high-precision fractional seconds", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const existing = await lease(root); - existing.expires_at = "2026-09-01T03:10:00.123456789Z"; - await writeFile( - join(root, "runtime", "goals", "goal-a", "task-leases", "todo_target.json"), - JSON.stringify(existing), - "utf8", - ); - - const renewed = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "renew"), - { now: () => new Date("2026-09-01T03:01:00.000Z") }, - ); - - assert.equal(renewed.ok, true); - assert.equal(renewed.renewed, true); -}); - -test("holder verification is hard-lease-only and fence close releases only its verified lease", async (t) => { - const root = await workspace(t); - const hardAuthority = await authority(root, { - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: "agent-a", - excluded_agents: [], - }], - }); - await executeTaskLeaseAcquire( - await acquireRequest(root, { authority: hardAuthority }), - { now: () => ACQUIRE_NOW }, - ); - - const checked = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "holder_verify", { - authority: hardAuthority, - idempotency_key: null, - expected_version: null, - owner: "agent-a", - }), - { now: () => new Date("2026-09-01T03:01:00.000Z") }, - ); - assert.equal(checked.ok, true); - const fence = checked.fence as Record; - assert.equal(fence.owner, "agent-a"); - assert.equal(fence.version, 1); - assert.equal(typeof fence.lock_token, "string"); - - const closed = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - committed: true, - release_lease: true, - fence_owner: "agent-a", - // Holder verification is actor/lease-tuple based; the close bridge can - // recover the private execution key from its durable fence receipt. - fence_idempotency_key: null, - fence_expected_version: 1, - }), - }, { now: () => new Date("2026-09-01T03:02:00.000Z") }); - assert.equal(closed.ok, true); - assert.equal(closed.released, true); - assert.equal((await lease(root)).status, "released"); - - const soft = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "holder_verify", { - authority: await authority(root, { handoff_mode: "soft_claim" }), - idempotency_key: null, - expected_version: null, - }), - { now: () => new Date("2026-09-01T03:03:00.000Z") }, - ); - assert.equal(soft.ok, false); - assert.equal(soft.error_code, "handoff_mode_forbids_lease"); -}); - -test("holder verification adopts one explicit retry receipt and fresh gates mint new ids", async (t) => { - const root = await workspace(t); - const hardAuthority = await authority(root, { - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: "agent-a", - excluded_agents: [], - }], - }); - await executeTaskLeaseAcquire( - await acquireRequest(root, { authority: hardAuthority }), - { now: () => ACQUIRE_NOW }, - ); - const request = await lifecycleRequest(root, "holder_verify", { - authority: hardAuthority, - idempotency_key: null, - expected_version: null, - owner: "agent-a", - fence_operation_id: "a".repeat(64), - }); - - const first = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - const firstFence = first.fence as Record; - const retry = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:01.000Z"), - }); - const retryFence = retry.fence as Record; - assert.equal(retry.ok, true); - assert.equal(retryFence.idempotent, true); - assert.equal(retryFence.lock_token, firstFence.lock_token); - assert.equal(retryFence.fence_operation_id, firstFence.fence_operation_id); - - const firstClose = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: firstFence.lock_token, - committed: false, - release_lease: false, - fence_owner: "agent-a", - fence_idempotency_key: null, - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - fence_operation_id: firstFence.fence_operation_id, - }), - ); - assert.equal(firstClose.ok, true); - assert.equal(firstClose.released, false); - - const fresh = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "holder_verify", { - authority: hardAuthority, - idempotency_key: null, - expected_version: null, - owner: "agent-a", - }), - { now: () => new Date("2026-09-01T03:01:02.000Z") }, - ); - const freshFence = fresh.fence as Record; - assert.equal(fresh.ok, true); - assert.notEqual(freshFence.fence_operation_id, firstFence.fence_operation_id); - assert.notEqual(freshFence.lock_token, firstFence.lock_token); - - const freshClose = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: freshFence.lock_token, - committed: false, - release_lease: false, - fence_owner: "agent-a", - fence_idempotency_key: null, - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - fence_operation_id: freshFence.fence_operation_id, - }), - ); - assert.equal(freshClose.ok, true); -}); - -test("fence close retires a lease that expires after verification", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire( - await acquireRequest(root, { ttl_seconds: 1 }), - { now: () => ACQUIRE_NOW }, - ); - - const checked = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "terminal_verify"), - { now: () => new Date("2026-09-01T03:00:00.500Z") }, - ); - assert.equal(checked.ok, true); - const fence = checked.fence as Record; - - const closed = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - }), - { now: () => new Date("2026-09-01T03:00:02.000Z") }, - ); - - assert.equal(closed.ok, true); - assert.equal(closed.released, true); - assert.equal((await lease(root)).status, "released"); - assert.equal((await lease(root)).version, 1); - assert.equal((await lease(root)).lease_epoch, 1); -}); - -test("fence close write failure leaves the lease unchanged and unlocks", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const checked = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "terminal_verify"), - { now: () => new Date("2026-09-01T03:01:00.000Z") }, - ); - const fence = checked.fence as Record; - - const failed = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - }), - { - now: () => new Date("2026-09-01T03:02:00.000Z"), - beforeWrite: () => { - throw new Error("simulated fence release write failure"); - }, - }, - ); - assert.equal(failed.ok, false); - assert.equal(failed.error_code, "unexpected_handler_error"); - assert.equal((await lease(root)).status, "active"); - - const cleanup = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "release", { authority: null }), - { now: () => new Date("2026-09-01T03:03:00.000Z") }, - ); - assert.equal(cleanup.ok, true); - assert.equal(cleanup.released, true); -}); - -test("user-gate auto-acquire returns a persistent fence that can be closed", async (t) => { - const root = await workspace(t); - const runtimeShadow = { - schema_version: "loopx_coordination_runtime_shadow_binding_v0", - provider: "file_v0", - }; - const facts = await authority(root, { - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: null, - excluded_agents: [], - role: "user", - task_class: "user_gate", - }], - }); - const checked = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "terminal_verify", { - authority: facts, - owner: "agent-a", - idempotency_key: null, - expected_version: null, - allow_user_gate_auto_acquire: true, - runtime_shadow: runtimeShadow, - }), - { now: () => ACQUIRE_NOW }, - ); - assert.equal(checked.ok, true); - const autoCapture = checked.coordination_runtime_shadow_capture as Record; - assert.equal(autoCapture.entry_id, null); - assert.equal(autoCapture.seq, null); - assert.equal(autoCapture.skipped_reason, "bootstrap_required"); - assert.equal(autoCapture.failure, null); - const fence = checked.fence as Record; - assert.equal(fence.auto_acquired, true); - assert.equal((await lease(root)).status, "active"); - - const closed = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: (await lease(root)).idempotency_key, - fence_expected_version: (await lease(root)).version, - runtime_shadow: runtimeShadow, - }), - }, { now: () => new Date("2026-09-01T03:01:00.000Z") }); - assert.equal(closed.ok, true); - assert.equal(closed.released, true); - const closeCapture = closed.coordination_runtime_shadow_capture as Record; - assert.equal(closeCapture.entry_id, null); - assert.equal(closeCapture.seq, null); - assert.equal(closeCapture.skipped_reason, "bootstrap_required"); - assert.equal(closeCapture.failure, null); - assert.equal((await lease(root)).status, "released"); -}); - -test("terminal fence retries an auto-acquire after the lease write window", async (t) => { - const root = await workspace(t); - const facts = await authority(root, { - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: null, - excluded_agents: [], - role: "user", - task_class: "user_gate", - }], - }); - const request = await lifecycleRequest(root, "terminal_verify", { - authority: facts, - owner: "agent-a", - idempotency_key: null, - expected_version: null, - allow_user_gate_auto_acquire: true, - }); - let injected = false; - const first = await executeTaskLeaseLifecycle(request, { - now: () => ACQUIRE_NOW, - beforeWrite: async (next) => { - if (injected) return; - injected = true; - const leasePath = join( - root, - "runtime", - "goals", - "goal-a", - "task-leases", - "todo_target.json", - ); - await atomicWriteJson(leasePath, next); - throw new Error("simulated response loss after auto-acquire write"); - }, - }); - assert.equal(first.ok, false); - assert.equal((await lease(root)).status, "active"); - - const retry = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:00:01.000Z"), - }); - assert.equal(retry.ok, true); - const fence = retry.fence as Record; - assert.equal(fence.auto_acquired, true); - assert.equal((await lease(root)).version, 1); - - const closed = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - fence_operation_id: fence.fence_operation_id, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: (await lease(root)).idempotency_key, - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - }); - assert.equal(closed.ok, true); - assert.equal(closed.released, true); -}); - -test("auto-acquire keeps its token claim until the held receipt is durable", async (t) => { - const root = await workspace(t); - const facts = await authority(root, { - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: null, - excluded_agents: [], - role: "user", - task_class: "user_gate", - }], - }); - const request = await lifecycleRequest(root, "terminal_verify", { - authority: facts, - owner: "agent-a", - idempotency_key: null, - expected_version: null, - allow_user_gate_auto_acquire: true, - }); - let resumeVerifier: () => void = () => { - throw new Error("verifier gate was not initialized"); - }; - let verifierEntered: () => void = () => { - throw new Error("verifier start gate was not initialized"); - }; - const verifierGate = new Promise((resolve) => { - resumeVerifier = resolve; - }); - const verifierStarted = new Promise((resolve) => { - verifierEntered = resolve; - }); - const leasePath = join( - root, - "runtime", - "goals", - "goal-a", - "task-leases", - "todo_target.json", - ); - const verifier = executeTaskLeaseLifecycle(request, { - now: () => ACQUIRE_NOW, - beforeWrite: async (next) => { - // Expose the exact response-loss window: the acquired receipt exists, - // and the lease has been written, but the verifier has not published - // its held receipt yet. - await atomicWriteJson(leasePath, next); - verifierEntered(); - await verifierGate; - }, - }); - await verifierStarted; - - const receiptDirectory = join( - root, - "runtime", - "goals", - "goal-a", - "task-leases", - ".lifecycle-fences", - ); - const [receiptName] = await readdir(receiptDirectory); - const acquiredReceipt = JSON.parse( - await readFile(join(receiptDirectory, receiptName), "utf8"), - ) as Record; - assert.equal(acquiredReceipt.state, "acquired"); - assert.equal(typeof acquiredReceipt.lock_token, "string"); - - let interruptedClose: Record; - try { - interruptedClose = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: acquiredReceipt.lock_token, - fence_operation_id: acquiredReceipt.operation_id, - committed: false, - release_lease: false, - fence_owner: "agent-a", - fence_idempotency_key: acquiredReceipt.fence_idempotency_key, - fence_expected_version: acquiredReceipt.fence_expected_version, - fence_expected_lease_epoch: acquiredReceipt.fence_expected_lease_epoch, - }), - ); - } finally { - // Keep the test from hanging if the contention assertion fails. - resumeVerifier(); +test("maintenance corruption blocks acquire and original release before mutation", async t => { + const f = await restore(t); const before = await readFile(f.path, "utf8"); + await atomicWriteJson(shadowManagementStatePath(join(f.root, "runtime"), "goal-a"), {}); + for (const result of [await executeTaskLeaseAcquire(f.request), + await executeTaskLeaseLifecycle(lifecycle(f, "release"))]) { + assert.equal(result.error_code, "shadow_management_state_invalid"); + assert.equal(await readFile(f.path, "utf8"), before); } - assert.equal(interruptedClose.ok, false); - assert.equal(interruptedClose.error_code, "fence_token_invalid"); - - const checked = await verifier; - assert.equal(checked.ok, true); - const heldReceipt = JSON.parse( - await readFile(join(receiptDirectory, receiptName), "utf8"), - ) as Record; - assert.equal(heldReceipt.state, "held"); - - const fence = checked.fence as Record; - const cleanup = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - fence_operation_id: fence.fence_operation_id, - committed: false, - release_lease: false, - fence_owner: "agent-a", - fence_idempotency_key: "auto-todo_target", - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - ); - assert.equal(cleanup.ok, true); -}); - -test("a concurrent fence verifier cannot reopen a receipt while close owns its claim", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const verifyRequest = await lifecycleRequest(root, "terminal_verify"); - const checked = await executeTaskLeaseLifecycle(verifyRequest, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(checked.ok, true); - const fence = checked.fence as Record; - - let releaseClose: () => void = () => { - throw new Error("close gate was not initialized"); - }; - let closeEntered: () => void = () => { - throw new Error("close start gate was not initialized"); - }; - const closeGate = new Promise((resolve) => { - releaseClose = resolve; - }); - const closeHookEntered = new Promise((resolve) => { - closeEntered = resolve; - }); - const closePromise = executeTaskLeaseLifecycle( - await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - fence_operation_id: fence.fence_operation_id, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - { - beforeWrite: async () => { - closeEntered(); - await closeGate; - }, - }, - ); - await closeHookEntered; - - let concurrentRetry: Record; - try { - concurrentRetry = await executeTaskLeaseLifecycle(verifyRequest, { - now: () => new Date("2026-09-01T03:01:01.000Z"), - }); - } finally { - // Always unblock the close attempt so a failed assertion cannot leave a - // pending promise holding the test process open. - releaseClose(); - } - assert.equal(concurrentRetry.ok, false); - assert.equal(concurrentRetry.error_code, "fence_token_invalid"); - - const closed = await closePromise; - assert.equal(closed.ok, true); - assert.equal(closed.released, true); - const receiptPath = join( - root, - "runtime", - "goals", - "goal-a", - "task-leases", - ".lifecycle-fences", - `${fence.fence_operation_id}.json`, - ); - const receipt = JSON.parse(await readFile(receiptPath, "utf8")) as Record; - assert.equal(receipt.state, "closed"); - assert.equal((receipt.response as Record).action, "fence_close"); }); -test("held and closed fence receipts replay without changing their intent", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const verifyRequest = await lifecycleRequest(root, "terminal_verify"); - const first = await executeTaskLeaseLifecycle(verifyRequest, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(first.ok, true); - const firstFence = first.fence as Record; - const replay = await executeTaskLeaseLifecycle(verifyRequest, { - now: () => new Date("2026-09-01T03:01:01.000Z"), - }); - assert.equal(replay.ok, true); - assert.equal(replay.fence?.lock_token, firstFence.lock_token); - assert.equal(replay.fence?.idempotent, true); - - const closeRequest = { - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: firstFence.lock_token, - fence_operation_id: firstFence.fence_operation_id, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - }; - const closed = await executeTaskLeaseLifecycle(closeRequest); - assert.equal(closed.ok, true); - assert.equal(closed.released, true); - const staleCleanupLockPath = join( - root, - "runtime", - "goals", - "goal-a", - "task-leases", - ".task-leases.ts-effect.lock", - ); - await writeFile( - staleCleanupLockPath, - JSON.stringify({ pid: process.pid, token: firstFence.lock_token }), - "utf8", - ); - const closeReplay = await executeTaskLeaseLifecycle(closeRequest); - assert.equal(closeReplay.ok, true); - assert.equal(closeReplay.idempotent, true); - await assert.rejects(readFile(staleCleanupLockPath), { code: "ENOENT" }); - const changedClose = await executeTaskLeaseLifecycle({ - ...closeRequest, - release_lease: false, - }); - assert.equal(changedClose.ok, false); - assert.equal(changedClose.error_code, "fence_operation_reuse"); -}); - -test("closed non-required terminal fences replay without a retired lock token", async (t) => { - const root = await workspace(t); - const request = await lifecycleRequest(root, "terminal_verify", { - authority: await authority(root, { handoff_mode: "legacy" }), - idempotency_key: "completion-turn-a", - expected_version: null, - require_active_when_fence_supplied: false, - }); - - const first = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(first.ok, true); - const firstFence = first.fence as Record; - assert.equal(firstFence.required, false); - assert.equal(firstFence.active, false); - assert.equal(firstFence.lock_token, undefined); - - const hardReplay = await executeTaskLeaseLifecycle({ - ...request, - authority: await authority(root), - }); - assert.equal(hardReplay.ok, false); - assert.equal(hardReplay.error_code, "handoff_mode_requires_lease"); - - const replay = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:02:00.000Z"), - }); - assert.equal(replay.ok, true); - const replayFence = replay.fence as Record; - assert.equal(replayFence.required, false); - assert.equal(replayFence.active, false); - assert.equal(replayFence.closed, true); - assert.equal(replayFence.idempotent, true); - assert.equal(replayFence.lock_token, undefined); - - await assert.rejects( - readFile(join( - root, - "runtime", - "goals", - "goal-a", - "task-leases", - ".task-leases.ts-effect.lock", - )), - { code: "ENOENT" }, - ); -}); - -test("aborted required fence close re-verifies the same lease generation", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const request = await lifecycleRequest(root, "terminal_verify"); - const first = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(first.ok, true); - const firstFence = first.fence as Record; - - const aborted = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: firstFence.lock_token, - fence_operation_id: firstFence.fence_operation_id, - committed: false, - release_lease: false, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - }); - assert.equal(aborted.ok, true); - assert.equal(aborted.released, false); - assert.equal((await lease(root)).status, "active"); - - const retry = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:01.000Z"), - }); - assert.equal(retry.ok, true); - const retryFence = retry.fence as Record; - assert.equal(retryFence.required, true); - assert.equal(retryFence.active, true); - assert.notEqual(retryFence.lock_token, firstFence.lock_token); - - const cleanup = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: retryFence.lock_token, - fence_operation_id: retryFence.fence_operation_id, - committed: false, - release_lease: false, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - }); - assert.equal(cleanup.ok, true); -}); - -test("closed non-required fence cannot bypass a lease acquired later", async (t) => { - const root = await workspace(t); - const legacyRequest = await lifecycleRequest(root, "terminal_verify", { - authority: await authority(root, { handoff_mode: "legacy" }), - idempotency_key: "completion-turn-a", - expected_version: null, - require_active_when_fence_supplied: false, - }); - const first = await executeTaskLeaseLifecycle(legacyRequest, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(first.ok, true); - assert.equal((first.fence as Record).required, false); - - await executeTaskLeaseAcquire(await acquireRequest(root), { - now: () => new Date("2026-09-01T03:01:01.000Z"), - }); - const replay = await executeTaskLeaseLifecycle(legacyRequest, { - now: () => new Date("2026-09-01T03:01:02.000Z"), - }); - assert.equal(replay.ok, false); - assert.equal(replay.error_code, "lease_cas_mismatch"); - assert.equal((await lease(root)).status, "active"); -}); - -test("closed terminal receipt rejects an old generation after lease reacquire", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const request = await lifecycleRequest(root, "terminal_verify"); - const checked = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(checked.ok, true); - const fence = checked.fence as Record; - const closed = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - fence_operation_id: fence.fence_operation_id, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - }); - assert.equal(closed.ok, true); - assert.equal((await lease(root)).status, "released"); - - await executeTaskLeaseAcquire(await acquireRequest(root, { - idempotency_key: "lease-b", - }), { - now: () => new Date("2026-09-01T03:02:00.000Z"), - }); - assert.equal((await lease(root)).version, 2); - assert.equal((await lease(root)).lease_epoch, 2); - - const replay = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:02:01.000Z"), - }); - assert.equal(replay.ok, false); - assert.equal(replay.error_code, "fence_state_invalid"); - assert.equal((await lease(root)).status, "active"); -}); - -test("held fence replay revalidates the canonical todo authority", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const request = await lifecycleRequest(root, "terminal_verify"); - const first = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(first.ok, true); - - const changedAuthority = await authority(root, { - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: "agent-b", - excluded_agents: [], - }], - }); - const replay = await executeTaskLeaseLifecycle( - { ...request, authority: changedAuthority }, - { now: () => new Date("2026-09-01T03:01:01.000Z") }, - ); - assert.equal(replay.ok, false); - assert.equal(replay.error_code, "authority_todo_mismatch"); - - const cleanup = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "release", { authority: null }), - { now: () => new Date("2026-09-01T03:02:00.000Z") }, - ); - assert.equal(cleanup.ok, true); - assert.equal(cleanup.released, true); -}); - -test("partial caller Todo snapshots inherit derived canonical metadata", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const request = await lifecycleRequest(root, "terminal_verify", { - authority: await authority(root, { - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: "agent-a", - excluded_agents: [], - task_class: "advancement_task", - }], - }), - todo: { - todo_id: "todo_target", - status: "open", - claimed_by: "agent-a", - excluded_agents: [], - }, - }); - const checked = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(checked.ok, true); - const fence = checked.fence as Record; - const closed = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - fence_operation_id: fence.fence_operation_id, - committed: false, - release_lease: false, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - }); - assert.equal(closed.ok, true); - - const explicitMismatch = await executeTaskLeaseLifecycle({ - ...request, - todo: { - todo_id: "todo_target", - status: "open", - claimed_by: "agent-a", - excluded_agents: [], - task_class: "continuous_monitor", - }, - }); - assert.equal(explicitMismatch.ok, false); - assert.equal(explicitMismatch.error_code, "authority_todo_mismatch"); -}); - -test("fence operation replay is independent of the caller process id", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const firstRequest = await lifecycleRequest(root, "terminal_verify", { - owner_pid: 999999, - }); - const first = await executeTaskLeaseLifecycle(firstRequest, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(first.ok, true); - const firstFence = first.fence as Record; - - const retry = await executeTaskLeaseLifecycle( - { ...firstRequest, owner_pid: process.pid }, - { now: () => new Date("2026-09-01T03:01:01.000Z") }, - ); - assert.equal(retry.ok, true); - const retryFence = retry.fence as Record; - assert.equal(retryFence.fence_operation_id, firstFence.fence_operation_id); - assert.notEqual(retryFence.lock_token, firstFence.lock_token); - - const closed = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: retryFence.lock_token, - fence_operation_id: retryFence.fence_operation_id, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - }); - assert.equal(closed.ok, true); - assert.equal(closed.released, true); -}); - -test("legacy fence receipts default omitted optional fields to null", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const request = await lifecycleRequest(root, "terminal_verify"); - const first = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - }); - assert.equal(first.ok, true); - const fence = first.fence as Record; - const receiptDirectory = join( - root, - "runtime", - "goals", - "goal-a", - "task-leases", - ".lifecycle-fences", - ); - const [name] = await readdir(receiptDirectory); - const receiptPath = join(receiptDirectory, name); - const receipt = JSON.parse(await readFile(receiptPath, "utf8")) as Record; - delete receipt.close_request_digest; - delete receipt.verify_response; - await writeFile(receiptPath, JSON.stringify(receipt), "utf8"); - - const replay = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:01.000Z"), - }); - assert.equal(replay.ok, true); - assert.equal(replay.fence?.idempotent, true); - - const closed = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - fence_operation_id: fence.fence_operation_id, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - fence_expected_lease_epoch: 1, - }), - }); - assert.equal(closed.ok, true); - - const cleanup = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "release", { authority: null }), - { now: () => new Date("2026-09-01T03:02:00.000Z") }, - ); - assert.equal(cleanup.ok, true); - assert.equal(cleanup.released, true); -}); - -test("fence close rejects a stale lease epoch without releasing the lease", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const checked = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "terminal_verify"), - { now: () => new Date("2026-09-01T03:01:00.000Z") }, - ); - const fence = checked.fence as Record; - const staleClose = await executeTaskLeaseLifecycle({ - ...await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - fence_operation_id: fence.fence_operation_id, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: "lease-a", - fence_expected_version: 1, - fence_expected_lease_epoch: 0, - }), - }); - assert.equal(staleClose.ok, false); - assert.equal(staleClose.error_code, "fence_cas_mismatch"); - assert.equal((await lease(root)).status, "active"); - - await assert.rejects( - readFile(join( - root, - "runtime", - "goals", - "goal-a", - "task-leases", - ".task-leases.ts-effect.lock", - )), - { code: "ENOENT" }, - ); -}); - -test("fence operation ids are strict and caller Todo snapshots cannot override authority", async (t) => { - const root = await workspace(t); - const invalid = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "terminal_verify", { - fence_operation_id: "../../escape", - }), - ); - assert.equal(invalid.ok, false); - assert.equal(invalid.error_code, "invalid_fence_operation_id"); - - const mismatch = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "terminal_verify", { - todo: { - todo_id: "todo_target", - status: "open", - claimed_by: "agent-b", - excluded_agents: [], - }, - }), - ); - assert.equal(mismatch.ok, false); - assert.equal(mismatch.error_code, "authority_todo_mismatch"); -}); - -test("prepared lifecycle receipts recover after a pre-write failure", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const request = await lifecycleRequest(root, "renew"); - const failed = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - beforeWrite: () => { - throw new Error("simulated response loss"); - }, - }); - assert.equal(failed.ok, false); - assert.equal((await lease(root)).version, 1); - - const retry = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:01.000Z"), - }); - assert.equal(retry.ok, true); - assert.equal(retry.renewed, true); - assert.equal((await lease(root)).version, 2); -}); - -test("release reports missing leases and validates the version first", async (t) => { - const root = await workspace(t); - const missingVersion = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "release", { - authority: null, - expected_version: null, - }), - ); - assert.equal(missingVersion.ok, false); - assert.equal(missingVersion.error_code, "version_required"); - - const missing = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "release", { - authority: null, - expected_version: 0, - }), - ); - assert.equal(missing.ok, true); - assert.equal(missing.released, false); - assert.equal(missing.missing, true); -}); - -test("release accepts an expired active lease and replays its tombstone", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire( - await acquireRequest(root, { ttl_seconds: 1 }), - { now: () => ACQUIRE_NOW }, - ); - const request = await lifecycleRequest(root, "release", { - authority: null, - expected_version: 1, - }); - const first = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:00:02.000Z"), - }); - const afterFirst = await lease(root); - const replay = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T04:00:00.000Z"), - }); - assert.equal(first.ok, true); - assert.equal(first.released, true); - assert.equal(replay.ok, true); - assert.equal(replay.idempotent, true); - assert.deepEqual(await lease(root), afterFirst); -}); - -test("release rejects owner, key, and version drift without mutation", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const ownerMismatch = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "release", { - authority: null, - owner: "agent-b", - expected_version: 1, - }), - ); - assert.equal(ownerMismatch.ok, false); - assert.equal(ownerMismatch.error_code, "lease_cas_mismatch"); - - const keyMismatch = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "release", { - authority: null, - idempotency_key: "lease-other", - expected_version: 1, - }), - ); - assert.equal(keyMismatch.ok, false); - assert.equal(keyMismatch.error_code, "lease_cas_mismatch"); - - const versionMismatch = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "release", { - authority: null, - expected_version: 0, - }), - ); - assert.equal(versionMismatch.ok, false); - assert.equal(versionMismatch.error_code, "version_mismatch"); - assert.equal((await lease(root)).status, "active"); -}); - -test("release retry after response loss commits one tombstone", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const request = await lifecycleRequest(root, "release", { authority: null }); - const failed = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - beforeWrite: () => { - throw new Error("simulated response loss"); - }, - }); - assert.equal(failed.ok, false); - assert.equal((await lease(root)).status, "active"); - - const committed = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:02:00.000Z"), +for (const [overrides, error] of [ + [{expected_version: "1"}, "invalid_request"], + [{expected_version: null}, "version_required"], + [{delegated_authority: "false"}, "invalid_request"], + [{operation: "terminal_verify", fence_operation_id: "../../escape"}, "invalid_fence_operation_id"], + [{operation: "fence_close", lock_token: " "}, "invalid_lock_token"], +] as const) test(`lifecycle input rejection: ${error} ${JSON.stringify(overrides)}`, async t => { + const f = await restore(t); const before = await readFile(f.path, "utf8"); + const r = await executeTaskLeaseLifecycle(lifecycle(f, "release", overrides)); + assert.equal(r.error_code, error); assert.equal(await readFile(f.path, "utf8"), before); +}); + +for (const [overrides, error] of [ + [{owner: "agent-b"}, "lease_cas_mismatch"], + [{idempotency_key: "other"}, "lease_cas_mismatch"], + [{expected_version: 0}, "version_mismatch"], +] as const) test(`original release preserves the lease on ${JSON.stringify(overrides)}`, async t => { + const f = await restore(t); const before = await readFile(f.path, "utf8"); + const r = await executeTaskLeaseLifecycle(lifecycle(f, "release", {authority: null, ...overrides}), {now}); + assert.equal(r.error_code, error); assert.equal(await readFile(f.path, "utf8"), before); +}); + +test("original release allows expiry, preserves generation and replays its tombstone", async t => { + const f = await restore(t); const original = await f.record(); + const request = lifecycle(f, "release", {authority: null}); + const released = await executeTaskLeaseLifecycle(request, {now: () => new Date("2026-09-02T00:00:00Z")}); + assert.equal(released.released, true); + const after = await f.record(); + assert.equal(after.version, original.version); assert.equal(after.lease_epoch, original.lease_epoch); + assert.equal(after.status, "released"); + const replay = await executeTaskLeaseLifecycle(request, {now}); + assert.equal(replay.idempotent, true); assert.deepEqual(await f.record(), after); +}); + +test("original release survives pre-write loss and missing leases remain a no-op", async t => { + const f = await restore(t); const request = lifecycle(f, "release", {authority: null}); + const failed = await executeTaskLeaseLifecycle(request, {now, beforeWrite: () => {throw new Error("write interrupted");}}); + assert.equal(failed.ok, false); assert.equal((await f.record()).status, "active"); + assert.equal((await executeTaskLeaseLifecycle(request, {now})).released, true); + assert.equal((await executeTaskLeaseLifecycle(request, {now})).idempotent, true); + const other = await restore(t); await rm(other.path); + const missing = await executeTaskLeaseLifecycle(lifecycle(other, "release", {expected_version: 0, authority: null}), {now}); + assert.equal(missing.ok, true); assert.equal(missing.missing, true); assert.equal(missing.released, false); +}); + +for (const name of ["renew", "transfer"]) { + for (const state of ["committed", "prepared"]) test(`1.x ${name} ${state} receipt recovers matching bytes once`, async t => { + const f = await restore(t, name); const before = await readFile(f.path, "utf8"); + if (state === "prepared") await atomicWriteJson(f.receiptPath!, {...await receipt(f), state}); + const r = await executeTaskLeaseLifecycle(f.request, {now, beforeWrite: () => assert.fail("recovery must not repeat a lease write")}); + assert.equal(r.ok, true); assert.equal(r.idempotent, true); + assert.equal((await receipt(f)).state, "committed"); + assert.equal(await readFile(f.path, "utf8"), before); + }); + for (const state of ["committed", "prepared"]) test(`1.x ${name} ${state} cannot overwrite a later lease`, async t => { + const f = await restore(t, name); + await atomicWriteJson(f.receiptPath!, {...await receipt(f), state}); + await atomicWriteJson(f.path, {...await f.record(), version: 9, lease_epoch: 9, idempotency_key: "later"}); + const before = await readFile(f.path, "utf8"); + const r = await executeTaskLeaseLifecycle(f.request, {now}); + assert.equal(r.error_code, state === "committed" ? "lifecycle_receipt_state_mismatch" : "canonical_authority_required"); + assert.equal(await readFile(f.path, "utf8"), before); + }); + test(`1.x ${name} rejects changed identity parameters and malformed receipts`, async t => { + const f = await restore(t, name); const before = await readFile(f.path, "utf8"); + assert.equal((await executeTaskLeaseLifecycle({...f.request, ttl_seconds: 300}, {now})).error_code, "idempotency_key_reuse"); + const original = await receipt(f); + await atomicWriteJson(f.receiptPath!, {...original, planned_lease: {...original.planned_lease as JsonObject, owner: 42}}); + assert.equal((await executeTaskLeaseLifecycle(f.request, {now})).error_code, "corrupt_lifecycle_receipt"); + assert.equal(await readFile(f.path, "utf8"), before); }); - assert.equal(committed.ok, true); - assert.equal(committed.released, true); - assert.equal((await lease(root)).status, "released"); +} - const replay = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:03:00.000Z"), +for (const name of ["terminal-held", "holder-held", "terminal-aborted", "auto-held", "auto-postwrite"]) { + test(`1.x ${name} resumes after restart and closes only the original generation`, async t => { + const f = await restore(t, name); const before = await f.record(); + const r = await executeTaskLeaseLifecycle(f.request, {now}); + assert.equal(r.ok, true, JSON.stringify(r)); + const fence = r.fence as JsonObject; + assert.equal(fence.fence_operation_id, (await receipt(f)).operation_id); + assert.deepEqual(await f.record(), before); + const close = closeRequest(f, fence); + const closed = await executeTaskLeaseLifecycle(close, {now}); + assert.equal(closed.released, true, JSON.stringify(closed)); + assert.equal((await f.record()).status, "released"); + assert.equal((await f.record()).version, before.version); + assert.equal((await executeTaskLeaseLifecycle(close, {now})).idempotent, true); }); - assert.equal(replay.ok, true); - assert.equal(replay.idempotent, true); - assert.equal((await lease(root)).status, "released"); -}); +} -test("malformed lifecycle receipts fail closed as receipt corruption", async (t) => { - const root = await workspace(t); - await executeTaskLeaseAcquire(await acquireRequest(root), { now: () => ACQUIRE_NOW }); - const request = await lifecycleRequest(root, "renew"); - await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:01:00.000Z"), - beforeWrite: () => { - throw new Error("leave prepared receipt"); - }, +test("1.x pre-write auto-acquire intent cannot mint a new lease after cutoff", async t => { + const f = await restore(t, "auto-prewrite"); + const r = await executeTaskLeaseLifecycle(f.request, {now, beforeWrite: () => assert.fail("fresh lease writer retired")}); + assert.equal(r.error_code, "canonical_authority_required", JSON.stringify(r)); + await assert.rejects(readFile(f.path), {code: "ENOENT"}); +}); + +test("closed original fence replays without a lock and refuses a later generation", async t => { + const f = await restore(t, "terminal-closed"); const before = await f.record(); + const r = await executeTaskLeaseLifecycle(f.request, {now}); + assert.equal(r.ok, true); assert.equal((r.fence as JsonObject).closed, true); + assert.equal((r.fence as JsonObject).lock_token, undefined); + assert.deepEqual(await f.record(), before); + await atomicWriteJson(f.path, {...before, status: "active", version: 9, lease_epoch: 9, idempotency_key: "later"}); + const later = await readFile(f.path, "utf8"); + assert.equal((await executeTaskLeaseLifecycle(f.request, {now})).error_code, "fence_state_invalid"); + assert.equal(await readFile(f.path, "utf8"), later); +}); + +test("closed legacy no-op replays only while no lease is present", async t => { + const f = await restore(t, "noop-closed"); + const r = await executeTaskLeaseLifecycle(f.request, {now}); + assert.equal(r.ok, true); assert.equal((r.fence as JsonObject).required, false); + const original = await restore(t); + await atomicWriteJson(f.path, await original.record()); + const before = await readFile(f.path, "utf8"); + assert.equal((await executeTaskLeaseLifecycle(f.request, {now})).error_code, "canonical_authority_required"); + assert.equal(await readFile(f.path, "utf8"), before); +}); + +for (const field of ["owner", "version", "lease_epoch", "idempotency_key"] as const) { + test(`held fence refuses changed ${field} and preserves later state`, async t => { + const f = await restore(t, "terminal-held"); + const original = await f.record(); + await atomicWriteJson(f.path, {...original, [field]: field === "owner" ? "agent-b" : field === "idempotency_key" ? "later" : 9}); + const before = await readFile(f.path, "utf8"); + const r = await executeTaskLeaseLifecycle(f.request, {now}); + assert.equal(r.ok, false); assert.equal(await readFile(f.path, "utf8"), before); }); - const receiptDirectory = join( - root, - "runtime", - "goals", - "goal-a", - "task-leases", - ".lifecycle-operations", - ); - const [receiptName] = await readdir(receiptDirectory); - const receiptPath = join(receiptDirectory, receiptName); - const receipt = JSON.parse(await readFile(receiptPath, "utf8")) as Record; - receipt.planned_lease = { - ...(receipt.planned_lease as Record), - owner: 42, - }; - await writeFile(receiptPath, JSON.stringify(receipt), "utf8"); +} - const rejected = await executeTaskLeaseLifecycle(request, { - now: () => new Date("2026-09-01T03:02:00.000Z"), +for (const name of ["terminal-held", "auto-postwrite"]) for (const change of ["claim", "excluded", "unregistered", "source"]) { + test(`${name} receipt does not bypass changed ${change} authority`, async t => { + const f = await restore(t, name); const before = await readFile(f.path, "utf8"); + const authority = f.request.authority as JsonObject; + const todo = (authority.todos as JsonObject[])[0]; + if (change === "claim") todo.claimed_by = "agent-b"; + if (change === "excluded") todo.excluded_agents = ["agent-a"]; + if (change === "unregistered") authority.registered_agent_candidates = [["agent-b"]]; + if (change === "source") await writeFile(join(f.root, "authority.json"), "later source"); + const r = await executeTaskLeaseLifecycle(f.request, {now}); + assert.equal(r.ok, false); assert.equal(r.error_code, change === "source" ? "authority_source_changed" + : name === "terminal-held" ? "authority_todo_mismatch" : "handoff_mode_lease_claim_divergence"); + assert.equal(await readFile(f.path, "utf8"), before); }); - assert.equal(rejected.ok, false); - assert.equal(rejected.error_code, "corrupt_lifecycle_receipt"); - assert.equal((await lease(root)).version, 1); -}); - -test("lifecycle boundary rejects non-boolean flags and unsafe fence tokens", async (t) => { - const root = await workspace(t); - const invalidFlag = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "renew", { - delegated_authority: "false", - }), - ); - assert.equal(invalidFlag.ok, false); - assert.equal(invalidFlag.error_code, "invalid_request"); - - const invalidToken = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "fence_close", { - authority: null, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: " ", - }), - ); - assert.equal(invalidToken.ok, false); - assert.equal(invalidToken.error_code, "invalid_lock_token"); -}); +} -test("fence close keeps its held fence when the authority source changed", async (t) => { - const root = await workspace(t); - const runtimeShadow = { - schema_version: "loopx_coordination_runtime_shadow_binding_v0", - provider: "file_v0", - }; - const hardAuthority = await authority(root, { - todos: [{ - todo_id: "todo_target", - status: "open", - claimed_by: "agent-a", - excluded_agents: [], - }], +for (const [field, value] of [["fence_operation_id", "a".repeat(64)], ["goal_id", "other-goal"], ["runtime_root", "$NEW_RUNTIME"]]) { + test(`receipt identity cannot move across ${field}`, async t => { + const f = await restore(t, "terminal-held"); const before = await readFile(f.path, "utf8"); + const r = await executeTaskLeaseLifecycle({...f.request, [field]: value === "$NEW_RUNTIME" ? join(f.root, "other-runtime") : value}, {now}); + assert.equal(r.ok, false); assert.equal(await readFile(f.path, "utf8"), before); }); - await executeTaskLeaseAcquire( - await acquireRequest(root, { authority: hardAuthority }), - { now: () => ACQUIRE_NOW }, - ); - const checked = await executeTaskLeaseLifecycle( - await lifecycleRequest(root, "holder_verify", { - authority: hardAuthority, - idempotency_key: null, - expected_version: null, - owner: "agent-a", - }), - { now: () => new Date("2026-09-01T03:01:00.000Z") }, - ); - assert.equal(checked.ok, true); - const fence = checked.fence as Record; +} - const closeRequest = await lifecycleRequest(root, "fence_close", { - authority: hardAuthority, - owner: null, - idempotency_key: null, - expected_version: null, - lock_token: fence.lock_token, - committed: true, - release_lease: true, - fence_owner: "agent-a", - fence_idempotency_key: null, - fence_expected_version: 1, - runtime_shadow: runtimeShadow, +for (const field of ["fence_expected_version", "fence_expected_lease_epoch"] as const) { + test(`original fence close rejects ${field} drift`, async t => { + const f = await restore(t, "terminal-held"); + const r = await executeTaskLeaseLifecycle(f.request, {now}); assert.equal(r.ok, true); + const before = await readFile(f.path, "utf8"); + const close = closeRequest(f, r.fence as JsonObject, {[field]: 0}); + assert.equal((await executeTaskLeaseLifecycle(close, {now})).error_code, "fence_cas_mismatch"); + assert.equal(await readFile(f.path, "utf8"), before); }); - // An equivalent rewrite of the canonical registry after the close snapshot. - // The decision facts are unchanged; only the source bytes moved. - const rewritten = await authoritySource(root, "authority-v2"); +} - const stale = await executeTaskLeaseLifecycle(closeRequest, { - now: () => new Date("2026-09-01T03:02:00.000Z"), - }); - assert.equal(stale.ok, false); +// The recovered lock is still a real transferable capability. A concurrent +// verifier must not steal it while the original closer owns its token claim. +test("recovered fence close excludes a concurrent verifier until its write commits", {timeout: 10_000}, async t => { + const f = await restore(t, "terminal-held"); + const verified = await executeTaskLeaseLifecycle(f.request, {now}); assert.equal(verified.ok, true); + let entered!: () => void; let release!: () => void; + const writing = new Promise(resolve => {entered = resolve;}); + const proceed = new Promise(resolve => {release = resolve;}); + const closing = executeTaskLeaseLifecycle(closeRequest(f, verified.fence as JsonObject), + {now, beforeWrite: async () => {entered(); await proceed;}}); + await writing; + try { + const retry = await executeTaskLeaseLifecycle(f.request, {now}); + assert.equal(retry.ok, false); assert.equal(retry.error_code, "fence_token_invalid"); + assert.equal((await f.record()).status, "active"); + } finally {release();} + assert.equal((await closing).released, true); +}); + +test("recovered fence close preserves its proof across source drift and pre-write failure", async t => { + const f = await restore(t, "terminal-held"); + const verified = await executeTaskLeaseLifecycle(f.request, {now}); assert.equal(verified.ok, true); + const close = closeRequest(f, verified.fence as JsonObject, {authority: f.request.authority, + runtime_shadow: {schema_version: "loopx_coordination_runtime_shadow_binding_v0", provider: "file_v0"}}); + await writeFile(join(f.root, "authority.json"), "changed source"); + const stale = await executeTaskLeaseLifecycle(close, {now}); assert.equal(stale.error_code, "authority_source_changed"); - assert.equal((await lease(root)).status, "active"); - - // The adapter answers a source mismatch by re-reading the graph and retrying - // the same held fence. A retryable precondition must not have consumed it. - const retried = await executeTaskLeaseLifecycle({ - ...closeRequest, - authority: { ...hardAuthority, source_receipts: [rewritten] }, - }, { now: () => new Date("2026-09-01T03:02:01.000Z") }); - assert.equal(retried.ok, true); - assert.equal(retried.released, true); - assert.equal((await lease(root)).status, "released"); + assert.equal((await f.record()).status, "active"); + await writeFile(join(f.root, "authority.json"), "authority-v1"); + const failed = await executeTaskLeaseLifecycle(close, {now, beforeWrite: () => {throw new Error("close interrupted");}}); + assert.equal(failed.ok, false); assert.equal((await f.record()).status, "active"); + const replay = await executeTaskLeaseLifecycle(f.request, {now}); + assert.equal(replay.ok, true); + assert.equal((await executeTaskLeaseLifecycle(closeRequest(f, replay.fence as JsonObject), {now})).released, true); }); - -for (const dimension of ["version", "epoch"] as const) { - test(`legacy lifecycle rejects exhausted ${dimension} before persistent mutation`, async t => { - const root = await workspace(t); - const acquired = await executeTaskLeaseAcquire(await acquireRequest(root)); assert.equal(acquired.ok, true); - const path = join(root, "runtime", "goals", "goal-a", "task-leases", "todo_target.json"); - const lease = JSON.parse(await readFile(path, "utf8")); - lease[dimension === "version" ? "version" : "lease_epoch"] = Number.MAX_SAFE_INTEGER; - await writeFile(path, JSON.stringify(lease)); const before = await readFile(path, "utf8"); - const result = await executeTaskLeaseLifecycle(await lifecycleRequest(root, dimension === "version" ? "renew" : "transfer", { - expected_version: lease.version, - ...(dimension === "epoch" ? {new_owner: "agent-b", new_idempotency_key: "lease-b"} : {}), - })); - assert.equal(result.error_code, "lease_generation_exhausted", JSON.stringify(result)); - assert.equal(await readFile(path, "utf8"), before); - }); -} diff --git a/tests/control_plane_ts/todo_update.test.ts b/tests/control_plane_ts/todo_update.test.ts index d66b71f712..89bade6108 100644 --- a/tests/control_plane_ts/todo_update.test.ts +++ b/tests/control_plane_ts/todo_update.test.ts @@ -861,7 +861,7 @@ test("retained execution fences distinguish recovery; soft released owner copy n assert.equal(recovery.lease_state, state); assert.equal(recovery.owner_relation, owner === "agent-a" ? "same_owner" : "foreign_owner"); assert.equal(recovery.action, state === "active" ? owner === "agent-a" ? "inspect_current_proof" : "reconcile_lease_owner" - : mode === "soft_claim" ? "resolve_acquire_rejection" : "acquire_fresh_lease"); + : mode !== "hard_lease" ? "resolve_acquire_rejection" : "acquire_fresh_lease"); assert.equal(JSON.stringify(result).includes("private-execution-key"), false); assert.deepEqual(await store.loadAuthority(), before); assert.equal((await store.readReceipt(request.operation_id)).status, "missing"); @@ -1000,7 +1000,7 @@ for (const blocker of ["scope", "acceptance"]) test(`recovery respects ${blocker const other = todo({todo_id: "todo_other", claimed_by: "agent-b"}); const todos = [...head.head.todos as Record[], other]; await store.commitAuthority({operation_id: "scope-conflict", expected_provider_revision: head.provider_revision, - events: [], receipts: [], next_projection: {...head.head, handoff_mode: "legacy", todos, + events: [], receipts: [], next_projection: {...head.head, handoff_mode: "hard_lease", todos, todo_read_model: {schema_version: TODO_DOMAIN_READ_RECORD_SCHEMA, todo_count: 2, records_sha256: canonicalAuthoritySha256(todos), contract_fields: [...TODO_DOMAIN_RECORD_CONTRACT.fields]}, leases: [ diff --git a/tests/fixtures/control_plane/legacy_writer_fence_caller_parity_v0.json b/tests/fixtures/control_plane/legacy_writer_fence_caller_parity_v0.json index ea3bb7bcec..cb04dc8799 100644 --- a/tests/fixtures/control_plane/legacy_writer_fence_caller_parity_v0.json +++ b/tests/fixtures/control_plane/legacy_writer_fence_caller_parity_v0.json @@ -942,57 +942,80 @@ } }, { - "id": "ts-acquire-absent", + "id": "ts-acquire-absent-cutoff", "surface": "ts_entry", "caller": "acquire", "fence_state": "absent", "expect": { - "ok": true, + "ok": false, "schema_version": "task_lease_v0", "action": "acquire", - "acquired": true, - "idempotent": false, - "lease": { - "schema_version": "task_lease_v0", + "error": "Normal writes require canonical authority; inspect and import the existing Goal with coordination-shadow prepare-import/apply-import, then review its ownership policy. Original receipts and lease release remain available; no source data was changed.", + "error_code": "canonical_authority_required", + "lease_path": "{runtime_root}/goals/goal-a/task-leases/todo_abc.json", + "write_check": { + "schema_version": "loopx_legacy_coordination_write_check_result_v0", + "status": "blocked", + "reason_code": "canonical_authority_required", "goal_id": "goal-a", - "todo_id": "todo_abc", - "owner": "agent-a", - "idempotency_key": "lease:parity-acquire", - "write_scopes": [], - "acquire_ttl_seconds": 600, - "version": 1, - "lease_epoch": 1, - "acquired_at": "2026-09-07T00:00:00Z", - "updated_at": "2026-09-07T00:00:00Z", - "expires_at": "2026-09-07T00:10:00Z", - "status": "active" + "reason": "Normal writes require canonical authority; inspect and import the existing Goal with coordination-shadow prepare-import/apply-import, then review its ownership policy. Original receipts and lease release remain available; no source data was changed." }, - "lease_path": "{runtime_root}/goals/goal-a/task-leases/todo_abc.json", "settlement": { - "effect_id": "goal-a:agent-a:todo_abc:lease:parity-acquire", - "receipts": [ - { - "step": "validation", - "status": "committed", - "effect_id": "goal-a:agent-a:todo_abc:lease:parity-acquire" - }, - { - "step": "durable_writeback", - "status": "committed", - "effect_id": "goal-a:agent-a:todo_abc:lease:parity-acquire" - } - ] + "effect_id": null, + "receipts": [], + "failure": { + "step": "validation", + "kind": "permission_denied", + "code": "canonical_authority_required" + } } }, "effect": { - "added": [ - "goals/goal-a/task-leases/todo_abc.json" - ], + "added": [], "removed": [], "changed": [] }, "retry": null, - "baseline": null + "baseline": { + "pre_2_0": { + "ok": true, + "schema_version": "task_lease_v0", + "action": "acquire", + "acquired": true, + "idempotent": false, + "lease": { + "schema_version": "task_lease_v0", + "goal_id": "goal-a", + "todo_id": "todo_abc", + "owner": "agent-a", + "idempotency_key": "lease:parity-acquire", + "write_scopes": [], + "acquire_ttl_seconds": 600, + "version": 1, + "lease_epoch": 1, + "acquired_at": "2026-09-07T00:00:00Z", + "updated_at": "2026-09-07T00:00:00Z", + "expires_at": "2026-09-07T00:10:00Z", + "status": "active" + }, + "lease_path": "{runtime_root}/goals/goal-a/task-leases/todo_abc.json", + "settlement": { + "effect_id": "goal-a:agent-a:todo_abc:lease:parity-acquire", + "receipts": [ + { + "step": "validation", + "status": "committed", + "effect_id": "goal-a:agent-a:todo_abc:lease:parity-acquire" + }, + { + "step": "durable_writeback", + "status": "committed", + "effect_id": "goal-a:agent-a:todo_abc:lease:parity-acquire" + } + ] + } + } + } }, { "id": "ts-release-absent", From c099ff2740c8e2a16384b1cb986bc24be9d2e5f3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:39:37 +0800 Subject: [PATCH 05/54] docs: align canonical creation catalog and cutoff checkpoint Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../rfcs/typescript-control-plane-migration-v0.md | 9 ++++++--- loopx/capabilities/configuration_ui.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 5a14f359ba..76cb484185 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -109,9 +109,12 @@ drain. This candidate changes v0/disabled creation admission and fresh legacy ownership grants intentionally; it does not change the compatible 1.x line. The normal-wheel import/consumer matrix, packaged Settings enable/refusal/readback, real PostgreSQL conformance and risk canary have focused passing evidence. The -native source-lease regression fixtures still need to preserve historical recovery -without recreating retired writers; the backend configuration catalog description -also needs alignment. Author integration and exact-head review remain open. Retained outbox recovery and source +native source-lease regression matrix now restores independently frozen 1.x +artifacts instead of recreating retired writers. It covers interrupted fence +recovery, lock-token renewal, stale generation rejection and later-write +protection; acquired intent alone cannot mint a new legacy execution grant. +The backend configuration catalog now describes the same 2.0 creation boundary. +Exact-head review, core-developer trial and release authorization remain open. Retained outbox recovery and source lease release are concrete remaining callers, not grounds for keeping normal Todo producers. The existing delivery Todos should carry these concrete exits: diff --git a/loopx/capabilities/configuration_ui.py b/loopx/capabilities/configuration_ui.py index 5b683ec971..2f40b425cb 100644 --- a/loopx/capabilities/configuration_ui.py +++ b/loopx/capabilities/configuration_ui.py @@ -100,7 +100,7 @@ def capability_configuration_editor( options=["file", "sqlite"], required=True, description="Fixed at creation. Existing Goals need a separate backed-up migration."), _field("canonical_creation", "Create canonical authority", "boolean", required=True, - description="Enabled by default for new Goals. Disabled retains the post-promotion target behavior."), + description="Required for new Goals in 2.0. Enable before creating a Goal; import existing Markdown Goals separately."), _field("new_goal_handoff_mode", "New Goal execution policy", "select", options=["soft_claim", "hard_lease"], required=True, description="Used only with canonical creation. Agents inherit the Goal policy; this grants no tool permissions.")], From 1e73b32c3fbdd7f92f53b52d1c3f53f34964f09c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:41:43 +0800 Subject: [PATCH 06/54] docs: close completed cutoff integration checkpoint Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/local-authority-provider-selection.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/local-authority-provider-selection.md b/docs/reference/local-authority-provider-selection.md index 719cd26231..91a3f0d348 100644 --- a/docs/reference/local-authority-provider-selection.md +++ b/docs/reference/local-authority-provider-selection.md @@ -296,8 +296,8 @@ capture adapter and shared outbox recovery remain live. File/SQLite source CLI, canonical Chat, original team-plan replay and interrupted outbox readback have focused tests. The normal-wheel import/consumer matrix, packaged Settings journey, real PostgreSQL conformance and risk canary pass their focused checks. Historical -native source-lease fixtures and the backend configuration description still need -integration; exact-head review remains open. These checks +native source-lease recovery now uses frozen 1.x artifacts, and the backend +configuration catalog describes the same cutoff; exact-head review remains open. These checks do not certify natural multi-developer adoption, sustained D2 or a public release. 2.0 alpha 候选把正常写入统一收口到 canonical authority,明确与兼容的 1.x 线区分。 From da6d5d1a1d36dd1142152b2cd303b18df885b064 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Tue, 13 Oct 2026 20:05:47 +0800 Subject: [PATCH 07/54] fix: preserve canonical external-wait diagnostics Signed-off-by: huangruiteng --- .../control_plane/coordination/todo_update.ts | 14 +++++++++++-- loopx/control_plane/todos/provider_update.py | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/loopx/control_plane/coordination/todo_update.ts b/loopx/control_plane/coordination/todo_update.ts index 1ff28f40a7..cf81d4888a 100644 --- a/loopx/control_plane/coordination/todo_update.ts +++ b/loopx/control_plane/coordination/todo_update.ts @@ -44,6 +44,7 @@ export type {CoordinationTodoUpdateInput} from "./todo_update_intent.ts"; import {normalizeTodoUpdateInput, prepareUpdatedTodo, type CoordinationTodoUpdateInput} from "./todo_update_intent.ts"; import {executeCoordinationTodoTerminalLifecycle} from "./todo_terminal_lifecycle.ts"; import {planCompletionValidationRevision} from "../todos/completion_validation_revision.ts"; +import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; export type CoordinationTodoUpdateResult = JsonObject & { readonly schema_version: typeof COORDINATION_TODO_UPDATE_RESULT_SCHEMA; @@ -193,8 +194,17 @@ export async function executeCoordinationTodoUpdate( ...(rejected.recovery === undefined ? {} : {recovery: rejected.recovery})}; let prepared: ReturnType; try { prepared = prepareUpdatedTodo(target.todo, input, head.head); } - catch (error) { return failure("invalid_coordination_todo_update", - error instanceof Error ? error.message : "invalid updated Todo"); } + catch (error) { + const code = error instanceof EffectRuntimeRequestError ? error.code : "invalid_coordination_todo_update"; + const rejected = failure(code, error instanceof Error ? error.message : "invalid updated Todo"); + if (!code.startsWith("external_wait_")) return rejected; + // The public repair hint needs the effective wait after a partial edit. + // These are source facts, not a second validation or a new grant. + return {...rejected, external_wait_authoring_context: { + resume_when: input.planning_intent?.resume_when ?? target.todo.resume_when ?? null, + successor_todo_ids: input.planning_intent?.successor_todo_ids ?? target.todo.successor_todo_ids ?? [], + }}; + } let {next, changed, clearFields} = prepared; let completionValidationRevisionReceipt: JsonObject | null = null; if (input.completion_validation_revision !== undefined) { diff --git a/loopx/control_plane/todos/provider_update.py b/loopx/control_plane/todos/provider_update.py index 4f5e1aa914..03473fc61b 100644 --- a/loopx/control_plane/todos/provider_update.py +++ b/loopx/control_plane/todos/provider_update.py @@ -43,6 +43,7 @@ ) from .path_resolution import resolve_todo_state_path from .monitor_metadata import MonitorPollObservation +from .external_wait_contract import TodoExternalWaitAuthoringError def _dispatch_canonical_todo_update( @@ -279,6 +280,25 @@ def update_canonical_todo_if_promoted( raise ValueError("Todo is missing from canonical authority") if result.get("reason_code") == "todo_role_mismatch": raise ValueError("Todo does not have the requested role") + # The TypeScript planner owns the rule and its typed diagnostic. Keep + # the public CLI repair projection without revalidating the transition. + code = result.get("reason_code") + if isinstance(code, str) and code.startswith("external_wait_"): + context = result.get("external_wait_authoring_context") + if not isinstance(context, dict): + context = planning_intent or {} + resume_when = context.get("resume_when") + monitor_id = ( + resume_when.split(":", 1)[1] + if isinstance(resume_when, str) and resume_when.startswith("monitor_changed:") + else None + ) + raise TodoExternalWaitAuthoringError( + str(result.get("reason") or "invalid external-wait transition"), + code=code, + monitor_todo_id=monitor_id, + successor_todo_ids=context.get("successor_todo_ids"), + ) if isinstance(result, dict) and ( result.get("status") == "missing" and result.get("source_authority") in LOCAL_AUTHORITY_SOURCES From 691b2910954f33d7e7ed729777eebff0502473f0 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Tue, 13 Oct 2026 20:05:55 +0800 Subject: [PATCH 08/54] test: exercise supported waits on canonical providers Signed-off-by: huangruiteng --- .../test_public_todo_update_plan.py | 60 +++++++++++------ .../control_plane/test_todo_external_wait.py | 66 +++++++++++++------ tests/control_plane_ts/todo_update.test.ts | 18 +++++ 3 files changed, 105 insertions(+), 39 deletions(-) diff --git a/tests/control_plane/test_public_todo_update_plan.py b/tests/control_plane/test_public_todo_update_plan.py index 789be33c56..20c5e5a34c 100644 --- a/tests/control_plane/test_public_todo_update_plan.py +++ b/tests/control_plane/test_public_todo_update_plan.py @@ -1,15 +1,20 @@ """Public update invariants: a partial edit must not invalidate an armed wait.""" import pytest -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 +from loopx.todos import add_goal_todo, list_goal_todos, 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, ) -def waiting_goal(tmp_path): +def waiting_goal(tmp_path, *, provider="file"): registry, runtime, state = _write_fixture(tmp_path) + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=[], handoff_mode="soft_claim"), + state_path=state, provider=provider) # 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,34 +32,49 @@ def add(text): return registry, runtime, state, waiting, successor +def _canonical_todo(registry, todo_id): + return list_goal_todos(registry_path=registry, goal_id=GOAL_ID, + todo_id=todo_id)["todo"] + + @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, match="external-wait") as rejected: update_goal_todo(registry_path=registry, goal_id=GOAL_ID, todo_id=waiting["todo_id"], agent_id=AGENT_ID, **edit) + assert rejected.value.authoring_contract["waiting_todo"]["resume_when"].startswith("monitor_changed:") assert state.read_bytes() == before -def test_clear_condition_and_successors_together_is_valid(tmp_path): - registry, _, state, waiting, _ = waiting_goal(tmp_path) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_clear_condition_and_successors_together_is_valid(tmp_path, monkeypatch, provider): + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, _, state, waiting, _ = waiting_goal(tmp_path, provider=provider) result = 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"] == [] + assert result["status"] == "applied" + updated = _canonical_todo(registry, waiting["todo_id"]) + assert updated.get("resume_when") is None + assert updated.get("resume_monitor_generation") is None + assert updated.get("resume_condition") is None + assert updated.get("resume_ready") is None + assert updated.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, todo_id=waiting["todo_id"], agent_id=AGENT_ID, note="Clarify context") - assert result["resume_when"].startswith("monitor_changed:") - assert result["resume_monitor_generation"] == 3 - assert "external_wait_transition" not in result + assert result["status"] == "applied" + updated = _canonical_todo(registry, waiting["todo_id"]) + assert updated["resume_when"].startswith("monitor_changed:") + assert updated["resume_monitor_generation"] == 3 + assert updated["note"] == "Clarify context" def test_real_cli_rejects_partial_wait_edit_and_supports_explicit_repair(tmp_path): @@ -64,6 +84,8 @@ def test_real_cli_rejects_partial_wait_edit_and_supports_explicit_repair(tmp_pat before = state.read_bytes() code, rejected = run_json_cli_result(*common, registry_path=registry, runtime_root=runtime) assert code != 0, rejected + assert rejected["error_code"] == "external_wait_todo_status_must_remain_open" + assert rejected["authoring_contract"]["waiting_todo"]["resume_when"].startswith("monitor_changed:") assert state.read_bytes() == before code, accepted = run_json_cli_result(*common, "--clear-resume-when", "--dry-run", registry_path=registry, runtime_root=runtime) @@ -72,17 +94,17 @@ def test_real_cli_rejects_partial_wait_edit_and_supports_explicit_repair(tmp_pat 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 + from loopx.control_plane.todos import provider_update 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): - monkeypatch.setattr(module, "effect_runtime_result", traced) + return actual(method, params, **kwargs) + monkeypatch.setattr(provider_update, "effect_runtime_result", traced) result = 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"]]) - assert result["resume_monitor_generation"] == 3 - assert calls == ["todo.public_update.plan"] + assert result["status"] in {"applied", "no_change"} + assert _canonical_todo(registry, waiting["todo_id"])["resume_monitor_generation"] == 3 + assert calls == ["coordination.local_authority.todo_update"] diff --git a/tests/control_plane/test_todo_external_wait.py b/tests/control_plane/test_todo_external_wait.py index 65118e7c23..a439e7d0a4 100644 --- a/tests/control_plane/test_todo_external_wait.py +++ b/tests/control_plane/test_todo_external_wait.py @@ -6,6 +6,8 @@ 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 loopx.control_plane.scheduler.monitor_poll_writeback import ( write_monitor_poll_todo_state, ) @@ -17,7 +19,8 @@ from loopx.control_plane.todos.external_wait_contract import ( TodoExternalWaitAuthoringError, ) -from loopx.todos import complete_goal_todo, update_goal_todo +from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable +from loopx.todos import complete_goal_todo, list_goal_todos, update_goal_todo GOAL_ID = "external-wait-fixture" @@ -28,7 +31,7 @@ FALLBACK_ID = "todo_fallback001" -def _write_fixture(root: Path, *, dependency: bool = False) -> tuple[Path, Path]: +def _write_fixture(root: Path, *, dependency: bool = False, provider: str = "file") -> tuple[Path, Path]: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -72,6 +75,12 @@ def _write_fixture(root: Path, *, dependency: bool = False) -> tuple[Path, Path] registered_agents=[AGENT_ID], quota_allowed_slots=None, ) + # Treat the frozen Markdown source as imported history. Active writes now + # execute through the canonical provider, not the retired source writer. + imported = list_goal_todos(registry_path=registry, goal_id=GOAL_ID, role="agent")["todos"] + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=imported, + handoff_mode="soft_claim"), state_path=state_file, provider=provider) return registry, state_file @@ -82,10 +91,18 @@ def _todos(state_file: Path) -> dict[str, dict]: return {item["todo_id"]: item for item in summary["items"]} +def _canonical_todo(registry: Path, todo_id: str) -> dict: + return list_goal_todos(registry_path=registry, goal_id=GOAL_ID, + todo_id=todo_id)["todo"] + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_monitor_change_wait_binds_generation_and_resumes_only_after_increment( - tmp_path: Path, + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, ) -> None: - registry, state_file = _write_fixture(tmp_path) + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, state_file = _write_fixture(tmp_path, provider=provider) transition = update_goal_todo( registry_path=registry, @@ -96,10 +113,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" + assert transition["status"] == "applied" + canonical_waiting = _canonical_todo(registry, WAITING_ID) + assert canonical_waiting["resume_monitor_generation"] == 2 + assert canonical_waiting["successor_todo_ids"] == [FALLBACK_ID] + assert canonical_waiting["status"] == "open" + assert canonical_waiting["resume_when"] == f"monitor_changed:{MONITOR_ID}" waiting = _todos(state_file)[WAITING_ID] assert waiting["resume_monitor_generation"] == 2 assert waiting["resume_ready"] is False @@ -270,13 +289,15 @@ def test_monitor_provider_effect_replay_does_not_advance_counters( assert monitor["monitor_effect_id"] == "quota-monitor-poll:provider-reentry" assert int(monitor["consecutive_no_change"]) == first["consecutive_no_change"] - with pytest.raises(ValueError, match="monitor effect identity is already bound"): + with pytest.raises(LocalCoordinationAuthorityUnavailable, + match="operation id already names a different coordination request") as conflict: write_monitor_poll_todo_state( **{ **kwargs, "result_hash": "review-v3-conflict", } ) + assert conflict.value.code == "coordination_operation_identity_mismatch" newer = write_monitor_poll_todo_state( **{ @@ -291,8 +312,12 @@ 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) + # Projection delivery describes the latest provider revision. A replay of + # an earlier business receipt must retain its facts even after a newer + # operation has advanced that separate delivery state. + for key in ("coordination_runtime_shadow", "projection_delivery", "projection_outbox"): + expected.pop(key, None) + historical.pop(key, None) expected["todo_update"] = {key: value for key, value in first["todo_update"].items() if key != "coordination_runtime_shadow"} assert historical == expected @@ -343,22 +368,21 @@ 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 + import loopx.control_plane.scheduler.provider_monitor_poll as batch_module registry, state_file = _write_fixture(tmp_path) - original_apply = batch_module.apply_legacy_monitor_poll + original_apply = batch_module.effect_runtime_result 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": + def delay_second_poll(method, request, **kwargs): + if method == "coordination.local_authority.monitor_poll" and request["observation"]["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) + return original_apply(method, request, **kwargs) - monkeypatch.setattr(batch_module, "apply_legacy_monitor_poll", delay_second_poll) + monkeypatch.setattr(batch_module, "effect_runtime_result", delay_second_poll) with ThreadPoolExecutor(max_workers=2) as executor: second_poll = executor.submit( write_monitor_poll_todo_state, @@ -395,7 +419,8 @@ def delay_second_poll(**kwargs): successor_todo_ids=[FALLBACK_ID], agent_id=AGENT_ID, ) - assert wait_transition["external_wait_transition"]["baseline_generation"] == 3 + assert wait_transition["status"] == "applied" + assert _canonical_todo(registry, WAITING_ID)["resume_monitor_generation"] == 3 release_second_poll.set() second_result = second_poll.result(timeout=5) @@ -418,7 +443,8 @@ 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 transition["status"] == "applied" + assert _canonical_todo(registry, WAITING_ID)["resume_when"] == f"todo_done:{DEPENDENCY_ID}" assert _todos(state_file)[WAITING_ID]["resume_ready"] is False complete_goal_todo( diff --git a/tests/control_plane_ts/todo_update.test.ts b/tests/control_plane_ts/todo_update.test.ts index 89bade6108..90e7a0598c 100644 --- a/tests/control_plane_ts/todo_update.test.ts +++ b/tests/control_plane_ts/todo_update.test.ts @@ -751,6 +751,24 @@ test("provider-first update rejects authority and lifecycle escalation", async ( } }); +test("canonical update preserves the planner's typed external-wait repair code", async () => { + const {store, request} = await seeded({todo_id: "todo_waiting001", task_class: "advancement_task"}); + const before = await store.loadAuthority(); + const result = await executeCoordinationTodoUpdate(store, {...request, todo_id: "todo_waiting001", + patch: {}, clear_fields: [], planning_intent: { + status: "blocked", + resume_when: "monitor_changed:todo_monitor001", + successor_todo_ids: ["todo_next001"], + }}); + assert.equal(result.status, "failed"); + assert.equal(result.reason_code, "external_wait_todo_status_must_remain_open", JSON.stringify(result)); + assert.deepEqual(result.external_wait_authoring_context, { + resume_when: "monitor_changed:todo_monitor001", successor_todo_ids: ["todo_next001"], + }); + assert.deepEqual(await store.loadAuthority(), before); + assert.equal((await store.readReceipt(request.operation_id)).status, "missing"); +}); + for (const claimed_by of ["agent-a", null]) { test(`provider-first update fails closed without a hard-lease execution proof (${claimed_by ?? "unclaimed"})`, async () => { const {store, request} = await seeded({claimed_by}); From 82a42a77a865a2cb69ab42f3f0258555a65a36be Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Tue, 13 Oct 2026 20:06:02 +0800 Subject: [PATCH 09/54] chore: refresh registry I/O inventory after rebase Signed-off-by: huangruiteng --- .../semantics/project_registry_io_manifest_v1.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index 3055d63e0e..9ab1aef4b5 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": 544, + "line": 527, "column": 14, "kind": "codec_transaction", "api": "project_registry_transaction", @@ -1127,7 +1127,7 @@ }, { "site": "loopx/control_plane/coordination/legacy_writer_fence.py::.legacy_todo_write_transaction::codec_read:load_registry#1", - "line": 378, + "line": 388, "column": 9, "kind": "codec_read", "api": "load_registry", @@ -1135,7 +1135,7 @@ }, { "site": "loopx/control_plane/coordination/legacy_writer_fence.py::.require_prose_state_write_allowed::codec_read:load_registry#1", - "line": 219, + "line": 229, "column": 35, "kind": "codec_read", "api": "load_registry", @@ -1143,7 +1143,7 @@ }, { "site": "loopx/control_plane/coordination/legacy_writer_fence.py::.require_registry_source_write_allowed::codec_read:load_registry#1", - "line": 161, + "line": 171, "column": 16, "kind": "codec_read", "api": "load_registry", @@ -1887,7 +1887,7 @@ }, { "site": "loopx/control_plane/work_items/team_plan_adapter.py::.apply_team_plan::codec_read:load_registry#1", - "line": 147, + "line": 134, "column": 27, "kind": "codec_read", "api": "load_registry", @@ -1895,7 +1895,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": 149, "column": 35, "kind": "codec_read", "api": "load_registry", @@ -1903,7 +1903,7 @@ }, { "site": "loopx/control_plane/work_items/team_plan_adapter.py::.team_plan_state_fingerprint::codec_read:load_registry#1", - "line": 86, + "line": 73, "column": 31, "kind": "codec_read", "api": "load_registry", From f5f5ffb17ea9259907988afb3225b88e9831a64b Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Tue, 13 Oct 2026 20:12:36 +0800 Subject: [PATCH 10/54] test: keep Dev Book on published release baseline Signed-off-by: huangruiteng --- examples/dev-book-publication-smoke.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/dev-book-publication-smoke.py b/examples/dev-book-publication-smoke.py index c380293e6c..ac6c5b29b1 100644 --- a/examples/dev-book-publication-smoke.py +++ b/examples/dev-book-publication-smoke.py @@ -7,7 +7,6 @@ from html.parser import HTMLParser from pathlib import Path import re -import tomllib from urllib.parse import unquote @@ -442,8 +441,9 @@ def main() -> int: assert "/loopx/docs/book/en/" in read(BOOK / "index.md") assert "/loopx/docs/book/" in read(BOOK / "en" / "index.md") - project_version = tomllib.loads(read(REPO_ROOT / "pyproject.toml"))["project"]["version"] - release_tag = f"v{project_version}" + # This independently reviewed book baseline advances with publication, + # not when pyproject moves ahead to an unreleased candidate version. + release_tag = "v1.4.0" # Historical migration milestones must not move with the package version. migration_baseline_tag = "v0.5.4" release_markers = { From b9651fb1e49d73b0dcf2d5d0ca831957314d25c0 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Tue, 13 Oct 2026 20:20:13 +0800 Subject: [PATCH 11/54] fix: select stable predecessor for prerelease bundle build Signed-off-by: huangruiteng --- scripts/chat_bundle.py | 10 ++++++++-- tests/presentation/test_chat_bundle.py | 13 +++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/chat_bundle.py b/scripts/chat_bundle.py index 1ca7895f53..9f1e3032c6 100644 --- a/scripts/chat_bundle.py +++ b/scripts/chat_bundle.py @@ -238,8 +238,14 @@ def release_build(tag: str, repository: str) -> None: and re.fullmatch(r"v\d+\.\d+\.\d+", r["tag_name"]) ] - def number(value: str) -> tuple[int, ...]: - return tuple(map(int, value.removeprefix("v").split("."))) + def number(value: str) -> tuple[int, int, int]: + # Only stable releases can supply retained assets. A prerelease target + # still follows the latest stable release with a *lower* core version; + # a future stable release with the same core is not its predecessor. + match = re.fullmatch(r"v(\d+)\.(\d+)\.(\d+)(?:(?:a|b|rc)\d+)?", value) + if match is None: + raise ValueError(f"unsupported LoopX release tag: {value}") + return tuple(map(int, match.groups())) 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 ae15eac37e..22a1289da0 100644 --- a/tests/presentation/test_chat_bundle.py +++ b/tests/presentation/test_chat_bundle.py @@ -240,8 +240,12 @@ def test_same_asset_name_different_bytes_cannot_overwrite_current(source, tmp_pa @pytest.mark.parametrize("checksum_valid", [True, False]) +@pytest.mark.parametrize( + ("target", "predecessor"), + [("v1.1.0", "v1.0.5"), ("v2.0.0a1", "v1.2.0")], +) def test_release_predecessor_is_stable_older_and_checksum_verified( - tmp_path, monkeypatch, checksum_valid + tmp_path, monkeypatch, checksum_valid, target, predecessor ): def release(tag, *, draft=False, prerelease=False): return { @@ -257,6 +261,7 @@ def release(tag, *, draft=False, prerelease=False): release("v1.0.5"), release("v1.1.0"), release("v1.2.0"), + release("v2.0.0"), release("v1.0.9", draft=True), release("v1.0.8", prerelease=True), release("desktop-main-123"), @@ -290,13 +295,13 @@ def download(command, **kwargs): ), ) if checksum_valid: - builder.release_build("v1.1.0", "fixture/repository") + builder.release_build(target, "fixture/repository") assert builds == ["old code"] else: with pytest.raises(RuntimeError, match="checksum mismatch"): - builder.release_build("v1.1.0", "fixture/repository") + builder.release_build(target, "fixture/repository") assert builds == [] - assert downloads == ["v1.0.5"] + assert downloads == [predecessor] def test_windows_drive_paths_are_not_portable_bundle_names(): From 0155a541e71c9b7d75c05a1de4c885ed0781face Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Tue, 13 Oct 2026 20:36:41 +0800 Subject: [PATCH 12/54] test: run managed host and turn fixtures on canonical authority Signed-off-by: huangruiteng --- tests/test_ark_managed_agent_host.py | 13 +++++ tests/test_loopx_turn_driver.py | 79 +++++++++++++++------------- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/tests/test_ark_managed_agent_host.py b/tests/test_ark_managed_agent_host.py index f615c8da74..b1da12cff8 100644 --- a/tests/test_ark_managed_agent_host.py +++ b/tests/test_ark_managed_agent_host.py @@ -14,6 +14,11 @@ run_json_cli, write_fixture_registry, ) +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority +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 ( @@ -455,6 +460,14 @@ def test_fresh_host_reconstructs_frontier_from_durable_loopx_state( tmp_path: Path, ) -> None: project, runtime, registry = _write_continuity_fixture(tmp_path) + state = project / ".codex" / "goals" / CONTINUITY_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=CONTINUITY_GOAL_ID, + handoff_mode="soft_claim", + todos=todos["agent_todos"]["items"], + ) + initialize_canonical_authority(runtime, CONTINUITY_GOAL_ID, projection, state_path=state) first_host = _fresh_host_quota_read( project=project, diff --git a/tests/test_loopx_turn_driver.py b/tests/test_loopx_turn_driver.py index 7c93bdff98..bf5b3bed4b 100644 --- a/tests/test_loopx_turn_driver.py +++ b/tests/test_loopx_turn_driver.py @@ -39,7 +39,7 @@ ) 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 +from loopx.todos import list_goal_todos def _envelope( @@ -951,6 +951,7 @@ def test_default_agent_session_reuses_planning_across_completed_todos(tmp_path, " ", )) + _promote_turn_fixture(project, runtime) binary, log = _fake_codex(tmp_path) monkeypatch.setenv("FAKE_CODEX_LOG", str(log)) home = tmp_path / "codex-home" @@ -1408,26 +1409,11 @@ def _promote_turn_fixture(project: Path, runtime: Path) -> None: / "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, @@ -1437,6 +1423,31 @@ def _promote_turn_fixture(project: Path, runtime: Path) -> None: ) +def _replace_canonical_completion_turn_key(runtime: Path, key: str | None) -> None: + """Perturb a disposable provider revision to exercise terminal recovery fencing.""" + store_module = (Path(__file__).resolve().parents[1] / "loopx/control_plane/coordination/local_authority_provider.ts").as_uri() + codec_module = (Path(__file__).resolve().parents[1] / "loopx/control_plane/coordination/authority_store_codec.ts").as_uri() + script = ( + f"import {{openLocalAuthorityStore}} from {json.dumps(store_module)};" + f"import {{canonicalAuthoritySha256}} from {json.dumps(codec_module)};" + "const store=await openLocalAuthorityStore(process.argv[1],'loopx-turn-fixture');" + "const head=await store.loadAuthority();" + "const todo=head.head.todos.find(item=>item.todo_id==='todo_fixture0001');" + "if(!todo)throw Error('fixture Todo missing');" + "const key=JSON.parse(process.argv[2]);" + "if(key===null)delete todo.completion_turn_key;else todo.completion_turn_key=key;" + "head.head.todo_read_model.records_sha256=canonicalAuthoritySha256(head.head.todos);" + "const result=await store.commitAuthority({expected_provider_revision:head.provider_revision," + "operation_id:'fixture-completion-key-change',events:[],receipts:[],next_projection:head.head});" + "if(result.status!=='applied')throw Error(JSON.stringify(result));" + ) + subprocess.run( + ["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", + "-e", script, str(runtime), json.dumps(key)], + check=True, capture_output=True, text=True, timeout=30, + ) + + def test_quota_cli_projects_outer_controller_without_codex_app_action( tmp_path: Path, ) -> None: @@ -2382,6 +2393,7 @@ def test_turn_run_once_cli_completes_selected_todo_after_validation( next_action: str, ) -> 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 = """ @@ -2530,16 +2542,11 @@ 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 + stored_completion = list_goal_todos( + registry_path=registry, goal_id="loopx-turn-fixture", todo_id="todo_fixture0001", + )["todo"] + assert stored_completion["status"] == "done" + assert stored_completion["completion_turn_key"] == payload["resume_turn_key"] replayed_output = io.StringIO() with contextlib.redirect_stdout(replayed_output): @@ -2727,6 +2734,7 @@ def test_turn_run_once_cli_repairs_committed_quota_spend_after_receipt_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() @@ -2834,6 +2842,7 @@ def test_turn_run_once_cli_projects_declared_successor_continuation( "task_class=advancement_task priority=P2 -->", ), ) + _promote_turn_fixture(project, runtime) host_project = project / "isolated-host-workspace" host_project.mkdir() host_script, validation_script = _completion_host_and_validation_scripts() @@ -2866,6 +2875,7 @@ def test_turn_run_once_cli_projects_durable_no_followup_continuation( tmp_path, todo_metadata_extra="no_followup=true", ) + _promote_turn_fixture(project, runtime) host_project = project / "isolated-host-workspace" host_project.mkdir() host_script, validation_script = _completion_host_and_validation_scripts() @@ -2903,6 +2913,7 @@ def test_turn_run_once_cli_terminal_recovery_rejects_unowned_completion( tmp_path, todo_metadata_extra="no_followup=true", ) + _promote_turn_fixture(project, runtime) host_project = project / "isolated-host-workspace" host_project.mkdir() host_script, validation_script = _completion_host_and_validation_scripts() @@ -2930,15 +2941,7 @@ def test_turn_run_once_cli_terminal_recovery_rejects_unowned_completion( state = state_path.read_text(encoding="utf-8") current_turn_field = f" completion_turn_key={turn_key}" assert current_turn_field in state - replacement = ( - "" - if durable_completion_turn_key is None - else f" completion_turn_key={durable_completion_turn_key}" - ) - state_path.write_text( - state.replace(current_turn_field, replacement), - encoding="utf-8", - ) + _replace_canonical_completion_turn_key(runtime, durable_completion_turn_key) event_path = ( runtime / "goals" / "loopx-turn-fixture" / "rollout-event-log.jsonl" @@ -3068,6 +3071,7 @@ def test_turn_run_once_cli_replays_declared_successor_after_interruption( "task_class=advancement_task priority=P2 -->", ), ) + _promote_turn_fixture(project, runtime) host_project = project / "isolated-host-workspace" host_project.mkdir() host_script, validation_script = _completion_host_and_validation_scripts() @@ -3406,6 +3410,7 @@ def test_turn_run_once_cli_uses_built_in_codex_host_and_typed_writeback( ), encoding="utf-8", ) + _promote_turn_fixture(project, runtime) subprocess.run( ["git", "init", "--initial-branch", "main", str(project)], check=True, From e9652bd7db7854125b5bdaddf2fdb5d8a295036b Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Tue, 13 Oct 2026 21:01:15 +0800 Subject: [PATCH 13/54] test: seed packaged user action journey on canonical authority Signed-off-by: huangruiteng --- .../personal-workspace-browser/attention-user-action.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/personal-workspace-browser/attention-user-action.mjs b/examples/personal-workspace-browser/attention-user-action.mjs index 73cce19142..84182f2eed 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']) 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 From 81347545f8ddfef4d1f3c775e47596ef92ffebde Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:23:42 +0800 Subject: [PATCH 14/54] test: recover immutable 1.x histories with the canonical cutoff receiver Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .github/workflows/python-tests.yml | 3 + .../typescript-control-plane-migration-v0.md | 8 + .../historical_shadow_fixture.py | 71 +++++++++ .../test_cold_source_disposition_e2e.py | 36 ++--- .../test_cold_source_inspection.py | 45 +++--- .../test_monitor_poll_transaction_recovery.py | 145 +++++++++++------- .../test_shadow_cursor_recovery_e2e.py | 30 ++-- .../test_shadow_drain_adversarial.py | 66 ++++---- tests/control_plane/test_shadow_drain_e2e.py | 62 ++++---- .../test_task_lease_argument_recovery.py | 25 +-- 10 files changed, 306 insertions(+), 185 deletions(-) create mode 100644 tests/control_plane/historical_shadow_fixture.py diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index f58736d1f9..e37ab661ac 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -677,6 +677,9 @@ jobs: - {suite: installed, shard: 0} steps: - uses: actions/checkout@v7 + with: + # Cross-version recovery generates originals with a pinned public 1.x source. + fetch-depth: 0 - uses: actions/download-artifact@v7 with: name: chat-bundle-${{ github.sha }} diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 76cb484185..04dc04f984 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -113,6 +113,14 @@ native source-lease regression matrix now restores independently frozen 1.x artifacts instead of recreating retired writers. It covers interrupted fence recovery, lock-token renewal, stale generation rejection and later-write protection; acquired intent alone cannot mint a new legacy execution grant. +Retained Monitor and outbox recovery tests now generate originals with a pinned +public 1.x Git source and execute inspection, drain and receipt settlement with +the current receiver. A source intent stopped before its business commit is +retained for import/reconciliation; it cannot authorize a new 2.0 source write. +Current consumer fixtures use canonical authority, while registered-peer legacy +assignment and original result shapes are checked only as historical receipts. +This repairs the executable recovery boundary; it does not qualify every old +shadow-writer ladder or replace the independent installed/natural-trial gates. The backend configuration catalog now describes the same 2.0 creation boundary. Exact-head review, core-developer trial and release authorization remain open. Retained outbox recovery and source lease release are concrete remaining callers, not grounds for keeping normal diff --git a/tests/control_plane/historical_shadow_fixture.py b/tests/control_plane/historical_shadow_fixture.py new file mode 100644 index 0000000000..1a656438b2 --- /dev/null +++ b/tests/control_plane/historical_shadow_fixture.py @@ -0,0 +1,71 @@ +"""Immutable 1.x producers for cross-version recovery, never current writes. + +Requires the public Git object below (CI checks out history). Only synthetic +originals use this source; inspection, disposition and later writes use HEAD. +""" +from __future__ import annotations + +from dataclasses import dataclass, replace +import io +from pathlib import Path +import subprocess +import sys +import tarfile + +import pytest + +from tests.control_plane.shadow_e2e_fixture import ShadowWorkspace, workspace + +# Last pre-cutoff baseline, retained as a public compatibility input. +ORIGINAL_REVISION = "60d833b13c85135587b725013b247e827663ffb9" +ROOT = Path(__file__).resolve().parents[2] + + +@pytest.fixture(name="original_source", scope="session") +def original_1x_source(tmp_path_factory): + root = tmp_path_factory.mktemp("loopx-original-1x") + result = subprocess.run(["git", "archive", ORIGINAL_REVISION, "loopx"], + cwd=ROOT, capture_output=True, check=False) + assert result.returncode == 0, ( + f"Historical recovery requires Git object {ORIGINAL_REVISION}; " + "fetch repository history before running this suite. " + result.stderr.decode()) + with tarfile.open(fileobj=io.BytesIO(result.stdout)) as archive: + for member in archive: + path = root / member.name + assert path.resolve().is_relative_to(root) + if member.isdir(): + path.mkdir(parents=True, exist_ok=True) + else: + assert member.isfile(), member.name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(archive.extractfile(member).read()) + imported = subprocess.run([sys.executable, "-c", "import loopx; print(loopx.__file__)"], + cwd=root, capture_output=True, text=True, check=True) + assert Path(imported.stdout.strip()).resolve() == root / "loopx/__init__.py" + return root + + +@dataclass +class HistoricalShadowWorkspace(ShadowWorkspace): + original_source: Path | None = None + + def original(self): + assert self.original_source is not None + return replace(self, cwd=self.original_source, package=self.original_source / "loopx") + + def original_cli(self, *args, **kwargs): + return ShadowWorkspace.cli(self.original(), *args, **kwargs) + + def original_add(self, text): + return self.original_cli("todo", "add", "--role", "agent", "--text", text) + + def original_crash(self, window, *args): + return ShadowWorkspace.crash(self.original(), window, *args) + + +@pytest.fixture(name="historical_workspace") +def historical_workspace_factory(original_source): + def create(path, **kwargs): + current = workspace(path, **kwargs) + return HistoricalShadowWorkspace(**vars(current), original_source=original_source) + return create diff --git a/tests/control_plane/test_cold_source_disposition_e2e.py b/tests/control_plane/test_cold_source_disposition_e2e.py index bcdd9be27e..8cbe2c2a5e 100644 --- a/tests/control_plane/test_cold_source_disposition_e2e.py +++ b/tests/control_plane/test_cold_source_disposition_e2e.py @@ -20,7 +20,7 @@ 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.historical_shadow_fixture import historical_workspace_factory, original_1x_source # noqa: F401 from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime @@ -85,8 +85,8 @@ def originals(path): if p.is_file() and not p.name.endswith(".lock")} -def cold_workspace(path): - fixture = workspace(path, bootstrap=False) +def cold_workspace(path, historical_workspace): + fixture = historical_workspace(path, bootstrap=False) registry = fixture.state.parent / ".loopx/registry.json" registry.parent.mkdir() fixture.registry.rename(registry) @@ -141,11 +141,11 @@ def provider_todos(receiver, fixture): ("before_commit", "committed", False), ("after_commit", "committed", False), ]) -def test_stopped_original_todo_recovers_once_without_python_producer( +def test_stopped_original_todo_recovers_once_without_python_producer(historical_workspace, tmp_path, receiver, window, resolution, no_op, ): - fixture = workspace(tmp_path) - fixture.crash(window, "todo", "add", "--role", "agent", "--text", "Interrupted original") + fixture = historical_workspace(tmp_path) + fixture.original_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" @@ -182,13 +182,13 @@ def test_stopped_original_todo_recovers_once_without_python_producer( @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("window", ["before_commit", "after_commit"]) -def test_original_lease_disposition_requires_release_before_cold_import( +def test_original_lease_disposition_requires_release_before_cold_import(historical_workspace, 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", + fixture = cold_workspace(tmp_path / "project", historical_workspace) + todo = fixture.original_add("Original leased task")["todo_id"] + fixture.original_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() @@ -254,14 +254,14 @@ def test_original_lease_disposition_requires_release_before_cold_import( @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_unproved_original_archival_then_cold_import_keeps_history_inert( +def test_unproved_original_archival_then_cold_import_keeps_history_inert(historical_workspace, 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"] + fixture = cold_workspace(tmp_path / "project", historical_workspace) + fixture.original_crash("before_marker", "handoff-mode", "set", "--mode", "soft_claim") + fixture.original_cli("handoff-mode", "set", "--mode", "hard_lease") + source_todo = fixture.original_add("Later source requirement")["todo_id"] pending_backup = backup(fixture, "ambiguous-original") before = originals(fixture.runtime) result = drain(receiver, fixture) @@ -335,7 +335,7 @@ def test_unproved_original_archival_then_cold_import_keeps_history_inert( ("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( +def test_original_outbox_disposition_then_cold_import_preserves_receipts_and_later_writes(historical_workspace, tmp_path, monkeypatch, receiver, receiver_package, provider, window, committed, ): """Join original effect recovery to cold import, without replaying history. @@ -345,8 +345,8 @@ def test_original_outbox_disposition_then_cold_import_preserves_receipts_and_lat 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") + fixture = cold_workspace(tmp_path / "project", historical_workspace) + fixture.original_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")) diff --git a/tests/control_plane/test_cold_source_inspection.py b/tests/control_plane/test_cold_source_inspection.py index 2620fd7ea0..d8cff2c8d6 100644 --- a/tests/control_plane/test_cold_source_inspection.py +++ b/tests/control_plane/test_cold_source_inspection.py @@ -7,6 +7,7 @@ import pytest +from tests.control_plane.historical_shadow_fixture import historical_workspace_factory, original_1x_source # noqa: F401 from tests.control_plane.shadow_e2e_fixture import workspace @@ -17,9 +18,9 @@ def capture_bytes(fixture): 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") +def test_capture_inventory_keeps_original_history_and_outbox_bytes(historical_workspace, tmp_path): + fixture = historical_workspace(tmp_path) + fixture.original_add("Original captured operation") outbox = fixture.runtime / "authority-shadow" / "outbox" / fixture.goal / "todos" outbox.mkdir(exist_ok=True) residue = outbox / "unrecognized-original-receipt.json" @@ -47,9 +48,9 @@ def test_capture_inventory_keeps_original_history_and_outbox_bytes(tmp_path): 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") +def test_capture_inventory_keeps_rolled_back_operation_archives(historical_workspace, tmp_path): + fixture = historical_workspace(tmp_path) + fixture.original_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" @@ -67,10 +68,10 @@ def test_capture_inventory_keeps_rolled_back_operation_archives(tmp_path): @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, +def test_original_lease_outbox_uses_its_own_partition_without_renewal_or_cleanup(historical_workspace, tmp_path, window): + fixture = historical_workspace(tmp_path) + todo_id = fixture.original_add("Original leased task")["todo_id"] + fixture.original_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") @@ -87,8 +88,8 @@ def test_original_lease_outbox_uses_its_own_partition_without_renewal_or_cleanup @pytest.mark.parametrize("corrupt", [False, True]) -def test_capture_inventory_reads_original_observation_through_existing_store(tmp_path, corrupt): - fixture = workspace(tmp_path) +def test_capture_inventory_reads_original_observation_through_existing_store(historical_workspace, tmp_path, corrupt): + fixture = historical_workspace(tmp_path) source = fixture.runtime / "authority-shadow" / "file-v0" retained = fixture.runtime / "authority-shadow" / "file" / fixture.goal retained.mkdir(parents=True) @@ -112,8 +113,8 @@ def test_capture_inventory_reads_original_observation_through_existing_store(tmp @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) +def test_capture_inventory_refuses_unreadable_or_unsafe_original_history(historical_workspace, tmp_path, unsafe): + fixture = historical_workspace(tmp_path) if unsafe == "corrupt_history": store = next((fixture.runtime / "authority-shadow" / "file-v0").glob("authority-store-*.json")) store.write_text('{"invalid":true}') @@ -139,9 +140,9 @@ def test_capture_inventory_refuses_unreadable_or_unsafe_original_history(tmp_pat @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") +def test_capture_inventory_does_not_drain_crashed_original_outbox(historical_workspace, tmp_path, window): + fixture = historical_workspace(tmp_path) + fixture.original_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"] @@ -160,10 +161,10 @@ def test_capture_inventory_does_not_drain_crashed_original_outbox(tmp_path, wind @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) +def test_outbox_review_refuses_unproved_disposition_but_retains_original_bytes(historical_workspace, tmp_path, defect): + fixture = historical_workspace(tmp_path) window = "after_commit" if defect == "receipt_bytes" else "before_commit" - fixture.crash(window, "todo", "add", "--role", "agent", "--text", "Original operation") + fixture.original_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": @@ -186,8 +187,8 @@ def test_outbox_review_refuses_unproved_disposition_but_retains_original_bytes(t @pytest.mark.parametrize("archive", ["candidate", "outbox"]) -def test_capture_inventory_refuses_missing_terminal_rollback_archive(tmp_path, archive): - fixture = workspace(tmp_path) +def test_capture_inventory_refuses_missing_terminal_rollback_archive(historical_workspace, tmp_path, archive): + fixture = historical_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": diff --git a/tests/control_plane/test_monitor_poll_transaction_recovery.py b/tests/control_plane/test_monitor_poll_transaction_recovery.py index e7c4c3c175..02f8d0ec7e 100644 --- a/tests/control_plane/test_monitor_poll_transaction_recovery.py +++ b/tests/control_plane/test_monitor_poll_transaction_recovery.py @@ -1,4 +1,4 @@ -"""Legacy Monitor poll recovery at the public CLI boundary.""" +"""Current Monitor consumers and frozen 1.x receipts at the public CLI boundary.""" from __future__ import annotations @@ -13,6 +13,8 @@ import pytest +from tests.control_plane.historical_shadow_fixture import original_1x_source # noqa: F401 + from loopx.control_plane.testing.canary_harness import ( read_run_index, write_fixture_registry, @@ -78,7 +80,7 @@ def interrupted_commit(request): def _cli( - registry: Path, runtime: Path, *args: str, fault: str | None = None, + registry: Path, runtime: Path, *args: str, fault: str | None = None, source: Path = REPO, ) -> tuple[int, dict | None]: if fault == "quota_commit": launcher = [sys.executable, "-c", _QUOTA_COMMIT_FAULT_WRAPPER] @@ -89,7 +91,7 @@ def _cli( result = subprocess.run( [*launcher, "--registry", str(registry), "--format", "json", "--runtime-root", str(runtime), *args], - cwd=REPO, + cwd=source, env={**os.environ, "LOOPX_USAGE_PING": "0", "MONITOR_FAULT_PHASE": fault or ""}, capture_output=True, text=True, check=False, timeout=60, @@ -97,14 +99,14 @@ def _cli( 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) +def _ok(registry: Path, runtime: Path, *args: str, source: Path = REPO) -> dict: + code, result = _cli(registry, runtime, *args, source=source) assert code == 0, result assert isinstance(result, dict) return result -def _fixture(tmp_path: Path) -> tuple[Path, Path, Path, str, tuple[str, ...]]: +def _fixture(tmp_path: Path, source: Path | None = None) -> tuple[Path, Path, Path, str, tuple[str, ...]]: project, runtime = tmp_path / "project", tmp_path / "runtime" state = project / ".codex" / "goals" / GOAL / "ACTIVE_GOAL_STATE.md" registry = project / ".loopx" / "registry.json" @@ -117,14 +119,15 @@ def _fixture(tmp_path: Path) -> tuple[Path, Path, Path, str, tuple[str, ...]]: 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"}, - ) + if source is None: + initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( + goal_id=GOAL, handoff_mode="soft_claim", todos=[]), state_path=state) + monitor = _ok(registry, runtime, "todo", "add", "--goal-id", GOAL, + "--role", "agent", "--text", "Observe public synthetic target", + "--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", + source=source or REPO) turn = "synthetic-monitor-turn-1" _ok(registry, runtime, "quota", "should-run", "--goal-id", GOAL, "--agent-id", AGENT, "--runtime-profile", "generic_cli", @@ -178,11 +181,12 @@ def _engage_fence(runtime: Path) -> None: }), encoding="utf-8") +@pytest.mark.parametrize("historical", [False, True], ids=["canonical", "original-receipt"]) @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, + original_source, tmp_path: Path, owner: str, historical: bool, ) -> None: - registry, runtime, state, _, poll = _fixture(tmp_path) + registry, runtime, state, _, poll = _fixture(tmp_path, original_source if historical else None) 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") @@ -190,7 +194,7 @@ def test_public_monitor_successor_claim_checks_registered_peer_before_writing( 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") + code, response = _cli(registry, runtime, *poll[:-1], "--next-claimed-by", owner, "--execute", source=original_source if historical else REPO) assert response is not None if owner == "unknown-builder": assert code != 0 @@ -200,6 +204,11 @@ def test_public_monitor_successor_claim_checks_registered_peer_before_writing( 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) == [] + elif owner != AGENT and not historical: + assert code != 0 and "actor to match owner" in response["reason"] + assert state.read_bytes() == before_state + assert _successors(registry, runtime) == {} + assert _poll_events(runtime) == [] else: assert code == 0, json.dumps(response, indent=2) successor = response["todo_writeback"]["next_todos"][0] @@ -239,13 +248,13 @@ def test_monitor_batch_handler_classifies_expected_input_errors() -> None: @pytest.mark.parametrize("fault", ["before", "after"]) -def test_interrupted_legacy_poll_retries_one_complete_batch( +def test_original_monitor_effect_recovers_but_uncommitted_intent_cannot_write(original_source, tmp_path: Path, fault: str, ) -> None: - registry, runtime, state, monitor_id, poll = _fixture(tmp_path) + registry, runtime, state, monitor_id, poll = _fixture(tmp_path, original_source) before = state.read_bytes() - code, result = _cli(registry, runtime, *poll, fault=fault) + code, result = _cli(registry, runtime, *poll, fault=fault, source=original_source) assert (code, result) == (83, None) if fault == "before": assert state.read_bytes() == before @@ -254,6 +263,14 @@ def test_interrupted_legacy_poll_retries_one_complete_batch( assert set(_successors(registry, runtime)) == {AGENT_TASK, USER_TASK} assert _poll_events(runtime) == [] + if fault == "before": + code, refusal = _cli(registry, runtime, *poll) + assert code != 0 and "canonical" in refusal["reason"].lower() + assert state.read_bytes() == before + assert _successors(registry, runtime) == {} + assert _poll_events(runtime) == [] + assert _quota_receipt(runtime).exists() + return recovered = _ok(registry, runtime, *poll) successors = _successors(registry, runtime) assert set(successors) == {AGENT_TASK, USER_TASK} @@ -270,10 +287,10 @@ def test_interrupted_legacy_poll_retries_one_complete_batch( 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) +def test_identity_less_legacy_pending_cannot_guess_successors(original_source, tmp_path: Path) -> None: + registry, runtime, state, _monitor_id, poll = _fixture(tmp_path, original_source) before = state.read_bytes() - code, result = _cli(registry, runtime, *poll, fault="before") + code, result = _cli(registry, runtime, *poll, fault="before", source=original_source) assert (code, result) == (83, None) pending_path = _quota_receipt(runtime) pending = json.loads(pending_path.read_text(encoding="utf-8")) @@ -286,16 +303,16 @@ def test_identity_less_legacy_pending_cannot_guess_successors(tmp_path: Path) -> code, rejected = _cli(registry, runtime, *poll) assert code != 0, rejected - assert "reconciliation" in str(rejected.get("reason")), rejected + assert "canonical authority" 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) +def test_malformed_business_receipt_is_not_treated_as_absent(original_source, tmp_path: Path) -> None: + registry, runtime, state, _monitor_id, poll = _fixture(tmp_path, original_source) + assert _cli(registry, runtime, *poll, fault="after", source=original_source) == (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")) @@ -307,10 +324,10 @@ def test_malformed_business_receipt_is_not_treated_as_absent(tmp_path: Path) -> @pytest.mark.parametrize("held_drain_lock", [False, True], ids=["uncontended", "deferred"]) -def test_enabled_shadow_business_replay_then_bounded_drain_recovers_batch( +def test_enabled_shadow_business_replay_then_bounded_drain_recovers_batch(original_source, tmp_path: Path, held_drain_lock: bool, ) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) + registry, runtime, state, _monitor_id, poll = _fixture(tmp_path, original_source) config = json.loads(registry.read_text(encoding="utf-8")) config["goals"][0]["coordination"]["runtime_shadow"] = { "enabled": True, @@ -319,9 +336,9 @@ def test_enabled_shadow_business_replay_then_bounded_drain_recovers_batch( } registry.write_text(json.dumps(config), encoding="utf-8") _ok(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", GOAL, - "--execute") + "--execute", source=original_source) - assert _cli(registry, runtime, *poll, fault="after") == (83, None) + assert _cli(registry, runtime, *poll, fault="after", source=original_source) == (83, None) outbox = runtime / "authority-shadow" / "outbox" / GOAL / "todos" (prepared,) = outbox.glob("*.prepared.json") prepared_bytes = prepared.read_bytes() @@ -384,9 +401,9 @@ def test_enabled_shadow_business_replay_then_bounded_drain_recovers_batch( )["proof"]["transactions"] == view["proof"]["transactions"] -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) +def test_committed_receipt_without_batch_version_still_replays(original_source, tmp_path: Path) -> None: + registry, runtime, state, _monitor_id, poll = _fixture(tmp_path, original_source) + committed = _ok(registry, runtime, *poll, source=original_source) receipt = json.loads(_quota_receipt(runtime).read_text(encoding="utf-8")) assert receipt["status"] != "provider_pending" assert "legacy_batch_version" not in receipt @@ -414,16 +431,25 @@ def test_committed_receipt_without_batch_version_still_replays(tmp_path: Path) - @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( +def test_original_monitor_writeback_contract_replays_and_preview_grants_no_write(original_source, tmp_path: Path, dry_run: bool, user_class: str, ) -> None: - registry, runtime, state, monitor_id, poll = _fixture(tmp_path) + registry, runtime, state, monitor_id, poll = _fixture(tmp_path, original_source) 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"] + if dry_run: + writeback = _ok(registry, runtime, *args, source=original_source)["todo_writeback"] + code, refusal = _cli(registry, runtime, *poll) + assert code != 0 and "canonical authority" in refusal["reason"] + assert _poll_events(runtime) == [] + else: + # Interrupt after the business effect, before quota settlement. + assert _cli(registry, runtime, *args, fault="after", source=original_source) == (83, None) + # The current receiver must expose the original complete result contract. + writeback = _ok(registry, runtime, *args)["todo_writeback"] update = writeback["todo_update"] assert update["dry_run"] is dry_run assert update["changed"] is True @@ -452,7 +478,12 @@ def test_public_monitor_writeback_retains_todo_result_contract( 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"] + if dry_run: + assert item["coordination_runtime_shadow"] == update["coordination_runtime_shadow"] + else: + # Post-commit drain diagnostics were never part of the atomic receipt. + assert "coordination_runtime_shadow" not in item + assert "coordination_runtime_shadow" not in update assert agent["unblocks_todo_id"] == monitor_id assert agent["agent_id"] is None assert user["agent_id"] == AGENT @@ -501,14 +532,14 @@ def test_existing_successor_semantics_are_checked_before_batch_write( 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 [todo["todo_id"] for todo in _successors(registry, runtime).values() + if todo["role"] == "agent"] == [existing["todo_id"]] 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") +def test_pending_legacy_receipt_replays_after_canonical_cutover(original_source, tmp_path: Path) -> None: + registry, runtime, state, _monitor_id, poll = _fixture(tmp_path, original_source) + code, result = _cli(registry, runtime, *poll, fault="after", source=original_source) assert (code, result) == (83, None) original_ids = {todo["todo_id"] for todo in _successors(registry, runtime).values()} state_after = state.read_bytes() @@ -539,10 +570,10 @@ def test_pending_legacy_receipt_replays_after_canonical_cutover(tmp_path: Path) @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_historical_canonical_pending_plan_settles_existing_business_receipt( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, + original_source, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, ) -> None: isolate_sqlite_runtime(tmp_path, monkeypatch) - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) + registry, runtime, state, _monitor_id, poll = _fixture(tmp_path, original_source) 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") @@ -579,8 +610,8 @@ def test_historical_canonical_pending_plan_settles_existing_business_receipt( 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) +def test_legacy_writer_fence_rejects_full_monitor_batch(original_source, tmp_path: Path) -> None: + registry, runtime, state, _monitor_id, poll = _fixture(tmp_path, original_source) before = state.read_bytes() _engage_fence(runtime) @@ -590,18 +621,18 @@ def test_legacy_writer_fence_rejects_full_monitor_batch(tmp_path: Path) -> None: 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) +def test_atomic_successor_reconciles_existing_typed_next_action(original_source, tmp_path: Path) -> None: + registry, runtime, state, _monitor_id, poll = _fixture(tmp_path, original_source) lower = _ok(registry, runtime, "todo", "add", "--goal-id", GOAL, "--role", "agent", "--text", "[P1] Existing lower priority task", - "--task-class", "advancement_task", "--action-kind", "validate") + "--task-class", "advancement_task", "--action-kind", "validate", source=original_source) 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") + code, result = _cli(registry, runtime, *command, fault="after", source=original_source) 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) @@ -615,11 +646,11 @@ def test_atomic_successor_reconciles_existing_typed_next_action(tmp_path: Path) @pytest.mark.parametrize("change", ["rename", "complete", "archive"]) -def test_pending_retry_keeps_original_successor_identity_after_public_edit( +def test_pending_retry_keeps_original_successor_identity_after_public_edit(original_source, tmp_path: Path, change: str, ) -> None: - registry, runtime, state, _monitor_id, poll = _fixture(tmp_path) - code, result = _cli(registry, runtime, *poll, fault="after") + registry, runtime, state, _monitor_id, poll = _fixture(tmp_path, original_source) + code, result = _cli(registry, runtime, *poll, fault="after", source=original_source) assert (code, result) == (83, None) first = _successors(registry, runtime) assert set(first) == {AGENT_TASK, USER_TASK} @@ -629,14 +660,14 @@ def test_pending_retry_keeps_original_successor_identity_after_public_edit( 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") + "--text", "Validate observed synthetic head with revised scope", source=original_source) 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") + "--no-follow-up", "--evidence", "Synthetic validation complete", source=original_source) if change == "archive": _ok(registry, runtime, "todo", "archive-completed", "--goal-id", GOAL, - "--role", "agent", "--max-active-done", "0", "--execute") + "--role", "agent", "--max-active-done", "0", "--execute", source=original_source) archived = _ok(registry, runtime, "todo", "list", "--goal-id", GOAL, "--role", "agent", "--todo-id", agent_id) assert archived["todo"]["archive_state"] == "archive" diff --git a/tests/control_plane/test_shadow_cursor_recovery_e2e.py b/tests/control_plane/test_shadow_cursor_recovery_e2e.py index 105f632c6a..88c08b93f5 100644 --- a/tests/control_plane/test_shadow_cursor_recovery_e2e.py +++ b/tests/control_plane/test_shadow_cursor_recovery_e2e.py @@ -9,7 +9,9 @@ import pytest -from shadow_e2e_fixture import REPO, ShadowWorkspace, workspace +from tests.control_plane.historical_shadow_fixture import historical_workspace_factory, original_1x_source # noqa: F401 + +from shadow_e2e_fixture import REPO, ShadowWorkspace 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 @@ -49,7 +51,7 @@ def acquire(w: ShadowWorkspace, todo: str, *, crash: bool = False) -> None: } 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()})] + 'module': (w.original_source / '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 @@ -73,17 +75,17 @@ def acquire(w: ShadowWorkspace, todo: str, *, crash: bool = False) -> None: @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( +def test_abandoned_cursor_survives_all_consumers(historical_workspace, tmp_path: Path, partition: str, prior_mutations: int, abandoned: int, ) -> None: - w = workspace(tmp_path, bootstrap=False) + w = historical_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' + ids = [w.original_add(f'Baseline task {index}')['todo_id'] for index in range(3)] + assert w.original_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']) + ids.append(w.original_add(f'Applied task {index}')['todo_id']) else: acquire(w, ids[index]) assert w.drain()['ok'] is True @@ -93,7 +95,7 @@ def mutate(index: int) -> None: 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}') + w.original_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 @@ -138,21 +140,21 @@ def mutate(index: int) -> None: @pytest.mark.parametrize('partition', ['todos', 'leases']) @pytest.mark.parametrize('applied', [False, True]) -def test_forged_applied_digest_holds_every_consumer_without_rewriting_bytes( +def test_forged_applied_digest_holds_every_consumer_without_rewriting_bytes(historical_workspace, 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') + w = historical_workspace(tmp_path, bootstrap=False) + todo = w.original_add('Baseline is not mutation coverage')['todo_id'] + w.original_cli('coordination-shadow', 'bootstrap', '--execute') if applied: if partition == 'todos': - w.add('Real mutation with an applied digest') + w.original_add('Real mutation with an applied digest') else: acquire(w, todo) elif partition == 'todos': - w.crash('before_replace', 'todo', 'add', '--role', 'agent', '--text', 'Abandoned') + w.original_crash('before_replace', 'todo', 'add', '--role', 'agent', '--text', 'Abandoned') else: acquire(w, todo, crash=True) assert w.drain()['ok'] is True diff --git a/tests/control_plane/test_shadow_drain_adversarial.py b/tests/control_plane/test_shadow_drain_adversarial.py index 26aeb6b39a..4f91e57d50 100644 --- a/tests/control_plane/test_shadow_drain_adversarial.py +++ b/tests/control_plane/test_shadow_drain_adversarial.py @@ -6,10 +6,10 @@ import pytest -from loopx.control_plane.effect_runtime import EffectRuntimeRejected +from tests.control_plane.historical_shadow_fixture import historical_workspace_factory, original_1x_source # noqa: F401 +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 @@ -18,12 +18,12 @@ @pytest.mark.parametrize("window", ["before_commit", "after_commit", "after_cursor"]) -def test_cleanup_permission_failure_reports_verified_commit_and_recovers( +def test_cleanup_permission_failure_reports_verified_commit_and_recovers(historical_workspace, 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") + w = historical_workspace(tmp_path) + w.original_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 @@ -51,14 +51,14 @@ def test_cleanup_permission_failure_reports_verified_commit_and_recovers( )["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") +def test_missing_cursor_cannot_reuse_a_sequence_when_the_next_writer_arrives_first(historical_workspace, tmp_path: Path) -> None: + w = historical_workspace(tmp_path) + w.original_add("First complete transaction") + w.original_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") + result = w.original_add("Writer arrives before cursor recovery") assert result["ok"] is True recovered = w.drain() assert recovered["ok"] is True, recovered @@ -68,12 +68,12 @@ def test_missing_cursor_cannot_reuse_a_sequence_when_the_next_writer_arrives_fir 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) +def test_small_recovery_budget_makes_progress_through_verified_residue(historical_workspace, tmp_path: Path) -> None: + w = historical_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}") + w.original_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(): @@ -88,12 +88,12 @@ def test_small_recovery_budget_makes_progress_through_verified_residue(tmp_path: 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) +def test_raw_residue_mismatch_preserves_every_file_before_any_cleanup(historical_workspace, tmp_path: Path) -> None: + w = historical_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}") + w.original_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(): @@ -108,17 +108,17 @@ def test_raw_residue_mismatch_preserves_every_file_before_any_cleanup(tmp_path: 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) +def test_real_prepare_io_failure_holds_public_primary_before_replace(historical_workspace, tmp_path: Path) -> None: + w = historical_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) + rejected = w.original_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") + w.original_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" @@ -130,11 +130,11 @@ def test_real_prepare_io_failure_holds_public_primary_before_replace(tmp_path: P ("window", "claimed_resolution"), [("before_replace", "committed_proven_by_readback"), ("before_marker", "abandoned")], ) -def test_native_markerless_resolution_requires_source_evidence( +def test_native_markerless_resolution_requires_source_evidence(historical_workspace, 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") + w = historical_workspace(tmp_path) + w.original_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), @@ -160,15 +160,15 @@ def test_native_markerless_resolution_requires_source_evidence( assert delivered["no_op"] is (window == "before_replace") -def test_registry_runtime_override_cannot_bypass_an_active_source_binding(tmp_path: Path) -> None: +def test_registry_runtime_override_cannot_bypass_an_active_source_binding(historical_workspace, tmp_path: Path) -> None: import json import subprocess import sys from shadow_e2e_fixture import REPO - w = workspace(tmp_path) + w = historical_workspace(tmp_path) for index in range(3): - w.add(f"Qualification evidence {index}") + w.original_add(f"Qualification evidence {index}") before = w.state.read_bytes() other_root = tmp_path / "other-runtime" results = [] @@ -189,11 +189,11 @@ def test_registry_runtime_override_cannot_bypass_an_active_source_binding(tmp_pa @pytest.mark.parametrize("missing", ["identity", "candidate"]) -def test_missing_cursor_source_allocation_never_recreates_missing_store_files( +def test_missing_cursor_source_allocation_never_recreates_missing_store_files(historical_workspace, tmp_path: Path, missing: str, ) -> None: - w = workspace(tmp_path) - w.add("Existing generation") + w = historical_workspace(tmp_path) + w.original_add("Existing generation") directory = outbox.partition_directory(w.runtime, w.goal, "todos") (directory / "drain-cursor.json").unlink() store = w.runtime / "authority-shadow" / "file-v0" @@ -202,21 +202,21 @@ def test_missing_cursor_source_allocation_never_recreates_missing_store_files( 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) + result = w.original_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) +def test_state_file_override_cannot_attribute_an_unbound_source_to_active_lineage(historical_workspace, tmp_path: Path) -> None: + w = historical_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)) + outcomes.append(w.original_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, diff --git a/tests/control_plane/test_shadow_drain_e2e.py b/tests/control_plane/test_shadow_drain_e2e.py index 760069d95e..982aacda86 100644 --- a/tests/control_plane/test_shadow_drain_e2e.py +++ b/tests/control_plane/test_shadow_drain_e2e.py @@ -9,7 +9,9 @@ import pytest -from shadow_e2e_fixture import REPO, ShadowWorkspace, workspace +from tests.control_plane.historical_shadow_fixture import historical_workspace_factory, original_1x_source # noqa: F401 + +from shadow_e2e_fixture import REPO, ShadowWorkspace from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox @@ -28,7 +30,7 @@ def history(w: ShadowWorkspace) -> dict: def pending(w: ShadowWorkspace, text: str = "One primary mutation") -> dict: - return w.crash("before_commit", "todo", "add", "--role", "agent", "--text", text) + return w.original_crash("before_commit", "todo", "add", "--role", "agent", "--text", text) def snapshot(path: Path) -> dict[str, bytes]: @@ -39,11 +41,11 @@ def snapshot(path: Path) -> dict[str, bytes]: } -def test_public_primary_maps_one_to_one_to_receipts_and_replays_idempotently( +def test_public_primary_maps_one_to_one_to_receipts_and_replays_idempotently(historical_workspace, tmp_path: Path, ) -> None: - w = workspace(tmp_path) - ids = [w.add(f"Public fact {index}")["todo_id"] for index in range(3)] + w = historical_workspace(tmp_path) + ids = [w.original_add(f"Public fact {index}")["todo_id"] for index in range(3)] w.drain() view = history(w) transactions = view["proof"]["transactions"] @@ -67,9 +69,9 @@ def test_public_primary_maps_one_to_one_to_receipts_and_replays_idempotently( 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 +def test_public_mutation_has_no_second_snapshot_mirror(historical_workspace, tmp_path: Path) -> None: + w = historical_workspace(tmp_path) + assert w.original_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())};" @@ -101,11 +103,11 @@ def test_public_mutation_has_no_second_snapshot_mirror(tmp_path: Path) -> None: "wrong_lineage", ], ) -def test_cursor_cannot_authorize_deletion_or_hide_mutations( +def test_cursor_cannot_authorize_deletion_or_hide_mutations(historical_workspace, tmp_path: Path, attack: str ) -> None: - w = workspace(tmp_path) - w.add("First captured mutation") + w = historical_workspace(tmp_path) + w.original_add("First captured mutation") pending(w, "Second real mutation") d = directory(w) cursor_path = d / "drain-cursor.json" @@ -144,11 +146,11 @@ def test_cursor_cannot_authorize_deletion_or_hide_mutations( @pytest.mark.parametrize("window", ["after_commit", "after_cursor", "between_unlinks"]) -def test_real_sigkill_drain_windows_recover_from_exact_receipts( +def test_real_sigkill_drain_windows_recover_from_exact_receipts(historical_workspace, tmp_path: Path, window: str ) -> None: - w = workspace(tmp_path) - w.crash( + w = historical_workspace(tmp_path) + w.original_crash( window, "todo", "add", @@ -168,28 +170,28 @@ def test_real_sigkill_drain_windows_recover_from_exact_receipts( assert [p.name for p in directory(w).iterdir()] == ["drain-cursor.json"] -def test_missing_cursor_recovers_only_from_complete_verified_history( +def test_missing_cursor_recovers_only_from_complete_verified_history(historical_workspace, tmp_path: Path, ) -> None: - w = workspace(tmp_path) - w.add("First") - w.add("Second") + w = historical_workspace(tmp_path) + w.original_add("First") + w.original_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") + w.original_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( +def test_primary_sigkill_preserves_complete_bytes_and_proves_before_marker(historical_workspace, tmp_path: Path, window: str ) -> None: - w = workspace(tmp_path) + w = historical_workspace(tmp_path) old = w.state.read_bytes() - w.crash( + w.original_crash( window, "todo", "add", "--role", "agent", "--text", "Atomic primary sentence" ) current = w.state.read_bytes() @@ -206,11 +208,11 @@ def test_primary_sigkill_preserves_complete_bytes_and_proves_before_marker( 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") +def test_prepared_a_b_a_never_infers_first_write_was_abandoned(historical_workspace, tmp_path: Path) -> None: + w = historical_workspace(tmp_path) + w.original_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") + w.original_cli("handoff-mode", "set", "--mode", "hard_lease") assert w.cli("handoff-mode", "show")["handoff_mode"] == "hard_lease" before = snapshot(directory(w)) result = w.drain() @@ -223,10 +225,10 @@ def test_prepared_a_b_a_never_infers_first_write_was_abandoned(tmp_path: Path) - assert qualify["ok"] is False -def test_concurrent_real_drainers_commit_once_without_cursor_regression( +def test_concurrent_real_drainers_commit_once_without_cursor_regression(historical_workspace, tmp_path: Path, ) -> None: - w = workspace(tmp_path) + w = historical_workspace(tmp_path) for index in range(3): pending(w, f"Concurrent {index}") argv = [ @@ -255,8 +257,8 @@ def test_concurrent_real_drainers_commit_once_without_cursor_regression( assert outbox.read_cursor(directory(w))["last_seq"] == 3 -def test_bounded_drain_preserves_unprocessed_entries(tmp_path: Path) -> None: - w = workspace(tmp_path) +def test_bounded_drain_preserves_unprocessed_entries(historical_workspace, tmp_path: Path) -> None: + w = historical_workspace(tmp_path) for index in range(3): pending(w, f"Bounded {index}") result = w.drain(max_entries="1") diff --git a/tests/control_plane/test_task_lease_argument_recovery.py b/tests/control_plane/test_task_lease_argument_recovery.py index b1912a13f3..2caae56a9f 100644 --- a/tests/control_plane/test_task_lease_argument_recovery.py +++ b/tests/control_plane/test_task_lease_argument_recovery.py @@ -1,4 +1,8 @@ -"""Execute CLI grammar repairs against disposable real lease authorities.""" +"""Execute current grant grammar repairs against File/SQLite authorities. + +Original 1.x lease release/recovery is covered by frozen-history lifecycle tests; +2.0 does not authorize new source grants, renewals or transfers. +""" from __future__ import annotations import json @@ -15,7 +19,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 +31,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): From 83dd8f77047fc36c72812cc2a7331ba8fcce1e3a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:33:07 +0800 Subject: [PATCH 15/54] test: seed retained native consumers from immutable 1.x source Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_shadow_observable_native_e2e.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/control_plane/test_shadow_observable_native_e2e.py b/tests/control_plane/test_shadow_observable_native_e2e.py index fc927e75a9..47f06fa74f 100644 --- a/tests/control_plane/test_shadow_observable_native_e2e.py +++ b/tests/control_plane/test_shadow_observable_native_e2e.py @@ -3,13 +3,24 @@ import json import sys +from pathlib import Path import pytest from test_shadow_observable_e2e import Caller, SOURCE, caller as caller +from historical_shadow_fixture import HistoricalShadowWorkspace, original_1x_source # noqa: F401 pytestmark = pytest.mark.stage2c_e2e + +def original_todo(w: Caller, source: Path, text: str, *extra: str) -> str: + """Create pre-upgrade input; every consumer below still runs current code.""" + original = HistoricalShadowWorkspace(w.registry, w.root, w.state, + goal="observable", original_source=source) + result = original.original_cli("todo", "add", "--role", "agent", "--text", text, *extra) + assert result["ok"] is True, result + return result["todo_id"] + NATIVE = r""" import {join} from 'node:path'; const input = JSON.parse(process.argv[1]); @@ -44,9 +55,9 @@ def native(w: Caller, action: str, request: dict) -> dict: '-e', NATIVE, json.dumps(value)], ['native', action, json.dumps(request)]) -def test_canonical_argument_intent_and_atomic_claim(caller: Caller) -> None: +def test_canonical_argument_intent_and_atomic_claim(caller: Caller, original_source: Path) -> None: w = caller - todo = w.add('Native canonical input', '--note', 'Preserve operator note', '--required-write-scope', 'src/**') + todo = original_todo(w, original_source, '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)]) @@ -76,9 +87,9 @@ def test_canonical_argument_intent_and_atomic_claim(caller: Caller) -> None: assert w.state.read_bytes() == recovered -def test_native_unclaimed_edit_and_explicit_note_clear(caller: Caller) -> None: +def test_native_unclaimed_edit_and_explicit_note_clear(caller: Caller, original_source: Path) -> None: w = caller - todo = w.add('Unclaimed correction', '--note', 'Keep until explicitly cleared') + todo = original_todo(w, original_source, '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)]) @@ -137,9 +148,9 @@ def post(path,body): """ -def test_http_reward_preserves_preview_gate_and_persisted_arguments(caller: Caller) -> None: +def test_http_reward_preserves_preview_gate_and_persisted_arguments(caller: Caller, original_source: Path) -> None: w = caller - todo = w.add('HTTP must preserve Todo ownership', '--claimed-by', 'agent-a') + todo = original_todo(w, original_source, '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) From c6f25e5a8965cce7a0472b807b1c9f4dfc642875 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:36:31 +0800 Subject: [PATCH 16/54] test: retain large historical source cutover after writer retirement Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_large_source_journey.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/control_plane/test_large_source_journey.py b/tests/control_plane/test_large_source_journey.py index 3d9e839301..48e92b49f3 100644 --- a/tests/control_plane/test_large_source_journey.py +++ b/tests/control_plane/test_large_source_journey.py @@ -8,6 +8,7 @@ import pytest from test_runtime_shadow_bounded_e2e import cli, enable, workspace +from historical_shadow_fixture import HistoricalShadowWorkspace, original_1x_source # noqa: F401 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 @@ -15,7 +16,7 @@ pytestmark = pytest.mark.stage2c_e2e -def test_large_complete_source_can_qualify_promote_and_read_back(tmp_path): +def test_large_historical_source_can_qualify_promote_and_read_back(tmp_path, original_source): registry, runtime, state = workspace(tmp_path) note = "完整🙂" * 1600 blocks = [f"- [ ] Retained work {index}\n " + format_todo_metadata_line( @@ -32,7 +33,9 @@ def test_large_complete_source_can_qualify_promote_and_read_back(tmp_path): 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") + original = HistoricalShadowWorkspace(registry, runtime, state, + goal="goal-a", original_source=original_source) + added = original.original_add("Captured before upgrading the receiver") 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" From a4af27d59119c2c5c10648c74a9e932b99d16a35 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:53:44 +0800 Subject: [PATCH 17/54] test: recover historical qualification and management races after cutoff Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 6 + .../test_runtime_shadow_bounded_e2e.py | 107 ++++++++++-------- .../test_shadow_fence_caller_parity_e2e.py | 70 ++++++++---- .../test_shadow_management_e2e.py | 51 +++++---- .../test_shadow_management_variant_e2e.py | 11 +- 5 files changed, 151 insertions(+), 94 deletions(-) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 04dc04f984..9ebecd0bf3 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -119,6 +119,12 @@ the current receiver. A source intent stopped before its business commit is retained for import/reconciliation; it cannot authorize a new 2.0 source write. Current consumer fixtures use canonical authority, while registered-peer legacy assignment and original result shapes are checked only as historical receipts. +Bounded qualification, lease-fence caller parity and management interleavings +now also consume pinned historical originals. Current explicit drain owns the +rollback/rebootstrap and corrupt-history races; it cannot count as another +primary mutation or report delivery into a superseded generation. Current +pre-import lease acquisition remains refused, and post-import canonical lease +CAS, transfer, release and caller envelopes keep their original assertions. This repairs the executable recovery boundary; it does not qualify every old shadow-writer ladder or replace the independent installed/natural-trial gates. The backend configuration catalog now describes the same 2.0 creation boundary. diff --git a/tests/control_plane/test_runtime_shadow_bounded_e2e.py b/tests/control_plane/test_runtime_shadow_bounded_e2e.py index 61762edbe8..c55ca1525a 100644 --- a/tests/control_plane/test_runtime_shadow_bounded_e2e.py +++ b/tests/control_plane/test_runtime_shadow_bounded_e2e.py @@ -1,7 +1,7 @@ -"""Real public CLI and independent native TypeScript file-profile qualification. +"""Current qualification and recovery of immutable 1.x file-profile histories. -The production writers, file provider and parsers run unchanged. Tests control -only process scheduling and deliberately edited source bytes. +Only original_cli/original native calls produce retired source mutations. +Inspection, qualification, promotion and recovery always use this checkout. """ from __future__ import annotations @@ -14,6 +14,11 @@ import pytest +from tests.control_plane.historical_shadow_fixture import ( + historical_workspace_factory, # noqa: F401 + original_1x_source, # noqa: F401 +) + from loopx.control_plane.effect_runtime import EffectRuntimeRejected from loopx.control_plane.coordination.coordination_state_contract_generated import ( @@ -46,10 +51,10 @@ def workspace(tmp_path: Path) -> tuple[Path, Path, Path]: return registry, runtime, state -def cli(registry: Path, runtime: Path, *arguments: str, success: bool = True) -> dict: +def cli(registry: Path, runtime: Path, *arguments: str, success: bool = True, source: Path = REPO) -> 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, + "--runtime-root", str(runtime), "--format", "json", *arguments], cwd=source, + env={**os.environ, "PYTHONPATH": str(source)}, capture_output=True, text=True, timeout=45, check=False) assert completed.stdout.strip(), completed.stderr payload = json.loads(completed.stdout) @@ -59,6 +64,11 @@ def cli(registry: Path, runtime: Path, *arguments: str, success: bool = True) -> return payload +def original_cli(source: Path, registry: Path, runtime: Path, *arguments: str) -> dict: + """Produce synthetic original history; never select this source for readback.""" + return cli(registry, runtime, *arguments, source=source) + + def enable(registry: Path) -> dict: value = json.loads(registry.read_text()) value["goals"][0]["coordination"]["runtime_shadow"]["enabled"] = True @@ -66,10 +76,10 @@ def enable(registry: Path) -> dict: return value["goals"][0] -def native(tmp_path: Path, module: str, function: str, request: dict) -> dict: +def native(tmp_path: Path, module: str, function: str, request: dict, *, source: Path = REPO) -> 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())};" + script = (f"import {{ {function} }} from {json.dumps((source / 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)], @@ -87,14 +97,14 @@ def history(tmp_path: Path, runtime: Path) -> list[dict]: return result["proof"]["transactions"] -def acquire_native(tmp_path: Path, registry: Path, runtime: Path, todo_id: str) -> dict: +def acquire_original_native(source: Path, 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"}, - }) + }, source=source) def test_source_snapshot_preserves_inventory_without_projecting_orphan_leases( @@ -124,17 +134,17 @@ def test_source_snapshot_preserves_inventory_without_projecting_orphan_leases( assert history(tmp_path, runtime)[0]["projection"]["leases"] == [] -def test_public_cli_and_independent_native_writer_qualify_one_complete_lineage(tmp_path: Path) -> None: +def test_historical_cli_and_native_writer_lineage_qualifies_in_current_receiver(tmp_path: Path, original_source: 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"], + original_cli(original_source, registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "soft_claim") + archived = original_cli(original_source, registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Previously completed task", "--claimed-by", "agent-a") + original_cli(original_source, 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"], + original_cli(original_source, registry, runtime, "todo", "archive-completed", "--goal-id", "goal-a", "--max-active-done", "0", "--execute") + original_cli(original_source, registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "hard_lease") + first = original_cli(original_source, registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Existing first task") + second = original_cli(original_source, registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Existing second task") + baseline_lease = original_cli(original_source, 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) @@ -148,9 +158,9 @@ def test_public_cli_and_independent_native_writer_qualify_one_complete_lineage(t "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) + acquired = native(tmp_path, "loopx/control_plane/work_items/task_lease_acquire.ts", "executeTaskLeaseAcquire", native_request, source=original_source) 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)] + additions = [original_cli(original_source, 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") @@ -184,7 +194,7 @@ def test_public_cli_and_independent_native_writer_qualify_one_complete_lineage(t def test_reviewed_promotion_survives_restart_and_enables_managed_codex_preflight( - tmp_path: Path, + tmp_path: Path, original_source: Path, ) -> None: registry, runtime, _state = workspace(tmp_path) registry_payload = json.loads(registry.read_text(encoding="utf-8")) @@ -203,7 +213,7 @@ def test_reviewed_promotion_survives_restart_and_enables_managed_codex_preflight json.dumps(registry_payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) - delegated = cli( + delegated = original_cli(original_source, registry, runtime, "todo", @@ -221,7 +231,7 @@ def test_reviewed_promotion_survives_restart_and_enables_managed_codex_preflight "--claimed-by", "agent-b", ) - cli( + original_cli(original_source, registry, runtime, "task-lease", @@ -247,7 +257,7 @@ def test_reviewed_promotion_survives_restart_and_enables_managed_codex_preflight "goal-a", "--execute", ) - cli( + original_cli(original_source, registry, runtime, "todo", @@ -393,13 +403,13 @@ def test_reviewed_promotion_survives_restart_and_enables_managed_codex_preflight 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: +def test_unrecorded_historical_source_change_cannot_become_qualified_after_a_later_write(tmp_path: Path, original_source: 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") + added = original_cli(original_source, 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 @@ -423,23 +433,23 @@ def test_snapshot_changed_between_python_builder_and_native_inspection_is_reject assert result["reason_code"] == "source_changed_retry" -def test_public_handoff_todo_and_atomic_monitor_batch_capture_each_primary_mutation(tmp_path: Path) -> None: +def test_historical_handoff_todo_and_monitor_batch_qualify_each_primary_mutation(tmp_path: Path, original_source: 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") + original_cli(original_source, registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "soft_claim") assert len(history(tmp_path, runtime)) == 2 - cli( + original_cli(original_source, 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", + monitor = original_cli(original_source, 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", + result = original_cli(original_source, 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", @@ -460,7 +470,7 @@ def test_public_handoff_todo_and_atomic_monitor_batch_capture_each_primary_mutat assert qualified["qualification"]["evidence"]["operation_count"] == 4 -def test_disabling_configuration_cannot_cancel_an_active_capture_obligation(tmp_path: Path) -> None: +def test_historical_disabled_configuration_retains_intervening_capture_evidence(tmp_path: Path, original_source: Path) -> None: registry, runtime, state = workspace(tmp_path) enable(registry) cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") @@ -468,8 +478,8 @@ def test_disabling_configuration_cannot_cancel_an_active_capture_obligation(tmp_ 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") + original_cli(original_source, registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "soft_claim") + original_cli(original_source, 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. @@ -481,41 +491,46 @@ def test_disabling_configuration_cannot_cancel_an_active_capture_obligation(tmp_ 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: +def test_current_drain_recovers_historical_native_sequence_after_missing_cursor(tmp_path: Path, original_source: 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") + first = original_cli(original_source, registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "First lease task") + second = original_cli(original_source, 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"], + original_cli(original_source, 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"]) + acquired = acquire_original_native(original_source, 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") + cli(registry, runtime, "authority-shadow", "drain", "--goal-id", "goal-a") 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 + # Explicit drain is not a third primary operation. The default three-operation + # policy must still refuse, while the two witnessed lease receipts qualify. + default = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", success=False) + assert default["qualification"]["qualified"] is False + assert default["qualification"]["evidence"]["operation_count"] == 2 + qualified = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", "--minimum-operations", "2") + assert qualified["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 +def test_historical_committed_primary_cannot_be_relabelled_abandoned_by_native_request(tmp_path: Path, historical_workspace) -> None: 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") + w = historical_workspace(tmp_path) + w.original_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 diff --git a/tests/control_plane/test_shadow_fence_caller_parity_e2e.py b/tests/control_plane/test_shadow_fence_caller_parity_e2e.py index 569c590282..cfb84790d4 100644 --- a/tests/control_plane/test_shadow_fence_caller_parity_e2e.py +++ b/tests/control_plane/test_shadow_fence_caller_parity_e2e.py @@ -4,11 +4,14 @@ 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. +Immutable 1.x produces the imported source records and the historical control +row; all fenced callers and canonical transitions run this checkout. Nothing is +mocked; the fence is engaged by the current TypeScript owner. """ from __future__ import annotations import json +from copy import copy import shutil import sys from collections.abc import Callable, Iterator @@ -17,6 +20,7 @@ import pytest from test_shadow_observable_e2e import Caller from test_shadow_observable_native_e2e import native +from tests.control_plane.historical_shadow_fixture import original_1x_source # noqa: F401 pytestmark = pytest.mark.stage2c_e2e @@ -44,8 +48,9 @@ def _posix_runtime_paths(value: object) -> object: class Workspace: """One seeded goal; rows are executed against it in fixture order.""" - def __init__(self, path: Path, mode: str, name: str) -> None: + def __init__(self, path: Path, mode: str, name: str, original_source: Path) -> None: self.w = Caller(path, mode, name) + self.original_source = original_source self.ids: dict[str, str] = {} self.lease_version = 0 self.lease_owner = "agent-a" @@ -55,6 +60,16 @@ def __init__(self, path: Path, mode: str, name: str) -> None: def call(self, *args: str) -> dict: return self.w.call(*args) + def original_call(self, *args: str) -> dict: + original = copy(self.w) + original.env = {**self.w.env, "PYTHONPATH": str(self.original_source)} + return original.call(*args) + + def original_add(self, text: str) -> str: + result = self.original_call("todo", "add", "--role", "agent", "--text", text) + assert result["ok"] is True, result + return result["todo_id"] + def exit(self) -> int: return int(self.w.rows[-1]["exit"]) @@ -76,9 +91,9 @@ def normalize(self, value: object) -> object: text = text.replace(self.ids[key], "{" + key + "}") return _posix_runtime_paths(json.loads(text)) - def observe(self, args: tuple[str, ...]) -> dict: + def observe(self, args: tuple[str, ...], *, historical: bool = False) -> dict: before, outbox_before = self.w.primary(), self.outbox() - envelope = self.call(*args) + envelope = self.original_call(*args) if historical else self.call(*args) after, outbox_after = self.w.primary(), self.outbox() return { "envelope": self.normalize(envelope), @@ -106,46 +121,50 @@ def seed_and_fence(ws: Workspace, todo_keys: tuple[str, ...]) -> None: 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") +def build_seeded(path: Path, mode: str, name: str, original_source: Path, *, gate: bool) -> Workspace: + ws = Workspace(path, mode, name, original_source) + assert ws.original_call("handoff-mode", "set", "--mode", "hard_lease")["ok"] is True + ws.ids["todo_a"] = ws.original_add("Parity target A") + ws.ids["todo_b"] = ws.original_add("Parity leased B") + ws.ids["todo_c"] = ws.original_add("Parity fresh acquisition C") if gate: - gate_add = ws.call("todo", "add", "--role", "user", "--task-class", "user_gate", "--global-gate", + gate_add = ws.original_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"))} + "--idempotency-key", "parity-lease", "--ttl-seconds", "3600"), historical=True)} lease = ws.control["cli-task_lease_acquire-absent"]["envelope"] assert lease.get("acquired") is True, lease ws.lease_version = int(lease["lease"]["version"]) + rejected = ws.observe(lease_args(ws, "acquire")) + assert rejected["envelope"].get("error_code") == "canonical_authority_required", rejected + assert rejected["effect"] == {"added": [], "removed": [], "changed": []} + assert rejected["outbox_added"] == [] 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) +def build_w3(path: Path, original_source: Path) -> Workspace: + ws = build_seeded(path, "absent", "fence-parity-invalid", original_source, 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) +def build_w4(path: Path, original_source: Path) -> Workspace: + ws = build_seeded(path, "absent", "fence-parity-unreadable", original_source, 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") +def build_w5(path: Path, original_source: Path) -> Workspace: + ws = Workspace(path, "absent", "fence-parity-no-store", original_source) + assert ws.original_call("handoff-mode", "set", "--mode", "hard_lease")["ok"] is True + ws.ids["todo_a"] = ws.original_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", @@ -201,8 +220,8 @@ def row_args(ws: Workspace, caller: str) -> tuple[str, ...]: 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), + "w1": lambda path, source: build_seeded(path, "absent", "fence-parity-engaged", source, gate=True), + "w2": lambda path, source: build_seeded(path, "enabled", "fence-parity-engaged-capture", source, gate=False), "w3": build_w3, "w4": build_w4, "w5": build_w5, @@ -217,14 +236,14 @@ def load_rows() -> list[dict]: @pytest.fixture(scope="module") -def workspaces(tmp_path_factory: pytest.TempPathFactory) -> Iterator[Callable[[str], Workspace]]: +def workspaces(tmp_path_factory: pytest.TempPathFactory, original_source: Path) -> 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)) + built[key] = WORKSPACES[key](tmp_path_factory.mktemp(key), original_source) return built[key] yield get @@ -234,6 +253,9 @@ def get(key: str) -> Workspace: def observe_row(ws: Workspace, row: dict) -> dict: if ws.control and row["id"] in ws.control: + # This literal row characterizes the imported original grant, not a + # supported current source writer (whose refusal is checked in setup). + assert row["fence_state"] == "absent" return ws.control[row["id"]] return ws.observe(row_args(ws, row["caller"])) diff --git a/tests/control_plane/test_shadow_management_e2e.py b/tests/control_plane/test_shadow_management_e2e.py index 7491afec69..e12b597973 100644 --- a/tests/control_plane/test_shadow_management_e2e.py +++ b/tests/control_plane/test_shadow_management_e2e.py @@ -16,6 +16,11 @@ import pytest +from tests.control_plane.historical_shadow_fixture import ( + HistoricalShadowWorkspace, + original_1x_source, # noqa: F401 +) + 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 ( @@ -78,7 +83,7 @@ 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: +def test_public_rollback_preserves_other_goal_and_replays_after_historical_primary_changes(tmp_path: Path, original_source: Path) -> None: registry, runtime = _workspace(tmp_path) first = _bootstrap(registry, runtime) _bootstrap(registry, runtime, "goal-b") @@ -87,8 +92,9 @@ def test_public_rollback_preserves_other_goal_and_replays_after_primary_changes( 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") + original = HistoricalShadowWorkspace(registry, runtime, tmp_path / "goal-a/ACTIVE_GOAL_STATE.md", "goal-a", original_source=original_source) + original.original_cli("todo", "add", "--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"] @@ -104,12 +110,12 @@ def test_public_rollback_preserves_other_goal_and_replays_after_primary_changes( _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 +# Original history is prepared by immutable 1.x before this current drain. +# The pause happens inside the real native batch: the 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 = """ +_DELAYED_DRAIN = """ import json, pathlib, subprocess, sys, time from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter from loopx.cli import main @@ -146,24 +152,26 @@ def delayed(method, request, **kwargs): """ -def _paused_writer(tmp_path: Path, registry: Path, runtime: Path, timing: str) -> tuple[subprocess.Popen, Path, dict]: +def _paused_drain(tmp_path: Path, registry: Path, runtime: Path, timing: str, original_source: Path) -> 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") + original = HistoricalShadowWorkspace(registry, runtime, tmp_path / "goal-a/ACTIVE_GOAL_STATE.md", "goal-a", original_source=original_source) + original.original_crash("before_commit", "todo", "add", "--role", "agent", + "--text", "Transaction across a management boundary", "--claimed-by", "agent-a") + args = _arguments(registry, runtime, "authority-shadow", "drain", "--goal-id", "goal-a") child = subprocess.Popen( - [sys.executable, "-c", _DELAYED_WRITER, str(barrier), str(release), _DRAIN_PHASE[timing], *args], + [sys.executable, "-c", _DELAYED_DRAIN, 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}") + pytest.fail(f"current drain 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}") + pytest.fail(f"current drain 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): @@ -183,17 +191,18 @@ def _release(child: subprocess.Popen, path: Path) -> dict: child.kill() child.communicate() raise - assert child.returncode == 0, f"{stdout}\n{stderr}" - return json.loads(stdout) + payload = json.loads(stdout) + assert child.returncode == (0 if payload["ok"] else 1), f"{stdout}\n{stderr}" + return payload @pytest.mark.parametrize("timing", ["before", "after"]) -def test_late_real_commit_cannot_cross_rollback_and_rebootstrap(tmp_path: Path, timing: str) -> None: +def test_late_real_commit_cannot_cross_rollback_and_rebootstrap(tmp_path: Path, timing: str, original_source: Path) -> 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) + child, release, barrier = _paused_drain(tmp_path, registry, runtime, timing, original_source) try: request = barrier["request"] assert request["schema_version"] == "loopx_shadow_drain_v0" @@ -220,7 +229,8 @@ def test_late_real_commit_cannot_cross_rollback_and_rebootstrap(tmp_path: Path, 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 payload["ok"] is False, payload + assert payload["drained_count"] == 0, payload 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 @@ -239,10 +249,10 @@ def test_late_real_commit_cannot_cross_rollback_and_rebootstrap(tmp_path: Path, child.communicate() -def test_corrupt_history_after_real_commit_cannot_authorize_cursor_cleanup(tmp_path: Path) -> None: +def test_corrupt_history_after_real_commit_cannot_authorize_cursor_cleanup(tmp_path: Path, original_source: Path) -> None: registry, runtime = _workspace(tmp_path) first = _bootstrap(registry, runtime) - child, release, barrier = _paused_writer(tmp_path, registry, runtime, "after") + child, release, barrier = _paused_drain(tmp_path, registry, runtime, "after", original_source) try: # The commit already happened: the durable candidate moved past bootstrap. assert barrier["request"]["goal_id"] == "goal-a" @@ -255,7 +265,8 @@ def test_corrupt_history_after_real_commit_cannot_authorize_cursor_cleanup(tmp_p record["committed"][0]["provider_revision"] = "file:1:" + "0" * 24 candidate.write_text(json.dumps(record)) corrupt = candidate.read_bytes() - _release(child, release) + refused = _release(child, release) + assert refused["ok"] is False and refused["drained_count"] == 0, refused 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() diff --git a/tests/control_plane/test_shadow_management_variant_e2e.py b/tests/control_plane/test_shadow_management_variant_e2e.py index ab045bfd02..a6ea818c5c 100644 --- a/tests/control_plane/test_shadow_management_variant_e2e.py +++ b/tests/control_plane/test_shadow_management_variant_e2e.py @@ -22,7 +22,10 @@ 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.historical_shadow_fixture import ( + HistoricalShadowWorkspace, + original_1x_source, # noqa: F401 +) from tests.control_plane.test_shadow_management_e2e import ( REPO_ROOT, _bootstrap, _candidate, _cli, _workspace, ) @@ -122,14 +125,14 @@ 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, + tmp_path: Path, archive: str, original_source: Path, ) -> 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") + w = HistoricalShadowWorkspace(registry, runtime, state, "goal-a", original_source=original_source) + w.original_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) From 1c6004d38355547ed574e5a5d0d21fa76ad40892 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:14:41 +0800 Subject: [PATCH 18/54] test: move observable callers to canonical authority Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 6 + .../test_shadow_observable_e2e.py | 179 ++++++++++++------ 2 files changed, 124 insertions(+), 61 deletions(-) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 9ebecd0bf3..b5830cfbe3 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -125,6 +125,12 @@ rollback/rebootstrap and corrupt-history races; it cannot count as another primary mutation or report delivery into a superseded generation. Current pre-import lease acquisition remains refused, and post-import canonical lease CAS, transfer, release and caller envelopes keep their original assertions. +Observable CLI consumers now exercise both real File and SQLite authority across +absent, disabled and enabled historical capture settings. Dry-run and rejection +checks include the canonical head and journal; accepted no-op mode intents retain +a receipt, while their exact retry leaves that journal unchanged. Bootstrap +replacement and terminal writes on an unmigrated source refuse before mutation; +current release still settles a genuine pre-upgrade lease. This repairs the executable recovery boundary; it does not qualify every old shadow-writer ladder or replace the independent installed/natural-trial gates. The backend configuration catalog now describes the same 2.0 creation boundary. diff --git a/tests/control_plane/test_shadow_observable_e2e.py b/tests/control_plane/test_shadow_observable_e2e.py index 570ee6d694..61e1f8a1a2 100644 --- a/tests/control_plane/test_shadow_observable_e2e.py +++ b/tests/control_plane/test_shadow_observable_e2e.py @@ -14,6 +14,8 @@ import pytest +from historical_shadow_fixture import HistoricalShadowWorkspace, original_1x_source # noqa: F401 + pytestmark = pytest.mark.stage2c_e2e SOURCE = Path(os.environ.get('LOOPX_SHADOW_COMPARISON_SOURCE', Path(__file__).resolve().parents[2])).resolve() @@ -87,8 +89,60 @@ 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 +class CanonicalCaller(Caller): + """Current supported consumers also compare the real provider, not only prose.""" + + def authority(self) -> dict: + script = """ +const input = JSON.parse(process.argv[1]); +const {openLocalAuthorityStore} = await import(input.module); +const store = await openLocalAuthorityStore(input.root, 'observable'); +process.stdout.write(JSON.stringify({head: await store.loadAuthority(), + history: await store.scanCommitted(null, 100)})); +""" + value = {'module': (SOURCE / 'loopx/control_plane/coordination/local_authority_provider.ts').as_uri(), + 'root': str(self.root)} + return self.invoke(['node', '--no-warnings', '--experimental-strip-types', + '--input-type=module', '-e', script, json.dumps(value)], ['canonical-authority-read']) + + def primary(self) -> dict: + return {**super().primary(), 'canonical-authority': self.authority()} + + def lease(self, todo: str) -> dict: + return next(row for row in self.authority()['head']['head']['leases'] if row['todo_id'] == todo) + + +@pytest.fixture(params=[(mode, provider) for mode in ('absent', 'disabled', 'enabled') + for provider in ('file', 'sqlite')]) +def canonical_caller(tmp_path: Path, request: pytest.FixtureRequest) -> CanonicalCaller: + # Keep the raw caller fixture unchanged: native/fence tests import it to + # exercise retained 1.x originals and the explicit 2.0 cutoff. + mode, provider = request.param + caller = CanonicalCaller(tmp_path, mode, request.node.name) + script = """ +import json,sys +from pathlib import Path +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.coordination.coordination_state_contract import TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, TODO_DOMAIN_RECORD_FIELDS +value=build_todo_runtime_shadow_projection(goal_id='observable', todos=[], handoff_mode='soft_claim') +value['todo_read_model'].update(schema_version=TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, contract_fields=list(TODO_DOMAIN_RECORD_FIELDS)) +print(json.dumps(initialize_canonical_authority(Path(sys.argv[1]), 'observable', value, + state_path=Path(sys.argv[2]), provider=sys.argv[3]))) +""" + caller.env['NODE_OPTIONS'] = caller.env.get('NODE_OPTIONS', '') + ' --experimental-sqlite' + result = caller.invoke([sys.executable, '-c', script, str(caller.root), str(caller.state), provider], + ['initialize-canonical', provider]) + assert result['status'] == 'applied', result + return caller + + +def original(w: Caller, source: Path) -> HistoricalShadowWorkspace: + return HistoricalShadowWorkspace(w.registry, w.root, w.state, goal='observable', original_source=source) + + +def test_todo_argument_intent_and_rejections(canonical_caller: CanonicalCaller) -> None: + w = canonical_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 @@ -122,11 +176,17 @@ def test_todo_argument_intent_and_rejections(caller: Caller) -> None: assert 'Corrected operator intent' in w.state.read_text() -def test_handoff_mode_quiescence(caller: Caller) -> None: - w = caller +def test_handoff_mode_quiescence(canonical_caller: CanonicalCaller) -> None: + w = canonical_caller assert w.call('handoff-mode', 'set', '--mode', 'hard_lease')['changed'] is True + before = w.authority()['head']['head'] + noop = ('handoff-mode', 'set', '--mode', 'hard_lease', '--operation-id', 'unchanged-mode') + assert w.call(*noop)['changed'] is False + assert w.authority()['head']['head'] == before + # A distinct canonical operation owns a receipt even without a mode event; + # retrying that exact operation must leave the full journal unchanged. before = w.primary() - assert w.call('handoff-mode', 'set', '--mode', 'hard_lease')['changed'] is False + assert w.call(*noop)['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') @@ -138,8 +198,8 @@ def test_handoff_mode_quiescence(caller: Caller) -> None: assert malformed['exit'] == 2 and w.primary() == before -def test_lease_arguments_cas_transfer_and_replay(caller: Caller) -> None: - w = caller +def test_lease_arguments_cas_transfer_and_replay(canonical_caller: CanonicalCaller) -> None: + w = canonical_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', @@ -147,12 +207,13 @@ def test_lease_arguments_cas_transfer_and_replay(caller: Caller) -> None: 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()) + persisted = w.lease(todo) + assert not lease_path.exists() assert persisted['owner'] == 'agent-a' and persisted['write_scopes'] == ['src/**'] assert persisted['acquire_ttl_seconds'] == 3600 - before = lease_path.read_bytes() + before = w.authority() assert w.call(*args)['ok'] is True - assert lease_path.read_bytes() == before + assert w.authority() == before before = w.primary() rejected = w.call('task-lease', 'renew', '--todo-id', todo, '--owner', 'agent-b', '--idempotency-key', 'wrong-key', '--expected-version', '999') @@ -164,16 +225,17 @@ def test_lease_arguments_cas_transfer_and_replay(caller: Caller) -> None: '--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' + assert w.lease(todo)['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.lease(todo)['status'] == 'released' + assert not lease_path.exists() 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 +def test_refresh_bound_step_and_reward_owned_prose(canonical_caller: CanonicalCaller) -> None: + w = canonical_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', @@ -204,23 +266,21 @@ def test_refresh_bound_step_and_reward_owned_prose(caller: Caller) -> None: @pytest.mark.parametrize('replacement', ['force', 'missing']) -def test_bootstrap_replacement_preserves_existing_authority(caller: Caller, replacement: str) -> None: +def test_source_bootstrap_replacement_requires_import(caller: Caller, original_source: Path, replacement: str) -> None: w = caller - w.add('Existing canonical state') + assert original(w, original_source).original_add('Existing source state')['ok'] is True 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 + before = w.registry.read_bytes(), w.primary() + preview = w.call(*args, '--dry-run') + assert preview['ok'] is False and preview['error_code'] == 'canonical_authority_required', preview + assert (w.registry.read_bytes(), w.primary()) == before if replacement == 'missing': w.state.unlink() - before = w.registry.read_bytes(), w.state.read_bytes() if w.state.exists() else None + before = w.registry.read_bytes(), w.primary() 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) + assert result['ok'] is False and result['error_code'] == 'canonical_authority_required', result + assert (w.registry.read_bytes(), w.primary()) == before def test_project_registration_and_missing_state_reconstruction(caller: Caller) -> None: @@ -279,9 +339,9 @@ def test_migration_target_and_preview_ownership(caller: Caller) -> None: 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: +def test_operator_reads_and_invalid_selector_have_no_primary_effect(caller: Caller, original_source: Path) -> None: w = caller - todo = w.add('Read policy fixture') + todo = original(w, original_source).original_add('Read policy fixture')['todo_id'] before = w.primary() for command in ['inspect', 'qualify', 'read-candidate']: args = ('--todo-id', todo) if command == 'read-candidate' else () @@ -295,8 +355,8 @@ def test_operator_reads_and_invalid_selector_have_no_primary_effect(caller: Call assert not (w.root / 'authority-shadow').exists() -def test_monitor_successor_retains_caller_routing(caller: Caller) -> None: - w = caller +def test_monitor_successor_retains_caller_routing(canonical_caller: CanonicalCaller) -> None: + w = canonical_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') @@ -312,8 +372,8 @@ def test_monitor_successor_retains_caller_routing(caller: Caller) -> None: 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 +def test_retired_observation_request_preserves_runtime_capture(canonical_caller: CanonicalCaller) -> None: + w = canonical_caller before = w.primary() for extra in ((), ('--execute',)): rejected = w.call('configure-goal', '--local-authority-shadow-file', *extra) @@ -333,8 +393,8 @@ def test_turn_input_rejection_has_no_host_or_primary_effect(caller: Caller) -> N assert w.primary() == before -def test_registry_relative_root_does_not_depend_on_callers_cwd(caller: Caller) -> None: - w = caller +def test_registry_relative_root_does_not_depend_on_callers_cwd(canonical_caller: CanonicalCaller) -> None: + w = canonical_caller registry = json.loads(w.registry.read_bytes()) registry['common_runtime_root'] = 'runtime' w.registry.write_text(json.dumps(registry)) @@ -352,40 +412,37 @@ def test_registry_relative_root_does_not_depend_on_callers_cwd(caller: Caller) - '--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 w.lease(added['todo_id'])['owner'] == 'agent-a' + assert not (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: +def test_legacy_holder_terminal_cutoff_and_original_release(caller: Caller, original_source: Path) -> None: w = caller - w.call('handoff-mode', 'set', '--mode', 'hard_lease') - todo = w.add('Holder and terminal write boundaries') + old = original(w, original_source) + assert old.original_cli('handoff-mode', 'set', '--mode', 'hard_lease')['ok'] is True + todo = old.original_add('Holder and terminal write boundaries')['todo_id'] + acquired = old.original_cli('task-lease', 'acquire', '--todo-id', todo, + '--owner', 'agent-a', '--idempotency-key', 'caller-holder') + assert acquired['ok'] is True, acquired + assert old.original_cli('todo', 'claim', '--todo-id', todo, + '--claimed-by', 'agent-a', '--agent-id', 'agent-a')['ok'] is True 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['ok'] is False, rejected_claim 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 + for proof in ((), ('--task-lease-idempotency-key', 'caller-holder', + '--task-lease-expected-version', str(acquired['lease']['version']))): + rejected = w.call('todo', 'complete', '--todo-id', todo, '--agent-id', 'agent-a', + '--evidence', 'validation://terminal', '--no-follow-up', *proof) + assert rejected['ok'] is False and rejected['error_code'] == 'canonical_authority_required', rejected + assert w.primary() == before + # Current code may settle a real pre-upgrade holder; it must not reopen the + # retired Todo terminal writer or invent a source lease on a fresh Goal. + released = w.call('task-lease', 'release', '--todo-id', todo, '--owner', 'agent-a', + '--idempotency-key', 'caller-holder', '--expected-version', str(acquired['lease']['version'])) + assert released['ok'] is True, released + assert released['lease']['status'] == 'released' + assert w.read(todo)['done'] is False From f2f848acb5e82e6576ee3d3940f4d5611d50863a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:42:29 +0800 Subject: [PATCH 19/54] test: qualify historical recovery and canonical writer races Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 11 +- .../testing/authority_e2e_fixtures.py | 49 +++++- .../testing/authority_e2e_ladder.py | 27 ++-- .../testing/authority_e2e_rows_stage2c2.py | 129 +++++++++++----- .../historical_shadow_fixture.py | 31 +--- .../test_shadow_writer_variant_e2e.py | 144 +++++++++++++++--- .../test_shared_goal_authority_e2e.py | 6 + 7 files changed, 287 insertions(+), 110 deletions(-) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index b5830cfbe3..2fba254d7d 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -131,8 +131,15 @@ checks include the canonical head and journal; accepted no-op mode intents retai a receipt, while their exact retry leaves that journal unchanged. Bootstrap replacement and terminal writes on an unmigrated source refuse before mutation; current release still settles a genuine pre-upgrade lease. -This repairs the executable recovery boundary; it does not qualify every old -shadow-writer ladder or replace the independent installed/natural-trial gates. +The shared-authority recovery ladder now identifies its immutable 1.x producer +revision separately from the current receiver. Current drain survives process +death at commit, cursor and cleanup boundaries on those original entries; source +replacement, rollback, archive/audit and bounded history checks retain their +assertions. Original handoff/acquire races retain recoverable receipts, while +current File/SQLite races independently prove quiescence and policy admission +against one canonical head. Legacy-source overrides refuse before writing. +These are executable recovery and current-consumer checks, not sustained +operation or substitutes for the independent installed/natural-trial gates. The backend configuration catalog now describes the same 2.0 creation boundary. Exact-head review, core-developer trial and release authorization remain open. Retained outbox recovery and source lease release are concrete remaining callers, not grounds for keeping normal diff --git a/loopx/control_plane/testing/authority_e2e_fixtures.py b/loopx/control_plane/testing/authority_e2e_fixtures.py index ffe2283135..6e58cd0b2c 100644 --- a/loopx/control_plane/testing/authority_e2e_fixtures.py +++ b/loopx/control_plane/testing/authority_e2e_fixtures.py @@ -8,15 +8,18 @@ from __future__ import annotations +import io import json import os import shutil import subprocess import sys +import tarfile import time import uuid from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass +from functools import cache from pathlib import Path from typing import Any, Protocol @@ -339,11 +342,46 @@ def build_legacy_migration_source( ) -def cli_env(workspace: CliWorkspace) -> dict[str, str]: - """Child environment: this checkout on ``PYTHONPATH`` and an isolated home.""" +# Public pre-cutoff compatibility input; never an alternate current writer. +ORIGINAL_1X_REVISION = "60d833b13c85135587b725013b247e827663ffb9" + + +@cache +def original_1x_source(root: Path) -> Path: + """Extract one immutable producer into a disposable test directory.""" + result = subprocess.run(["git", "archive", ORIGINAL_1X_REVISION, "loopx"], + cwd=REPO_ROOT, capture_output=True, check=False) + if result.returncode: + raise ProbeError(f"Historical recovery requires Git object {ORIGINAL_1X_REVISION}; fetch repository history") + root.mkdir(parents=True, exist_ok=True) + with tarfile.open(fileobj=io.BytesIO(result.stdout)) as archive: + for member in archive: + path = root / member.name + if not path.resolve().is_relative_to(root.resolve()): + raise ProbeError("historical source archive escapes its scratch root") + if member.isdir(): + path.mkdir(parents=True, exist_ok=True) + elif member.isfile(): + path.parent.mkdir(parents=True, exist_ok=True) + content = archive.extractfile(member) + if content is None: + raise ProbeError("historical source archive member has no content") + with content: + path.write_bytes(content.read()) + else: + raise ProbeError("historical source archive contains a non-file member") + imported = subprocess.run([sys.executable, "-c", "import loopx; print(loopx.__file__)"], + cwd=root, env={**os.environ, "PYTHONPATH": str(root)}, capture_output=True, text=True, check=True) + if Path(imported.stdout.strip()).resolve() != (root / "loopx/__init__.py").resolve(): + raise ProbeError("historical producer imported the wrong source") + return root + + +def cli_env(workspace: CliWorkspace, *, source: Path = REPO_ROOT) -> dict[str, str]: + """Child environment: explicit source (current by default) and an isolated home.""" env = os.environ.copy() - env["PYTHONPATH"] = str(REPO_ROOT) + env["PYTHONPATH"] = str(source) env["HOME"] = str(workspace.home) if os.name == "nt": env["USERPROFILE"] = str(workspace.home) @@ -359,14 +397,15 @@ def run_cli( *args: str, timeout: float = 60, check: bool = True, + source: Path = REPO_ROOT, ) -> JsonObject: """Run one product CLI command and return its JSON object response.""" command = cli_command(workspace, *args) completed = subprocess.run( command, - cwd=REPO_ROOT, - env=cli_env(workspace), + cwd=source, + env=cli_env(workspace, source=source), capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, diff --git a/loopx/control_plane/testing/authority_e2e_ladder.py b/loopx/control_plane/testing/authority_e2e_ladder.py index ff7350c89b..551a2588d1 100644 --- a/loopx/control_plane/testing/authority_e2e_ladder.py +++ b/loopx/control_plane/testing/authority_e2e_ladder.py @@ -32,8 +32,6 @@ RowAssertionError, RowContext, RowOutcome, - acquire_lease, - add_todo, expect, passed, sha256_hex, @@ -43,6 +41,9 @@ from .authority_e2e_rows_stage2c2 import ( capture_workspace, delivered, + original_add_todo, + original_acquire_lease, + original_goal_cli, row_archive_after_leased_completion_parity, row_drain_idempotent, row_event_only_todo_source_holds, @@ -56,13 +57,13 @@ row_sigkill_mid_drain, ) from .authority_e2e_fixtures import ( + ORIGINAL_1X_REVISION, REPO_ROOT, CliOutputError, TS_READBACK_PROBE, JsonObject, node_executable, parse_json_object, - run_cli, tap_summary, ts_readback, unique_goal_id, @@ -250,20 +251,18 @@ def _row_cli_document_decodes_through_ts_store(context: RowContext) -> RowOutcom 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.") + added = original_add_todo(workspace, "Decode this source transaction through the TypeScript store.") todo_id = str(added["todo_id"]) - acquired = acquire_lease( + acquired = original_acquire_lease( workspace, todo_id=todo_id, owner=AGENT_A, idempotency_key="ladder-s1-lease", ) - updated = run_cli( + updated = original_goal_cli( workspace, "todo", "update", - "--goal-id", - workspace.goal_id, "--todo-id", todo_id, "--note", @@ -292,7 +291,9 @@ def _row_cli_document_decodes_through_ts_store(context: RowContext) -> RowOutcom assert isinstance(receipt_probe, dict) receipt = receipt_probe.get("receipt") expect(isinstance(receipt, dict) and receipt.get("status") == "found", "readReceipt must find the first source write") - return passed(cursor="4", scan_pages=scan.get("pages"), transaction_count=4) + return passed(cursor="4", scan_pages=scan.get("pages"), transaction_count=4, + historical_producer_revision=ORIGINAL_1X_REVISION, receiver_source="current_checkout", + normal_legacy_writes_supported=False) # --------------------------------------------------------------------------- @@ -500,7 +501,7 @@ def _row_postgresql_conformance_live(context: RowContext) -> RowOutcome: LadderRow( id="s1.cli_document_decodes_through_ts_store", stage="1", - title="CLI-written candidate documents decode through the TypeScript FileAuthorityStore", + title="Original 1.x CLI documents decode through the current TypeScript FileAuthorityStore", product_path="real_cli", gate="deterministic", posix_only=False, @@ -536,7 +537,7 @@ def _row_postgresql_conformance_live(context: RowContext) -> RowOutcome: 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", + title="Original 1.x writers leave committed outbox records that the current drain delivers once", product_path="real_cli", gate="deterministic", posix_only=False, @@ -563,7 +564,7 @@ def _row_postgresql_conformance_live(context: RowContext) -> RowOutcome: LadderRow( id="s2c2.sigkill_mid_drain", stage="2c2", - title="A SIGKILL inside the inline drain is recovered from exact receipts without a second delivery", + title="A SIGKILL inside current drain recovers original entries without a second delivery", product_path="real_cli", gate="deterministic", posix_only=True, @@ -581,7 +582,7 @@ def _row_postgresql_conformance_live(context: RowContext) -> RowOutcome: LadderRow( id="s2c2.parity_equal", stage="2c2", - title="Sustained interleaving of Python and TypeScript writers keeps every bounded qualification matched", + title="Interleaved original 1.x writer receipts pass current bounded parity qualification", product_path="real_cli", gate="deterministic", posix_only=False, diff --git a/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py b/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py index dfd6df21a4..690eb3d290 100644 --- a/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py +++ b/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py @@ -1,12 +1,15 @@ """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 +Rows retain immutable 1.x producers and drive current recovery 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. +Producer evidence is explicitly historical: it cannot qualify current normal +Markdown writes, canonical adoption, or sustained operation. Two scheduling-only seams exist, both outside every product decision: @@ -31,6 +34,8 @@ from ..coordination import local_authority_shadow_outbox as shadow_outbox from .authority_e2e_fixtures import ( REPO_ROOT, + ORIGINAL_1X_REVISION, + original_1x_source, GoalWorkspace, JsonObject, build_goal_workspace, @@ -45,11 +50,9 @@ AGENT_B, RowContext, RowOutcome, - acquire_lease, - add_todo, expect, lease_version, - passed, + passed as row_passed, ) RUNTIME_SHADOW_CONFIG_SCHEMA = "loopx_coordination_runtime_shadow_config_v0" @@ -196,6 +199,26 @@ def goal_cli(workspace: GoalWorkspace, *args: str, check: bool = True) -> JsonOb return run_cli(workspace, *args, "--goal-id", workspace.goal_id, check=check) +def original_goal_cli(workspace: GoalWorkspace, *args: str, check: bool = True) -> JsonObject: + return run_cli(workspace, *args, "--goal-id", workspace.goal_id, check=check, + source=original_1x_source(workspace.root / "original-1x")) + + +def original_add_todo(workspace: GoalWorkspace, text: str) -> JsonObject: + return original_goal_cli(workspace, "todo", "add", "--role", "agent", "--text", text, + "--task-class", "advancement_task") + + +def original_acquire_lease(workspace: GoalWorkspace, *, todo_id: str, owner: str, idempotency_key: str) -> JsonObject: + return original_goal_cli(workspace, "task-lease", "acquire", "--todo-id", todo_id, + "--owner", owner, "--idempotency-key", idempotency_key, "--ttl-seconds", "120") + + +def passed(**evidence: object) -> RowOutcome: + return row_passed(**evidence, historical_producer_revision=ORIGINAL_1X_REVISION, + receiver_source="current_checkout", normal_legacy_writes_supported=False) + + def bootstrap_capture(workspace: GoalWorkspace) -> JsonObject: """Bootstrap the capture lineage; returns the whole CLI payload.""" @@ -224,6 +247,10 @@ def capture_workspace( set_runtime_shadow(workspace, enabled=True) if bootstrap: bootstrap_capture(workspace) + before = workspace.state_path.read_bytes() + rejected = goal_cli(workspace, "todo", "add", "--role", "agent", "--text", "Fresh legacy intent", check=False) + expect(rejected.get("error_code") == "canonical_authority_required", "current writers must require canonical import") + expect(workspace.state_path.read_bytes() == before, "cutoff must preserve the historical source") return workspace @@ -423,11 +450,29 @@ def hold_drain_lock(workspace: GoalWorkspace) -> Iterator[None]: 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] + if window in DRAIN_CRASH_WINDOWS: + with hold_drain_lock(workspace): + deferred(original_goal_cli(workspace, *args), label="original before drain crash") + args = ("authority-shadow", "drain") + source = REPO_ROOT + else: + source = original_1x_source(workspace.root / "original-1x") + driver = workspace.home / "drain-fault.mts" + module = REPO_ROOT / "loopx/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);" + "await drainShadowOutbox(request,{afterEffect:async observed=>{if(observed===phase){" + "process.stdout.write('BARRIER '+JSON.stringify({native_pid:process.pid})+'\\n');" + "await new Promise(()=>{setInterval(()=>{},1000);});}}});") + worker = CRASH_WORKER.replace("'loopx/control_plane/testing/shadow_drain_fault_process.ts'", repr(str(driver))) + command = [sys.executable, "-c", 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), + cwd=source, + env=cli_env(workspace, source=source), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -464,10 +509,10 @@ def row_outbox_prepared_then_committed_entries(context: RowContext) -> RowOutcom workspace = capture_workspace(context, "ladder-outbox") with hold_drain_lock(workspace): - added = add_todo(workspace, "Prepared and committed while the drain lock is held.") + added = original_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") + acquired = original_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"), @@ -510,7 +555,7 @@ def row_outbox_prepared_then_committed_entries(context: RowContext) -> RowOutcom 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)") + inline = delivered(original_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, @@ -528,7 +573,7 @@ def row_drain_idempotent(context: RowContext) -> RowOutcome: 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}") + deferred(original_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") @@ -560,10 +605,10 @@ def row_drain_idempotent(context: RowContext) -> RowOutcome: 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"), + original_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") + replayed = original_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") @@ -614,7 +659,7 @@ def row_sigkill_between_primary_write_and_drain(context: RowContext) -> RowOutco 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.""" + """Current drain interrupted on original entries recovers from exact receipts without duplicate delivery.""" outcomes: dict[str, JsonObject] = {} for window in DRAIN_CRASH_WINDOWS: @@ -652,9 +697,9 @@ def row_rollback_with_pending_entries(context: RowContext) -> RowOutcome: 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") + delivered(original_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)") + deferred(original_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") @@ -690,8 +735,8 @@ def row_rollback_with_pending_entries(context: RowContext) -> RowOutcome: 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") + after = original_add_todo(workspace, "Primary write after rollback.") + expect(after.get("added") is True, "the immutable original writer 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) @@ -704,7 +749,7 @@ def row_rollback_with_pending_entries(context: RowContext) -> RowOutcome: 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)") + resumed = delivered(original_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") @@ -728,7 +773,7 @@ class _MixedWriterLedger: no_change_writes: int = 0 def cli(self, *args: str) -> JsonObject: - return goal_cli(self.workspace, *args) + return original_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") @@ -753,9 +798,9 @@ def _mixed_writer_cycle(ledger: _MixedWriterLedger, cycle: int) -> None: 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") + first = ledger.mutate("todo add", original_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") + acquired = ledger.mutate("task-lease acquire", original_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)) @@ -785,7 +830,7 @@ def _mixed_writer_cycle(ledger: _MixedWriterLedger, cycle: int) -> None: ), flag="completed", ) - second = ledger.mutate("todo add (second)", add_todo(workspace, f"Cycle {cycle}: replace this work with a successor."), flag="added") + second = ledger.mutate("todo add (second)", original_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)", @@ -795,7 +840,7 @@ def _mixed_writer_cycle(ledger: _MixedWriterLedger, cycle: int) -> None: 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") + held = ledger.mutate("task-lease acquire (second)", original_acquire_lease(workspace, todo_id=second_id, owner=AGENT_A, idempotency_key=key_c), flag="acquired") ledger.mutate( "todo supersede", ledger.cli( @@ -807,7 +852,7 @@ def _mixed_writer_cycle(ledger: _MixedWriterLedger, cycle: int) -> None: ) ledger.mutate( "todo add (verification)", - add_todo(workspace, f"Cycle {cycle}: verify the captured projection."), + original_add_todo(workspace, f"Cycle {cycle}: verify the captured projection."), flag="added", ) @@ -837,11 +882,11 @@ def _assert_bounded_parity(ledger: _MixedWriterLedger, cycle: int, anchor_todo_i def row_parity_equal(context: RowContext) -> RowOutcome: - """Sustained interleaving of Python and TypeScript writers keeps every bounded qualification matched.""" + """Interleaved original writers retain receipts accepted by current bounded qualification.""" 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 = ledger.mutate("todo add (anchor)", original_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): @@ -876,7 +921,7 @@ def _recover_by_rollback_and_rebootstrap(workspace: GoalWorkspace, *, label: str 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}") + delivered(original_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") @@ -887,7 +932,7 @@ def row_parity_divergent_detects_foreign_edit(context: RowContext) -> RowOutcome workspace = capture_workspace(context, "ladder-drift") todo_ids: list[str] = [] for index in range(3): - added = add_todo(workspace, f"Drift baseline {index}.") + added = original_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") @@ -904,7 +949,7 @@ def row_parity_divergent_detects_foreign_edit(context: RowContext) -> RowOutcome 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.") + after = original_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") @@ -936,7 +981,7 @@ def row_event_only_todo_source_holds(context: RowContext) -> RowOutcome: workspace = capture_workspace(context, "ladder-event") todo_ids: list[str] = [] for index in range(3): - added = add_todo(workspace, f"Markdown baseline {index}.") + added = original_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") @@ -1020,7 +1065,7 @@ def row_migration_seeds_and_drains(context: RowContext) -> RowOutcome: 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") + delivered(original_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() @@ -1057,7 +1102,7 @@ def row_migration_seeds_and_drains(context: RowContext) -> RowOutcome: 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") + added = run_cli(target, "todo", "add", "--goal-id", target.goal_id, "--role", "agent", "--text", "Captured in the migrated runtime root.", "--task-class", "advancement_task", source=original_1x_source(context.root / "original-1x")) 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") @@ -1085,7 +1130,7 @@ def row_growth_measurement_gate(context: RowContext) -> RowOutcome: 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}") + delivered(original_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")))) @@ -1125,23 +1170,23 @@ def row_archive_after_leased_completion_parity(context: RowContext) -> RowOutcom """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 = original_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") + acquired = original_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( + completed = original_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 = original_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") + archived = original_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( @@ -1165,10 +1210,10 @@ def row_archive_after_leased_completion_parity(context: RowContext) -> RowOutcom # 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 = original_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( + successor_lease = original_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)") @@ -1179,7 +1224,7 @@ def row_archive_after_leased_completion_parity(context: RowContext) -> RowOutcom ) # 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( + successor_completed = original_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"), diff --git a/tests/control_plane/historical_shadow_fixture.py b/tests/control_plane/historical_shadow_fixture.py index 1a656438b2..eed0abfbbb 100644 --- a/tests/control_plane/historical_shadow_fixture.py +++ b/tests/control_plane/historical_shadow_fixture.py @@ -6,43 +6,20 @@ from __future__ import annotations from dataclasses import dataclass, replace -import io from pathlib import Path -import subprocess -import sys -import tarfile import pytest from tests.control_plane.shadow_e2e_fixture import ShadowWorkspace, workspace -# Last pre-cutoff baseline, retained as a public compatibility input. -ORIGINAL_REVISION = "60d833b13c85135587b725013b247e827663ffb9" -ROOT = Path(__file__).resolve().parents[2] +from loopx.control_plane.testing.authority_e2e_fixtures import ( + original_1x_source as extract_original_source, +) @pytest.fixture(name="original_source", scope="session") def original_1x_source(tmp_path_factory): - root = tmp_path_factory.mktemp("loopx-original-1x") - result = subprocess.run(["git", "archive", ORIGINAL_REVISION, "loopx"], - cwd=ROOT, capture_output=True, check=False) - assert result.returncode == 0, ( - f"Historical recovery requires Git object {ORIGINAL_REVISION}; " - "fetch repository history before running this suite. " + result.stderr.decode()) - with tarfile.open(fileobj=io.BytesIO(result.stdout)) as archive: - for member in archive: - path = root / member.name - assert path.resolve().is_relative_to(root) - if member.isdir(): - path.mkdir(parents=True, exist_ok=True) - else: - assert member.isfile(), member.name - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(archive.extractfile(member).read()) - imported = subprocess.run([sys.executable, "-c", "import loopx; print(loopx.__file__)"], - cwd=root, capture_output=True, text=True, check=True) - assert Path(imported.stdout.strip()).resolve() == root / "loopx/__init__.py" - return root + return extract_original_source(tmp_path_factory.mktemp("loopx-original-1x")) @dataclass diff --git a/tests/control_plane/test_shadow_writer_variant_e2e.py b/tests/control_plane/test_shadow_writer_variant_e2e.py index 569ac4da71..5d529170e0 100644 --- a/tests/control_plane/test_shadow_writer_variant_e2e.py +++ b/tests/control_plane/test_shadow_writer_variant_e2e.py @@ -2,6 +2,7 @@ from __future__ import annotations import json +import hashlib from pathlib import Path import select import subprocess @@ -10,9 +11,10 @@ import pytest from shadow_e2e_fixture import ShadowWorkspace, workspace as make_workspace +from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime +from historical_shadow_fixture import HistoricalShadowWorkspace, original_1x_source # noqa: F401 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 @@ -35,8 +37,8 @@ def public(ws: ShadowWorkspace, *args: str, goal: str | None = None) -> dict: 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 start(args: list[str], *, cwd: Path = REPO) -> subprocess.Popen[str]: + return subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) def barrier(child: subprocess.Popen[str], phase: str) -> None: @@ -65,14 +67,22 @@ def stop(child: subprocess.Popen[str] | None) -> None: import {syncBuiltinESMExports} from 'node:module'; import {once} from 'node:events'; const input = JSON.parse(process.argv[1]); -if (input.wait_lock) { +if (input.wait_lock || input.pause_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); + const handle = await actual(path, flags, ...args); + if (String(path) === input.pause_lock + '.ts-effect.lock' && flags === 'wx') { + const write = handle.writeFile.bind(handle); + handle.writeFile = async (...args) => { + await write(...args); + process.stdout.write('BARRIER native-held\n'); await once(process.stdin, 'data'); + }; + } + return handle; }; syncBuiltinESMExports(); } @@ -84,9 +94,9 @@ def stop(child: subprocess.Popen[str] | None) -> None: """ -def native(module: str, function: str, request: dict, **options: object) -> list[str]: +def native(module: str, function: str, request: dict, *, source: Path = REPO, **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})] + json.dumps({"module": (source / module).as_uri(), "function": function, "request": request, **options})] def fence_request(ws: ShadowWorkspace) -> dict: @@ -112,11 +122,14 @@ def test_other_goal_cannot_write_a_protected_goal_source_via_state_override(tmp_ 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. + # An unbound source no longer grants normal writes, even before capture or a fence. + before = ws.state.read_bytes(), other_state.read_bytes() 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 + "--text", "An unbound shared-state write requires import.", + "--evidence", "Canonical import requirement.", goal="goal-other") + assert legacy["ok"] is False and legacy["added"] is False, legacy + assert legacy["error_code"] == "canonical_authority_required", legacy + assert (ws.state.read_bytes(), other_state.read_bytes()) == before 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", "")) @@ -129,8 +142,8 @@ def test_other_goal_cannot_write_a_protected_goal_source_via_state_override(tmp_ "--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 + # Current admission rejects before any legacy source writer is selected. + assert result["error_code"] == "canonical_authority_required", 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": @@ -187,23 +200,34 @@ def lock(path, *args, **kwargs): def paused_public(ws: ShadowWorkspace, stage: str, *args: str) -> subprocess.Popen[str]: - return start([sys.executable, "-c", PUBLIC_WORKER, stage, *command(ws, *args)[3:]]) + return start([sys.executable, "-c", PUBLIC_WORKER, stage, *command(ws, *args)[3:]], cwd=ws.cwd) @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) +def test_current_drain_recovers_original_handoff_acquire_races( + tmp_path: Path, first: str, original_source: Path, +) -> None: + current = workspace(tmp_path) + ws = HistoricalShadowWorkspace(**vars(current), original_source=original_source).original() + # The original writers establish authentic 1.x receipts. All recovery below + # uses HEAD; no current normal writer is redirected to a compatibility 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" + assert ws.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" + facts = subprocess.run([sys.executable, "-c", + "import json,sys; from pathlib import Path; " + "from loopx.control_plane.work_items.task_lease_acquire_adapter import task_lease_acquire_authority_facts; " + "print(json.dumps(task_lease_acquire_authority_facts(registry_path=Path(sys.argv[1]), " + "goal_id=sys.argv[2], todo_id=sys.argv[3])))", str(ws.registry), ws.goal, todo_id], + cwd=original_source, capture_output=True, text=True, check=True, timeout=30) 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)} + "authority": json.loads(facts.stdout)} 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) + "executeTaskLeaseAcquire", request, source=original_source, **options) handoff_args = ("handoff-mode", "set", "--mode", "soft_claim") lease = handoff = None original = ws.state.read_bytes() @@ -232,8 +256,13 @@ def lease_command(**options: object) -> list[str]: 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") + before = ws.state.read_bytes() + refused = public(current, "todo", "add", "--role", "agent", "--text", "Fresh legacy intent") + assert refused["error_code"] == "canonical_authority_required", refused + assert ws.state.read_bytes() == before + drained = public(current, "authority-shadow", "drain") + assert drained["ok"] is True, drained + inspected = public(current, "coordination-shadow", "inspect") assert inspected["inspection"]["status"] == "matched", inspected assert inspected["inspection"]["cursor"] == "2", inspected finally: @@ -241,6 +270,79 @@ def lease_command(**options: object) -> list[str]: stop(handoff) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("first", ["lease", "handoff"]) +def test_canonical_handoff_acquire_race_uses_one_provider_head(tmp_path, monkeypatch, provider, first): + 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, + ) + from loopx.control_plane.coordination.coordination_state_contract_generated import TASK_LEASE_CANONICAL_ACQUIRE_REQUEST_SCHEMA + from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted + from loopx.control_plane.work_items.task_lease_acquire_adapter import _canonical_lease_authority_facts + + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + ws = workspace(tmp_path) + projection = build_todo_runtime_shadow_projection(goal_id=ws.goal, todos=[], handoff_mode="hard_lease") + projection["todo_read_model"].update(schema_version=TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + contract_fields=list(TODO_DOMAIN_RECORD_FIELDS)) + initialize_canonical_authority(ws.runtime, ws.goal, projection, state_path=ws.state, provider=provider) + todo = ws.add("Race mode change against execution admission.")["todo_id"] + original = ws.state.read_bytes() + def read(): + return read_canonical_todos_if_promoted(runtime_root=ws.runtime, goal_id=ws.goal, include_leases=True) + initial = read() + acquire = {"schema_version": TASK_LEASE_CANONICAL_ACQUIRE_REQUEST_SCHEMA, + "runtime_root": str(ws.runtime), "goal_id": ws.goal, "todo_id": todo, "owner": "agent-a", + "idempotency_key": "canonical-race", "expected_version": 0, "ttl_seconds": 120, "write_scopes": [], + "authority": _canonical_lease_authority_facts(ws.registry, ws.goal)} + handoff = {"schema_version": "loopx_coordination_handoff_mode_set_request_v0", + "runtime_root": str(ws.runtime), "goal_id": ws.goal, "operation_id": "canonical-race-mode", + "requested_mode": "soft_claim", "dry_run": False} + # A real clock makes the just-acquired lease time-active for the mode decision. + from datetime import datetime, timezone + handoff["observed_at"] = datetime.now(timezone.utc).isoformat() + digest = hashlib.sha256(ws.goal.encode()).hexdigest()[:16] + lock = str(ws.runtime / "authority-transition/file-v0" / ("shadow-management-" + digest) / "maintenance") + lease = mode = None + try: + if first == "lease": + lease = start(native("loopx/control_plane/work_items/task_lease_acquire.ts", + "executeTaskLeaseAcquire", acquire, pause_write=True)) + barrier(lease, "native-write") + mode = start(native("loopx/control_plane/coordination/handoff_mode_runtime.ts", + "setLocalHandoffMode", handoff, wait_lock=lock)) + barrier(mode, "native-lock") + assert mode.poll() is None + assert finish(lease, resume=True)["acquired"] is True + refused = finish(mode) + assert refused["reason_code"] == "handoff_mode_not_quiescent", refused + final = read() + assert final["handoff_mode"] == "hard_lease" + assert len(final["leases"]) == 1 and final["leases"][0]["owner"] == "agent-a" + else: + mode = start(native("loopx/control_plane/coordination/handoff_mode_runtime.ts", + "setLocalHandoffMode", handoff, pause_lock=lock)) + barrier(mode, "native-held") + lease = start(native("loopx/control_plane/work_items/task_lease_acquire.ts", + "executeTaskLeaseAcquire", acquire, wait_lock=lock)) + barrier(lease, "native-lock") + assert lease.poll() is None + assert finish(mode, resume=True)["changed"] is True + refused = finish(lease) + assert refused["error_code"] == "handoff_mode_forbids_lease", refused + final = read() + assert final["handoff_mode"] == "soft_claim" and final["leases"] == [] + assert final["todos"] == initial["todos"] + assert int(final["cursor"]) == int(initial["cursor"]) + 1 + assert ws.state.read_bytes() == original + assert not list((ws.runtime / "goals" / ws.goal / "task-leases").glob("*.json")) + finally: + stop(lease) + stop(mode) + + def prepare_rewards(ws: ShadowWorkspace) -> Path: index = ws.runtime / "goals" / ws.goal / "runs" / "index.jsonl" index.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/control_plane/test_shared_goal_authority_e2e.py b/tests/control_plane/test_shared_goal_authority_e2e.py index 9be2ebbe50..6105a5e73d 100644 --- a/tests/control_plane/test_shared_goal_authority_e2e.py +++ b/tests/control_plane/test_shared_goal_authority_e2e.py @@ -110,6 +110,12 @@ def test_ladder_row_passes_or_is_declared_unverified( ) reported = report["rows"][0] assert reported["status"] == "pass", (reported["reason_code"], reported["evidence"]) + if row.stage == "2c2" or row.id == "s1.cli_document_decodes_through_ts_store": + # Historical input and current receiver evidence never certify live writers. + assert reported["evidence"]["historical_producer_revision"] == ladder.ORIGINAL_1X_REVISION + assert reported["evidence"]["receiver_source"] == "current_checkout" + assert reported["evidence"]["normal_legacy_writes_supported"] is False + assert report["summary"] == {"pass": 1, "fail": 0, "unverified": 0, "pending": 0, "executed": 1, "privacy_violations": 0} assert report["exit_policy"]["exit_code"] == 0 From 3549d6529f7c207d6d235a997d1906ece691c545 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:52:41 +0800 Subject: [PATCH 20/54] test: restore original team-plan and promotion recovery coverage Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_new_goal_storage_defaults.py | 2 +- .../control_plane/test_team_plan_authority.py | 45 +++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/tests/control_plane/test_new_goal_storage_defaults.py b/tests/control_plane/test_new_goal_storage_defaults.py index d9dafee837..7c31246b7d 100644 --- a/tests/control_plane/test_new_goal_storage_defaults.py +++ b/tests/control_plane/test_new_goal_storage_defaults.py @@ -809,7 +809,7 @@ def test_machine_editor_is_machine_only_and_configuration_rejects_activation(): @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_historical_target_is_used_by_reviewed_promotion(environment, provider): from tests.control_plane.shadow_e2e_fixture import workspace, seed_legacy_todo_capture - from loopx.capabilities.machine_configuration.goal_storage import initialize_goal_storage_target + from loopx.control_plane.coordination.new_goal_storage_transport import initialize_goal_storage_target _, _, _, project, _ = environment ws = workspace(project, bootstrap=False) data = json.loads(ws.registry.read_text()) diff --git a/tests/control_plane/test_team_plan_authority.py b/tests/control_plane/test_team_plan_authority.py index 03442395a9..a54261bf99 100644 --- a/tests/control_plane/test_team_plan_authority.py +++ b/tests/control_plane/test_team_plan_authority.py @@ -502,12 +502,11 @@ def promotion_race(**kwargs): 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(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") + proposal=history["plan"], + expected_state_fingerprint=history["basis"], basis_agent_id="alpha") assert len(observed) == 2 assert state.read_bytes() == display and not backend.exists() finally: @@ -519,6 +518,7 @@ def promotion_race(**kwargs): def test_legacy_chat_replay_survives_interrupted_shadow_bootstrap(tmp_path, monkeypatch): """A stopped writer must not strand an already-published plan receipt.""" import select + from loopx.control_plane.effect_runtime import EffectRuntimeRejected from loopx.control_plane.work_items import team_plan_adapter 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 @@ -537,15 +537,34 @@ def test_legacy_chat_replay_survives_interrupted_shadow_bootstrap(tmp_path, monk preview = service.preview({"action_kind": "team.plan", "summary": "Assign work", "context": {}, "normalized_parameters": {"goal_id": "goal-a", "plan": _agent_plan()}, "idempotency_key": "lost-original-response"}) - write = team_plan_adapter.write_captured_todo_state - - def lose_response(*args, **kwargs): - write(*args, **kwargs) - raise OSError("response lost after publication") - - with monkeypatch.context() as failed_response: - failed_response.setattr(team_plan_adapter, "write_captured_todo_state", lose_response) - assert service.apply(preview["proposal_id"])["proposal"]["status"] == "failed" + # Only the immutable original producer has the retired capture seam. + # Current Chat recovery below runs with that module physically absent. + from loopx.control_plane.testing.authority_e2e_fixtures import original_1x_source + original_source = original_1x_source(tmp_path / "original-1x") + producer = r""" +import json, sys +from pathlib import Path +import loopx +from loopx.chat_actions import ChatActionService +from loopx.chat_action_store import ChatActionStore +from loopx.control_plane.work_items import team_plan_adapter +source, runtime, registry, proposal = sys.argv[1:] +assert Path(loopx.__file__).resolve() == Path(source) / 'loopx/__init__.py' +service = ChatActionService(store=ChatActionStore(Path(runtime) / 'chat/actions'), + registry_path=Path(registry)) +write = team_plan_adapter.write_captured_todo_state +def lose_response(*args, **kwargs): + write(*args, **kwargs) + raise OSError('response lost after publication') +team_plan_adapter.write_captured_todo_state = lose_response +print(json.dumps(service.apply(proposal))) +""" + produced = subprocess.run([sys.executable, "-c", producer, str(original_source), + str(runtime), str(registry), preview["proposal_id"]], cwd=original_source, + env={**os.environ, "PYTHONPATH": str(original_source)}, + capture_output=True, text=True, timeout=45) + assert produced.returncode == 0, produced.stdout + produced.stderr + assert json.loads(produced.stdout)["proposal"]["status"] == "failed" assert state.read_text().count("loopx:team-plan:") == 1 state.write_text(state.read_text() + "\nLater owner data must survive.\n") original = state.read_bytes() @@ -586,7 +605,7 @@ def lose_response(*args, **kwargs): {"agent_id": "alpha"}, {"expected_state_fingerprint": "different-basis"}, ): - with pytest.raises(team_plan_adapter.EffectRuntimeRejected, match="identity mismatch"): + with pytest.raises(EffectRuntimeRejected, match="identity mismatch"): team_plan_adapter.settle_team_plan(**{ "registry_path": registry, "goal_id": "goal-a", "agent_id": None, "proposal": original_plan, "expected_state_fingerprint": saved["expected_state_fingerprint"], From fc3cffb364ef206ec5c7ae40ed502b17e30d8dd9 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:18:18 +0800 Subject: [PATCH 21/54] test: qualify settlement consumers after canonical cutoff Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_quota_settlement_cli.py | 121 ++++++++++++++---- 1 file changed, 93 insertions(+), 28 deletions(-) diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index 054e25fb9c..87ada23d71 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -12,6 +12,8 @@ import pytest +from historical_shadow_fixture import original_1x_source # noqa: F401 + from loopx.bootstrap_command_pack import build_start_goal_guided_packet from loopx.control_plane.work_items.delivery_outcome import ( PROGRESS_DELIVERY_OUTCOMES, @@ -163,6 +165,7 @@ def _run_cli( runtime: Path, *args: str, cwd: Path = REPO_ROOT, + source: Path = REPO_ROOT, ) -> tuple[int, dict[str, Any]]: result = subprocess.run( [ @@ -177,18 +180,42 @@ def _run_cli( "json", *args, ], - cwd=cwd, + cwd=source if source != REPO_ROOT else cwd, check=False, capture_output=True, text=True, env={ **os.environ, - "PYTHONPATH": str(REPO_ROOT), + "PYTHONPATH": str(source), }, ) + assert result.stdout, result.stderr return result.returncode, json.loads(result.stdout) +def _initialize_settlement_authority(project: Path, runtime: Path) -> None: + """Seed a real canonical File store after synthetic source setup. + + This is consumer setup, not import qualification. Read-only source cases + stay source-backed; normal settlement mutations must use canonical authority. + """ + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + parsed = parse_active_state_todos(state.read_text(), item_limit=None) + initialize_canonical_authority( + runtime, GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, handoff_mode="soft_claim", + todos=parsed["agent_todos"]["items"], + ), + state_path=state, + ) + + def _run_generated_cli( command: str, *, @@ -895,6 +922,7 @@ def test_codex_app_refresh_stages_validated_memory_and_spend_finalizes_hook( separators=(",", ":"), ) + _initialize_settlement_authority(project, runtime) guard_rc, guard = _run_cli( registry_path, runtime, @@ -1094,6 +1122,10 @@ def test_typed_outcome_gap_settles_exact_turn_without_becoming_progress( assert "pending resume_when=resume_at" in unscheduled["error"] assert _classification_count(runtime, "typed_blocker_writeback") == 0 + # Source-only readback cannot invent a retry clock; canonical settlement + # has a separately covered implicit retry contract. Seed authority before + # the supported normal mutation, retaining the negative source check above. + _initialize_settlement_authority(project, runtime) due_at = (datetime.now(timezone.utc) + timedelta(minutes=5)).replace( microsecond=0 ).isoformat().replace("+00:00", "Z") @@ -1210,6 +1242,7 @@ def test_typed_blocked_retry_without_successor_defers_the_only_todo( "--todo-id", TODO_ID, "--turn-instance-id", turn_id, ) + _initialize_settlement_authority(project, runtime) guard_rc, guard = _run_cli( registry_path, runtime, "quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, *binding, "--scan-path", str(project), cwd=project, @@ -1273,6 +1306,7 @@ def test_scheduler_cache_misses_after_todo_state_changes_without_run( "--scan-path", str(project), ) + _initialize_settlement_authority(project, runtime) rc, initial = _run_cli( registry_path, runtime, @@ -1302,7 +1336,13 @@ 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" + rc, readback = _run_cli( + registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", TODO_ID, + ) + assert rc == 0, readback + assert readback["todo"]["status"] == "deferred" assert (index_path.read_bytes() if index_path.exists() else None) == index_before rc, refreshed = _run_cli( @@ -1349,6 +1389,7 @@ def test_scheduler_cache_tracks_todo_changes_beyond_display_limit( "--scan-path", str(project), ) + _initialize_settlement_authority(project, runtime) initial_rc, initial = _run_cli( registry_path, runtime, @@ -2266,6 +2307,7 @@ def test_recovery_does_not_bind_current_replan_and_reenters_same_turn( # to bind an independent successor instead of preserving an obsolete replan. state_path.write_text(state_before_replan, encoding="utf-8") + _initialize_settlement_authority(project, runtime) wait_rc, wait = _run_cli( registry_path, runtime, @@ -2285,9 +2327,14 @@ 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 - ] + read_rc, readback = _run_cli( + registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", TODO_ID, + ) + assert read_rc == 0, readback + assert readback["todo"]["successor_todo_ids"] == [ALTERNATIVE_TODO_ID] + assert readback["todo"]["resume_when"] == f"monitor_changed:{DUE_MONITOR_TODO_ID}" + assert readback["todo"]["resume_ready"] is False current_turn_id = recovery["heartbeat_receipt"]["turn_instance_id"] resumed_rc, resumed = _run_cli( @@ -2466,11 +2513,13 @@ def test_prior_turn_with_several_receipts_recovers_once(tmp_path: Path) -> None: @pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) @pytest.mark.parametrize("hidden_count", [0, 6]) def test_prior_host_closeout_survives_hidden_todo_lifecycle( + original_source: Path, tmp_path: Path, hidden_count: int, provider: str, monkeypatch: pytest.MonkeyPatch, ) -> None: + # The legacy arm writes original 1.x state; every recovery read uses HEAD. from canonical_authority_fixture import ( initialize_canonical_authority, isolate_sqlite_runtime, @@ -2546,6 +2595,7 @@ def test_prior_host_closeout_survives_hidden_todo_lifecycle( "Waiting on independent validation.", "--successor-todo-id", ALTERNATIVE_TODO_ID, + source=original_source if provider == "legacy" else REPO_ROOT, ) assert rc == 0, json.dumps(update, indent=2) turn_id = recovery["heartbeat_receipt"]["turn_instance_id"] @@ -2573,10 +2623,12 @@ def test_prior_host_closeout_survives_hidden_todo_lifecycle( @pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"]) def test_prior_host_closeout_reads_archived_exact_todo( + original_source: Path, tmp_path: Path, provider: str, monkeypatch: pytest.MonkeyPatch, ) -> None: + # The legacy arm writes original 1.x state; every recovery read uses HEAD. from canonical_authority_fixture import ( initialize_canonical_authority, isolate_sqlite_runtime, @@ -2650,6 +2702,7 @@ def test_prior_host_closeout_reads_archived_exact_todo( AGENT_ID, "--next-action-kind", "implement", + source=original_source if provider == "legacy" else REPO_ROOT, ) assert rc == 0, complete rc, archived = _run_cli( @@ -2662,6 +2715,7 @@ def test_prior_host_closeout_reads_archived_exact_todo( "--max-active-done", "0", "--execute", + source=original_source if provider == "legacy" else REPO_ROOT, ) assert rc == 0, archived assert archived["moved_count"] == 1 @@ -2716,6 +2770,7 @@ def test_standard_codex_app_settlement_is_receipted_and_idempotent( TURN_ID, ) + _initialize_settlement_authority(project, runtime) guard_rc, guard = _run_cli( registry_path, runtime, @@ -3152,6 +3207,7 @@ def test_same_turn_identityless_guard_upgrades_and_settles_full_chain( str(project), ) + _initialize_settlement_authority(project, runtime) first_rc, first = _run_cli(registry_path, runtime, *guard_args) upgraded_rc, upgraded = _run_cli( registry_path, @@ -5169,6 +5225,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_settlement_authority(project, runtime) selected_rc, selected = _run_cli( registry_path, runtime, @@ -5267,6 +5324,7 @@ def test_prior_monitor_turn_accepts_exact_committed_receipt( "--available-capability", "external_evidence_poll", ) + _initialize_settlement_authority(project, runtime) prior_rc, prior = _run_cli( registry_path, runtime, @@ -5342,6 +5400,7 @@ def test_prior_monitor_turn_recovery_rejects_wrong_identity_receipt( "--available-capability", "external_evidence_poll", ) + _initialize_settlement_authority(project, runtime) prior_rc, prior = _run_cli( registry_path, runtime, @@ -5450,6 +5509,7 @@ def test_receipt_bound_advancement_turn_records_multiple_due_monitors( "--available-capability", "external_evidence_poll", ) + _initialize_settlement_authority(project, runtime) first_rc, first = _run_cli(registry_path, runtime, *guard_args) assert first_rc == 0, first @@ -6540,6 +6600,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_settlement_authority(project, runtime) capability_args = ( "--available-capability", "network", @@ -6695,12 +6756,14 @@ 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, provider: str, ) -> None: + # Normal completion now requires canonical authority. Historical source + # fallback has its own original-producer/current-reader case below. # The two legal completion phases may commit within one clock tick. Keep # the real CLI, authority and hook journal; freeze only their commit clock. run = subprocess.run @@ -6708,10 +6771,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:]] @@ -6740,26 +6802,25 @@ def frozen_completion_clock(argv, *args, **kwargs): ), encoding="utf-8", ) - if provider != "legacy": - 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 ( + 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) - todos = parse_active_state_todos(state_path.read_text(), item_limit=None) - initialize_canonical_authority( - runtime, GOAL_ID, - build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, todos=todos["agent_todos"]["items"], - handoff_mode="soft_claim", - ), - state_path=state_path, provider=provider, - ) + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + todos = parse_active_state_todos(state_path.read_text(), item_limit=None) + initialize_canonical_authority( + runtime, GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=todos["agent_todos"]["items"], + handoff_mode="soft_claim", + ), + state_path=state_path, provider=provider, + ) binding = ( "--agent-id", AGENT_ID, @@ -7039,6 +7100,7 @@ def test_same_turn_terminal_receipt_replay_preempts_autonomous_replan( TURN_ID, ) + _initialize_settlement_authority(project, runtime) first_rc, first = _run_cli(registry_path, runtime, *guard_args) assert first_rc == 0, first assert first["selected_todo"]["todo_id"] == TODO_ID @@ -7150,6 +7212,7 @@ def test_settled_turn_defers_prior_unsettled_history_to_fresh_turn( TURN_ID, ) + _initialize_settlement_authority(project, runtime) first_rc, first = _run_cli(registry_path, runtime, *guard_args) assert first_rc == 0, first assert first["selected_todo"]["todo_id"] == TODO_ID @@ -7263,6 +7326,7 @@ def test_settled_turn_defers_prior_unsettled_history_to_fresh_turn( def test_legacy_read_only_workspace_mismatch_fails_then_corrects_from_todo_contract( + original_source: Path, tmp_path: Path, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) @@ -7311,6 +7375,7 @@ def test_legacy_read_only_workspace_mismatch_fails_then_corrects_from_todo_contr "--evidence", "legacy read-only characterization validated", "--no-follow-up", + source=original_source, ) assert complete_rc == 0, complete From 27e7bebb4053cbd78a8069c6d5473fc3b9d7fd3f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:24:53 +0800 Subject: [PATCH 22/54] test: preserve original planning API parity after writer retirement Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_native_todo_planning_update.py | 108 +++++++++++++----- .../test_quota_settlement_cli.py | 2 +- 2 files changed, 79 insertions(+), 31 deletions(-) diff --git a/tests/control_plane/test_native_todo_planning_update.py b/tests/control_plane/test_native_todo_planning_update.py index d69fd0822d..c6835be185 100644 --- a/tests/control_plane/test_native_todo_planning_update.py +++ b/tests/control_plane/test_native_todo_planning_update.py @@ -1,13 +1,20 @@ -"""Real CLI and canonical FileAuthorityStore; no production registry or promotion.""" +"""Frozen 1.x producer parity against the current canonical FileAuthorityStore. + +All source-only writes execute the immutable original. Current normal writes +remain canonical-only; no production registry or promotion is involved. +""" from __future__ import annotations import json +import os import shlex import subprocess import sys from pathlib import Path +from functools import partial import pytest +from tests.control_plane.historical_shadow_fixture import original_1x_source # noqa: F401 from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection @@ -45,25 +52,56 @@ def fixture(tmp_path: Path, promoted: bool, provider: str = "file") -> tuple[Pat return registry, state -def update(registry: Path, *args: str, ok: bool = True) -> dict: +def update(registry: Path, *args: str, ok: bool = True, source: Path | None = None) -> 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, - ], capture_output=True, text=True, timeout=45) + ], capture_output=True, text=True, timeout=45, cwd=source, + env={**os.environ, "PYTHONPATH": str(source)} if source is not None else None) result = json.loads(process.stdout) assert (process.returncode == 0) is ok, (result, process.stderr) return result +def original_update_goal_todo(*, source: Path, registry_path: Path, **kwargs) -> dict: + """Run the original public Python API in its own verified source process.""" + program = """ +import json, sys +from pathlib import Path +from loopx.todos import update_goal_todo +request = json.load(sys.stdin) +request['registry_path'] = Path(request['registry_path']) +try: + result = update_goal_todo(**request) +except (ValueError, RuntimeError) as error: + print(json.dumps({'original_error': str(error)})) + raise SystemExit(1) +print(json.dumps(result)) +""" + result = subprocess.run( + [sys.executable, "-c", program], cwd=source, + env={**os.environ, "PYTHONPATH": str(source)}, + input=json.dumps({"registry_path": str(registry_path), **kwargs}), + text=True, capture_output=True, timeout=45, + ) + assert result.stdout, result.stderr + payload = json.loads(result.stdout) + if "original_error" in payload: + raise ValueError(payload["original_error"]) + assert result.returncode == 0, result.stderr + return payload + + 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: +def test_work_requirements_update_preserves_authority_and_supports_explicit_empty(original_source: Path, tmp_path: Path, promoted: bool) -> None: + update_api = update_goal_todo if promoted else partial(original_update_goal_todo, source=original_source) registry, _state = fixture(tmp_path, promoted) before = records(registry) - result = update_goal_todo( + result = update_api( registry_path=registry, goal_id="goal-a", todo_id="todo_target", agent_id="agent-a", action_kind="IMPLEMENT", task_domain="Code.Review", task_repository="git@github.com:example/project.git", @@ -82,7 +120,7 @@ def test_work_requirements_update_preserves_authority_and_supports_explicit_empt assert todo["explore_result_node_refs"] == ["Node:alpha"] assert todo["claimed_by"] == "agent-a" assert records(registry)["todo_other"] == before["todo_other"] - update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id="todo_target", + update_api(registry_path=registry, goal_id="goal-a", todo_id="todo_target", agent_id="agent-a", required_capabilities=[], required_write_scopes=[], target_capabilities=[], explore_result_node_refs=[]) cleared = records(registry)["todo_target"] @@ -99,11 +137,12 @@ 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: +def test_invalid_work_requirement_is_not_silently_dropped(original_source: Path, tmp_path: Path, promoted: bool, intent: dict) -> None: + update_api = update_goal_todo if promoted else partial(original_update_goal_todo, source=original_source) registry, state = fixture(tmp_path, promoted) before = records(registry) with pytest.raises((ValueError, RuntimeError)): - update_goal_todo(registry_path=registry, goal_id="goal-a", todo_id="todo_target", + update_api(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: @@ -111,7 +150,8 @@ def test_invalid_work_requirement_is_not_silently_dropped(tmp_path: Path, promot @pytest.mark.parametrize("promoted", [False, True]) -def test_cli_routes_work_requirements_without_display_dependency(tmp_path: Path, promoted: bool) -> None: +def test_cli_routes_work_requirements_without_display_dependency(original_source: Path, tmp_path: Path, promoted: bool) -> None: + update_cli = partial(update, source=None if promoted else original_source) registry, state = fixture(tmp_path, promoted) args = ["--action-kind", "IMPLEMENT", "--task-domain", "code", "--task-repository", "https://github.com/example/project", @@ -119,17 +159,17 @@ def test_cli_routes_work_requirements_without_display_dependency(tmp_path: Path, "--required-write-scope", "src/**", "--explore-result-node-ref", "Node:alpha"] if promoted: args += ["--update-operation-id", "requirements-cli"] - update(registry, *args, "--dry-run") + update_cli(registry, *args, "--dry-run") if promoted: assert not state.exists() - update(registry, *args) + update_cli(registry, *args) assert records(registry)["todo_target"]["required_capabilities"] == ["code_review"] if promoted: - assert update(registry, *args)["status"] == "replayed" - update(registry, "--clear-explore-result-node-refs") + assert update_cli(registry, *args)["status"] == "replayed" + update_cli(registry, "--clear-explore-result-node-refs") assert not records(registry)["todo_target"].get("explore_result_node_refs") before = records(registry) - update(registry, "--required-capability", "valid", "--required-capability", "bad/token", ok=False) + update_cli(registry, "--required-capability", "valid", "--required-capability", "bad/token", ok=False) assert records(registry) == before @@ -143,20 +183,22 @@ 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( +def test_cli_repository_ports_match_original_and_canonical( + original_source: Path, tmp_path: Path, promoted: bool, remote: str, expected: str, ) -> None: + update_cli = partial(update, source=None if promoted else original_source) registry, _state = fixture(tmp_path, promoted) before = records(registry) args = ["--task-repository", remote] if promoted: args += ["--update-operation-id", "repository-port-cli"] - update(registry, *args) + update_cli(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 update_cli(registry, *args)["status"] == "replayed" assert records(registry) == after @@ -168,10 +210,13 @@ 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( +def test_note_input_semantics_match_original_and_canonical( + original_source: Path, tmp_path: Path, promoted: bool, surface: str, label: str, note: str | None, expected_note: str, ) -> None: + update_cli = partial(update, source=None if promoted else original_source) + update_api = update_goal_todo if promoted else partial(original_update_goal_todo, source=original_source) registry, _state = fixture(tmp_path, promoted) operation_id = f"note-{surface}-{label}" if promoted else None if surface == "cli": @@ -180,9 +225,9 @@ def test_note_input_semantics_match_before_and_after_promotion( args += ["--note", note] if operation_id is not None: args += ["--update-operation-id", operation_id] - update(registry, *args) + update_cli(registry, *args) else: - result = update_goal_todo( + result = update_api( registry_path=registry, goal_id="goal-a", todo_id="todo_target", role="agent", agent_id="agent-a", text="Corrected task", note=note, update_operation_id=operation_id, @@ -204,18 +249,19 @@ def test_promoted_v0_replay_uses_normalized_empty_note_identity(tmp_path: Path) @pytest.mark.parametrize("promoted", [False, True]) -def test_public_cli_nonterminal_wait_update_and_clear(tmp_path: Path, promoted: bool) -> None: +def test_public_cli_nonterminal_wait_update_and_clear(original_source: Path, tmp_path: Path, promoted: bool) -> None: + update_cli = partial(update, source=None if promoted else original_source) registry, state = fixture(tmp_path, promoted) 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"] - update(registry, *args, "--dry-run") + update_cli(registry, *args, "--dry-run") assert records(registry) == before if promoted: assert not state.exists() - update(registry, *args) + update_cli(registry, *args) waiting = records(registry) todo = waiting["todo_target"] assert todo["status"] == "deferred" and todo["done"] is True @@ -226,11 +272,11 @@ def test_public_cli_nonterminal_wait_update_and_clear(tmp_path: Path, promoted: 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 update_cli(registry, *args)["status"] == "replayed" + update_cli(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") + update_cli(registry, "--status", "open", "--clear-resume-when") resumed = records(registry)["todo_target"] assert resumed["status"] == "open" and resumed["done"] is False assert not resumed.get("resume_when") @@ -573,8 +619,10 @@ def test_native_target_lookup_keeps_public_value_error(tmp_path: Path, todo_id: @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( + original_source: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, condition_kind: str, satisfied: bool, ) -> None: + update_cli = partial(update, source=None if provider != "legacy" else original_source) if provider == "sqlite": isolate_sqlite_runtime(tmp_path, monkeypatch) registry, state = fixture(tmp_path, False) @@ -615,14 +663,14 @@ def test_public_cli_external_wait_uses_current_successor_source( if not satisfied: source = list_goal_todos(registry_path=registry, goal_id="goal-a") display = state.read_bytes() if state.exists() else None - rejected = update(registry, *args, ok=False) + rejected = update_cli(registry, *args, ok=False) assert "external-wait successor must be runnable, not resume-gated" in json.dumps(rejected) assert list_goal_todos(registry_path=registry, goal_id="goal-a") == source assert (state.read_bytes() if state.exists() else None) == display return - update(registry, *args, "--dry-run") + update_cli(registry, *args, "--dry-run") assert records(registry) == before - update(registry, *args) + update_cli(registry, *args) after = records(registry) assert after["todo_target"]["status"] == "open" assert after["todo_target"]["resume_ready"] is False @@ -631,5 +679,5 @@ def test_public_cli_external_wait_uses_current_successor_source( 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 update_cli(registry, *args)["status"] == "replayed" assert records(registry) == after diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index 87ada23d71..5896856e2b 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -12,7 +12,7 @@ import pytest -from historical_shadow_fixture import original_1x_source # noqa: F401 +from tests.control_plane.historical_shadow_fixture import original_1x_source # noqa: F401 from loopx.bootstrap_command_pack import build_start_goal_guided_packet from loopx.control_plane.work_items.delivery_outcome import ( From ba4073d058fcb47db307334fe5e42b9a80f0d254 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:17:20 +0800 Subject: [PATCH 23/54] test: qualify packaged task creation with canonical authority Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../goal-workspace-create.mjs | 6 +++++- .../goal-workspace-todo-backend.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/examples/personal-workspace-browser/goal-workspace-create.mjs b/examples/personal-workspace-browser/goal-workspace-create.mjs index e9e62cb641..fbc85077c0 100644 --- a/examples/personal-workspace-browser/goal-workspace-create.mjs +++ b/examples/personal-workspace-browser/goal-workspace-create.mjs @@ -38,7 +38,11 @@ export const goalWorkspaceCreateScenario = { await now.getByRole('button',{name:'添加任务',exact:true}).click(); const task=now.getByRole('textbox',{name:'任务内容',exact:true}); const text='Verify the packaged workspace task creation result'; - await task.fill(text);await now.getByRole('button',{name:'检查任务',exact:true}).click(); + await task.fill(text); + const previewResponse=page.waitForResponse(response=>new URL(response.url()).pathname==='/api/actions/preview'); + await now.getByRole('button',{name:'检查任务',exact:true}).click(); + const preview=await previewResponse; + assert.equal(preview.status(),201,`Task preview failed: ${await preview.text()}`); const drawer=page.locator('.personal-context-drawer');await drawer.waitFor(); assert.equal(requests.length,1);assert.equal(requests[0].normalized_parameters.text,text); await drawer.locator('.personal-primary-action').click(); diff --git a/examples/personal-workspace-browser/goal-workspace-todo-backend.py b/examples/personal-workspace-browser/goal-workspace-todo-backend.py index 508bdf46cf..49ee2cb734 100644 --- a/examples/personal-workspace-browser/goal-workspace-todo-backend.py +++ b/examples/personal-workspace-browser/goal-workspace-todo-backend.py @@ -11,6 +11,11 @@ from loopx.chat_actions import ChatActionService from loopx.chat_server import ChatHTTPServer, ChatRequestHandler from loopx.control_plane.effect_runtime import restart_effect_runtime +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, +) +from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority root = Path(sys.argv[1]).resolve() tempfile.tempdir = str(root) @@ -22,6 +27,17 @@ data["goals"][0].update(id="progress-projection", display_name="Synthetic workspace") registry.write_text(json.dumps(data), encoding="utf-8") runtime = Path(data["common_runtime_root"]) +# This scenario qualifies normal task creation after the 2.0 cutoff. Its +# disposable Goal must already have authority, just like a newly created Goal. +# Cold-source import is qualified separately; this seed is not migration proof. +projection = build_todo_runtime_shadow_projection( + goal_id="progress-projection", todos=[], handoff_mode="soft_claim" +) +projection["todo_read_model"].update( + schema_version=TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + contract_fields=list(TODO_DOMAIN_RECORD_FIELDS), +) +initialize_canonical_authority(runtime, "progress-projection", projection, state_path=state) 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 59fe4d1d6683e8f939cf0a6a95722149a9ba21d2 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:36:04 +0800 Subject: [PATCH 24/54] test: preserve pinned history in isolated mutation controls Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- examples/shared-goal-authority-e2e/mutants.py | 33 ++++++++++++-- .../test_shadow_mutant_history.py | 43 +++++++++++++++++++ .../test_shadow_observable_e2e.py | 2 +- 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 tests/control_plane/test_shadow_mutant_history.py diff --git a/examples/shared-goal-authority-e2e/mutants.py b/examples/shared-goal-authority-e2e/mutants.py index 5661d25221..0f34712dff 100644 --- a/examples/shared-goal-authority-e2e/mutants.py +++ b/examples/shared-goal-authority-e2e/mutants.py @@ -95,7 +95,7 @@ def command(self) -> list[str]: 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]'), + 'tests/control_plane/test_shadow_observable_e2e.py::test_registry_relative_root_does_not_depend_on_callers_cwd[disabled-file]'), 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;')),), @@ -363,16 +363,39 @@ def apply(source: str) -> str: ), "tests/control_plane_ts/legacy_writer_fence_caller_parity.test.ts", pattern="^fence parity: ts-release-engaged$")) -def run(case: Case, directory: Path, log: Path) -> subprocess.CompletedProcess[str]: +def isolated_environment(directory: Path) -> dict[str, str]: environment = {key: value for key, value in os.environ.items() - if not key.startswith(("PYTHON", "LOOPX", "NODE", "COVERAGE"))} + if not key.startswith(("PYTHON", "LOOPX", "NODE", "COVERAGE", "GIT_"))} environment.update(PYTHONPATH=str(directory), PYTHONNOUSERSITE="1") + return environment + + +def run(case: Case, directory: Path, log: Path) -> subprocess.CompletedProcess[str]: + environment = isolated_environment(directory) result = subprocess.run(case.command(), cwd=directory, env=environment, capture_output=True, text=True, timeout=120, check=False) log.write_text(result.stdout + result.stderr, encoding="utf-8") return result +def freeze_historical_git_source(source: Path, frozen: Path) -> str: + """Retain the fixture's pinned producer without borrowing a live Git directory.""" + environment = isolated_environment(frozen) + # Read the pin from the frozen fixture owner, including for --source. Do not + # duplicate it in this runner or redirect every test's Git commands upstream. + revision = subprocess.check_output([ + sys.executable, "-c", + "from loopx.control_plane.testing.authority_e2e_fixtures import ORIGINAL_1X_REVISION; " + "print(ORIGINAL_1X_REVISION)", + ], cwd=frozen, env=environment, text=True, timeout=30).strip() + if re.fullmatch(r"[0-9a-f]{40}", revision) is None: + raise ValueError("historical fixture must pin a complete Git object id") + subprocess.run(["git", "init", "--quiet", str(frozen)], env=environment, check=True, timeout=30) + subprocess.run(["git", "-C", str(frozen), "fetch", "--quiet", "--no-tags", "--depth=1", + str(source), revision], env=environment, check=True, timeout=60) + return revision + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--source", type=Path, default=Path(__file__).resolve().parents[2]) @@ -399,6 +422,10 @@ def main() -> int: shutil.copy2(source / name, frozen / name) if (source / "node_modules").is_dir(): (frozen / "node_modules").symlink_to(source / "node_modules", target_is_directory=True) + historical_revision = freeze_historical_git_source(source, frozen) + (output / "historical-source.json").write_text(json.dumps({ + "original_1x_revision": historical_revision, + }, indent=2) + "\n") manifest = {str(path.relative_to(frozen)): hashlib.sha256(path.read_bytes()).hexdigest() for folder in ("loopx", "tests") for path in (frozen / folder).rglob("*") if path.is_file()} manifest.update({name: hashlib.sha256((frozen / name).read_bytes()).hexdigest() diff --git a/tests/control_plane/test_shadow_mutant_history.py b/tests/control_plane/test_shadow_mutant_history.py new file mode 100644 index 0000000000..93740db814 --- /dev/null +++ b/tests/control_plane/test_shadow_mutant_history.py @@ -0,0 +1,43 @@ +"""Frozen mutation oracles retain only their pinned historical producer.""" +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from test_shadow_mutant_locators import HARNESS + + +@pytest.mark.parametrize("available", [True, False], ids=["pinned-history", "missing-history"]) +def test_frozen_history_is_independent_and_missing_history_fails(tmp_path: Path, monkeypatch, available): + source, frozen = tmp_path / "upstream", tmp_path / "frozen" + source.mkdir() + subprocess.run(["git", "init", "--quiet", str(source)], check=True) + (source / "producer.txt").write_text("original producer bytes\n") + subprocess.run(["git", "-C", str(source), "add", "producer.txt"], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Fixture Author", + "-c", "user.email=fixture@example.invalid", "-c", "commit.gpgsign=false", + "commit", "-qsm", "Original synthetic producer"], check=True) + revision = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() + # A dirty present-day source must not become the historical input. + (source / "producer.txt").write_text("current worktree bytes\n") + owner = frozen / "loopx/control_plane/testing/authority_e2e_fixtures.py" + owner.parent.mkdir(parents=True) + for folder in (owner.parent, owner.parent.parent, owner.parent.parent.parent): + (folder / "__init__.py").touch() + owner.write_text(f"ORIGINAL_1X_REVISION = {revision if available else '0' * 40!r}\n") + # Ambient Git routing must not redirect the frozen repository or its fetch. + monkeypatch.setenv("GIT_DIR", str(tmp_path / "not-a-repository")) + if not available: + with pytest.raises(subprocess.CalledProcessError): + HARNESS.freeze_historical_git_source(source, frozen) + return + assert HARNESS.freeze_historical_git_source(source, frozen) == revision + monkeypatch.delenv("GIT_DIR") + # Verify the object remains available without the origin at all. + source.rename(tmp_path / "detached-upstream") + content = subprocess.check_output(["git", "-C", str(frozen), "show", f"{revision}:producer.txt"]) + assert content == b"original producer bytes\n" + assert not (frozen / "producer.txt").exists(), "history fetch must not replace the frozen working tree" + assert owner.read_text() == f"ORIGINAL_1X_REVISION = {revision!r}\n" diff --git a/tests/control_plane/test_shadow_observable_e2e.py b/tests/control_plane/test_shadow_observable_e2e.py index 61e1f8a1a2..77c1695f8a 100644 --- a/tests/control_plane/test_shadow_observable_e2e.py +++ b/tests/control_plane/test_shadow_observable_e2e.py @@ -113,7 +113,7 @@ def lease(self, todo: str) -> dict: @pytest.fixture(params=[(mode, provider) for mode in ('absent', 'disabled', 'enabled') - for provider in ('file', 'sqlite')]) + for provider in ('file', 'sqlite')], ids=lambda value: '-'.join(value)) def canonical_caller(tmp_path: Path, request: pytest.FixtureRequest) -> CanonicalCaller: # Keep the raw caller fixture unchanged: native/fence tests import it to # exercise retained 1.x originals and the explicit 2.0 cutoff. From 36025d2d09be4c00a7db0d7a1f0375ed3b95fb23 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:53:31 +0800 Subject: [PATCH 25/54] test: qualify Explore writeback on canonical authority Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../capabilities/test_explore_turn_context.py | 18 ++++++ .../test_explore_capture_identity.py | 2 - .../test_explore_durable_links.py | 2 - .../test_explore_result_writeback.py | 59 +++++++++---------- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/tests/capabilities/test_explore_turn_context.py b/tests/capabilities/test_explore_turn_context.py index 6bb54d382a..67ae4fca28 100644 --- a/tests/capabilities/test_explore_turn_context.py +++ b/tests/capabilities/test_explore_turn_context.py @@ -52,6 +52,24 @@ def registry(tmp_path, *, graph=False, planning=False): } ) ) + # Ordinary Explore callers use canonical authority after the 2.0 cutoff. + # This consumer seed is not a qualification of legacy import. + 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.coordination_state_contract import ( + TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, TODO_DOMAIN_RECORD_FIELDS, + ) + + projection = build_todo_runtime_shadow_projection( + goal_id="research", todos=[], handoff_mode="soft_claim" + ) + projection["todo_read_model"].update( + schema_version=TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + contract_fields=list(TODO_DOMAIN_RECORD_FIELDS), + ) + initialize_canonical_authority( + tmp_path / "runtime", "research", projection, state_path=tmp_path / "goal.md" + ) return path diff --git a/tests/control_plane/test_explore_capture_identity.py b/tests/control_plane/test_explore_capture_identity.py index c49b63d7af..149f9293f5 100644 --- a/tests/control_plane/test_explore_capture_identity.py +++ b/tests/control_plane/test_explore_capture_identity.py @@ -27,8 +27,6 @@ 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"), diff --git a/tests/control_plane/test_explore_durable_links.py b/tests/control_plane/test_explore_durable_links.py index 677aba5408..4e6277f828 100644 --- a/tests/control_plane/test_explore_durable_links.py +++ b/tests/control_plane/test_explore_durable_links.py @@ -25,8 +25,6 @@ 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"] initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=rows, handoff_mode="hard_lease"), diff --git a/tests/control_plane/test_explore_result_writeback.py b/tests/control_plane/test_explore_result_writeback.py index 8358110791..8c3a253c74 100644 --- a/tests/control_plane/test_explore_result_writeback.py +++ b/tests/control_plane/test_explore_result_writeback.py @@ -5,6 +5,7 @@ from tests.capabilities.test_explore_turn_context import registry from loopx.control_plane.effect_runtime import EffectRuntimeRejected +from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable from loopx.todos import add_goal_todo, update_goal_todo, list_goal_todos from loopx.capabilities.explore.result_writeback import ( prepare_result_attachment, @@ -125,12 +126,15 @@ def test_todo_additive_owner_preserves_all_links_and_rejects_conflicting_intents exact = list_goal_todos(registry_path=args["registry_path"], goal_id="research", todo_id=args["todo_id"])["todo"] assert exact["explore_result_node_refs"] == ["prior", "prefix-bound", *additions] - with pytest.raises(ValueError): + with pytest.raises(LocalCoordinationAuthorityUnavailable, match="cannot replace and append"): update_goal_todo( **keys, explore_result_node_refs=[], append_explore_result_node_refs=["other"], ) + assert list_goal_todos( + registry_path=args["registry_path"], goal_id="research", todo_id=args["todo_id"] + )["todo"] == exact @pytest.mark.parametrize( @@ -188,13 +192,15 @@ def interrupt(**kwargs): "path-disabled", "path-missing", "path-no-evidence", "path-overflow"]) def test_real_cli_inline_validation_precedes_primary_commit(tmp_path, case): from tests.control_plane.test_quota_settlement_cli import ( - _write_fixture, _run_cli, GOAL_ID, AGENT_ID, TODO_ID, TURN_ID, + _write_fixture, _run_cli, _initialize_settlement_authority, + GOAL_ID, AGENT_ID, TODO_ID, TURN_ID, ) project, runtime, path = _write_fixture(tmp_path) 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)) + _initialize_settlement_authority(project, runtime) 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) @@ -279,32 +285,6 @@ 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 - if source_mode == "linked_path_delta": - from loopx.capabilities.explore.result_log import ( - append_explore_result_event, build_explore_node_event, - ) - append_explore_result_event(explore_result_log_path(runtime, GOAL_ID), - 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"]]) terminal_proof = {} if terminal: from canonical_authority_fixture import initialize_canonical_authority @@ -315,6 +295,13 @@ def test_real_cli_normal_writeback_persists_attachment_and_rejects_conflicting_r from loopx.todos import complete_goal_todo todos = list_goal_todos(registry_path=path, goal_id=GOAL_ID)["todos"] + if source_mode == "linked_path_delta": + # The evidence scope belongs to the task declaration before its + # execution grant; a lease cannot be repurposed by metadata edits. + for todo in todos: + if todo["todo_id"] == TODO_ID: + todo["explore_result_node_refs"] = [ATTACHMENT["node_id"]] + initialize_canonical_authority( runtime, GOAL_ID, @@ -336,6 +323,20 @@ 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"], ) + else: + from tests.control_plane.test_quota_settlement_cli import _initialize_settlement_authority + _initialize_settlement_authority(project, runtime) + if source_mode == "linked_path_delta": + from loopx.capabilities.explore.result_log import ( + append_explore_result_event, build_explore_node_event, + ) + append_explore_result_event(explore_result_log_path(runtime, GOAL_ID), + build_explore_node_event(goal_id=GOAL_ID, node_id=ATTACHMENT["node_id"], + node_kind="question", title=ATTACHMENT["question"], + summary=ATTACHMENT["applicability"], status="open")) + if not terminal: + 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"]]) binding = ( "--goal-id", GOAL_ID, @@ -798,8 +799,6 @@ def run(*command): assert rc == 0, packet return packet - 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"), From 9bf742d0e3e651e992390fc6f97150d68918a0ed Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:09:35 +0800 Subject: [PATCH 26/54] test: qualify installed canonical creation and historical import Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- examples/shared-goal-authority-e2e/README.md | 8 +- .../shared-goal-authority-e2e/correctness.md | 9 + .../shared-goal-authority-e2e/installed.py | 165 +++++++++++++++--- 3 files changed, 154 insertions(+), 28 deletions(-) diff --git a/examples/shared-goal-authority-e2e/README.md b/examples/shared-goal-authority-e2e/README.md index 7b16b0709a..d0bfdec0e8 100644 --- a/examples/shared-goal-authority-e2e/README.md +++ b/examples/shared-goal-authority-e2e/README.md @@ -166,8 +166,12 @@ 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) +boundaries. [Installed-package E2E](installed.py) qualifies both wheel and sdist in empty +virtual environments outside the checkout. Immutable 1.x producers generate +historical source transactions; the installed receiver proves cutoff rejection, +receipt recovery, lease release, explicit backup/import, and preservation of later +canonical writes on import replay. Fresh File and SQLite Goals separately prove +canonical creation and preservation of later writes on creation replay. [Negative controls](mutants.py) deliberately remove correctness checks in disposable source copies. The ten `s2c2.*` ladder rows above exercise the same lifecycle through the diff --git a/examples/shared-goal-authority-e2e/correctness.md b/examples/shared-goal-authority-e2e/correctness.md index dde1310522..f5ba2b1c34 100644 --- a/examples/shared-goal-authority-e2e/correctness.md +++ b/examples/shared-goal-authority-e2e/correctness.md @@ -298,6 +298,15 @@ 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. +Installed qualification requires repository history for the immutable 1.x Git +revision owned by `authority_e2e_fixtures.ORIGINAL_1X_REVISION`. That archived +source creates original transactions only; every recovery, import, canonical +write and readback uses the supplied installed artifact. It cannot restore a +current Markdown writer to satisfy a historical test. Both fresh canonical +providers and source-free import replay must preserve later data and receipts. +The JSON report records artifact hashes and separate producer/receiver provenance; +keep its raw paths and process logs private. + 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 diff --git a/examples/shared-goal-authority-e2e/installed.py b/examples/shared-goal-authority-e2e/installed.py index 924f693b9c..f01008971a 100644 --- a/examples/shared-goal-authority-e2e/installed.py +++ b/examples/shared-goal-authority-e2e/installed.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """Qualify one built wheel or sdist through an isolated installed-package E2E. -This runner never imports the checkout. It installs the supplied artifact into -an empty temporary venv, executes the installed console command from a separate +The current receiver never imports the checkout. Historical writes use only +the immutable 1.x Git object pinned by the installed recovery fixture owner. +It installs the supplied artifact into an empty temporary venv, executes the installed console command from a separate working directory, and retains all process results in the requested JSON report. Run it once per distribution format; every stage is required. """ @@ -16,6 +17,7 @@ import shutil import subprocess import sys +import tarfile import tempfile import time from typing import Any @@ -26,6 +28,8 @@ "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_fallback_dispatch", + "cutoff_import_recovery_preserves_later_writes", + "canonical_file_creation_recovery", "canonical_sqlite_creation_recovery", } @@ -44,7 +48,7 @@ def __init__(self, artifact: Path, workspace: Path, report: dict[str, Any]) -> N self.cwd = workspace / "outside-checkout" self.cwd.mkdir() self.runtime = self.cwd / "runtime" - self.registry = self.cwd / "registry.json" + self.registry = self.cwd / "project/.loopx/registry.json" self.project = self.cwd / "project" self.project.mkdir() self.python = self.venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python") @@ -52,7 +56,7 @@ def __init__(self, artifact: Path, workspace: Path, report: dict[str, Any]) -> N node = shutil.which("node") require(node is not None, "Node.js is required; this qualification cannot skip native execution") self.node = Path(str(node)).absolute() - removed = [key for key in os.environ if key.startswith(("PYTHON", "LOOPX", "NODE", "PIP_")) or key in {"VIRTUAL_ENV", "CONDA_PREFIX"}] + removed = [key for key in os.environ if key.startswith(("PYTHON", "LOOPX", "NODE", "PIP_", "GIT_")) or key in {"VIRTUAL_ENV", "CONDA_PREFIX"}] self.environment = {key: value for key, value in os.environ.items() if key not in removed} self.environment["PATH"] = os.pathsep.join([str(self.python.parent), str(self.node.parent), os.defpath]) self.environment["PYTHONNOUSERSITE"] = "1" @@ -60,26 +64,31 @@ 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]: - 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}") + def cli(self, stage: str, *args: str, historical: bool = False, succeeds: bool = True) -> dict[str, Any]: + command = [str(self.console)] + if historical: + command = [str(self.python), "-I", "-c", + "import sys;sys.path.insert(0,sys.argv.pop(1));" + "from loopx.entrypoint import main;raise SystemExit(main())", str(self.historical)] + result = self.process(stage, [*command, "--registry", str(self.registry), + "--runtime-root", str(self.runtime), "--format", "json", *args], succeeds=succeeds) + require(isinstance(result, dict) and result.get("ok") is succeeds, f"{stage} did not return ok: {result}") return result - def sdk_add(self, stage: str, text: str) -> dict[str, Any]: + def sdk_add(self, stage: str, text: str, *, historical: bool = False) -> dict[str, Any]: code = """ import json, sys from pathlib import Path @@ -88,25 +97,33 @@ def sdk_add(self, stage: str, text: str) -> dict[str, Any]: goal_id=sys.argv[3], role='agent', text=sys.argv[4], task_class='advancement_task', action_kind='analyze') print(json.dumps(value)) """ + if historical: + code = "import sys;sys.path.insert(0,sys.argv.pop(1));" + code value = self.process(stage, [str(self.python), "-I", "-c", code, + *([str(self.historical)] if historical else []), 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 - def read_store(self, stage: str, package: Path) -> dict[str, Any]: - module = package / "control_plane/coordination/file_authority_store.ts" + def read_store(self, stage: str, package: Path, *, canonical: bool = False) -> dict[str, Any]: + module = package / "control_plane/coordination" / ( + "local_authority_provider.ts" if canonical else "file_authority_store.ts") code = """ import {pathToFileURL} from 'node:url'; 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 [modulePath, root, goal, canonical] = process.argv.slice(1); +// Imported modules must not mistake a probe argument for their CLI entrypoint. +process.argv[1] = "installed-authority-readback"; +const api = await import(pathToFileURL(modulePath).href); +const store = canonical === 'true' + ? await api.openLocalAuthorityStore(root, goal, {}, {existingOnly: true}) + : new api.FileAuthorityStore(join(root, 'authority-shadow', '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})); """ - return self.process(stage, [str(self.node), "--no-warnings", "--experimental-strip-types", - "--input-type=module", "-e", code, str(module), str(self.runtime), GOAL]) + return self.process(stage, [str(self.node), "--no-warnings", "--experimental-strip-types", "--experimental-sqlite", + "--input-type=module", "-e", code, str(module), str(self.runtime), GOAL, str(canonical).lower()]) def run(self) -> None: self.process("create_empty_venv", [sys.executable, "-I", "-m", "venv", str(self.venv)], @@ -117,6 +134,7 @@ def run(self) -> None: provenance_code = """ import hashlib, importlib.metadata, importlib.resources, json, pathlib, sys import loopx, loopx.todos, loopx.control_plane.coordination.shadow_management +from loopx.control_plane.testing.authority_e2e_fixtures import ORIGINAL_1X_REVISION package = pathlib.Path(loopx.__file__).resolve().parent resources = {} for relative in ['control_plane/coordination/runtime_shadow.ts', 'control_plane/coordination/shadow_management.ts', @@ -133,6 +151,7 @@ def run(self) -> None: resources[relative] = {'path': str(resource), 'sha256': hashlib.sha256(raw).hexdigest()} print(json.dumps({'executable': sys.executable, 'package': str(package), 'version': importlib.metadata.version('loopx'), 'resources': resources, + 'original_1x_revision': ORIGINAL_1X_REVISION, 'python_modules': [loopx.__file__, loopx.todos.__file__, loopx.control_plane.coordination.shadow_management.__file__]})) """ provenance = self.process("installed_resource_provenance", [str(self.python), "-I", "-c", provenance_code]) @@ -158,9 +177,31 @@ def run(self) -> None: require(fallback.get("kind") == "resolved", "empty fallback declarations must resolve") self.checked("installed_fallback_dispatch") - # This lifecycle qualifies pre-promotion capture. Explicitly retain the - # supported File target-only source instead of inheriting new-Goal - # canonical SQLite creation, which correctly fences legacy capture. + # Archive immutable producer history; the receiver remains the installed artifact. + self.historical = self.workspace / "historical-source" + self.historical.mkdir() + archive_path = self.workspace / "historical.tar" + self.process("archive_original_producer", ["git", "-C", str(Path(__file__).resolve().parents[2]), + "archive", "--format=tar", "--output", str(archive_path), provenance["original_1x_revision"], "loopx"], parse_json=False) + with tarfile.open(archive_path) as archive: + for member in archive: + target = self.historical / member.name + require(target.resolve().is_relative_to(self.historical), "historical archive escaped isolation") + if member.isdir(): + target.mkdir(parents=True, exist_ok=True) + else: + require(member.isfile(), "historical archive contains a non-file member") + content = archive.extractfile(member) + require(content is not None, "historical archive member is empty") + target.parent.mkdir(parents=True, exist_ok=True) + with content: + target.write_bytes(content.read()) + self.report["historical_producer"] = {"revision": provenance["original_1x_revision"], + "archive_sha256": hashlib.sha256(archive_path.read_bytes()).hexdigest()} + + # This lifecycle qualifies historical pre-promotion capture. Explicitly retain the + # File target-only source for the original 1.x producer. The installed + # receiver must reject new legacy writes and require explicit import. source_storage = {"schema_version": "loopx_goal_storage_defaults_v1", "new_goal_provider": "file", "canonical_creation": False, "new_goal_handoff_mode": "hard_lease"} @@ -178,10 +219,11 @@ def run(self) -> None: "target-only source configuration did not read back exactly") created = self.cli("console_project_bootstrap", "bootstrap", "--project", str(self.project), "--goal-id", GOAL, "--objective", "Qualify installed authority transactions.", - "--no-global-sync") + "--no-global-sync", historical=True) require(created.get("storage_target") == { "schema_version": "loopx_new_goal_storage_target_v0", "provider": "file"}, "shadow source was not created with the target-only File contract") + self.source_state = Path(created["state_file"]) selection = created["storage_selection"] require(selection.get("provider") == "file" and selection.get("promotion_performed") is False and "authority_initialized" not in selection, @@ -197,12 +239,12 @@ def run(self) -> None: 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.") + first = self.sdk_add("original_python_todo_add", "First original producer mutation.", historical=True) require(first["coordination_runtime_shadow"]["outcome"] == "delivered", f"first Todo capture failed: {first}") 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") + "--todo-id", first["todo_id"], "--owner", "agent-a", "--idempotency-key", "installed-lease-a", "--ttl-seconds", "600", historical=True) 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.") + second = self.sdk_add("original_python_second_todo_add", "Third original producer mutation.", historical=True) 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, @@ -232,7 +274,7 @@ def run(self) -> None: 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.") + inactive = self.sdk_add("original_python_inactive_primary_write", "Original 1.x primary remains writable after rollback.", historical=True) 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"] @@ -242,6 +284,77 @@ def run(self) -> None: 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"]) + self.import_and_recover(package, first["todo_id"], lease["lease"]["version"], final) + for provider in ("file", "sqlite"): + self.canonical_creation(package, provider) + + def import_and_recover(self, package: Path, todo_id: str, lease_version: int, shadow: dict[str, Any]) -> None: + # Stop capture, settle the original lease, then import with a real backup. + self.cli("retire_capture_for_import", "coordination-shadow", "rollback", "--goal-id", GOAL, + "--provider-revision", shadow["head"]["provider_revision"], "--execute") + state = self.source_state + original = state.read_bytes() + rejected = self.cli("installed_legacy_write_rejected", "todo", "add", "--goal-id", GOAL, + "--role", "agent", "--text", "Must not resume source writes", succeeds=False) + require(rejected.get("error_code") == "canonical_authority_required" and state.read_bytes() == original, + "installed receiver did not reject source mutation without modifying it") + released = self.cli("installed_original_lease_release", "task-lease", "release", "--goal-id", GOAL, + "--todo-id", todo_id, "--owner", "agent-a", "--idempotency-key", "installed-lease-a", + "--expected-version", str(lease_version)) + require(released["released"] is True and released["lease"]["status"] == "released", "original lease was not settled") + backup = self.cli("installed_source_backup", "backup-state", "--project", str(self.project), + "--output-dir", str(self.cwd / "backups"), "--backup-id", "installed-original", + "--current-project-only", "--no-skills", "--no-automations", "--execute") + prepared = self.cli("installed_prepare_import", "coordination-shadow", "prepare-import", "--goal-id", GOAL, + "--operation-id", "installed-import", "--backup-manifest", backup["manifest_path"], + "--provider", "sqlite", "--target-handoff-mode", "hard_lease")["cold_import"] + require(prepared["status"] == "prepared", "cold import was not prepared") + digest = prepared["plan_sha256"] + applied = self.cli("installed_apply_import", "coordination-shadow", "apply-import", "--goal-id", GOAL, + "--operation-id", "installed-import", "--plan-sha256", digest, "--writers-stopped", "--execute")["cold_import"] + require(applied["status"] == "applied" and applied["execution_authority_granted"] is False, + "import did not preserve the execution authority boundary") + require(state.read_bytes() == original, "import rewrote original source") + imported = self.read_store("installed_import_native_readback", package, canonical=True) + expected = {item["todo_id"]: item for item in shadow["head"]["head"]["todos"]} + require(expected == {item["todo_id"]: item for item in imported["head"]["head"]["todos"]}, "import changed original Todo records") + retained_lease = self.cli("installed_imported_lease_readback", "task-lease", "inspect", "--goal-id", GOAL, "--todo-id", todo_id) + require(retained_lease["lease"] == released["lease"], "import changed the released original lease") + later = self.sdk_add("installed_canonical_later_add", "Retain canonical writes after original import.") + before = self.read_store("installed_later_native_readback", package, canonical=True) + require(later["todo_id"] in {item["todo_id"] for item in before["head"]["head"]["todos"]}, "later write missing") + state.unlink() + recovered = self.cli("installed_source_free_import_recovery", "coordination-shadow", "recover-import", "--goal-id", GOAL, + "--operation-id", "installed-import", "--plan-sha256", digest, "--execute")["cold_import"] + require(recovered["status"] == "replayed", "original import was not replayed") + after = self.read_store("installed_recovery_native_readback", package, canonical=True) + require(after == before, "original recovery replaced later canonical data or receipts") + self.checked("cutoff_import_recovery_preserves_later_writes", provider="sqlite", todo_id=later["todo_id"]) + + def canonical_creation(self, package: Path, provider: str) -> None: + self.project = self.cwd / f"new-{provider}" + self.project.mkdir() + self.registry = self.project / ".loopx/registry.json" + self.runtime = self.cwd / f"runtime-{provider}" + config = self.runtime / "machine/configuration.json" + config.parent.mkdir(parents=True) + config.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"}}})) + args = ("bootstrap", "--project", str(self.project), "--goal-id", GOAL, + "--objective", "Qualify fresh canonical creation and retained writes.", "--no-global-sync") + created = self.cli(f"{provider}_canonical_bootstrap", *args) + require(created["storage_selection"]["authority_initialized"] is True + and created["storage_selection"]["provider"] == provider, "fresh Goal was not canonical") + later = self.sdk_add(f"{provider}_canonical_sdk_add", "Preserve work after original creation.") + before = self.read_store(f"{provider}_canonical_native_readback", package, canonical=True) + require(later["todo_id"] in {item["todo_id"] for item in before["head"]["head"]["todos"]}, "new canonical Todo missing") + Path(created["state_file"]).unlink() + self.cli(f"{provider}_creation_recovery", *args) + after = self.read_store(f"{provider}_creation_recovery_readback", package, canonical=True) + require(before == after, "creation recovery replaced later canonical data or receipts") + self.checked(f"canonical_{provider}_creation_recovery", todo_id=later["todo_id"]) + def main(argv: list[str] | None = None) -> int: From 4483d5c874f268567701735dfee839de835574a8 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:34:45 +0800 Subject: [PATCH 27/54] test(monitor): adopt canonical and historical fixture ownership Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_monitor_poll_transaction_recovery.py | 8 +++-- .../test_monitor_poll_unavailable_recovery.py | 15 +++----- .../test_monitor_receipt_writer_isolation.py | 12 ++++--- .../test_monitor_target_key_replay.py | 35 +++++-------------- 4 files changed, 27 insertions(+), 43 deletions(-) diff --git a/tests/control_plane/test_monitor_poll_transaction_recovery.py b/tests/control_plane/test_monitor_poll_transaction_recovery.py index 02f8d0ec7e..32d962b522 100644 --- a/tests/control_plane/test_monitor_poll_transaction_recovery.py +++ b/tests/control_plane/test_monitor_poll_transaction_recovery.py @@ -106,7 +106,10 @@ def _ok(registry: Path, runtime: Path, *args: str, source: Path = REPO) -> dict: return result -def _fixture(tmp_path: Path, source: Path | None = None) -> tuple[Path, Path, Path, str, tuple[str, ...]]: +def _fixture( + tmp_path: Path, source: Path | None = None, *, provider: str = "file", +) -> tuple[Path, Path, Path, str, tuple[str, ...]]: + """Create current authority once, or use an explicit original receipt producer.""" project, runtime = tmp_path / "project", tmp_path / "runtime" state = project / ".codex" / "goals" / GOAL / "ACTIVE_GOAL_STATE.md" registry = project / ".loopx" / "registry.json" @@ -121,7 +124,8 @@ def _fixture(tmp_path: Path, source: Path | None = None) -> tuple[Path, Path, Pa ) if source is None: initialize_canonical_authority(runtime, GOAL, build_todo_runtime_shadow_projection( - goal_id=GOAL, handoff_mode="soft_claim", todos=[]), state_path=state) + goal_id=GOAL, handoff_mode="soft_claim", todos=[]), + state_path=state, provider=provider) monitor = _ok(registry, runtime, "todo", "add", "--goal-id", GOAL, "--role", "agent", "--text", "Observe public synthetic target", "--task-class", "continuous_monitor", "--claimed-by", AGENT, diff --git a/tests/control_plane/test_monitor_poll_unavailable_recovery.py b/tests/control_plane/test_monitor_poll_unavailable_recovery.py index 64aab772fe..ff625fc4b7 100644 --- a/tests/control_plane/test_monitor_poll_unavailable_recovery.py +++ b/tests/control_plane/test_monitor_poll_unavailable_recovery.py @@ -7,11 +7,10 @@ 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 loopx.todos import update_goal_todo from test_monitor_poll_transaction_recovery import _fixture, _cli, _poll_events, _quota_receipt, GOAL, AGENT, REPO _ACTOR_POLL_WRAPPER = """ @@ -48,17 +47,13 @@ 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-release-observer') if not material: args = list(poll) args.remove('--material-change') diff --git a/tests/control_plane/test_monitor_receipt_writer_isolation.py b/tests/control_plane/test_monitor_receipt_writer_isolation.py index 425a06c8d1..2461d67ff9 100644 --- a/tests/control_plane/test_monitor_receipt_writer_isolation.py +++ b/tests/control_plane/test_monitor_receipt_writer_isolation.py @@ -7,6 +7,8 @@ import pytest +from tests.control_plane.historical_shadow_fixture import original_1x_source # noqa: F401 + 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 @@ -26,11 +28,13 @@ @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 + original_source, 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") + # Only the immutable producer creates the historical business receipt. + # All later writes and recovery below use the current isolated distribution. + registry, runtime, state, _monitor, poll = _fixture(tmp_path, original_source) + code, response = _cli(registry, runtime, *poll, fault="after", source=original_source) assert (code, response) == (83, None) original = _business_receipt(state)["writeback"] successor_ids = {item["todo_id"] for item in original["next_todos"]} @@ -107,7 +111,7 @@ def facts(): 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, + result = subprocess.run([sys.executable, "-S", "-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) diff --git a/tests/control_plane/test_monitor_target_key_replay.py b/tests/control_plane/test_monitor_target_key_replay.py index 2335d07c14..e202c66082 100644 --- a/tests/control_plane/test_monitor_target_key_replay.py +++ b/tests/control_plane/test_monitor_target_key_replay.py @@ -9,18 +9,17 @@ 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 loopx.todos import add_goal_todo, update_goal_todo from test_monitor_poll_transaction_recovery import AGENT, GOAL, MONITOR_TARGET, REPO, _cli, _fixture, _poll_events _NULL_TODO_WRAPPER = """ @@ -88,14 +87,11 @@ 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) target_poll = _target_poll(poll, material=material) rc, first = _cli(registry, runtime, *target_poll, fault='quota_commit' if completion == 'interrupted' else None) @@ -149,14 +145,11 @@ 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 +207,7 @@ 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 +269,7 @@ 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 +294,7 @@ 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 +341,11 @@ 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 e15f2ab58126bb53e92c8dfa46d85a079f494a4b Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:53:37 +0800 Subject: [PATCH 28/54] test(authority): bind mutation oracles to current recovery owners Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../shared-goal-authority-e2e/correctness.md | 20 +++++ examples/shared-goal-authority-e2e/mutants.py | 35 ++++---- tests/control_plane/test_shadow_drain_e2e.py | 15 ++++ .../test_shadow_writer_boundaries.py | 86 +++++++++++++++++++ 4 files changed, 136 insertions(+), 20 deletions(-) diff --git a/examples/shared-goal-authority-e2e/correctness.md b/examples/shared-goal-authority-e2e/correctness.md index f5ba2b1c34..14df24d961 100644 --- a/examples/shared-goal-authority-e2e/correctness.md +++ b/examples/shared-goal-authority-e2e/correctness.md @@ -316,6 +316,26 @@ 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. +For the 2.0 cutoff candidate, mutation edits must reach the current owner. +Immutable 1.x producers still supply the original crash and receipt fixtures; +changing an unused current capture writer cannot qualify their recovery. +`prepared_receiver_early_commit` replaces the retired `early_committed` producer +probe and changes the current receiver's abandoned-versus-committed decision. +The original three crash-window assertions remain. `duplicate_mirror` now adds +through current canonical authority and requires the retained shadow history to +remain valid and unchanged. The original 1.x one-write/one-receipt test remains. Cursor regression uses the +existing missing-cursor reconstruction test so the current planner executes. + +The fence removal, fence lock-placement, source lock-placement and cross-Goal +and bound-state-path mutants exercise the retained bootstrap/registration +transaction guard. Its +scheduling-only probes publish real fence bytes or change the registry binding +before state-lock acquisition, then run the real guard and TypeScript check. +These are retained transaction-boundary proofs, not proof that retired ordinary +Markdown Todo writes work. A green locator check only proves that an edit still +applies; each selected control must pass and each mutation must fail a semantic +assertion. An exception caused by a missing field or import does not qualify. + 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, diff --git a/examples/shared-goal-authority-e2e/mutants.py b/examples/shared-goal-authority-e2e/mutants.py index 0f34712dff..b933f38e4e 100644 --- a/examples/shared-goal-authority-e2e/mutants.py +++ b/examples/shared-goal-authority-e2e/mutants.py @@ -143,10 +143,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]", @@ -155,14 +151,12 @@ def command(self) -> list[str]: 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_missing_cursor_recovers_only_from_complete_verified_history"), + # The 1.x producer is immutable. Mutate the current receiver's decision, + # not an unused present-day copy of the retired capture writer. + Case("prepared_receiver_early_commit", ((COORDINATION + "shadow_entry_evidence.ts", replacement( + ' entry.resolution = "abandoned";', + ' entry.resolution = "committed_proven_by_readback";')),), "tests/control_plane/test_shadow_drain_e2e.py::test_primary_sigkill_preserves_complete_bytes_and_proves_before_marker[before_replace]"), ]) @@ -223,7 +217,7 @@ 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") + and node.name == "require_legacy_todo_write_allowed") 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] @@ -236,13 +230,14 @@ def apply(source: str) -> str: 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:] + lifted = "".join(" " + line for line in segment.splitlines(keepends=True)) + lifted = lifted.replace("runtime_root=runtime_root", "runtime_root=resolved_runtime_root") + return text[:start] + " if not dry_run:\n" + lifted + 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" +FENCE_TEST = WRITER_TEST + "test_retained_transaction_rechecks_fence_after_lock_acquisition" CASES.extend([ Case("management_result_binding", ((COORDINATION + "shadow_management.ts", replacement( " await validateReplayResult(request, manifest, prior.result, state);", @@ -251,7 +246,7 @@ def apply(source: str) -> str: 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]"), + WRITER_TEST + "test_retained_transaction_rechecks_source_binding_after_lock_acquisition"), Case("cleanup_hides_verified_commit", ((COORDINATION + "shadow_drain.ts", replacement( " if (plan.view !== null && typeof plan.view === \"object\") observe(plan.view as JsonObject);\n", "")),), @@ -268,7 +263,7 @@ def apply(source: str) -> str: 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"), + WRITER_TEST + "test_retained_transaction_rechecks_source_binding_after_lock_acquisition"), 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.")),), @@ -285,7 +280,7 @@ def apply(source: str) -> str: 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"), + WRITER_TEST + "test_retained_transaction_rejects_an_unbound_state_path"), Case("bootstrap_unregistered_root", ((COORDINATION + "runtime_shadow.ts", replacement( "if (!await sameSourceRuntimeRoot(registeredRoot, request.runtime_root))", "if (false)")),), @@ -331,7 +326,7 @@ def apply(source: str) -> str: ''' 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")) +), "tests/control_plane/test_shadow_drain_e2e.py::test_current_canonical_mutation_does_not_mirror_retained_shadow")) # Fenced sibling-caller parity: each mutant is caught by exactly one parity row. # Promoted CLI acquisition now succeeds through canonical authority. Probe the diff --git a/tests/control_plane/test_shadow_drain_e2e.py b/tests/control_plane/test_shadow_drain_e2e.py index 982aacda86..f906191eff 100644 --- a/tests/control_plane/test_shadow_drain_e2e.py +++ b/tests/control_plane/test_shadow_drain_e2e.py @@ -91,6 +91,21 @@ def test_public_mutation_has_no_second_snapshot_mirror(historical_workspace, tmp assert page["transactions"][1]["receipts"][0]["write_class"] == "todo_add" +def test_current_canonical_mutation_does_not_mirror_retained_shadow(historical_workspace, tmp_path): + from canonical_authority_fixture import initialize_canonical_authority + w = historical_workspace(tmp_path) + w.original_add("Historical operation remains retained") + w.drain() + retained = history(w) + initialize_canonical_authority(w.runtime, w.goal, retained["head"], state_path=w.state) + added = w.add("Current canonical operation has one authority") + assert added["added"] is True, added + assert w.cli("todo", "list", "--todo-id", added["todo_id"])["todo"]["text"] == "Current canonical operation has one authority" + observed = history(w) + assert observed.get("proof", {}).get("transactions") == retained["proof"]["transactions"], observed + assert "Current canonical operation has one authority" in w.state.read_text() + + @pytest.mark.parametrize( "attack", [ diff --git a/tests/control_plane/test_shadow_writer_boundaries.py b/tests/control_plane/test_shadow_writer_boundaries.py index eb8eaa3fad..9ca6a93ea9 100644 --- a/tests/control_plane/test_shadow_writer_boundaries.py +++ b/tests/control_plane/test_shadow_writer_boundaries.py @@ -608,3 +608,89 @@ def test_public_refresh_step_does_not_write_owned_paragraph(tmp_path: Path) -> N assert state.read_bytes() == before assert "Compare the current evidence." not in state.read_text() assert not (root / "authority-shadow").exists() + + +def test_retained_transaction_rechecks_fence_after_lock_acquisition(tmp_path, monkeypatch): + """Bootstrap/registration still use this transaction; normal Todo writing is retired.""" + from contextlib import contextmanager + from loopx.control_plane.coordination import legacy_writer_fence as owner + from test_shadow_writer_variant_e2e import fence_command + from shadow_e2e_fixture import ShadowWorkspace + + registry, state, runtime = fixture(tmp_path) + ws = ShadowWorkspace(registry, runtime, state, goal=GOAL) + result = subprocess.run(fence_command(ws), cwd=REPO, capture_output=True, text=True, check=True) + assert json.loads(result.stdout)["status"] == "applied" + fence = owner.legacy_coordination_writer_fence_path(runtime_root=runtime, goal_id=GOAL) + fence_bytes = fence.read_bytes() + fence.unlink() + lock = owner.exclusive_cross_runtime_file_lock + acquired = [] + + @contextmanager + def raced_lock(path, *args, **kwargs): + # Scheduling seam only: another holder published a real fence before S. + if path == state: + fence.write_bytes(fence_bytes) + with lock(path, *args, **kwargs) as held: + acquired.append(path) + yield held + + monkeypatch.setattr(owner, "exclusive_cross_runtime_file_lock", raced_lock) + before = state.read_bytes() + with pytest.raises(owner.LegacyCoordinationWriterFenced) as error: + with owner.legacy_todo_write_transaction(registry, GOAL, state, None, "retained-recovery", False): + pass # The owning guard must raise before the transaction body. + assert error.value.code == "legacy_coordination_writer_fenced" + assert acquired[-1] == state + assert state.read_bytes() == before + + +def test_retained_transaction_rechecks_source_binding_after_lock_acquisition(tmp_path, monkeypatch): + from contextlib import contextmanager + from loopx.control_plane.coordination import legacy_writer_fence as owner + from loopx.control_plane.coordination.shadow_management import ShadowManagementError + from shadow_e2e_fixture import workspace + + ws = workspace(tmp_path) + # Bootstrap owns this real source path; remove frontmatter to test registry ownership. + ws.state.write_text(ws.state.read_text().replace(f"goal_id: {ws.goal}\n", "")) + registry = json.loads(ws.registry.read_text()) + protected = registry["goals"][0]["state_file"] + registry["goals"][0]["state_file"] = "elsewhere.md" + ws.registry.write_text(json.dumps(registry)) + lock = owner.exclusive_cross_runtime_file_lock + acquired = [] + + @contextmanager + def raced_lock(path, *args, **kwargs): + if path == ws.state: + registry["goals"][0]["state_file"] = protected + ws.registry.write_text(json.dumps(registry)) + with lock(path, *args, **kwargs) as held: + acquired.append(path) + yield held + + monkeypatch.setattr(owner, "exclusive_cross_runtime_file_lock", raced_lock) + before = ws.state.read_bytes() + with pytest.raises(ShadowManagementError) as error: + with owner.legacy_todo_write_transaction(ws.registry, "other-goal", ws.state, None, "retained-recovery", False): + pass # The owning guard must raise before the transaction body. + assert error.value.code == "shadow_source_goal_mismatch" + assert acquired[-1] == ws.state + assert ws.state.read_bytes() == before + + +def test_retained_transaction_rejects_an_unbound_state_path(tmp_path): + from loopx.control_plane.coordination.shadow_management import ShadowManagementError + from shadow_e2e_fixture import workspace + + ws = workspace(tmp_path) + alternate = tmp_path / "alternate.md" + original = ws.state.read_bytes() + alternate.write_bytes(original) + with pytest.raises(ShadowManagementError) as error: + with legacy_todo_write_transaction(ws.registry, ws.goal, alternate, None, "retained-recovery", False): + pass + assert error.value.code == "shadow_source_state_path_mismatch" + assert ws.state.read_bytes() == alternate.read_bytes() == original From e66269be0cac0a0c7273f874fedf6e069380fa02 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:23:11 +0800 Subject: [PATCH 29/54] fix: reject canonical Todo completion policy failures Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../coordination/todo_terminal_lifecycle.ts | 7 +++ tests/control_plane/test_completion_policy.py | 61 +++++++++++++++++-- .../authority_store_conformance.ts | 23 +++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index 3380de5bd5..74139adedc 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -1478,6 +1478,13 @@ export async function executeCoordinationTodoTerminalLifecycle( cursor: head.cursor, }; } + if (completion.decision === "policy_reject") { + const failure = completion.completion_policy_failure; + return terminalFailure( + failure.kind, failure.summary, + {completion_policy_failure: failure}, "decision_rejection", + ); + } if (completion.decision === "reject") { return terminalFailure( completion.failure.kind, diff --git a/tests/control_plane/test_completion_policy.py b/tests/control_plane/test_completion_policy.py index b6472ab877..8945425f85 100644 --- a/tests/control_plane/test_completion_policy.py +++ b/tests/control_plane/test_completion_policy.py @@ -4,6 +4,11 @@ from dataclasses import fields from pathlib import Path +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 loopx.control_plane.todos.completion_policy import CompletionPolicy from loopx.control_plane.testing.canary_harness import ( run_json_cli, @@ -20,9 +25,9 @@ def test_completion_policy_does_not_duplicate_runtime_model_authority() -> None: assert "agent_model" not in {field.name for field in fields(CompletionPolicy)} -def test_completion_rejects_unknown_runtime_model_before_write( - tmp_path: Path, -) -> None: +@pytest.fixture(params=["file", "sqlite"]) +def canonical_policy_goal(tmp_path: Path, monkeypatch, request): + isolate_sqlite_runtime(tmp_path, monkeypatch) project = tmp_path / "project" runtime = tmp_path / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -42,6 +47,11 @@ def test_completion_rejects_unknown_runtime_model_before_write( registered_agents=[AGENT_ID, "codex-main-control"], quota_allowed_slots=None, ) + initialize_canonical_authority( + runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=[], handoff_mode="soft_claim"), + state_path=state_file, provider=request.param, + ) source = run_json_cli( "todo", "add", @@ -55,6 +65,11 @@ def test_completion_rejects_unknown_runtime_model_before_write( AGENT_ID, registry_path=registry_path, ) + return registry_path, state_file, source["todo_id"] + + +def test_completion_rejects_unknown_runtime_model_before_write(canonical_policy_goal) -> None: + registry_path, state_file, todo_id = canonical_policy_goal 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") @@ -66,7 +81,7 @@ def test_completion_rejects_unknown_runtime_model_before_write( "--goal-id", GOAL_ID, "--todo-id", - source["todo_id"], + todo_id, "--claimed-by", AGENT_ID, "--agent-id", @@ -80,3 +95,41 @@ def test_completion_rejects_unknown_runtime_model_before_write( assert returncode == 1 assert "coordination.agent_model must be peer_v1" in result["error"] assert state_file.read_text(encoding="utf-8") == before + + +def test_self_merge_policy_rejection_preserves_authority_and_retry(canonical_policy_goal) -> None: + registry_path, state_file, todo_id = canonical_policy_goal + + def listing(): + return run_json_cli("todo", "list", "--goal-id", GOAL_ID, registry_path=registry_path) + + def complete(evidence): + return run_json_cli_result( + "todo", "complete", "--goal-id", GOAL_ID, "--todo-id", todo_id, + "--agent-id", AGENT_ID, "--claimed-by", AGENT_ID, + "--self-merged", "--evidence", evidence, "--no-follow-up", + "--note", "No further work remains.", + registry_path=registry_path, + ) + + before = listing() + display = state_file.read_bytes() + # Python whitespace includes NEL; BOM remains non-whitespace in this contract. + for evidence in ("", "\u0085", " \t\n"): + returncode, result = complete(evidence) + assert returncode == 1, result + assert "--self-merged requires --evidence" in result["error"] + after = listing() + assert after["authority_read"] == before["authority_read"] + assert after["todos"] == before["todos"] + assert state_file.read_bytes() == display + + returncode, accepted = complete("\ufeff") + assert returncode == 0, accepted + assert accepted["completed"] is True + assert accepted["completion_policy"]["self_merged"] is True + committed = listing() + returncode, replay = complete("\ufeff") + assert returncode == 0, replay + assert replay["idempotent_replay"] is True + assert listing()["authority_read"] == committed["authority_read"] diff --git a/tests/control_plane_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts index 4b6b752a0a..b9c623dec1 100644 --- a/tests/control_plane_ts/authority_store_conformance.ts +++ b/tests/control_plane_ts/authority_store_conformance.ts @@ -761,6 +761,29 @@ export function registerAuthorityStoreConformance( local_path_captured: false as const, }, }; + // A policy rejection must not commit the Todo, create successors, release + // its lease or consume the operation identity. Exercise the production + // boundary on every real provider, then reuse the identity below. + const beforePolicyRejection = await store.loadAuthority(); + for (const successorIntents of [[], commitRequest.successor_intents]) { + for (const evidence of ["", "\u0085", " \t\n"]) { + const rejected = await executeCoordinationTodoTerminalLifecycle(store, { + ...commitRequest, + successor_intents: successorIntents, + requested_no_followup: successorIntents.length === 0, + completion_policy_request: { + ...commitRequest.completion_policy_request, self_merged: true, evidence, + next_agent_todo: successorIntents.length === 0 ? null : successorIntent.text, + }, + }); + assert.equal(rejected.status, "failed", JSON.stringify(rejected)); + assert.equal(rejected.changed, false); + assert.equal(rejected.failure_kind, "decision_rejection"); + assert.equal(rejected.reason_code, "completion_policy_rejected"); + assert.match(String(rejected.reason), /--self-merged requires --evidence/); + assert.deepEqual(await store.loadAuthority(), beforePolicyRejection); + } + } const dangling = await executeCoordinationTodoTerminalLifecycle(store, { ...commitRequest, operation_identity: {kind: "explicit" as const, operation_id: "complete-dangling-successor"}, From ee9ebea845817218489355f5988a174fc3cc0c85 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:41:56 +0800 Subject: [PATCH 30/54] fix(chat): bind reviewed Todo identity to canonical operation Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- examples/loopx-chat-actions-smoke.py | 18 ++++-- examples/loopx-chat-contract-smoke.py | 9 ++- examples/loopx-chat-server-smoke.py | 8 ++- loopx/chat.py | 56 +++++++++++++------ .../canonical_authority_fixture.py | 22 ++++++++ .../test_chat_reviewed_create.py | 34 ++++++++++- 6 files changed, 120 insertions(+), 27 deletions(-) diff --git a/examples/loopx-chat-actions-smoke.py b/examples/loopx-chat-actions-smoke.py index 14d35d35ae..a9189956fe 100644 --- a/examples/loopx-chat-actions-smoke.py +++ b/examples/loopx-chat-actions-smoke.py @@ -24,6 +24,8 @@ from loopx.chat_store import ChatSessionStore # noqa: E402 from loopx.history import collect_history # noqa: E402 +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 + def preview_request( *, @@ -195,8 +197,9 @@ def write_registry_fixture(root: Path) -> tuple[Path, Path]: return registry, state -def assert_http_action_api(root: Path) -> None: +def assert_http_action_api(root: Path, *, provider: str = "file") -> None: registry_path, state_path = write_registry_fixture(root) + initialize_canonical_registry_fixture(registry_path, provider=provider) runtime_root = root / "runtime" action_store = ChatActionStore(runtime_root / "chat" / "actions") chat_store = ChatSessionStore(runtime_root) @@ -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_invalid_decision = state_path.read_bytes() + proposals_before_invalid_decision = action_store.path.read_bytes() code, misdirected_decision = request_json( f"{base_url}/api/actions/preview", method="POST", @@ -975,7 +980,10 @@ 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 "requested role" in misdirected_decision["error"], misdirected_decision + assert state_path.read_bytes() == before_invalid_decision + assert action_store.path.read_bytes() == proposals_before_invalid_decision for index, (action_kind, params, gate_kind) in enumerate( [ @@ -1010,7 +1018,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", @@ -1128,7 +1136,7 @@ def assert_zero_goal_workspace_selection(root: Path) -> None: raise AssertionError("multiple workspaces must require a public-safe selection") -def main() -> None: +def main(*, provider: str = "file") -> None: with tempfile.TemporaryDirectory(prefix="loopx-chat-actions-") as raw_tmp: root = Path(raw_tmp) / "runtime" / "actions" store = ChatActionStore(root) @@ -1265,7 +1273,7 @@ def main() -> None: with tempfile.TemporaryDirectory(prefix="loopx-chat-action-http-") as raw_tmp: root = Path(raw_tmp) - assert_http_action_api(root) + assert_http_action_api(root, provider=provider) assert_zero_goal_workspace_selection(root) print("loopx-chat-actions-smoke: ok") diff --git a/examples/loopx-chat-contract-smoke.py b/examples/loopx-chat-contract-smoke.py index 24a95d5a2b..e52d02e30b 100644 --- a/examples/loopx-chat-contract-smoke.py +++ b/examples/loopx-chat-contract-smoke.py @@ -21,6 +21,8 @@ ) +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 + GOAL_ID = "loopx-chat-contract-smoke" TODO_TEXT = "[P0] Add a reviewable Goal Studio interaction." @@ -39,7 +41,9 @@ 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" + "## User Todo / Owner Review Reading Queue\n\n" + "## Agent Todo\n", encoding="utf-8", ) registry_path = project / ".loopx" / "registry.json" @@ -72,10 +76,11 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: return registry_path, state_file, project -def main() -> None: +def main(*, provider: str = "file") -> None: with tempfile.TemporaryDirectory(prefix="loopx-chat-contract-") as raw_tmp: root = Path(raw_tmp) registry_path, state_file, project = write_fixture(root) + initialize_canonical_registry_fixture(registry_path, provider=provider) raw_response = ( "draft text that must not become authority\n" diff --git a/examples/loopx-chat-server-smoke.py b/examples/loopx-chat-server-smoke.py index 7fdd559c5d..f0bb3bafb3 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 tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # 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." @@ -206,8 +210,7 @@ def wait_for_json(url: str) -> dict: raise RuntimeError(f"timed out waiting for LoopX Chat: {last_error}") -def main() -> None: - sys.path.insert(0, str(REPO_ROOT)) +def main(*, provider: str = "file") -> None: from loopx.chat_server import ( ChatHTTPServer, ChatRequestHandler, @@ -226,6 +229,7 @@ def main() -> None: with tempfile.TemporaryDirectory(prefix="loopx-chat-server-") as raw_tmp: root = Path(raw_tmp) registry, state_file = write_fixture(root) + initialize_canonical_registry_fixture(registry, provider=provider) bounded_status = build_bounded_chat_status_projection( registry=json.loads(registry.read_text(encoding="utf-8")), selected_goal_id=GOAL_ID, diff --git a/loopx/chat.py b/loopx/chat.py index 12ec2880ec..23f63e9aee 100644 --- a/loopx/chat.py +++ b/loopx/chat.py @@ -710,8 +710,8 @@ def _compact_todo_payload(payload: dict[str, Any], *, applied: bool) -> dict[str def _todo_preview_fingerprint(payload: dict[str, Any]) -> str: compact = _compact_todo_payload(payload, applied=False) if payload.get("decision_read_from_provider") is True: - # A create dry-run allocates a proposed operation identity without a - # write. Its random Todo id is not the reviewed intent or source head. + # Bind the transport operation to reviewed intent and provider head. + # The provider allocates Todo identity from that operation separately. compact["todo"].pop("todo_id") return _stable_digest( { @@ -761,7 +761,7 @@ def _todo_no_write_receipt( "outcome": "preview_stale", "write_attempted": write_attempted, **({"current_preview_id": _todo_preview_fingerprint(current_preview)} - if not write_attempted else {}), + if not write_attempted and state_revision is None else {}), "state_revision": state_revision or _todo_revision(current_preview), } receipt["receipt_id"] = _stable_digest(receipt) @@ -776,6 +776,7 @@ def _add_review_todo( priority: str | None = None, dry_run: bool, expected_provider_revision: str | None = None, + operation_id: str | None = None, ) -> dict[str, Any]: return add_goal_todo( registry_path=registry_path, @@ -786,11 +787,41 @@ def _add_review_todo( task_class="advancement_task", action_kind=CHAT_TODO_ACTION_KIND, dry_run=dry_run, + **({"operation_id": operation_id} if operation_id is not None else {}), **({"expected_provider_revision": expected_provider_revision} if expected_provider_revision is not None else {}), ) +def _bound_todo_review_preview( + *, registry_path: Path, goal_id: str, text: str, priority: str | None, +) -> dict[str, Any]: + payload = _add_review_todo( + registry_path=registry_path, goal_id=goal_id, text=text, priority=priority, dry_run=True, + ) + if payload.get("decision_read_from_provider") is not True: + return payload + # Reuse the existing canonical operation contract instead of predicting a + # Todo id here. Both dry-runs are read-only; the second refuses a changed + # provider head. Confirmation uses this same operation and revision. + try: + return _add_review_todo( + registry_path=registry_path, goal_id=goal_id, text=text, priority=priority, dry_run=True, + operation_id="chat-todo:" + _todo_preview_fingerprint(payload), + expected_provider_revision=_todo_revision(payload), + ) + except LocalCoordinationAuthorityUnavailable as exc: + if exc.code != "provider_revision_mismatch": + raise + raise TodoReviewPreviewConflict( + "stale todo preview; preview the proposal again before applying", + receipt=_todo_no_write_receipt( + goal_id=goal_id, current_preview=payload, + state_revision=exc.payload.get("current_provider_revision"), + ), + ) from exc + + def build_todo_review_preview( *, registry_path: Path, @@ -798,12 +829,8 @@ def build_todo_review_preview( text: str, priority: str | None = None, ) -> dict[str, Any]: - payload = _add_review_todo( - registry_path=registry_path, - goal_id=goal_id, - text=text, - priority=priority, - dry_run=True, + payload = _bound_todo_review_preview( + registry_path=registry_path, goal_id=goal_id, text=text, priority=priority, ) compact = _compact_todo_payload(payload, applied=False) compact["preview_id"] = _todo_preview_fingerprint(payload) @@ -818,12 +845,8 @@ def apply_todo_review_preview( priority: str | None = None, preview_id: str, ) -> dict[str, Any]: - current_preview = _add_review_todo( - registry_path=registry_path, - goal_id=goal_id, - text=text, - priority=priority, - dry_run=True, + current_preview = _bound_todo_review_preview( + registry_path=registry_path, goal_id=goal_id, text=text, priority=priority, ) if not preview_id or preview_id != _todo_preview_fingerprint(current_preview): raise TodoReviewPreviewConflict( @@ -840,7 +863,8 @@ def apply_todo_review_preview( text=text, priority=priority, dry_run=False, - **({"expected_provider_revision": _todo_revision(current_preview)} + **({"expected_provider_revision": _todo_revision(current_preview), + "operation_id": current_preview["operation_id"]} if current_preview.get("decision_read_from_provider") is True else {}), ) except LocalCoordinationAuthorityUnavailable as exc: diff --git a/tests/control_plane/canonical_authority_fixture.py b/tests/control_plane/canonical_authority_fixture.py index b8d8fbd004..a834c8eb3e 100644 --- a/tests/control_plane/canonical_authority_fixture.py +++ b/tests/control_plane/canonical_authority_fixture.py @@ -118,3 +118,25 @@ def promoted_create_fixture(tmp_path: Path, *, provider: str = "file") -> tuple[ runtime_root, "goal-a", projection, state_path=state_file, provider=provider ) return registry_path, runtime_root, state_file + + +def initialize_canonical_registry_fixture(registry_path: Path, *, provider: str = "file") -> None: + """Seed disposable consumer fixtures; this is not cold-source import evidence. + + Fixture prose supplies synthetic records only. Normal writes and subsequent + reads must use the real canonical provider and its explicit ownership policy. + """ + from loopx.control_plane.coordination.authority_source_capture import build_todo_runtime_shadow_projection + from loopx.control_plane.todos.goal_todo_projection import project_goal_todo_items + + registry = json.loads(registry_path.read_text(encoding="utf-8")) + runtime = Path(registry["common_runtime_root"]) + for goal in registry["goals"]: + state = Path(goal["repo"]) / goal["state_file"] + todos = project_goal_todo_items( + goal, state_text=state.read_text(encoding="utf-8"), state_path=state, rollout_events=[] + ) + 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) diff --git a/tests/control_plane/test_chat_reviewed_create.py b/tests/control_plane/test_chat_reviewed_create.py index 51cb1e12c5..418eb446ef 100644 --- a/tests/control_plane/test_chat_reviewed_create.py +++ b/tests/control_plane/test_chat_reviewed_create.py @@ -8,7 +8,7 @@ @pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("timing", ["before-confirmation", "after-final-preview"]) +@pytest.mark.parametrize("timing", ["before-confirmation", "between-preview-reads", "after-final-preview"]) def test_concurrent_create_refuses_reviewed_intent(tmp_path, monkeypatch, provider, timing): isolate_sqlite_runtime(tmp_path, monkeypatch) registry, runtime, _ = promoted_create_fixture(tmp_path, provider=provider) @@ -25,7 +25,9 @@ def independent_write(): original = chat._add_review_todo def interleaved(**kwargs): - if not kwargs["dry_run"]: + if (timing == "after-final-preview" and not kwargs["dry_run"]) or ( + timing == "between-preview-reads" and kwargs["dry_run"] and kwargs.get("operation_id") + ): independent_write() return original(**kwargs) @@ -38,6 +40,15 @@ def interleaved(**kwargs): assert ("current_preview_id" in conflict.value.receipt) is (timing == "before-confirmation") snapshot = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") assert [todo["text"] for todo in snapshot["todos"]] == ["Independent accepted request"] + if timing != "before-confirmation": + monkeypatch.setattr(chat, "_add_review_todo", original) + refreshed = chat.build_todo_review_preview( + registry_path=registry, goal_id="goal-a", text="Reviewed request") + applied = chat.apply_todo_review_preview(registry_path=registry, goal_id="goal-a", + text="Reviewed request", preview_id=refreshed["preview_id"]) + assert applied["todo"]["todo_id"] == refreshed["todo"]["todo_id"] + snapshot = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + assert {todo["text"] for todo in snapshot["todos"]} == {"Independent accepted request", "Reviewed request"} @pytest.mark.parametrize("provider", ["file", "sqlite"]) @@ -45,10 +56,29 @@ def test_current_review_creates_once_and_duplicate_remains_noop(tmp_path, monkey isolate_sqlite_runtime(tmp_path, monkeypatch) registry, runtime, _ = promoted_create_fixture(tmp_path, provider=provider) for _ in range(2): + before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") preview = chat.build_todo_review_preview( registry_path=registry, goal_id="goal-a", text="Reviewed request") + assert chat.build_todo_review_preview( + registry_path=registry, goal_id="goal-a", text="Reviewed request") == preview + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == before result = chat.apply_todo_review_preview(registry_path=registry, goal_id="goal-a", text="Reviewed request", preview_id=preview["preview_id"]) assert result["applied"] is True + assert result["todo"]["todo_id"] == preview["todo"]["todo_id"] snapshot = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") assert [todo["text"] for todo in snapshot["todos"]] == ["Reviewed request"] + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_changed_intent_cannot_apply_another_preview(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, runtime, _ = promoted_create_fixture(tmp_path, provider=provider) + preview = chat.build_todo_review_preview( + registry_path=registry, goal_id="goal-a", text="Reviewed request") + before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + for changed in ({"text": "Different request"}, {"text": "Reviewed request", "priority": "P0"}): + with pytest.raises(chat.TodoReviewPreviewConflict): + chat.apply_todo_review_preview(registry_path=registry, goal_id="goal-a", + preview_id=preview["preview_id"], **changed) + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == before From 4f49400c209fcec85e27736c4243c3d7e34841a1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:58:22 +0800 Subject: [PATCH 31/54] fix(todos): preserve canonical completion successor contracts Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../todo-continuation-policy-smoke.py | 6 ++-- .../control_plane/todo_lifecycle_fixtures.py | 6 ++-- .../coordination/todo_terminal_lifecycle.ts | 5 ++- .../todos/provider_terminal_lifecycle.py | 13 ++++++- .../test_canonical_completion_journeys.py | 34 +++++++++++++++++++ .../test_reviewed_terminal_actions.py | 7 ++-- .../authority_store_conformance.ts | 26 +++++++++++++- 7 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 tests/control_plane/test_canonical_completion_journeys.py diff --git a/examples/control_plane/todo-continuation-policy-smoke.py b/examples/control_plane/todo-continuation-policy-smoke.py index 2a5098a5ff..5234d4ce5c 100644 --- a/examples/control_plane/todo-continuation-policy-smoke.py +++ b/examples/control_plane/todo-continuation-policy-smoke.py @@ -18,6 +18,7 @@ write_fixture_registry, ) from loopx.status import parse_active_state_todos # noqa: E402 +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 GOAL_ID = "continuation-policy-fixture" @@ -78,9 +79,10 @@ def agent_todo(state_file: Path, todo_id: str) -> dict: return next(item for item in items if item["todo_id"] == todo_id) -def main() -> int: +def main(*, provider: str = "file") -> int: with tempfile.TemporaryDirectory(prefix="loopx-continuation-policy-") as tmp: registry_path, state_file = write_fixture(Path(tmp)) + initialize_canonical_registry_fixture(registry_path, provider=provider) same_peer = add_todo(registry_path, "Complete one bounded migration slice.") same_peer_result = run_json_cli( @@ -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_lifecycle_fixtures.py b/examples/control_plane/todo_lifecycle_fixtures.py index ddbfe4982b..ebec1ce0e1 100644 --- a/examples/control_plane/todo_lifecycle_fixtures.py +++ b/examples/control_plane/todo_lifecycle_fixtures.py @@ -20,6 +20,7 @@ write_fixture_registry, ) from loopx.status import parse_active_state_todos # noqa: E402 +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 GOAL_ID = "todo-lifecycle-goal" @@ -38,7 +39,7 @@ UNCLAIMED_SUCCESSOR_TODO = "Continue the unclaimed successor routing check." -def write_fixture(root: Path) -> tuple[Path, Path]: +def write_fixture(root: Path, *, provider: str = "file") -> tuple[Path, Path]: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -66,6 +67,7 @@ def write_fixture(root: Path) -> tuple[Path, Path]: registered_agents=["codex-main-control", "codex-side-bypass"], quota_allowed_slots=None, ) + initialize_canonical_registry_fixture(registry_path, provider=provider) return registry_path, state_file @@ -301,7 +303,7 @@ def assert_configured_peer_handoff() -> None: "--next-excluded-agent", "codex-side-bypass", ) - assert "cannot also appear in next_excluded_agents" in ( + assert "cannot also appear in excluded_agents" in ( same_agent_handoff["error"] ), same_agent_handoff diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index 74139adedc..e02bf66656 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -1020,8 +1020,10 @@ function terminalTarget( function semanticDuplicate( todos: ReadonlyMap, successor: JsonObject, + completingTodoId: string, ): JsonObject | undefined { return [...todos.values()].find((todo) => + todo.todo_id !== completingTodoId && todo.role === successor.role && todo.archive_state === "active" && todo.status !== "done" && todo.status !== "deferred" && todo.text === successor.text); } @@ -1624,7 +1626,7 @@ export async function executeCoordinationTodoTerminalLifecycle( todo_id: successorId, }); } - const duplicate = semanticDuplicate(projection.todos, successor); + const duplicate = semanticDuplicate(projection.todos, successor, input.todo_id); if (duplicate !== undefined) { return terminalFailure( "todo_semantic_duplicate_conflict", @@ -1678,6 +1680,7 @@ export async function executeCoordinationTodoTerminalLifecycle( completion_recovery: completion.completion_state.recovery, } : {}), completion_policy: completionPolicy, + no_followup: target.todo.no_followup === true, completion_identity_key: completion === null ? null : completion.completion_identity_key, completion_identity_source: diff --git a/loopx/control_plane/todos/provider_terminal_lifecycle.py b/loopx/control_plane/todos/provider_terminal_lifecycle.py index 9ef427c2b3..d1163868bf 100644 --- a/loopx/control_plane/todos/provider_terminal_lifecycle.py +++ b/loopx/control_plane/todos/provider_terminal_lifecycle.py @@ -35,6 +35,7 @@ ) from .contract import resolve_next_user_task_class from .mutation_authority import todo_lifecycle_facts +from .mutation_response import serialize_added_todo_payload from ..coordination.authority_source_capture import authority_registry_source from .path_resolution import resolve_todo_state_path from .provider_projection import projection_delivery_requires_ack, settle_canonical_todo_projection @@ -521,7 +522,17 @@ def terminal_canonical_todo_if_promoted( "updated_at": payload.get("completed_at") if command == "complete" else ( payload.get("completed_at") if payload.get("changed") else None ), - "next_todos": payload.get("generated_successors") or [], + "self_merged": bool((payload.get("completion_policy") or {}).get("self_merged")), + "next_todos": [ + serialize_added_todo_payload( + add_result={**successor, "already_exists": False}, + goal_id=goal_id, role=successor["role"], todo_text=successor["text"], + agent_id=actor_agent_id, state_file=state_file, project=project, + updated_at=successor.get("updated_at"), dry_run=dry_run, + added=True, metadata_updated=False, changed=payload.get("changed") is True and not dry_run, + handoff_gate={}, + ) for successor in payload.get("generated_successors") or [] + ], "mutation_authority": terminal_decision, "task_lease_fence": terminal_decision, } diff --git a/tests/control_plane/test_canonical_completion_journeys.py b/tests/control_plane/test_canonical_completion_journeys.py new file mode 100644 index 0000000000..e7c740c927 --- /dev/null +++ b/tests/control_plane/test_canonical_completion_journeys.py @@ -0,0 +1,34 @@ +"""Public completion/continuation journeys against disposable real providers.""" +from __future__ import annotations + +import runpy +from pathlib import Path + +import pytest + +from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_canonical_completion_journeys(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + lifecycle = runpy.run_path(str(root / "examples/control_plane/todo-lifecycle-cli-smoke.py")) + # Select a real backend for the existing CLI scenarios, without replacing + # any production decisions, transport responses or assertions. + import todo_lifecycle_fixtures as fixtures + original = fixtures.write_fixture + def write_fixture(path): + return original(path, provider=provider) + monkeypatch.setattr(fixtures, "write_fixture", write_fixture) + lifecycle["assert_peer_monitor_no_followup"].__globals__["write_fixture"] = write_fixture + for name in ( + "assert_configured_peer_handoff", + "assert_no_followup_cli_metadata", + "assert_complete_links_existing_successor", + "assert_same_title_completion_creates_fresh_successor", + "assert_peer_monitor_no_followup", + ): + lifecycle[name]() + continuation = runpy.run_path(str(root / "examples/control_plane/todo-continuation-policy-smoke.py")) + assert continuation["main"](provider=provider) == 0 diff --git a/tests/control_plane/test_reviewed_terminal_actions.py b/tests/control_plane/test_reviewed_terminal_actions.py index 0116b2b097..1f82342b5d 100644 --- a/tests/control_plane/test_reviewed_terminal_actions.py +++ b/tests/control_plane/test_reviewed_terminal_actions.py @@ -7,11 +7,10 @@ from pathlib import Path import pytest -from canonical_authority_fixture import initialize_canonical_authority +from canonical_authority_fixture import initialize_canonical_registry_fixture from loopx.chat_action_store import ChatActionStore from loopx.chat_actions import ChatActionService -from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.todos import add_goal_todo, complete_goal_todo, list_goal_todos @@ -24,15 +23,13 @@ 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"]}}]})) + initialize_canonical_registry_fixture(registry, 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_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts index b9c623dec1..91ed524072 100644 --- a/tests/control_plane_ts/authority_store_conformance.ts +++ b/tests/control_plane_ts/authority_store_conformance.ts @@ -691,7 +691,7 @@ export function registerAuthorityStoreConformance( assert.equal(initialized.status, "applied"); const successorIntent = { role: "agent", - text: "Continue with the next provider-neutral transaction", + text: "Finish the provider-neutral transaction family", task_class: "advancement_task", }; const request = { @@ -784,6 +784,30 @@ export function registerAuthorityStoreConformance( assert.deepEqual(await store.loadAuthority(), beforePolicyRejection); } } + // Same-title succession is legal because the parent closes in this CAS. + // A different open row with that title still blocks, without consuming the + // completion identity or mutating the parent/lease. Retry after its removal. + const cleanHead = await store.loadAuthority(); + assert.equal(cleanHead.status, "loaded"); + if (cleanHead.status !== "loaded") return; + const duplicateProjection = structuredClone(cleanHead.head); + (duplicateProjection.todos as Record[]).push({ + ...(duplicateProjection.todos as Record[]).find(todo => todo.todo_id === "todo-terminal")!, + todo_id: "todo-other-open", + }); + const seededDuplicate = await store.commitAuthority({expected_provider_revision: cleanHead.provider_revision, + operation_id: "seed-other-open", events: [], receipts: [], next_projection: authorityProjectionFixture(goalId, + duplicateProjection.todos as Record[], duplicateProjection.leases as Record[], + "native", {handoff_mode: "hard_lease"})}); + assert.equal(seededDuplicate.status, "applied"); + const withDuplicate = await store.loadAuthority(); + const duplicateRejected = await executeCoordinationTodoTerminalLifecycle(store, commitRequest); + assert.equal(duplicateRejected.status, "failed"); + assert.equal(duplicateRejected.reason_code, "todo_semantic_duplicate_conflict", JSON.stringify(duplicateRejected)); + assert.deepEqual(await store.loadAuthority(), withDuplicate); + if (withDuplicate.status !== "loaded") return; + assert.equal((await store.commitAuthority({expected_provider_revision: withDuplicate.provider_revision, + operation_id: "remove-other-open", events: [], receipts: [], next_projection: cleanHead.head})).status, "applied"); const dangling = await executeCoordinationTodoTerminalLifecycle(store, { ...commitRequest, operation_identity: {kind: "explicit" as const, operation_id: "complete-dangling-successor"}, From 889a7e4b32955338c6a20ff43cc35cc4da028a62 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:13:47 +0800 Subject: [PATCH 32/54] fix(todos): project update results from original transaction facts Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../control_plane/todo-lifecycle-cli-smoke.py | 2 +- .../coordination/todo_terminal_lifecycle.ts | 3 +- .../control_plane/coordination/todo_update.ts | 9 ++-- .../coordination/todo_update_intent.ts | 16 +++++- loopx/control_plane/todos/provider_update.py | 24 ++++++++- .../test_canonical_completion_journeys.py | 52 +++++++++++++++++++ .../test_canonical_update_dispatch.py | 3 +- .../test_shadow_native_todo_update_e2e.py | 12 +++-- .../native_planning_update_conformance.ts | 7 +++ .../user_completion_update_conformance.ts | 4 ++ 10 files changed, 119 insertions(+), 13 deletions(-) diff --git a/examples/control_plane/todo-lifecycle-cli-smoke.py b/examples/control_plane/todo-lifecycle-cli-smoke.py index 023e169551..ffa7c408b4 100644 --- a/examples/control_plane/todo-lifecycle-cli-smoke.py +++ b/examples/control_plane/todo-lifecycle-cli-smoke.py @@ -639,7 +639,7 @@ def main() -> int: "--next-excluded-agent", "codex-side-bypass", ) - assert "cannot also appear in next_excluded_agents" in ( + assert "cannot also appear in excluded_agents" in ( side_review_claim_error["error"] ), side_review_claim_error diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index e02bf66656..67b6cba604 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -6,7 +6,7 @@ import {AUTHORITY_SOURCE_CHANGED, uncheckedAuthoritySource, type AuthoritySource export type TodoTerminalEvidenceGuard = (context: { goal_id: string; todo: JsonObject; todos: JsonObject[]; actor_agent_id: string | null; command: string; }) => Promise; -import {normalizeTodoUpdateInput, prepareUpdatedTodo, type CoordinationTodoUpdateInput, type TodoCompletionEdit} from "./todo_update_intent.ts"; +import {todoUpdateResultFacts, normalizeTodoUpdateInput, prepareUpdatedTodo, type CoordinationTodoUpdateInput, type TodoCompletionEdit} from "./todo_update_intent.ts"; import {todoUpdateAdmissionRejection} from "./todo_update_admission.ts"; import { createHash } from "node:crypto"; import {acceptanceWorkGuard, acceptanceCompletionRequirements, validateAcceptanceCompletion, @@ -1660,6 +1660,7 @@ export async function executeCoordinationTodoTerminalLifecycle( ? projection.todos.get(String(todo.unblocks_todo_id)) : undefined; const changed = authority.outcome === "apply" || edit?.changed === true || terminalUpgradeReceipt !== null; const result: JsonObject = { + ...(input.user_update == null ? {} : {todo_update: todoUpdateResultFacts(authorityTodo, target.todo)}), todo_id: input.todo_id, command: input.command, changed, diff --git a/loopx/control_plane/coordination/todo_update.ts b/loopx/control_plane/coordination/todo_update.ts index cf81d4888a..1373b33d91 100644 --- a/loopx/control_plane/coordination/todo_update.ts +++ b/loopx/control_plane/coordination/todo_update.ts @@ -41,7 +41,7 @@ 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 {todoUpdateResultFacts, normalizeTodoUpdateInput, prepareUpdatedTodo, type CoordinationTodoUpdateInput} from "./todo_update_intent.ts"; import {executeCoordinationTodoTerminalLifecycle} from "./todo_terminal_lifecycle.ts"; import {planCompletionValidationRevision} from "../todos/completion_validation_revision.ts"; import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; @@ -68,6 +68,8 @@ function updateReceipt(input: CoordinationTodoUpdateInput, requestSha: string) { 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, + ...(original.todo_update === undefined ? {} : {todo_update: + canonicalAuthorityObject(original.todo_update, "Todo update receipt facts")}), ...(input.monitor_observation === undefined ? {} : { monitor_poll_transition: canonicalAuthorityObject(original.monitor_poll_transition, "Monitor update receipt transition")}), ...(original.monitor_lifecycle_transition === undefined ? {} : {monitor_lifecycle_transition: @@ -278,7 +280,8 @@ export async function executeCoordinationTodoUpdate( // The provider CAS covers Todo/lease state. Registry configuration is a // separate source witness, not part of a distributed transaction. if (!await authoritySourcesCurrent()) return sourceChanged(); - if (input.dry_run) return {schema_version: COORDINATION_TODO_UPDATE_RESULT_SCHEMA, + const todoUpdate = todoUpdateResultFacts(target.todo, next); + if (input.dry_run) return {todo_update: todoUpdate, schema_version: COORDINATION_TODO_UPDATE_RESULT_SCHEMA, status: changed ? "planned" : "no_change", changed, todo_id: input.todo_id, provider_revision: head.provider_revision, cursor: head.cursor, dry_run: true, ...(prepared.monitorTransition ? {monitor_poll_transition: prepared.monitorTransition} : {}), @@ -289,7 +292,7 @@ export async function executeCoordinationTodoUpdate( ...(blockedCycle?.transition == null ? {} : {blocked_lifecycle_transition: blockedCycle.transition})}; commit.receipts = [{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, changed, + todo_id: input.todo_id, request_sha256: requestSha, changed, todo_update: todoUpdate, ...(prepared.monitorTransition ? {monitor_poll_transition: prepared.monitorTransition} : {}), ...(completionValidationRevisionReceipt === null ? {} : {completion_validation_revision: completionValidationRevisionReceipt}), diff --git a/loopx/control_plane/coordination/todo_update_intent.ts b/loopx/control_plane/coordination/todo_update_intent.ts index 24025ea38d..f77933f4f9 100644 --- a/loopx/control_plane/coordination/todo_update_intent.ts +++ b/loopx/control_plane/coordination/todo_update_intent.ts @@ -211,8 +211,20 @@ export function prepareUpdatedTodo( const changed = !canonicalAuthorityBytes(changedBeforeAudit).equals( canonicalAuthorityBytes(originalBeforeAudit)); if (!changed) { - next.last_actor_agent_id = todo.last_actor_agent_id; - next.updated_at = todo.updated_at; + for (const field of ["last_actor_agent_id", "updated_at"]) { + if (Object.hasOwn(todo, field)) next[field] = todo[field]; + else delete next[field]; + } } return {next, changed, clearFields: [...clearFields], ...(monitorTransition ? {monitorTransition} : {})}; } + +/** Capture decided edit facts inside the transaction, including on previews. + * Receipt replay must never reconstruct these from a later provider head. */ +export function todoUpdateResultFacts(before: JsonObject, after: JsonObject): JsonObject { + const metadata = (todo: JsonObject) => Object.fromEntries(Object.entries(todo).filter( + ([key]) => !["text", "status", "done", "updated_at", "last_actor_agent_id"].includes(key))); + return {todo: after, status_changed: before.status !== after.status, + text_changed: before.text !== after.text, + metadata_updated: !canonicalAuthorityBytes(metadata(before)).equals(canonicalAuthorityBytes(metadata(after)))}; +} diff --git a/loopx/control_plane/todos/provider_update.py b/loopx/control_plane/todos/provider_update.py index 03473fc61b..beaec5eb15 100644 --- a/loopx/control_plane/todos/provider_update.py +++ b/loopx/control_plane/todos/provider_update.py @@ -23,6 +23,8 @@ EffectRuntimeResponseAmbiguous, effect_runtime_result, ) +from .mutation_response import serialize_todo_update_result +from .succession_warning import build_open_parent_successor_advisory from .contract import compact_todo_text from .provider_projection import settle_canonical_todo_projection from .text import normalize_new_todo @@ -342,9 +344,29 @@ def update_canonical_todo_if_promoted( declaration=declaration, result=result, ) + # Older receipts remain replayable without invented historical fields. + # New transactions carry their original row/deltas, including dry runs. + public = {} + facts = result.get("todo_update") + if isinstance(facts, dict): + todo = facts["todo"] + public = serialize_todo_update_result( + role=todo["role"], section=todo.get("source_section") or ( + "Agent Todo" if todo["role"] == "agent" else "User Todo / Owner Review Reading Queue"), + todo=todo.get("text"), todo_id=todo_id, status=todo["status"], + priority=todo.get("priority"), metadata=todo, + status_changed=facts["status_changed"], text_changed=facts["text_changed"], + metadata_updated=facts["metadata_updated"], + ) + advisory = build_open_parent_successor_advisory( + todo_id=todo_id, status=todo["status"], successor_todo_ids=todo.get("successor_todo_ids")) + if advisory: + public["parent_successor_advisory"] = advisory + public["changed"] = result["changed"] and not dry_run + public["provider_status"] = result["status"] return settle_canonical_todo_projection( {"ok": True, "goal_id": goal_id, "todo_id": todo_id, - "role": role, "dry_run": dry_run, **result}, + "role": role, "dry_run": dry_run, **result, **public}, registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id, project=project, state_file=state_file, ) diff --git a/tests/control_plane/test_canonical_completion_journeys.py b/tests/control_plane/test_canonical_completion_journeys.py index e7c740c927..6a1ed8d14b 100644 --- a/tests/control_plane/test_canonical_completion_journeys.py +++ b/tests/control_plane/test_canonical_completion_journeys.py @@ -28,7 +28,59 @@ def write_fixture(path): "assert_complete_links_existing_successor", "assert_same_title_completion_creates_fresh_successor", "assert_peer_monitor_no_followup", + "assert_open_parent_successor_advisory", ): lifecycle[name]() continuation = runpy.run_path(str(root / "examples/control_plane/todo-continuation-policy-smoke.py")) assert continuation["main"](provider=provider) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_update_response_preserves_original_facts_after_later_edit(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + runpy.run_path(str(root / "examples/control_plane/todo-lifecycle-cli-smoke.py")) + from todo_lifecycle_fixtures import GOAL_ID, run_cli, write_fixture + registry, _ = write_fixture(tmp_path / "fixture", provider=provider) + added = run_cli(registry, "todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--text", "Original response task", "--claimed-by", "codex-main-control") + base = ("todo", "update", "--goal-id", GOAL_ID, "--todo-id", added["todo_id"], + "--agent-id", "codex-main-control") + first = (*base, "--text", "First accepted wording", "--update-operation-id", "first-update") + preview = run_cli(registry, *first, "--dry-run") + assert preview["status"] == "open" + assert preview["text_changed"] is True + assert preview["changed"] is False + result = run_cli(registry, *first) + assert result["todo"] == "First accepted wording" + assert result["status"] == "open" + assert result["text_changed"] is True + assert result["status_changed"] is False + assert result["metadata_updated"] is False + noop = run_cli(registry, *base, "--text", "First accepted wording") + assert noop["changed"] is False + assert noop["metadata_updated"] is False + later = run_cli(registry, *base, "--text", "Later accepted wording") + assert later["todo"] == "Later accepted wording" + replay = run_cli(registry, *first) + for field in ("todo", "status", "text_changed", "status_changed", "metadata_updated", "todo_update"): + assert replay[field] == result[field] + current = run_cli(registry, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", added["todo_id"]) + assert current["todo"]["text"] == "Later accepted wording" + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_user_completion_update_projects_combined_edit(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + runpy.run_path(str(root / "examples/control_plane/todo-lifecycle-cli-smoke.py")) + from todo_lifecycle_fixtures import GOAL_ID, run_cli, write_fixture + registry, _ = write_fixture(tmp_path / "fixture", provider=provider) + added = run_cli(registry, "todo", "add", "--goal-id", GOAL_ID, "--role", "user", + "--text", "Confirm bounded change", "--task-class", "user_action", "--bound-agent", "codex-main-control") + result = run_cli(registry, "todo", "update", "--goal-id", GOAL_ID, "--todo-id", added["todo_id"], + "--agent-id", "codex-main-control", "--status", "done", "--text", "Confirmed bounded change") + assert result["status"] == "done" + assert result["todo"] == "Confirmed bounded change" + assert result["status_changed"] is True + assert result["text_changed"] is True diff --git a/tests/control_plane/test_canonical_update_dispatch.py b/tests/control_plane/test_canonical_update_dispatch.py index e3b06c2303..8f0aeb17b1 100644 --- a/tests/control_plane/test_canonical_update_dispatch.py +++ b/tests/control_plane/test_canonical_update_dispatch.py @@ -206,7 +206,8 @@ def test_empty_promoted_edits_reject_without_the_source_writer(update_workspace, "--update-operation-id", "empty-update") assert replay.returncode == 0, replay.stdout + replay.stderr recovered = json.loads(replay.stdout) - assert recovered["status"] == "replayed" + assert recovered["provider_status"] == "replayed" + assert recovered["status"] == "open" assert recovered["original_receipt"] == first["original_receipt"] assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == newer diff --git a/tests/control_plane/test_shadow_native_todo_update_e2e.py b/tests/control_plane/test_shadow_native_todo_update_e2e.py index 9a75ec2578..de74bb8c66 100644 --- a/tests/control_plane/test_shadow_native_todo_update_e2e.py +++ b/tests/control_plane/test_shadow_native_todo_update_e2e.py @@ -253,10 +253,12 @@ def pending(workspace: Workspace, kind: str) -> subprocess.Popen[str]: def test_native_update_preserves_complete_records_and_exact_receipts(workspace: Workspace, transport: str) -> None: before = inspect(workspace) preview = invoke(workspace, transport, dry_run=True) - assert preview["status"] == "planned", preview + assert preview["provider_status" if transport == "cli" else "status"] == "planned", preview assert inspect(workspace)["head"] == before["head"] result = invoke(workspace, transport) - assert result["status"] == "applied", result + assert result["provider_status" if transport == "cli" else "status"] == "applied", result + if transport == "cli": + assert result["status"] == preview["status"] == "open" receipt = result["original_receipt"] after = inspect(workspace, receipt["operation_id"]) assert after["receipt"]["status"] == "found" @@ -297,7 +299,8 @@ def timed_call(method: str, request: dict, **kwargs: object) -> object: text="Updated with the bounded canonical writer budget", note=None, dry_run=False, operation_id="budgeted-update", ) - assert result is not None and result["status"] == "applied", result + assert result is not None and result["provider_status"] == "applied", result + assert result["status"] == "open" assert observed and all(timeout > 35 for timeout in observed) receipt = inspect(workspace, "budgeted-update")["receipt"] assert receipt["status"] == "found" @@ -315,7 +318,8 @@ def test_cli_update_waits_past_default_rpc_budget_for_real_writer_lock(workspace assert result.returncode == 0, result.stdout + result.stderr payload = json.loads(result.stdout) assert elapsed > 10, elapsed - assert payload["status"] == "applied", payload + assert payload["provider_status"] == "applied", payload + assert payload["status"] == "open" receipt = payload["original_receipt"] assert inspect(workspace, receipt["operation_id"])["receipt"]["status"] == "found" finally: diff --git a/tests/control_plane_ts/native_planning_update_conformance.ts b/tests/control_plane_ts/native_planning_update_conformance.ts index 6861b0d67b..3d70a4744e 100644 --- a/tests/control_plane_ts/native_planning_update_conformance.ts +++ b/tests/control_plane_ts/native_planning_update_conformance.ts @@ -51,6 +51,12 @@ export function registerNativePlanningUpdateConformance(provider: string, factor assert.deepEqual(await head(store), before); const applied = await executeCoordinationTodoUpdate(store, request); assert.equal(applied.status, "applied", JSON.stringify(applied)); + const facts = applied.todo_update as JsonObject; + assert.ok(facts, "update must return its original transaction facts"); + assert.equal((facts.todo as JsonObject).note, "Owner reviewed correction"); + assert.equal(facts.status_changed, false); + assert.equal(facts.text_changed, false); + assert.equal(facts.metadata_updated, true); const original = await store.readReceipt(request.operation_id); const after = await head(store); assert.deepEqual(after.head.leases, before.head.leases); @@ -64,6 +70,7 @@ export function registerNativePlanningUpdateConformance(provider: string, factor const replay = await executeCoordinationTodoUpdate(store, {...request, lifecycle_grants: []}, async () => { throw new Error("historical receipt must precede current source admission"); }); assert.equal(replay.status, "replayed"); + assert.deepEqual(replay.todo_update, facts); assert.deepEqual(await head(store), latest); assert.deepEqual(await store.readReceipt(request.operation_id), original); assert.equal((await executeCoordinationTodoUpdate(store, {...request, authority_reason: "Changed justification"})).reason_code, diff --git a/tests/control_plane_ts/user_completion_update_conformance.ts b/tests/control_plane_ts/user_completion_update_conformance.ts index 0c15d69fae..e210080559 100644 --- a/tests/control_plane_ts/user_completion_update_conformance.ts +++ b/tests/control_plane_ts/user_completion_update_conformance.ts @@ -92,6 +92,10 @@ export function registerUserCompletionUpdateConformance(provider: string, factor const completed = (after.head.todos as JsonObject[]).find(todo => todo.todo_id === target.todo_id)!; assert.equal(completed.status, "done"); assert.equal(completed.text, request.patch.text); + const facts = applied.todo_update as JsonObject; + assert.deepEqual(facts.todo, completed); + assert.equal(facts.text_changed, true); + assert.equal(facts.status_changed, true); const retired = (after.head.leases as JsonObject[]).find(row => row.todo_id === target.todo_id)!; assert.deepEqual(retired, {...lease, status: "released", updated_at: "2026-09-18T06:00:00Z"}); assert.equal(retired.status, "released"); From 52052147f0c54cc04dd9b78ad00c5f22dcbfb253 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:27:40 +0800 Subject: [PATCH 33/54] fix(todos): restore canonical create responses and full lifecycle checks Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../todo-deferred-capacity-cli-smoke.py | 13 +-- .../control_plane/todo-lifecycle-cli-smoke.py | 25 ++++++ .../control_plane/todo_lifecycle_fixtures.py | 4 +- loopx/control_plane/todos/provider_create.py | 27 ++++++ .../test_canonical_completion_journeys.py | 74 +++++++++++++--- .../test_local_coordination_authority.py | 84 +++++++++++-------- .../test_validated_create_recovery.py | 12 ++- 7 files changed, 173 insertions(+), 66 deletions(-) diff --git a/examples/control_plane/todo-deferred-capacity-cli-smoke.py b/examples/control_plane/todo-deferred-capacity-cli-smoke.py index e065bc02e5..c99852b5b0 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) + registry_path, state_file = write_fixture(Path(tmp), placeholder=False) before = state_file.read_text(encoding="utf-8") unsupported = run_cli_error( diff --git a/examples/control_plane/todo-lifecycle-cli-smoke.py b/examples/control_plane/todo-lifecycle-cli-smoke.py index ffa7c408b4..09f6183c23 100644 --- a/examples/control_plane/todo-lifecycle-cli-smoke.py +++ b/examples/control_plane/todo-lifecycle-cli-smoke.py @@ -706,6 +706,27 @@ def main() -> int: assert review_item.get("updated_at"), review_item assert side_completed["next_todos"][0].get("updated_at") == review_item["updated_at"], side_completed + # Omitting ownership changes the persisted terminal intent. Only the + # exact original request may replay its receipt. + changed_retry = run_cli_error( + registry_path, + "todo", + "complete", + "--goal-id", + GOAL_ID, + "--todo-id", + run_todo_id, + "--agent-id", + "codex-main-control", + "--evidence", + "fresh-repeat-result-ready", + "--next-agent-todo", + REBUILD_TODO, + "--next-action-kind", + "rebuild_score", + ) + assert changed_retry["error_code"] == "coordination_operation_identity_mismatch", changed_retry + repeated = run_cli( registry_path, "todo", @@ -714,12 +735,16 @@ def main() -> int: GOAL_ID, "--todo-id", run_todo_id, + "--claimed-by", + "codex-main-control", "--agent-id", "codex-main-control", "--evidence", "fresh-repeat-result-ready", "--next-agent-todo", REBUILD_TODO, + "--next-claimed-by", + "codex-main-control", "--next-action-kind", "rebuild_score", ) diff --git a/examples/control_plane/todo_lifecycle_fixtures.py b/examples/control_plane/todo_lifecycle_fixtures.py index ebec1ce0e1..61b6c87ab0 100644 --- a/examples/control_plane/todo_lifecycle_fixtures.py +++ b/examples/control_plane/todo_lifecycle_fixtures.py @@ -39,7 +39,7 @@ UNCLAIMED_SUCCESSOR_TODO = "Continue the unclaimed successor routing check." -def write_fixture(root: Path, *, provider: str = "file") -> tuple[Path, Path]: +def write_fixture(root: Path, *, provider: str = "file", placeholder: bool = True) -> tuple[Path, Path]: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -54,7 +54,7 @@ def write_fixture(root: Path, *, provider: str = "file") -> tuple[Path, Path]: "## Objective\n\n" "Exercise todo lifecycle transitions.\n\n" "## Agent Todo\n\n" - "- [ ] Legacy monitor-only placeholder.\n", + + ("- [ ] Legacy monitor-only placeholder.\n" if placeholder else ""), encoding="utf-8", ) write_fixture_registry( diff --git a/loopx/control_plane/todos/provider_create.py b/loopx/control_plane/todos/provider_create.py index b3f8e0aba4..8f1f654c3f 100644 --- a/loopx/control_plane/todos/provider_create.py +++ b/loopx/control_plane/todos/provider_create.py @@ -37,6 +37,8 @@ prepare_completion_validation_declaration, read_completion_validation_declaration, ) +from .mutation_response import serialize_added_todo_payload +from .path_resolution import resolve_todo_state_path from .provider_projection import settle_canonical_todo_projection @@ -58,6 +60,10 @@ def create_canonical_todo_if_promoted( # Identity is independent of current Todo count: exact retries still address # the original operation after other creates, edits, completion or archive. todo_id = "todo_" + hashlib.sha256(operation_id.encode("utf-8")).hexdigest()[:24] + project, state_file = resolve_todo_state_path( + registry_path=registry_path, goal_id=goal_id, project=project, + state_file=state_file, require_existing=False, + ) normalized_metadata = normalize_todo_metadata_for_write(metadata) validation_source = { **normalized_metadata, @@ -207,6 +213,26 @@ def create_canonical_todo_if_promoted( "provider_status": result.get("status"), }, ) + # Public fields describe the original accepted Todo, including receipt + # replay. The native commit phase stays available as provider_status. + # Historical receipts without row facts retain their existing response. + public = {} + if isinstance(canonical_todo, dict): + public = serialize_added_todo_payload( + add_result={**canonical_todo, + "section": canonical_todo.get("source_section") or ( + "Agent Todo" if canonical_todo["role"] == "agent" + else "User Todo / Owner Review Reading Queue"), + "already_exists": result.get("changed") is not True}, + goal_id=goal_id, role=canonical_todo["role"], + todo_text=canonical_todo["text"], agent_id=actor_agent_id, + state_file=state_file, project=project, + updated_at=canonical_todo.get("updated_at"), dry_run=dry_run, + added=result.get("changed") is True, metadata_updated=False, + changed=result.get("changed") is True and not dry_run, handoff_gate={}, + ) + public["provider_status"] = result["status"] + public["changed"] = result.get("changed") is True and not dry_run settled = settle_canonical_todo_projection({ "ok": True, "operation_id": operation_id, @@ -218,6 +244,7 @@ def create_canonical_todo_if_promoted( "added": result.get("changed") is True, "already_exists": result.get("changed") is not True, **result, + **public, }, registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id, project=project, state_file=state_file) return cast(dict[str, Any], settled) diff --git a/tests/control_plane/test_canonical_completion_journeys.py b/tests/control_plane/test_canonical_completion_journeys.py index 6a1ed8d14b..27b3122679 100644 --- a/tests/control_plane/test_canonical_completion_journeys.py +++ b/tests/control_plane/test_canonical_completion_journeys.py @@ -18,19 +18,13 @@ def test_canonical_completion_journeys(tmp_path, monkeypatch, provider): # any production decisions, transport responses or assertions. import todo_lifecycle_fixtures as fixtures original = fixtures.write_fixture - def write_fixture(path): - return original(path, provider=provider) + def write_fixture(path, **options): + return original(path, provider=provider, **options) monkeypatch.setattr(fixtures, "write_fixture", write_fixture) lifecycle["assert_peer_monitor_no_followup"].__globals__["write_fixture"] = write_fixture - for name in ( - "assert_configured_peer_handoff", - "assert_no_followup_cli_metadata", - "assert_complete_links_existing_successor", - "assert_same_title_completion_creates_fresh_successor", - "assert_peer_monitor_no_followup", - "assert_open_parent_successor_advisory", - ): - lifecycle[name]() + assert lifecycle["main"]() == 0 + deferred = runpy.run_path(str(root / "examples/control_plane/todo-deferred-capacity-cli-smoke.py")) + assert deferred["main"]() == 0 continuation = runpy.run_path(str(root / "examples/control_plane/todo-continuation-policy-smoke.py")) assert continuation["main"](provider=provider) == 0 @@ -84,3 +78,61 @@ def test_user_completion_update_projects_combined_edit(tmp_path, monkeypatch, pr assert result["todo"] == "Confirmed bounded change" assert result["status_changed"] is True assert result["text_changed"] is True + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_create_response_preserves_original_todo_after_later_edit(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + runpy.run_path(str(root / "examples/control_plane/todo-lifecycle-cli-smoke.py")) + from todo_lifecycle_fixtures import GOAL_ID, run_cli, run_cli_error, write_fixture + registry, _ = write_fixture(tmp_path / "fixture", provider=provider) + intent = ("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--text", "Wait for worker capacity", "--claimed-by", "codex-main-control", + "--status", "deferred", + "--resume-when", "capacity_available:short_pool", "--operation-id", "original-create") + preview = run_cli(registry, *intent, "--dry-run") + assert preview["status"] == "deferred" + assert preview["provider_status"] == "planned" + assert preview["changed"] is False + original = run_cli(registry, *intent) + assert original["todo"] == "Wait for worker capacity" + assert original["status"] == "deferred" + assert original["provider_status"] == "applied" + assert original["claimed_by"] == "codex-main-control" + assert original["resume_when"] == "capacity_available:short_pool" + assert original["added"] is True and original["already_exists"] is False + run_cli(registry, "todo", "update", "--goal-id", GOAL_ID, + "--todo-id", original["todo_id"], "--agent-id", "codex-main-control", + "--text", "Worker capacity available", "--status", "open", "--clear-resume-when") + replay = run_cli(registry, *intent) + assert replay["provider_status"] == "replayed" + assert replay["changed"] is False and replay["updated_at"] is None + assert original["updated_at"] is not None + for field in ("todo", "todo_id", "status", "claimed_by", "resume_when"): + assert replay[field] == original[field] + rejected = run_cli_error(registry, *intent, "--text", "Different create intent") + assert rejected["error_code"] == "coordination_operation_identity_mismatch" + current = run_cli(registry, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", original["todo_id"]) + assert current["todo"]["text"] == "Worker capacity available" + assert current["todo"]["status"] == "open" + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_create_duplicate_projects_existing_open_todo(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + runpy.run_path(str(root / "examples/control_plane/todo-lifecycle-cli-smoke.py")) + from todo_lifecycle_fixtures import GOAL_ID, run_cli, write_fixture + registry, _ = write_fixture(tmp_path / "fixture", provider=provider) + intent = ("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--text", "An existing open task", "--claimed-by", "codex-main-control") + original = run_cli(registry, *intent) + duplicate = run_cli(registry, *intent) + assert duplicate["status"] == "open" + assert duplicate["todo"] == "An existing open task" + assert duplicate["todo_id"] == original["todo_id"] + assert duplicate["claimed_by"] == "codex-main-control" + assert duplicate["provider_status"] == "no_change" + assert duplicate["added"] is False and duplicate["already_exists"] is True + assert duplicate["changed"] is False diff --git a/tests/control_plane/test_local_coordination_authority.py b/tests/control_plane/test_local_coordination_authority.py index d40b850ecc..2f60db23d1 100644 --- a/tests/control_plane/test_local_coordination_authority.py +++ b/tests/control_plane/test_local_coordination_authority.py @@ -19,6 +19,7 @@ TODO_DOMAIN_RECORD_FIELDS, ) from loopx.control_plane.coordination.legacy_writer_fence import ( + LegacyCoordinationWriterFenced, legacy_coordination_writer_fence_path, ) from loopx.control_plane.coordination.local_authority import ( @@ -37,8 +38,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, @@ -562,7 +561,8 @@ def crash_once(**kwargs: object) -> str: ) is None recovered = add_goal_todo(**create_kwargs) - assert recovered["status"] == "no_change" + assert recovered["status"] == "open" + assert recovered["provider_status"] == "no_change" assert recovered["todo_id"] == todo_id assert recovered["added"] is False assert recovered["already_exists"] is True @@ -618,7 +618,8 @@ def test_promoted_validator_revision_updates_canonical_digest_and_private_readba validation_command_json=json.dumps(replacement_argv), validation_label="focused validation", ) - assert revised["status"] == "applied" + assert revised["status"] == "open" + assert revised["provider_status"] == "applied" canonical = read_canonical_todos_if_promoted( runtime_root=runtime_root, goal_id="goal-a" ) @@ -658,7 +659,8 @@ def test_promoted_validator_revision_updates_canonical_digest_and_private_readba validation_command_json=json.dumps(replacement_argv), validation_label="focused validation", ) - assert replay["status"] == "replayed" + assert replay["status"] == "open" + assert replay["provider_status"] == "replayed" assert read_canonical_todos_if_promoted( runtime_root=runtime_root, goal_id="goal-a" ) == canonical @@ -830,7 +832,8 @@ def crash(*_args: object, **_kwargs: object) -> None: validation_label="recoverable provider validation", ) - assert applied["status"] == "applied" + assert applied["status"] == "open" + assert applied["provider_status"] == "applied" assert applied["projection_delivery"] == "pending" canonical = read_canonical_todos_if_promoted( runtime_root=runtime_root, @@ -856,7 +859,8 @@ def crash(*_args: object, **_kwargs: object) -> None: validation_command_json=json.dumps(["python3", "-c", "raise SystemExit(0)"]), validation_label="recoverable provider validation", ) - assert replay["status"] == "no_change" + assert replay["status"] == "open" + assert replay["provider_status"] == "no_change" assert replay["projection_delivery"] == "delivered" rendered = state_file.read_text(encoding="utf-8") assert "Recover the native compatibility projection" in rendered @@ -1296,14 +1300,17 @@ def invoke_edit(argv): return subprocess.run(argv, capture_output=True, text=True, timeout=30) preview_edit = invoke_edit([*edit, "--dry-run"]) assert preview_edit.returncode == 0, preview_edit.stdout + preview_edit.stderr - assert json.loads(preview_edit.stdout)["status"] == "planned" + assert json.loads(preview_edit.stdout)["status"] == "open" + assert json.loads(preview_edit.stdout)["provider_status"] == "planned" assert list_goal_todos(registry_path=registry_path, goal_id="goal-a") == after first_edit = invoke_edit(edit) assert first_edit.returncode == 0, first_edit.stdout + first_edit.stderr - assert json.loads(first_edit.stdout)["status"] == "applied" + assert json.loads(first_edit.stdout)["status"] == "open" + assert json.loads(first_edit.stdout)["provider_status"] == "applied" retry_edit = invoke_edit(edit) assert retry_edit.returncode == 0, retry_edit.stdout + retry_edit.stderr - assert json.loads(retry_edit.stdout)["status"] == "replayed" + assert json.loads(retry_edit.stdout)["status"] == "open" + assert json.loads(retry_edit.stdout)["provider_status"] == "replayed" changed_edit = invoke_edit([*edit, "--note", "Different intent"]) assert changed_edit.returncode != 0 final = list_goal_todos(registry_path=registry_path, goal_id="goal-a") @@ -1594,7 +1601,7 @@ def count_authority_runtime_call( assert "Continue provider-first delivery." in rendered -def test_public_terminal_optional_prose_matches_before_and_after_promotion( +def test_terminal_prose_requires_import_then_preserves_normalization( tmp_path: Path, ) -> None: cases = [ @@ -1687,6 +1694,16 @@ def exercise(root: Path, *, promoted: bool) -> dict[str, object]: state_path=state_file, ) + if not promoted: + before = state_file.read_bytes() + for command in (complete_goal_todo, supersede_goal_todo): + with pytest.raises(LegacyCoordinationWriterFenced) as denied: + command(registry_path=registry_path, runtime_root_arg=str(runtime_root), + goal_id="goal-a", todo_id="todo_prose_01", role="agent", agent_id="agent-a") + assert denied.value.code == "canonical_authority_required" + assert state_file.read_bytes() == before + return {} + for index, (field, value, _expected) in enumerate(cases, start=1): common = { "registry_path": registry_path, @@ -1708,24 +1725,11 @@ 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) @@ -1738,14 +1742,14 @@ def exercise(root: Path, *, promoted: bool) -> dict[str, object]: f"{field}:{index}": value for index, (field, _input, value) in enumerate(cases, start=1) } - assert legacy == expected + assert legacy == {} assert canonical == expected -def test_illegal_terminal_actor_is_a_domain_valueerror_before_and_after_promotion( +def test_terminal_admission_rejects_legacy_then_illegal_canonical_actor( tmp_path: Path, ) -> None: - def rejected(root: Path, *, promoted: bool) -> ValueError: + def rejected(root: Path, *, promoted: bool) -> Exception: runtime_root = root / "runtime" project = root / "project" state_file = project / "ACTIVE_GOAL_STATE.md" @@ -1808,7 +1812,8 @@ def rejected(root: Path, *, promoted: bool) -> ValueError: ), state_path=state_file, ) - with pytest.raises(ValueError) as exc_info: + before = state_file.read_bytes() + with pytest.raises(ValueError if promoted else LegacyCoordinationWriterFenced) as exc_info: complete_goal_todo( registry_path=registry_path, runtime_root_arg=str(runtime_root), @@ -1819,11 +1824,13 @@ def rejected(root: Path, *, promoted: bool) -> ValueError: agent_id="agent-b", no_followup=True, ) + assert state_file.read_bytes() == before return exc_info.value legacy = rejected(tmp_path / "legacy", promoted=False) canonical = rejected(tmp_path / "canonical", promoted=True) - assert not isinstance(legacy, LocalCoordinationAuthorityUnavailable) + assert isinstance(legacy, LegacyCoordinationWriterFenced) + assert legacy.code == "canonical_authority_required" assert isinstance(canonical, LocalCoordinationAuthorityRejection) assert canonical.code == "actor_not_registered" @@ -2379,7 +2386,8 @@ def test_real_canonical_provider_preserves_complete_complex_todo_semantics( text=True, check=True, ) - assert json.loads(create_preview.stdout)["status"] == "planned" + assert json.loads(create_preview.stdout)["status"] == "open" + assert json.loads(create_preview.stdout)["provider_status"] == "planned" assert not state_file.exists() created = json.loads( subprocess.run( @@ -2430,14 +2438,16 @@ def test_real_canonical_provider_preserves_complete_complex_todo_semantics( preview = subprocess.run( [*command, "--dry-run"], capture_output=True, text=True, check=True ) - assert json.loads(preview.stdout)["status"] == "planned" + assert json.loads(preview.stdout)["status"] == "open" + assert json.loads(preview.stdout)["provider_status"] == "planned" assert ( list_goal_todos(registry_path=registry_path, goal_id="goal-a")["todos"] == after_create["todos"] ) edited = subprocess.run(command, capture_output=True, text=True, check=True) edit_result = json.loads(edited.stdout) - assert edit_result["status"] == "applied" + assert edit_result["status"] == "open" + assert edit_result["provider_status"] == "applied" assert edit_result["projection_delivery"] == "pending" assert not state_file.exists() after_edit = list_goal_todos(registry_path=registry_path, goal_id="goal-a") diff --git a/tests/control_plane/test_validated_create_recovery.py b/tests/control_plane/test_validated_create_recovery.py index a89b9f7b58..b479cc1009 100644 --- a/tests/control_plane/test_validated_create_recovery.py +++ b/tests/control_plane/test_validated_create_recovery.py @@ -137,7 +137,8 @@ def test_public_cli_replays_prior_normalized_create_without_replacing_new_data(p retried = subprocess.run(command, capture_output=True, text=True, timeout=60) assert retried.returncode == 0, retried.stdout + retried.stderr replay = json.loads(retried.stdout) - assert replay["status"] == "replayed", replay + assert replay["status"] == "open" + assert replay["provider_status"] == "replayed", replay assert replay["original_receipt"] == receipt assert read(runtime) == current @@ -181,7 +182,8 @@ def lose_response(method, request, **kwargs): role="agent", agent_id="agent-a", text="A subsequent work declaration") before_retry = read(runtime) replay = add_goal_todo(**intent(registry)) - assert replay["status"] == "replayed" + assert replay["status"] == "open" + assert replay["provider_status"] == "replayed" assert replay["todo_id"] == todo["todo_id"] assert read(runtime) == before_retry # No new commit or stale declaration overwrite. with pytest.raises(LocalCoordinationAuthorityUnavailable): @@ -314,7 +316,8 @@ def exit_after_commit(method, request, **kwargs): ], capture_output=True, text=True, timeout=60) assert retried.returncode == 0, retried.stdout + retried.stderr result = json.loads(retried.stdout) - assert result["status"] == "replayed" + assert result["status"] == "open" + assert result["provider_status"] == "replayed" assert result["todo_id"] == before["todos"][0]["todo_id"] assert read(runtime) == before @@ -379,7 +382,8 @@ def head(action): assert len(damaged["head"]["todos"]) == int(existing_receipt) if existing_receipt: replay = add_goal_todo(**original) - assert replay["status"] == "replayed" + assert replay["status"] == "open" + assert replay["provider_status"] == "replayed" assert replay["changed"] is False assert replay["projection_delivery"] == "pending" assert head("read") == damaged # A historical receipt grants no new write. From aabbbe43b0fa56fcb69c68fba3b1e2395bc069ed Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:42:50 +0800 Subject: [PATCH 34/54] fix(todos): preserve complete legacy rows during canonical projection Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../todo-archive-completed-smoke.py | 59 +++++++++++++++---- loopx/control_plane/todos/machine_region.py | 13 +++- .../test_archive_retry_delivery.py | 5 +- .../test_canonical_completion_journeys.py | 8 +++ .../control_plane/test_todo_machine_region.py | 44 ++++++++++++++ 5 files changed, 116 insertions(+), 13 deletions(-) diff --git a/examples/control_plane/todo-archive-completed-smoke.py b/examples/control_plane/todo-archive-completed-smoke.py index 70d8923454..e722fef513 100644 --- a/examples/control_plane/todo-archive-completed-smoke.py +++ b/examples/control_plane/todo-archive-completed-smoke.py @@ -47,6 +47,7 @@ def state_text() -> str: + "- [ ] [P2] Keep monitor work after completed items visible.\n\n" "## Completed Work Archive\n\n" "- [x] [P1] Previously archived completed work.\n" + " \n" ) @@ -80,9 +81,11 @@ 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, provider: str = "file", canonical: bool = True) -> tuple[Path, Path, Path]: + root = root.resolve() project = root / "project" runtime = root / "runtime" + runtime.mkdir(parents=True, exist_ok=True) state_file = ".codex/goals/todo-archive-completed-goal/ACTIVE_GOAL_STATE.md" state_path = project / state_file registry_path = project / ".loopx" / "registry.json" @@ -119,6 +122,19 @@ def write_fixture(root: Path, *, text: str | None = None) -> tuple[Path, Path, P + "\n", encoding="utf-8", ) + if canonical: + saved = run_cli(registry_path, runtime, "backup-state", "--project", str(project), + "--output-dir", str(root / "backups"), "--backup-id", "archive-source", + "--current-project-only", "--no-skills", "--no-automations", "--execute") + prepared = run_cli(registry_path, runtime, "coordination-shadow", "prepare-import", + "--goal-id", GOAL_ID, "--operation-id", "archive-source-import", + "--backup-manifest", saved["manifest_path"], "--provider", provider, + "--target-handoff-mode", "soft_claim")["cold_import"] + assert prepared["status"] == "prepared", prepared + applied = run_cli(registry_path, runtime, "coordination-shadow", "apply-import", + "--goal-id", GOAL_ID, "--operation-id", "archive-source-import", + "--plan-sha256", prepared["plan_sha256"], "--writers-stopped", "--execute")["cold_import"] + assert applied["status"] == "applied", applied return registry_path, runtime, state_path @@ -137,14 +153,15 @@ def run_cli(registry_path: Path, runtime: Path, *args: str, check: bool = True) *args, ], cwd=REPO_ROOT, - check=check, + check=False, capture_output=True, text=True, ) + assert not check or result.returncode == 0, result.stdout + result.stderr return json.loads(result.stdout) -def assert_user_role_archive_retains_standing_decision() -> None: +def assert_user_role_archive_retains_standing_decision(*, provider: str = "file") -> None: """`--role user` archives ordinary done gates but never a standing receipt. The shipped CLI accepts `--role`, yet the user-role archive path was only @@ -155,7 +172,7 @@ def assert_user_role_archive_retains_standing_decision() -> None: with tempfile.TemporaryDirectory(prefix="loopx-todo-archive-user-role-") as tmp: registry_path, runtime, state_path = write_fixture( - Path(tmp), text=user_role_state_text() + Path(tmp), text=user_role_state_text(), provider=provider ) original = state_path.read_text(encoding="utf-8") parsed = parse_active_state_todos(original) @@ -216,6 +233,12 @@ def assert_user_role_archive_retains_standing_decision() -> None: "todo_user_standing_approve" ), authority + receipt = run_cli(registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", "todo_user_standing_approve") + assert receipt["todo"]["archive_state"] == "active", receipt + assert receipt["todo"]["decision_outcome"] == "approve", receipt + assert receipt["todo"]["blocks_agent"] == "codex-reviewer", receipt + # Role scoping: compacting the user section must not move Agent Todo work. agent_ids = [item["todo_id"] for item in parsed_after["agent_todos"]["items"]] assert "todo_agent_completed" in agent_ids, parsed_after @@ -228,9 +251,9 @@ def assert_user_role_archive_retains_standing_decision() -> None: assert status_after["contract_errors"] == [], status_after -def main() -> int: +def main(*, provider: str = "file") -> int: with tempfile.TemporaryDirectory(prefix="loopx-todo-archive-completed-") as tmp: - registry_path, runtime, state_path = write_fixture(Path(tmp)) + registry_path, runtime, state_path = write_fixture(Path(tmp), provider=provider) original = state_path.read_text(encoding="utf-8") parsed = parse_active_state_todos(original) assert parsed["agent_todos"]["done_count"] == 15, parsed @@ -312,7 +335,17 @@ 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 + # Canonical projection owns Todo regions, not Goal narrative/frontmatter. + assert "updated_at: 2026-01-01T00:00:00+00:00" in updated + archived_read = run_cli(registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", "todo_completed_lane_1") + assert archived_read["todo"]["archive_state"] == "archive", archived_read + assert archived_read["todo"]["updated_at"] != "2026-01-01T00:00:00+00:00", archived_read + + retained = run_cli(registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", "todo_previous_archive") + assert retained["todo"]["archive_state"] == "archive", retained + assert retained["todo"]["text"] == "[P1] Previously archived completed work.", retained status_after = run_cli(registry_path, runtime, "status") assert status_after["ok"] is True, status_after @@ -438,8 +471,12 @@ def main() -> int: assert lineage_status["ok"] is True, lineage_status assert lineage_status["contract_errors"] == [], lineage_status - state_path.write_text( - archived_lineage + # A separate unimported source retains reader diagnostics. Canonical + # status must not regain authority from edits to its Markdown display. + invalid_registry, invalid_runtime, invalid_state = write_fixture( + Path(tmp) / "invalid-source", text=state_text(), canonical=False) + invalid_state.write_text( + state_text() + "- [ ] [P1] Contradictory archived work item.\n" + " \n" @@ -451,7 +488,7 @@ def main() -> int: + "task_class=advancement_task excluded_agents=codex-reviewer -->\n", encoding="utf-8", ) - invalid_status = run_cli(registry_path, runtime, "status", check=False) + invalid_status = run_cli(invalid_registry, invalid_runtime, "status", check=False) assert invalid_status["ok"] is False, invalid_status assert any( "todo_archived_open" in item and "executor exclusions" in item @@ -466,7 +503,7 @@ def main() -> int: for item in invalid_status["contract_errors"] ), invalid_status - assert_user_role_archive_retains_standing_decision() + assert_user_role_archive_retains_standing_decision(provider=provider) print("todo-archive-completed-smoke ok") return 0 diff --git a/loopx/control_plane/todos/machine_region.py b/loopx/control_plane/todos/machine_region.py index 381daf74b2..50a8b319ff 100644 --- a/loopx/control_plane/todos/machine_region.py +++ b/loopx/control_plane/todos/machine_region.py @@ -94,15 +94,25 @@ 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 + has_task = 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_line = TODO_TASK_PATTERN.match(content) is not None + # The legacy reader folds indented continuation text into its Todo. + # Adopt that same row body on the first projection, otherwise the + # continuation and every later row survive as duplicate narrative. + # Marked output stays strict; headings and invisible examples never + # become generated content merely because they follow a task. + continuation = (not marked and has_task and content.startswith((" ", "\t")) + and re.match(r"^ {0,3}#{1,6}(?:[ \t]|$)", content) is None) generated = index in visible and ( not content.strip() - or TODO_TASK_PATTERN.match(content) is not None + or task_line + or continuation or parse_todo_metadata_line(content) is not None or content.startswith("\n" + "- [ ] Keep second active.\n" + " \n" + "\nHuman explanation stays outside the generated region.\n").replace("\n", newline) + active, _, _ = parse_todo_source(source) + records = [canonical_todo_read_record(structured_todo_item(item, role="agent", + source_section="Agent Todo", text_limit=None)) for item in active["agent"]] + records[0].update(archive_state="archive", source_section="Completed Work Archive") + projected = render_canonical_todo_sections(source, records, provider_revision="rev-archive") + for text in ("Completed first.", "Continuation detail belongs", "Keep second active.", "todo_first", "todo_second"): + assert projected.markdown.count(text) == 1 + assert "Human explanation stays outside the generated region." + newline in projected.markdown + assert projected.markdown.index("## Completed Work Archive") < projected.markdown.index("Completed first.") + assert not render_canonical_todo_sections(projected.markdown, records, provider_revision="rev-archive").changed + + +@pytest.mark.parametrize("tail", [ + " ## Next Action\n\n- [ ] Narrative checkbox.\n", + " ```markdown\n continuation example\n ```\n", + " \n", + "Human narrative.\n Indented human narrative.\n", +]) +def test_legacy_continuation_does_not_consume_following_narrative(tail): + source = "## Agent Todo\n- [ ] A task.\n Its continuation.\n" + tail + lines = source.splitlines(keepends=True) + region, = find_todo_regions(lines) + assert region.end == 3 + assert "".join(lines[region.end:]) == tail + + +def test_marked_region_still_rejects_unrendered_continuation(): + source = ("## Agent Todo\n" + todo_region_marker("agent", "begin") + + "\n- [ ] A task.\n Handwritten addition.\n" + todo_region_marker("agent", "end")) + with pytest.raises(ValueError, match="non-generated content"): + find_todo_regions(source.splitlines()) From e79836889efedb8891177117dcdb80fc98f8585a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:58:11 +0800 Subject: [PATCH 35/54] test(todos): qualify canonical monitor and imported durability journeys Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../monitor-poll-writeback-smoke.py | 20 ++++++++ .../todo-durability-fixture-smoke.py | 50 ++++++++++++++++--- .../test_canonical_completion_journeys.py | 22 ++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/examples/control_plane/monitor-poll-writeback-smoke.py b/examples/control_plane/monitor-poll-writeback-smoke.py index a3ebdb1b14..1ba1268359 100644 --- a/examples/control_plane/monitor-poll-writeback-smoke.py +++ b/examples/control_plane/monitor-poll-writeback-smoke.py @@ -20,6 +20,7 @@ import loopx.cli_commands.quota as quota_command # noqa: E402 from loopx.quota import record_quota_monitor_poll # noqa: E402 +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 from loopx.rollout_event_log import load_rollout_events, rollout_event_log_path # noqa: E402 from loopx.control_plane.quota.monitor_poll import ( # noqa: E402 build_quota_monitor_poll_event, @@ -106,6 +107,7 @@ def write_fixture( other_no_change_count: int = 1, monitor_required_capabilities: tuple[str, ...] = (), monitor_task_repository: str | None = None, + provider: str = "file", ) -> tuple[Path, Path]: project = root / "project" runtime = root / "runtime" @@ -238,6 +240,11 @@ def write_fixture( registered_agents=[AGENT_ID], quota_allowed_slots=None, ) + # Consumer coverage uses a disposable real canonical authority. Legacy + # Markdown is only synthetic input, never the normal write owner. + from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture + + initialize_canonical_registry_fixture(registry_path, provider=provider) return registry_path, state_file @@ -303,6 +310,16 @@ def find_todo(state_file: Path, todo_id: str) -> dict: raise AssertionError(f"missing todo {todo_id}") +def assert_provider_monitor_state(registry_path: Path, writeback: dict) -> None: + source = read_canonical_todos_if_promoted( + runtime_root=runtime_root_from_registry(registry_path), goal_id=GOAL_ID) + assert source is not None + assert source["legacy_fallback_used"] is False, source + monitor = next(row for row in source["todos"] if row["todo_id"] == writeback["todo_id"]) + assert monitor["result_hash"] == writeback["result_hash"], monitor + assert int(monitor["consecutive_no_change"]) == writeback["consecutive_no_change"], monitor + + def run_index_records(registry_path: Path) -> list[dict]: runtime = runtime_root_from_registry(registry_path) assert runtime is not None, registry_path @@ -361,6 +378,7 @@ def assert_unchanged_writeback() -> None: assert "turn_instance_id" not in payload, payload assert "replayed" not in payload, payload writeback = payload["todo_writeback"] + assert_provider_monitor_state(registry_path, writeback) assert writeback["todo_id"] == TODO_ID, payload assert writeback["consecutive_no_change"] == 5, payload item = find_todo(state_file, TODO_ID) @@ -563,6 +581,7 @@ def assert_material_transition_followup() -> None: assert payload["ok"] is True, payload assert payload["material_change"] is True, payload writeback = payload["todo_writeback"] + assert_provider_monitor_state(registry_path, writeback) assert writeback["consecutive_no_change"] == 0, payload assert writeback["next_todos"], payload monitor = find_todo(state_file, TODO_ID) @@ -1172,6 +1191,7 @@ def assert_writeback_helper_preview_contract() -> None: runtime_root=runtime_root_from_registry(registry_path), goal_id=GOAL_ID, generated_at=generated_at, + agent_id=AGENT_ID, execute=False, todo_id=TODO_ID, result_hash="old", diff --git a/examples/control_plane/todo-durability-fixture-smoke.py b/examples/control_plane/todo-durability-fixture-smoke.py index 6fc1c3cdbd..d504b09509 100644 --- a/examples/control_plane/todo-durability-fixture-smoke.py +++ b/examples/control_plane/todo-durability-fixture-smoke.py @@ -14,6 +14,7 @@ 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.status import parse_active_state_todos # noqa: E402 from loopx.rollout_event_log import ( # noqa: E402 append_rollout_event, @@ -49,16 +50,18 @@ def run_cli(*args: str, registry_path: Path, runtime: Path) -> dict: *args, ], cwd=REPO_ROOT, - check=True, + check=False, capture_output=True, text=True, ) + assert result.returncode == 0, result.stdout + result.stderr return json.loads(result.stdout) 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 ( @@ -83,7 +86,8 @@ def state_text() -> str: ) -def write_fixture(root: Path) -> tuple[Path, Path]: +def write_fixture(root: Path, *, provider: str = "file") -> tuple[Path, Path]: + root = root.resolve() project = root / "project" runtime = root / "runtime" state_file = f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" @@ -114,6 +118,10 @@ def write_fixture(root: Path) -> tuple[Path, Path]: "kind": "harness_self_improvement", "status": "connected-read-only", }, + "coordination": {"agent_model": "peer_v1", "registered_agents": [ + "codex-product-capability", "codex-side-bypass", + "codex-reviewer", "codex-builder", + ]}, "authority_sources": [], "quota": { "compute": 1.0, @@ -129,6 +137,20 @@ def write_fixture(root: Path) -> tuple[Path, Path]: + "\n", encoding="utf-8", ) + # Import the full cold source, including all historical archive rows. + saved = run_cli("backup-state", "--project", str(project), + "--output-dir", str(root / "backups"), "--backup-id", "durability-source", + "--current-project-only", "--no-skills", "--no-automations", "--execute", + registry_path=registry_path, runtime=runtime) + prepared = run_cli("coordination-shadow", "prepare-import", "--goal-id", GOAL_ID, + "--operation-id", "durability-import", "--backup-manifest", saved["manifest_path"], + "--provider", provider, "--target-handoff-mode", "soft_claim", + registry_path=registry_path, runtime=runtime)["cold_import"] + assert prepared["status"] == "prepared", prepared + applied = run_cli("coordination-shadow", "apply-import", "--goal-id", GOAL_ID, + "--operation-id", "durability-import", "--plan-sha256", prepared["plan_sha256"], + "--writers-stopped", "--execute", registry_path=registry_path, runtime=runtime)["cold_import"] + assert applied["status"] == "applied", applied return registry_path, runtime @@ -273,13 +295,17 @@ def assert_pr_resume_repository_binding() -> None: ], ambiguous -def main() -> int: +def main(*, provider: str = "file") -> 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"]) with tempfile.TemporaryDirectory(prefix="loopx-todo-durability-") as tmp: - registry_path, runtime = write_fixture(Path(tmp)) + registry_path, runtime = write_fixture(Path(tmp), provider=provider) + source = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert source is not None + archived = {row["todo_id"]: row for row in source["todos"] if row.get("archive_state") == "archive"} + assert set(archived) == {f"todo_archive_{index}" for index in range(1, ARCHIVED_DONE_TODOS + 1)} status_payload = run_cli("status", registry_path=registry_path, runtime=runtime) item = attention_item(status_payload) @@ -324,11 +350,17 @@ def main() -> int: assert indexed_handoff["handoff_note"]["to_agent"] == "codex-reviewer", indexed_handoff guard = run_cli("quota", "should-run", "--goal-id", GOAL_ID, registry_path=registry_path, runtime=runtime) - assert guard["should_run"] is True, guard + # A registered multi-Agent Goal cannot execute an anonymous guard. + assert guard["decision"] == "automation_prompt_upgrade", guard + assert guard["should_run"] is False, guard assert_parseable_agent_todos(guard["agent_todo_summary"], hot_path=True) assert "completed_todo_archive_warning" not in guard, guard - first_todo_id = guard["agent_todo_summary"]["first_executable_items"][0]["todo_id"] + scoped_guard = run_cli("quota", "should-run", "--goal-id", GOAL_ID, + "--agent-id", "codex-product-capability", "--codex-app", + registry_path=registry_path, runtime=runtime) + assert scoped_guard["should_run"] is True, scoped_guard + first_todo_id = scoped_guard["agent_todo_summary"]["first_executable_items"][0]["todo_id"] lifecycle_result = run_cli( "todo", "complete", @@ -338,6 +370,8 @@ def main() -> int: "agent", "--todo-id", first_todo_id, + "--agent-id", + "codex-product-capability", "--evidence", "fixture completed", "--next-agent-todo", @@ -362,6 +396,10 @@ def main() -> int: 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 + after = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert after is not None + assert {row["todo_id"]: row for row in after["todos"] if row.get("archive_state") == "archive"} == archived + print("todo-durability-fixture-smoke ok") return 0 diff --git a/tests/control_plane/test_canonical_completion_journeys.py b/tests/control_plane/test_canonical_completion_journeys.py index 4374a469ab..1af2f78d8e 100644 --- a/tests/control_plane/test_canonical_completion_journeys.py +++ b/tests/control_plane/test_canonical_completion_journeys.py @@ -144,3 +144,25 @@ def test_imported_archive_complete_journey(tmp_path, monkeypatch, provider): root = Path(__file__).resolve().parents[2] archive = runpy.run_path(str(root / "examples/control_plane/todo-archive-completed-smoke.py")) assert archive["main"](provider=provider) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_canonical_monitor_writeback_journey(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + monitor = runpy.run_path(str(root / "examples/control_plane/monitor-poll-writeback-smoke.py")) + original = monitor["write_fixture"] + + def write_fixture(path, **options): + return original(path, provider=provider, **options) + + monkeypatch.setitem(monitor["main"].__globals__, "write_fixture", write_fixture) + assert monitor["main"]() == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_imported_durability_journey(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + durability = runpy.run_path(str(root / "examples/control_plane/todo-durability-fixture-smoke.py")) + assert durability["main"](provider=provider) == 0 From e7ba19e7fe75379ed1b13b0d3b6bce488d193046 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 04:15:15 +0800 Subject: [PATCH 36/54] test(todos): qualify canonical CLI, contention and user gate journeys Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- examples/control_plane/todo-cli-smoke.py | 131 +++++++++++------- .../todo-concurrent-write-lock-smoke.py | 86 ++++++++---- .../todo-user-gate-scope-smoke.py | 123 +++++++++------- .../test_canonical_completion_journeys.py | 24 ++++ 4 files changed, 243 insertions(+), 121 deletions(-) diff --git a/examples/control_plane/todo-cli-smoke.py b/examples/control_plane/todo-cli-smoke.py index 693129e18f..04329b5289 100644 --- a/examples/control_plane/todo-cli-smoke.py +++ b/examples/control_plane/todo-cli-smoke.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Smoke-test the agent-facing todo add command.""" +"""Exercise canonical Todo CLI CRUD, duplicate rejection and soft claims.""" from __future__ import annotations @@ -19,6 +19,9 @@ ) from loopx.status import parse_active_state_todos # noqa: E402 from loopx.quota import build_quota_should_run # noqa: E402 +from tests.control_plane.canonical_authority_fixture import ( # noqa: E402 + initialize_canonical_registry_fixture, +) GOAL_ID = "todo-cli-goal" @@ -34,7 +37,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, provider: str = "file") -> tuple[Path, Path]: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -48,6 +51,8 @@ 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" "## Next Action\n\n" "- Choose the next bounded step.\n", encoding="utf-8", @@ -64,6 +69,7 @@ def write_fixture(root: Path, *, register_agents: bool = True) -> tuple[Path, Pa else None, quota_allowed_slots=None, ) + initialize_canonical_registry_fixture(registry_path, provider=provider) return registry_path, state_file @@ -82,12 +88,12 @@ def run_cli_error(registry_path: Path, *args: str) -> dict: return payload -def main() -> int: +def main(*, provider: str = "file") -> int: 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) - claim_state_registry_path, claim_state_file = write_fixture(root / "claim-state") + registry_path, state_file = write_fixture(root, provider=provider) + unregistered_registry_path, _ = write_fixture(root / "unregistered", register_agents=False, provider=provider) + claim_state_registry_path, claim_state_file = write_fixture(root / "claim-state", provider=provider) original = state_file.read_text(encoding="utf-8") blocked_todo = run_cli( @@ -119,10 +125,9 @@ 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 "requires status=open" in blocked_claim["error"], blocked_claim assert claim_state_file.read_text(encoding="utf-8") == before_rejected_claim bare_user_error = run_cli_error( @@ -213,7 +218,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 + assert "updated_at: 2026-01-01T00:00:00+00:00" in after_user + assert user_payload["updated_at"] is not None duplicate = run_cli( registry_path, @@ -271,8 +277,8 @@ def main() -> int: assert agent_payload["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( - legacy_registry_path, + unregistered_agent = run_cli( + unregistered_registry_path, "todo", "add", "--goal-id", @@ -283,35 +289,44 @@ def main() -> int: AGENT_TODO, ) # 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 - legacy_claim = run_cli_error( - legacy_registry_path, + assert unregistered_agent["added"] is True, unregistered_agent + assert not unregistered_agent.get("note"), unregistered_agent + unregistered_claim = run_cli_error( + unregistered_registry_path, "todo", "claim", "--goal-id", GOAL_ID, "--todo-id", - legacy_agent["todo_id"], + unregistered_agent["todo_id"], "--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", - ) + assert unregistered_claim["error_code"] == "actor_not_registered", unregistered_claim + assert unregistered_claim["claimed_by"] == "codex-main-control", unregistered_claim + # Canonical creation cannot silently change an existing task. The + # duplicate must leave both authority and its display unchanged. + before_duplicate = state_file.read_bytes() + conflicting_duplicate = run_cli_error( + registry_path, "todo", "add", "--goal-id", GOAL_ID, + "--role", "agent", "--text", AGENT_TODO, + "--note", UPDATED_AGENT_TODO_NOTE, + ) + assert conflicting_duplicate["error_code"] == "todo_semantic_duplicate_conflict", conflicting_duplicate + assert state_file.read_bytes() == before_duplicate + unchanged = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", agent_payload["todo_id"]) + assert unchanged["todo"]["note"] == AGENT_TODO_NOTE, unchanged metadata_payload = run_cli( registry_path, "todo", - "add", + "update", "--goal-id", GOAL_ID, - "--role", - "agent", + "--todo-id", + agent_payload["todo_id"], + "--agent-id", + "codex-main-control", "--text", AGENT_TODO, "--task-class", @@ -331,12 +346,10 @@ 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_payload["todo_update"]["todo"]["note"] == UPDATED_AGENT_TODO_NOTE, metadata_payload assert ( metadata_payload["continuation_policy"] == "independent_handoff" ), metadata_payload @@ -364,8 +377,10 @@ def main() -> int: in after_metadata ) fields = parse_active_state_todos(state_file.read_text(encoding="utf-8")) - assert fields["user_todos"]["items"][0]["text"] == USER_TODO, fields - assert fields["user_todos"]["items"][0]["todo_id"].startswith("todo_"), fields + user_item = next(item for item in fields["user_todos"]["items"] + if item["todo_id"] == user_payload["todo_id"]) + assert user_item["text"] == USER_TODO, fields + assert user_item["global_gate"] is True, fields scoped_user_item = next( item for item in fields["user_todos"]["items"] if item["text"] == SCOPED_USER_GATE ) @@ -654,7 +669,7 @@ def main() -> int: assert claim_payload["changed"] is True, claim_payload assert claim_payload["claimed_by"] == "codex-main-control", claim_payload claimed_fields = parse_active_state_todos(state_file.read_text(encoding="utf-8")) - claimed_item = claimed_fields["agent_todos"]["items"][0] + claimed_item = next(item for item in claimed_fields["agent_todos"]["items"] if item["todo_id"] == metadata_payload["todo_id"]) assert claimed_item["claimed_by"] == "codex-main-control", claimed_item assert claimed_fields["agent_todos"]["claimed_open_count"] == 2, claimed_fields assert claimed_fields["agent_todos"]["claimed_advancement_open_count"] == 1, claimed_fields @@ -711,7 +726,7 @@ def main() -> int: assert preserve_claim_payload["changed"] is True, preserve_claim_payload assert preserve_claim_payload["claimed_by"] == "codex-main-control", preserve_claim_payload preserved_claim_fields = parse_active_state_todos(state_file.read_text(encoding="utf-8")) - preserved_claim_item = preserved_claim_fields["agent_todos"]["items"][0] + preserved_claim_item = next(item for item in preserved_claim_fields["agent_todos"]["items"] if item["todo_id"] == metadata_payload["todo_id"]) assert preserved_claim_item["claimed_by"] == "codex-main-control", preserved_claim_item assert preserved_claim_item["task_class"] == "advancement_task", preserved_claim_item assert preserved_claim_item["action_kind"] == "run_eval", preserved_claim_item @@ -736,8 +751,8 @@ def main() -> int: "--agent-id", "codex-side-bypass", ) - assert "cannot claim" in conflicting_claim["error"], conflicting_claim - assert "claimed_by='codex-main-control'" in conflicting_claim["error"], conflicting_claim + assert conflicting_claim["error_code"] == "claim_owner_mismatch", conflicting_claim + assert conflicting_claim["claim_owner"] == "codex-main-control", conflicting_claim clear_claim_payload = run_cli( registry_path, @@ -753,17 +768,19 @@ def main() -> int: ) assert clear_claim_payload["changed"] is True, clear_claim_payload cleared_fields = parse_active_state_todos(state_file.read_text(encoding="utf-8")) - assert "claimed_by" not in cleared_fields["agent_todos"]["items"][0], cleared_fields + assert "claimed_by" not in next(item for item in cleared_fields["agent_todos"]["items"] if item["todo_id"] == metadata_payload["todo_id"]), cleared_fields + # Handwritten content inside a machine-owned region must not be + # silently discarded. Business state can commit while display delivery + # remains pending, then recover from the same canonical head. + valid_display = state_file.read_text(encoding="utf-8") stale_continuation = " Stale continuation after structured metadata." - state_lines = state_file.read_text(encoding="utf-8").splitlines() - metadata_index = next( - index - for index, line in enumerate(state_lines) - if f"todo_id={metadata_payload['todo_id']}" in line - ) + state_lines = valid_display.splitlines() + metadata_index = next(index for index, line in enumerate(state_lines) + if f"todo_id={metadata_payload['todo_id']}" in line) state_lines.insert(metadata_index + 1, stale_continuation) - state_file.write_text("\n".join(state_lines) + "\n", encoding="utf-8") + malformed_display = "\n".join(state_lines) + "\n" + state_file.write_text(malformed_display, encoding="utf-8") update_payload = run_cli( registry_path, @@ -785,6 +802,24 @@ def main() -> int: assert update_payload["status_changed"] is False, update_payload assert update_payload["todo"] == UPDATED_AGENT_TODO, update_payload + assert update_payload["projection_delivery"] == "pending", update_payload + assert update_payload["projection_outbox"]["reason_code"] == "todo_projection_render_rejected", update_payload + assert state_file.read_text(encoding="utf-8") == malformed_display + native_update = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", metadata_payload["todo_id"]) + assert native_update["todo"]["text"] == UPDATED_AGENT_TODO, native_update + revision = native_update["authority_read"]["provider_revision"] + state_file.write_text(valid_display, encoding="utf-8") + recovered = run_cli(registry_path, "todo", "project-markdown", "--goal-id", GOAL_ID, + "--provider-revision", revision, "--execute") + assert recovered["provider_revision"] == revision, recovered + assert recovered["parse_render_parity"] is True, recovered + assert stale_continuation not in state_file.read_text(encoding="utf-8") + after_recovery = run_cli(registry_path, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", metadata_payload["todo_id"]) + assert after_recovery["authority_read"]["provider_revision"] == revision, after_recovery + assert after_recovery["todo"] == native_update["todo"], after_recovery + terminal_update = run_cli_error( registry_path, "todo", @@ -818,11 +853,11 @@ def main() -> int: "--no-follow-up", ) assert completed_payload["changed"] is True, completed_payload - assert completed_payload["status_changed"] is True, completed_payload + assert completed_payload["completed"] is True, completed_payload + assert completed_payload["terminal_decision"]["next_todo_status"] == "done", completed_payload after_update = state_file.read_text(encoding="utf-8") assert f"- [x] {UPDATED_AGENT_TODO}" in after_update, after_update assert "Summarize the read-only evidence after the user" not in after_update, after_update - assert stale_continuation not in after_update, after_update assert "status=done" in after_update, after_update assert "Validated%20compact%20evidence%20summary." in after_update, after_update updated_fields = parse_active_state_todos(after_update) diff --git a/examples/control_plane/todo-concurrent-write-lock-smoke.py b/examples/control_plane/todo-concurrent-write-lock-smoke.py index ee88eb10eb..51724ef52b 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.""" +"""Canonical writers commit independently of a contended display lock.""" from __future__ import annotations @@ -15,18 +15,20 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) -from loopx.file_lock import exclusive_file_lock # noqa: E402 +from loopx.file_lock import exclusive_cross_runtime_file_lock # noqa: E402 +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # 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 preserves another canonical task while display delivery waits." +PARENT_ADD_TODO = "Parent task commits while child add waits for display delivery." 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 task commits while child update waits for display delivery." -def write_fixture(root: Path) -> tuple[Path, Path]: +def write_fixture(root: Path, *, provider: str = "file") -> tuple[Path, Path]: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -73,6 +75,7 @@ def write_fixture(root: Path) -> tuple[Path, Path]: + "\n", encoding="utf-8", ) + initialize_canonical_registry_fixture(registry_path, provider=provider) return registry_path, state_file @@ -116,16 +119,21 @@ def wait_json(process: subprocess.Popen[str]) -> dict[str, Any]: return json.loads(stdout) -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" - ) - - -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_commit(registry: Path, process: subprocess.Popen[str], text: str, + *, status: str = "open") -> None: + runtime = Path(json.loads(registry.read_text())["common_runtime_root"]) + deadline = time.monotonic() + 10 + while time.monotonic() < deadline: + readback = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert readback and readback["legacy_fallback_used"] is False, readback + if any(row["text"] == text and row["status"] == status for row in readback["todos"]): + # The provider committed, but display delivery must wait for the + # shared cross-runtime lock, not overwrite the parent's narrative. + assert process.poll() is None + return + assert process.poll() is None, process.communicate() + time.sleep(0.05) + raise AssertionError(f"canonical commit did not become visible: {text}") def agent_items(state_file: Path) -> list[dict[str, Any]]: @@ -137,11 +145,11 @@ def find_item(state_file: Path, text: str) -> dict[str, Any]: return next(item for item in agent_items(state_file) if item.get("text") == text) -def main() -> int: +def main(*, provider: str = "file") -> int: with tempfile.TemporaryDirectory(prefix="loopx-todo-lock-smoke-") as tmp: - registry_path, state_file = write_fixture(Path(tmp)) + registry_path, state_file = write_fixture(Path(tmp), provider=provider) - with exclusive_file_lock(state_file): + with exclusive_cross_runtime_file_lock(state_file, operation="fixture_narrative_edit"): add_process = start_cli( registry_path, "todo", @@ -153,10 +161,19 @@ def main() -> int: "--text", CHILD_ADD_TODO, ) - assert_waiting_on_lock(add_process, "todo add") - append_agent_todo(state_file, PARENT_ADD_TODO) + wait_for_commit(registry_path, add_process, CHILD_ADD_TODO) + parent_process = start_cli( + registry_path, "todo", "add", "--goal-id", GOAL_ID, + "--role", "agent", "--text", PARENT_ADD_TODO, + ) + wait_for_commit(registry_path, parent_process, PARENT_ADD_TODO) + state_file.write_text(state_file.read_text().replace( + "Exercise todo writer locking.", "Narrative edited while both displays wait.")) + parent_result = wait_json(parent_process) + assert parent_result["added"] is True, parent_result add_result = wait_json(add_process) assert add_result["added"] is True, add_result + assert "Narrative edited while both displays wait." in state_file.read_text() 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") @@ -173,7 +190,7 @@ def main() -> int: ) target_id = target["todo_id"] - with exclusive_file_lock(state_file): + with exclusive_cross_runtime_file_lock(state_file, operation="fixture_narrative_edit"): update_process = start_cli( registry_path, "todo", @@ -191,16 +208,37 @@ def main() -> int: "--claimed-by", "codex-side-bypass", ) - assert_waiting_on_lock(update_process, "todo update") - append_agent_todo(state_file, PARENT_UPDATE_TODO) + wait_for_commit(registry_path, update_process, UPDATE_TARGET_TODO, status="blocked") + parent_process = start_cli( + registry_path, "todo", "add", "--goal-id", GOAL_ID, + "--role", "agent", "--text", PARENT_UPDATE_TODO, + ) + wait_for_commit(registry_path, parent_process, PARENT_UPDATE_TODO) + state_file.write_text(state_file.read_text().replace( + "Narrative edited while both displays wait.", "Narrative preserved across update and add.")) + parent_result = wait_json(parent_process) + assert parent_result["added"] is True, parent_result update_result = wait_json(update_process) assert update_result["changed"] is True, update_result + assert "Narrative preserved across update and add." in state_file.read_text() 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") + final_read = read_canonical_todos_if_promoted( + runtime_root=Path(json.loads(registry_path.read_text())["common_runtime_root"]), + goal_id=GOAL_ID, + ) + assert final_read and final_read["legacy_fallback_used"] is False, final_read + projected = {item["todo_id"]: (item["text"], item["status"], item.get("claimed_by")) + for item in agent_items(state_file)} + native = {item["todo_id"]: (item["text"], item["status"], item.get("claimed_by")) + for item in final_read["todos"]} + assert len(native) == 5, native + assert native == projected, (native, projected) + print("todo-concurrent-write-lock-smoke ok") return 0 diff --git a/examples/control_plane/todo-user-gate-scope-smoke.py b/examples/control_plane/todo-user-gate-scope-smoke.py index c3e022c8b0..c08d54d786 100644 --- a/examples/control_plane/todo-user-gate-scope-smoke.py +++ b/examples/control_plane/todo-user-gate-scope-smoke.py @@ -14,6 +14,8 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 +from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable # noqa: E402 from loopx.contract import check_contract # 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 @@ -26,9 +28,9 @@ SIDE_AGENT = "codex-product-capability" -def write_fixture(root: Path) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, provider: str | None = "file", user_rows: str = "", agent_rows: str = "") -> tuple[Path, Path, Path]: repo = root / "repo" - repo.mkdir() + repo.mkdir(parents=True) state = repo / "ACTIVE_GOAL_STATE.md" state.write_text( "\n".join( @@ -38,7 +40,10 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: "updated_at: 2026-06-26T00:00:00+00:00", "---", "", + "## User Todo", + user_rows, "## Agent Todo", + agent_rows, "", ] ) @@ -68,25 +73,22 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: ), encoding="utf-8", ) + # Seed historical malformed records before authority begins; normal writes + # below always go through the real provider. This is not import evidence. + if provider is not None: + initialize_canonical_registry_fixture(registry, provider=provider) return repo, state, registry def assert_raises_message(action: Callable[[], object], expected: str) -> None: try: action() - except ValueError as exc: + except (ValueError, LocalCoordinationAuthorityUnavailable) as exc: assert expected in str(exc), str(exc) return raise AssertionError(f"expected ValueError containing {expected!r}") -def insert_before_agent_section(state: Path, block: str) -> None: - text = state.read_text(encoding="utf-8") - marker = "\n## Agent Todo\n" - assert marker in text, text - state.write_text(text.replace(marker, f"\n{block.rstrip()}\n{marker}", 1), encoding="utf-8") - - def assert_user_action_does_not_drive_active_state_attention() -> None: item = active_state_todo_attention_item( {"id": GOAL_ID}, @@ -164,12 +166,12 @@ def assert_user_action_does_not_drive_active_state_attention() -> None: assert gate_item["waiting_on"] == "controller", gate_item -def main() -> int: +def main(*, provider: str = "file") -> int: assert_user_action_does_not_drive_active_state_attention() with tempfile.TemporaryDirectory(prefix="loopx-user-gate-scope-") as tmp: root = Path(tmp) - repo, state, registry = write_fixture(root) + repo, state, registry = write_fixture(root, provider=provider) assert_raises_message( lambda: add_goal_todo( @@ -311,7 +313,7 @@ def main() -> int: evidence="wrong-lane response attempt", dry_run=True, ), - "response continuation is bound", + "bound_agent_mismatch", ) approval_completed = complete_goal_todo( registry_path=registry, @@ -487,9 +489,9 @@ def main() -> int: lambda: update_goal_todo( registry_path=registry, goal_id=GOAL_ID, - todo_id=str(agent_todo["todo_id"]), + todo_id=str(side_agent_todo["todo_id"]), role="agent", - agent_id=PRIMARY_AGENT, + agent_id=SIDE_AGENT, global_gate=True, dry_run=True, ), @@ -504,8 +506,10 @@ def main() -> int: ) assert clean_check["ok"] is True, clean_check - insert_before_agent_section( - state, + fixtures_root = root + root = fixtures_root / "unscoped" + repo, state, registry = write_fixture( + root, provider=provider, user_rows= "- [ ] Approve an intentionally unscoped gate.\n" " ", ) @@ -535,8 +539,9 @@ def main() -> int: ) assert repaired_check["ok"] is True, repaired_check - insert_before_agent_section( - state, + root = fixtures_root / "unbound" + repo, state, registry = write_fixture( + root, provider=provider, user_rows= "- [ ] Track an unbound non-blocking user action.\n" " ", ) @@ -568,8 +573,9 @@ def main() -> int: ) assert rebound_check["ok"] is True, rebound_check - insert_before_agent_section( - state, + root = fixtures_root / "mismatched" + repo, state, registry = write_fixture( + root, provider=provider, user_rows= "- [ ] Resolve a mismatched gate binding.\n" f" ", ) @@ -618,6 +625,14 @@ def main() -> int: 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 + assert_raises_message( + lambda: update_goal_todo( + registry_path=registry, goal_id=GOAL_ID, + todo_id="todo_untyped_user", role="user", + agent_id=PRIMARY_AGENT, status="done", + ), + "user todo requires explicit --task-class", + ) closed_untyped = update_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -625,11 +640,14 @@ def main() -> int: role="user", agent_id=PRIMARY_AGENT, status="done", + task_class="user_action", + bound_agent=PRIMARY_AGENT, ) assert closed_untyped["changed"] is True, closed_untyped - insert_before_agent_section( - state, + root = fixtures_root / "invalidscope" + repo, state, registry = write_fixture( + root, provider=provider, user_rows= "- [ ] Approve a gate with an unregistered blocked agent.\n" " \n" "- [ ] Approve a gate with conflicting scope.\n" @@ -645,17 +663,15 @@ def main() -> int: 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" + root = fixtures_root / "hard-cut" + repo, state, registry = write_fixture( + root, provider=provider, agent_rows= + "\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", + + " \n", ) hard_cut_check = check_contract( registry_path=registry, @@ -664,22 +680,13 @@ def main() -> int: 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 + for todo_id, code in ( + ("todo_removed_continuation", "agent_todo_removed_continuation_policy"), + ("todo_removed_agent_gate", "agent_todo_blocks_agent_invalid"), + ("todo_unknown_exclusion", "todo_excludes_unregistered_agent"), + ): + assert any(item["code"] == code and todo_id in item["message"] + for item in hard_cut_check["error_diagnostics"]), hard_cut_check repaired_agent_gate = update_goal_todo( registry_path=registry, @@ -701,6 +708,24 @@ def main() -> int: for item in repaired_hard_cut_check["errors"] ), repaired_hard_cut_check + # Invalid wire syntax belongs to the retained source inspector. Do not + # normalize it into a canonical fixture and claim the error survived. + root = fixtures_root / "malformed-source" + repo, state, registry = write_fixture( + root, provider=None, agent_rows= + "- [ ] Repair malformed executor exclusion metadata.\n" + " \n", + ) + before_inspection = state.read_bytes() + malformed_check = check_contract( + registry_path=registry, runtime_root_override=str(root / "runtime"), + scan_roots=[repo], limit=1, + ) + assert malformed_check["ok"] is False, malformed_check + assert any("todo_malformed_exclusion" in item and "malformed excluded_agents" in item + for item in malformed_check["errors"]), malformed_check + assert state.read_bytes() == before_inspection + print("todo-user-gate-scope-smoke ok") return 0 diff --git a/tests/control_plane/test_canonical_completion_journeys.py b/tests/control_plane/test_canonical_completion_journeys.py index 1af2f78d8e..98c1b07317 100644 --- a/tests/control_plane/test_canonical_completion_journeys.py +++ b/tests/control_plane/test_canonical_completion_journeys.py @@ -166,3 +166,27 @@ def test_imported_durability_journey(tmp_path, monkeypatch, provider): root = Path(__file__).resolve().parents[2] durability = runpy.run_path(str(root / "examples/control_plane/todo-durability-fixture-smoke.py")) assert durability["main"](provider=provider) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_canonical_todo_cli_journey(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + cli = runpy.run_path(str(root / "examples/control_plane/todo-cli-smoke.py")) + assert cli["main"](provider=provider) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_canonical_concurrent_writer_journey(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + concurrency = runpy.run_path(str(root / "examples/control_plane/todo-concurrent-write-lock-smoke.py")) + assert concurrency["main"](provider=provider) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_canonical_user_gate_scope_journey(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + gates = runpy.run_path(str(root / "examples/control_plane/todo-user-gate-scope-smoke.py")) + assert gates["main"](provider=provider) == 0 From 165ffea1b0a9f26fca4814a297989d0730f3d849 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 04:25:55 +0800 Subject: [PATCH 37/54] refactor(todos): retire unused source preview decision chain Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../todo-write-correctness-smoke.py | 334 +++++------------- .../runtime/local_state_write_correctness.py | 143 -------- .../control_plane/todos/write_correctness.py | 42 --- loopx/todos.py | 2 - .../test_canonical_completion_journeys.py | 8 + .../test_canonical_todo_writer_isolation.py | 5 + 6 files changed, 106 insertions(+), 428 deletions(-) delete mode 100644 loopx/control_plane/todos/write_correctness.py diff --git a/examples/control_plane/todo-write-correctness-smoke.py b/examples/control_plane/todo-write-correctness-smoke.py index 9dceb28031..11a85d11cf 100644 --- a/examples/control_plane/todo-write-correctness-smoke.py +++ b/examples/control_plane/todo-write-correctness-smoke.py @@ -1,34 +1,31 @@ #!/usr/bin/env python3 -"""Smoke-test todo add/update dry-run write correctness packets.""" - +"""Qualify canonical Todo preview, revision and claim boundaries through the CLI.""" from __future__ import annotations -import hashlib import json import subprocess import sys import tempfile from pathlib import Path - 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.runtime.local_state_write_correctness import ( # noqa: E402 - shadow_validate_local_state_write_correctness_packet, +from loopx.control_plane.coordination.local_authority import ( # noqa: E402 + read_canonical_todos_if_promoted, ) -from loopx.control_plane.testing.canary_harness import ( # noqa: E402 - run_json_cli, - write_fixture_registry, +from loopx.control_plane.testing.canary_harness import write_fixture_registry # noqa: E402 +from tests.control_plane.canonical_authority_fixture import ( # noqa: E402 + initialize_canonical_registry_fixture, ) GOAL_ID = "todo-write-correctness-goal" AGENT_ID = "codex-product-capability" -TODO_TEXT = "Preview a todo write correctness packet before mutating state." +TODO_TEXT = "Preview a canonical Todo before committing it." -def write_fixture(root: Path) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, provider: str = "file") -> tuple[Path, Path, Path]: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -40,6 +37,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: "updated_at: 2026-01-01T00:00:00+00:00\n" "---\n\n" "# Todo Write Correctness Fixture\n\n" + "## User Todo / Owner Review Reading Queue\n\n" "## Agent Todo\n\n" "- [ ] Existing todo for update preview.\n" " \n", @@ -55,244 +53,98 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: registered_agents=("codex-main-control", AGENT_ID), quota_allowed_slots=None, ) + initialize_canonical_registry_fixture(registry_path, provider=provider) return registry_path, state_file, project -def run_cli(registry_path: Path, *args: str, as_json: bool = True) -> dict | str: - if as_json: - return run_json_cli( - *args, - registry_path=registry_path, - include_returncode=False, - ) - command = [ - sys.executable, - "-m", - "loopx.cli", - "--registry", - str(registry_path), - ] - command.extend(args) + +def run_cli(registry: Path, *args: str, succeeds: bool = True) -> dict: result = subprocess.run( - command, - cwd=REPO_ROOT, - check=True, - text=True, - capture_output=True, + [sys.executable, "-m", "loopx.cli", "--registry", str(registry), + "--format", "json", "todo", *args, "--goal-id", GOAL_ID], + cwd=REPO_ROOT, capture_output=True, text=True, timeout=45, ) - 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 + assert (result.returncode == 0) is succeeds, result.stdout + result.stderr + return json.loads(result.stdout) -def main() -> int: +def main(*, provider: str = "file") -> 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" - ) + root = Path(tmp) + registry, display, _ = write_fixture(root, provider=provider) + + def snapshot(): + return read_canonical_todos_if_promoted( + runtime_root=root / "runtime", goal_id=GOAL_ID, include_leases=True + ) + + before, original_display = snapshot(), display.read_bytes() + create = ("add", "--role", "agent", "--text", TODO_TEXT, + "--claimed-by", AGENT_ID, + "--operation-id", "preview-create") + preview = run_cli(registry, *create, "--dry-run") + repeat = run_cli(registry, *create, "--dry-run") + for result in (preview, repeat): + assert result["dry_run"] is True and result["changed"] is False, result + assert result["provider_status"] == "planned", result + assert result["source_authority"] == f"{provider}_v0", result + assert result["decision_read_from_provider"] is True, result + assert result["legacy_fallback_used"] is False, result + assert "local_state_write_correctness" not in result, result + assert repeat["todo_id"] == preview["todo_id"] + assert snapshot() == before and display.read_bytes() == original_display + + # Preview must not consume the operation id or fabricate a commit receipt. + accepted = run_cli(registry, *create) + assert accepted["changed"] is True and accepted["provider_status"] == "applied", accepted + assert accepted["todo_id"] == preview["todo_id"] + after_create = snapshot() + assert len(after_create["todos"]) == len(before["todos"]) + 1 + created = next(t for t in after_create["todos"] if t["todo_id"] == accepted["todo_id"]) + assert created["text"] == TODO_TEXT and created["claimed_by"] == AGENT_ID + assert run_cli(registry, *create)["provider_status"] == "replayed" + assert snapshot() == after_create + + update = ("update", "--todo-id", accepted["todo_id"], "--agent-id", AGENT_ID, + "--note", "Reviewed canonical edit", "--update-operation-id", "preview-update") + revision = after_create["provider_revision"] + reviewed = (*update, "--update-expected-provider-revision", revision) + before_update_display = display.read_bytes() + planned = run_cli(registry, *reviewed, "--dry-run") + assert planned["provider_status"] == "planned" and planned["changed"] is False, planned + assert planned["todo_update"]["todo"]["note"] == "Reviewed canonical edit", planned + assert snapshot() == after_create and display.read_bytes() == before_update_display + + # A concurrent accepted operation invalidates the reviewed revision. + run_cli(registry, "add", "--role", "agent", "--text", "Concurrent independent task", + "--operation-id", "concurrent-create") + concurrent, concurrent_display = snapshot(), display.read_bytes() + for suffix in (("--dry-run",), ()): + rejected = run_cli(registry, *reviewed, *suffix, succeeds=False) + assert rejected["error_code"] == "provider_revision_mismatch", rejected + assert snapshot() == concurrent and display.read_bytes() == concurrent_display + + # Current revision is insufficient authority to edit another actor's claim. + foreign = ("update", "--todo-id", accepted["todo_id"], "--agent-id", "codex-main-control", + "--note", "Unauthorized overwrite") + for suffix in (("--dry-run",), ()): + rejected = run_cli(registry, *foreign, *suffix, succeeds=False) + assert rejected["error_code"] == "update_owner_mismatch", rejected + assert snapshot() == concurrent and display.read_bytes() == concurrent_display + + # Re-inspect and retry the original uncommitted intent at the fresh revision. + fresh = (*update, "--update-expected-provider-revision", concurrent["provider_revision"]) + assert run_cli(registry, *fresh, "--dry-run")["provider_status"] == "planned" + assert snapshot() == concurrent + committed = run_cli(registry, *fresh) + assert committed["provider_status"] == "applied" and committed["changed"] is True, committed + final, final_display = snapshot(), display.read_bytes() + updated = next(t for t in final["todos"] if t["todo_id"] == accepted["todo_id"]) + assert updated["note"] == "Reviewed canonical edit" + assert updated["claimed_by"] == AGENT_ID + replay = run_cli(registry, *fresh) + assert replay["provider_status"] == "replayed" and replay["changed"] is False, replay + assert snapshot() == final and display.read_bytes() == final_display print("todo-write-correctness-smoke ok") return 0 diff --git a/loopx/control_plane/runtime/local_state_write_correctness.py b/loopx/control_plane/runtime/local_state_write_correctness.py index 4fd72cd17f..f8e2f26310 100644 --- a/loopx/control_plane/runtime/local_state_write_correctness.py +++ b/loopx/control_plane/runtime/local_state_write_correctness.py @@ -2,7 +2,6 @@ import hashlib import json -import re from typing import Any @@ -98,145 +97,3 @@ def build_local_state_write_correctness_dry_run_packet( }, }, } - - -def shadow_validate_local_state_write_correctness_packet( - packet: dict[str, Any], - *, - current_state_text: str, - observed_lease_ref: dict[str, Any] | None = None, -) -> dict[str, Any]: - """Preview revision and lease conflicts without changing write behavior.""" - - validated = json.loads(json.dumps(packet, ensure_ascii=False)) - intent = validated.get("write_intent") if isinstance(validated, dict) else {} - if not isinstance(intent, dict): - return validated - apply_result = validated.setdefault("apply_result", {}) - if not isinstance(apply_result, dict): - apply_result = {} - validated["apply_result"] = apply_result - apply_result.setdefault("status", "preview_only") - apply_result.setdefault("applied_revision", None) - apply_result.setdefault("duplicate_of", None) - apply_result.setdefault("conflict", None) - - expected_revision = intent.get("expected_revision") - current_revision = active_state_revision(current_state_text) - if ( - isinstance(expected_revision, dict) - and expected_revision.get("value") - and expected_revision.get("value") != current_revision["value"] - ): - apply_result.update( - { - "status": "revision_conflict", - "applied_revision": None, - "duplicate_of": None, - "conflict": { - "kind": "revision_conflict", - "expected_revision": expected_revision, - "current_revision": current_revision, - }, - } - ) - return validated - - expected_lease_ref = intent.get("lease_ref") - if _lease_refs_conflict(expected_lease_ref, observed_lease_ref): - apply_result.update( - { - "status": "lease_conflict", - "applied_revision": None, - "duplicate_of": None, - "conflict": { - "kind": "lease_conflict", - "expected_lease_ref": expected_lease_ref, - "observed_lease_ref": observed_lease_ref, - }, - } - ) - return validated - - -def _lease_refs_conflict(expected: Any, observed: Any) -> bool: - if not isinstance(expected, dict) or not isinstance(observed, dict): - return False - if str(expected.get("kind") or "") != str(observed.get("kind") or ""): - return False - if str(expected.get("goal_id") or "") != str(observed.get("goal_id") or ""): - return False - if str(expected.get("todo_id") or "") != str(observed.get("todo_id") or ""): - return False - expected_holder = str(expected.get("claimed_by") or "") - observed_holder = str(observed.get("claimed_by") or "") - return bool(expected_holder and observed_holder and expected_holder != observed_holder) - - -def _todo_lease_ref( - *, - goal_id: str, - todo_id: str | None, - claimed_by: str | None, -) -> dict[str, str] | None: - if not todo_id or not claimed_by: - return None - safe_todo_id = re.sub(r"[^A-Za-z0-9_]+", "_", todo_id).strip("_") or "todo" - safe_claimed_by = re.sub(r"[^A-Za-z0-9_]+", "_", claimed_by).strip("_") or "agent" - return { - "kind": "todo_claim", - "goal_id": goal_id, - "todo_id": todo_id, - "claimed_by": claimed_by, - "lease_id": f"lease_{safe_todo_id}_{safe_claimed_by}", - } - - -def _todo_lease_projection(lease_ref: dict[str, str] | None) -> dict[str, str] | None: - if not lease_ref: - return None - return { - "todo_id": lease_ref["todo_id"], - "claimed_by": lease_ref["claimed_by"], - "lease_state": "preview_only", - } - - -def build_todo_write_correctness_dry_run_packet( - *, - goal_id: str, - write_class: str, - state_text: str, - todo_id: str | None, - role: str | None, - section: str | None, - claimed_by: str | None, - changed: bool, -) -> dict[str, Any]: - target = todo_id or "new_todo" - role_text = role or "unknown_role" - effect = "would change active state" if changed else "would leave active state unchanged" - patch_summary = f"preview {write_class} for {role_text} todo {target}: {effect}" - lease_ref = _todo_lease_ref( - goal_id=goal_id, - todo_id=todo_id, - claimed_by=claimed_by, - ) - return build_local_state_write_correctness_dry_run_packet( - goal_id=goal_id, - writer_id="loopx.todo", - write_class=write_class, - state_text=state_text, - target_refs={ - "state_file_ref": "registry.goal.state_file", - "todo_id": todo_id, - "role": role, - "section": section, - }, - patch_summary=patch_summary, - expected_write_scopes=["active_state"], - lease_ref=lease_ref, - lease_projection=_todo_lease_projection(lease_ref), - narrower_lock_allowed="per_todo_when_patch_is_single_todo_and_order_independent", - projection_status_surface=patch_summary, - ) diff --git a/loopx/control_plane/todos/write_correctness.py b/loopx/control_plane/todos/write_correctness.py deleted file mode 100644 index 44503a013c..0000000000 --- a/loopx/control_plane/todos/write_correctness.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from ..runtime.local_state_write_correctness import ( - build_todo_write_correctness_dry_run_packet, -) -from .contract import normalize_todo_claimed_by, normalize_todo_id - - -def attach_todo_write_correctness_dry_run_packet( - payload: dict[str, Any], - *, - goal_id: str, - write_class: str, - state_text: str, -) -> dict[str, Any]: - if not payload.get("dry_run"): - return payload - changed = any( - payload.get(field) - for field in ( - "changed", - "added", - "metadata_updated", - "completed", - "superseded", - ) - ) - payload["local_state_write_correctness"] = ( - build_todo_write_correctness_dry_run_packet( - goal_id=goal_id, - write_class=write_class, - state_text=state_text, - todo_id=normalize_todo_id(payload.get("todo_id")), - role=str(payload.get("role") or ""), - section=str(payload.get("section") or ""), - claimed_by=normalize_todo_claimed_by(payload.get("claimed_by")), - changed=changed, - ) - ) - return payload diff --git a/loopx/todos.py b/loopx/todos.py index cc647b1ff0..2bae3d483f 100644 --- a/loopx/todos.py +++ b/loopx/todos.py @@ -110,7 +110,6 @@ "apply_completed_user_todo_lifecycle": "loopx.control_plane.todos.unblock_resume", "completion_decision_target": "loopx.control_plane.todos.unblock_resume", "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", @@ -136,7 +135,6 @@ _ALIASES = { "completion_validation_module": "completion_validation", "todo_monitor_metadata": "monitor_metadata", - "_attach_todo_write_correctness_dry_run_packet": "attach_todo_write_correctness_dry_run_packet", } diff --git a/tests/control_plane/test_canonical_completion_journeys.py b/tests/control_plane/test_canonical_completion_journeys.py index 98c1b07317..fd8ed66579 100644 --- a/tests/control_plane/test_canonical_completion_journeys.py +++ b/tests/control_plane/test_canonical_completion_journeys.py @@ -190,3 +190,11 @@ def test_canonical_user_gate_scope_journey(tmp_path, monkeypatch, provider): root = Path(__file__).resolve().parents[2] gates = runpy.run_path(str(root / "examples/control_plane/todo-user-gate-scope-smoke.py")) assert gates["main"](provider=provider) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_canonical_write_correctness_journey(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + preview = runpy.run_path(str(root / "examples/control_plane/todo-write-correctness-smoke.py")) + assert preview["main"](provider=provider) == 0 diff --git a/tests/control_plane/test_canonical_todo_writer_isolation.py b/tests/control_plane/test_canonical_todo_writer_isolation.py index 07ff44aea1..590195de0a 100644 --- a/tests/control_plane/test_canonical_todo_writer_isolation.py +++ b/tests/control_plane/test_canonical_todo_writer_isolation.py @@ -468,6 +468,11 @@ def test_retired_source_writer_exports_are_absent(tmp_path): from importlib.util import find_spec assert find_spec('loopx.control_plane.todos.line_update') is None assert find_spec('loopx.control_plane.todos.legacy_mutation') is None +assert find_spec('loopx.control_plane.todos.write_correctness') is None +assert not hasattr(todos, '_attach_todo_write_correctness_dry_run_packet') +from loopx.control_plane.runtime import local_state_write_correctness as correctness +assert not hasattr(correctness, 'build_todo_write_correctness_dry_run_packet') +assert not hasattr(correctness, 'shadow_validate_local_state_write_correctness_packet') for name in ('apply_todo_update_to_lines', 'link_generated_successor_todo_ids', 'link_superseding_todo_id', 'upsert_todo_metadata'): assert not hasattr(todos, name), name try: From e8ecea25f91fe276888c05b4e8966260634cd656 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 04:34:43 +0800 Subject: [PATCH 38/54] test(todos): migrate validation and scheduling journeys to canonical authority Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...pletion-validation-accountability-smoke.py | 36 ++++++++++--------- .../quota-no-git-status-smoke.py | 26 ++++++++++++-- .../status-projection-cache-smoke.py | 13 ++++--- .../test_canonical_completion_journeys.py | 9 +++++ 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/examples/control_plane/completion-validation-accountability-smoke.py b/examples/control_plane/completion-validation-accountability-smoke.py index e8a0efd022..e9a115ad16 100755 --- a/examples/control_plane/completion-validation-accountability-smoke.py +++ b/examples/control_plane/completion-validation-accountability-smoke.py @@ -13,20 +13,19 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) -from loopx.control_plane.quota.slot_accounting import ( +from loopx.control_plane.quota.slot_accounting import ( # noqa: E402 build_quota_slot_preview_for_decision, ) -from loopx.control_plane.todos.active_state_todo_parser import ( - parse_active_state_todos, -) -from loopx.state_refresh import refresh_state_run -from loopx.todos import add_goal_todo, complete_goal_todo +from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted # noqa: E402 +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 +from loopx.state_refresh import refresh_state_run # noqa: E402 +from loopx.todos import add_goal_todo, complete_goal_todo # noqa: E402 GOAL_ID = "completion-validation-accountability" AGENT_ID = "codex-author" -def write_fixture(root: Path) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, provider: str = "file") -> tuple[Path, Path, Path]: project = root / "project" project.mkdir() state = project / "ACTIVE_GOAL_STATE.md" @@ -39,6 +38,8 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: "updated_at: 2026-08-15T00:00:00+00:00", "---", "", + "## User Todo / Owner Review Reading Queue", + "", "## Agent Todo", "", ] @@ -71,14 +72,14 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: ), encoding="utf-8", ) + initialize_canonical_registry_fixture(registry, provider=provider) return registry, runtime, state -def agent_summary(state: Path) -> dict: - return parse_active_state_todos( - state.read_text(encoding="utf-8"), - item_limit=None, - )["agent_todos"] +def agent_summary(runtime: Path) -> dict: + source = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id=GOAL_ID) + assert source is not None and source["decision_read_from_provider"] is True + return {"items": [item for item in source["todos"] if item["role"] == "agent"]} def quota_preview( @@ -127,12 +128,12 @@ def quota_preview( ) -def main() -> None: +def main(*, provider: str = "file") -> None: with tempfile.TemporaryDirectory( prefix="loopx-completion-validation-accountability-" ) as tmp: root = Path(tmp) - registry, runtime, state = write_fixture(root) + registry, runtime, state = write_fixture(root, provider=provider) independent_result = root / "controller-result.complete" validation_command = ( f"{shlex.quote(sys.executable)} -c " @@ -154,7 +155,7 @@ def main() -> None: ) todo_id = str(added["todo_id"]) - open_summary = agent_summary(state) + open_summary = agent_summary(runtime) projected = next( item for item in open_summary["items"] if item["todo_id"] == todo_id ) @@ -202,6 +203,9 @@ def main() -> None: evidence="provisional agent-side verifier result", ) assert blocked_completion["validation_blocked_completion"] is True + still_open = next(item for item in agent_summary(runtime)["items"] if item["todo_id"] == todo_id) + assert still_open["status"] == "open" and still_open["done"] is False + assert not (runtime / "goals" / GOAL_ID / "runs" / "index.jsonl").exists() independent_result.write_text("complete\n", encoding="utf-8") completed = complete_goal_todo( @@ -215,7 +219,7 @@ def main() -> None: assert completed["ok"] is True, completed assert completed["changed"] is True, completed - done_summary = agent_summary(state) + done_summary = agent_summary(runtime) durable = next( item for item in done_summary["items"] if item["todo_id"] == todo_id ) diff --git a/examples/control_plane/quota-no-git-status-smoke.py b/examples/control_plane/quota-no-git-status-smoke.py index 6cf51ef55d..a61573c052 100644 --- a/examples/control_plane/quota-no-git-status-smoke.py +++ b/examples/control_plane/quota-no-git-status-smoke.py @@ -5,6 +5,7 @@ import json import os +import shutil import subprocess import sys import tempfile @@ -12,11 +13,16 @@ REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 + GOAL_ID = "no-git-quota-fixture" AGENT_ID = "codex-benchmark-agent" -def write_fixture(root: Path) -> Path: +def write_fixture(root: Path, *, provider: str = "file") -> Path: project = root / "project" runtime = root / "runtime" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -29,6 +35,8 @@ def write_fixture(root: Path) -> Path: "# Active Goal State\n\n" "## Objective\n\n" "Solve this no-git fixture.\n\n" + "## User Todo / Owner Review Reading Queue\n\n" + "## Agent Todo\n\n" "## Next Action\n\n" "- Pick the seeded todo.\n", encoding="utf-8", @@ -60,6 +68,7 @@ def write_fixture(root: Path) -> Path: + "\n", encoding="utf-8", ) + initialize_canonical_registry_fixture(registry_path, provider=provider) return registry_path @@ -84,10 +93,10 @@ def run_cli(registry_path: Path, *args: str, env: dict[str, str] | None = None) return json.loads(result.stdout) -def main() -> int: +def main(*, provider: str = "file") -> int: with tempfile.TemporaryDirectory(prefix="loopx-no-git-quota-") as tmp: root = Path(tmp) - registry_path = write_fixture(root) + registry_path = write_fixture(root, provider=provider) todo = run_cli( registry_path, "todo", @@ -100,12 +109,23 @@ def main() -> int: "Solve the no-git quota fixture.", ) assert todo["ok"] is True, todo + assert todo["source_authority"] == f"{provider}_v0", todo + assert todo["legacy_fallback_used"] is False, todo no_git_bin = root / "no-git-bin" no_git_bin.mkdir() + # Canonical decisions require Node, but Git remains unavailable. + node = shutil.which("node") + assert node is not None, "Node is required for the canonical authority" + executable = no_git_bin / ("node.exe" if os.name == "nt" else "node") + try: + executable.symlink_to(Path(node).resolve()) + except OSError: + shutil.copy2(node, executable) env = os.environ.copy() env["PATH"] = str(no_git_bin) env["PYTHONPATH"] = str(REPO_ROOT) + assert shutil.which("git", path=env["PATH"]) is None quota = run_cli( registry_path, "quota", diff --git a/examples/control_plane/status-projection-cache-smoke.py b/examples/control_plane/status-projection-cache-smoke.py index 1b81901b4e..888cf61dcd 100644 --- a/examples/control_plane/status-projection-cache-smoke.py +++ b/examples/control_plane/status-projection-cache-smoke.py @@ -18,12 +18,14 @@ from loopx.control_plane.runtime.status_projection_cache import write_status_projection_cache # noqa: E402 +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 + GOAL_ID = "status-projection-cache-fixture" AGENT_ID = "codex-main-control" PRIVATE_DOC_MARKER = "https://" + "la" + "rk" + "office.example/doc" -def write_fixture(root: Path) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, provider: str = "file") -> tuple[Path, Path, Path]: project = root / "project" runtime = root / "runtime" state_rel = f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" @@ -40,6 +42,8 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: "# Status Projection Cache Fixture\n\n" "## Objective\n\n" "Validate opt-in status projection cache behavior.\n\n" + "## User Todo / Owner Review Reading Queue\n\n" + "## Agent Todo\n\n" "## Next Action\n\n" "- Exercise the status/quota projection cache.\n", encoding="utf-8", @@ -81,6 +85,7 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: + "\n", encoding="utf-8", ) + initialize_canonical_registry_fixture(registry_path, provider=provider) return registry_path, runtime, public_doc @@ -163,9 +168,9 @@ def assert_next_action(payload: dict[str, Any], expected_todo_id: str) -> None: assert next_action.get("todo_id") == expected_todo_id, payload -def main() -> int: +def main(*, provider: str = "file") -> int: with tempfile.TemporaryDirectory(prefix="loopx-status-projection-cache-") as tmp: - registry_path, runtime, public_doc = write_fixture(Path(tmp)) + registry_path, runtime, public_doc = write_fixture(Path(tmp), provider=provider) todo_payload = run_cli( registry_path, @@ -258,7 +263,7 @@ def main() -> int: "agent", "--todo-id", initial_todo_id, - "--claimed-by", + "--agent-id", AGENT_ID, "--evidence", "fixture cache state transition", diff --git a/tests/control_plane/test_canonical_completion_journeys.py b/tests/control_plane/test_canonical_completion_journeys.py index fd8ed66579..ec36478577 100644 --- a/tests/control_plane/test_canonical_completion_journeys.py +++ b/tests/control_plane/test_canonical_completion_journeys.py @@ -198,3 +198,12 @@ def test_canonical_write_correctness_journey(tmp_path, monkeypatch, provider): root = Path(__file__).resolve().parents[2] preview = runpy.run_path(str(root / "examples/control_plane/todo-write-correctness-smoke.py")) assert preview["main"](provider=provider) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("script", ["completion-validation-accountability", "quota-no-git-status", "status-projection-cache"]) +def test_canonical_validation_and_scheduling_journey(tmp_path, monkeypatch, provider, script): + isolate_sqlite_runtime(tmp_path, monkeypatch) + root = Path(__file__).resolve().parents[2] + journey = runpy.run_path(str(root / f"examples/control_plane/{script}-smoke.py")) + assert journey["main"](provider=provider) in (None, 0) From f9b2c0666a3bd60322dbf6ec5154a12d9bd658cb Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 04:50:43 +0800 Subject: [PATCH 39/54] test(dashboard): qualify blocked readback on canonical providers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../blocked-reason-readback.mjs | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/examples/personal-workspace-browser/blocked-reason-readback.mjs b/examples/personal-workspace-browser/blocked-reason-readback.mjs index 9bffabf017..ba13633aee 100644 --- a/examples/personal-workspace-browser/blocked-reason-readback.mjs +++ b/examples/personal-workspace-browser/blocked-reason-readback.mjs @@ -13,9 +13,9 @@ const REASON = "Waiting for the upstream dataset export to be published"; const CHANGED_REASON = "The replacement export requires independent validation"; const execute = promisify(execFile); -// A disposable canonical File authority, real CLI add/update and real status +// Disposable canonical File/SQLite authorities, real CLI add/update and real status // HTTP owner. No live Goal, Todo or lease is touched. -async function startBlockedAuthority() { +async function startBlockedAuthority(provider) { const root = await mkdtemp(resolve(tmpdir(), "loopx-blocked-reason-")); const child = spawn(resolveTestPython(), ["-u", "-c", ` import json, os, runpy, signal, subprocess, sys @@ -24,7 +24,7 @@ from loopx.chat_server import ChatHTTPServer, ChatRequestHandler from loopx.control_plane.effect_runtime import restart_effect_runtime root, reason = Path(sys.argv[1]), sys.argv[2] fixture = runpy.run_path('tests/control_plane/canonical_authority_fixture.py') -registry, runtime, state = fixture['promoted_create_fixture'](root) +registry, runtime, state = fixture['promoted_create_fixture'](root, provider=sys.argv[3]) base = [sys.executable, '-c', 'from loopx.entrypoint import main; main()', '--format', 'json', '--registry', str(registry), '--runtime-root', str(runtime)] def cli(*args): result = subprocess.run(base + list(args), capture_output=True, text=True) @@ -33,7 +33,7 @@ def cli(*args): def blocked(text, *extra): created = cli('todo', 'add', '--goal-id', 'goal-a', '--role', 'agent', '--text', text, '--priority', 'P2') assert created['ok'], created - todo_id = created['todo']['todo_id'] + todo_id = created['todo_id'] updated = cli('todo', 'update', '--goal-id', 'goal-a', '--role', 'agent', '--todo-id', todo_id, '--status', 'blocked', *extra) assert updated['ok'], updated return todo_id @@ -41,7 +41,7 @@ try: with_reason = blocked('Synthetic blocked readback', '--reason', reason) without_reason = blocked('Synthetic unexplained blocker') def add_task(text): - return cli('todo', 'add', '--goal-id', 'goal-a', '--role', 'agent', '--text', text)['todo']['todo_id'] + return cli('todo', 'add', '--goal-id', 'goal-a', '--role', 'agent', '--text', text)['todo_id'] cyclic = add_task('Synthetic circular dependency') prerequisite = add_task('Synthetic cycle prerequisite') independent = add_task('Synthetic independent work') @@ -57,7 +57,7 @@ try: server.serve_forever() finally: restart_effect_runtime() -`, root, REASON], { cwd: repoRoot, env: { ...process.env, TMPDIR: root, TEMP: root, TMP: root, LOOPX_USAGE_PING: "0" }, stdio: ["ignore", "pipe", "pipe"] }); +`, root, REASON, provider], { cwd: repoRoot, env: { ...process.env, TMPDIR: root, TEMP: root, TMP: root, LOOPX_USAGE_PING: "0" }, stdio: ["ignore", "pipe", "pipe"] }); let diagnostic = ""; child.stderr.on("data", (chunk) => { diagnostic = (diagnostic + chunk).slice(-3000); }); const lines = createInterface({ input: child.stdout }); @@ -89,10 +89,9 @@ finally: } catch (error) { await close(); throw error; } } -export const blockedReasonReadbackScenario = { - id: "blocked-reason-readback", - async run({ browser, collectCoverage, url }) { - const authority = await startBlockedAuthority(); +const blockedReasonProviderScenario = { + async run({ browser, collectCoverage, url }, provider) { + const authority = await startBlockedAuthority(provider); const coverageEntries = []; const receipts = []; try { @@ -140,7 +139,7 @@ export const blockedReasonReadbackScenario = { }, }); const { page, errors } = context; - const suffix = `${locale}-${mobile ? "mobile" : "desktop"}`; + const suffix = `${provider}-${locale}-${mobile ? "mobile" : "desktop"}`; try { const openTodo = async (text) => { const card = page.locator("button[aria-pressed]", { hasText: text }); @@ -245,11 +244,11 @@ export const blockedReasonReadbackScenario = { await drawer.locator(".personal-task-request [role]").waitFor({ state: "hidden" }); assert.equal(await missing.count(), 0, "Archived completed history must not display blocked advice"); assert.equal(await field(drawer, "Status"), "Completed"); - await drawer.screenshot({ path: resolve(outputDir, "blocked-reason-archived-history.png") }); + await drawer.screenshot({ path: resolve(outputDir, `blocked-reason-archived-history-${provider}.png`) }); } assert.equal(writes, 0, "Reading a blocked Todo emitted a write"); assert.deepEqual(errors, []); - receipts.push({ locale, mobile, todo_id: authority.with_reason, reason_rendered: true, + receipts.push({ provider, locale, mobile, todo_id: authority.with_reason, reason_rendered: true, dynamic_refresh: !mobile, missing_optional_reason_transport: !mobile, archived_history_readback: locale === "en" && mobile, missing_reason_todo_id: authority.without_reason, writes }); } finally { @@ -258,8 +257,22 @@ export const blockedReasonReadbackScenario = { } } } - await writeFile(resolve(outputDir, "blocked-reason-receipts.json"), JSON.stringify({ synthetic: true, real_cli_http_owner: true, receipts }, null, 2)); - return { coverageEntries, note: "Canonical CLI→HTTP→packaged drawer; desktop/mobile Chinese/English, same open desktop inspector follows changed cause, injected missing optional reason and real blocked→open. Real completion/archive becomes unavailable in the active view and reopens through retained history, zero UI writes." }; + return { coverageEntries, receipts }; } finally { await authority.close(); } }, }; + +export const blockedReasonReadbackScenario = { + id: "blocked-reason-readback", + async run(context) { + const coverageEntries = []; + const receipts = []; + for (const provider of ["file", "sqlite"]) { + const result = await blockedReasonProviderScenario.run(context, provider); + coverageEntries.push(...result.coverageEntries); + receipts.push(...result.receipts); + } + await writeFile(resolve(outputDir, "blocked-reason-receipts.json"), JSON.stringify({ synthetic: true, real_cli_http_owner: true, receipts }, null, 2)); + return { coverageEntries, note: "Canonical File/SQLite CLI→HTTP→packaged drawer; desktop/mobile Chinese/English, same open desktop inspector follows changed cause, injected missing optional reason and real blocked→open. Real completion/archive becomes unavailable in the active view and reopens through retained history, zero UI writes." }; + }, +}; From 1e689b81ab90a0c274a6265e08c036edb3ca8fac Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:00:53 +0800 Subject: [PATCH 40/54] refactor(todos): isolate canonical update response projection Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/canary/maintainability_ratchet.py | 7 --- .../control_plane/todos/mutation_response.py | 29 +++++++++++ loopx/control_plane/todos/provider_update.py | 24 +--------- .../test_control_plane_import_boundaries.py | 48 +++++++++++-------- 4 files changed, 60 insertions(+), 48 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/todos/mutation_response.py b/loopx/control_plane/todos/mutation_response.py index 3b8e5da54e..9b111a3459 100644 --- a/loopx/control_plane/todos/mutation_response.py +++ b/loopx/control_plane/todos/mutation_response.py @@ -20,6 +20,8 @@ normalize_todo_completion_continuation, normalize_todo_completion_recovery, ) +from .succession_warning import build_open_parent_successor_advisory + def serialize_added_todo_payload( *, add_result: Mapping[str, Any], goal_id: str, role: str, todo_text: str, agent_id: str | None, state_file: Path, project: Path | None, @@ -176,3 +178,30 @@ def serialize_todo_update_result( "material_change_generation" ), } + + +def serialize_canonical_todo_update_result( + result: Mapping[str, Any], *, todo_id: str, dry_run: bool, +) -> dict[str, Any]: + """Project original transaction facts without rereading current authority.""" + # Older receipts remain replayable without invented historical fields. + # New transactions carry their original row/deltas, including dry runs. + public: dict[str, Any] = {} + facts = result.get("todo_update") + if isinstance(facts, dict): + todo = facts["todo"] + public = serialize_todo_update_result( + role=todo["role"], section=todo.get("source_section") or ( + "Agent Todo" if todo["role"] == "agent" else "User Todo / Owner Review Reading Queue"), + todo=todo.get("text"), todo_id=todo_id, status=todo["status"], + priority=todo.get("priority"), metadata=todo, + status_changed=facts["status_changed"], text_changed=facts["text_changed"], + metadata_updated=facts["metadata_updated"], + ) + advisory = build_open_parent_successor_advisory( + todo_id=todo_id, status=todo["status"], successor_todo_ids=todo.get("successor_todo_ids")) + if advisory: + public["parent_successor_advisory"] = advisory + public["changed"] = result["changed"] and not dry_run + public["provider_status"] = result["status"] + return public diff --git a/loopx/control_plane/todos/provider_update.py b/loopx/control_plane/todos/provider_update.py index beaec5eb15..3354937e19 100644 --- a/loopx/control_plane/todos/provider_update.py +++ b/loopx/control_plane/todos/provider_update.py @@ -23,8 +23,7 @@ EffectRuntimeResponseAmbiguous, effect_runtime_result, ) -from .mutation_response import serialize_todo_update_result -from .succession_warning import build_open_parent_successor_advisory +from .mutation_response import serialize_canonical_todo_update_result from .contract import compact_todo_text from .provider_projection import settle_canonical_todo_projection from .text import normalize_new_todo @@ -344,26 +343,7 @@ def update_canonical_todo_if_promoted( declaration=declaration, result=result, ) - # Older receipts remain replayable without invented historical fields. - # New transactions carry their original row/deltas, including dry runs. - public = {} - facts = result.get("todo_update") - if isinstance(facts, dict): - todo = facts["todo"] - public = serialize_todo_update_result( - role=todo["role"], section=todo.get("source_section") or ( - "Agent Todo" if todo["role"] == "agent" else "User Todo / Owner Review Reading Queue"), - todo=todo.get("text"), todo_id=todo_id, status=todo["status"], - priority=todo.get("priority"), metadata=todo, - status_changed=facts["status_changed"], text_changed=facts["text_changed"], - metadata_updated=facts["metadata_updated"], - ) - advisory = build_open_parent_successor_advisory( - todo_id=todo_id, status=todo["status"], successor_todo_ids=todo.get("successor_todo_ids")) - if advisory: - public["parent_successor_advisory"] = advisory - public["changed"] = result["changed"] and not dry_run - public["provider_status"] = result["status"] + public = serialize_canonical_todo_update_result(result, todo_id=todo_id, dry_run=dry_run) return settle_canonical_todo_projection( {"ok": True, "goal_id": goal_id, "todo_id": todo_id, "role": role, "dry_run": dry_run, **result, **public}, diff --git a/tests/architecture/test_control_plane_import_boundaries.py b/tests/architecture/test_control_plane_import_boundaries.py index 4734e5f2bb..4fa6683686 100644 --- a/tests/architecture/test_control_plane_import_boundaries.py +++ b/tests/architecture/test_control_plane_import_boundaries.py @@ -208,8 +208,7 @@ def _public_contract_evidence() -> dict[tuple[str, str], set[str]]: def _review_outward_dependencies(edges, *, reviewed=REVIEWED_MAINTAINABILITY_EXCEPTIONS): - # Use the existing exact debt owner: the quarantined legacy writer retains - # its Explore completion guard until retirement. This is not a prefix waiver. + # Exceptions are exact edges, never prefix waivers. findings = [ {"id": f"dependency_debt:{source}->{target}", "category": "dependency_debt"} for source, target in sorted(edges) @@ -237,35 +236,46 @@ def test_control_plane_has_no_unreviewed_outward_dependencies() -> None: ) +# Synthetic review evidence exercises the exception evaluator independently of +# the production registry, where the retired legacy writer has no exception. +_REVIEWED_OUTWARD_EDGE = ( + "loopx.control_plane.fixture.adapter", "loopx.capabilities.fixture.owner" +) +_OUTWARD_REVIEW_EVIDENCE = { + "reason": "Synthetic existing caller is covered by the exact edge review.", + "retirement_plan": "Remove the fixture edge when its synthetic caller retires.", +} + + @pytest.mark.parametrize("edge", [ - ("loopx.control_plane.todos.provider_update", "loopx.capabilities.explore.research_frontier"), - ("loopx.control_plane.todos.legacy_mutation_copy", "loopx.capabilities.explore.research_frontier"), - ("loopx.control_plane.todos.legacy_mutation", "loopx.capabilities.explore.research_frontier_extra"), - ("loopx.control_plane.todos.legacy_mutation", "loopx.capabilities.explore.research_frontier.child"), - ("loopx.control_plane.todos.legacy_mutation", "loopx.capabilities.issue_fix"), + ("loopx.control_plane.fixture.other", "loopx.capabilities.fixture.owner"), + ("loopx.control_plane.fixture.adapter_copy", "loopx.capabilities.fixture.owner"), + ("loopx.control_plane.fixture.adapter", "loopx.capabilities.fixture.owner_extra"), + ("loopx.control_plane.fixture.adapter", "loopx.capabilities.fixture.owner.child"), + ("loopx.control_plane.fixture.adapter", "loopx.capabilities.other"), ]) -def test_reviewed_legacy_bridge_never_allows_another_outward_edge(edge): - legacy = ("loopx.control_plane.todos.legacy_mutation", "loopx.capabilities.explore.research_frontier") - key = f"dependency_debt:{legacy[0]}->{legacy[1]}" - reviewed = {key: REVIEWED_MAINTAINABILITY_EXCEPTIONS[key]} - assert _review_outward_dependencies({legacy}, reviewed=reviewed)["ok"] - report = _review_outward_dependencies({legacy, edge}, reviewed=reviewed) +def test_reviewed_bridge_never_allows_another_outward_edge(edge): + reviewed_edge = _REVIEWED_OUTWARD_EDGE + key = f"dependency_debt:{reviewed_edge[0]}->{reviewed_edge[1]}" + reviewed = {key: _OUTWARD_REVIEW_EVIDENCE} + assert _review_outward_dependencies({reviewed_edge}, reviewed=reviewed)["ok"] + report = _review_outward_dependencies({reviewed_edge, edge}, reviewed=reviewed) assert not report["ok"] and report["unreviewed_count"] == 1 assert report["unreviewed_findings"][0]["id"] == f"dependency_debt:{edge[0]}->{edge[1]}" @pytest.mark.parametrize("field", ["reason", "retirement_plan"]) def test_outward_dependency_exception_requires_its_existing_review_evidence(field): - legacy = ("loopx.control_plane.todos.legacy_mutation", "loopx.capabilities.explore.research_frontier") - key = f"dependency_debt:{legacy[0]}->{legacy[1]}" - reviewed = {key: {**REVIEWED_MAINTAINABILITY_EXCEPTIONS[key], field: ""}} - report = _review_outward_dependencies({legacy}, reviewed=reviewed) + reviewed_edge = _REVIEWED_OUTWARD_EDGE + key = f"dependency_debt:{reviewed_edge[0]}->{reviewed_edge[1]}" + reviewed = {key: {**_OUTWARD_REVIEW_EVIDENCE, field: ""}} + report = _review_outward_dependencies({reviewed_edge}, reviewed=reviewed) assert not report["ok"] and report["invalid_exception_count"] == 1 def test_retired_outward_dependency_cannot_keep_a_stale_exception(): - key = "dependency_debt:loopx.control_plane.todos.legacy_mutation->loopx.capabilities.explore.research_frontier" - report = _review_outward_dependencies(set(), reviewed={key: REVIEWED_MAINTAINABILITY_EXCEPTIONS[key]}) + key = f"dependency_debt:{_REVIEWED_OUTWARD_EDGE[0]}->{_REVIEWED_OUTWARD_EDGE[1]}" + report = _review_outward_dependencies(set(), reviewed={key: _OUTWARD_REVIEW_EVIDENCE}) assert not report["ok"] and report["stale_exception_count"] == 1 From 86265a9df6ac83c14c02d10b0ca1ec1be012c773 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:00:53 +0800 Subject: [PATCH 41/54] test(todos): distinguish public state from transaction status Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...st_completion_validation_initial_binding.py | 18 +++++++++++++----- .../test_public_todo_update_plan.py | 9 ++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/control_plane/test_completion_validation_initial_binding.py b/tests/control_plane/test_completion_validation_initial_binding.py index de1c24d925..156c48023e 100644 --- a/tests/control_plane/test_completion_validation_initial_binding.py +++ b/tests/control_plane/test_completion_validation_initial_binding.py @@ -48,10 +48,14 @@ def run(extra: list[str] | None = None) -> dict: assert proc.returncode == 0, proc.stderr + proc.stdout return json.loads(proc.stdout) - assert run(["--dry-run"])["status"] == "planned" + preview = run(["--dry-run"]) + assert preview["status"] == "open" + assert preview["provider_status"] == "planned" assert canonical(runtime) == before assert not marker.exists() - assert run()["status"] == "applied" + applied = run() + assert applied["status"] == "open" + assert applied["provider_status"] == "applied" bound = canonical(runtime) work = bound["todos"][0] assert work["status"] == "open" @@ -66,7 +70,9 @@ def run(extra: list[str] | None = None) -> dict: assert "validation_command_argv" not in public_work assert "validation_command" not in public_work assert read_completion_validation_declaration(runtime_root=runtime, goal_id="goal-a", todo_id=todo_id)["validation_command_argv"] == argv - assert run()["status"] == "replayed" + replayed = run() + assert replayed["status"] == "open" + assert replayed["provider_status"] == "replayed" assert canonical(runtime) == bound assert not marker.exists() @@ -81,7 +87,7 @@ def run(extra: list[str] | None = None) -> dict: assert update_goal_todo(registry_path=registry, runtime_root_arg=str(runtime), goal_id="goal-a", todo_id=todo_id, role="agent", agent_id="agent-a", update_operation_id="replace", update_expected_provider_revision=current["provider_revision"], - validation_command_json=json.dumps(replacement), validation_label="Independent check")["status"] == "applied" + validation_command_json=json.dumps(replacement), validation_label="Independent check")["provider_status"] == "applied" replaced = canonical(runtime) assert replaced["todos"][0]["completion_validation_revision"] == 2 # The original first-binding replay is historical, not permission to restore old bytes. @@ -123,7 +129,9 @@ def lose_publication(**_kwargs): sidecar = completion_validation_declaration_path(runtime_root=runtime, goal_id="goal-a", todo_id=todo_id) assert not sidecar.exists() monkeypatch.setattr(provider_update, "persist_completion_validation_declaration", publish) - assert update_goal_todo(**args)["status"] == "replayed" + replayed = update_goal_todo(**args) + assert replayed["status"] == "open" + assert replayed["provider_status"] == "replayed" assert canonical(runtime) == committed assert sidecar.exists() assert read_completion_validation_declaration(runtime_root=runtime, goal_id="goal-a", todo_id=todo_id) is not None diff --git a/tests/control_plane/test_public_todo_update_plan.py b/tests/control_plane/test_public_todo_update_plan.py index 20c5e5a34c..186e752008 100644 --- a/tests/control_plane/test_public_todo_update_plan.py +++ b/tests/control_plane/test_public_todo_update_plan.py @@ -57,7 +57,8 @@ def test_clear_condition_and_successors_together_is_valid(tmp_path, monkeypatch, result = 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["status"] == "applied" + assert result["status"] == "open" + assert result["provider_status"] == "applied" updated = _canonical_todo(registry, waiting["todo_id"]) assert updated.get("resume_when") is None assert updated.get("resume_monitor_generation") is None @@ -70,7 +71,8 @@ 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, todo_id=waiting["todo_id"], agent_id=AGENT_ID, note="Clarify context") - assert result["status"] == "applied" + assert result["status"] == "open" + assert result["provider_status"] == "applied" updated = _canonical_todo(registry, waiting["todo_id"]) assert updated["resume_when"].startswith("monitor_changed:") assert updated["resume_monitor_generation"] == 3 @@ -105,6 +107,7 @@ def traced(method, params, **kwargs): result = 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"]]) - assert result["status"] in {"applied", "no_change"} + assert result["status"] == "open" + assert result["provider_status"] in {"applied", "no_change"} assert _canonical_todo(registry, waiting["todo_id"])["resume_monitor_generation"] == 3 assert calls == ["coordination.local_authority.todo_update"] From eb1b5981fcc49b3dc998116fa2fd9d0668f39c83 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:13:32 +0800 Subject: [PATCH 42/54] test(coordination): qualify canonical workspace and lease CLI journeys Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../peer-agent-workspace-guard-smoke.py | 168 +++++++++++------- .../control_plane/task-lease-runtime-smoke.py | 148 +++++++++++---- 2 files changed, 224 insertions(+), 92 deletions(-) diff --git a/examples/control_plane/peer-agent-workspace-guard-smoke.py b/examples/control_plane/peer-agent-workspace-guard-smoke.py index 4488459a4f..87e57dca42 100644 --- a/examples/control_plane/peer-agent-workspace-guard-smoke.py +++ b/examples/control_plane/peer-agent-workspace-guard-smoke.py @@ -12,6 +12,11 @@ REPO_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO_ROOT)) +from tests.control_plane.canonical_authority_fixture import ( # noqa: E402 + initialize_canonical_registry_fixture, +) + GOAL_ID = "workspace-guard-canary" PEER_ALPHA = "codex-alpha" PEER_BETA = "codex-beta" @@ -47,15 +52,18 @@ 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) -def write_fixture(root: Path) -> tuple[Path, Path, Path, Path, Path, Path]: +def write_fixture( + root: Path, *, provider: str, monitor: bool = False +) -> tuple[Path, Path, Path, Path, Path, Path]: project = root / "project" independent = root / "peer-worktree" task_project = root / "task-project" @@ -91,7 +99,13 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path, Path, Path, Path]: task_project.mkdir(parents=True) (task_project / "README.md").write_text("# Task Repository\n", encoding="utf-8") run_git(task_project, "init", "--initial-branch", "main") - run_git(task_project, "remote", "add", "origin", "https://example.invalid/loopx/task-repo.git") + run_git( + task_project, + "remote", + "add", + "origin", + "https://example.invalid/loopx/task-repo.git", + ) run_git(task_project, "add", "README.md") run_git( task_project, @@ -103,7 +117,9 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path, Path, Path, Path]: "-m", "initial task fixture", ) - run_git(task_project, "worktree", "add", "-b", "task-peer-work", str(task_independent)) + run_git( + task_project, "worktree", "add", "-b", "task-peer-work", str(task_independent) + ) state_path.parent.mkdir(parents=True, exist_ok=True) state_path.write_text( @@ -155,30 +171,37 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path, Path, Path, Path]: + "\n", encoding="utf-8", ) + if monitor: + registry = json.loads(registry_path.read_text(encoding="utf-8")) + registry["goals"][0].pop("workspace_guard_policy", None) + registry_path.write_text( + 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", + ) + initialize_canonical_registry_fixture(registry_path, provider=provider) return project, independent, task_project, task_independent, runtime, registry_path -def set_task_repository(project: Path, runtime: Path, registry_path: Path) -> None: - updated = run_cli( - project, - "todo", - "update", - "--goal-id", - GOAL_ID, - "--role", - "agent", - "--todo-id", - "todo_workspace_guard", - "--agent-id", - PEER_ALPHA, - "--task-repository", - "https://example.invalid/loopx/task-repo.git", - registry_path=registry_path, - runtime=runtime, - ) - assert updated["task_repository"] == TASK_REPOSITORY, updated - - def should_run( cwd: Path, project: Path, @@ -200,7 +223,7 @@ def should_run( ) -def main() -> None: +def run_provider(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-peer-workspace-guard-") as tmp: ( project, @@ -209,7 +232,7 @@ def main() -> None: task_independent, runtime, registry_path, - ) = write_fixture(Path(tmp)) + ) = write_fixture(Path(tmp) / "goal-route", provider=provider) guarded = should_run(project, project, runtime, registry_path) assert guarded["decision"] == "workspace_guard", guarded @@ -235,7 +258,9 @@ def main() -> None: runtime, registry_path, ) - assert foreign_without_route["decision"] == "workspace_guard", foreign_without_route + assert foreign_without_route["decision"] == "workspace_guard", ( + foreign_without_route + ) assert foreign_without_route["workspace_guard"]["current_workspace"] == ( "foreign_git_worktree" ), foreign_without_route @@ -243,13 +268,46 @@ def main() -> None: "goal.repo" ), foreign_without_route - set_task_repository(project, runtime, registry_path) + # This unleased soft-claim Todo may update its canonical work declaration. + updated = run_cli( + project, + "todo", + "update", + "--goal-id", + GOAL_ID, + "--todo-id", + "todo_workspace_guard", + "--agent-id", + PEER_ALPHA, + "--task-repository", + TASK_REPOSITORY, + registry_path=registry_path, + runtime=runtime, + ) + assert updated["task_repository"] == TASK_REPOSITORY, updated + readback = run_cli( + project, + "todo", + "list", + "--goal-id", + GOAL_ID, + "--todo-id", + "todo_workspace_guard", + registry_path=registry_path, + runtime=runtime, + ) + assert readback["todo"]["task_repository"] == TASK_REPOSITORY, readback + assert readback["authority_read"]["source_authority"] == provider + "_v0", ( + readback + ) task_allowed = should_run(task_independent, project, runtime, registry_path) assert task_allowed["decision"] == "run", task_allowed assert task_allowed["effective_action"] == "normal_run", task_allowed assert "workspace_guard" not in task_allowed, task_allowed - assert task_allowed["selected_todo"]["task_repository"] == TASK_REPOSITORY, task_allowed + assert task_allowed["selected_todo"]["task_repository"] == TASK_REPOSITORY, ( + task_allowed + ) task_primary_guarded = should_run( task_project, @@ -258,11 +316,15 @@ def main() -> None: registry_path, ) task_primary_guard = task_primary_guarded["workspace_guard"] - assert task_primary_guard["current_workspace"] == "canonical_checkout", task_primary_guard + assert task_primary_guard["current_workspace"] == "canonical_checkout", ( + task_primary_guard + ) assert task_primary_guard["repository_source"] == ( "selected_todo.task_repository" ), task_primary_guard - assert task_primary_guard["task_repository"] == TASK_REPOSITORY, task_primary_guard + assert task_primary_guard["task_repository"] == TASK_REPOSITORY, ( + task_primary_guard + ) goal_worktree_guarded = should_run(independent, project, runtime, registry_path) assert goal_worktree_guarded["workspace_guard"]["current_workspace"] == ( @@ -272,44 +334,30 @@ def main() -> None: TASK_REPOSITORY ), goal_worktree_guarded - registry = json.loads(registry_path.read_text(encoding="utf-8")) - registry["goals"][0].pop("workspace_guard_policy", None) - registry_path.write_text( - 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", + project, _, _, _, runtime, registry_path = write_fixture( + Path(tmp) / "monitor", provider=provider, monitor=True ) monitor_selected = should_run(project, project, runtime, registry_path) assert monitor_selected["decision"] == "run", monitor_selected 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_selected + assert monitor_selected["selected_todo"]["todo_id"] == ("todo_due_monitor"), ( + monitor_selected + ) assert monitor_selected["selected_todo"]["task_repository"] == ( PROJECT_REPOSITORY ), monitor_selected +def main() -> None: + os.environ["NODE_OPTIONS"] = ( + os.environ.get("NODE_OPTIONS", "") + " --experimental-sqlite" + ) + for provider in ("file", "sqlite"): + run_provider(provider) + print("peer-agent-workspace-guard-smoke ok: File and SQLite") + + if __name__ == "__main__": main() diff --git a/examples/control_plane/task-lease-runtime-smoke.py b/examples/control_plane/task-lease-runtime-smoke.py index 1b0ac95ac7..522b3bd322 100644 --- a/examples/control_plane/task-lease-runtime-smoke.py +++ b/examples/control_plane/task-lease-runtime-smoke.py @@ -4,7 +4,7 @@ from __future__ import annotations import json -import re +import os import subprocess import sys import tempfile @@ -15,13 +15,21 @@ REPO_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(REPO_ROOT)) +from loopx.control_plane.coordination.runtime_shadow import ( # noqa: E402 + build_todo_runtime_shadow_projection, +) +from loopx.control_plane.todos.goal_todo_projection import project_goal_todo_items # noqa: E402 +from tests.control_plane.canonical_authority_fixture import ( # noqa: E402 + initialize_canonical_authority, +) + GOAL_ID = "task-lease-runtime-goal" TODO_A = "todo_taskleasea" TODO_B = "todo_taskleaseb" 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" @@ -35,7 +43,7 @@ def write_fixture(root: Path) -> tuple[Path, Path]: "# Active Goal State\n\n" "## Agent Todo\n\n" f"- [ ] First independently claimable todo.\n" - f" \n" + f" \n" f"- [ ] Second independently claimable todo.\n" f" \n" f"- [ ] Conflicting write-scope todo.\n" @@ -76,29 +84,29 @@ def write_fixture(root: Path) -> tuple[Path, Path]: + "\n", encoding="utf-8", ) + goal = json.loads(registry_path.read_text())["goals"][0] + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, + handoff_mode="hard_lease", + todos=project_goal_todo_items( + goal, + state_text=state_file.read_text(), + state_path=state_file, + rollout_events=[], + ), + ) + 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]: - return subprocess.run( + result = subprocess.run( [ sys.executable, "-m", @@ -110,11 +118,15 @@ def command( *args, ], cwd=REPO_ROOT, - check=check, + check=False, text=True, capture_output=True, ) + if check: + assert result.returncode == 0, result.stdout + result.stderr + return result + def cli( registry_path: Path, *args: str, check: bool = True @@ -158,15 +170,9 @@ def inspected_lease(registry_path: Path, todo_id: str) -> dict[str, Any]: ) -def main() -> int: +def run_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, _ = write_fixture(Path(tmp), provider=provider) first = payload( cli( registry_path, @@ -186,6 +192,8 @@ def main() -> int: ) ) assert first["ok"] is True and first["acquired"] is True, first + assert first["source_authority"] == provider + "_v0", first + assert first["legacy_fallback_used"] is False, first assert first["lease"]["schema_version"] == "task_lease_v0", first assert first["lease"]["version"] == 1, first assert first["lease"]["lease_epoch"] == 1, first @@ -296,11 +304,49 @@ 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) + before_handoff = payload( + command( + registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_A + ) + ) + # A metadata edit cannot split an execution grant. Use atomic transfer. + cleared = command( + registry_path, + "todo", + "update", + "--goal-id", + GOAL_ID, + "--todo-id", + TODO_A, + "--agent-id", + "codex-main-control", + "--clear-claim", + "--task-lease-idempotency-key", + "turn-1", + "--task-lease-expected-version", + "2", + check=False, + ) + assert cleared.returncode == 1, cleared.stdout + assert payload(cleared)["error_code"] == "handoff_mode_requires_lease", ( + cleared.stdout + ) + after_rejection = payload( + command( + registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_A + ) + ) + assert ( + after_rejection["authority_read"]["provider_revision"] + == before_handoff["authority_read"]["provider_revision"] + ) + assert after_rejection["todo"] == before_handoff["todo"] + assert inspected_lease(registry_path, TODO_A)["lease"] == renewed["lease"] transferred = payload( cli( registry_path, "transfer", + "--transfer-claim", "--goal-id", GOAL_ID, "--todo-id", @@ -321,6 +367,12 @@ def main() -> int: assert transferred["lease"]["version"] == 3, transferred assert transferred["lease"]["lease_epoch"] == 2, transferred + after_handoff = payload( + command( + registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_A + ) + ) + assert after_handoff["todo"]["claimed_by"] == transferred["lease"]["owner"] stale_owner = lifecycle_cli( registry_path, "renew", @@ -330,7 +382,16 @@ def main() -> int: check=False, ) assert stale_owner.returncode == 1, stale_owner.stdout - assert payload(stale_owner)["error_code"] == "lease_cas_mismatch" + assert payload(stale_owner)["error_code"] == "owner_conflicts_with_claim", ( + stale_owner.stdout + ) + 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", ( + stale_key.stdout + ) after_stale_owner = inspected_lease(registry_path, TODO_A) assert after_stale_owner["lease"] == transferred["lease"], after_stale_owner @@ -457,7 +518,9 @@ def main() -> int: check=False, ) assert stale_completion.returncode == 1, stale_completion.stdout - assert payload(stale_completion)["error_code"] == "lease_cas_mismatch" + assert payload(stale_completion)["error_code"] == "lease_cas_mismatch", ( + stale_completion.stdout + ) completed = payload( command( @@ -480,9 +543,30 @@ def main() -> int: ) ) assert completed["ok"] is True, completed - assert completed["task_lease_fence"]["lease_epoch"] == 3, completed + assert completed["task_lease_fence"]["lease_fence"] == "required", completed + assert completed["task_lease_fence"]["next_lease"]["lease_epoch"] == 3, ( + completed + ) + final_lease = inspected_lease(registry_path, TODO_A)["lease"] + assert final_lease["status"] == "released", final_lease + assert final_lease["lease_epoch"] == 3 and final_lease["version"] == 5, ( + final_lease + ) + final_todos = payload( + command( + registry_path, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_A + ) + ) + assert final_todos["todo"]["status"] == "done", final_todos + - print("task-lease-runtime-smoke ok") +def main() -> int: + os.environ["NODE_OPTIONS"] = ( + os.environ.get("NODE_OPTIONS", "") + " --experimental-sqlite" + ) + for provider in ("file", "sqlite"): + run_provider(provider) + print("task-lease-runtime-smoke ok: File and SQLite") return 0 From 4e94b08be54603352ccd18d1237e498cdcdb8200 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:28:53 +0800 Subject: [PATCH 43/54] fix(todos): preserve canonical actor refusal and delegation boundaries Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../coordination/todo_lifecycle_decision.ts | 20 +- .../control_plane/coordination/todo_update.ts | 7 +- .../control_plane/todos/mutation_authority.py | 13 +- loopx/control_plane/todos/provider_update.py | 137 ++++---- .../test_todo_mutation_authority.py | 303 ++++++++++-------- .../todo_terminal_decision.test.ts | 20 ++ 6 files changed, 301 insertions(+), 199 deletions(-) diff --git a/loopx/control_plane/coordination/todo_lifecycle_decision.ts b/loopx/control_plane/coordination/todo_lifecycle_decision.ts index 8f732ccdde..2e65c6dab7 100644 --- a/loopx/control_plane/coordination/todo_lifecycle_decision.ts +++ b/loopx/control_plane/coordination/todo_lifecycle_decision.ts @@ -79,7 +79,7 @@ interface LifecycleDecisionRequest { readonly command: LifecycleCommand; readonly handoff_mode: HandoffMode; readonly registered_agents: readonly string[]; - readonly lifecycle_grants: readonly LifecycleGrant[]; + readonly resolve_lifecycle_grants: () => readonly LifecycleGrant[]; readonly todo: TodoFact; readonly decision_target: TodoFact | null; readonly lease: LeaseFact | null; @@ -233,7 +233,7 @@ function lifecycleGrants( agent_id: agentId, actions: [...new Set(actions)], requires_reason: requireBoolean( - grant.requires_reason, + grant.requires_reason === undefined ? true : grant.requires_reason, `lifecycle_grants[${index}].requires_reason`, ), }; @@ -276,14 +276,22 @@ function decodeRequest(value: unknown, kind: "terminal" | "mutation" = "terminal ); const actor = optionalAgent(request.actor_agent_id, "actor_agent_id"); const internalReclaim = kind === "mutation" && request.authority_action === EXECUTOR_RECLAIM_ACTION; + // The executor-only action is never a public delegation, even if unused. + if (!internalReclaim && Array.isArray(request.lifecycle_grants) && + request.lifecycle_grants.some(grant => grant !== null && typeof grant === "object" && + Array.isArray(grant.actions) && grant.actions.some((action: unknown) => + typeof action === "string" && action.trim().toLowerCase() === EXECUTOR_RECLAIM_ACTION))) { + throw new EffectRuntimeRequestError("executor reclaim is not a public lifecycle grant"); + } + const reclaimGrants = internalReclaim ? executorReclaimGrant(request, actor) : null; const outcome = kind === "terminal" ? optionalString(request.decision_outcome, "decision_outcome") : null; const decoded = { command: requireStringLiteral(request.command, kind === "terminal" ? COMMANDS : MUTATION_COMMANDS, "command"), handoff_mode: requireStringLiteral(request.handoff_mode, HANDOFF_MODES, "handoff_mode"), registered_agents: registeredAgents, - lifecycle_grants: internalReclaim ? executorReclaimGrant(request, actor) - : lifecycleGrants(request.lifecycle_grants ?? [], registeredAgents), + resolve_lifecycle_grants: () => reclaimGrants ?? + lifecycleGrants(request.lifecycle_grants ?? [], registeredAgents), todo: request.todo === null ? null : todoFact(request.todo, "todo"), decision_target: kind !== "terminal" || request.decision_target === null || request.decision_target === undefined ? null @@ -395,7 +403,9 @@ function authority(request: LifecycleDecisionRequest): const rejection = registeredTodoMutationRejection(todo, actor, registered); if (rejection !== null && rejection !== "claim_owner_mismatch") return result("rejected", rejection); if (rejection === "claim_owner_mismatch") { - const grant = request.lifecycle_grants.find((candidate) => candidate.agent_id === actor); + // Delegation is consulted only after ordinary actor/owner authorization. + // Unrelated invalid grants cannot disable the owner or exact user gate. + const grant = request.resolve_lifecycle_grants().find((candidate) => candidate.agent_id === actor); if (grant === undefined) return result("rejected", "claim_owner_mismatch"); if (!grant.actions.includes(request.authority_action)) { return result("rejected", "delegation_action_not_granted"); diff --git a/loopx/control_plane/coordination/todo_update.ts b/loopx/control_plane/coordination/todo_update.ts index 1373b33d91..aaa529c161 100644 --- a/loopx/control_plane/coordination/todo_update.ts +++ b/loopx/control_plane/coordination/todo_update.ts @@ -171,11 +171,13 @@ export async function executeCoordinationTodoUpdate( // 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 {...failure("actor_not_registered", "Todo update requires a registered actor"), + failure_kind: "decision_rejection"}; } } 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 {...failure("actor_not_registered", "Todo update requires a registered actor"), + failure_kind: "decision_rejection"}; } const observation = await receipt.observe(store); if (observation.kind === "receipt") return observation.result; @@ -192,6 +194,7 @@ export async function executeCoordinationTodoUpdate( if (isFailure(target)) return target; const rejected = todoUpdateAdmissionRejection(head.head, target.todo, target.leases, input); if (rejected !== null) return {...failure(rejected.code, rejected.reason), + failure_kind: "decision_rejection", ...(rejected.handoff_mode === undefined ? {} : {handoff_mode: rejected.handoff_mode}), ...(rejected.recovery === undefined ? {} : {recovery: rejected.recovery})}; let prepared: ReturnType; diff --git a/loopx/control_plane/todos/mutation_authority.py b/loopx/control_plane/todos/mutation_authority.py index 1c78eb8fa8..60364480de 100644 --- a/loopx/control_plane/todos/mutation_authority.py +++ b/loopx/control_plane/todos/mutation_authority.py @@ -34,17 +34,16 @@ def todo_lifecycle_facts( registry_path: Path, goal_id: str, -) -> tuple[list[str], list[dict[str, Any]]]: +) -> tuple[list[str], Any]: """Project the existing registry owner for all canonical Todo mutations.""" goal = load_goal_from_registry(registry_path, goal_id) registered = registered_agent_ids_for_goal(goal) 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, - registered_agents=registered, - ) - return registered, grants + # Forward registry facts. The TypeScript owner validates grants only when + # resolving an otherwise-denied owner transition, after ordinary authority. + grants = (coordination.get("todo_lifecycle_authority") + if isinstance(coordination, Mapping) else None) + return registered, [] if grants is None else grants def normalize_todo_lifecycle_authority( diff --git a/loopx/control_plane/todos/provider_update.py b/loopx/control_plane/todos/provider_update.py index 3354937e19..9b2d103830 100644 --- a/loopx/control_plane/todos/provider_update.py +++ b/loopx/control_plane/todos/provider_update.py @@ -15,6 +15,7 @@ from ..coordination.local_authority import ( LOCAL_AUTHORITY_SOURCES, LocalCoordinationAuthorityUnavailable, + LocalCoordinationAuthorityRejection, local_authority_is_promoted, read_canonical_todos_if_promoted, ) @@ -177,6 +178,80 @@ def _publish_completion_validation_revision( ) +def _require_canonical_update_result( + result: Any, *, planning_intent: dict[str, Any] | None, + runtime_root: Path, goal_id: str, completion_validation_executed: bool, +) -> dict[str, Any]: + """Adapt typed owner failures to the established public exception contract.""" + # 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": + raise ValueError("Todo is missing from canonical authority") + if result.get("reason_code") == "todo_role_mismatch": + raise ValueError("Todo does not have the requested role") + # The TypeScript planner owns the rule and its typed diagnostic. Keep + # the public CLI repair projection without revalidating the transition. + code = result.get("reason_code") + if isinstance(code, str) and code.startswith("external_wait_"): + context = result.get("external_wait_authoring_context") + if not isinstance(context, dict): + context = planning_intent or {} + resume_when = context.get("resume_when") + monitor_id = ( + resume_when.split(":", 1)[1] + if isinstance(resume_when, str) and resume_when.startswith("monitor_changed:") + else None + ) + raise TodoExternalWaitAuthoringError( + str(result.get("reason") or "invalid external-wait transition"), + code=code, + monitor_todo_id=monitor_id, + successor_todo_ids=context.get("successor_todo_ids"), + ) + if isinstance(result, dict) and ( + result.get("status") == "missing" + and result.get("source_authority") in LOCAL_AUTHORITY_SOURCES + and result.get("decision_read_from_provider") is True + and result.get("legacy_fallback_used") is False + ): + payload = dict(result) + payload["recovery"] = { + "action": "restore_canonical_authority", + "runtime_root": str(runtime_root.expanduser().resolve(strict=False)), + "goal_id": goal_id, + "legacy_markdown_fallback_allowed": False, + "retry_after": "canonical_provider_readback_loaded", + } + raise LocalCoordinationAuthorityUnavailable( + "canonical Todo authority is unavailable", + code="local_authority_todo_list_unavailable", + payload=payload, + ) + if isinstance(result, dict) and result.get("status") == "failed" and ( + result.get("failure_kind") == "decision_rejection" + ): + raise LocalCoordinationAuthorityRejection( + str(result.get("reason") or "canonical Todo update was rejected"), + code=str(result.get("reason_code") or "todo_update_rejected"), + payload=result, + ) + if not isinstance(result, dict) or result.get("status") not in { + "applied", "recovered", "replayed", "no_change", "planned", + } 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 = dict(result) if isinstance(result, dict) else {} + if completion_validation_executed: + payload["completion_validation_executed"] = True + raise LocalCoordinationAuthorityUnavailable( + str(payload.get("reason") or "canonical Todo update failed; reread before retry"), + code=str(payload.get("reason_code") or payload.get("conflict_kind") + or "local_authority_todo_update_failed"), payload=payload, + ) + return 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, @@ -275,64 +350,10 @@ def update_canonical_todo_if_promoted( 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": - raise ValueError("Todo is missing from canonical authority") - if result.get("reason_code") == "todo_role_mismatch": - raise ValueError("Todo does not have the requested role") - # The TypeScript planner owns the rule and its typed diagnostic. Keep - # the public CLI repair projection without revalidating the transition. - code = result.get("reason_code") - if isinstance(code, str) and code.startswith("external_wait_"): - context = result.get("external_wait_authoring_context") - if not isinstance(context, dict): - context = planning_intent or {} - resume_when = context.get("resume_when") - monitor_id = ( - resume_when.split(":", 1)[1] - if isinstance(resume_when, str) and resume_when.startswith("monitor_changed:") - else None - ) - raise TodoExternalWaitAuthoringError( - str(result.get("reason") or "invalid external-wait transition"), - code=code, - monitor_todo_id=monitor_id, - successor_todo_ids=context.get("successor_todo_ids"), - ) - if isinstance(result, dict) and ( - result.get("status") == "missing" - and result.get("source_authority") in LOCAL_AUTHORITY_SOURCES - and result.get("decision_read_from_provider") is True - and result.get("legacy_fallback_used") is False - ): - payload = dict(result) - payload["recovery"] = { - "action": "restore_canonical_authority", - "runtime_root": str(runtime_root.expanduser().resolve(strict=False)), - "goal_id": goal_id, - "legacy_markdown_fallback_allowed": False, - "retry_after": "canonical_provider_readback_loaded", - } - raise LocalCoordinationAuthorityUnavailable( - "canonical Todo authority is unavailable", - code="local_authority_todo_list_unavailable", - payload=payload, - ) - if not isinstance(result, dict) or result.get("status") not in { - "applied", "recovered", "replayed", "no_change", "planned", - } 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 = dict(result) if isinstance(result, dict) else {} - if completion_validation_executed: - payload["completion_validation_executed"] = True - raise LocalCoordinationAuthorityUnavailable( - str(payload.get("reason") or "canonical Todo update failed; reread before retry"), - code=str(payload.get("reason_code") or payload.get("conflict_kind") - or "local_authority_todo_update_failed"), payload=payload, - ) + result = _require_canonical_update_result( + result, planning_intent=planning_intent, runtime_root=runtime_root, + goal_id=goal_id, completion_validation_executed=completion_validation_executed, + ) if validation_revision is not None and not dry_run: declaration = dict(validation_revision["declaration"]) _publish_completion_validation_revision( diff --git a/tests/control_plane/test_todo_mutation_authority.py b/tests/control_plane/test_todo_mutation_authority.py index e65263948b..8d730d0336 100644 --- a/tests/control_plane/test_todo_mutation_authority.py +++ b/tests/control_plane/test_todo_mutation_authority.py @@ -6,6 +6,9 @@ import pytest from loopx.cli import main as cli_main +from loopx.control_plane.coordination.local_authority import ( + LocalCoordinationAuthorityRejection, LocalCoordinationAuthorityUnavailable, +) from loopx.control_plane.scheduler.monitor_poll_writeback import ( write_monitor_poll_todo_state, ) @@ -18,6 +21,7 @@ ) from loopx.status import parse_active_state_todos from loopx.todos import ( + list_goal_todos, add_goal_todo, complete_goal_todo, supersede_goal_todo, @@ -89,6 +93,34 @@ def _write_fixture( return registry, state +@pytest.fixture(params=["file", "sqlite"]) +def canonical_workspace(request, tmp_path, monkeypatch): + """Exercise actor policy through the real provider, not Markdown writes.""" + from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_registry_fixture, isolate_sqlite_runtime, + ) + isolate_sqlite_runtime(tmp_path, monkeypatch) + + def create(root, **kwargs): + registry, state = _write_fixture(root, **kwargs) + initialize_canonical_registry_fixture(registry, provider=request.param) + readback = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) + assert readback["authority_read"]["source_authority"] == request.param + "_v0" + assert readback["authority_read"]["legacy_fallback_used"] is False + return registry, state + + return create + + +def _canonical_todo(registry: Path, todo_id: str) -> dict: + return list_goal_todos(registry_path=registry, goal_id=GOAL_ID, todo_id=todo_id)["todo"] + + +def _canonical_snapshot(registry: Path, state: Path) -> tuple: + result = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) + return result["authority_read"]["provider_revision"], result["todos"], state.read_bytes() + + def _lease_path(tmp_path: Path, todo_id: str) -> Path: return tmp_path / "runtime" / "goals" / GOAL_ID / "task-leases" / f"{todo_id}.json" @@ -196,46 +228,47 @@ 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) +def test_multi_agent_update_requires_actor_and_is_atomic(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = _add_agent_todo(registry) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="requires --agent-id"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: 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 rejected.value.code == "actor_not_registered" - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before -def test_registered_actor_can_correct_unclaimed_todo_without_claim(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) +def test_registered_actor_can_correct_unclaimed_todo_without_claim(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(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 result["provider_status"] == "applied" + corrected = _canonical_todo(registry, 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) +def test_excluded_actor_cannot_mutate_unclaimed_todo(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = _add_agent_todo( registry, claimed_by=None, excluded_agents=[AUTHOR_AGENT], ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="is excluded from mutating"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: update_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -243,8 +276,9 @@ def test_excluded_actor_cannot_mutate_unclaimed_todo(tmp_path: Path) -> None: agent_id=AUTHOR_AGENT, note="An excluded author cannot rewrite review work.", ) + assert rejected.value.code == "actor_excluded" - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before def test_idempotent_add_preserves_existing_executor_exclusions( @@ -316,14 +350,14 @@ def test_unresolved_decision_scope_is_not_a_local_claim_gate(tmp_path: Path) -> @pytest.mark.parametrize("command", ["update", "complete", "supersede"]) def test_non_owner_cannot_mutate_claimed_todo( - tmp_path: Path, + tmp_path: Path, canonical_workspace, command: str, ) -> None: - registry, state = _write_fixture(tmp_path) + registry, state = canonical_workspace(tmp_path) todo = _add_agent_todo(registry, claimed_by=REVIEW_AGENT) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="claimed_by='codex-review'"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: if command == "update": update_goal_todo( registry_path=registry, @@ -348,8 +382,9 @@ def test_non_owner_cannot_mutate_claimed_todo( agent_id=AUTHOR_AGENT, reason="unauthorized", ) + assert rejected.value.code == ("update_owner_mismatch" if command == "update" else "claim_owner_mismatch") - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before def test_completion_policy_unicode_parity_through_public_facade( @@ -459,8 +494,8 @@ def test_completion_policy_errors_do_not_preempt_actor_or_lease_fences( 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) +def test_owner_actor_update_returns_typed_receipt(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = _add_agent_todo(registry) result = update_goal_todo( @@ -471,16 +506,12 @@ def test_owner_actor_update_returns_typed_receipt(tmp_path: Path) -> None: 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" + assert result["schema_version"] == "loopx_coordination_todo_update_result_v0" + assert result["provider_status"] == "applied" + assert result["todo_update"]["todo"]["last_actor_agent_id"] == AUTHOR_AGENT + assert result["todo_update"]["todo"]["claimed_by"] == AUTHOR_AGENT + + assert _canonical_todo(registry, todo["todo_id"])["note"] == "owner-attributed update" @pytest.mark.parametrize("status", ["deferred", "blocked", "done"]) @@ -1081,8 +1112,6 @@ def test_terminal_upgrade_rejects_untyped_legacy_completion_until_repaired( 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) @@ -1415,12 +1444,6 @@ def test_exception_after_verified_fence_leaves_lease_intact( 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( @@ -1446,16 +1469,6 @@ def test_capability_binding_cannot_be_rebound_by_duplicate_add(tmp_path: Path) - assert state.read_text(encoding="utf-8") == before - - - - - - - - - - def test_duplicate_todo_add_rejects_invalid_task_domain_atomically( tmp_path: Path, ) -> None: @@ -1527,12 +1540,12 @@ def test_monitor_schedule_fields_remain_monitor_only(tmp_path: Path) -> None: ) -def test_claim_actor_must_match_requested_owner(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) +def test_claim_actor_must_match_requested_owner(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = _add_agent_todo(registry, claimed_by=None) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="requires --claimed-by to match"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: update_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1541,7 +1554,8 @@ def test_claim_actor_must_match_requested_owner(tmp_path: Path) -> None: agent_id=REVIEW_AGENT, claim_only=True, ) - assert state.read_text(encoding="utf-8") == before + assert rejected.value.code == "claim_actor_mismatch" + assert _canonical_snapshot(registry, state) == before result = update_goal_todo( registry_path=registry, @@ -1551,8 +1565,8 @@ def test_claim_actor_must_match_requested_owner(tmp_path: Path) -> None: agent_id=AUTHOR_AGENT, claim_only=True, ) - assert result["mutation_authority"]["command"] == "claim" - assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AUTHOR_AGENT + assert result["claimed_by"] == AUTHOR_AGENT + assert _canonical_todo(registry, todo["todo_id"])["claimed_by"] == AUTHOR_AGENT def test_monitor_writeback_propagates_multi_agent_actor(tmp_path: Path) -> None: @@ -1649,9 +1663,9 @@ def test_monitor_successor_replay_preserves_independent_reviewer_exclusion( def test_exact_user_gate_decision_scope_uses_controller_override( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, _state = _write_fixture(tmp_path) + registry, _state = canonical_workspace(tmp_path) target = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1682,16 +1696,18 @@ 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" + assert authority["authority_mode"] == "exact_user_gate_decision_scope_override" + gate_readback = _canonical_todo(registry, gate["todo_id"]) + assert gate_readback["status"] == "done" + assert gate_readback["unblocks_todo_id"] == target["todo_id"] + assert gate_readback["decision_scope"]["scope_key"] == "publish_release" + assert gate_readback["decision_outcome"] == "approve" def test_exact_user_gate_ignores_unrelated_malformed_lifecycle_grant( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, _state = _write_fixture( + registry, _state = canonical_workspace( tmp_path, lifecycle_authority=[ { @@ -1729,13 +1745,13 @@ def test_exact_user_gate_ignores_unrelated_malformed_lifecycle_grant( evidence="owner approved the exact decision scope", ) - assert result["mutation_authority"]["mode"] == ( + assert result["mutation_authority"]["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) +def test_non_exact_user_gate_cannot_bypass_actor_attribution(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) target = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1755,9 +1771,9 @@ def test_non_exact_user_gate_cannot_bypass_actor_attribution(tmp_path: Path) -> decision_scope=DECISION_SCOPE, unblocks_todo_id=target["todo_id"], ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="requires --agent-id"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1766,29 +1782,30 @@ def test_non_exact_user_gate_cannot_bypass_actor_attribution(tmp_path: Path) -> decision_outcome="approve", evidence="scope mismatch", ) + assert rejected.value.code == "actor_required" - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == 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) +def test_unowned_single_agent_goal_keeps_lifecycle_compatibility(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path, multi_agent=False) + todo = _add_agent_todo(registry, claimed_by=None) result = update_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - note="legacy single-agent update", + note="unowned single-agent update", ) - assert result["mutation_authority"]["mode"] == "single_agent_compatibility" - assert _agent_todo(state, todo["todo_id"])["note"] == "legacy single-agent update" + assert result["provider_status"] == "applied" + assert _canonical_todo(registry, todo["todo_id"])["note"] == "unowned single-agent update" def test_single_agent_ignores_unrelated_malformed_lifecycle_grant( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture( + registry, state = canonical_workspace( tmp_path, multi_agent=False, lifecycle_authority=[ @@ -1798,23 +1815,23 @@ def test_single_agent_ignores_unrelated_malformed_lifecycle_grant( } ], ) - todo = _add_agent_todo(registry) + todo = _add_agent_todo(registry, claimed_by=None) result = update_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - note="legacy single-agent update", + note="unowned single-agent update", ) - assert result["mutation_authority"]["mode"] == "single_agent_compatibility" - assert _agent_todo(state, todo["todo_id"])["note"] == "legacy single-agent update" + assert result["provider_status"] == "applied" + assert _canonical_todo(registry, todo["todo_id"])["note"] == "unowned single-agent update" def test_current_owner_ignores_unrelated_malformed_lifecycle_grant( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture( + registry, state = canonical_workspace( tmp_path, lifecycle_authority=[ { @@ -1833,24 +1850,24 @@ def test_current_owner_ignores_unrelated_malformed_lifecycle_grant( note="owner-attributed update", ) - assert result["mutation_authority"]["mode"] == "registered_peer_actor" - assert _agent_todo(state, todo["todo_id"])["note"] == "owner-attributed update" + assert result["provider_status"] == "applied" + assert _canonical_todo(registry, todo["todo_id"])["note"] == "owner-attributed update" @pytest.mark.parametrize("command", ["complete", "supersede"]) def test_author_cannot_replace_explicit_independent_review( - tmp_path: Path, + tmp_path: Path, canonical_workspace, command: str, ) -> None: - registry, state = _write_fixture(tmp_path) + registry, state = canonical_workspace(tmp_path) todo = _add_agent_todo( registry, claimed_by=REVIEW_AGENT, excluded_agents=[AUTHOR_AGENT], ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="is excluded from mutating"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: if command == "complete": complete_goal_todo( registry_path=registry, @@ -1867,14 +1884,15 @@ def test_author_cannot_replace_explicit_independent_review( agent_id=AUTHOR_AGENT, reason="author cannot replace review", ) + assert rejected.value.code == "actor_excluded" - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before def test_delegated_orchestrator_can_complete_claimed_todo_with_reason( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture( + registry, state = canonical_workspace( tmp_path, lifecycle_authority=[ { @@ -1895,26 +1913,18 @@ 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" + assert result["mutation_authority"]["authority_mode"] == "delegated_orchestration_override" + assert result["mutation_authority"]["outcome"] == "apply" + assert result["mutation_authority"]["lease_fence"] == "not_required" + assert _canonical_todo(registry, todo["todo_id"])["last_actor_agent_id"] == ORCHESTRATION_AGENT + + assert _canonical_todo(registry, todo["todo_id"])["status"] == "done" def test_delegated_orchestrator_requires_reason_before_state_write( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture( + registry, state = canonical_workspace( tmp_path, lifecycle_authority=[ { @@ -1925,9 +1935,9 @@ def test_delegated_orchestrator_requires_reason_before_state_write( ], ) todo = _add_agent_todo(registry, claimed_by=REVIEW_AGENT) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="requires --authority-reason"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1935,14 +1945,15 @@ def test_delegated_orchestrator_requires_reason_before_state_write( agent_id=ORCHESTRATION_AGENT, evidence="must remain atomic", ) + assert rejected.value.code == "delegation_reason_required" - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before def test_delegated_orchestration_authority_is_action_scoped( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture( + registry, state = canonical_workspace( tmp_path, lifecycle_authority=[ { @@ -1953,9 +1964,9 @@ def test_delegated_orchestration_authority_is_action_scoped( ], ) todo = _add_agent_todo(registry, claimed_by=REVIEW_AGENT) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="does not grant action='complete'"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1964,19 +1975,21 @@ def test_delegated_orchestration_authority_is_action_scoped( authority_reason="This grant only permits reassignment.", evidence="unauthorized-action", ) - assert state.read_text(encoding="utf-8") == before + assert rejected.value.code == "delegation_action_not_granted" + assert _canonical_snapshot(registry, state) == before - with pytest.raises(ValueError, match="does not grant action='update'"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: 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", + note="A reassign grant cannot edit copy.", authority_reason="A reassign grant cannot carry another mutation.", ) - assert state.read_text(encoding="utf-8") == before + assert rejected.value.code == "delegation_action_not_granted" + assert _canonical_snapshot(registry, state) == before result = update_goal_todo( registry_path=registry, @@ -1987,14 +2000,14 @@ def test_delegated_orchestration_authority_is_action_scoped( 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 + assert result["claimed_by"] == AUTHOR_AGENT + assert _canonical_todo(registry, todo["todo_id"])["claimed_by"] == AUTHOR_AGENT def test_delegated_override_never_bypasses_explicit_exclusion( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture( + registry, state = canonical_workspace( tmp_path, lifecycle_authority=[ { @@ -2009,9 +2022,9 @@ def test_delegated_override_never_bypasses_explicit_exclusion( claimed_by=REVIEW_AGENT, excluded_agents=[ORCHESTRATION_AGENT], ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="is excluded from mutating"): + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -2020,5 +2033,41 @@ def test_delegated_override_never_bypasses_explicit_exclusion( authority_reason="An explicit exclusion remains authoritative.", evidence="must-not-write", ) + assert rejected.value.code == "actor_excluded" - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before + + +@pytest.mark.parametrize("command", ["update", "complete"]) +def test_canonical_single_agent_owned_todo_requires_explicit_actor( + tmp_path: Path, canonical_workspace, command: str, +) -> None: + registry, state = canonical_workspace(tmp_path, multi_agent=False) + todo = _add_agent_todo(registry) + before = _canonical_snapshot(registry, state) + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: + if command == "update": + update_goal_todo(registry_path=registry, goal_id=GOAL_ID, + todo_id=todo["todo_id"], note="Actorless owned mutation is forbidden") + else: + complete_goal_todo(registry_path=registry, goal_id=GOAL_ID, + todo_id=todo["todo_id"], evidence="Actorless owned mutation is forbidden") + assert rejected.value.code == "actor_required" + assert _canonical_snapshot(registry, state) == before + + +def test_canonical_update_revision_conflict_is_not_actor_rejection( + tmp_path: Path, canonical_workspace, +) -> None: + registry, state = canonical_workspace(tmp_path) + todo = _add_agent_todo(registry) + before = _canonical_snapshot(registry, state) + with pytest.raises(LocalCoordinationAuthorityUnavailable) as rejected: + update_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], + agent_id=AUTHOR_AGENT, note="A stale writer cannot commit", + update_expected_provider_revision="stale-revision", + ) + assert rejected.value.code == "provider_revision_mismatch" + assert not isinstance(rejected.value, ValueError) + assert _canonical_snapshot(registry, state) == before diff --git a/tests/control_plane_ts/todo_terminal_decision.test.ts b/tests/control_plane_ts/todo_terminal_decision.test.ts index 08d755ab50..2e97441d59 100644 --- a/tests/control_plane_ts/todo_terminal_decision.test.ts +++ b/tests/control_plane_ts/todo_terminal_decision.test.ts @@ -440,3 +440,23 @@ test("hard-lease divergence and terminal replay fail closed in the established o assert.equal(replayed.code, "terminal_replay"); assert.equal(replayed.idempotent, true); }); + +test("unneeded malformed delegation cannot block ordinary actor authority", () => { + for (const evaluate of [evaluateCoordinationTodoTerminalDecision, evaluateCoordinationTodoMutationDecision]) { + const build = evaluate === evaluateCoordinationTodoTerminalDecision ? request : mutation; + const invalid = [{agent_id: "agent-b", actions: [], requires_reason: true}]; + const own = build({handoff_mode: "soft_claim", lifecycle_grants: invalid}); + assert.equal(evaluate(own).outcome, "apply"); + assert.equal(evaluate({...own, actor_agent_id: "unknown"}).code, "actor_not_registered"); + assert.equal(evaluate({...own, todo: {...own.todo, excluded_agents: ["agent-a"]}}).code, "actor_excluded"); + // This caller really needs delegation: malformed authority must fail closed. + assert.throws(() => evaluate({...own, actor_agent_id: "agent-b"}), /supported actions/); + const granted = {...own, actor_agent_id: "agent-b", authority_reason: "reviewed worker result", + lifecycle_grants: [{agent_id: "agent-b", actions: [own.authority_action]}]}; + assert.equal(evaluate(granted).authority_mode, "delegated_orchestration_override"); + assert.equal(evaluate({...granted, authority_reason: null}).code, "delegation_reason_required"); + assert.throws(() => evaluate({...granted, lifecycle_grants: [ + {agent_id: "agent-b", actions: [own.authority_action], requires_reason: null}, + ]}), /requires_reason/); + } +}); From 8f96961ee4b358a22418e56dd8a665b1d45793ea Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:41:55 +0800 Subject: [PATCH 44/54] fix: classify terminal completion fence rejections consistently Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../coordination/todo_terminal_lifecycle.ts | 7 +- .../canonical_authority_fixture.py | 6 +- .../test_todo_mutation_authority.py | 211 +++++++++--------- .../authority_store_conformance.ts | 5 +- 4 files changed, 121 insertions(+), 108 deletions(-) diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index 67b6cba604..c703d4fa84 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -1,3 +1,4 @@ +import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; import {planUserCompletion, requireCompletionDecisionOutcome} from "../todos/user_completion.ts"; import {AUTHORITY_SOURCE_CHANGED, uncheckedAuthoritySource, type AuthoritySourceCheck} from "./authority_source.ts"; @@ -1351,7 +1352,8 @@ export async function executeCoordinationTodoTerminalLifecycle( requested_completion_identity_source: input.requested_completion_identity_source}); } catch (error) { return terminalFailure("invalid_todo_completion_transaction", - error instanceof Error ? error.message : "Invalid original completion identity"); + error instanceof Error ? error.message : "Invalid original completion identity", {}, + error instanceof EffectRuntimeRequestError ? "decision_rejection" : "protocol_failure"); } if (fence.outcome === "continue" && fence.reason === "same_turn_terminal_upgrade") { const originalInput = {...input, requested_no_followup: false, @@ -1444,7 +1446,8 @@ export async function executeCoordinationTodoTerminalLifecycle( } catch (error) { return terminalFailure( "invalid_todo_completion_transaction", - error instanceof Error ? error.message : "invalid Todo completion transaction", + error instanceof Error ? error.message : "invalid Todo completion transaction", {}, + error instanceof EffectRuntimeRequestError ? "decision_rejection" : "protocol_failure", ); } if (completion.decision === "execute_validation" || diff --git a/tests/control_plane/canonical_authority_fixture.py b/tests/control_plane/canonical_authority_fixture.py index a834c8eb3e..93b12ad1ea 100644 --- a/tests/control_plane/canonical_authority_fixture.py +++ b/tests/control_plane/canonical_authority_fixture.py @@ -120,7 +120,9 @@ def promoted_create_fixture(tmp_path: Path, *, provider: str = "file") -> tuple[ return registry_path, runtime_root, state_file -def initialize_canonical_registry_fixture(registry_path: Path, *, provider: str = "file") -> None: +def initialize_canonical_registry_fixture( + registry_path: Path, *, provider: str = "file", handoff_mode: str = "soft_claim", +) -> None: """Seed disposable consumer fixtures; this is not cold-source import evidence. Fixture prose supplies synthetic records only. Normal writes and subsequent @@ -137,6 +139,6 @@ def initialize_canonical_registry_fixture(registry_path: Path, *, provider: str goal, state_text=state.read_text(encoding="utf-8"), state_path=state, rollout_events=[] ) projection = build_todo_runtime_shadow_projection( - goal_id=goal["id"], todos=todos, handoff_mode="soft_claim" + goal_id=goal["id"], todos=todos, handoff_mode=handoff_mode ) initialize_canonical_authority(runtime, goal["id"], projection, state_path=state, provider=provider) diff --git a/tests/control_plane/test_todo_mutation_authority.py b/tests/control_plane/test_todo_mutation_authority.py index 8d730d0336..5f4af81da9 100644 --- a/tests/control_plane/test_todo_mutation_authority.py +++ b/tests/control_plane/test_todo_mutation_authority.py @@ -17,6 +17,7 @@ from loopx.control_plane.work_items.task_lease import ( TaskLeaseError, acquire_task_lease, + inspect_task_lease, release_task_lease, ) from loopx.status import parse_active_state_todos @@ -101,9 +102,9 @@ def canonical_workspace(request, tmp_path, monkeypatch): ) isolate_sqlite_runtime(tmp_path, monkeypatch) - def create(root, **kwargs): + def create(root, *, handoff_mode="soft_claim", **kwargs): registry, state = _write_fixture(root, **kwargs) - initialize_canonical_registry_fixture(registry, provider=request.param) + initialize_canonical_registry_fixture(registry, provider=request.param, handoff_mode=handoff_mode) readback = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) assert readback["authority_read"]["source_authority"] == request.param + "_v0" assert readback["authority_read"]["legacy_fallback_used"] is False @@ -121,6 +122,16 @@ def _canonical_snapshot(registry: Path, state: Path) -> tuple: return result["authority_read"]["provider_revision"], result["todos"], state.read_bytes() +def _canonical_lease(registry: Path, tmp_path: Path, todo_id: str) -> dict | None: + result = inspect_task_lease( + registry_path=registry, runtime_root=tmp_path / "runtime", + goal_id=GOAL_ID, todo_id=todo_id, + ) + assert result["source_authority"] in {"file_v0", "sqlite_v0"} + assert result["legacy_fallback_used"] is False + return result.get("lease") + + def _lease_path(tmp_path: Path, todo_id: str) -> Path: return tmp_path / "runtime" / "goals" / GOAL_ID / "task-leases" / f"{todo_id}.json" @@ -754,9 +765,9 @@ def test_capability_binding_follows_generated_agent_successor(tmp_path: Path) -> def test_active_task_lease_fences_same_agent_completion_instance( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) + registry, state = canonical_workspace(tmp_path, multi_agent=False, handoff_mode="hard_lease") todo = _add_agent_todo(registry) lease_key = "turn-owner-instance" acquire_task_lease( @@ -768,72 +779,69 @@ def test_active_task_lease_fences_same_agent_completion_instance( idempotency_key=lease_key, ttl_seconds=600, ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(TaskLeaseError) as missing_fence: + with pytest.raises(LocalCoordinationAuthorityRejection) as missing_fence: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=AUTHOR_AGENT, evidence="stale execution instance", next_agent_todo="Create a stale successor.", ) assert missing_fence.value.code == "lease_fence_required" - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before - with pytest.raises(TaskLeaseError) as mismatched_fence: + with pytest.raises(LocalCoordinationAuthorityRejection) as mismatched_fence: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=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 _canonical_snapshot(registry, state) == before completed = complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=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" - ) + decision = completed["task_lease_fence"] + assert decision["outcome"] == "apply" + assert decision["lease_fence"] == "required" + tombstone = _canonical_lease(registry, tmp_path, todo["todo_id"]) + assert tombstone["status"] == "released" + assert tombstone["owner"] == AUTHOR_AGENT + assert tombstone["version"] == tombstone["lease_epoch"] == 1 + completed_state = _canonical_snapshot(registry, state) replayed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, - evidence="late result from another execution instance", - next_agent_todo="Create a duplicate stale successor.", + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], + agent_id=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.", ) + with pytest.raises(LocalCoordinationAuthorityRejection) as changed_intent: + complete_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], + agent_id=AUTHOR_AGENT, evidence="late result from another execution instance", + next_agent_todo="Create a duplicate stale successor.", + ) + assert changed_intent.value.code == "coordination_operation_identity_mismatch" 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"] + assert _canonical_snapshot(registry, state) == completed_state + assert _canonical_lease(registry, tmp_path, todo["todo_id"]) == tombstone + agent_todos = list_goal_todos(registry_path=registry, goal_id=GOAL_ID)["todos"] todo_titles = [ str(item.get("title") or item.get("text") or "") for item in agent_todos ] @@ -842,9 +850,9 @@ def test_active_task_lease_fences_same_agent_completion_instance( def test_released_lease_generation_rejects_stale_completion_fences( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) + registry, state = canonical_workspace(tmp_path, multi_agent=False, handoff_mode="hard_lease") todo = _add_agent_todo(registry) runtime_root = tmp_path / "runtime" first = acquire_task_lease( @@ -869,7 +877,7 @@ def test_released_lease_generation_rejects_stale_completion_fences( registry_path=registry, ) assert released["lease"]["status"] == "released" - assert _lease_path(tmp_path, todo["todo_id"]).exists() + assert _canonical_lease(registry, tmp_path, todo["todo_id"])["status"] == "released" with pytest.raises(TaskLeaseError) as reused_execution: acquire_task_lease( @@ -894,70 +902,68 @@ def test_released_lease_generation_rejects_stale_completion_fences( ) assert second["lease"]["version"] == 2 assert second["lease"]["lease_epoch"] == 2 - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(TaskLeaseError) as old_key: + with pytest.raises(LocalCoordinationAuthorityRejection) as old_key: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=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 + assert _canonical_snapshot(registry, state) == before - with pytest.raises(TaskLeaseError) as old_version: + with pytest.raises(LocalCoordinationAuthorityRejection) as old_version: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=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 + assert _canonical_snapshot(registry, state) == before - with pytest.raises(TaskLeaseError) as missing_version: + with pytest.raises(LocalCoordinationAuthorityRejection) as missing_version: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=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 + assert _canonical_snapshot(registry, state) == before completed = complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=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 completed["task_lease_fence"]["next_lease"]["lease_epoch"] == 2 + tombstone = _canonical_lease(registry, tmp_path, todo["todo_id"]) 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) +def test_completion_turn_key_rejects_cross_turn_replay(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = _add_agent_todo(registry) completed = complete_goal_todo( @@ -970,10 +976,10 @@ def test_completion_turn_key_rejects_cross_turn_replay(tmp_path: Path) -> None: no_followup=True, ) assert completed["completed"] is True - completed_todo = _agent_todo(state, todo["todo_id"]) + completed_todo = _canonical_todo(registry, 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") + completed_state = _canonical_snapshot(registry, state) with pytest.raises(ValueError, match="different completion_turn_key"): complete_goal_todo( @@ -985,7 +991,7 @@ def test_completion_turn_key_rejects_cross_turn_replay(tmp_path: Path) -> None: completion_turn_key="turn-b", no_followup=True, ) - assert state.read_text(encoding="utf-8") == completed_state + assert _canonical_snapshot(registry, state) == completed_state replayed = complete_goal_todo( registry_path=registry, @@ -998,17 +1004,18 @@ def test_completion_turn_key_rejects_cross_turn_replay(tmp_path: Path) -> None: ) assert replayed["idempotent_replay"] is True assert replayed["changed"] is False + assert _canonical_snapshot(registry, state) == completed_state -def test_terminal_upgrade_cannot_replace_existing_successor(tmp_path: Path) -> None: - registry, _state = _write_fixture(tmp_path, multi_agent=False) +def test_terminal_upgrade_cannot_replace_existing_successor(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(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, + agent_id=AUTHOR_AGENT, completion_turn_key="turn-a", evidence="ordinary completion created the successor", next_agent_todo="Continue the durable successor.", @@ -1016,42 +1023,46 @@ def test_terminal_upgrade_cannot_replace_existing_successor(tmp_path: Path) -> N assert completed["changed"] is True assert completed["completion_continuation"] == "successor" - with pytest.raises(ValueError, match="cannot replace an existing successor"): + completed_state = _canonical_snapshot(registry, state) + with pytest.raises(ValueError, match="operation id already names a different coordination request"): complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=AUTHOR_AGENT, completion_turn_key="turn-a", evidence="terminal closeout must not erase the successor", no_followup=True, ) + assert _canonical_snapshot(registry, state) == completed_state -def test_terminal_upgrade_requires_original_completion_turn_key(tmp_path: Path) -> None: - registry, _state = _write_fixture(tmp_path, multi_agent=False) +def test_terminal_upgrade_requires_original_completion_turn_key(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(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, + agent_id=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" + completed_state = _canonical_snapshot(registry, state) 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, + agent_id=AUTHOR_AGENT, evidence="an unscoped caller cannot claim terminal closeout", no_followup=True, ) + assert _canonical_snapshot(registry, state) == completed_state def test_terminal_upgrade_rejects_untyped_legacy_completion_until_repaired( @@ -1112,25 +1123,22 @@ def test_terminal_upgrade_rejects_untyped_legacy_completion_until_repaired( 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) +def test_unfenced_completion_leaves_no_lease_artifacts(tmp_path: Path, canonical_workspace) -> None: + registry, _state = canonical_workspace(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, + agent_id=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, - } + assert completed["task_lease_fence"]["lease_fence"] == "not_required" + assert _canonical_lease(registry, tmp_path, todo["todo_id"]) is None lease_dir = _lease_path(tmp_path, todo["todo_id"]).parent assert list(lease_dir.glob("todo_*.json")) == [] @@ -1180,8 +1188,8 @@ def test_divergent_claim_completion_leaves_foreign_lease_untouched( 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) +def test_dry_run_completion_does_not_release_lease(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path, multi_agent=False, handoff_mode="hard_lease") todo = _add_agent_todo(registry) lease_key = "dry-run-instance" acquire_task_lease( @@ -1193,13 +1201,13 @@ def test_dry_run_completion_does_not_release_lease(tmp_path: Path) -> None: idempotency_key=lease_key, ttl_seconds=600, ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) completed = complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=AUTHOR_AGENT, task_lease_idempotency_key=lease_key, task_lease_expected_version=1, evidence="dry run must not touch the lease", @@ -1208,11 +1216,10 @@ def test_dry_run_completion_does_not_release_lease(tmp_path: Path) -> None: ) 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 + assert completed["task_lease_fence"]["lease_fence"] == "required" + assert completed["task_lease_fence"]["next_lease"]["status"] == "released" + assert _canonical_lease(registry, tmp_path, todo["todo_id"])["status"] == "active" + assert _canonical_snapshot(registry, state) == before def test_release_failure_after_commit_keeps_completion_ok( @@ -1342,12 +1349,12 @@ def test_reopened_todo_completes_unfenced_after_lease_release( def test_cli_release_after_auto_release_replays_terminal_tombstone( - tmp_path: Path, + tmp_path: Path, canonical_workspace, capsys: pytest.CaptureFixture[str], ) -> None: """The public CLI replays the retained terminal result, not `missing`.""" - registry, _state = _write_fixture(tmp_path, multi_agent=False) + registry, _state = canonical_workspace(tmp_path, multi_agent=False, handoff_mode="hard_lease") todo = _add_agent_todo(registry) lease_key = "double-release-instance" acquire_task_lease( @@ -1363,14 +1370,13 @@ def test_cli_release_after_auto_release_replays_terminal_tombstone( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - claimed_by=AUTHOR_AGENT, + agent_id=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")) + tombstone = _canonical_lease(registry, tmp_path, todo["todo_id"]) exit_code = cli_main( [ @@ -1404,13 +1410,13 @@ def test_cli_release_after_auto_release_replays_terminal_tombstone( 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 + assert _canonical_lease(registry, tmp_path, todo["todo_id"]) == tombstone -def test_exception_after_verified_fence_leaves_lease_intact( - tmp_path: Path, +def test_invalid_completion_keeps_existing_lease_and_state( + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) + registry, state = canonical_workspace(tmp_path, multi_agent=False, handoff_mode="hard_lease") todo = _add_agent_todo(registry) lease_key = "post-fence-failure-instance" acquire_task_lease( @@ -1422,26 +1428,25 @@ def test_exception_after_verified_fence_leaves_lease_intact( 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") + lease_before = _canonical_lease(registry, tmp_path, todo["todo_id"]) + state_before = _canonical_snapshot(registry, state) 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, + agent_id=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", + evidence="invalid successor must leave state and lease unchanged", 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 + assert _canonical_lease(registry, tmp_path, todo["todo_id"]) == lease_before + assert lease_before["status"] == "active" + assert _canonical_snapshot(registry, state) == state_before def test_capability_binding_cannot_be_rebound_by_duplicate_add(tmp_path: Path) -> None: diff --git a/tests/control_plane_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts index 91ed524072..d525f92f15 100644 --- a/tests/control_plane_ts/authority_store_conformance.ts +++ b/tests/control_plane_ts/authority_store_conformance.ts @@ -848,6 +848,7 @@ export function registerAuthorityStoreConformance( {...commitRequest, operation_identity: {kind: "explicit" as const, operation_id: "unknown-terminal-operation"}}); assert.equal(noReceipt.status, "failed"); assert.equal(noReceipt.reason_code, "invalid_todo_completion_transaction"); + assert.equal(noReceipt.failure_kind, "decision_rejection"); assert.deepEqual(await store.loadAuthority(), committedHead); const committedRevisions = [first, second] @@ -1034,7 +1035,9 @@ export function registerAuthorityStoreConformance( for (const change of [{lease_expected_version: 99}, {lease_idempotency_key: "borrowed"}, {requested_completion_turn_key: "different-turn"}, {actor_agent_id: "agent-b"}, {validation_declaration: {...TERMINAL_VALIDATION_DECLARATION, validation_label: "different check"}}]) { - assert.equal((await executeCoordinationTodoTerminalLifecycle(store, {...closeout, ...change})).status, "failed"); + const rejected = await executeCoordinationTodoTerminalLifecycle(store, {...closeout, ...change}); + assert.equal(rejected.status, "failed"); + assert.equal(rejected.failure_kind, "decision_rejection"); assert.deepEqual(await store.loadAuthority(), before); } const missingReceipt = new Proxy(store, {get(target, key) { From a39a24423b04e17a23a6b5bc7abbfdf5bb86692c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:43:59 +0800 Subject: [PATCH 45/54] test: distinguish Todo state from SQLite operation status Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_sqlite_authority_cli.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/control_plane/test_sqlite_authority_cli.py b/tests/control_plane/test_sqlite_authority_cli.py index 6da7f9eaa3..ff5635ddb1 100644 --- a/tests/control_plane/test_sqlite_authority_cli.py +++ b/tests/control_plane/test_sqlite_authority_cli.py @@ -68,10 +68,17 @@ def cli(*args, expected_code=0): "--status", "deferred", "--resume-when", "resume_at:2026-09-14T09:30:00+08:00", "--reason", "Await the exact scheduled instant", "--update-operation-id", "sqlite-planning") - assert cli(*planning, "--dry-run")["status"] == "planned" + planned = cli(*planning, "--dry-run") + assert planned["status"] == "deferred" + assert planned["provider_status"] == "planned" + assert cli("list")["todos"][0]["status"] == "open" assert not state.exists() assert cli(*planning)["source_authority"] == "sqlite_v0" - assert cli(*planning)["status"] == "replayed" + before_replay = cli("list") + replayed = cli(*planning) + assert replayed["status"] == "deferred" + assert replayed["provider_status"] == "replayed" + assert cli("list")["authority_read"]["provider_revision"] == before_replay["authority_read"]["provider_revision"] assert "resume_at:2026-09-14T01:30:00Z" in json.dumps(cli("list")) resumed = cli("update", "--role", "agent", "--todo-id", "todo_sqlite", "--agent-id", "agent-a", "--status", "open", "--clear-resume-when") @@ -219,7 +226,9 @@ def todos(): spent = command(*spend_args) assert spent["settlement_progress"]["state"] == "settled" restart_effect_runtime() - assert command(*update)["status"] == "replayed" + replayed_update = command(*update) + assert replayed_update["status"] == "open" + assert replayed_update["provider_status"] == "replayed" replay = command(*spend_args) assert replay["idempotent_replay"] is True and replay["appended"] is False settled = command(*guard_args) From 37fc52f8e8fd187e217b2e9d7c3cd49dff214f53 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:04:19 +0800 Subject: [PATCH 46/54] fix: retain delegated terminal authority reasons in receipts Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../coordination/todo_terminal_lifecycle.ts | 9 ++- .../test_todo_lifecycle_authority_config.py | 63 +++++++++++-------- .../authority_store_conformance.ts | 30 +++++++-- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index c703d4fa84..9d7a8a13de 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -1505,6 +1505,11 @@ export async function executeCoordinationTodoTerminalLifecycle( ); } + // Persist the accepted delegation justification in both changed and no-op + // receipts. Replay returns the stored value; historical receipts stay intact. + const terminalDecisionReceipt = authority.authority_mode === "delegated_orchestration_override" + ? {...authority, authority_reason: input.authority_reason} : authority; + 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) => @@ -1523,7 +1528,7 @@ export async function executeCoordinationTodoTerminalLifecycle( todo_id: input.todo_id, command: input.command, changed: false, - terminal_decision: authority, + terminal_decision: terminalDecisionReceipt, successor_todo_ids: existingSuccessorIds, generated_successor_todo_ids: [], validation_receipt: null, @@ -1670,7 +1675,7 @@ export async function executeCoordinationTodoTerminalLifecycle( ...(input.decision_outcome === null ? {} : {decision_outcome: input.decision_outcome}), ...(followthrough?.unblock_resume ? {unblock_resume: followthrough.unblock_resume} : {}), ...(followthrough?.decision_scope_resolution ? {decision_scope_resolution: followthrough.decision_scope_resolution} : {}), - terminal_decision: authority, + terminal_decision: terminalDecisionReceipt, successor_todo_ids: successorIds, generated_successor_todo_ids: generatedSuccessorIds, generated_successors: successorCandidates, diff --git a/tests/control_plane/test_todo_lifecycle_authority_config.py b/tests/control_plane/test_todo_lifecycle_authority_config.py index 8e55b1197c..73fc534616 100644 --- a/tests/control_plane/test_todo_lifecycle_authority_config.py +++ b/tests/control_plane/test_todo_lifecycle_authority_config.py @@ -7,7 +7,7 @@ from loopx.cli import main from loopx.configure_goal import configure_goal -from loopx.todos import add_goal_todo +from loopx.todos import add_goal_todo, list_goal_todos GOAL_ID = "todo-lifecycle-authority-config" @@ -27,6 +27,7 @@ def _registry(tmp_path: Path) -> Path: registry.write_text( json.dumps( { + "common_runtime_root": str(tmp_path / "runtime"), "goals": [ { "id": GOAL_ID, @@ -176,11 +177,16 @@ def test_configure_goal_cli_previews_authority_grant( assert payload["after"]["todo_lifecycle_authority"] == [grant] +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_todo_cli_emits_delegated_override_receipt( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], + tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch, provider: str, ) -> None: + from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_registry_fixture, isolate_sqlite_runtime, + ) + isolate_sqlite_runtime(tmp_path, monkeypatch) registry = _registry(tmp_path) + initialize_canonical_registry_fixture(registry, provider=provider) configure_goal( registry_path=registry, goal_id=GOAL_ID, @@ -202,32 +208,39 @@ def test_todo_cli_emits_delegated_override_receipt( claimed_by=OWNER_AGENT, ) - exit_code = main( - [ - "--registry", - str(registry), - "--format", - "json", - "todo", - "complete", - "--goal-id", - GOAL_ID, - "--todo-id", - todo["todo_id"], - "--agent-id", - ORCHESTRATION_AGENT, - "--authority-reason", - "Verified the result before orchestration closeout.", - "--evidence", - "validation://cli-override", - ] - ) + argv = [ + "--registry", + str(registry), + "--format", + "json", + "todo", + "complete", + "--goal-id", + GOAL_ID, + "--todo-id", + todo["todo_id"], + "--agent-id", + ORCHESTRATION_AGENT, + "--authority-reason", + "Verified the result before orchestration closeout.", + "--evidence", + "validation://cli-override", + ] - assert exit_code == 0 + assert main(argv) == 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." ) + + before = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) + assert before["authority_read"]["source_authority"] == provider + "_v0" + assert before["authority_read"]["legacy_fallback_used"] is False + assert payload["original_receipt"]["result"]["terminal_decision"]["authority_reason"] == ( + "Verified the result before orchestration closeout." + ) + assert before["todos"][0]["status"] == "done" + assert before["todos"][0]["last_actor_agent_id"] == ORCHESTRATION_AGENT diff --git a/tests/control_plane_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts index d525f92f15..4157770031 100644 --- a/tests/control_plane_ts/authority_store_conformance.ts +++ b/tests/control_plane_ts/authority_store_conformance.ts @@ -1080,20 +1080,24 @@ export function registerAuthorityStoreConformance( assert.deepEqual(await store.loadAuthority(), after); }); - test(`${providerName} conformance: pre-upgrade terminal receipt recovers before current authority access`, async t => { + for (const delegated of [false, true]) test(`${providerName} conformance: original terminal receipt recovers before current authority access (delegated=${delegated})`, async t => { const {store} = await factory(t); const goalId = "goal-historical-terminal", turnKey = `${goalId}:agent-a:todo-terminal:original-turn`; const digest = createHash("sha256").update( `loopx-provider-terminal-operation-v0\0complete\0${goalId}\0todo-terminal\0${turnKey}`, ).digest("hex").slice(0, 32); + const projection = todoTerminalProjection(goalId); + // Delegate an idle soft-claim task; active lease fencing is an independent gate. + if (delegated) { projection.handoff_mode = "soft_claim"; projection.leases = []; } assert.equal((await store.commitAuthority({expected_provider_revision: null, operation_id: "seed-historical-terminal", - next_projection: todoTerminalProjection(goalId), events: [], receipts: []})).status, "applied"); + next_projection: projection, events: [], receipts: []})).status, "applied"); const request = { goal_id: goalId, todo_id: "todo-terminal", expected_role: "agent" as const, - command: "complete" as const, actor_agent_id: "agent-a", registered_agents: ["agent-a", "agent-b"], - lifecycle_grants: [], authority_reason: null, decision_outcome: null, + command: "complete" as const, actor_agent_id: delegated ? "agent-b" : "agent-a", registered_agents: ["agent-a", "agent-b"], + lifecycle_grants: delegated ? [{agent_id: "agent-b", actions: ["complete"], requires_reason: true}] : [], + authority_reason: delegated ? "Verified the original worker result" : null, decision_outcome: null, operation_identity: {kind: "explicit" as const, operation_id: `todo-terminal:${digest}`}, - lease_idempotency_key: "terminal-lease", lease_expected_version: 1, allow_user_gate_auto_acquire: false, + lease_idempotency_key: delegated ? null : "terminal-lease", lease_expected_version: delegated ? null : 1, allow_user_gate_auto_acquire: false, requested_no_followup: true, requested_completion_turn_key: turnKey, requested_completion_identity_source: "turn_settlement" as const, linked_successor_todo_ids: [], successor_intents: [], note: null, evidence: "Declared check passed", @@ -1104,6 +1108,16 @@ export function registerAuthorityStoreConformance( }; const applied = await executeCoordinationTodoTerminalLifecycle(store, request); assert.equal(applied.status, "applied", JSON.stringify(applied)); + const authority = applied.terminal_decision as JsonObject; + if (delegated) { + assert.equal(authority.authority_mode, "delegated_orchestration_override"); + assert.equal(authority.authority_reason, request.authority_reason); + const original = applied.original_receipt as JsonObject; + assert.equal(((original.result as JsonObject).terminal_decision as JsonObject).authority_reason, + request.authority_reason); + } else { + assert.equal(Object.hasOwn(authority, "authority_reason"), false); + } const before = await store.loadAuthority(); const historicalOnly = new Proxy(store, {get(target, key) { if (key === "loadAuthority" || key === "commitAuthority") return async () => { @@ -1117,6 +1131,12 @@ export function registerAuthorityStoreConformance( }, async () => {throw new Error("historical recovery precedes current source authorization");}); assert.equal(recovered.status, "replayed", JSON.stringify(recovered)); assert.deepEqual(recovered.original_receipt, applied.original_receipt); + assert.deepEqual(recovered.terminal_decision, applied.terminal_decision); + if (delegated) { + const changed = await executeCoordinationTodoTerminalLifecycle(historicalOnly, + {...request, authority_reason: "Replacement justification"}); + assert.equal(changed.reason_code, "coordination_operation_identity_mismatch"); + } assert.deepEqual(await store.loadAuthority(), before); assert.equal((await store.readReceipt(`todo-terminal-closeout:${digest}`)).status, "missing"); }); From 7229e95c8883f2823ab7ce1d61629d11a9b61a3f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:04:19 +0800 Subject: [PATCH 47/54] refactor: retire callerless Python Todo lifecycle authorization Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../control_plane/todos/mutation_authority.py | 333 +----------------- loopx/todos.py | 2 - .../test_todo_mutation_authority.py | 241 +++++-------- 3 files changed, 82 insertions(+), 494 deletions(-) diff --git a/loopx/control_plane/todos/mutation_authority.py b/loopx/control_plane/todos/mutation_authority.py index 60364480de..cf3d0e461f 100644 --- a/loopx/control_plane/todos/mutation_authority.py +++ b/loopx/control_plane/todos/mutation_authority.py @@ -7,26 +7,9 @@ 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"} ) @@ -95,317 +78,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/todos.py b/loopx/todos.py index 2bae3d483f..cc4de21f2e 100644 --- a/loopx/todos.py +++ b/loopx/todos.py @@ -97,8 +97,6 @@ "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", diff --git a/tests/control_plane/test_todo_mutation_authority.py b/tests/control_plane/test_todo_mutation_authority.py index 5f4af81da9..4910de79ed 100644 --- a/tests/control_plane/test_todo_mutation_authority.py +++ b/tests/control_plane/test_todo_mutation_authority.py @@ -12,7 +12,6 @@ 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, @@ -23,6 +22,7 @@ from loopx.status import parse_active_state_todos from loopx.todos import ( list_goal_todos, + archive_completed_todos, add_goal_todo, complete_goal_todo, supersede_goal_todo, @@ -102,8 +102,10 @@ def canonical_workspace(request, tmp_path, monkeypatch): ) isolate_sqlite_runtime(tmp_path, monkeypatch) - def create(root, *, handoff_mode="soft_claim", **kwargs): + def create(root, *, handoff_mode="soft_claim", source_rows="", **kwargs): registry, state = _write_fixture(root, **kwargs) + if source_rows: + state.write_text(state.read_text(encoding="utf-8") + source_rows, encoding="utf-8") initialize_canonical_registry_fixture(registry, provider=request.param, handoff_mode=handoff_mode) readback = list_goal_todos(registry_path=registry, goal_id=GOAL_ID) assert readback["authority_read"]["source_authority"] == request.param + "_v0" @@ -526,194 +528,113 @@ def test_owner_actor_update_returns_typed_receipt(tmp_path: Path, canonical_work @pytest.mark.parametrize("status", ["deferred", "blocked", "done"]) -def test_claim_rejection_preserves_status_suffix( - tmp_path: Path, - status: str, +def test_canonical_claim_rejects_nonopen_status_atomically( + tmp_path: Path, canonical_workspace, status: str, ) -> None: - """Claim rejections keep the legacy todo_id/status diagnostic suffix.""" - - registry, state = _write_fixture(tmp_path) + registry, state = canonical_workspace(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", + 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", + 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, + 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: + before = _canonical_snapshot(registry, state) + with pytest.raises(LocalCoordinationAuthorityRejection) as error: 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, + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], + claim_only=True, claimed_by=AUTHOR_AGENT, agent_id=AUTHOR_AGENT, ) + assert error.value.code == "todo_not_open" + assert error.value.payload["todo_status"] == status + assert _canonical_snapshot(registry, state) == before + assert _canonical_lease(registry, tmp_path, todo["todo_id"]) is None - assert str(excinfo.value) == ( - f"todo claim requires status=open; todo_id '{todo['todo_id']}' " - f"is status='{status}'" - ) - 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.""" - registry, state = _write_fixture(tmp_path) +def test_canonical_claim_rejects_user_todo_atomically(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="user", - text="Owner reviews the delivered change.", - task_class="user_action", + 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: + before = _canonical_snapshot(registry, state) + with pytest.raises(LocalCoordinationAuthorityRejection) as error: 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, + 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", - ) - before = state.read_text(encoding="utf-8") - - with pytest.raises(ValueError) as excinfo: + assert error.value.code == "todo_not_agent" + assert _canonical_snapshot(registry, state) == before + assert _canonical_todo(registry, todo["todo_id"])["bound_agent"] == AUTHOR_AGENT + + +def test_canonical_claim_requires_removed_policy_repair(tmp_path: Path, canonical_workspace) -> None: + # A synthetic historical record is captured once, before canonical creation. + # The claim then reads the real provider; no live authority is rewritten. + registry, state = canonical_workspace(tmp_path, source_rows=( + "- [ ] Repair this imported continuation before claiming.\n" + " \n" + )) + assert _canonical_todo(registry, "todo_removedpolicy")["removed_continuation_policy"] == "primary_review" + before = _canonical_snapshot(registry, state) + with pytest.raises(LocalCoordinationAuthorityRejection) as error: 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, + registry_path=registry, goal_id=GOAL_ID, todo_id="todo_removedpolicy", + claim_only=True, claimed_by=AUTHOR_AGENT, agent_id=AUTHOR_AGENT, ) + assert error.value.code == "removed_continuation_policy" + assert "must be repaired before claiming" in str(error.value) + assert _canonical_snapshot(registry, state) == before - 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_canonical_claim_cannot_reactivate_archived_work(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) + todo = _add_agent_todo(registry) + complete_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], + agent_id=AUTHOR_AGENT, evidence="finished before archival", ) - - -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, + archive_completed_todos( + registry_path=registry, goal_id=GOAL_ID, max_active_done=0, dry_run=False, ) - - 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 _canonical_todo(registry, todo["todo_id"])["archive_state"] == "archive" + before = _canonical_snapshot(registry, state) + with pytest.raises(LocalCoordinationAuthorityRejection) as error: + 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, ) + # Status eligibility precedes archive eligibility in the shared claim owner. + assert error.value.code == "todo_not_open" + assert _canonical_snapshot(registry, state) == before - assert str(excinfo.value) == ( - "todo_id 'todo_rolemismatchprobe' was not found in active user or agent todos" - ) + +def test_canonical_claim_rejects_role_mismatch_atomically(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) + todo = _add_agent_todo(registry, claimed_by=None) + before = _canonical_snapshot(registry, state) + with pytest.raises(LocalCoordinationAuthorityRejection) as error: + update_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], role="user", + claim_only=True, claimed_by=AUTHOR_AGENT, agent_id=AUTHOR_AGENT, + ) + assert error.value.code == "todo_role_mismatch" + assert error.value.payload["requested_role"] == "user" + assert error.value.payload["todo_role"] == "agent" + assert _canonical_snapshot(registry, state) == before def test_advancement_todo_preserves_public_target_key(tmp_path: Path) -> None: From 6a01d3ff9978560b331a342d01bac04f8cc61018 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:04:19 +0800 Subject: [PATCH 48/54] docs: reconcile canonical lifecycle ownership and retirement Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../rfcs/typescript-control-plane-migration-v0.md | 8 ++++++++ .../control-plane-course/05-work-graph-and-peers.md | 10 +++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 2fba254d7d..7456fdfec8 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -140,6 +140,14 @@ current File/SQLite races independently prove quiescence and policy admission against one canonical head. Legacy-source overrides refuse before writing. These are executable recovery and current-consumer checks, not sustained operation or substitutes for the independent installed/natural-trial gates. +The callerless Python lifecycle authorization chain and its facade exports are +retired. Canonical File/SQLite entrypoints qualify claim eligibility, immutable +rejection, actor/delegation and lease fences; configuration-time grant validation +and registry fact transport retain their active callers. New delegated terminal +receipts retain the accepted authority reason; original receipt recovery preserves +historical results and rejects a changed reason under the same operation identity. +This closes that private decision chain, not the remaining monitor/handoff caller +families, installed import journey or sustained SQLite qualification. The backend configuration catalog now describes the same 2.0 creation boundary. Exact-head review, core-developer trial and release authorization remain open. Retained outbox recovery and source lease release are concrete remaining callers, not grounds for keeping normal 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..440e21df59 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,11 @@ 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 事务复用 +该 TS owner;`mutation_authority.py::todo_lifecycle_facts` 只传递 registry facts, +grant 的按需校验和执行授权由事务统一决定。配置入口仍用 +`normalize_todo_lifecycle_authority` 校验输入。旧 Python 生命周期授权分支及其 +无人调用的 facade 导出已删除;未晋升源须先导入 canonical 才能执行普通写入。 这份 grant 有四个边界: From 5454df66345619a0326e8977ae2bd4e1db508b97 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:29:36 +0800 Subject: [PATCH 49/54] fix(todos): persist imported terminal continuation repair Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../coordination/todo_terminal_lifecycle.ts | 12 ++++- .../terminal_reentry_conformance.ts | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index 9d7a8a13de..e2a033e17d 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -1505,6 +1505,13 @@ export async function executeCoordinationTodoTerminalLifecycle( ); } + // The completion owner explicitly permits repairing imported records whose + // terminal continuation was never typed. A terminal status alone must not + // discard that commit decision. Reuse the existing fence reason; ordinary + // terminal replay remains a no-op and cannot add new successor intent. + const untypedCompletionRepair = completion?.decision === "commit" && + completion.fence.reason === "untyped_completion_repair"; + // Persist the accepted delegation justification in both changed and no-op // receipts. Replay returns the stored value; historical receipts stay intact. const terminalDecisionReceipt = authority.authority_mode === "delegated_orchestration_override" @@ -1524,7 +1531,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, @@ -1666,7 +1673,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 = { ...(input.user_update == null ? {} : {todo_update: todoUpdateResultFacts(authorityTodo, target.todo)}), todo_id: input.todo_id, 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 430edf185cb9901182ebedee7f0dad2a3c4e6717 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:29:46 +0800 Subject: [PATCH 50/54] test(todos): qualify canonical actor and claim lifecycle callers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_todo_claim_argument_recovery.py | 7 + .../test_todo_mutation_authority.py | 467 ++++++++---------- .../test_unpromoted_todo_claim.py | 46 +- 3 files changed, 217 insertions(+), 303 deletions(-) diff --git a/tests/control_plane/test_todo_claim_argument_recovery.py b/tests/control_plane/test_todo_claim_argument_recovery.py index 9654394698..8a4552d99d 100644 --- a/tests/control_plane/test_todo_claim_argument_recovery.py +++ b/tests/control_plane/test_todo_claim_argument_recovery.py @@ -94,6 +94,13 @@ def test_one_packet_repairs_all_grammar_errors_and_preserves_route(claim_cli): if lease: for flag, value in zip(lease[::2], lease[1::2]): assert repaired[repaired.index(flag) + 1] == value + if provider == "legacy": + # Fixing syntax cannot authorize a fresh write to an unmigrated source. + for arguments in (repaired, [arg for arg in repaired if arg != "--dry-run"]): + refused = run([*arguments, "--claimed-by", "peer-a"], expected_exit=1) + assert refused["error_code"] == "canonical_authority_required" + assert snapshot() == before + return preview = run([*repaired, "--claimed-by", "peer-a"]) assert preview["ok"] is True assert snapshot() == before diff --git a/tests/control_plane/test_todo_mutation_authority.py b/tests/control_plane/test_todo_mutation_authority.py index 4910de79ed..956cdcc3e5 100644 --- a/tests/control_plane/test_todo_mutation_authority.py +++ b/tests/control_plane/test_todo_mutation_authority.py @@ -12,14 +12,13 @@ from loopx.control_plane.scheduler.monitor_poll_writeback import ( write_monitor_poll_todo_state, ) -import loopx.control_plane.work_items.task_lease as task_lease_module +import loopx.control_plane.todos.provider_terminal_lifecycle as terminal_provider from loopx.control_plane.work_items.task_lease import ( TaskLeaseError, acquire_task_lease, inspect_task_lease, release_task_lease, ) -from loopx.status import parse_active_state_todos from loopx.todos import ( list_goal_todos, archive_completed_todos, @@ -138,13 +137,6 @@ 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( - item for item in todos["agent_todos"]["items"] if item["todo_id"] == todo_id - ) - - def _add_agent_todo( registry: Path, *, @@ -163,8 +155,8 @@ def _add_agent_todo( ) -def test_continuous_monitor_requires_explicit_boundedness(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) +def test_continuous_monitor_requires_explicit_boundedness(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path, multi_agent=False) common = { "registry_path": registry, "goal_id": GOAL_ID, @@ -192,18 +184,18 @@ def test_continuous_monitor_requires_explicit_boundedness(tmp_path: Path) -> Non }, } ) - item = _agent_todo(state, str(result["todo_id"])) + item = _canonical_todo(registry, str(result["todo_id"])) assert item["watch_only"] == "true" def test_update_to_continuous_monitor_requires_explicit_boundedness( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) + registry, state = canonical_workspace(tmp_path, multi_agent=False) todo = _add_agent_todo(registry) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="requires one of: --expires-at"): + with pytest.raises(LocalCoordinationAuthorityUnavailable, match="requires one of: --expires-at"): update_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -217,11 +209,11 @@ def test_update_to_continuous_monitor_requires_explicit_boundedness( }, ) - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before -def test_continuous_monitor_accepts_resume_condition_bound(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path, multi_agent=False) +def test_continuous_monitor_accepts_resume_condition_bound(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path, multi_agent=False) result = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -237,7 +229,7 @@ def test_continuous_monitor_accepts_resume_condition_bound(tmp_path: Path) -> No "next_due_at": "2026-08-12T00:00:00Z", }, ) - item = _agent_todo(state, str(result["todo_id"])) + item = _canonical_todo(registry, str(result["todo_id"])) assert item["resume_when"] == "pr_merged:#3083" @@ -295,9 +287,9 @@ def test_excluded_actor_cannot_mutate_unclaimed_todo(tmp_path: Path, canonical_w def test_idempotent_add_preserves_existing_executor_exclusions( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture(tmp_path) + registry, state = canonical_workspace(tmp_path) todo = _add_agent_todo( registry, text="Independently review one exact PR head.", @@ -317,19 +309,13 @@ def test_idempotent_add_preserves_existing_executor_exclusions( 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] - + assert _canonical_todo(registry, 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. - The shared-authority RFC names dependency and decision-gate revisions as - command preconditions. The current Markdown writer stores the scopes but - has no authoritative publisher for their resolution, so Stage 1 must not - silently invent a gate and change local claim behavior. - """ +def test_unresolved_decision_scope_is_not_a_local_claim_gate(tmp_path: Path, canonical_workspace) -> None: + """A recorded decision scope alone does not create an active User gate.""" - registry, state = _write_fixture(tmp_path) + registry, state = canonical_workspace(tmp_path) todo = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -349,7 +335,7 @@ def test_unresolved_decision_scope_is_not_a_local_claim_gate(tmp_path: Path) -> ) assert result["claimed_by"] == AUTHOR_AGENT - item = _agent_todo(state, str(todo["todo_id"])) + item = _canonical_todo(registry, str(todo["todo_id"])) assert item["required_decision_scopes"] == [ { "schema_version": "decision_scope_v0", @@ -401,9 +387,9 @@ def test_non_owner_cannot_mutate_claimed_todo( def test_completion_policy_unicode_parity_through_public_facade( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture(tmp_path) + registry, state = canonical_workspace(tmp_path) todo = _add_agent_todo(registry, text="Continue with Python whitespace parity.") result = complete_goal_todo( @@ -417,14 +403,14 @@ def test_completion_policy_unicode_parity_through_public_facade( next_continuation_policy="\u0085same_agent_non_delivery\u0085", ) - successor = _agent_todo(state, result["next_todos"][0]["todo_id"]) + successor = _canonical_todo(registry, result["next_todos"][0]["todo_id"]) assert successor["claimed_by"] == AUTHOR_AGENT blank_evidence = _add_agent_todo( registry, text="Reject Python-blank self-merge evidence.", ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) with pytest.raises(ValueError, match="--self-merged requires --evidence"): complete_goal_todo( registry_path=registry, @@ -435,7 +421,7 @@ def test_completion_policy_unicode_parity_through_public_facade( evidence="\u0085", no_followup=True, ) - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before bom_evidence = _add_agent_todo( registry, @@ -454,17 +440,17 @@ def test_completion_policy_unicode_parity_through_public_facade( def test_completion_policy_errors_do_not_preempt_actor_or_lease_fences( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture(tmp_path) + registry, state = canonical_workspace(tmp_path, handoff_mode="hard_lease") claimed_by_other = _add_agent_todo( registry, text="Keep actor authority ahead of policy diagnostics.", claimed_by=REVIEW_AGENT, ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="claimed_by='codex-review'"): + with pytest.raises(LocalCoordinationAuthorityRejection) as owner_error: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -474,7 +460,8 @@ def test_completion_policy_errors_do_not_preempt_actor_or_lease_fences( evidence="\u0085", no_followup=True, ) - assert state.read_text(encoding="utf-8") == before + assert owner_error.value.code == "claim_owner_mismatch" + assert _canonical_snapshot(registry, state) == before leased = _add_agent_todo( registry, @@ -490,8 +477,8 @@ def test_completion_policy_errors_do_not_preempt_actor_or_lease_fences( idempotency_key=lease_key, ttl_seconds=600, ) - before = state.read_text(encoding="utf-8") - with pytest.raises(TaskLeaseError) as stale_lease: + before = _canonical_snapshot(registry, state) + with pytest.raises(LocalCoordinationAuthorityRejection) as stale_lease: complete_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -504,7 +491,7 @@ def test_completion_policy_errors_do_not_preempt_actor_or_lease_fences( no_followup=True, ) assert stale_lease.value.code == "version_mismatch" - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before def test_owner_actor_update_returns_typed_receipt(tmp_path: Path, canonical_workspace) -> None: @@ -637,8 +624,8 @@ def test_canonical_claim_rejects_role_mismatch_atomically(tmp_path: Path, canoni assert _canonical_snapshot(registry, state) == before -def test_advancement_todo_preserves_public_target_key(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) +def test_advancement_todo_preserves_public_target_key(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = add_goal_todo( registry_path=registry, @@ -651,13 +638,13 @@ def test_advancement_todo_preserves_public_target_key(tmp_path: Path) -> None: monitor_metadata={"target_key": "issue-fix:owner/repo:issue_42"}, ) - projected = _agent_todo(state, todo["todo_id"]) + projected = _canonical_todo(registry, todo["todo_id"]) assert projected["action_kind"] == "issue_fix_branch_validation" assert projected["target_key"] == "issue-fix:owner/repo:issue_42" -def test_capability_binding_follows_generated_agent_successor(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) +def test_capability_binding_follows_generated_agent_successor(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -681,7 +668,7 @@ def test_capability_binding_follows_generated_agent_successor(tmp_path: Path) -> next_continuation_policy="same_agent_non_delivery", ) - successor = _agent_todo(state, result["next_todos"][0]["todo_id"]) + successor = _canonical_todo(registry, result["next_todos"][0]["todo_id"]) assert successor["capability_binding_ref"] == ("issue-fix:feasibility-a1b2c3d4") @@ -986,62 +973,45 @@ def test_terminal_upgrade_requires_original_completion_turn_key(tmp_path: Path, assert _canonical_snapshot(registry, state) == completed_state -def test_terminal_upgrade_rejects_untyped_legacy_completion_until_repaired( - tmp_path: Path, +def test_imported_untyped_completion_requires_explicit_repair( + tmp_path: Path, canonical_workspace, ) -> 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"): + # Synthetic historical metadata is seeded before provider creation; editing + # its Markdown display afterward must never change canonical authority. + todo_id = "todo_untypedcompletion" + registry, state = canonical_workspace(tmp_path, multi_agent=False, source_rows=( + "- [x] Historical completion with missing continuation.\n" + f" \n" + )) + before = _canonical_snapshot(registry, state) + with pytest.raises(LocalCoordinationAuthorityRejection, 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, + registry_path=registry, goal_id=GOAL_ID, todo_id=todo_id, + agent_id=AUTHOR_AGENT, completion_turn_key="turn-a", + completion_identity_source="turn_settlement", + evidence="legacy ambiguity must not be inferred", no_followup=True, ) - + assert _canonical_snapshot(registry, state) == before repaired = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo_id, - claimed_by=AUTHOR_AGENT, - completion_turn_key="turn-a", + registry_path=registry, goal_id=GOAL_ID, todo_id=todo_id, + agent_id=AUTHOR_AGENT, completion_turn_key="turn-a", + completion_identity_source="turn_settlement", evidence="repair explicit completion continuation", note="repair explicit completion continuation", ) - assert repaired["completion_continuation"] == "active_goal" - + assert repaired["ok"] is True + assert _canonical_todo(registry, todo_id)["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, + registry_path=registry, goal_id=GOAL_ID, todo_id=todo_id, + agent_id=AUTHOR_AGENT, completion_turn_key="turn-a", + completion_identity_source="turn_settlement", + evidence="repair explicit completion continuation", + note="repair explicit completion continuation", no_followup=True, ) - assert upgraded["completion_continuation"] == "no_followup" - assert upgraded["completion_recovery"] == "same_turn_terminal_closeout" + assert upgraded["ok"] is True + assert _canonical_todo(registry, todo_id)["completion_recovery"] == "same_turn_terminal_closeout" + assert _canonical_todo(registry, todo_id)["completion_continuation"] == "no_followup" def test_unfenced_completion_leaves_no_lease_artifacts(tmp_path: Path, canonical_workspace) -> None: @@ -1064,49 +1034,37 @@ def test_unfenced_completion_leaves_no_lease_artifacts(tmp_path: Path, canonical assert list(lease_dir.glob("todo_*.json")) == [] -def test_divergent_claim_completion_leaves_foreign_lease_untouched( - tmp_path: Path, +def test_foreign_claim_and_completion_preserve_current_execution_lease( + tmp_path: Path, canonical_workspace, ) -> None: - registry, _state = _write_fixture(tmp_path) + # Canonical hard-lease claim cannot create the old split-brain state. + registry, state = canonical_workspace(tmp_path, handoff_mode="hard_lease") 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, + 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, ) - 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 + before = _canonical_snapshot(registry, state) + lease_before = _canonical_lease(registry, tmp_path, todo["todo_id"]) + for command in ("claim", "complete"): + with pytest.raises(LocalCoordinationAuthorityRejection): + if command == "claim": + 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, + task_lease_idempotency_key="author-instance", task_lease_expected_version=1, + ) + else: + complete_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], + agent_id=REVIEW_AGENT, task_lease_idempotency_key="author-instance", + task_lease_expected_version=1, evidence="A foreign actor cannot borrow the holder's proof.", + ) + assert _canonical_snapshot(registry, state) == before + assert _canonical_lease(registry, tmp_path, todo["todo_id"]) == lease_before + assert lease_before["owner"] == AUTHOR_AGENT + assert lease_before["status"] == "active" def test_dry_run_completion_does_not_release_lease(tmp_path: Path, canonical_workspace) -> None: @@ -1143,130 +1101,84 @@ def test_dry_run_completion_does_not_release_lease(tmp_path: Path, canonical_wor assert _canonical_snapshot(registry, state) == before -def test_release_failure_after_commit_keeps_completion_ok( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, +def test_completion_and_lease_release_recover_after_response_loss( + tmp_path: Path, canonical_workspace, monkeypatch: pytest.MonkeyPatch, ) -> None: - registry, _state = _write_fixture(tmp_path, multi_agent=False) + # Canonical completion releases its lease in the same commit; there is no + # independent post-commit release call to fail. Lose the real response instead. + registry, state = canonical_workspace(tmp_path, multi_agent=False, handoff_mode="hard_lease") todo = _add_agent_todo(registry) - lease_key = "release-failure-instance" + lease_key = "response-loss-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, + 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", + request = dict( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], agent_id=AUTHOR_AGENT, + task_lease_idempotency_key=lease_key, task_lease_expected_version=1, + completion_turn_key="response-loss-turn", evidence="Original completion committed once.", 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" + native_effect = terminal_provider.effect_runtime_result + committed = [] + def lose_response(method, payload, **kwargs): + result = native_effect(method, payload, **kwargs) + if method == "coordination.local_authority.todo_terminal" and not committed: + assert result["status"] == "applied" + committed.append(result) + raise OSError("response lost after real canonical commit") + return result + with monkeypatch.context() as patch: + patch.setattr(terminal_provider, "effect_runtime_result", lose_response) + with pytest.raises(OSError, match="response lost"): + complete_goal_todo(**request) + assert len(committed) == 1 + assert _canonical_todo(registry, todo["todo_id"])["status"] == "done" + lease_after_commit = _canonical_lease(registry, tmp_path, todo["todo_id"]) + assert lease_after_commit["status"] == "released" + replay = complete_goal_todo(**request) + assert replay["ok"] is True and replay["idempotent_replay"] is True + assert _canonical_lease(registry, tmp_path, todo["todo_id"]) == lease_after_commit + after_recovery = _canonical_snapshot(registry, state) + assert complete_goal_todo(**request)["idempotent_replay"] is True + assert _canonical_snapshot(registry, state) == after_recovery -def test_reopened_todo_completes_unfenced_after_lease_release( - tmp_path: Path, +def test_completed_todo_rejects_metadata_reopen_and_new_execution( + tmp_path: Path, canonical_workspace, ) -> 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) + registry, state = canonical_workspace(tmp_path, multi_agent=False, handoff_mode="hard_lease") 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", + registry_path=registry, runtime_root=tmp_path / "runtime", goal_id=GOAL_ID, + todo_id=todo["todo_id"], owner=AUTHOR_AGENT, idempotency_key="first-instance", ttl_seconds=600, ) - 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, + complete_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], agent_id=AUTHOR_AGENT, + task_lease_idempotency_key="first-instance", task_lease_expected_version=1, + completion_turn_key="first-turn", evidence="First completion releases its lease.", ) - assert recompleted["ok"] is True - assert recompleted["task_lease_fence"] == { - "schema_version": "task_lease_v0", - "required": False, - "active": False, - } + released = _canonical_lease(registry, tmp_path, todo["todo_id"]) + assert released["status"] == "released" + before = _canonical_snapshot(registry, state) + # A completed ordinary Todo has no metadata-reopen contract. Keep terminal + # history intact rather than silently resurrecting a retired execution. + with pytest.raises(LocalCoordinationAuthorityRejection) as rejected: + update_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], agent_id=AUTHOR_AGENT, + status="open", note="Metadata cannot reopen a completed execution.", + ) + assert rejected.value.code == "unsupported_todo_update_target" + assert _canonical_snapshot(registry, state) == before + assert _canonical_lease(registry, tmp_path, todo["todo_id"]) == released + with pytest.raises((ValueError, LocalCoordinationAuthorityRejection)): + complete_goal_todo( + registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], agent_id=AUTHOR_AGENT, + task_lease_idempotency_key="first-instance", task_lease_expected_version=1, + completion_turn_key="second-turn", evidence="Released proof cannot authorize new execution.", + ) + assert _canonical_snapshot(registry, state) == before + assert _canonical_lease(registry, tmp_path, todo["todo_id"]) == released def test_cli_release_after_auto_release_replays_terminal_tombstone( @@ -1370,8 +1282,8 @@ def test_invalid_completion_keeps_existing_lease_and_state( assert _canonical_snapshot(registry, state) == state_before -def test_capability_binding_cannot_be_rebound_by_duplicate_add(tmp_path: Path) -> None: - registry, state = _write_fixture(tmp_path) +def test_capability_binding_cannot_be_rebound_by_duplicate_add(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1380,9 +1292,9 @@ def test_capability_binding_cannot_be_rebound_by_duplicate_add(tmp_path: Path) - task_class="advancement_task", capability_binding_ref="issue-fix:feasibility-a1b2c3d4", ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="immutable once set"): + with pytest.raises(LocalCoordinationAuthorityUnavailable, match="different capability_binding_ref"): add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1392,13 +1304,13 @@ def test_capability_binding_cannot_be_rebound_by_duplicate_add(tmp_path: Path) - capability_binding_ref="issue-fix:feasibility-e5f6a7b8", ) - assert state.read_text(encoding="utf-8") == before + assert _canonical_snapshot(registry, state) == before def test_duplicate_todo_add_rejects_invalid_task_domain_atomically( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture(tmp_path) + registry, state = canonical_workspace(tmp_path) todo = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1407,7 +1319,7 @@ def test_duplicate_todo_add_rejects_invalid_task_domain_atomically( task_class="advancement_task", task_domain="validation", ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) with pytest.raises(ValueError, match="task_domain"): add_goal_todo( @@ -1419,12 +1331,12 @@ def test_duplicate_todo_add_rejects_invalid_task_domain_atomically( task_domain="../private", ) - assert state.read_text(encoding="utf-8") == before - assert _agent_todo(state, todo["todo_id"])["task_domain"] == "validation" + assert _canonical_snapshot(registry, state) == before + assert _canonical_todo(registry, 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) +def test_todo_update_rejects_invalid_task_domain_atomically(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1434,9 +1346,9 @@ def test_todo_update_rejects_invalid_task_domain_atomically(tmp_path: Path) -> N task_domain="validation", claimed_by=AUTHOR_AGENT, ) - before = state.read_text(encoding="utf-8") + before = _canonical_snapshot(registry, state) - with pytest.raises(ValueError, match="task_domain"): + with pytest.raises(LocalCoordinationAuthorityUnavailable, match="task_domain"): update_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1445,8 +1357,8 @@ def test_todo_update_rejects_invalid_task_domain_atomically(tmp_path: Path) -> N agent_id=AUTHOR_AGENT, ) - assert state.read_text(encoding="utf-8") == before - assert _agent_todo(state, todo["todo_id"])["task_domain"] == "validation" + assert _canonical_snapshot(registry, state) == before + assert _canonical_todo(registry, todo["todo_id"])["task_domain"] == "validation" def test_monitor_schedule_fields_remain_monitor_only(tmp_path: Path) -> None: @@ -1495,8 +1407,8 @@ def test_claim_actor_must_match_requested_owner(tmp_path: Path, canonical_worksp assert _canonical_todo(registry, 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) +def test_monitor_writeback_propagates_multi_agent_actor(tmp_path: Path, canonical_workspace) -> None: + registry, state = canonical_workspace(tmp_path) todo = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1511,6 +1423,7 @@ def test_monitor_writeback_propagates_multi_agent_actor(tmp_path: Path) -> None: }, ) + before = _canonical_snapshot(registry, state) result = write_monitor_poll_todo_state( registry_path=registry, runtime_root=tmp_path / "runtime", @@ -1523,15 +1436,23 @@ def test_monitor_writeback_propagates_multi_agent_actor(tmp_path: Path) -> None: ) assert result is not None - authority = result["todo_update"]["mutation_authority"] - assert authority["mode"] == "registered_peer_actor" - assert authority["actor_agent_id"] == AUTHOR_AGENT + assert _canonical_snapshot(registry, state) == before + written = 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=True, + todo_id=todo["todo_id"], result_hash="unchanged", agent_id=AUTHOR_AGENT, + ) + assert written is not None + current = _canonical_todo(registry, todo["todo_id"]) + assert current["last_actor_agent_id"] == AUTHOR_AGENT + assert current["claimed_by"] == AUTHOR_AGENT + assert current["result_hash"] == "unchanged" def test_monitor_successor_replay_preserves_independent_reviewer_exclusion( - tmp_path: Path, + tmp_path: Path, canonical_workspace, ) -> None: - registry, state = _write_fixture(tmp_path) + registry, state = canonical_workspace(tmp_path) monitor = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, @@ -1583,7 +1504,7 @@ 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"] == [ + assert _canonical_todo(registry, review["todo_id"])["excluded_agents"] == [ AUTHOR_AGENT ] diff --git a/tests/control_plane/test_unpromoted_todo_claim.py b/tests/control_plane/test_unpromoted_todo_claim.py index 9a6960ee4c..4d5c90c770 100644 --- a/tests/control_plane/test_unpromoted_todo_claim.py +++ b/tests/control_plane/test_unpromoted_todo_claim.py @@ -1,4 +1,4 @@ -"""Default-off counterfactual: run unchanged against the pre-claim base too.""" +"""The 2.0 cutoff refuses new execution before an explicit canonical import.""" from __future__ import annotations @@ -7,15 +7,17 @@ import pytest -from loopx.todos import add_goal_todo, list_goal_todos, update_goal_todo +from loopx.todos import list_goal_todos, update_goal_todo +from loopx.control_plane.coordination.legacy_writer_fence import LegacyCoordinationWriterFenced @pytest.mark.parametrize("with_note", [False, True]) -def test_unpromoted_claim_retains_markdown_semantics( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, with_note: bool +def test_unpromoted_claim_requires_import_without_source_writes( + tmp_path: Path, with_note: bool ) -> None: state = tmp_path / "ACTIVE_GOAL_STATE.md" - state.write_text("# Goal\n\n## Agent Todo\n\n", encoding="utf-8") + state.write_text("# Goal\n\n## Agent Todo\n\n- [ ] Historical unclaimed task.\n" + " \n", encoding="utf-8") runtime = tmp_path / "runtime" registry = tmp_path / "registry.json" registry.write_text(json.dumps({ @@ -25,35 +27,19 @@ def test_unpromoted_claim_retains_markdown_semantics( "registered_agents": ["agent-a", "agent-b"], }}], }), encoding="utf-8") - created = add_goal_todo(registry_path=registry, goal_id="goal-a", role="agent", - text="Claim through the unchanged Markdown path") - monkeypatch.setattr( - "loopx.control_plane.coordination.local_authority.effect_runtime_result", - lambda *_args, **_kwargs: pytest.fail("unpromoted call must not enter provider runtime"), - ) - request = dict(registry_path=registry, goal_id="goal-a", todo_id=created["todo_id"], + request = dict(registry_path=registry, goal_id="goal-a", todo_id="todo_import_claim", claimed_by="agent-a", agent_id="agent-a", claim_only=True) if with_note: - request["note"] = "legacy combined claim remains supported" + request["note"] = "A note cannot authorize a new source claim" before = state.read_bytes() with pytest.raises(ValueError, match="requires promoted canonical authority"): update_goal_todo(**request, claim_operation_id="explicit-retry") assert state.read_bytes() == before - preview = update_goal_todo(**request, dry_run=True) - assert preview["ok"] is True - assert preview["changed"] is True - assert state.read_bytes() == before - claimed = update_goal_todo(**request) - assert claimed["ok"] is True - assert claimed["changed"] is True - assert claimed["mutation_authority"]["command"] == "claim" - assert "source_authority" not in claimed - todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] - todo = next(item for item in todos if item["todo_id"] == created["todo_id"]) - assert todo["claimed_by"] == "agent-a" - if with_note: - assert todo["note"] == request["note"] - after = state.read_bytes() - assert update_goal_todo(**request)["changed"] is False - assert state.read_bytes() == after + before_todos = list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] + for dry_run in (True, False): + with pytest.raises(LegacyCoordinationWriterFenced) as rejected: + update_goal_todo(**request, dry_run=dry_run) + assert rejected.value.code == "canonical_authority_required" + assert state.read_bytes() == before + assert list_goal_todos(registry_path=registry, goal_id="goal-a")["todos"] == before_todos assert not (runtime / "authority" / "file-v0").exists() From cb39361857464ad9e0f19bff77144dd1ad2c330d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:29:46 +0800 Subject: [PATCH 51/54] docs: reconcile canonical terminal repair and caller boundaries Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../rfcs/typescript-control-plane-migration-v0.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 7456fdfec8..f2588dba19 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -148,6 +148,18 @@ receipts retain the accepted authority reason; original receipt recovery preserv historical results and rejects a changed reason under the same operation identity. This closes that private decision chain, not the remaining monitor/handoff caller families, installed import journey or sustained SQLite qualification. +The remaining actor-module Monitor, binding and terminal consumers now use real +File/SQLite authority. Imported completions without a typed continuation expose +an integration gap: the completion reducer permits explicit repair, but the +outer terminal no-op path discarded that commit. The terminal transaction now +honors that existing repair decision and retains its original operation receipt; +shared provider tests cover rejection, preview, replay and subsequent closeout. +Canonical completion releases its lease atomically, so response-loss recovery +replaces the obsolete separate-release failure seam. A completed ordinary Todo +cannot be reopened through metadata update, and fresh unmigrated claims refuse +without changing the source. These are explicit 2.0 boundaries, not parity with +the retired Markdown writer's split claim/lease and reopen behavior. Historical +receipt recovery, broader CI and installed/natural-trial acceptance stay separate. The backend configuration catalog now describes the same 2.0 creation boundary. Exact-head review, core-developer trial and release authorization remain open. Retained outbox recovery and source lease release are concrete remaining callers, not grounds for keeping normal From 09e42d68b1d52914877dc73a8ea9cbb6a3d3203e Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:56:56 +0800 Subject: [PATCH 52/54] test: migrate complete handoff families to canonical authority Retain frozen 1.x characterization, cover current File/SQLite permissions and CLI readback, and replace retired two-write and display-authority assumptions with explicit canonical rejection and replay invariants. Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_goal_handoff_mode.py | 2110 +++++------------ .../test_goal_handoff_mode_cli.py | 170 +- 2 files changed, 744 insertions(+), 1536 deletions(-) diff --git a/tests/control_plane/test_goal_handoff_mode.py b/tests/control_plane/test_goal_handoff_mode.py index fd2a148c53..db373f5fd3 100644 --- a/tests/control_plane/test_goal_handoff_mode.py +++ b/tests/control_plane/test_goal_handoff_mode.py @@ -1,31 +1,31 @@ -"""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. +"""Current canonical handoff policy, with immutable 1.x characterization pins. + +Both real local providers must enforce one claim/lease owner. Ownership transfer +is atomic; metadata and duplicate create cannot perform the old two-write handoff. +Frozen historical tests describe retired behavior, never enable current writers. """ from __future__ import annotations import json -from contextlib import ExitStack +import os +import subprocess +import sys 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.coordination.local_authority import ( + LocalCoordinationAuthorityRejection, + LocalCoordinationAuthorityUnavailable, + read_canonical_todos_if_promoted, +) +from loopx.control_plane.coordination.legacy_writer_fence import ( + LegacyCoordinationWriterFenced, +) +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, ) from loopx.control_plane.todos.handoff_mode import ( HANDOFF_MODE_HARD_LEASE, @@ -40,27 +40,38 @@ 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.control_plane.testing.authority_e2e_fixtures import ORIGINAL_1X_REVISION from loopx.todos import ( add_goal_todo, + list_goal_todos, complete_goal_todo, supersede_goal_todo, update_goal_todo, ) +from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_authority, + isolate_sqlite_runtime, +) +from tests.control_plane.historical_shadow_fixture import original_1x_source # noqa: F401 GOAL_ID = "handoff-mode-gate" -AGENT_A = "agent-a" -AGENT_B = "agent-b" -ORCHESTRATOR = "agent-orchestrator" +AGENT_A, AGENT_B, ORCHESTRATOR = "agent-a", "agent-b", "agent-orchestrator" DECISION_SCOPE = "direction:action:approve_handoff" +HISTORICAL_PINS = [ + "test_legacy_pin_claim_over_foreign_active_lease_silently_succeeds", + "test_legacy_pin_foreign_claim_makes_lease_renew_fail_typed", + "test_legacy_pin_fence_self_disarms_and_claimant_completes_keyless", + "test_legacy_pin_lease_holder_cannot_complete_after_foreign_claim", + "test_legacy_lease_verbs_report_handoff_mode", + "test_legacy_pin_todo_add_reassigns_a_leased_todo", + "test_legacy_pin_supersede_ignores_missing_lease_but_fences_effective_lease", +] def _write_workspace( @@ -139,17 +150,10 @@ def _add_todo(registry: Path, *, claimed_by: str | None = None) -> dict[str, Any 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 - ) + registry = state.parent.parent / "registry.global.json" + return list_goal_todos(registry_path=registry, goal_id=GOAL_ID, todo_id=todo_id)[ + "todo" + ] def _add_exact_user_gate_pair( @@ -209,9 +213,36 @@ def _claim(registry: Path, todo_id: str, agent: str, **kwargs: Any) -> dict[str, ) -# --------------------------------------------------------------------------- -# Mode reader -# --------------------------------------------------------------------------- +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_goal_handoff_mode_defaults_to_legacy_when_absent() -> None: @@ -232,1524 +263,647 @@ def test_goal_handoff_mode_rejects_unknown_value_loudly() -> None: 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 +@pytest.mark.parametrize("case", HISTORICAL_PINS) +def test_original_1x_legacy_policy_characterization(tmp_path, original_source, case): + """These vulnerabilities describe 1.x only; current writes must be fenced.""" + original_test = subprocess.check_output( + [ + "git", + "show", + ORIGINAL_1X_REVISION + ":tests/control_plane/test_goal_handoff_mode.py", + ], + cwd=Path(__file__).resolve().parents[2], + text=True, + ) + test_path = tmp_path / "test_original_policy.py" + test_path.write_text(original_test, encoding="utf-8") + result = subprocess.run( + [sys.executable, "-m", "pytest", "-q", str(test_path) + "::" + case], + cwd=original_source, + env={**os.environ, "PYTHONPATH": str(original_source)}, + capture_output=True, + text=True, + timeout=90, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert "1 passed" in result.stdout + + +@pytest.fixture(params=["file", "sqlite"]) +def canonical_workspace(request, tmp_path, monkeypatch): + isolate_sqlite_runtime(tmp_path, monkeypatch) + + def create(root, *, handoff_mode="soft_claim", retained=None, **kwargs): + registry, state = _write_workspace(root, handoff_mode=handoff_mode, **kwargs) + todos, leases = [], [] + if retained: + # Synthetic imported facts, committed once before any current call. + # This fixture is consumer-policy evidence, not an import journey. + todos = [ + { + "schema_version": "todo_item_v0", + "todo_id": "todo_retained", + "role": "agent", + "status": "open", + "done": False, + "text": "Retained work", + "archive_state": "active", + "source_section": "Agent Todo", + "index": 1, + "task_class": "advancement_task", + **retained.get("todo", {}), + } + ] + now = task_lease.now_utc() + start = now - timedelta(hours=2) if retained.get("expired") else now + leases = [ + build_lease( + goal_id=GOAL_ID, + todo_id="todo_retained", + owner=AGENT_A, + idempotency_key="turn-lease-1", + write_scopes=[], + acquire_ttl_seconds=600, + version=1, + lease_epoch=1, + acquired_at=task_lease.isoformat(start), + updated_at=task_lease.isoformat(start), + expires_at=task_lease.isoformat(start + timedelta(seconds=600)), + ) + ] + projection = build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=todos, leases=leases, handoff_mode=handoff_mode + ) + initialize_canonical_authority( + root / "runtime", + GOAL_ID, + projection, + state_path=state, + provider=request.param, + ) + assert _snapshot(registry)["source_authority"] == request.param + "_v0" + return registry, state + return create -# --------------------------------------------------------------------------- -# (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) +def _snapshot(registry): + result = read_canonical_todos_if_promoted( + runtime_root=registry.parent / "runtime", goal_id=GOAL_ID, include_leases=True + ) + assert result["decision_read_from_provider"] is True + assert result["legacy_fallback_used"] is False + return result - 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", +def _lease(registry, todo_id): + result = inspect_task_lease( + registry_path=registry, + runtime_root=registry.parent / "runtime", goal_id=GOAL_ID, - todo_id=todo["todo_id"], + todo_id=todo_id, ) - lease = json.loads(lease_file.read_text(encoding="utf-8")) - assert lease["owner"] == AGENT_A - assert lease["status"] == "active" + assert result["legacy_fallback_used"] is False + return result["lease"] -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.""" +def _terminal(registry, todo_id, operation="complete", **kwargs): + options = { + "registry_path": registry, + "goal_id": GOAL_ID, + "todo_id": todo_id, + "agent_id": AGENT_A, + **kwargs, + } + if operation == "complete": + return complete_goal_todo(**options, no_followup=True) + return supersede_goal_todo(**options, reason="Retire obsolete work") - 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", - ) +def _authority_unchanged(registry, before): + after = _snapshot(registry) + for field in ("provider_revision", "todos", "leases", "handoff_mode"): + assert after[field] == before[field], field - 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 _unchanged(registry, state, before, display): + _authority_unchanged(registry, before) + assert state.read_bytes() == display -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", +@pytest.mark.parametrize("operation", ["claim", "replay"]) +def test_display_mode_cannot_override_canonical_policy( + tmp_path, canonical_workspace, operation +): + registry, state = canonical_workspace(tmp_path) + todo = _add_todo(registry, claimed_by=AGENT_A) + if operation == "replay": + original = _terminal( + registry, todo["todo_id"], completion_turn_key="display-replay" + ) + _set_frontmatter_mode(state, "banana") + if operation == "claim": + assert _claim(registry, todo["todo_id"], AGENT_A)["ok"] + else: + before, display = _snapshot(registry), state.read_bytes() + replay = _terminal( + registry, todo["todo_id"], completion_turn_key="display-replay" ) + assert replay["idempotent_replay"] and not replay["changed"] + assert replay["original_receipt"] == original["original_receipt"] + _unchanged(registry, state, before, display) + assert _snapshot(registry)["handoff_mode"] == "soft_claim" -def test_legacy_lease_verbs_report_handoff_mode(tmp_path: Path) -> None: - registry, _state = _write_workspace(tmp_path) - todo = _add_todo(registry) +@pytest.mark.parametrize("verb", ["acquire", "renew", "transfer"]) +def test_soft_claim_forbids_new_lease_effects_even_with_retained_lineage( + tmp_path, canonical_workspace, verb +): + registry, state = canonical_workspace( + tmp_path, retained={"todo": {"claimed_by": AGENT_A}} + ) + before, display = _snapshot(registry), state.read_bytes() + with pytest.raises(TaskLeaseError) as error: + if verb == "acquire": + _acquire(registry, tmp_path, "todo_retained", owner=AGENT_A) + elif verb == "renew": + renew_task_lease( + registry_path=registry, + runtime_root=tmp_path / "runtime", + goal_id=GOAL_ID, + todo_id="todo_retained", + owner=AGENT_A, + idempotency_key="turn-lease-1", + expected_version=1, + ) + else: + transfer_task_lease( + registry_path=registry, + runtime_root=tmp_path / "runtime", + goal_id=GOAL_ID, + todo_id="todo_retained", + owner=AGENT_A, + idempotency_key="turn-lease-1", + expected_version=1, + new_owner=AGENT_B, + new_idempotency_key="peer", + transfer_claim=True, + ) + assert error.value.code == ( + "claim_transfer_requires_hard_lease" + if verb == "transfer" + else "handoff_mode_forbids_lease" + ) + _unchanged(registry, state, before, display) - 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( +def test_soft_claim_retained_lease_can_be_inspected_and_released( + tmp_path, canonical_workspace +): + registry, _ = canonical_workspace( + tmp_path, retained={"todo": {"claimed_by": AGENT_A}} + ) + assert _lease(registry, "todo_retained")["status"] == "active" + result = release_task_lease( registry_path=registry, runtime_root=tmp_path / "runtime", goal_id=GOAL_ID, - todo_id=todo["todo_id"], + todo_id="todo_retained", + owner=AGENT_A, + idempotency_key="turn-lease-1", + expected_version=1, ) - assert inspected["handoff_mode"] == HANDOFF_MODE_LEGACY - + assert result["released"] + assert _lease(registry, "todo_retained")["status"] == "released" -# --------------------------------------------------------------------------- -# (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) +@pytest.mark.parametrize("operation", ["complete", "supersede"]) +def test_soft_claim_claim_and_terminal_need_no_execution_lease( + tmp_path, canonical_workspace, operation +): + registry, state = canonical_workspace(tmp_path) todo = _add_todo(registry) + assert _claim(registry, todo["todo_id"], AGENT_A)["ok"] + result = _terminal(registry, todo["todo_id"], operation) + assert result["ok"] and result["task_lease_fence"]["lease_fence"] == "not_required" + assert _agent_todo(state, todo["todo_id"])["status"] == "done" + assert _snapshot(registry)["leases"] == [] - 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 +@pytest.mark.parametrize("mode", ["soft_claim", "hard_lease"]) +def test_terminal_replay_preserves_receipt_and_does_not_rewind_policy( + tmp_path, canonical_workspace, mode +): + registry, state = canonical_workspace(tmp_path, handoff_mode=mode) + todo = _add_todo(registry, claimed_by=AGENT_A) + proof = {} + if mode == "hard_lease": + _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) + proof = dict( + task_lease_idempotency_key="turn-lease-1", task_lease_expected_version=1 + ) + first = _terminal( + registry, todo["todo_id"], completion_turn_key="terminal-replay", **proof + ) + target = "hard_lease" if mode == "soft_claim" else "soft_claim" + assert set_goal_handoff_mode(registry_path=registry, goal_id=GOAL_ID, mode=target)[ + "changed" + ] + before = _snapshot(registry) + replay = _terminal( + registry, todo["todo_id"], completion_turn_key="terminal-replay", **proof + ) + assert replay["idempotent_replay"] and not replay["changed"] + assert replay["original_receipt"] == first["original_receipt"] + assert _snapshot(registry)["handoff_mode"] == target + # Replaying may deliver a pending projection at the current revision; + # the business receipt and canonical state must remain unchanged. + _authority_unchanged(registry, before) + assert todo["todo_id"] in state.read_text() + + +@pytest.mark.parametrize("lineage", ["missing", "foreign", "expired"]) +def test_hard_lease_claim_rejects_missing_foreign_or_expired_holder( + tmp_path, canonical_workspace, lineage +): + registry, state = canonical_workspace( + tmp_path, + handoff_mode="hard_lease", + retained={"expired": True} if lineage == "expired" else None, + ) + todo_id = ( + "todo_retained" if lineage == "expired" else _add_todo(registry)["todo_id"] + ) + if lineage == "foreign": + _acquire(registry, tmp_path, todo_id, owner=AGENT_B) + before, display = _snapshot(registry), state.read_bytes() + with pytest.raises(LocalCoordinationAuthorityRejection) as error: + _claim(registry, todo_id, AGENT_A) + assert error.value.code == "handoff_mode_requires_lease" + _unchanged(registry, state, before, display) -def test_soft_claim_rejects_renew_and_transfer_of_legacy_leftover( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path) +@pytest.mark.parametrize( + "owner,claimant", [(AGENT_A, AGENT_A), (" Agent-A ", "AGENT-A")] +) +def test_hard_lease_holder_claim_normalizes_identities( + tmp_path, canonical_workspace, owner, claimant +): + registry, state = canonical_workspace(tmp_path, handoff_mode="hard_lease") todo = _add_todo(registry) - _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) - _set_frontmatter_mode(state, HANDOFF_MODE_SOFT_CLAIM) + _acquire(registry, tmp_path, todo["todo_id"], owner=owner) + assert _claim(registry, todo["todo_id"], claimant)["ok"] + assert _agent_todo(state, todo["todo_id"])["claimed_by"] == AGENT_A + assert _lease(registry, todo["todo_id"])["owner"] == AGENT_A - with pytest.raises(TaskLeaseError) as renew_error: - renew_task_lease( + +@pytest.mark.parametrize( + "ownership", + [{"clear_claim": True}, {"claimed_by": AGENT_A}, {"claimed_by": AGENT_B}], +) +@pytest.mark.parametrize("leased", [False, True]) +def test_metadata_cannot_rewrite_hard_lease_execution_grants( + tmp_path, canonical_workspace, ownership, leased +): + grant = [ + { + "agent_id": ORCHESTRATOR, + "actions": ["reassign", "update"], + "requires_reason": True, + } + ] + registry, state = canonical_workspace( + tmp_path, handoff_mode="hard_lease", lifecycle_authority=grant + ) + todo = _add_todo(registry, claimed_by=AGENT_A) + if leased: + _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) + before, display = _snapshot(registry), state.read_bytes() + # Even explicit orchestration authority cannot split claim and lease owners. + with pytest.raises(LocalCoordinationAuthorityRejection) as error: + update_goal_todo( 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", + agent_id=ORCHESTRATOR, + authority_reason="Hand over work", + **ownership, ) - assert renew_error.value.code == "handoff_mode_forbids_lease" + assert error.value.code == "update_lease_ownership_transition_unsupported" + _unchanged(registry, state, before, display) + - with pytest.raises(TaskLeaseError) as transfer_error: - transfer_task_lease( +def test_metadata_edit_requires_current_execution_proof(tmp_path, canonical_workspace): + registry, state = canonical_workspace(tmp_path, handoff_mode="hard_lease") + todo = _add_todo(registry, claimed_by=AGENT_A) + before, display = _snapshot(registry), state.read_bytes() + with pytest.raises(LocalCoordinationAuthorityRejection): + update_goal_todo( 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", + agent_id=AGENT_A, + note="Validated progress", ) - 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) + _unchanged(registry, state, before, display) _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( + updated = update_goal_todo( registry_path=registry, goal_id=GOAL_ID, todo_id=todo["todo_id"], - agent_id=AGENT_B, - evidence="soft mode keyless completion", + agent_id=AGENT_A, + note="Validated progress", + task_lease_idempotency_key="turn-lease-1", + task_lease_expected_version=1, ) - assert result["ok"] is True - assert result["handoff_mode"] == HANDOFF_MODE_SOFT_CLAIM - assert result["task_lease_fence"]["required"] is False + assert updated["ok"] + assert "Validated progress" in _agent_todo(state, todo["todo_id"])["note"] + + +def test_create_assigns_new_work_but_duplicate_create_cannot_reassign( + tmp_path, canonical_workspace +): + registry, state = canonical_workspace(tmp_path, handoff_mode="hard_lease") + created = _add_todo(registry, claimed_by=AGENT_A) + assert created["added"] + _acquire(registry, tmp_path, created["todo_id"], owner=AGENT_A) + before, display = _snapshot(registry), state.read_bytes() + assert _add_todo(registry, claimed_by=AGENT_A)["already_exists"] + _authority_unchanged(registry, before) + display = state.read_bytes() + with pytest.raises( + LocalCoordinationAuthorityUnavailable, match="different claimed_by" + ): + _add_todo(registry, claimed_by=AGENT_B) + _unchanged(registry, state, before, display) +@pytest.mark.parametrize("operation", ["complete", "supersede"]) @pytest.mark.parametrize( - ("configured_mode", "expected_mode"), + "fault,code", [ - (None, HANDOFF_MODE_LEGACY), - (HANDOFF_MODE_SOFT_CLAIM, HANDOFF_MODE_SOFT_CLAIM), + ("missing", "handoff_mode_requires_lease"), + ("keyless", "lease_fence_required"), + ("wrong_key", "lease_cas_mismatch"), + ("foreign", "lease_cas_mismatch"), + ("diverged", "handoff_mode_lease_claim_divergence"), ], ) -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) +def test_terminal_fence_rejects_invalid_execution_without_mutation( + tmp_path, canonical_workspace, operation, fault, code +): + registry, state = canonical_workspace( + tmp_path, + handoff_mode="hard_lease", + retained={"todo": {"claimed_by": AGENT_B}} if fault == "diverged" else None, + ) + todo_id = ( + "todo_retained" + if fault == "diverged" + else _add_todo(registry, claimed_by=None if fault == "foreign" else AGENT_A)[ + "todo_id" + ] + ) + if fault not in ("missing", "diverged"): + _acquire(registry, tmp_path, todo_id, owner=AGENT_A) + proof = {} + if fault in ("wrong_key", "foreign"): + proof = dict( + task_lease_idempotency_key="wrong" + if fault == "wrong_key" + else "turn-lease-1", + task_lease_expected_version=1, + ) + before, display = _snapshot(registry), state.read_bytes() + actor = AGENT_B if fault in ("diverged", "foreign") else AGENT_A + with pytest.raises(LocalCoordinationAuthorityRejection) as error: + _terminal(registry, todo_id, operation, agent_id=actor, **proof) + assert error.value.code == code + _unchanged(registry, state, before, display) + + +@pytest.mark.parametrize("operation", ["complete", "supersede"]) +@pytest.mark.parametrize("dry_run", [False, True]) +def test_terminal_verified_execution_releases_only_on_commit( + tmp_path, canonical_workspace, operation, dry_run +): + registry, state = canonical_workspace(tmp_path, handoff_mode="hard_lease") 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, + _acquire(registry, tmp_path, todo["todo_id"], owner=AGENT_A) + before, display = _snapshot(registry), state.read_bytes() + result = _terminal( + registry, + todo["todo_id"], + operation, + dry_run=dry_run, + task_lease_idempotency_key="turn-lease-1", + task_lease_expected_version=1, ) - completed_state = state.read_text(encoding="utf-8") - replayed = complete_goal_todo( + assert result["ok"] and result["task_lease_fence"]["lease_fence"] == "required" + if dry_run: + _unchanged(registry, state, before, display) + else: + assert _agent_todo(state, todo["todo_id"])["status"] == "done" + assert _lease(registry, todo["todo_id"])["status"] == "released" + assert not task_lease_path( + runtime_root=tmp_path / "runtime", goal_id=GOAL_ID, todo_id=todo["todo_id"] + ).exists() + + +def test_completion_can_assign_successor_without_inventing_its_lease( + tmp_path, canonical_workspace +): + registry, state = canonical_workspace(tmp_path, 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, - evidence="retry the same local completion turn", - completion_turn_key=turn_key, - no_followup=True, + task_lease_idempotency_key="turn-lease-1", + task_lease_expected_version=1, + next_agent_todo="Follow-up slice", + next_claimed_by=AGENT_B, ) + successor = result["next_todos"][0]["todo_id"] + assert _agent_todo(state, successor)["claimed_by"] == AGENT_B + assert all(lease["todo_id"] != successor for lease in _snapshot(registry)["leases"]) - 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 +@pytest.mark.parametrize("operation", ["complete", "supersede"]) +@pytest.mark.parametrize("authorized", [False, True]) +def test_delegated_terminal_authority_requires_real_grant( + tmp_path, canonical_workspace, operation, authorized +): + grants = ( + [{"agent_id": ORCHESTRATOR, "actions": [operation], "requires_reason": True}] + if authorized + else [] + ) + registry, state = canonical_workspace( + tmp_path, handoff_mode="hard_lease", lifecycle_authority=grants + ) + todo = _add_todo(registry, claimed_by=AGENT_A) + before, display = _snapshot(registry), state.read_bytes() + if not authorized: + with pytest.raises(LocalCoordinationAuthorityRejection): + _terminal( + registry, + todo["todo_id"], + operation, + agent_id=ORCHESTRATOR, + authority_reason="Retire stalled item", + ) + _unchanged(registry, state, before, display) + else: + result = _terminal( + registry, + todo["todo_id"], + operation, + agent_id=ORCHESTRATOR, + authority_reason="Retire stalled item", + ) + assert ( + result["mutation_authority"]["authority_mode"] + == "delegated_orchestration_override" + ) + assert result["task_lease_fence"]["lease_fence"] == "delegated_override" + assert _agent_todo(state, todo["todo_id"])["status"] == "done" -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" +@pytest.mark.parametrize( + "proof", ["auto", "verified", "wrong_key", "foreign", "foreign_keyless"] +) +def test_exact_user_gate_completion_preserves_execution_fence( + tmp_path, canonical_workspace, proof +): + registry, state = canonical_workspace(tmp_path, handoff_mode="hard_lease") + target, gate = _add_exact_user_gate_pair(registry) + options = dict(role="user", decision_outcome="approve") + if proof != "auto": + _acquire( + registry, + tmp_path, + gate["todo_id"], + owner=AGENT_B if proof.startswith("foreign") else AGENT_A, + ) + if proof != "foreign_keyless": + options.update( + task_lease_idempotency_key="wrong" + if proof == "wrong_key" + else "turn-lease-1", + task_lease_expected_version=1, + ) + before, display = _snapshot(registry), state.read_bytes() + if proof in ("wrong_key", "foreign", "foreign_keyless"): + with pytest.raises(LocalCoordinationAuthorityRejection) as error: + _terminal(registry, gate["todo_id"], **options) + assert error.value.code == ( + "lease_fence_required" + if proof == "foreign_keyless" + else "lease_cas_mismatch" + ) + _unchanged(registry, state, before, display) + else: + result = _terminal(registry, gate["todo_id"], **options) + assert ( + result["mutation_authority"]["authority_mode"] + == "exact_user_gate_decision_scope_override" + ) + assert result["task_lease_fence"]["lease_fence"] == ( + "auto_acquire" if proof == "auto" else "required" + ) + assert _lease(registry, gate["todo_id"])["status"] == "released" + assert _agent_todo(state, gate["todo_id"])["status"] == "done" + assert not _agent_todo(state, target["todo_id"]).get("required_decision_scopes") + assert result["decision_scope_resolution"]["state"] == "resolved" - 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( +def test_bound_user_action_completes_without_gate_or_execution_claim( + tmp_path, canonical_workspace +): + registry, state = canonical_workspace(tmp_path, handoff_mode="hard_lease") + todo = add_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, + role="user", + text="Owner follow-up", + task_class="user_action", + bound_agent=AGENT_A, ) + result = _terminal(registry, todo["todo_id"], role="user") + assert result["task_lease_fence"]["lease_fence"] == "not_required" + assert _snapshot(registry)["leases"] == [] + assert _agent_todo(state, todo["todo_id"])["status"] == "done" + - 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 +@pytest.mark.parametrize("mode", ["soft_claim", "hard_lease"]) +@pytest.mark.parametrize("dry_run", [False, True]) +def test_force_bootstrap_cannot_erase_canonical_policy_or_work( + tmp_path, canonical_workspace, mode, dry_run +): + registry, state = canonical_workspace(tmp_path, handoff_mode=mode) + _add_todo(registry, claimed_by=AGENT_A) + before, display = _snapshot(registry), state.read_bytes() + with pytest.raises( + ValueError, match="Canonical authority cannot rebuild existing state" + ): + _force_bootstrap(registry, state, tmp_path, dry_run=dry_run) + _unchanged(registry, state, before, display) + + +@pytest.mark.parametrize("mode", [None, "legacy", "soft_claim", "hard_lease", "banana"]) +def test_force_bootstrap_requires_import_for_any_existing_uncanonical_source( + tmp_path, mode +): + registry, state = _write_workspace(tmp_path, handoff_mode=mode) + before = state.read_bytes() + with pytest.raises(LegacyCoordinationWriterFenced) as error: + _force_bootstrap(registry, state, tmp_path) + assert error.value.code == "canonical_authority_required" + assert state.read_bytes() == before -def test_local_terminal_replay_fails_closed_on_current_invalid_mode( - tmp_path: Path, -) -> None: - registry, state = _write_workspace(tmp_path) +def test_delegated_terminal_replay_retains_original_authority_receipt( + tmp_path, canonical_workspace +): + registry, state = canonical_workspace( + tmp_path, + handoff_mode="hard_lease", + lifecycle_authority=[ + {"agent_id": ORCHESTRATOR, "actions": ["complete"], "requires_reason": True} + ], + ) 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, + intent = dict( + agent_id=ORCHESTRATOR, + authority_reason="Close stalled work", + completion_turn_key="delegated-close", + ) + result = _terminal(registry, todo["todo_id"], **intent) + before, display = _snapshot(registry), state.read_bytes() + replay = _terminal(registry, todo["todo_id"], **intent) + assert replay["idempotent_replay"] and not replay["changed"] + assert replay["original_receipt"] == result["original_receipt"] + assert ( + replay["mutation_authority"]["authority_mode"] + == "delegated_orchestration_override" ) - _set_frontmatter_mode(state, "banana") - invalid_state = state.read_text(encoding="utf-8") + _unchanged(registry, state, before, display) + +def test_invalid_requested_policy_leaves_canonical_authority_unchanged( + tmp_path, canonical_workspace +): + registry, state = canonical_workspace(tmp_path) + _add_todo(registry, claimed_by=AGENT_A) + before, display = _snapshot(registry), state.read_bytes() 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) - + set_goal_handoff_mode(registry_path=registry, goal_id=GOAL_ID, mode="banana") assert error.value.code == "invalid_handoff_mode" - assert state.read_bytes() == before + _unchanged(registry, state, before, display) diff --git a/tests/control_plane/test_goal_handoff_mode_cli.py b/tests/control_plane/test_goal_handoff_mode_cli.py index 28cf4ec2c3..4ba23d253e 100644 --- a/tests/control_plane/test_goal_handoff_mode_cli.py +++ b/tests/control_plane/test_goal_handoff_mode_cli.py @@ -1,11 +1,7 @@ -"""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. +"""Canonical handoff CLI policy and typed diagnostics. + +Current show/set use the selected provider. Unimported sources remain readable +but ordinary writes require import; corrupt display cannot override authority. """ from __future__ import annotations @@ -17,6 +13,9 @@ import pytest from loopx.cli import main as cli_main +from loopx.control_plane.coordination.local_authority import ( + read_canonical_todos_if_promoted, +) from loopx.control_plane.todos.handoff_mode import ( HANDOFF_MODE_HARD_LEASE, HANDOFF_MODE_LEGACY, @@ -81,10 +80,22 @@ def _run_cli( registry: Path, *argv: str, ) -> tuple[int, dict[str, Any]]: - exit_code = cli_main( - ["--registry", str(registry), "--format", "json", *argv] + before = read_canonical_todos_if_promoted( + runtime_root=registry.parent / "runtime", goal_id=GOAL_ID, include_leases=True ) + exit_code = cli_main(["--registry", str(registry), "--format", "json", *argv]) captured = capsys.readouterr() + if before is not None: + assert before["source_authority"] in {"file_v0", "sqlite_v0"} + assert before["legacy_fallback_used"] is False + if exit_code != 0 or argv[:2] == ("handoff-mode", "show"): + after = read_canonical_todos_if_promoted( + runtime_root=registry.parent / "runtime", + goal_id=GOAL_ID, + include_leases=True, + ) + for field in ("provider_revision", "todos", "leases", "handoff_mode"): + assert after[field] == before[field], field return exit_code, json.loads(captured.out) @@ -135,9 +146,19 @@ def test_handoff_mode_show_defaults_to_legacy( def test_unpromoted_legacy_set_is_retired_without_mutation(tmp_path, capsys): registry, state = _write_workspace(tmp_path) 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 + code, payload = _run_cli( + capsys, + registry, + "handoff-mode", + "set", + "--goal-id", + GOAL_ID, + "--mode", + "legacy", + ) + assert code == 1 and payload["error_code"] == "canonical_authority_required", ( + payload + ) assert state.read_bytes() == before @@ -159,11 +180,13 @@ 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_canonical_set_ignores_invalid_display_without_repairing_it( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, state = _write_workspace(tmp_path, handoff_mode="banana") + registry, state = canonical_workspace(tmp_path) + _set_frontmatter_mode(state, "banana") before = state.read_bytes() exit_code, payload = _run_cli( @@ -180,20 +203,18 @@ def test_handoff_mode_set_repairs_invalid_frontmatter_on_quiescent_goal( 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["previous_mode"] == HANDOFF_MODE_SOFT_CLAIM + assert payload["previous_mode_valid"] is True assert payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert state.read_bytes() == before.replace( - b"handoff_mode: banana", b"handoff_mode: hard_lease" - ) + assert state.read_bytes() == before -def test_handoff_mode_set_on_quiescent_goal_updates_frontmatter( +def test_handoff_mode_set_on_quiescent_goal_updates_canonical_policy( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, state = _write_workspace(tmp_path) + registry, state = canonical_workspace(tmp_path) exit_code, payload = _run_cli( capsys, @@ -209,10 +230,10 @@ 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 "handoff_mode: soft_claim" in text.split("---")[1] assert f"goal_id: {GOAL_ID}" in text show_code, show_payload = _run_cli( @@ -220,14 +241,15 @@ def test_handoff_mode_set_on_quiescent_goal_updates_frontmatter( ) assert show_code == 0 assert show_payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE - assert show_payload["source"] == "frontmatter" + assert show_payload["source"] == "canonical_provider" -def test_handoff_mode_set_invalid_previous_mode_still_refuses_claimed_todo( +def test_handoff_mode_set_invalid_display_still_refuses_claimed_todo( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, state = _write_workspace(tmp_path) + registry, state = canonical_workspace(tmp_path) todo = _add_todo(registry, claimed_by=AGENT_A) _set_frontmatter_mode(state, "banana") before = state.read_bytes() @@ -240,25 +262,23 @@ def test_handoff_mode_set_invalid_previous_mode_still_refuses_claimed_todo( "--goal-id", GOAL_ID, "--mode", - HANDOFF_MODE_SOFT_CLAIM, + 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["claimed_todos"]] == [ - todo["todo_id"] - ] + assert payload["previous_mode"] == HANDOFF_MODE_SOFT_CLAIM + assert payload["previous_mode_valid"] is True + assert [item["todo_id"] for item in payload["claimed_todos"]] == [todo["todo_id"]] assert state.read_bytes() == before def test_handoff_mode_set_refused_when_open_claimed_todo_exists( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, state = _write_workspace(tmp_path) + registry, state = canonical_workspace(tmp_path) todo = _add_todo(registry, claimed_by=AGENT_A) before = state.read_text(encoding="utf-8") @@ -270,7 +290,7 @@ def test_handoff_mode_set_refused_when_open_claimed_todo_exists( "--goal-id", GOAL_ID, "--mode", - HANDOFF_MODE_SOFT_CLAIM, + HANDOFF_MODE_HARD_LEASE, ) assert exit_code == 1 @@ -282,11 +302,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( +def test_handoff_mode_set_invalid_display_still_refuses_active_lease( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, state = _write_workspace(tmp_path) + registry, state = canonical_workspace( + tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE + ) todo = _add_todo(registry) acquire_task_lease( registry_path=registry, @@ -308,25 +331,25 @@ def test_handoff_mode_set_invalid_previous_mode_still_refuses_active_lease( "--goal-id", GOAL_ID, "--mode", - HANDOFF_MODE_HARD_LEASE, + 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["active_leases"]] == [ - todo["todo_id"] - ] + assert payload["previous_mode"] == HANDOFF_MODE_HARD_LEASE + assert payload["previous_mode_valid"] is True + 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], + canonical_workspace, ) -> None: - registry, _state = _write_workspace(tmp_path) + registry, _state = canonical_workspace( + tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE + ) todo = _add_todo(registry) acquire_task_lease( registry_path=registry, @@ -346,7 +369,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 @@ -359,8 +382,9 @@ def test_handoff_mode_set_refused_when_time_active_lease_exists( def test_handoff_mode_set_allowed_after_goal_becomes_quiescent( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, _state = _write_workspace(tmp_path) + registry, _state = canonical_workspace(tmp_path) todo = _add_todo(registry, claimed_by=AGENT_A) update_goal_todo( registry_path=registry, @@ -378,19 +402,22 @@ def test_handoff_mode_set_allowed_after_goal_becomes_quiescent( "--goal-id", GOAL_ID, "--mode", - HANDOFF_MODE_SOFT_CLAIM, + HANDOFF_MODE_HARD_LEASE, ) assert exit_code == 0 assert payload["ok"] is True - assert payload["handoff_mode"] == HANDOFF_MODE_SOFT_CLAIM + assert payload["handoff_mode"] == HANDOFF_MODE_HARD_LEASE def test_handoff_mode_set_same_value_is_idempotent( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_SOFT_CLAIM) + registry, _state = canonical_workspace( + tmp_path, handoff_mode=HANDOFF_MODE_SOFT_CLAIM + ) exit_code, payload = _run_cli( capsys, @@ -416,8 +443,11 @@ def test_handoff_mode_set_same_value_is_idempotent( def test_cli_todo_claim_in_hard_mode_maps_typed_error_code( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) + registry, _state = canonical_workspace( + tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE + ) todo = _add_todo(registry) exit_code, payload = _run_cli( @@ -444,8 +474,11 @@ def test_cli_todo_claim_in_hard_mode_maps_typed_error_code( def test_cli_task_lease_acquire_in_soft_mode_maps_typed_error_code( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_SOFT_CLAIM) + registry, _state = canonical_workspace( + tmp_path, handoff_mode=HANDOFF_MODE_SOFT_CLAIM + ) todo = _add_todo(registry) exit_code, payload = _run_cli( @@ -477,8 +510,11 @@ def test_cli_task_lease_acquire_in_soft_mode_maps_typed_error_code( def test_todo_supersede_cli_requires_key_in_hard_lease_and_accepts_it( tmp_path: Path, capsys: pytest.CaptureFixture[str], + canonical_workspace, ) -> None: - registry, _state = _write_workspace(tmp_path, handoff_mode=HANDOFF_MODE_HARD_LEASE) + registry, _state = 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 +564,24 @@ 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 + assert payload["task_lease_fence"]["lease_fence"] == "required" + assert payload["task_lease_fence"]["next_lease"]["status"] == "released" + + +@pytest.fixture(params=["file", "sqlite"]) +def canonical_workspace(request, tmp_path, monkeypatch): + from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_registry_fixture, + isolate_sqlite_runtime, + ) + + isolate_sqlite_runtime(tmp_path, monkeypatch) + + def create(root, *, handoff_mode=HANDOFF_MODE_SOFT_CLAIM): + registry, state = _write_workspace(root, handoff_mode=handoff_mode) + initialize_canonical_registry_fixture( + registry, provider=request.param, handoff_mode=handoff_mode + ) + return registry, state + + return create From e9148facad12bc7452a8ff21ecf50f9e6461f75f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:07:49 +0800 Subject: [PATCH 53/54] test: observe real writer contention before releasing fence oracle Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../shadow_native_writer_boundary.test.ts | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/tests/control_plane_ts/shadow_native_writer_boundary.test.ts b/tests/control_plane_ts/shadow_native_writer_boundary.test.ts index b4a31c6b11..4b46152e53 100644 --- a/tests/control_plane_ts/shadow_native_writer_boundary.test.ts +++ b/tests/control_plane_ts/shadow_native_writer_boundary.test.ts @@ -1,9 +1,10 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import { mkdtemp, rm } from "node:fs/promises"; +import fs, { mkdtemp, rm } from "node:fs/promises"; +import { syncBuiltinESMExports } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import test from "node:test"; +import test, { type TestContext } from "node:test"; import { atomicWriteJson } from "../../loopx/control_plane/effect_runtime_io.ts"; import { withCanonicalWriter } from "../../loopx/control_plane/coordination/local_authority_write.ts"; import { @@ -155,23 +156,70 @@ test("fence engagement refuses malformed durable maintenance before publishing", await assert.rejects(access(legacyCoordinationWriterFencePath(root, "goal-a"))); }); +// Observe a real failed exclusive open, rather than assuming an operation has +// reached its lock after a wall-clock sleep. Slow startup must not let a broken +// lock path pass before the test releases the primary writer. +async function whileWriterContended( + t: TestContext, target: string, invoke: () => Promise>, + checkBlocked: () => Promise, +): Promise> { + let pending: Promise> | undefined; + try { + await withFileMutationLock(target, async () => { + const heldPath = await fs.realpath(`${target}.ts-effect.lock`); + let observed!: () => void; + const contention = new Promise(resolve => { observed = resolve; }); + const originalOpen = fs.open; + const probe = t.mock.method(fs, "open", async (...args: Parameters) => { + try { + return await originalOpen(...args); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST" && + await fs.realpath(String(args[0])) === heldPath) observed(); + throw error; + } + }); + syncBuiltinESMExports(); + try { + pending = invoke(); + const first = await Promise.race([ + contention.then(() => "contended"), + pending.then(() => "completed"), + ]); + assert.equal(first, "contended", "the fence must encounter the active primary writer lock"); + await checkBlocked(); + } finally { + probe.mock.restore(); + syncBuiltinESMExports(); + } + }); + return await pending!; + } finally { + // The real holder is released even after a failed assertion. Drain the + // operation before fixture cleanup, preserving the original test failure. + await pending?.catch(() => undefined); + } +} + for (const kind of ["todo", "state", "lease"] as const) { - test(`fence engagement waits for an existing ${kind} writer before publication`, async (t) => { - const root = await mkdtemp(join(tmpdir(), "loopx-fence-lock-")); - t.after(() => rm(root, {recursive: true, force: true})); - const statePath = join(root, "ACTIVE_GOAL_STATE.md"); - await writeFile(statePath, "---\ngoal_id: goal-a\n---\n"); - const lock = kind === "todo" ? legacyCoordinationTodoLockPath(root, "goal-a") : kind === "state" ? statePath : taskLeaseLockPath({runtime_root: root, goal_id: "goal-a"}); - let pending: Promise> | undefined; - let completed = false; - await withFileMutationLock(lock, async () => { - pending = engageLegacyCoordinationWriterFence(fenceRequest(root)).then((result) => {completed = true; return result;}); - await new Promise((resolve) => setTimeout(resolve, 100)); - assert.equal(completed, false, "the fence must not pass an active primary writer"); - await assert.rejects(access(legacyCoordinationWriterFencePath(root, "goal-a"))); + for (const startupDelay of [0, 200]) { + const suffix = startupDelay ? " after delayed startup" : ""; + test(`fence engagement waits for an existing ${kind} writer before publication${suffix}`, {timeout: 10_000}, async (t) => { + const root = await mkdtemp(join(tmpdir(), "loopx-fence-lock-")); + t.after(() => rm(root, {recursive: true, force: true})); + const statePath = join(root, "ACTIVE_GOAL_STATE.md"); + await writeFile(statePath, "---\ngoal_id: goal-a\n---\n"); + const lock = kind === "todo" ? legacyCoordinationTodoLockPath(root, "goal-a") : kind === "state" ? statePath : taskLeaseLockPath({runtime_root: root, goal_id: "goal-a"}); + const result = await whileWriterContended(t, lock, + async () => { + if (startupDelay) await new Promise(resolve => setTimeout(resolve, startupDelay)); + return engageLegacyCoordinationWriterFence(fenceRequest(root)); + }, + async () => { await assert.rejects(access(legacyCoordinationWriterFencePath(root, "goal-a"))); }); + assert.equal(result.status, "applied"); + assert.equal(JSON.parse(await readFile(legacyCoordinationWriterFencePath(root, "goal-a"), "utf8")).fence_id, "fence-a"); }); - assert.equal((await pending)!.status, "applied"); - }); + } } test("fence engagement requires the actual source state path", async (t) => { From 409cf8ef91d2bb30de8a0ddd23e15f54af0afd91 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:18:13 +0800 Subject: [PATCH 54/54] test: run successor and integrated canaries on canonical providers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...esearch-role-successor-completion-smoke.py | 62 ++++++++++--- .../control-plane-integrated-canary-smoke.py | 91 +++++++++++-------- 2 files changed, 98 insertions(+), 55 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..5e7ce41ddb 100644 --- a/examples/control_plane/auto-research-role-successor-completion-smoke.py +++ b/examples/control_plane/auto-research-role-successor-completion-smoke.py @@ -12,6 +12,7 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 from loopx.control_plane.testing.canary_harness import ( # noqa: E402 run_json_cli, run_json_cli_result, @@ -27,7 +28,7 @@ EXECUTOR_TODO = "todo_auto_research_execute" -def write_fixture(root: Path, *, auto_research: bool) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, auto_research: bool, provider: str) -> tuple[Path, Path, Path]: project = root / "project" runtime = root / "runtime" registry_path = project / ".loopx" / "registry.json" @@ -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, ) + initialize_canonical_registry_fixture(registry_path, provider=provider) return project, runtime, registry_path @@ -110,11 +112,34 @@ def complete_proposer( ) -def assert_actor_attribution_fails_closed() -> None: +def read_todos(registry_path: Path, runtime: Path, *, provider: str) -> dict: + result = run_json_cli( + "todo", "list", "--goal-id", GOAL_ID, "--role", "agent", + registry_path=registry_path, runtime_root=runtime, cwd=REPO_ROOT, + ) + authority = result["authority_read"] + assert authority["source_authority"] == f"{provider}_v0", result + assert authority["decision_read_from_provider"] is True, result + assert authority["legacy_fallback_used"] is False, result + return {"revision": authority["provider_revision"], "todos": result["todos"]} + + +def assert_completed_successor(registry_path: Path, runtime: Path, *, provider: str) -> None: + durable = read_todos(registry_path, runtime, provider=provider) + by_id = {todo["todo_id"]: todo for todo in durable["todos"]} + assert len(by_id) == 2, durable + assert by_id[PROPOSER_TODO]["status"] == "done", durable + assert by_id[PROPOSER_TODO]["successor_todo_ids"] == [EXECUTOR_TODO], durable + assert by_id[EXECUTOR_TODO]["status"] == "open", durable + assert by_id[EXECUTOR_TODO]["claimed_by"] == EXECUTOR, durable + + +def assert_actor_attribution_fails_closed(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-role-successor-actor-") as tmp: - project, runtime, registry_path = write_fixture(Path(tmp), auto_research=True) + project, runtime, registry_path = write_fixture(Path(tmp), auto_research=True, provider=provider) state_path = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" before = state_path.read_text(encoding="utf-8") + canonical_before = read_todos(registry_path, runtime, provider=provider) missing_code, missing = complete_proposer( registry_path, @@ -122,8 +147,9 @@ 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 + assert read_todos(registry_path, runtime, provider=provider) == canonical_before wrong_code, wrong = complete_proposer( registry_path, @@ -131,18 +157,23 @@ 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 + assert read_todos(registry_path, runtime, provider=provider) == canonical_before + + recovered_code, recovered = complete_proposer(registry_path, runtime) + assert recovered_code == 0 and recovered["ok"] is True, recovered + assert_completed_successor(registry_path, runtime, provider=provider) -def assert_auto_research_role_successor_allowed() -> None: +def assert_auto_research_role_successor_allowed(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-auto-research-role-successor-") as tmp: - _project, runtime, registry_path = write_fixture(Path(tmp), auto_research=True) + _project, runtime, registry_path = write_fixture(Path(tmp), auto_research=True, provider=provider) returncode, completed = complete_proposer(registry_path, runtime) assert returncode == 0, completed assert completed["ok"] is True, completed + assert_completed_successor(registry_path, runtime, provider=provider) 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 @@ -162,21 +193,22 @@ def assert_auto_research_role_successor_allowed() -> None: assert successor_lookup["todo"]["claimed_by"] == EXECUTOR, successor_lookup -def assert_generic_peer_route_links_existing_successor() -> None: +def assert_generic_peer_route_links_existing_successor(provider: str) -> None: with tempfile.TemporaryDirectory(prefix="loopx-generic-peer-successor-") as tmp: - _project, runtime, registry_path = write_fixture(Path(tmp), auto_research=False) + _project, runtime, registry_path = write_fixture(Path(tmp), auto_research=False, provider=provider) 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(registry_path, runtime, provider=provider) assert completed["successor_todo_ids"] == [EXECUTOR_TODO], completed def main() -> None: - assert_actor_attribution_fails_closed() - assert_auto_research_role_successor_allowed() - assert_generic_peer_route_links_existing_successor() - print("auto-research-role-successor-completion-smoke ok") + for provider in ("file", "sqlite"): + assert_actor_attribution_fails_closed(provider) + assert_auto_research_role_successor_allowed(provider) + assert_generic_peer_route_links_existing_successor(provider) + print("auto-research-role-successor-completion-smoke ok: File and SQLite") if __name__ == "__main__": diff --git a/examples/control_plane/control-plane-integrated-canary-smoke.py b/examples/control_plane/control-plane-integrated-canary-smoke.py index 63049aa45b..bd10829571 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 @@ -16,6 +16,7 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) +from tests.control_plane.canonical_authority_fixture import initialize_canonical_registry_fixture # noqa: E402 from loopx.control_plane.testing.canary_harness import ( # noqa: E402 read_run_index, run_json_cli, @@ -25,13 +26,13 @@ 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 +74,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 +104,14 @@ def write_monitor_fixture(root: Path) -> tuple[Path, Path]: registered_agents=[AGENT_ID], quota_allowed_slots=10, ) + initialize_canonical_registry_fixture(registry_path, 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 +119,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" "