From d40862f2a377771113447017ebea33867b904b12 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:41:15 +0800 Subject: [PATCH 1/4] refactor(control-plane): retire unused coordination mutation wires Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../coordination/coordination_projection.ts | 157 --------------- .../coordination_state_contract.generated.ts | 4 - .../coordination_state_contract_generated.py | 6 +- .../coordination_state_contract_v0.json | 2 - .../coordination/local_authority_runtime.ts | 118 ------------ .../coordination/todo_compatibility_edit.ts | 142 -------------- .../control_plane/effect_runtime_handlers.ts | 4 - .../generate_coordination_state_contract.py | 2 - .../test_coordination_state_contract.py | 2 +- .../authority_store_conformance.ts | 100 ---------- .../coordination_projection.test.ts | 179 ++--------------- .../local_authority_provider.test.ts | 5 +- .../local_authority_runtime.test.ts | 181 ++++-------------- .../shadow_native_writer_boundary.test.ts | 4 - 14 files changed, 47 insertions(+), 859 deletions(-) delete mode 100644 loopx/control_plane/coordination/todo_compatibility_edit.ts diff --git a/loopx/control_plane/coordination/coordination_projection.ts b/loopx/control_plane/coordination/coordination_projection.ts index 7bed26b71c..92d8dededa 100644 --- a/loopx/control_plane/coordination/coordination_projection.ts +++ b/loopx/control_plane/coordination/coordination_projection.ts @@ -1,10 +1,6 @@ import type { JsonObject } from "../effect_program.ts"; import type { - AuthorityStore, AuthorityStoreCommit, - AuthorityStoreCommitResult, - AuthorityStoreReadFailure, - AuthorityStoreReceiptResult, } from "./authority_store.ts"; import { AuthorityStoreProtocolError, @@ -79,22 +75,6 @@ export interface CoordinationProjectionCommitInput { readonly mutations: readonly CoordinationProjectionMutation[]; } -export interface CoordinationProjectionMutationInput { - readonly goal_id: string; - readonly operation_id: string; - readonly expected_provider_revision: string; - readonly mutations: readonly CoordinationProjectionMutation[]; -} - -export type CoordinationProjectionMutationResult = - | { - readonly status: "applied" | "replayed" | "recovered"; - readonly provider_revision: string; - readonly cursor: string; - } - | Extract - | AuthorityStoreReadFailure; - function sortedIds(values: Iterable): string[] { return [...values].sort(authorityUnicodeCompare); } @@ -411,140 +391,3 @@ export function prepareCoordinationProjectionCommit( }], }; } - -function expectedMutationReceiptIdentity( - input: CoordinationProjectionMutationInput, -): JsonObject { - return canonicalAuthorityObject({ - schema_version: COORDINATION_PROJECTION_MUTATION_RECEIPT_SCHEMA, - operation_id: requireAuthorityStoreId(input.operation_id, "operation id"), - goal_id: requireAuthorityStoreId(input.goal_id, "goal id"), - mutation_sha256: canonicalAuthoritySha256(input.mutations), - }, "coordination mutation receipt identity"); -} - -function receiptProvesMutation( - result: AuthorityStoreReceiptResult, - expected: JsonObject, -): result is Extract { - if (result.status !== "found" || result.receipts.length !== 1) return false; - const receipt = result.receipts[0]!; - return receipt.schema_version === expected.schema_version && - receipt.operation_id === expected.operation_id && - receipt.goal_id === expected.goal_id && - receipt.mutation_sha256 === expected.mutation_sha256; -} - -function receiptIdentityMismatch( - result: AuthorityStoreReceiptResult, - expected: JsonObject, -): CoordinationProjectionMutationResult | null { - if (result.status !== "found" || receiptProvesMutation(result, expected)) return null; - return { - status: "failed", - reason_code: "coordination_operation_identity_mismatch", - reason: "operation id already names a different coordination mutation", - }; -} - -/** - * Execute one provider-first coordination mutation against the exact loaded - * head. The caller supplies no projection, which prevents a legacy snapshot - * from being smuggled back into the canonical write path after promotion. - */ -export async function commitCoordinationProjectionMutation( - store: AuthorityStore, - input: CoordinationProjectionMutationInput, -): Promise { - let expectedReceipt: JsonObject; - try { - expectedReceipt = expectedMutationReceiptIdentity(input); - } catch (error) { - return { - status: "failed", - reason_code: "invalid_coordination_mutation", - reason: error instanceof Error ? error.message : "invalid coordination mutation", - }; - } - - const existing = await store.readReceipt(input.operation_id); - if (existing.status === "found") { - return receiptProvesMutation(existing, expectedReceipt) - ? { - status: "replayed", - provider_revision: existing.provider_revision, - cursor: existing.cursor, - } - : { - status: "failed", - reason_code: "coordination_operation_identity_mismatch", - reason: "operation id already names a different coordination mutation", - }; - } - if (existing.status !== "missing") return existing; - - const head = await store.loadAuthority(); - if (head.status === "missing") { - return { - status: "failed", - reason_code: "coordination_authority_missing", - reason: "canonical coordination authority must be initialized before mutation", - }; - } - if (head.status !== "loaded") return head; - - let commit: AuthorityStoreCommit; - try { - commit = prepareCoordinationProjectionCommit({ - ...input, - projection: head.head, - }); - } catch (error) { - return { - status: "failed", - reason_code: "invalid_coordination_mutation", - reason: error instanceof Error ? error.message : "invalid coordination mutation", - }; - } - const committed = await store.commitAuthority(commit); - if (committed.status === "conflict" || committed.status === "ambiguous") { - const readback = await store.readReceipt(input.operation_id); - if (receiptProvesMutation(readback, expectedReceipt)) { - return { - status: "recovered", - provider_revision: readback.provider_revision, - cursor: readback.cursor, - }; - } - const mismatch = receiptIdentityMismatch(readback, expectedReceipt); - if (mismatch !== null) return mismatch; - return committed; - } - if (committed.status === "failed") { - const readback = await store.readReceipt(input.operation_id); - if (receiptProvesMutation(readback, expectedReceipt)) { - return { - status: "recovered", - provider_revision: readback.provider_revision, - cursor: readback.cursor, - }; - } - const mismatch = receiptIdentityMismatch(readback, expectedReceipt); - if (mismatch !== null) return mismatch; - return committed; - } - - const readback = await store.readReceipt(input.operation_id); - if (!receiptProvesMutation(readback, expectedReceipt)) { - return { - status: "failed", - reason_code: "coordination_commit_readback_mismatch", - reason: "applied coordination mutation lacks its exact durable receipt", - }; - } - return { - status: "applied", - provider_revision: readback.provider_revision, - cursor: readback.cursor, - }; -} diff --git a/loopx/control_plane/coordination/coordination_state_contract.generated.ts b/loopx/control_plane/coordination/coordination_state_contract.generated.ts index dc0f7384ba..c7b84ecdb3 100644 --- a/loopx/control_plane/coordination/coordination_state_contract.generated.ts +++ b/loopx/control_plane/coordination/coordination_state_contract.generated.ts @@ -8,8 +8,6 @@ function deepFreeze(value: T): T { return value; } -export const LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA = "loopx_local_coordination_mutation_request_v0"; -export const LOCAL_COORDINATION_MUTATION_RESULT_SCHEMA = "loopx_local_coordination_mutation_result_v0"; export const LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA = "loopx_local_coordination_todo_read_request_v0"; export const LOCAL_COORDINATION_TODO_READ_RESULT_SCHEMA = "loopx_local_coordination_todo_read_result_v0"; export const LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA = "loopx_local_coordination_todo_list_request_v0"; @@ -219,8 +217,6 @@ export const COORDINATION_STATE_CONTRACT = deepFreeze({ ] }, "local_authority_protocol": { - "mutation_request_schema": LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, - "mutation_result_schema": LOCAL_COORDINATION_MUTATION_RESULT_SCHEMA, "todo_read_request_schema": LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA, "todo_read_result_schema": LOCAL_COORDINATION_TODO_READ_RESULT_SCHEMA, "todo_list_request_schema": LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA, diff --git a/loopx/control_plane/coordination/coordination_state_contract_generated.py b/loopx/control_plane/coordination/coordination_state_contract_generated.py index 4750859529..6a58432c69 100644 --- a/loopx/control_plane/coordination/coordination_state_contract_generated.py +++ b/loopx/control_plane/coordination/coordination_state_contract_generated.py @@ -102,9 +102,7 @@ def _freeze(value: Any) -> Any: 'archive_state']}, 'todo_projection_metadata': {'fields': ['source_section', 'index'], 'required_fields': ['source_section']}, - 'local_authority_protocol': {'mutation_request_schema': 'loopx_local_coordination_mutation_request_v0', - 'mutation_result_schema': 'loopx_local_coordination_mutation_result_v0', - 'todo_read_request_schema': 'loopx_local_coordination_todo_read_request_v0', + 'local_authority_protocol': {'todo_read_request_schema': 'loopx_local_coordination_todo_read_request_v0', 'todo_read_result_schema': 'loopx_local_coordination_todo_read_result_v0', 'todo_list_request_schema': 'loopx_local_coordination_todo_list_request_v0', 'todo_list_result_schema': 'loopx_local_coordination_todo_list_result_v0', @@ -194,8 +192,6 @@ def _freeze(value: Any) -> Any: 'compatibility': {'unknown_field_policy': 'reject', 'field_removal_policy': 'maintainer_approval_required', 'markdown_role': 'human_workbench_and_compatibility_projection'}}) -LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA: Final[str] = 'loopx_local_coordination_mutation_request_v0' -LOCAL_COORDINATION_MUTATION_RESULT_SCHEMA: Final[str] = 'loopx_local_coordination_mutation_result_v0' LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA: Final[str] = 'loopx_local_coordination_todo_read_request_v0' LOCAL_COORDINATION_TODO_READ_RESULT_SCHEMA: Final[str] = 'loopx_local_coordination_todo_read_result_v0' LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA: Final[str] = 'loopx_local_coordination_todo_list_request_v0' diff --git a/loopx/control_plane/coordination/coordination_state_contract_v0.json b/loopx/control_plane/coordination/coordination_state_contract_v0.json index 8243c9de54..841fc254bd 100644 --- a/loopx/control_plane/coordination/coordination_state_contract_v0.json +++ b/loopx/control_plane/coordination/coordination_state_contract_v0.json @@ -96,8 +96,6 @@ "required_fields": ["source_section"] }, "local_authority_protocol": { - "mutation_request_schema": "loopx_local_coordination_mutation_request_v0", - "mutation_result_schema": "loopx_local_coordination_mutation_result_v0", "todo_read_request_schema": "loopx_local_coordination_todo_read_request_v0", "todo_read_result_schema": "loopx_local_coordination_todo_read_result_v0", "todo_list_request_schema": "loopx_local_coordination_todo_list_request_v0", diff --git a/loopx/control_plane/coordination/local_authority_runtime.ts b/loopx/control_plane/coordination/local_authority_runtime.ts index 9ade2350c2..b7bb6e10e3 100644 --- a/loopx/control_plane/coordination/local_authority_runtime.ts +++ b/loopx/control_plane/coordination/local_authority_runtime.ts @@ -15,8 +15,6 @@ import {executeCoordinationMonitorPoll, COORDINATION_MONITOR_POLL_REQUEST_SCHEMA COORDINATION_MONITOR_POLL_RESULT_SCHEMA} from "./todo_monitor_poll.ts"; import { requireJsonObject } from "../runtime_decode.ts"; import { - LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, - LOCAL_COORDINATION_MUTATION_RESULT_SCHEMA, LOCAL_COORDINATION_PROMOTION_RECEIPT_SCHEMA, LOCAL_COORDINATION_PROMOTION_REQUEST_SCHEMA, LOCAL_COORDINATION_PROMOTION_RESULT_SCHEMA, @@ -26,11 +24,9 @@ import { LOCAL_COORDINATION_TODO_READ_RESULT_SCHEMA, } from "./coordination_state_contract.generated.ts"; import { - commitCoordinationProjectionMutation, indexCoordinationProjection, indexCoordinationProjectionTodos, validateCoordinationTodoReadModel, - type CoordinationProjectionMutation, } from "./coordination_projection.ts"; import { authorityStoreSourceAuthority, type AuthorityStore, type AuthorityStoreReceiptResult } from "./authority_store.ts"; import { @@ -79,7 +75,6 @@ import { executeLocalArchiveAttempt, LOCAL_TODO_ARCHIVE_ACK_RESULT_SCHEMA, } from "./local_archive_attempt.ts"; -import { editCoordinationTodo, TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA } from "./todo_compatibility_edit.ts"; import { normalizeIdempotencyKey, normalizeTtl, @@ -100,8 +95,6 @@ export const LOCAL_COORDINATION_TODO_ARCHIVE_REQUEST_SCHEMA = export const LOCAL_COORDINATION_TODO_ARCHIVE_ACK_REQUEST_SCHEMA = "loopx_local_coordination_todo_archive_ack_request_v0"; export { - LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, - LOCAL_COORDINATION_MUTATION_RESULT_SCHEMA, LOCAL_COORDINATION_PROMOTION_RECEIPT_SCHEMA, LOCAL_COORDINATION_PROMOTION_REQUEST_SCHEMA, LOCAL_COORDINATION_PROMOTION_RESULT_SCHEMA, @@ -590,91 +583,6 @@ export async function promoteLocalCoordinationAuthority( } } -function decodeMutations(value: unknown): CoordinationProjectionMutation[] { - if (!Array.isArray(value) || value.length === 0) { - throw new Error("mutations must be a non-empty array"); - } - return value.map((candidate, index) => { - const mutation = canonicalAuthorityObject(candidate, `mutations[${index}]`); - switch (mutation.kind) { - case "todo_upsert": - return { - kind: "todo_upsert", - todo: canonicalAuthorityObject(mutation.todo, `mutations[${index}].todo`), - ...(mutation.clear_fields === undefined ? {} : { - clear_fields: requiredUniqueStrings( - mutation.clear_fields, `mutations[${index}].clear_fields`, - ), - }), - }; - case "todo_remove": - return { - kind: "todo_remove", - todo_id: requireAuthorityStoreId(mutation.todo_id, `mutations[${index}].todo_id`), - }; - case "lease_upsert": - return { - kind: "lease_upsert", - lease: canonicalAuthorityObject(mutation.lease, `mutations[${index}].lease`), - }; - case "lease_remove": - return { - kind: "lease_remove", - todo_id: requireAuthorityStoreId(mutation.todo_id, `mutations[${index}].todo_id`), - }; - default: - throw new Error(`mutations[${index}].kind is unsupported`); - } - }); -} - -/** Provider-first mutation entry point. It never reads a legacy projection. */ -export async function mutateLocalCoordinationAuthority( - value: unknown, - dependencies: LocalAuthorityRuntimeDependencies = {}, -): Promise { - let sourceAuthority = "file_v0"; - try { - const input = requireJsonObject(value, "local coordination mutation request"); - if (input.schema_version !== LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA) { - throw new Error("local coordination mutation request schema mismatch"); - } - const root = runtimeRoot(input.runtime_root); - const goalId = requireAuthorityStoreId(input.goal_id, "goal id"); - return await withCanonicalWriter(root, goalId, false, async () => { - const store = await openRuntimeStore(root, goalId, dependencies); - sourceAuthority = sourceAuthorityFor(store); - const result = await commitCoordinationProjectionMutation(store, { - goal_id: goalId, - operation_id: requireAuthorityStoreId(input.operation_id, "operation id"), - expected_provider_revision: requireAuthorityStoreId( - input.expected_provider_revision, - "expected provider revision", - ), - mutations: decodeMutations(input.mutations), - }); - return { - schema_version: LOCAL_COORDINATION_MUTATION_RESULT_SCHEMA, - ...result, - source_authority: sourceAuthority, - decision_read_from_provider: true, - legacy_fallback_used: false, - }; - }); - } catch (error) { - return { - schema_version: LOCAL_COORDINATION_MUTATION_RESULT_SCHEMA, - status: "failed", - reason_code: error instanceof ShadowManagementError ? error.reason_code : "invalid_local_coordination_mutation_request", - reason: error instanceof Error ? error.message : "invalid mutation request", - source_authority: sourceAuthority, - decision_read_from_provider: true, - legacy_fallback_used: false, - ...localAuthorityOpenFailure(error), - }; - } -} - /** Local provider adapter for the provider-neutral Todo claim transaction. */ export async function claimLocalCoordinationTodo( value: unknown, @@ -1093,32 +1001,6 @@ export async function acknowledgeLocalCoordinationTodoArchive( } } -/** Embedded provider adapter; no Markdown input or projection write is accepted. */ -export async function editLocalCoordinationTodo( - value: unknown, - dependencies: LocalAuthorityRuntimeDependencies = {}, -): Promise { - let sourceAuthority = "file_v0"; - try { - const input = requireJsonObject(value, "local compatibility edit"); - const {runtime_root, ...request} = input; - const root = runtimeRoot(runtime_root); - const goalId = requireAuthorityStoreId(input.goal_id, "goal id"); - return await withCanonicalWriter(root, goalId, input.dry_run === true, async () => { - const store = await openRuntimeStore(root, goalId, dependencies); - sourceAuthority = sourceAuthorityFor(store); - return {...await editCoordinationTodo(store, request), - source_authority: sourceAuthority, decision_read_from_provider: true, legacy_fallback_used: false}; - }); - } catch (error) { - return {schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, status: "failed", - reason_code: error instanceof ShadowManagementError ? error.reason_code : "invalid_local_compatibility_edit", changed: false, - reason: error instanceof Error ? error.message : "invalid local compatibility edit", - ...localAuthorityOpenFailure(error), - }; - } -} - /** Provider-first exact Todo read. Missing/unavailable state never falls back. */ export async function readLocalCoordinationTodo( value: unknown, diff --git a/loopx/control_plane/coordination/todo_compatibility_edit.ts b/loopx/control_plane/coordination/todo_compatibility_edit.ts deleted file mode 100644 index 94cde3e4c2..0000000000 --- a/loopx/control_plane/coordination/todo_compatibility_edit.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { JsonObject } from "../effect_program.ts"; -import type { AuthorityStore } from "./authority_store.ts"; -import { canonicalAuthorityBytes, canonicalAuthorityObject, canonicalAuthoritySha256, requireAuthorityStoreId } from "./authority_store_codec.ts"; -import { prepareCoordinationProjectionCommit, indexCoordinationProjection, validateCoordinationTodoReadModel } from "./coordination_projection.ts"; -import { projectionDelivery } from "../todos/projection_delivery.ts"; - -export const TODO_COMPATIBILITY_EDIT_SCHEMA = "loopx_todo_compatibility_edit_request_v0"; -export const TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA = "loopx_todo_compatibility_edit_result_v0"; - -/** - * A compatibility editor proposes only text/note changes against an exact - * provider revision. It cannot submit a snapshot, change ownership/lifecycle, - * clear unrepresented fields, or make Markdown the commit authority. - * - * This trusted embedded entrypoint does not grant remote service authority. - * Conflict requires rereading and rerunning the editor, never snapshot rebasing. - */ -export async function editCoordinationTodo( - store: AuthorityStore, - value: unknown, -): Promise { - const failure = (reason_code: string, reason: string): JsonObject => ({ - schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, - status: "failed", changed: false, reason_code, reason, - }); - try { - const input = canonicalAuthorityObject(value, "Todo compatibility edit"); - if (input.schema_version !== TODO_COMPATIBILITY_EDIT_SCHEMA) { - throw new Error("Todo compatibility edit schema mismatch"); - } - const allowed = new Set(["schema_version", "goal_id", "todo_id", "operation_id", - "expected_provider_revision", "actor_agent_id", "registered_agents", "patch", "dry_run", "observed_at"]); - if (Object.keys(input).some((key) => !allowed.has(key))) { - throw new Error("Todo compatibility edit contains unsupported fields"); - } - const goalId = requireAuthorityStoreId(input.goal_id, "goal id"); - const todoId = requireAuthorityStoreId(input.todo_id, "todo id"); - const operationId = requireAuthorityStoreId(input.operation_id, "operation id"); - const revision = requireAuthorityStoreId(input.expected_provider_revision, "expected provider revision"); - const actor = requireAuthorityStoreId(input.actor_agent_id, "actor agent id"); - if (typeof input.observed_at !== "string" || - !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test(input.observed_at) || - Number.isNaN(Date.parse(input.observed_at))) { - throw new Error("observed_at must be an ISO timestamp"); - } - const updatedAt = new Date(input.observed_at).toISOString(); - if (!Array.isArray(input.registered_agents) || - input.registered_agents.some((agent) => typeof agent !== "string") || - new Set(input.registered_agents).size !== input.registered_agents.length) { - throw new Error("registered_agents must be a unique string array"); - } - if (typeof input.dry_run !== "boolean") throw new Error("dry_run must be a boolean"); - const patch = canonicalAuthorityObject(input.patch, "compatibility patch"); - if (Object.keys(patch).length === 0 || Object.entries(patch).some(([key, item]) => - !["text", "note"].includes(key) || typeof item !== "string" || !item.trim() - )) { - throw new Error("compatibility patch accepts only non-empty text and note strings"); - } - const requestSha = canonicalAuthoritySha256({ - goal_id: goalId, todo_id: todoId, actor_agent_id: actor, - expected_provider_revision: revision, patch, - }); - const readReceipt = async (status: string): Promise => { - const receipt = await store.readReceipt(operationId); - if (receipt.status === "missing") return null; - if (receipt.status !== "found") return {schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, ...receipt, changed: false}; - const original = receipt.receipts[0]; - if (receipt.receipts.length !== 1 || - original?.schema_version !== "loopx_todo_compatibility_edit_receipt_v0" || - original.operation_id !== operationId || original.request_sha256 !== requestSha || - typeof original.changed !== "boolean") { - return failure("coordination_operation_identity_mismatch", "operation id names a different compatibility edit"); - } - return { - schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, - status: status === "applied" && !original.changed ? "no_change" : status, - changed: status !== "replayed" && original.changed, - provider_revision: receipt.provider_revision, cursor: receipt.cursor, - projection_delivery: projectionDelivery(original.changed), - projection_source: "committed_authority_journal", - }; - }; - // Preview evaluates current eligibility and never consumes/replays identity. - if (!input.dry_run) { - const replay = await readReceipt("replayed"); - if (replay !== null) return replay; - } - if (!input.registered_agents.includes(actor)) { - return failure("actor_not_registered", "compatibility edit requires a registered actor"); - } - const head = await store.loadAuthority(); - if (head.status !== "loaded") return {schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, ...head, changed: false}; - validateCoordinationTodoReadModel(head.head, goalId); - const index = indexCoordinationProjection(head.head, goalId); - if (head.provider_revision !== revision) return { - schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, - status: "conflict", changed: false, conflict_kind: "provider_revision_mismatch", - current_provider_revision: head.provider_revision, current_cursor: head.cursor, - }; - const todo = index.todos.get(todoId); - if (todo === undefined) return failure("todo_not_found", "canonical Todo is missing"); - if (todo.role !== "agent" || todo.status !== "open" || todo.archive_state !== "active") { - return failure("unsupported_compatibility_edit_target", "compatibility editing requires an active open agent Todo"); - } - if (todo.claimed_by !== actor || - (Array.isArray(todo.excluded_agents) && todo.excluded_agents.includes(actor)) || - (typeof todo.removed_continuation_policy === "string" && todo.removed_continuation_policy)) { - return failure("compatibility_edit_owner_mismatch", "compatibility editing requires the current non-excluded claim owner"); - } - // Lease-bearing edits need a separate execution-instance proof; do not - // infer that proof merely from actor identity or bypass the existing fence. - if (![undefined, "legacy", "soft_claim"].includes(head.head.handoff_mode as string | undefined) || index.leases.has(todoId)) { - return failure("compatibility_edit_lease_unsupported", "lease-bearing compatibility edits are not yet supported"); - } - const next = {...todo, ...patch}; - const changed = !canonicalAuthorityBytes(next).equals(canonicalAuthorityBytes(todo)); - if (input.dry_run) return { - schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, status: changed ? "planned" : "no_change", - changed, provider_revision: revision, cursor: head.cursor, - }; - if (changed) next.updated_at = updatedAt; - const commit = changed ? prepareCoordinationProjectionCommit({ - goal_id: goalId, operation_id: operationId, - expected_provider_revision: revision, - projection: head.head, - mutations: [{kind: "todo_upsert", todo: next}], - }) : {operation_id: operationId, expected_provider_revision: revision, - next_projection: head.head, events: [], receipts: []}; - commit.receipts = [{schema_version: "loopx_todo_compatibility_edit_receipt_v0", - operation_id: operationId, request_sha256: requestSha, changed}]; - const result = await store.commitAuthority(commit); - const readback = await readReceipt(result.status === "applied" ? "applied" : "recovered"); - if (readback !== null) return readback; - if (result.status === "applied") return failure("compatibility_commit_readback_mismatch", "applied edit lacks its durable receipt"); - return { - schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, ...result, - changed: false, - }; - } catch (error) { - return failure("invalid_todo_compatibility_edit", error instanceof Error ? error.message : "invalid compatibility edit"); - } -} diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index f1a2760aec..34c277640e 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -146,10 +146,8 @@ import { claimLocalCoordinationTodo, continueLocalTodo, createLocalCoordinationTodo, - editLocalCoordinationTodo, updateLocalCoordinationTodo, pollLocalCoordinationMonitor, - mutateLocalCoordinationAuthority, listLocalCoordinationTodos, promoteLocalCoordinationAuthority, readLocalCoordinationTodo, @@ -516,8 +514,6 @@ export function createEffectRuntimeHandlers( ["coordination.local_authority.todo_terminal", terminalLifecycleLocalCoordinationTodo], ["coordination.local_authority.todo_archive", archiveLocalCoordinationTodos], ["coordination.local_authority.todo_archive_ack", acknowledgeLocalCoordinationTodoArchive], - ["coordination.local_authority.todo_compatibility_edit", editLocalCoordinationTodo], - ["coordination.local_authority.mutate", mutateLocalCoordinationAuthority], ["coordination.local_authority.todo_read", readLocalCoordinationTodo], ["coordination.ownership_observation", projectOwnershipObservation], ["coordination.local_authority.ownership_observation", observeLocalCoordinationOwnership], diff --git a/scripts/generate_coordination_state_contract.py b/scripts/generate_coordination_state_contract.py index 17347c960a..b833ea3239 100644 --- a/scripts/generate_coordination_state_contract.py +++ b/scripts/generate_coordination_state_contract.py @@ -34,8 +34,6 @@ "markdown_role": "human_workbench_and_compatibility_projection", } LOCAL_AUTHORITY_PROTOCOL_KEYS = ( - "mutation_request_schema", - "mutation_result_schema", "todo_read_request_schema", "todo_read_result_schema", "todo_list_request_schema", diff --git a/tests/control_plane/test_coordination_state_contract.py b/tests/control_plane/test_coordination_state_contract.py index 0d58e3e2f0..5e0fda9734 100644 --- a/tests/control_plane/test_coordination_state_contract.py +++ b/tests/control_plane/test_coordination_state_contract.py @@ -83,7 +83,7 @@ def test_generated_coordination_bindings_are_current() -> None: @pytest.mark.parametrize( ("source_family", "source_key", "message"), [ - ("local_authority_protocol", "mutation_request_schema", "across families"), + ("local_authority_protocol", "todo_read_request_schema", "across families"), ("local_authority_protocol", "promotion_receipt_schema", "across families"), ("runtime_shadow_protocol", "inspect_request_schema", "must be unique"), ("local_authority_shadow_protocol", "outbox_entry_schema", "across families"), diff --git a/tests/control_plane_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts index cae48387d1..1f0f106bf9 100644 --- a/tests/control_plane_ts/authority_store_conformance.ts +++ b/tests/control_plane_ts/authority_store_conformance.ts @@ -48,7 +48,6 @@ import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; import { executeCoordinationTodoTerminalLifecycle, } from "../../loopx/control_plane/coordination/todo_terminal_lifecycle.ts"; -import { editCoordinationTodo, TODO_COMPATIBILITY_EDIT_SCHEMA } from "../../loopx/control_plane/coordination/todo_compatibility_edit.ts"; import { PRODUCTION_SCALE_VALIDATION_DECLARATION, productionScaleCoordinationFixture, @@ -1113,105 +1112,6 @@ export function registerAuthorityStoreConformance( {...request, operation_id: "bad-projection", todo: {...todo, todo_id: "todo-projection", source_section: "Agent Todo"}}, ]) assert.equal((await executeCoordinationTodoCreate(store, invalid)).status, "failed"); }); - test(`${providerName} conformance: compatibility edit cannot overwrite a concurrent claim (${native ? "native" : "v0"})`, async (t) => { - const {store, contender} = await factory(t); - const goalId = "goal-claim"; - const projection = todoClaimProjection(goalId, native); - const initialized = await store.commitAuthority({ - expected_provider_revision: null, operation_id: "init-compatibility", - events: [], receipts: [], next_projection: projection, - }); - assert.equal(initialized.status, "applied"); - if (initialized.status !== "applied") return; - const request = { - schema_version: TODO_COMPATIBILITY_EDIT_SCHEMA, goal_id: goalId, - todo_id: "todo-claim", operation_id: "edit-compatibility", - actor_agent_id: "agent-a", registered_agents: ["agent-a", "agent-b"], - expected_provider_revision: initialized.provider_revision, - patch: {text: "Edited through a compatibility buffer"}, dry_run: false, - observed_at: "2026-09-05T05:00:00Z", - }; - assert.equal((await executeCoordinationTodoClaim(contender, { - goal_id: goalId, todo_id: "todo-claim", claimed_by: "agent-a", - actor_agent_id: "agent-a", expected_role: "agent", registered_agents: ["agent-a", "agent-b"], - operation_id: "claim-before-edit", dry_run: false, now: new Date("2026-09-05T04:30:00Z"), - })).status, "applied"); - const current = await store.loadAuthority(); - assert.equal(current.status, "loaded"); - if (current.status !== "loaded") return; - assert.equal((await editCoordinationTodo(store, request)).status, "conflict"); - assert.deepEqual(await store.loadAuthority(), current); - request.expected_provider_revision = current.provider_revision; - const preview = await editCoordinationTodo(store, {...request, dry_run: true}); - assert.equal(preview.status, "planned"); - assert.deepEqual(await store.loadAuthority(), current); - assert.equal((await store.readReceipt(request.operation_id)).status, "missing"); - for (const extra of [{claimed_by: "agent-b"}, {archive_state: "archive"}, {source_section: "fake"}]) { - assert.equal((await editCoordinationTodo(store, {...request, patch: extra})).status, "failed"); - } - assert.equal((await editCoordinationTodo(store, {...request, actor_agent_id: "agent-b"})).status, "failed"); - assert.deepEqual(await store.loadAuthority(), current); - const applied = await editCoordinationTodo(store, request); - assert.equal(applied.status, "applied", JSON.stringify(applied)); - const after = await store.loadAuthority(); - assert.equal(after.status, "loaded"); - if (after.status !== "loaded") return; - const old = (current.head.todos as Record[])[0]!; - assert.deepEqual(after.head.todos, [{...old, text: request.patch.text, updated_at: "2026-09-05T05:00:00.000Z"}]); - assert.deepEqual(after.head.leases, current.head.leases); - assert.equal((await editCoordinationTodo(store, {...request, registered_agents: []})).status, "replayed"); - assert.deepEqual(await store.loadAuthority(), after); - assert.equal((await editCoordinationTodo(store, {...request, patch: {note: "different intent"}})).status, "failed"); - const noop = {...request, operation_id: "edit-noop", expected_provider_revision: after.provider_revision}; - assert.equal((await editCoordinationTodo(store, noop)).status, "no_change"); - const afterNoop = await store.loadAuthority(); - assert.equal(afterNoop.status, "loaded"); - if (afterNoop.status !== "loaded") return; - assert.deepEqual(afterNoop.head, after.head); - assert.equal((await editCoordinationTodo(store, noop)).status, "replayed"); - assert.deepEqual(await store.loadAuthority(), afterNoop); - // Losing the response after commit is recovered by the exact receipt. - const ambiguousStore: AuthorityStore = { - storeIdentity: () => store.storeIdentity(), loadAuthority: () => store.loadAuthority(), - readReceipt: (id) => store.readReceipt(id), scanCommitted: (cursor, limit) => store.scanCommitted(cursor, limit), - commitAuthority: async (commit) => { - assert.equal((await store.commitAuthority(commit)).status, "applied"); - return {status: "ambiguous", reason_code: "lost_response", reason: "synthetic lost response"}; - }, - }; - const recover = {...request, operation_id: "edit-recover", - expected_provider_revision: afterNoop.provider_revision, patch: {note: "Recovered edit"}}; - assert.equal((await editCoordinationTodo(ambiguousStore, recover)).status, "recovered"); - assert.equal((await editCoordinationTodo(store, recover)).status, "replayed"); - const recoveredHead = await store.loadAuthority(); - assert.equal(recoveredHead.status, "loaded"); - if (recoveredHead.status !== "loaded") return; - // A competing receipt-only commit after read still invalidates the CAS. - const racingStore: AuthorityStore = {...ambiguousStore, - commitAuthority: async (commit) => { - assert.equal((await contender.commitAuthority({ - ...commit, operation_id: "concurrent-writer", next_projection: recoveredHead.head, - events: [], receipts: [], - })).status, "applied"); - return store.commitAuthority(commit); - }, - }; - assert.equal((await editCoordinationTodo(racingStore, {...recover, - operation_id: "edit-race", expected_provider_revision: recoveredHead.provider_revision, - patch: {note: "Must not commit"}, - })).status, "conflict"); - const afterRace = await store.loadAuthority(); - assert.equal(afterRace.status, "loaded"); - if (afterRace.status !== "loaded") return; - assert.deepEqual(afterRace.head, recoveredHead.head); - assert.equal((await store.readReceipt("edit-race")).status, "missing"); - for (const invalid of [{dry_run: "false"}, {patch: {}}, {patch: {text: ""}}, - {registered_agents: ["agent-a", "agent-a"]}, {observed_at: "yesterday"}, - {projection: recoveredHead.head}]) { - assert.equal((await editCoordinationTodo(store, {...request, ...invalid})).status, "failed"); - } - assert.deepEqual(await store.loadAuthority(), afterRace); - }); test(`${providerName} conformance: Todo claim atomically acquires canonical ownership (${native ? "native" : "v0"})`, async (t) => { const {store} = await factory(t); const goalId = "goal-atomic-ownership"; diff --git a/tests/control_plane_ts/coordination_projection.test.ts b/tests/control_plane_ts/coordination_projection.test.ts index 5605b6a267..b472df1fd2 100644 --- a/tests/control_plane_ts/coordination_projection.test.ts +++ b/tests/control_plane_ts/coordination_projection.test.ts @@ -4,14 +4,9 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import type { - AuthorityStoreCommit, - AuthorityStoreCommitResult, -} from "../../loopx/control_plane/coordination/authority_store.ts"; import { canonicalAuthoritySha256 } from "../../loopx/control_plane/coordination/authority_store_codec.ts"; import type { JsonObject } from "../../loopx/control_plane/effect_program.ts"; import { - commitCoordinationProjectionMutation, indexCoordinationProjection, indexCoordinationProjectionTodos, prepareCoordinationProjectionCommit, @@ -61,8 +56,14 @@ test("native provider Todo creation and archival need no Markdown address", asyn expected_provider_revision: initial.provider_revision, mutations: [{ kind: "todo_upsert" as const, todo }], }; - assert.equal((await commitCoordinationProjectionMutation(store, input)).status, "applied"); - assert.equal((await commitCoordinationProjectionMutation(store, input)).status, "replayed"); + const seeded = await store.loadAuthority(); + assert.equal(seeded.status, "loaded"); + if (seeded.status !== "loaded") return; + const create = prepareCoordinationProjectionCommit({ + ...input, + projection: seeded.head, + }); + assert.equal((await store.commitAuthority(create)).status, "applied"); const head = await store.loadAuthority(); assert.equal(head.status, "loaded"); if (head.status !== "loaded") return; @@ -74,11 +75,13 @@ test("native provider Todo creation and archival need no Markdown address", asyn assert.throws(() => reduceCoordinationProjection(head.head, "goal-a", [{ kind: "todo_upsert", todo: missingArchive, }]), /omits existing fields: archive_state/); - assert.equal((await commitCoordinationProjectionMutation(store, { + const archive = prepareCoordinationProjectionCommit({ goal_id: "goal-a", operation_id: "archive:domain", expected_provider_revision: head.provider_revision, + projection: head.head, mutations: [{ kind: "todo_upsert", todo: { ...todo, archive_state: "archive" } }], - })).status, "applied"); + }); + assert.equal((await store.commitAuthority(archive)).status, "applied"); const reopened = await new FileAuthorityStore(root, "goal-a").loadAuthority(); assert.equal(reopened.status, "loaded"); if (reopened.status === "loaded") { @@ -343,161 +346,3 @@ test("coordination projection commit derives one auditable atomic transaction", { lease_epoch: 1, owner: "agent-a", todo_id: "todo_a" }, ]); }); - -test("provider-first coordination mutation applies, replays, and reads its receipt", async () => { - const root = await mkdtemp(join(tmpdir(), "loopx-coordination-mutation-")); - const store = new FileAuthorityStore(root, "goal-a"); - const initial = await store.commitAuthority({ - expected_provider_revision: null, - operation_id: "bootstrap:goal-a", - events: [{ schema_version: "bootstrap_v0" }], - next_projection: { - schema_version: "loopx_coordination_runtime_shadow_projection_v0", - goal_id: "goal-a", - source_authority: "file_v0", - todos: [{ todo_id: "todo_a", status: "open" }], - leases: [], - }, - receipts: [], - }); - assert.equal(initial.status, "applied"); - if (initial.status !== "applied") return; - - const input = { - goal_id: "goal-a", - operation_id: "claim:goal-a:todo_a:1", - expected_provider_revision: initial.provider_revision, - mutations: [ - { - kind: "todo_upsert" as const, - todo: { todo_id: "todo_a", status: "open", claimed_by: "agent-a" }, - }, - { - kind: "lease_upsert" as const, - lease: { todo_id: "todo_a", owner: "agent-a", lease_epoch: 1 }, - }, - ], - }; - const applied = await commitCoordinationProjectionMutation(store, input); - assert.equal(applied.status, "applied"); - const replayed = await commitCoordinationProjectionMutation(store, input); - assert.equal(replayed.status, "replayed"); - - const head = await store.loadAuthority(); - assert.equal(head.status, "loaded"); - if (head.status === "loaded") { - assert.equal( - (head.head.todos as Array>)[0]?.claimed_by, - "agent-a", - ); - assert.equal((head.head.leases as Array>)[0]?.owner, "agent-a"); - } -}); - -test("provider-first coordination mutation fences stale revision and operation reuse", async () => { - const root = await mkdtemp(join(tmpdir(), "loopx-coordination-mutation-fence-")); - const store = new FileAuthorityStore(root, "goal-a"); - const initial = await store.commitAuthority({ - expected_provider_revision: null, - operation_id: "bootstrap:goal-a", - events: [{ schema_version: "bootstrap_v0" }], - next_projection: { - goal_id: "goal-a", - source_authority: "file_v0", - todos: [{ todo_id: "todo_a", status: "open" }], - leases: [], - }, - receipts: [], - }); - assert.equal(initial.status, "applied"); - if (initial.status !== "applied") return; - - const stale = await commitCoordinationProjectionMutation(store, { - goal_id: "goal-a", - operation_id: "claim:stale", - expected_provider_revision: "file:stale", - mutations: [{ - kind: "todo_upsert", - todo: { todo_id: "todo_a", status: "open", claimed_by: "agent-a" }, - }], - }); - assert.equal(stale.status, "conflict"); - - const applied = await commitCoordinationProjectionMutation(store, { - goal_id: "goal-a", - operation_id: "claim:reused", - expected_provider_revision: initial.provider_revision, - mutations: [{ - kind: "todo_upsert", - todo: { todo_id: "todo_a", status: "open", claimed_by: "agent-a" }, - }], - }); - assert.equal(applied.status, "applied"); - const mismatch = await commitCoordinationProjectionMutation(store, { - goal_id: "goal-a", - operation_id: "claim:reused", - expected_provider_revision: initial.provider_revision, - mutations: [{ - kind: "todo_upsert", - todo: { todo_id: "todo_a", status: "open", claimed_by: "agent-b" }, - }], - }); - assert.equal(mismatch.status, "failed"); - if (mismatch.status === "failed") { - assert.equal(mismatch.reason_code, "coordination_operation_identity_mismatch"); - } -}); - -test("provider-first coordination mutation recovers a lost applied response", async () => { - const root = await mkdtemp(join(tmpdir(), "loopx-coordination-mutation-recover-")); - class LostResponseStore extends FileAuthorityStore { - override async commitAuthority( - commit: AuthorityStoreCommit, - ): Promise { - const result = await super.commitAuthority(commit); - return result.status === "applied" - ? { - status: "ambiguous", - reason_code: "simulated_response_loss", - reason: "commit response was lost", - } - : result; - } - } - const store = new LostResponseStore(root, "goal-a"); - const bootstrap = await FileAuthorityStore.prototype.commitAuthority.call(store, { - expected_provider_revision: null, - operation_id: "bootstrap:goal-a", - events: [{ schema_version: "bootstrap_v0" }], - next_projection: { - goal_id: "goal-a", - source_authority: "file_v0", - todos: [{ todo_id: "todo_a", status: "open" }], - leases: [], - }, - receipts: [], - }); - assert.equal(bootstrap.status, "applied"); - if (bootstrap.status !== "applied") return; - - const recovered = await commitCoordinationProjectionMutation(store, { - goal_id: "goal-a", - operation_id: "claim:recover", - expected_provider_revision: bootstrap.provider_revision, - mutations: [{ - kind: "todo_upsert", - todo: { todo_id: "todo_a", status: "open", claimed_by: "agent-a" }, - }], - }); - assert.equal(recovered.status, "recovered"); - const replayed = await commitCoordinationProjectionMutation(store, { - goal_id: "goal-a", - operation_id: "claim:recover", - expected_provider_revision: bootstrap.provider_revision, - mutations: [{ - kind: "todo_upsert", - todo: { todo_id: "todo_a", status: "open", claimed_by: "agent-a" }, - }], - }); - assert.equal(replayed.status, "replayed"); -}); diff --git a/tests/control_plane_ts/local_authority_provider.test.ts b/tests/control_plane_ts/local_authority_provider.test.ts index 28579d7170..b6f84b9133 100644 --- a/tests/control_plane_ts/local_authority_provider.test.ts +++ b/tests/control_plane_ts/local_authority_provider.test.ts @@ -16,7 +16,7 @@ import { authorityStoreCommitFixture } from "./authority_store_conformance.ts"; import * as runtime from "../../loopx/control_plane/coordination/local_authority_runtime.ts"; import { qualifiedShadow, promotionRequest, engageFence } from "./local_promotion_fixture.ts"; import { loadLegacyCoordinationWriterFence, legacyCoordinationWriterFencePath } from "../../loopx/control_plane/coordination/legacy_writer_fence.ts"; -import { acknowledgeLocalCoordinationTodoArchive, archiveLocalCoordinationTodos, listLocalCoordinationTodos, mutateLocalCoordinationAuthority } from "../../loopx/control_plane/coordination/local_authority_runtime.ts"; +import { acknowledgeLocalCoordinationTodoArchive, archiveLocalCoordinationTodos, listLocalCoordinationTodos } from "../../loopx/control_plane/coordination/local_authority_runtime.ts"; for (const [fault, source, reason] of [ ["database_missing", "sqlite_v0", "local_authority_provider_missing"], @@ -113,8 +113,6 @@ function providerCalls(directory: string, revision: string, dryRun: boolean) { observeLocalCoordinationOwnership: [{...input, schema_version: "loopx_local_ownership_observation_request_v0"}], listLocalCoordinationTodos: [{...input, schema_version: runtime.LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA}], readLocalCoordinationTodo: [{...input, schema_version: runtime.LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA}], - mutateLocalCoordinationAuthority: [{...input, schema_version: runtime.LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, - mutations: [{kind: "todo_remove", todo_id: "todo-a"}]}], createLocalCoordinationTodo: [ {...input, schema_version: runtime.LOCAL_COORDINATION_TODO_CREATE_REQUEST_SCHEMA, todo: {}}, {...witnessed, schema_version: runtime.LOCAL_COORDINATION_TODO_CREATE_WITNESSED_REQUEST_SCHEMA, todo: {}}], @@ -125,7 +123,6 @@ function providerCalls(directory: string, revision: string, dryRun: boolean) { {...updateInput, schema_version: "loopx_local_coordination_todo_update_request_v0"}, {...updateInput, schema_version: "loopx_local_coordination_todo_update_request_v1", planning_intent: {status: "blocked"}}, {...witnessed, schema_version: "loopx_local_coordination_todo_update_request_v2"}], - editLocalCoordinationTodo: [input], terminalLifecycleLocalCoordinationTodo: [ {...input, schema_version: runtime.LOCAL_COORDINATION_TODO_TERMINAL_LIFECYCLE_REQUEST_SCHEMA}, {...witnessed, schema_version: runtime.LOCAL_COORDINATION_TODO_TERMINAL_LIFECYCLE_WITNESSED_REQUEST_SCHEMA}], diff --git a/tests/control_plane_ts/local_authority_runtime.test.ts b/tests/control_plane_ts/local_authority_runtime.test.ts index 34117a5b32..b38ab8128b 100644 --- a/tests/control_plane_ts/local_authority_runtime.test.ts +++ b/tests/control_plane_ts/local_authority_runtime.test.ts @@ -22,9 +22,10 @@ import { import { TODO_CANONICAL_READ_RECORD_FIELDS, TODO_CANONICAL_READ_RECORD_SCHEMA, + prepareCoordinationProjectionCommit, + type CoordinationProjectionMutation, } from "../../loopx/control_plane/coordination/coordination_projection.ts"; import { - LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, LOCAL_COORDINATION_TODO_ARCHIVE_REQUEST_SCHEMA, LOCAL_COORDINATION_TODO_CLAIM_REQUEST_SCHEMA, LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA, @@ -33,7 +34,6 @@ import { archiveLocalCoordinationTodos, listLocalCoordinationTodos, claimLocalCoordinationTodo, - mutateLocalCoordinationAuthority, promoteLocalCoordinationAuthority, readLocalCoordinationTodo, terminalLifecycleLocalCoordinationTodo, @@ -91,6 +91,24 @@ function todoRecord(overrides: Record = {}): Record { - const root = await mkdtemp(join(tmpdir(), "loopx-canonical-todo-mutation-")); - const store = new FileAuthorityStore(join(root, "authority", "file-v0"), "goal-a"); - const applied = await store.commitAuthority({ expected_provider_revision: null, operation_id: "canonical-seed", - events: [], next_projection: withTodoReadModel({goal_id: "goal-a", handoff_mode: "soft_claim", - todos: [todoRecord({claimed_by: "agent-a"})], leases: []}), receipts: [] }); - assert.equal(applied.status, "applied"); - if (applied.status !== "applied") throw new Error("canonical fixture failed"); - - const advanced = await mutateLocalCoordinationAuthority({ - schema_version: LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, - runtime_root: root, - goal_id: "goal-a", - operation_id: "todo:goal-a:todo_a:advance-after-promotion", - expected_provider_revision: applied.provider_revision, - mutations: [{ - kind: "todo_upsert", - todo: todoRecord({ status: "in_progress", claimed_by: "agent-a" }), - }], - }); - assert.equal(advanced.status, "applied"); - - const partialReplacement = await mutateLocalCoordinationAuthority({ - schema_version: LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, - runtime_root: root, - goal_id: "goal-a", - operation_id: "todo:goal-a:todo_a:partial-after-promotion", - expected_provider_revision: advanced.provider_revision, - mutations: [{ - kind: "todo_upsert", - todo: { - schema_version: "todo_item_v0", - todo_id: "todo_a", - role: "agent", - status: "done", - done: true, - text: "Qualify canonical Todo semantics", - archive_state: "active", - source_section: "Agent Todo", - }, - }], - }); - assert.equal(partialReplacement.status, "failed"); - assert.equal(partialReplacement.reason_code, "invalid_coordination_mutation"); - assert.match(String(partialReplacement.reason ?? ""), /omits existing fields: claimed_by/); - const unchanged = await readLocalCoordinationTodo({ - schema_version: LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA, - runtime_root: root, - goal_id: "goal-a", - todo_id: "todo_a", - }); - assert.equal(unchanged.status, "found"); - assert.equal((unchanged.todo as Record).claimed_by, "agent-a"); - assert.equal((unchanged.todo as Record).status, "in_progress"); - - const receipt = await store.readReceipt("todo:goal-a:todo_a:advance-after-promotion"); - assert.equal(receipt.status, "found"); - if (receipt.status !== "found") throw new Error("mutation receipt missing"); - assert.equal(receipt.provider_revision, advanced.provider_revision); - - const read = await readLocalCoordinationTodo({ - schema_version: LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA, - runtime_root: root, - goal_id: "goal-a", - todo_id: "todo_a", - }); - assert.equal(read.status, "found"); - assert.equal((read.todo as Record).claimed_by, "agent-a"); - assert.equal((read.todo as Record).status, "in_progress"); - assert.equal(read.legacy_fallback_used, false); -}); - test("local promotion fences shadow revision, digest, and writer-fence identity", async () => { const root = await mkdtemp(join(tmpdir(), "loopx-local-authority-promote-fence-")); const shadow = await qualifiedShadow(root); @@ -313,69 +259,6 @@ test("new bootstrap and provider list fail closed without exact Todo consumer se assert.equal(listed.reason_code, "invalid_local_coordination_todo_list_request"); }); -test("local canonical runtime reads and mutates only the provider head", async () => { - const root = await mkdtemp(join(tmpdir(), "loopx-local-authority-runtime-")); - const store = new FileAuthorityStore(join(root, "authority", "file-v0"), "goal-a"); - const initial = await store.commitAuthority({ - expected_provider_revision: null, - operation_id: "promote:goal-a", - events: [{ schema_version: "promotion_v0" }], - next_projection: withTodoReadModel({ - goal_id: "goal-a", - source_authority: "file_v0", - todos: [todoRecord()], - leases: [], - }), - receipts: [], - }); - assert.equal(initial.status, "applied"); - if (initial.status !== "applied") return; - - const before = await readLocalCoordinationTodo({ - schema_version: LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA, - runtime_root: root, - goal_id: "goal-a", - todo_id: "todo_a", - }); - assert.equal(before.status, "found"); - assert.equal(before.decision_read_from_provider, true); - assert.equal(before.legacy_fallback_used, false); - - const mutation = await mutateLocalCoordinationAuthority({ - schema_version: LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, - runtime_root: root, - goal_id: "goal-a", - operation_id: "claim:goal-a:todo_a:1", - expected_provider_revision: initial.provider_revision, - mutations: [{ - kind: "todo_upsert", - todo: todoRecord({ claimed_by: "agent-a" }), - }], - }); - assert.equal(mutation.status, "applied"); - assert.equal(mutation.decision_read_from_provider, true); - assert.equal(mutation.legacy_fallback_used, false); - - const after = await readLocalCoordinationTodo({ - schema_version: LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA, - runtime_root: root, - goal_id: "goal-a", - todo_id: "todo_a", - }); - assert.equal((after.todo as Record).claimed_by, "agent-a"); - - const listed = await listLocalCoordinationTodos({ - schema_version: LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA, - runtime_root: root, - goal_id: "goal-a", - }); - assert.equal(listed.status, "loaded"); - assert.deepEqual(listed.todo_ids, ["todo_a"]); - assert.equal((listed.todos as Record[])[0]?.claimed_by, "agent-a"); - assert.equal(listed.decision_read_from_provider, true); - assert.equal(listed.legacy_fallback_used, false); -}); - test("provider-first Todo claim preserves the complete record and is replay-safe", async () => { const root = await mkdtemp(join(tmpdir(), "loopx-local-authority-claim-")); const store = new FileAuthorityStore(join(root, "authority", "file-v0"), "goal-a"); @@ -908,13 +791,13 @@ for (const native of [false, true]) { } // Operation B completes/archives/reassigns the Todo. A retry of A must // return A's receipt even after its actor registration and lease expire. - const completed = await mutateLocalCoordinationAuthority({ - schema_version: LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, - runtime_root: root, goal_id: "goal-a", operation_id: "complete-b", - expected_provider_revision: claimed.provider_revision, - mutations: [{kind: "todo_upsert", todo: {...claimedTodo, status: "done", done: true, + const completed = await applyTestProjectionMutation( + store, + "complete-b", + claimed.provider_revision, + [{kind: "todo_upsert", todo: {...claimedTodo, status: "done", done: true, archive_state: "archive", claimed_by: "agent-b"}}], - }); + ); assert.equal(completed.status, "applied"); const afterB = await store.loadAuthority(); for (const registered_agents of [["agent-b"], []]) { @@ -985,12 +868,12 @@ for (const native of [false, true]) { if (scan.status !== "page") return; assert.equal(scan.transactions.length, 1); assert.deepEqual(scan.transactions[0]?.events, []); - const changed = await mutateLocalCoordinationAuthority({ - schema_version: LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, - runtime_root: root, goal_id: "goal-a", operation_id: "later-change", - expected_provider_revision: afterA.provider_revision, - mutations: [{kind: "todo_upsert", todo: {...todo, ...later}}], - }); + const changed = await applyTestProjectionMutation( + store, + "later-change", + afterA.provider_revision, + [{kind: "todo_upsert", todo: {...todo, ...later}}], + ); assert.equal(changed.status, "applied"); const afterB = await store.loadAuthority(); const replayed = await claimLocalCoordinationTodo({...request, registered_agents: [], 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 a1357c9cda..c2206e8af9 100644 --- a/tests/control_plane_ts/shadow_native_writer_boundary.test.ts +++ b/tests/control_plane_ts/shadow_native_writer_boundary.test.ts @@ -13,20 +13,16 @@ import { archiveLocalCoordinationTodos, acknowledgeLocalCoordinationTodoArchive, createLocalCoordinationTodo, claimLocalCoordinationTodo, - mutateLocalCoordinationAuthority, editLocalCoordinationTodo, terminalLifecycleLocalCoordinationTodo, LOCAL_COORDINATION_TODO_ARCHIVE_REQUEST_SCHEMA, LOCAL_COORDINATION_TODO_ARCHIVE_ACK_REQUEST_SCHEMA, LOCAL_COORDINATION_TODO_CREATE_REQUEST_SCHEMA, LOCAL_COORDINATION_TODO_CLAIM_REQUEST_SCHEMA, LOCAL_COORDINATION_TODO_TERMINAL_LIFECYCLE_REQUEST_SCHEMA, - LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, } from "../../loopx/control_plane/coordination/local_authority_runtime.ts"; for (const [name, invoke, schema, requestFields] of [ ["create", createLocalCoordinationTodo, LOCAL_COORDINATION_TODO_CREATE_REQUEST_SCHEMA, {}], ["claim", claimLocalCoordinationTodo, LOCAL_COORDINATION_TODO_CLAIM_REQUEST_SCHEMA, {}], - ["mutate", mutateLocalCoordinationAuthority, LOCAL_COORDINATION_MUTATION_REQUEST_SCHEMA, {}], - ["edit", editLocalCoordinationTodo, "loopx_todo_compatibility_edit_request_v0", {}], ["terminal", terminalLifecycleLocalCoordinationTodo, LOCAL_COORDINATION_TODO_TERMINAL_LIFECYCLE_REQUEST_SCHEMA, { registered_agents: [], lifecycle_grants: [], successor_intents: [], From f07689dc3d527bcdc4ec1f06b6a747fba8c4b9ab Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:41:25 +0800 Subject: [PATCH 2/4] refactor(todo): retire capture-followups command Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../todo-capture-followups-smoke.py | 205 ------------------ examples/shared-goal-authority-e2e/README.md | 6 +- .../shared-goal-authority-e2e/correctness.md | 10 +- examples/shared-goal-authority-e2e/mutants.py | 2 +- loopx/cli_commands/todo.py | 22 -- .../cli_commands/todo_argument_validation.py | 40 +--- loopx/cli_commands/todo_event.py | 1 - loopx/cli_commands/todo_registration.py | 10 +- .../testing/authority_e2e_rows_stage2c.py | 20 +- .../testing/authority_e2e_rows_stage2c2.py | 11 +- loopx/todo_followups.py | 203 ----------------- .../test_local_authority_shadow_cli_e2e.py | 14 +- .../test_local_authority_shadow_runtime.py | 16 +- .../test_runtime_shadow_bounded_e2e.py | 12 +- .../test_shadow_fence_caller_parity_e2e.py | 4 - .../test_shadow_observable_e2e.py | 11 +- .../test_shadow_writer_boundaries.py | 87 +++----- .../test_shadow_writer_variant_e2e.py | 21 +- .../legacy_writer_fence_caller_parity_v0.json | 139 ------------ tests/test_cli_argument_diagnostics.py | 82 +------ 20 files changed, 102 insertions(+), 814 deletions(-) delete mode 100644 examples/control_plane/todo-capture-followups-smoke.py delete mode 100644 loopx/todo_followups.py diff --git a/examples/control_plane/todo-capture-followups-smoke.py b/examples/control_plane/todo-capture-followups-smoke.py deleted file mode 100644 index b44eef21e6..0000000000 --- a/examples/control_plane/todo-capture-followups-smoke.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke-test capped public-safe follow-up todo capture.""" - -from __future__ import annotations - -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.status import parse_active_state_todos # noqa: E402 - - -GOAL_ID = "todo-capture-followups-goal" -FOLLOWUP_ONE = "[P1] Add a public fixture that exercises deferred candidate promotion." -FOLLOWUP_TWO = "[P1] Document the public operator command summary contract." -FOLLOWUP_THREE = "[P2] Compare stale open todos against recent validation gaps." - - -def write_fixture(root: Path) -> tuple[Path, Path]: - project = root / "project" - runtime = root / "runtime" - state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" - registry_path = project / ".loopx" / "registry.json" - state_file.parent.mkdir(parents=True) - state_file.write_text( - "---\n" - "status: active\n" - "updated_at: 2026-01-01T00:00:00+00:00\n" - "---\n\n" - "# Active Goal State\n\n" - "## Objective\n\n" - "Keep follow-up capture bounded.\n\n" - "## Agent Todo\n\n" - "- [ ] [P1] Preserve the existing agent todo for duplicate detection.\n" - " \n", - encoding="utf-8", - ) - registry_path.parent.mkdir(parents=True) - registry_path.write_text( - json.dumps( - { - "schema_version": 1, - "updated_at": "2026-01-01T00:00:00+00:00", - "common_runtime_root": str(runtime), - "goals": [ - { - "id": GOAL_ID, - "domain": "todo-capture-followups-fixture", - "status": "active", - "repo": str(project), - "state_file": f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md", - "adapter": {"kind": "generic_project_goal_v0", "status": "connected"}, - "authority_sources": [], - } - ], - }, - ensure_ascii=False, - indent=2, - ) - + "\n", - encoding="utf-8", - ) - return registry_path, state_file - - -def run_cli(registry_path: Path, *args: str, check: bool = True) -> dict: - result = subprocess.run( - [ - sys.executable, - "-m", - "loopx.cli", - "--registry", - str(registry_path), - "--format", - "json", - *args, - ], - cwd=REPO_ROOT, - check=False, - text=True, - capture_output=True, - ) - if check and result.returncode != 0: - raise AssertionError(result.stderr or result.stdout) - return json.loads(result.stdout) - - -def main() -> int: - with tempfile.TemporaryDirectory(prefix="loopx-todo-capture-followups-") as tmp: - registry_path, state_file = write_fixture(Path(tmp)) - original = state_file.read_text(encoding="utf-8") - - dry_run = run_cli( - registry_path, - "todo", - "capture-followups", - "--goal-id", - GOAL_ID, - "--follow-up", - FOLLOWUP_ONE, - "--follow-up", - FOLLOWUP_TWO, - "--follow-up", - FOLLOWUP_THREE, - "--evidence", - "examples/control_plane/todo-capture-followups-smoke.py#main", - "--action-kind", - "implement", - "--required-write-scope", - "examples/**", - "--dry-run", - ) - assert dry_run["ok"] is True, dry_run - assert dry_run["dry_run"] is True, dry_run - assert dry_run["recorded_count"] == 2, dry_run - assert dry_run["skipped_count"] == 1, dry_run - assert dry_run["items"][2]["skipped_reason"] == "max_items_exceeded", dry_run - assert dry_run["items"][0]["updated_at"] == dry_run["updated_at"], dry_run - assert state_file.read_text(encoding="utf-8") == original - - payload = run_cli( - registry_path, - "todo", - "capture-followups", - "--goal-id", - GOAL_ID, - "--follow-up", - FOLLOWUP_ONE, - "--follow-up", - FOLLOWUP_ONE, - "--follow-up", - FOLLOWUP_TWO, - "--follow-up", - "Inspect /private/tmp/raw-output before writing a todo.", - "--evidence", - "examples/control_plane/todo-capture-followups-smoke.py#main", - "--action-kind", - "implement", - "--required-write-scope", - "examples/**", - ) - assert payload["ok"] is True, payload - assert payload["dry_run"] is False, payload - assert payload["recorded_count"] == 2, payload - assert payload["skipped_count"] == 2, payload - assert payload["items"][1]["skipped_reason"] == "duplicate", payload - assert payload["items"][3]["skipped_reason"] == "unsafe_boundary:local_absolute_path", payload - - fields = parse_active_state_todos(state_file.read_text(encoding="utf-8")) - agent_items = fields["agent_todos"]["items"] - texts = [item["text"] for item in agent_items] - assert FOLLOWUP_ONE in texts, texts - assert FOLLOWUP_TWO in texts, texts - assert FOLLOWUP_THREE not in texts, texts - assert not any("claimed_by" in item for item in agent_items), agent_items - first_followup = next(item for item in agent_items if item["text"] == FOLLOWUP_ONE) - assert first_followup["task_class"] == "advancement_task", first_followup - assert first_followup["action_kind"] == "implement", first_followup - assert first_followup["required_write_scopes"] == ["examples/**"], first_followup - assert first_followup["evidence"] == "examples/control_plane/todo-capture-followups-smoke.py#main", first_followup - assert first_followup["updated_at"] == payload["updated_at"], first_followup - assert payload["items"][0]["updated_at"] == payload["updated_at"], payload - - duplicate_again = run_cli( - registry_path, - "todo", - "capture-followups", - "--goal-id", - GOAL_ID, - "--follow-up", - FOLLOWUP_ONE, - "--evidence", - "examples/control_plane/todo-capture-followups-smoke.py#main", - ) - assert duplicate_again["recorded_count"] == 0, duplicate_again - assert duplicate_again["items"][0]["skipped_reason"] == "duplicate", duplicate_again - - unsafe_evidence = run_cli( - registry_path, - "todo", - "capture-followups", - "--goal-id", - GOAL_ID, - "--follow-up", - "[P2] This item is otherwise public-safe.", - "--evidence", - "/private/tmp/raw-evidence.txt", - check=False, - ) - assert unsafe_evidence["ok"] is False, unsafe_evidence - assert "evidence is not public-safe" in unsafe_evidence["error"], unsafe_evidence - - print("todo-capture-followups-smoke ok") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/examples/shared-goal-authority-e2e/README.md b/examples/shared-goal-authority-e2e/README.md index 0857a3c61a..71e2292798 100644 --- a/examples/shared-goal-authority-e2e/README.md +++ b/examples/shared-goal-authority-e2e/README.md @@ -38,18 +38,18 @@ suites rather than in the pytest shards. | `s2a.nokv_live_qualification` | 2a | store_direct | env:nokv_authority | runs the merged `examples/nokv-authority-store/live-qualification.ts --execute-live` against an existing workbench with a fresh tenant/goal pair; requires `ok=true`, the single-node store-conformance scope, every check `passed`, NoKV SDK `0.11.0` / API `1`, and no promotion or availability claim; evidence carries check ids, counts, and config and workbench digest prefixes, never a configuration value or the workbench name | | `s2b.postgresql_conformance_live` | 2b | store_direct | env:postgresql | `postgresql_authority_store.integration.test.ts` under node's TAP reporter: `# pass >= 9`, `# fail 0`, `# skipped 0` | | `s2c1.configure_enable_disable_roundtrip` | 2c1 | real_cli | deterministic | `configure-goal` preview does not write, enable writes, captured observations for a todo and a lease, read-back summary `enabled/file_one_way`, disable writes and later writes neither observe nor touch candidate bytes | -| `s2c1.every_writer_family_captures` | 2c1 | real_cli | deterministic | handoff-mode set, todo add/update/complete/supersede/capture-followups/archive-completed, task-lease acquire/renew/transfer each carry `outcome in {captured, replayed, ambiguous_reconciled}`, `primary_writeback_preserved=true`, `provider_to_local_writes=false`, `candidate_read_for_decision=false`; an idempotent re-acquire carries no `authority_shadow`; candidate `cursor == captured count`, operation ids equal observation ids, no time-active lease in the head, head todos equal `todo list` | +| `s2c1.every_writer_family_captures` | 2c1 | real_cli | deterministic | handoff-mode set, todo add/update/complete/supersede/archive-completed, task-lease acquire/renew/transfer each carry `outcome in {captured, replayed, ambiguous_reconciled}`, `primary_writeback_preserved=true`, `provider_to_local_writes=false`, `candidate_read_for_decision=false`; an idempotent re-acquire carries no `authority_shadow`; candidate `cursor == captured count`, operation ids equal observation ids, no time-active lease in the head, head todos equal `todo list` | | `s2c1.default_off_isolation` | 2c1 | real_cli | deterministic | a default-off goal returns the same response fields as an observed goal, carries no `authority_shadow`, and creates no `authority-shadow/` directory | | `s2c1.candidate_failure_preserves_primary` | 2c1 | real_cli | deterministic | a blocked candidate directory yields `outcome=failed`, `reason_code=shadow_observation_failed`, and the committed todo is in the primary state | | `s2c1.crash_gap_loses_observation` | 2c1 | real_cli | deterministic (POSIX) | a writer SIGKILLed while the observation lock is held commits its todo but leaves no candidate document; the next write captures the full two-todo snapshot without claiming an outbox or correlation | -| `s2c1.dual_runtime_root_consistency` | 2c1 | real_cli | deterministic | with `common_runtime_root` different from `--runtime-root`, todo add, task-lease acquire, todo update, capture-followups, and a leased completion all observe into one store identity; the head holds both todos and the released lease; the registry root gains neither a candidate lineage nor lease state | +| `s2c1.dual_runtime_root_consistency` | 2c1 | real_cli | deterministic | with `common_runtime_root` different from `--runtime-root`, two todo adds, task-lease acquire, todo update, and a leased completion all observe into one store identity; the head holds both todos and the released lease; the registry root gains neither a candidate lineage nor lease state | | `s2c1.migration_seeds_new_lineage` | 2c1 | real_cli | deterministic | `migrate-state` dry run plans the seed without writing; execute seeds one fresh `file:` lineage at cursor `1` that carries no legacy identity, revision, source path, or private byte | | `s2c2.outbox_prepared_then_committed_entries` | 2c2 | real_cli | deterministic | with the maintenance lock held, `todo add` (Python) and `task-lease acquire` (TypeScript) report `drain_deferred/drain_lock_busy`, `status` shows one `committed_pending` entry per partition with one prepared record and one committed marker on disk; one `drain` delivers both (`delivered=2`), history holds the bootstrap plus two committed receipts from both writer runtimes, and the next write delivers inline at cursor `4` | | `s2c2.drain_idempotent` | 2c2 | real_cli | deterministic | three deferred entries: `drain --max-entries 1` delivers one (`pending_after=2`, `budget_exhausted`), the next `drain` delivers two, an idle `drain` reports `nothing_pending` with unchanged cursor, `head_digest` and `provider_revision`; receipts settle sequences 1..3; an idempotent same-key re-acquire carries no capture evidence and adds no transaction | | `s2c2.sigkill_between_primary_write_and_drain` | 2c2 | real_cli | deterministic (POSIX) | `todo add` SIGKILLed at `before_replace`, `after_replace` and `before_marker` leaves one prepared-only entry each; `drain` settles it as `abandoned` (no-op, primary unchanged) or `committed_proven_by_readback`, the projection equals the primary, and `inspect` ends `matched` | | `s2c2.sigkill_mid_drain` | 2c2 | real_cli | deterministic (POSIX) | `todo add` SIGKILLed at `before_commit`, `after_commit`, `after_cursor` and `between_unlinks`: the next `drain` delivers the uncommitted entry once or replays the committed one (`replayed=1, delivered=0`), history holds exactly one delivery, only the cursor remains, and a further drain is idle | | `s2c2.rollback_with_pending_entries` | 2c2 | real_cli | deterministic (POSIX) | with one committed-pending and one prepared-only entry, `inspect` reports `outbox_pending` at the exact revision, a rollback preview writes nothing, `rollback --execute` applies and archives the outbox with both entries, the marker, the cursor and the manifest; capture then reports `bootstrap_required` while primary writes continue, a rebootstrap starts a new lineage from the current primary (three todos), and the historical rollback replays against it | -| `s2c2.parity_equal` | 2c2 | real_cli | deterministic | three cycles interleave Python Markdown writers (add, note update plus a no-change repeat, explicit exclusion set and clear plus a no-change repeat, complete, supersede, capture-followups) with TypeScript lease writers (acquire, renew, transfer, and the fence close of a leased complete or supersede); after each cycle `inspect` is `matched`, `qualify` with every required write class is `qualified` with `operation_count` equal to the delivered mutations, `read-candidate` returns the anchor todo, and `sustained_parity_verdict` stays `not_evaluated` | +| `s2c2.parity_equal` | 2c2 | real_cli | deterministic | three cycles interleave Python Markdown writers (add, note update plus a no-change repeat, explicit exclusion set and clear plus a no-change repeat, complete, supersede, and a second add) with TypeScript lease writers (acquire, renew, transfer, and the fence close of a leased complete or supersede); after each cycle `inspect` is `matched`, `qualify` with every required write class is `qualified` with `operation_count` equal to the delivered mutations, `read-candidate` returns the anchor todo, and `sustained_parity_verdict` stays `not_evaluated` | | `s2c2.parity_divergent_detects_foreign_edit` | 2c2 | real_cli | deterministic | a direct edit of the primary makes `inspect` report `drifted/shadow_projection_drift`, `qualify` and `read-candidate` reject, a later `todo add` commits but its capture holds on `source_partition_continuity_unproved`; restoring the bytes does not requalify (`outbox_pending`), `drain` stays `stopped`, and only `rollback --execute` plus a fresh bootstrap qualifies again | | `s2c2.event_only_todo_source_holds` | 2c2 | real_cli | deterministic | an event-only Todo appended to the goal's state event log makes `inspect`, `qualify` and `read-candidate` fail closed with `event_log_writer_not_bound`, `status` stays readable, a Markdown write still commits with its capture held, the event log is untouched; removing the event source does not requalify, and rollback plus rebootstrap recovers | | `s2c2.migration_seeds_and_drains` | 2c2 | real_cli | deterministic | `migrate-state` previews an actively captured goal without writing, refuses `--execute` with `shadow_source_replacement_requires_rebootstrap` (also when capture is merely disabled), and executes only after `rollback`; the migrated goal carries its disabled capture configuration, plans no observation seed, requires its own `bootstrap`, then captures a write to cursor `2` and qualifies on it while the legacy archive is retained | diff --git a/examples/shared-goal-authority-e2e/correctness.md b/examples/shared-goal-authority-e2e/correctness.md index 5e835e9cd7..1485789f93 100644 --- a/examples/shared-goal-authority-e2e/correctness.md +++ b/examples/shared-goal-authority-e2e/correctness.md @@ -143,14 +143,14 @@ Appendix C). `effect_id` follows each verb's existing rule: acquire reports settlement identity when the request carries `owner` and `idempotency_key` and `null` otherwise. -Previews: `archive-completed` without `--execute` and `capture-followups ---dry-run` write nothing. After promotion, public terminal/archive commands +Previews: `archive-completed` without `--execute` and `todo add --dry-run` +write nothing. After promotion, public terminal/archive commands route to canonical authority instead of treating the promotion fence as an error. A hard-lease terminal preview therefore rejects with `handoff_mode_requires_lease` when its target has no lease, succeeds for a matching lease, and may preview the declared user-gate auto-acquire path; all three leave the primary record and receipts unchanged. Legacy-only writers -such as `todo update` and `capture-followups --execute` remain fenced. A fenced +such as `todo add` and `todo update` remain fenced. A fenced committed releasing `fence_close` releases the caller's claimed mutation lock in `finally` while the lease stays `active` and its `held` receipt is untouched; the caller's fence token is spent, a retry reports @@ -169,7 +169,7 @@ completion-policy admission on an otherwise unchanged legacy terminal call. The complete observable behaviour is pinned row by row in `tests/fixtures/control_plane/legacy_writer_fence_caller_parity_v0.json` -(21 TypeScript entry rows, 26 real-process CLI rows; whole-object legacy +(21 TypeScript entry rows, 21 real-process CLI rows; whole-object legacy envelopes, stable-field subsets for provider-first rows, exact exit status, exclusion-free effect snapshots, and declared after-state) and enforced by `tests/control_plane_ts/legacy_writer_fence_caller_parity.test.ts` @@ -315,7 +315,7 @@ Python/TS/JSON provenance, and reads back through an independent native process. | Obligation | Retained oracle | | --- | --- | -| Full baseline, mixed Python/native writers, handoff, followups, monitor successor, one receipt per mutation | `test_runtime_shadow_bounded_e2e.py`, `test_shadow_drain_e2e.py` | +| Full baseline, mixed Python/native writers, handoff, todo add, monitor successor, one receipt per mutation | `test_runtime_shadow_bounded_e2e.py`, `test_shadow_drain_e2e.py` | | Cursor attacks, complete proof before bounded cleanup, missing cursor writer-first, dual drainers | `test_shadow_cursor_safety.py`, `test_shadow_drain_adversarial.py`, `shadow_cursor_safety.test.ts` | | Todo/lease abandoned prefixes, later mutations, all cursor consumers and forged applied digests | `test_shadow_cursor_recovery_e2e.py` | | Full caller diagnostics, argument readback and no-effect controls with absent/disabled/enabled capture | `test_shadow_observable_e2e.py` | diff --git a/examples/shared-goal-authority-e2e/mutants.py b/examples/shared-goal-authority-e2e/mutants.py index d2e2e8229e..c4bbd31fa0 100644 --- a/examples/shared-goal-authority-e2e/mutants.py +++ b/examples/shared-goal-authority-e2e/mutants.py @@ -339,7 +339,7 @@ def apply(source: str) -> str: (COORDINATION + "legacy_writer_fence.py", replacement( 'LEGACY_WRITER_FENCED_REMEDIATION = (\n "legacy coordination writer is fenced; use the promoted canonical authority "\n "({authority_mode}) for goal {goal_id}; fence {fence_id}; "\n "the primary record was not changed"\n)', 'LEGACY_WRITER_FENCED_REMEDIATION = "legacy coordination writer is fenced"')), -), "tests/control_plane/test_shadow_fence_caller_parity_e2e.py::test_fence_caller_parity[cli-todo_capture_followups-engaged]")) +), "tests/control_plane/test_shadow_fence_caller_parity_e2e.py::test_fence_caller_parity[cli-todo_update_status-engaged]")) CASES.append(Case("fence_envelope_schema_leak", ( (COORDINATION + "legacy_writer_fence.ts", replacement( " this.payload = { write_check: writeCheck };", diff --git a/loopx/cli_commands/todo.py b/loopx/cli_commands/todo.py index 83c9b4786b..afaf2ef9b1 100644 --- a/loopx/cli_commands/todo.py +++ b/loopx/cli_commands/todo.py @@ -25,7 +25,6 @@ from ..control_plane.work_items.semantic_replan_writeback import ( qualify_replan_writeback, ) -from ..todo_followups import capture_followup_todos from ..todo_suggestion_prompt import ( build_todo_suggestion_prompt_packet, render_todo_suggestion_prompt_markdown, @@ -48,7 +47,6 @@ validate_shared_todo_options, validate_todo_add_options, validate_todo_archive_completed_options, - validate_todo_capture_followups_options, validate_todo_claim_options, validate_todo_complete_options, validate_todo_list_options, @@ -586,26 +584,6 @@ def handle_todo_command( trigger=args.suggestion_trigger, ) payload["dry_run"] = True - elif args.todo_command == "capture-followups": - validate_todo_capture_followups_options(args) - followups = list(args.followups or []) - if args.text: - followups.append(args.text) - payload = capture_followup_todos( - registry_path=registry_path, - runtime_root_arg=runtime_root_arg, - goal_id=args.goal_id, - followups=followups, - evidence=args.evidence or "", - task_class=args.task_class, - action_kind=args.action_kind, - required_write_scopes=args.required_write_scopes, - required_capabilities=args.required_capabilities, - target_capabilities=args.target_capabilities, - required_decision_scopes=args.required_decision_scopes, - **_todo_path_args(args), - dry_run=bool(args.dry_run), - ) else: raise ValueError("unsupported todo command") except Exception as exc: diff --git a/loopx/cli_commands/todo_argument_validation.py b/loopx/cli_commands/todo_argument_validation.py index 40becb7dc0..8dcdc0b414 100644 --- a/loopx/cli_commands/todo_argument_validation.py +++ b/loopx/cli_commands/todo_argument_validation.py @@ -8,7 +8,6 @@ TODO_OPTION_FIELDS = ( ("--role", "role"), ("--text", "text"), - ("--follow-up", "followups"), ("--todo-id", "todo_id"), ("--claim-operation-id", "claim_operation_id"), ("--update-operation-id", "update_operation_id"), @@ -84,7 +83,7 @@ ) _TODO_UPDATE_MUTABLE_FIELDS = ( - "text", "followups", "status", "note", "evidence", "reason", "task_class", + "text", "status", "note", "evidence", "reason", "task_class", "action_kind", "task_domain", "task_repository", "continuation_policy", "required_write_scopes", "required_capabilities", "target_capabilities", "capability_gap_status", "explore_result_node_refs", "clear_explore_result_node_refs", "decision_scope", @@ -100,10 +99,6 @@ "decision_outcome", "todo update does not accept --decision-outcome; use todo complete", ), - ( - "followups", - "todo update does not support --follow-up; use `todo capture-followups`", - ), ("next_claimed_by", "todo update does not support --next-claimed-by"), ( "next_task_repository", @@ -123,7 +118,6 @@ _TODO_ADD_INITIAL_RULES = ( ("decision_outcome", True, "does not accept --decision-outcome; record it on completion"), - ("followups", True, "does not support --follow-up; use `todo capture-followups`"), ("role", False, "requires --role"), ("text", False, "requires --text"), ("clear_claim", True, "accepts --claimed-by but not --clear-claim"), @@ -413,8 +407,6 @@ def validate_todo_complete_options(args: argparse.Namespace) -> None: raise ValueError("--successor-todo-id links existing work and cannot be combined with --next-agent-todo or --next-user-todo") if args.no_follow_up and not (args.note or args.evidence): raise ValueError("--no-follow-up requires --note or --evidence") - if args.followups: - raise ValueError("todo complete does not support --follow-up; use `todo capture-followups`") if args.continuation_policy: raise ValueError("todo complete does not update --continuation-policy; use todo update first") validate_successor_routing_options(args) @@ -439,8 +431,6 @@ def validate_todo_supersede_options(args: argparse.Namespace) -> None: raise ValueError("todo supersede does not support --self-merged") if args.no_follow_up: raise ValueError("todo supersede does not support --no-follow-up") - if args.followups: - raise ValueError("todo supersede does not support --follow-up; use `todo capture-followups`") if args.continuation_policy: raise ValueError("todo supersede does not update --continuation-policy; use todo update first") validate_successor_routing_options(args) @@ -461,7 +451,6 @@ def validate_todo_archive_completed_options(args: argparse.Namespace) -> None: (args.next_task_repository or args.next_required_capabilities, "todo archive-completed does not support successor routing metadata"), (args.self_merged, "todo archive-completed does not support --self-merged"), (args.no_follow_up, "todo archive-completed does not support --no-follow-up"), - (args.followups, "todo archive-completed does not support --follow-up; use `todo capture-followups`"), (args.successor_todo_ids, "todo archive-completed does not support --successor-todo-id"), ) for triggered, message in checks: @@ -478,27 +467,6 @@ def validate_todo_suggest_options(args: argparse.Namespace) -> None: ) -def validate_todo_capture_followups_options(args: argparse.Namespace) -> None: - checks = ( - (args.role, "todo capture-followups always records agent todos; do not pass --role"), - (args.claimed_by, "todo capture-followups writes unclaimed todos; do not pass --claimed-by"), - ) - for triggered, message in checks: - if triggered: - raise ValueError(message) - _validate_todo_option_subset( - args, - { - "text", "followups", "evidence", "task_class", "action_kind", - "continuation_policy", "required_write_scopes", "required_capabilities", - "target_capabilities", "required_decision_scopes", "state_file", - }, - "todo capture-followups only accepts --goal-id, --follow-up, optional " - "--text shorthand, --evidence, routing metadata, --project, --state-file, " - "and --dry-run; unsupported: ", - ) - - def validate_shared_todo_options(args: argparse.Namespace) -> None: agent_id_allowed_for_user_authoring = ( args.todo_command == "add" @@ -563,7 +531,7 @@ def validate_shared_todo_options(args: argparse.Namespace) -> None: "--authority-reason is supported only by todo update/complete/supersede" ) if ( - args.todo_command not in {"suggest", "plan", "capture-followups"} + args.todo_command not in {"suggest", "plan"} and args.agent_id and not agent_id_allowed_for_user_authoring and not agent_id_allowed_for_read @@ -596,12 +564,12 @@ def validate_shared_todo_options(args: argparse.Namespace) -> None: "todo update accepts either --resume-when or --clear-resume-when, not both" ) if ( - args.todo_command not in {"suggest", "capture-followups"} + args.todo_command != "suggest" and (args.suggestion_sources or args.suggestion_trigger) ): raise ValueError("--from and --trigger are supported only by todo suggest") if ( - args.todo_command not in {"suggest", "list", "capture-followups"} + args.todo_command not in {"suggest", "list"} and args.todo_limit is not None ): raise ValueError( diff --git a/loopx/cli_commands/todo_event.py b/loopx/cli_commands/todo_event.py index 8a942c4f4a..44855b2e76 100644 --- a/loopx/cli_commands/todo_event.py +++ b/loopx/cli_commands/todo_event.py @@ -27,7 +27,6 @@ "complete": "todo_complete", "supersede": "todo_supersede", "archive-completed": "todo_archive_completed", - "capture-followups": "todo_capture_followups", } diff --git a/loopx/cli_commands/todo_registration.py b/loopx/cli_commands/todo_registration.py index a9d51172e9..43509b360d 100644 --- a/loopx/cli_commands/todo_registration.py +++ b/loopx/cli_commands/todo_registration.py @@ -42,7 +42,6 @@ def register_todo_command( "archive-completed", "suggest", "plan", - "capture-followups", "project-markdown", ], default=None, @@ -51,19 +50,12 @@ def register_todo_command( "agent id, list to read projected todos, update/complete/supersede to transition by todo_id, or " "archive-completed to move older completed todos into Completed Work Archive. " "Use suggest to generate an agent-facing candidate todo analysis prompt without writing state. " - "Use plan with --text and --agent-id for the existing Goal's model planning checkpoint; the caller owns subsequent execution. " - "Use capture-followups to record a capped public-safe unclaimed follow-up batch." + "Use plan with --text and --agent-id for the existing Goal's model planning checkpoint; the caller owns subsequent execution." ), ) todo_parser.add_argument("--goal-id", required=True, help="Goal id whose active state should receive the todo.") todo_parser.add_argument("--role", choices=["user", "agent"], help="Todo owner. Required for add; optional todo_id search scope for lifecycle commands. Defaults to agent for archive-completed.") todo_parser.add_argument("--text", help="Todo text. Required for add; keep it short and public-safe enough for local status.") - todo_parser.add_argument( - "--follow-up", - dest="followups", - action="append", - help="For capture-followups, append one public-safe agent follow-up todo. Repeat up to the requested batch.", - ) todo_parser.add_argument("--todo-id", help="Structured todo id from status/quota, such as todo_ab12cd34ef56.") todo_parser.add_argument( "--update-operation-id", diff --git a/loopx/control_plane/testing/authority_e2e_rows_stage2c.py b/loopx/control_plane/testing/authority_e2e_rows_stage2c.py index 8a3385b7ff..15f3edb7a7 100644 --- a/loopx/control_plane/testing/authority_e2e_rows_stage2c.py +++ b/loopx/control_plane/testing/authority_e2e_rows_stage2c.py @@ -261,13 +261,9 @@ def _writer_sequence_supersede_and_hygiene(sequence: _WriterSequence) -> str: flag="superseded", ) sequence.commit( - "todo capture-followups", - sequence.cli( - "todo", "capture-followups", - "--follow-up", "Verify the migrated authority projection.", - "--evidence", "validation://ladder-followup", - ), - flag="changed", + "todo add (verification)", + add_todo(sequence.workspace, "Verify the migrated authority projection."), + flag="added", ) sequence.commit( "todo archive-completed", @@ -434,11 +430,7 @@ def row_dual_runtime_root_consistency(context: RowContext) -> RowOutcome: workspace, "todo", "update", "--goal-id", workspace.goal_id, "--todo-id", todo_id, "--note", "Observed under the override root.", "--agent-id", AGENT_A, ) - followups = run_cli( - workspace, "todo", "capture-followups", "--goal-id", workspace.goal_id, - "--follow-up", "Keep one candidate lineage per goal.", - "--evidence", "validation://ladder-one-root", - ) + second_add = add_todo(workspace, "Keep one candidate lineage per goal.") completed = run_cli( workspace, "todo", "complete", "--goal-id", workspace.goal_id, "--todo-id", todo_id, "--agent-id", AGENT_A, "--task-lease-idempotency-key", "ladder-one-root-lease", @@ -451,7 +443,7 @@ def row_dual_runtime_root_consistency(context: RowContext) -> RowOutcome: ("todo add", added), ("task-lease acquire", acquired), ("todo update", updated), - ("todo capture-followups", followups), + ("todo add (second)", second_add), ("todo complete", completed), ) ] @@ -462,7 +454,7 @@ def row_dual_runtime_root_consistency(context: RowContext) -> RowOutcome: expect(document.cursor == str(len(observations)), "candidate cursor must equal the observation count") expect( todo_id in document.todo_ids and len(document.todo_ids) == 2, - "head must hold the completed todo and its captured follow-up", + "head must hold the completed todo and the second added todo", ) expect( [lease.get("todo_id") for lease in document.leases] == [todo_id] diff --git a/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py b/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py index 2892f00c1b..ad561c83f9 100644 --- a/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py +++ b/loopx/control_plane/testing/authority_e2e_rows_stage2c2.py @@ -72,7 +72,6 @@ "todo_update", "todo_complete", "todo_supersede", - "todo_capture_followups", "task_lease_acquire", "task_lease_renew", "task_lease_transfer", @@ -795,13 +794,9 @@ def _mixed_writer_cycle(ledger: _MixedWriterLedger, cycle: int) -> None: flag="superseded", ) ledger.mutate( - "todo capture-followups", - ledger.cli( - "todo", "capture-followups", - "--follow-up", f"Cycle {cycle}: verify the captured projection.", - "--evidence", "validation://ladder-parity-followup", - ), - flag="changed", + "todo add (verification)", + add_todo(workspace, f"Cycle {cycle}: verify the captured projection."), + flag="added", ) diff --git a/loopx/todo_followups.py b/loopx/todo_followups.py deleted file mode 100644 index 8d3a0a5ffb..0000000000 --- a/loopx/todo_followups.py +++ /dev/null @@ -1,203 +0,0 @@ -from __future__ import annotations - -import re -from pathlib import Path -from typing import Any - -from .control_plane.coordination.legacy_writer_fence import legacy_todo_write_transaction -from .control_plane.coordination.local_authority_shadow_adapter import effective_runtime_root -from .control_plane.coordination.runtime_shadow_writer_adapter import ( - write_captured_todo_state, - begin_todo_runtime_shadow_capture, - settle_todo_runtime_shadow_capture, -) -from .state_refresh import now_local -from .control_plane.todos.contract import TODO_TASK_CLASS_ADVANCEMENT -from .control_plane.todos.todo_summary import normalize_todo_text -from .todos import ( - TODO_SECTION_HEADINGS, - add_todo_to_lines, - replace_updated_at, - resolve_todo_state_path, - section_bounds, - todo_blocks, -) - - -MAX_CAPTURED_FOLLOWUP_TODOS = 2 - -_UNSAFE_FOLLOWUP_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = ( - ("local_absolute_path", re.compile(r"(?i)(?:/Users/|/private/|/var/folders/|file://)")), - ("local_state_path", re.compile(r"(?i)(?:^|[\s\"'`])(?:\.local/|\.codex/|\.loopx/)")), - ("credential_literal", re.compile(r"(?i)\b(?:api[_-]?key|secret|password|token)\s*[:=]")), - ("internal_only_marker", re.compile(r"(?i)\binternal[-_\s]?only\b")), -) - - -def _unsafe_followup_reason(value: str) -> str | None: - for reason, pattern in _UNSAFE_FOLLOWUP_PATTERNS: - if pattern.search(value): - return reason - return None - - -def _compact_text(value: Any) -> str: - return " ".join(str(value or "").strip().split()) - - -def _existing_agent_todo_texts(lines: list[str]) -> set[str]: - bounds = section_bounds(lines, "agent") - if not bounds: - return set() - start, end, section = bounds - return { - normalize_todo_text(str(block.get("text") or "")) - for block in todo_blocks(lines, start, end, role="agent", source_section=section) - if block.get("text") - } - - -def capture_followup_todos( - *, - registry_path: Path, - goal_id: str, - followups: list[str], - evidence: str, - task_class: str | None = None, - action_kind: str | None = None, - required_write_scopes: list[str] | None = None, - required_capabilities: list[str] | None = None, - target_capabilities: list[str] | None = None, - required_decision_scopes: Any = None, - project: Path | None = None, - state_file: Path | None = None, - dry_run: bool = False, - runtime_root_arg: str | None = None, -) -> dict[str, Any]: - if not followups: - raise ValueError("todo capture-followups requires at least one --follow-up") - evidence_text = _compact_text(evidence) - if not evidence_text: - raise ValueError("todo capture-followups requires --evidence with a public-safe pointer") - evidence_reason = _unsafe_followup_reason(evidence_text) - if evidence_reason: - raise ValueError(f"todo capture-followups evidence is not public-safe: {evidence_reason}") - - resolved_project, resolved_state_file = resolve_todo_state_path( - registry_path=registry_path, - goal_id=goal_id, - project=project, - state_file=state_file, - ) - - items: list[dict[str, Any]] = [] - runtime_root = effective_runtime_root(registry_path, runtime_root_arg) - with legacy_todo_write_transaction( - registry_path, goal_id, resolved_state_file, None, "todo_capture_followups", - dry_run, runtime_root=runtime_root, - ): - original = resolved_state_file.read_text(encoding="utf-8") - 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="todo_capture_followups", - original_text=original, - ) - lines = original.splitlines() - existing_texts = _existing_agent_todo_texts(lines) - seen_texts: set[str] = set() - updated_at = now_local() - changed = False - recorded_count = 0 - - for raw_followup in followups: - todo_text = _compact_text(raw_followup) - item: dict[str, Any] = { - "todo": todo_text, - "added": False, - "already_exists": False, - "skipped": False, - "skipped_reason": None, - } - if not todo_text: - item.update({"skipped": True, "skipped_reason": "empty"}) - items.append(item) - continue - - unsafe_reason = _unsafe_followup_reason(todo_text) - if unsafe_reason: - item.update({"skipped": True, "skipped_reason": f"unsafe_boundary:{unsafe_reason}"}) - items.append(item) - continue - - normalized = normalize_todo_text(todo_text) - if normalized in existing_texts or normalized in seen_texts: - item.update({"already_exists": True, "skipped": True, "skipped_reason": "duplicate"}) - items.append(item) - continue - - if recorded_count >= MAX_CAPTURED_FOLLOWUP_TODOS: - item.update({"skipped": True, "skipped_reason": "max_items_exceeded"}) - items.append(item) - continue - - add_result = add_todo_to_lines( - lines, - role="agent", - text=todo_text, - task_class=task_class or TODO_TASK_CLASS_ADVANCEMENT, - action_kind=action_kind, - required_write_scopes=required_write_scopes, - required_capabilities=required_capabilities, - target_capabilities=target_capabilities, - required_decision_scopes=required_decision_scopes, - evidence=evidence_text, - updated_at=updated_at, - ) - changed = changed or bool(add_result.get("added")) or bool(add_result.get("metadata_updated")) - recorded_count += 1 - seen_texts.add(normalized) - item.update(add_result) - items.append(item) - - if changed: - new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "") - new_text = replace_updated_at(new_text, updated_at) - if not dry_run: - write_captured_todo_state(capture, runtime_root=runtime_root, goal_id=goal_id, - state_path=resolved_state_file, text=new_text) - - result = { - "ok": True, - "dry_run": dry_run, - "changed": changed, - "goal_id": goal_id, - "role": "agent", - "section": TODO_SECTION_HEADINGS["agent"], - "state_file": str(resolved_state_file), - "project": str(resolved_project) if resolved_project else None, - "max_items": MAX_CAPTURED_FOLLOWUP_TODOS, - "requested_count": len(followups), - "recorded_count": recorded_count, - "skipped_count": sum(1 for item in items if item.get("skipped")), - "evidence": evidence_text, - "items": items, - "updated_at": updated_at if changed else None, - } - if changed and not dry_run: - from .control_plane.coordination.local_authority_shadow_observation import observe_local_authority_commit - - shadow = observe_local_authority_commit( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - observation_trigger=( - f"todo_capture_followups:{recorded_count}:{updated_at}" - ), - ) - if shadow is not None: - result["authority_shadow"] = shadow - return settle_todo_runtime_shadow_capture( - result, registry_path=registry_path, runtime_root=runtime_root, - goal_id=goal_id, write_class="todo_capture_followups", capture=capture, - observe_legacy=False, emit_disabled=False, - ) diff --git a/tests/control_plane/test_local_authority_shadow_cli_e2e.py b/tests/control_plane/test_local_authority_shadow_cli_e2e.py index e0c112464b..8773e2425f 100644 --- a/tests/control_plane/test_local_authority_shadow_cli_e2e.py +++ b/tests/control_plane/test_local_authority_shadow_cli_e2e.py @@ -423,19 +423,21 @@ def test_product_cli_runtime_root_override_keeps_one_candidate_lineage( "agent-a", ) assert updated["changed"] is True - followups = _cli( + second_add = _cli( registry, override_runtime, "todo", - "capture-followups", + "add", "--goal-id", goal_id, - "--follow-up", + "--role", + "agent", + "--text", "Verify that one goal keeps one candidate lineage.", "--evidence", - "validation://one-root-followup", + "validation://one-root-add", ) - assert followups["changed"] is True + assert second_add["added"] is True completed = _cli( registry, override_runtime, @@ -461,7 +463,7 @@ def test_product_cli_runtime_root_override_keeps_one_candidate_lineage( "todo add": added, "task-lease acquire": lease, "todo update": updated, - "todo capture-followups": followups, + "todo add (second)": second_add, "todo complete": completed, } for label, payload in responses.items(): diff --git a/tests/control_plane/test_local_authority_shadow_runtime.py b/tests/control_plane/test_local_authority_shadow_runtime.py index dccae6e59c..b848152971 100644 --- a/tests/control_plane/test_local_authority_shadow_runtime.py +++ b/tests/control_plane/test_local_authority_shadow_runtime.py @@ -18,7 +18,6 @@ AppendOnlyStateEventStore, make_state_event, ) -from loopx.todo_followups import capture_followup_todos from loopx.todos import ( add_goal_todo, archive_completed_todos, @@ -277,7 +276,7 @@ def test_enabled_task_lease_facades_shadow_only_committed_mutations( assert "authority_shadow" not in replayed_acquire -def test_handoff_mode_and_direct_followup_writers_refresh_the_same_shadow( +def test_handoff_mode_and_todo_add_refresh_the_same_shadow( tmp_path: Path, ) -> None: registry, _state, runtime_root = _fixture(tmp_path, enabled=True) @@ -287,21 +286,22 @@ def test_handoff_mode_and_direct_followup_writers_refresh_the_same_shadow( goal_id=GOAL_ID, mode="legacy", ) - followups = capture_followup_todos( + added = add_goal_todo( registry_path=registry, goal_id=GOAL_ID, - followups=["Verify the migrated authority projection."], - evidence="validation://local-shadow-followup", + role="agent", + text="Verify the migrated authority projection.", + task_class="advancement_task", ) assert mode["changed"] is True assert mode["authority_shadow"]["outcome"] == "captured" - assert followups["changed"] is True - assert followups["authority_shadow"]["outcome"] == "captured" + assert added["added"] is True + assert added["authority_shadow"]["outcome"] == "captured" head = _shadow_document(runtime_root)["head"] assert head["handoff_mode"] == "legacy" assert [todo["todo_id"] for todo in head["todos"]] == [ - followups["items"][0]["todo_id"] + added["todo_id"] ] diff --git a/tests/control_plane/test_runtime_shadow_bounded_e2e.py b/tests/control_plane/test_runtime_shadow_bounded_e2e.py index 6a7c46f864..860de58588 100644 --- a/tests/control_plane/test_runtime_shadow_bounded_e2e.py +++ b/tests/control_plane/test_runtime_shadow_bounded_e2e.py @@ -209,14 +209,16 @@ def test_snapshot_changed_between_python_builder_and_native_inspection_is_reject assert result["reason_code"] == "source_changed_retry" -def test_public_handoff_followups_and_monitor_successor_capture_each_primary_mutation(tmp_path: Path) -> None: +def test_public_handoff_todo_and_monitor_successor_capture_each_primary_mutation(tmp_path: Path) -> None: registry, runtime, _state = workspace(tmp_path) enable(registry) cli(registry, runtime, "coordination-shadow", "bootstrap", "--goal-id", "goal-a", "--execute") cli(registry, runtime, "handoff-mode", "set", "--goal-id", "goal-a", "--mode", "soft_claim") assert len(history(tmp_path, runtime)) == 2 - cli(registry, runtime, "todo", "capture-followups", "--goal-id", "goal-a", - "--follow-up", "First retained followup", "--follow-up", "Second retained followup", "--evidence", "validation://followups") + cli( + registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", + "--text", "Validate the retained projection", "--evidence", "validation://todo-add", + ) assert len(history(tmp_path, runtime)) == 3 monitor = cli(registry, runtime, "todo", "add", "--goal-id", "goal-a", "--role", "agent", "--text", "Observe the public release", "--task-class", "continuous_monitor", "--action-kind", "monitor", @@ -231,11 +233,11 @@ def test_public_handoff_followups_and_monitor_successor_capture_each_primary_mut "--next-continuation-policy", "same_agent_non_delivery", "--next-claimed-by", "agent-a", "--execute") assert len(result["successor_todo_ids"]) == 1 transactions = history(tmp_path, runtime) - assert len(transactions) == 6 # Baseline, handoff, followup batch, monitor add, observation update, successor add. + assert len(transactions) == 6 # Baseline, handoff, todo add, monitor add, observation update, successor add. receipts = [transaction["receipts"][0] for transaction in transactions[1:]] assert len({receipt["entry_id"] for receipt in receipts}) == 5 assert [receipt["seq"] for receipt in receipts] == [1, 2, 3, 4, 5] - assert {receipt["write_class"] for receipt in receipts} >= {"handoff_mode_set", "todo_capture_followups", "todo_add", "todo_update"} + assert {receipt["write_class"] for receipt in receipts} >= {"handoff_mode_set", "todo_add", "todo_update"} qualified = cli(registry, runtime, "coordination-shadow", "qualify", "--goal-id", "goal-a", "--minimum-operations", "5") assert qualified["qualification"]["qualified"] is True assert qualified["qualification"]["evidence"]["operation_count"] == 5 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 ae1d489bd3..db803da2d7 100644 --- a/tests/control_plane/test_shadow_fence_caller_parity_e2e.py +++ b/tests/control_plane/test_shadow_fence_caller_parity_e2e.py @@ -175,10 +175,6 @@ def row_args(ws: Workspace, caller: str) -> tuple[str, ...]: "--text", "Parity successor", "--evidence", "validation://parity"), "todo_archive_completed_execute": ("todo", "archive-completed", "--execute"), "todo_archive_completed_preview": ("todo", "archive-completed"), - "todo_capture_followups": ("todo", "capture-followups", "--follow-up", "Parity follow-up.", - "--evidence", "parity fixture"), - "todo_capture_followups_dry_run": ("todo", "capture-followups", "--follow-up", "Parity follow-up.", - "--evidence", "parity fixture", "--dry-run"), "handoff_mode_set": ("handoff-mode", "set", "--mode", "soft_claim"), "todo_complete_dry_run_gate": ("todo", "complete", "--todo-id", gate, "--agent-id", "agent-a", "--decision-outcome", "approve", "--evidence", "validation://parity", diff --git a/tests/control_plane/test_shadow_observable_e2e.py b/tests/control_plane/test_shadow_observable_e2e.py index 9f010f3bcf..d4ecce056c 100644 --- a/tests/control_plane/test_shadow_observable_e2e.py +++ b/tests/control_plane/test_shadow_observable_e2e.py @@ -120,17 +120,8 @@ def test_todo_argument_intent_and_rejections(caller: Caller) -> None: assert 'Corrected operator intent' in w.state.read_text() -def test_handoff_followup_preview_batch_and_quiescence(caller: Caller) -> None: +def test_handoff_mode_quiescence(caller: Caller) -> None: w = caller - args = ('todo', 'capture-followups', '--follow-up', 'First bounded followup', - '--follow-up', 'Second bounded followup', '--evidence', 'validation://followups') - before = w.primary() - assert w.call(*args, '--dry-run')['recorded_count'] == 2 - assert w.primary() == before - assert w.call(*args)['recorded_count'] == 2 - before = w.primary() - assert w.call(*args)['recorded_count'] == 0 - assert w.primary() == before assert w.call('handoff-mode', 'set', '--mode', 'hard_lease')['changed'] is True before = w.primary() assert w.call('handoff-mode', 'set', '--mode', 'hard_lease')['changed'] is False diff --git a/tests/control_plane/test_shadow_writer_boundaries.py b/tests/control_plane/test_shadow_writer_boundaries.py index bfdc48b11e..716badd2c6 100644 --- a/tests/control_plane/test_shadow_writer_boundaries.py +++ b/tests/control_plane/test_shadow_writer_boundaries.py @@ -15,7 +15,7 @@ legacy_todo_write_transaction, ) from loopx.control_plane.todos.handoff_mode import set_goal_handoff_mode -from loopx.todo_followups import capture_followup_todos +from loopx.todos import add_goal_todo GOAL = "writer-boundary" @@ -42,9 +42,8 @@ def fixture(tmp_path: Path) -> tuple[Path, Path, Path]: return registry, state, root -@pytest.mark.parametrize("writer", ["handoff", "followups"]) -def test_omitted_writers_refuse_a_fence_before_primary( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, writer: str, +def test_handoff_writer_refuses_a_fence_before_primary( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: registry, state, root = fixture(tmp_path) fence = legacy_coordination_writer_fence_path(runtime_root=root, goal_id=GOAL) @@ -58,13 +57,7 @@ def test_omitted_writers_refuse_a_fence_before_primary( ) before = state.read_bytes() with pytest.raises(LegacyCoordinationWriterFenced): - if writer == "handoff": - set_goal_handoff_mode(registry_path=registry, goal_id=GOAL, mode="soft_claim") - else: - capture_followup_todos( - registry_path=registry, goal_id=GOAL, - followups=["Review the durable boundary."], evidence="review fixture", - ) + set_goal_handoff_mode(registry_path=registry, goal_id=GOAL, mode="soft_claim") assert state.read_bytes() == before assert not (root / "authority-shadow").exists() @@ -213,7 +206,7 @@ def cli(registry: Path, *args: str) -> dict: return json.loads(result.stdout) -def test_real_cli_handoff_and_followup_batch_have_one_receipt_each(tmp_path: Path) -> None: +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"] = { @@ -222,21 +215,17 @@ def test_real_cli_handoff_and_followup_batch_have_one_receipt_each(tmp_path: Pat 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") - followed = cli(registry, "todo", "capture-followups", "--goal-id", GOAL, - "--follow-up", "Inspect the read path.", "--follow-up", "Inspect the write path.", + 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 followed["coordination_runtime_shadow"]["outcome"] == "delivered", followed - assert followed["recorded_count"] == 2 + 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() - noop = cli(registry, "todo", "capture-followups", "--goal-id", GOAL, - "--follow-up", "Inspect the read path.", "--evidence", "review fixture") - assert noop["changed"] is False - assert json.loads((root / "authority-shadow" / "file-v0" / f"authority-store-{digest}.json").read_text())["cursor"] == "3" @pytest.mark.parametrize("phase", ["before", "after"]) @@ -313,7 +302,7 @@ def traced(path, **kwargs): 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", "capture-followups", "--goal-id", GOAL, "--follow-up", "Must be fenced.", + "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) @@ -359,8 +348,8 @@ def paused(*args): 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", "capture-followups", "--goal-id", GOAL, - "--follow-up", "Primary won the lock.", "--evidence", "ordering fixture"], + "--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 @@ -381,7 +370,7 @@ def paused(*args): assert engager.poll() is None output, error = writer.communicate("continue\n", timeout=30) assert writer.returncode == 0, output + error - assert json.loads(output)["recorded_count"] == 1 + 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() @@ -408,8 +397,10 @@ def fail_primary(source: object, target: object) -> None: original(source, target) monkeypatch.setattr(active_state_editing.os, "replace", fail_primary) with pytest.raises(OSError, match="primary replace refused"): - capture_followup_todos(registry_path=registry, goal_id=GOAL, - followups=["Must stay prepared."], evidence="replace failure") + 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 @@ -454,13 +445,17 @@ def test_override_root_is_the_only_maintenance_authority(tmp_path: Path) -> None management.parent.mkdir(parents=True) management.write_text("{}") with pytest.raises(ShadowManagementError): - capture_followup_todos(registry_path=registry, goal_id=GOAL, - runtime_root_arg=str(override), followups=["Hold override."], evidence="root fixture") + 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 = capture_followup_todos(registry_path=registry, goal_id=GOAL, - followups=["Default root remains writable."], evidence="root fixture") - assert result["recorded_count"] == 1 + 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"]) @@ -475,8 +470,10 @@ def test_override_root_cannot_bypass_registry_source_maintenance(tmp_path: Path, before = state.read_bytes() with pytest.raises(ShadowManagementError): if writer == "todo": - capture_followup_todos(registry_path=registry, goal_id=GOAL, - runtime_root_arg=str(override), followups=["Cannot bypass source maintenance."], evidence="root fixture") + add_goal_todo( + registry_path=registry, goal_id=GOAL, runtime_root_arg=str(override), + role="agent", text="Cannot bypass source maintenance.", + ) else: refresh_state_run(registry_path=registry, runtime_root_override=str(override), goal_id=GOAL, project=None, state_file=None, classification="continue", recommended_action="Continue inspection.", @@ -532,7 +529,7 @@ def traced(path, **kwargs): """ 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", "capture-followups", "--goal-id", GOAL, "--follow-up", "Must observe the new source binding.", + "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 @@ -638,23 +635,7 @@ def refuse_prepare(path: Path, record: object) -> None: assert list(directory.glob("*.committed.json")) == [] -def test_public_preview_does_not_require_primary_write_permission(tmp_path: Path) -> None: - from loopx.control_plane.coordination.shadow_management import shadow_management_state_path - registry, state, root = fixture(tmp_path) - before = state.read_bytes() - management = shadow_management_state_path(root, GOAL) - management.parent.mkdir(parents=True) - management.write_text("{}") - fence = legacy_coordination_writer_fence_path(runtime_root=root, goal_id=GOAL) - fence.write_text("{invalid") - preview = cli(registry, "todo", "capture-followups", "--goal-id", GOAL, - "--follow-up", "Preview remains read-only.", "--evidence", "preview fixture", "--dry-run") - assert preview["dry_run"] is True - assert state.read_bytes() == before - assert not (root / "authority-shadow").exists() - - -@pytest.mark.parametrize("operation", ["add", "update", "complete", "supersede", "archive", "followups"]) +@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: @@ -694,10 +675,8 @@ def fail_prepare(path: Path, record: object) -> None: 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") - elif operation == "archive": - todos.archive_completed_todos(**identity, max_active_done=0, dry_run=False) else: - capture_followup_todos(**identity, followups=["Capture another owner."], evidence="Boundary fixture.") + 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 diff --git a/tests/control_plane/test_shadow_writer_variant_e2e.py b/tests/control_plane/test_shadow_writer_variant_e2e.py index e26c156892..bb40dd0807 100644 --- a/tests/control_plane/test_shadow_writer_variant_e2e.py +++ b/tests/control_plane/test_shadow_writer_variant_e2e.py @@ -113,10 +113,10 @@ def test_other_goal_cannot_write_a_protected_goal_source_via_state_override(tmp_ "coordination": {"registered_agents": ["agent-a"]}}) ws.registry.write_text(json.dumps(registry)) # Preserve the existing opt-out contract before any source authority exists. - legacy = public(ws, "todo", "capture-followups", "--state-file", str(ws.state), - "--follow-up", "An unbound shared-state write remains supported.", + 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["recorded_count"] == 1, legacy + assert legacy["ok"] is True and legacy["added"] is True, legacy if authority == "active_capture": # A registered source stays protected even without a frontmatter owner. ws.state.write_text(ws.state.read_text().replace(f"goal_id: {ws.goal}\n", "")) @@ -125,8 +125,8 @@ def test_other_goal_cannot_write_a_protected_goal_source_via_state_override(tmp_ result = subprocess.run(fence_command(ws), cwd=REPO, capture_output=True, text=True, check=True, timeout=20) assert json.loads(result.stdout)["status"] == "applied" before = ws.state.read_bytes(), other_state.read_bytes() - result = public(ws, "todo", "capture-followups", "--state-file", str(ws.state), - "--follow-up", "Must not bypass another goal's source authority.", + result = public(ws, "todo", "add", "--state-file", str(ws.state), "--role", "agent", + "--text", "Must not bypass another goal's source authority.", "--evidence", "Cross-goal source boundary.", goal="goal-other") assert not result.get("ok"), json.dumps(result, indent=2) expected = "shadow_source_goal_mismatch" if authority == "active_capture" else "legacy_coordination_writer_fenced" @@ -134,8 +134,8 @@ def test_other_goal_cannot_write_a_protected_goal_source_via_state_override(tmp_ assert (ws.state.read_bytes(), other_state.read_bytes()) == before assert not (ws.runtime / "authority-shadow" / "outbox" / "goal-other").exists() if authority == "active_capture": - unknown = public(ws, "todo", "capture-followups", "--state-file", str(ws.state), - "--follow-up", "An unregistered goal cannot bypass source ownership.", + unknown = public(ws, "todo", "add", "--state-file", str(ws.state), "--role", "agent", + "--text", "An unregistered goal cannot bypass source ownership.", "--evidence", "Unregistered goal control.", goal="unregistered-goal") assert not unknown.get("ok"), unknown assert (ws.state.read_bytes(), other_state.read_bytes()) == before @@ -292,8 +292,11 @@ def test_native_fence_waits_for_public_prose_and_then_blocks_todo_writes(tmp_pat assert finish(prose, resume=True)["ok"] is True assert finish(fence)["status"] == "applied" before = ws.state.read_bytes() - result = public(ws, "todo", "capture-followups", "--follow-up", "Must now be fenced.", "--evidence", "Boundary check.") - assert result["error_code"] == "legacy_coordination_writer_fenced", result + result = public( + ws, "todo", "add", "--role", "agent", "--text", "Must now be fenced.", + "--evidence", "Boundary check.", + ) + assert result["error_code"] == "local_authority_todo_list_unavailable", result assert ws.state.read_bytes() == before assert "Prose committed before the fence." in ws.state.read_text() assert len(index.read_text().splitlines()) == 2 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 6093a190c5..ea3bb7bcec 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 @@ -1424,50 +1424,6 @@ } } }, - { - "id": "cli-todo_capture_followups-engaged", - "surface": "cli", - "workspace": "w1", - "caller": "todo_capture_followups", - "fence_state": "engaged", - "exit": 1, - "expect": { - "ok": false, - "dry_run": false, - "added": false, - "already_exists": false, - "goal_id": "observable", - "role": null, - "todo": "", - "error": "legacy coordination writer is fenced; use the promoted canonical authority (file_v0) for goal observable; fence caller-fixture; the primary record was not changed", - "error_code": "legacy_coordination_writer_fenced", - "write_check": { - "schema_version": "loopx_legacy_coordination_write_check_result_v0", - "status": "blocked", - "reason_code": "legacy_coordination_writer_fenced", - "authority_mode": "file_v0", - "fence_id": "caller-fixture" - } - }, - "effect": { - "added": [], - "removed": [], - "changed": [] - }, - "outbox_added": [], - "baseline": { - "note": "recorded through the real CLI against each revision", - "0fb497af8": { - "ok": true - }, - "ee1b17217": { - "ok": false, - "error": "legacy coordination writer is fenced; use the canonical file authority", - "error_code": "legacy_coordination_writer_fenced", - "schema_version": "loopx_legacy_coordination_write_check_result_v0" - } - } - }, { "id": "cli-handoff_mode_set-canonical-quiescence", "surface": "cli", @@ -1543,35 +1499,6 @@ }, "match": "subset" }, - { - "id": "cli-todo_capture_followups_dry_run-engaged", - "surface": "cli", - "workspace": "w1", - "caller": "todo_capture_followups_dry_run", - "fence_state": "engaged", - "exit": 0, - "expect": { - "ok": true, - "dry_run": true, - "changed": true - }, - "effect": { - "added": [], - "removed": [], - "changed": [] - }, - "outbox_added": [], - "baseline": { - "note": "previews skip every fence check and write nothing; successful output holds workspace paths and timestamps, so only the outcome keys are pinned", - "0fb497af8": { - "ok": true - }, - "ee1b17217": { - "ok": true - } - }, - "match": "subset" - }, { "id": "cli-todo_complete_dry_run_gate-engaged", "surface": "cli", @@ -1976,39 +1903,6 @@ }, "match": "subset" }, - { - "id": "cli-todo_capture_followups-invalid", - "surface": "cli", - "workspace": "w3", - "caller": "todo_capture_followups", - "fence_state": "invalid", - "exit": 1, - "expect": { - "ok": false, - "dry_run": false, - "added": false, - "already_exists": false, - "goal_id": "observable", - "role": null, - "todo": "", - "error": "legacy coordination writer fence must be engaged", - "error_code": "legacy_writer_fence_read_failed", - "write_check": { - "schema_version": "loopx_legacy_coordination_write_check_result_v0", - "status": "failed", - "reason_code": "legacy_writer_fence_read_failed", - "reason": "legacy coordination writer fence must be engaged", - "authority_mode": "unknown_fail_closed" - } - }, - "effect": { - "added": [], - "removed": [], - "changed": [] - }, - "outbox_added": [], - "baseline": null - }, { "id": "cli-task_lease_acquire-unreadable", "surface": "cli", @@ -2070,39 +1964,6 @@ }, "match": "subset" }, - { - "id": "cli-todo_capture_followups-unreadable", - "surface": "cli", - "workspace": "w4", - "caller": "todo_capture_followups", - "fence_state": "unreadable", - "exit": 1, - "expect": { - "ok": false, - "dry_run": false, - "added": false, - "already_exists": false, - "goal_id": "observable", - "role": null, - "todo": "", - "error": "EISDIR: illegal operation on a directory, read", - "error_code": "legacy_writer_fence_read_failed", - "write_check": { - "schema_version": "loopx_legacy_coordination_write_check_result_v0", - "status": "failed", - "reason_code": "legacy_writer_fence_read_failed", - "reason": "EISDIR: illegal operation on a directory, read", - "authority_mode": "unknown_fail_closed" - } - }, - "effect": { - "added": [], - "removed": [], - "changed": [] - }, - "outbox_added": [], - "baseline": null - }, { "id": "cli-task_lease_acquire-fence_without_store", "surface": "cli", diff --git a/tests/test_cli_argument_diagnostics.py b/tests/test_cli_argument_diagnostics.py index 3ae8f61ab9..2b2b966a43 100644 --- a/tests/test_cli_argument_diagnostics.py +++ b/tests/test_cli_argument_diagnostics.py @@ -12,7 +12,6 @@ from loopx.cli_commands.todo_argument_validation import ( validate_todo_add_options, validate_todo_archive_completed_options, - validate_todo_capture_followups_options, validate_todo_claim_options, validate_todo_complete_options, validate_todo_list_options, @@ -288,8 +287,6 @@ def test_todo_list_validation_accepts_read_filters() -> None: "Continue.", "--decision-outcome", "approve", - "--follow-up", - "Later.", ], "todo add does not accept --decision-outcome; record it on completion", ), @@ -943,11 +940,6 @@ def test_todo_supersede_validation_accepts_successor_creation() -> None: ["--no-follow-up"], "todo archive-completed does not support --no-follow-up", ), - ( - ["--follow-up", "Continue."], - "todo archive-completed does not support --follow-up; " - "use `todo capture-followups`", - ), ( ["--successor-todo-id", "todo_successor"], "todo archive-completed does not support --successor-todo-id", @@ -1033,72 +1025,18 @@ def test_todo_suggest_validation_accepts_suggestion_scope_options() -> None: validate_todo_suggest_options(args) -@pytest.mark.parametrize( - ("extra_args", "expected"), - [ - ( - ["--role", "agent"], - "todo capture-followups always records agent todos; do not pass --role", - ), - ( - ["--claimed-by", "codex-example"], - "todo capture-followups writes unclaimed todos; do not pass --claimed-by", - ), - ( - ["--todo-id", "todo_example", "--note", "not accepted"], - "todo capture-followups only accepts --goal-id, --follow-up, optional " - "--text shorthand, --evidence, routing metadata, --project, --state-file, " - "and --dry-run; unsupported: --todo-id, --note", - ), - ], -) -def test_todo_capture_followups_validation_preserves_exact_diagnostics( - extra_args: list[str], - expected: str, +def test_todo_capture_followups_is_not_a_registered_command( + capsys: pytest.CaptureFixture[str], ) -> None: - args = build_parser().parse_args( - ["todo", "capture-followups", "--goal-id", "example-goal", *extra_args] - ) - - with pytest.raises(ValueError) as exc_info: - validate_todo_capture_followups_options(args) - - assert str(exc_info.value) == expected - - -def test_todo_capture_followups_validation_accepts_routing_options() -> None: - args = build_parser().parse_args( - [ - "todo", - "capture-followups", - "--goal-id", - "example-goal", - "--follow-up", - "Continue.", - "--text", - "Then validate.", - "--evidence", - "tests/test_cli_argument_diagnostics.py", - "--task-class", - "advancement_task", - "--action-kind", - "implement", - "--continuation-policy", - "same_agent_non_delivery", - "--required-write-scope", - "tests/**", - "--required-capability", - "shell", - "--target-capability", - "quality", - "--required-decision-scope", - "merge", - "--state-file", - "ACTIVE_GOAL_STATE.md", - ] - ) + with pytest.raises(SystemExit) as exc_info: + build_parser().parse_args( + ["todo", "capture-followups", "--goal-id", "example-goal"] + ) - validate_todo_capture_followups_options(args) + assert exc_info.value.code == 2 + diagnostic = capsys.readouterr().err + assert "invalid choice" in diagnostic + assert "capture-followups" in diagnostic @pytest.mark.parametrize( From bc3e0c63ad2dd8e63aae2f3e734c37184835b131 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:41:30 +0800 Subject: [PATCH 3/4] docs: record todo command retirement boundary Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../rfcs/human-attention-wishlist-v0.md | 16 +++++++------- .../rfcs/human-attention-wishlist-v0.zh-CN.md | 9 +++++--- ...shared-goal-authority-state-provider-v0.md | 21 +++++++++++++------ ...-goal-authority-state-provider-v0.zh-CN.md | 15 +++++++++---- .../typescript-control-plane-migration-v0.md | 10 +++++++++ ...script-control-plane-migration-v0.zh-CN.md | 8 +++++++ .../protocols/todo-suggestion-prompt-v0.md | 6 ++++++ 7 files changed, 65 insertions(+), 20 deletions(-) diff --git a/docs/architecture/rfcs/human-attention-wishlist-v0.md b/docs/architecture/rfcs/human-attention-wishlist-v0.md index 57e2d67deb..a8a0a2e3ce 100644 --- a/docs/architecture/rfcs/human-attention-wishlist-v0.md +++ b/docs/architecture/rfcs/human-attention-wishlist-v0.md @@ -59,7 +59,8 @@ The current seams do not compose into that outcome: - an open `user_action` can enter the user notification channel even when it is non-blocking; - `todo suggest` creates a read-only candidate queue that requires later - promotion, while `todo capture-followups` writes only agent work; + promotion; the retired `todo capture-followups` command wrote only agent + work and never provided a human-wish route; - the compact turn envelope carries required execution and writeback actions, but no signed optional sidecar hint. @@ -168,10 +169,11 @@ It must: `duplicate_updated` result; - perform no quota spend and claim no delivery progress by itself. -The exact command name is open to implementation review. The behavior above is -the contract; extending `todo capture-followups` is acceptable only if it keeps -agent follow-up and human-wish routing explicit and cannot silently change the -role or task class. +The exact command shape is open to implementation review. The behavior above +is the contract. The retired `todo capture-followups` batch command is not an +extension point; a future implementation must use a typed wish-specific helper +or an explicit canonical `todo add` option that cannot silently change role or +task class. ## 5. Skill and Heartbeat Generation Rule @@ -400,8 +402,8 @@ contract. ## 14. Open Questions -1. Should the helper be `todo capture-wishes`, or should the existing - `capture-followups` command accept an explicit destination kind? +1. Should the helper be `todo capture-wishes`, or should canonical `todo add` + accept an explicit human-attention kind? 2. Should v0 cap active wishes per agent, per goal, or both? 3. Should piggyback delivery be part of the initial slice, or should the first implementation expose wishes only through status/review packets? diff --git a/docs/architecture/rfcs/human-attention-wishlist-v0.zh-CN.md b/docs/architecture/rfcs/human-attention-wishlist-v0.zh-CN.md index e097574803..664fd1f365 100644 --- a/docs/architecture/rfcs/human-attention-wishlist-v0.zh-CN.md +++ b/docs/architecture/rfcs/human-attention-wishlist-v0.zh-CN.md @@ -40,7 +40,8 @@ LoopX 已经区分阻塞性的 `user_gate` todo 与非阻塞的 `user_action` to - heartbeat 指南要求记录高价值候选,却没有定义 wishlist 写命令或生命周期; - `todo_write_hint` 提供 gate、user-action 和 agent-todo 模板,却没有“不通知的可选人类请求”模板; - 一个打开的 `user_action` 即使非阻塞,也可能进入用户通知通道; -- `todo suggest` 只产生只读候选队列,还需要后续 promotion;`todo capture-followups` 则只写 agent work; +- `todo suggest` 只产生只读候选队列,还需要后续 promotion;已退役的 + `todo capture-followups` 只写 agent work,从未提供 human wish 路由; - compact turn envelope 带有必须执行的动作和写回,却没有签名过的可选 sidecar 提示。 结果是一种可以避免的生产偏差:agent 要么把可选价值升级成 blocker,要么制造提醒噪音,要么遗忘它。 @@ -124,7 +125,9 @@ loopx todo capture-wishes \ - 限制每个 agent 的活跃 wish 数,并返回 typed `max_items_exceeded` 或 `duplicate_updated` 结果; - 自身不 spend quota,也不声明 delivery progress。 -精确命令名留给实现评审。以上行为才是协议;只有在能保持 agent follow-up 与 human wish 路由显式、且不会静默改变 role/task class 时,才可选择扩展 `todo capture-followups`。 +精确命令形态留给实现评审。以上行为才是协议。已退役的 +`todo capture-followups` 批量命令不再作为扩展点;未来实现必须使用 wish 专属的 +typed helper,或为 canonical `todo add` 增加不会静默改变 role/task class 的显式选项。 ## 5. Skill 与 Heartbeat 生成规则 @@ -292,7 +295,7 @@ v0 拒绝。它会扩大每个 task-class switch、CLI validator、state project ## 14. 开放问题 -1. Helper 应命名为 `todo capture-wishes`,还是让现有 `capture-followups` 接受显式 destination kind? +1. Helper 应命名为 `todo capture-wishes`,还是让 canonical `todo add` 接受显式 human-attention kind? 2. v0 应按 agent、按 goal,还是同时限制 active wish? 3. Piggyback 呈现应进入初始切片,还是第一版只通过 status/review packet 暴露 wish? 4. 在专用 typed outcome 出现前,哪一个 public-safe lifecycle field 最适合记录用户的显式 decline? diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md index de7e65bd5f..db914d158a 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -2107,16 +2107,16 @@ Per stage, this increment implements: failures, and zero skips. - Stage 2C observation foundation: seven `s2c1.*` rows port the local-shadow CLI E2E and migration assertions and pin the single-lineage guarantee. The configure round trip previews, enables, - reads back, and disables the observer; every writer family (handoff-mode, - todo add/update/complete/supersede/capture-followups/archive-completed, + reads back, and disables the observer; every retained writer family (handoff-mode, + todo add/update/complete/supersede/archive-completed, task-lease acquire/renew/transfer) captures with `primary_writeback_preserved`, `provider_to_local_writes=false`, and `candidate_read_for_decision=false`, while an idempotent re-acquire does not observe; default-off goals stay isolated; candidate failure preserves the primary commit; a POSIX SIGKILL in the crash gap loses only that observation; a `--runtime-root` override that differs from - `common_runtime_root` keeps todo add, task-lease acquire, todo update, - follow-up capture, and a leased completion in one store identity while the + `common_runtime_root` keeps two todo adds, task-lease acquire, todo update, + and a leased completion in one store identity while the registry root gains neither a candidate lineage nor lease state; and `migrate-state` seeds a fresh lineage without legacy bytes. - Stage 2C parity half: ten `s2c2.*` rows drive one explicitly enabled @@ -2133,8 +2133,8 @@ Per stage, this increment implements: as `bootstrap_required`, rebootstraps a fresh lineage and replays; three cycles of interleaved writers (add, note update with a no-change repeat, explicit exclusion set and clear with a no-change repeat, acquire, renew, - transfer, leased complete and supersede with their fence closes, - capture-followups) keep every bounded qualification matched with + transfer, leased complete and supersede with their fence closes, and a + second add) keep every bounded qualification matched with `sustained_parity_verdict=not_evaluated`; a direct primary edit reports `shadow_projection_drift`, a later write holds on `source_partition_continuity_unproved`, and only rollback plus rebootstrap @@ -2802,6 +2802,15 @@ one-way projections. Do not add a third TS-Markdown backend, bidirectional live synchronization, or per-command split authority. Unsupported post-cutover commands fail closed; they do not fall back to the old writer. +The 2026-09-19 command-retirement checkpoint removes the unconsumed +`coordination.local_authority.mutate` and Todo compatibility-edit execution +wrappers while retaining `prepareCoordinationProjectionCommit` and the shared +reducer used by live domain transactions. It also removes the unrelated public +`todo capture-followups` product command. That command retirement does not +remove, weaken, or rename the runtime shadow-capture mechanism described here. +`todo suggest` remains a manual read-only discovery entrypoint and is not part +of provider promotion qualification. + #### Refactoring roadmap overview The Monitor state owner now lives in TS and is composed with authoring scope, diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md index 33c3aa55b6..73a6328487 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md @@ -1678,14 +1678,14 @@ CLI runner、observation-lock 窗口、候选回读)、只读 TypeScript 探 PostgreSQL 集成测试文件,要求至少九个 pass、零 fail、零 skip。 - Stage 2C 观察基础:七个 `s2c1.*` 行移植本地 shadow CLI E2E 与迁移断言,并钉住单一 lineage 保证。 - configure 往返先预览、再开启、回读、最后关闭 observer;每个 writer family - (handoff-mode、todo add/update/complete/supersede/capture-followups/ + configure 往返先预览、再开启、回读、最后关闭 observer;每个保留的 writer family + (handoff-mode、todo add/update/complete/supersede/ archive-completed、task-lease acquire/renew/transfer)都以 `primary_writeback_preserved`、`provider_to_local_writes=false`、 `candidate_read_for_decision=false` 完成 capture,而幂等 re-acquire 不产生 observation;default-off goal 保持隔离;候选失败不推翻主写;POSIX SIGKILL 落在崩溃间隙时只丢失该次 observation;`--runtime-root` 与 `common_runtime_root` - 不同时,todo add、task-lease acquire、todo update、follow-up 捕获与带 lease 的 + 不同时,两次 todo add、task-lease acquire、todo update 与带 lease 的 complete 仍落入同一个 store identity,registry root 既不产生候选 lineage 也不 产生 lease 状态;`migrate-state` 在不携带 legacy 字节的前提下建立新 lineage。 - Stage 2C parity 后半段:十个 `s2c2.*` 行只通过公开 CLI 驱动一个显式开启 @@ -1699,7 +1699,7 @@ CLI runner、observation-lock 窗口、候选回读)、只读 TypeScript 探 条目、把 capture 置于 `bootstrap_required`、重新 bootstrap 出新 lineage 并可重放; 三轮交错 writer(add、note update 及其无变化重复、显式 exclusion 设置与清除及其 无变化重复、acquire、renew、transfer、带 lease 的 complete 与 supersede 及其 - fence close、capture-followups)让每次有界 qualification 都保持 matched,且 + fence close,以及第二次 add)让每次有界 qualification 都保持 matched,且 `sustained_parity_verdict=not_evaluated`; 直接改主文件会报告 `shadow_projection_drift`,其后的写入以 `source_partition_continuity_unproved` 挂起,只有 rollback 加重新 bootstrap 才能恢复; @@ -2223,6 +2223,13 @@ CLI / Agent / Dashboard → 唯一 TS Todo 事务 owner → canonical authority 供数;cutover 后,所选 canonical provider 向单向投影供数。不增加第三种 TS-Markdown backend、实时双向同步或按命令拆开的权威;晋升后不支持的命令 fail closed,不能回退旧 writer。 +2026-09-19 命令退役检查点删除了无人消费的 +`coordination.local_authority.mutate` 与 Todo compatibility-edit 执行包装,同时保留 +实际领域事务仍复用的 `prepareCoordinationProjectionCommit` 与共享 reducer;另行删除 +无关的公开产品命令 `todo capture-followups`。后一个命令的退役不删除、不削弱、也不 +重命名本 RFC 的 runtime shadow-capture 机制。`todo suggest` 继续作为人工触发的只读 +发现入口,不纳入 provider promotion 资格。 + #### 重构主线总览 Monitor 状态 owner 现位于 TS,并与 authoring scope、external-wait 校验及字段更新 diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index bbd8e37082..9d3a6848ce 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -975,6 +975,16 @@ readers but does not finish Todo writers, retention/compaction or promotion. **T4 — collect full-writer retirement after durability cutover.** +- The 2026-09-19 command audit retires two already-typed but unconsumed + execution surfaces: `coordination.local_authority.todo_compatibility_edit` + and `coordination.local_authority.mutate`. Shared projection reduction and + commit preparation remain because live claim, lease, update, archive, + monitor, and team-plan transactions use them. The same audit retires the + unused public `todo capture-followups` batch command instead of migrating it; + ordinary `todo add` remains available but is not claimed to preserve the + retired command's atomic batch, deduplication, or replay contract. The + read-only, manually invoked `todo suggest` surface remains, but is not a + provider-default prerequisite. - Depends on T1–T3 and the shared RFC's [D1–D3](shared-goal-authority-state-provider-v0.md#durability-execution-cards), including owner approval and the explicit legacy migration window. Search remaining imports and public command routes before deleting old Markdown business writers, diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md index 98d69fabd3..d5dfcf50db 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md @@ -757,6 +757,14 @@ T3/D1 reader,未完成全部 Todo writer、retention/compaction 或 promotion **T4 — durable cutover 后兑现完整 writer 删除。** +- 2026-09-19 命令审计退役两条已经 typed、但没有实际消费者的执行面: + `coordination.local_authority.todo_compatibility_edit` 与 + `coordination.local_authority.mutate`。claim、lease、update、archive、monitor + 和 team-plan 事务仍复用 projection reduction 与 commit preparation,因此这些 + 公共内核保留。同一审计直接退役无实际调用的公开批量命令 + `todo capture-followups`,不再为它继续迁移;普通 `todo add` 仍可用,但不宣称保留 + 已退役命令的批量原子性、去重或 replay 合同。人工触发、只读的 `todo suggest` + 保留,但不是 provider 默认切换的前置条件。 - 前提是 T1–T3 和 shared RFC 的 [D1–D3](shared-goal-authority-state-provider-v0.zh-CN.md#持久化执行卡),包括 owner 批准及明确的 legacy 迁移窗口。 搜索剩余 import 和公开路由后,删除旧 Markdown 业务 writer、capture-only adapter、 重复 reference aggregate。 diff --git a/docs/reference/protocols/todo-suggestion-prompt-v0.md b/docs/reference/protocols/todo-suggestion-prompt-v0.md index 94d0a13ea4..559d6f9993 100644 --- a/docs/reference/protocols/todo-suggestion-prompt-v0.md +++ b/docs/reference/protocols/todo-suggestion-prompt-v0.md @@ -9,6 +9,12 @@ frequency limits. The project agent reads the current repo and returns `suggested_todos`; those candidates are not formal LoopX todos until the user or primary controller promotes one. +This is intentionally a manual, read-only discovery surface. It is retained +even without automatic product callers because an explicit operator request is +its product entrypoint. It is not a provider-default or TypeScript-migration +prerequisite, and it should not gain a dedicated write path unless a real +replacement interaction is first accepted. + ## Command ```bash From 8f780185079aafa20b163a525b9d6d6e1ba099c8 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:19:51 +0800 Subject: [PATCH 4/4] test: rebind retired fence remediation mutant Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- examples/shared-goal-authority-e2e/mutants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/shared-goal-authority-e2e/mutants.py b/examples/shared-goal-authority-e2e/mutants.py index c4bbd31fa0..9a457186a1 100644 --- a/examples/shared-goal-authority-e2e/mutants.py +++ b/examples/shared-goal-authority-e2e/mutants.py @@ -339,7 +339,7 @@ def apply(source: str) -> str: (COORDINATION + "legacy_writer_fence.py", replacement( 'LEGACY_WRITER_FENCED_REMEDIATION = (\n "legacy coordination writer is fenced; use the promoted canonical authority "\n "({authority_mode}) for goal {goal_id}; fence {fence_id}; "\n "the primary record was not changed"\n)', 'LEGACY_WRITER_FENCED_REMEDIATION = "legacy coordination writer is fenced"')), -), "tests/control_plane/test_shadow_fence_caller_parity_e2e.py::test_fence_caller_parity[cli-todo_update_status-engaged]")) +), "tests/control_plane/test_legacy_coordination_writer_fence.py::test_present_fence_delegates_to_typescript_and_blocks")) CASES.append(Case("fence_envelope_schema_leak", ( (COORDINATION + "legacy_writer_fence.ts", replacement( " this.payload = { write_check: writeCheck };",