diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index e7dcd45329..f6c6dafbcf 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -1619,6 +1619,61 @@ invalid Vision/workspace also remain refusals. The temporary measurement harness and raw captures stay private; this recipe describes the repeated workload and its oracles, not a distributed benchmark runner or public reproduction receipt. +### 2026-11-07 — Original-Turn closeout survives automatic Todo archival + +The complete CLI comparison in #8257 exposed a lifecycle failure with 101 +synthetic Todos. Fresh source-console reproductions on `4a33eb4b5` retain +`todo_archived` on both File and default SQLite after ordinary completion, +durable writeback and one quota debit. The frozen comparison remains unchanged; +these failed episodes are not accepted latency samples. + +The proposed repair stays in `todo_terminal_lifecycle.ts`. Its existing +same-Turn fence and exact ordinary-completion receipt verification also qualify +an archived Todo's closeout before the active-only rejection. One local helper +owns both active and archived parent checks. Original claim/lease, task digest, +validation declaration/receipt and provider CAS checks remain in force; archive +selection and retention thresholds are unchanged. This permits only the +original completion's terminal phase, without reopening archived work. + +Real File/SQLite console fixtures now complete the large-state sequence and +replay completion, writeback, quota spend and terminal closeout with one debit +and an unchanged independently verified JSON result. The durable CLI regression +uses a real validator and ordinary hard-lease claim: premature settlement, +unrelated archived work, another actor, claim or Turn, and changed completion +intent are refused. Terminal closeout retains the original passed validation +receipt, first completion time and archive state without rerunning the validator. +Existing provider conformance retains source/digest, validation, lease and replay +checks; an isolated real PostgreSQL suite qualifies its shared owner separately. + +The author proposal also integrates main `abf144202` (#8161), whose explicit +Goal-scoped checks do not gate local Todo completion. The archive regression +covers a failed Goal commitment alongside the local validator: ordinary and +terminal completion still preserve the original receipt and one debit, never +rerun the Goal checker, and leave the overall check failed. This is author-side +integration coverage; it does not replace independent review or reviewed-repair +wheel qualification. + +Independent review of #8264 at `12627eff8` found that changing only `--evidence` +could replace the archived Todo's accepted artifact while returning the old +validation receipt. All four File/SQLite × Goal-check fixtures reproduce this +failure on that head. The revision integrates target main `10f66ec5e` and rejects +different supplied evidence before archived closeout. Its new archived-closeout +receipt also retains the accepted evidence, so a changed retry cannot bypass the +check through receipt recovery. Omitted or empty evidence with a valid completion +note keeps the original value; the CLI's completion-note requirement still applies. +Historical fingerprints and older receipts remain compatible; active +completion behavior is unchanged. The regression checks canonical evidence, +original validation and completion time, and once-only validation/writeback/debit +before and after terminal replay. The earlier reviewed head remains unaccepted; +the revised head still needs independent review. + +This is a proposed lifecycle repair enabling the remaining R1/V4 journey and +large-state comparison. Independent exact-head review, reviewed-repair wheel +readback and remaining guard/parser characterization still govern the next +step. It does not upgrade an installed host or qualify interruption/restart, +split installation, full R0/V10/R1, M1/#8011, D2/D5/D6/D3, context/model, +adoption or complete-cost benefit. I3 remains at least one minor release. + ## Appendix B: Decision log | Date | Decision | Owner / approval | Alternatives | Normative sections changed | diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md index d1ae12fed5..f1ac16c0ce 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -1223,6 +1223,51 @@ stale claim 使用普通 release 后的 lease。它们必须在 writeback/debit Vision/workspace 同样保留拒绝。临时测量 harness 和 raw capture 保持私有;这个 recipe 描述重复的 workload/oracle,不是分发的 benchmark runner 或公共复现 receipt。 +### 2026-11-07 — 原 Turn 终结在 Todo 自动归档后仍可完成 + +#8257 的完整 CLI 比较在 101 个合成 Todo 下暴露了生命周期故障。基于 +`4a33eb4b5` 的新源码 console 复现在 File 和默认 SQLite 上均保留同一 +`todo_archived`:普通完成、持久写回和一次额度扣账已提交。冻结比较保持原样, +这些失败 episode 不计入验收通过的延迟样本。 + +修复提案仍由 `todo_terminal_lifecycle.ts` 拥有。在仅限 active 的拒绝之前, +复用已有同 Turn fence 与精确普通 completion receipt 校验,验证已归档 Todo +的终结资格。一个文件内 helper 统一 active 和已归档路径的父 receipt 检查。 +原 claim/lease、任务 digest、validation 声明与 receipt、provider CAS 校验 +继续生效;归档选择与保留阈值保持原样。允许的只是原 completion 的终结阶段, +不会重新开放已归档工作。 + +真实 File/SQLite console fixture 现在可以完成大状态整轮,并回放 completion、 +写回、扣账和 terminal closeout:只有一次扣账,独立验证的 JSON 结果保持原样。 +持久 CLI 回归使用真实 validator 和普通 hard-lease claim,拒绝过早结算、 +无关归档工作、其他 actor、claim 或 Turn,以及变更后的 completion intent。 +终结保留原已通过的 validation receipt、首次完成时间和归档状态,不重复执行 +validator。既有 provider conformance 继续检查 source/digest、validation、 +lease 和 replay;隔离真实 PostgreSQL suite 单独验证其共享 owner。 + +作者提案还集成了 main `abf144202`(#8161):显式 Goal 范围的检查不约束局部 +Todo 完成。归档回归在局部 validator 之外加入未通过的整体 Goal 承诺检查: +普通完成和终结仍保留原 receipt 与一次扣账,不重复执行 Goal checker,整体 +检查继续失败。这是作者阶段的集成覆盖,不能替代独立审阅或审阅修复后的 +wheel 资格验证。 + +#8264 `12627eff8` 的独立审阅发现:只改 `--evidence` 就能替换已归档 Todo +接受的产物,却仍返回旧 validation receipt。File/SQLite × 有无整体 Goal 检查 +的四个 fixture 均在该 head 复现失败。修订集成 target main `10f66ec5e`,在归档 +终结前拒绝不同的显式 evidence;新归档 closeout receipt 同时保存已接受证据, +让变更后的重试不能通过 receipt recovery 绕过检查。带有效 completion note 时, +省略或空 evidence 保留原值;CLI 原有的完成说明要求继续适用。 +历史 fingerprint 和旧 receipt 继续兼容,active completion 行为保持原样。 +回归在 terminal replay 前后检查 canonical evidence、原 validation 和完成时间, +以及只执行一次的 validation/writeback/debit。此前被审阅的 head 仍未获验收, +修订 head 仍需独立审阅。 + +这是支持后续 R1/V4 journey 和大状态比较的生命周期修复提案。下一步仍需 +独立 exact-head review、审阅修复后的 wheel 回读,以及剩余 guard/parser +刻画。它不会升级 installed host,也不验收中断/重启、split installation、 +完整 R0/V10/R1、M1/#8011、D2/D5/D6/D3、context/model、adoption 或完整成本 +收益。I3 继续保留至少一个 minor release。 + ## 附录 B:决策日志 | 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 | diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts index c7c9fb11da..387f7e65f1 100644 --- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts +++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts @@ -487,6 +487,15 @@ function terminalFailure( }; } +function completionEvidenceRejection( + input: CoordinationTodoTerminalLifecycleInput, + acceptedEvidence: unknown, +): CoordinationTodoTerminalLifecycleResult | null { + if (input.evidence === null || input.evidence === (acceptedEvidence ?? null)) return null; + return terminalFailure("terminal_completion_receipt_mismatch", + "Archived closeout must retain the evidence accepted by ordinary completion.", {}, "decision_rejection"); +} + function terminalRequestSha(input: CoordinationTodoTerminalLifecycleInput): string { const completionPolicyIdentity = input.completion_policy_request === null ? null @@ -1194,6 +1203,59 @@ function semanticDuplicate( return undefined; } +async function readSameTurnCompletionReceipt( + store: AuthorityStore, + input: ResolvedCoordinationTodoTerminalLifecycleInput, + normalized: CoordinationTodoTerminalLifecycleInput, + todo: JsonObject, +): Promise { + if (input.command !== "complete" || normalized.operation_identity.kind !== "completion_turn" || + !input.requested_no_followup) return null; + const validationRequired = todo.completion_validation_required === true; + const validationSha256 = todo.completion_validation_sha256; + let fence; + try { + fence = evaluateTodoCompletionFence({schema_version: TODO_COMPLETION_FENCE_REQUEST_SCHEMA, + projection_source: "materialized", goal_id: input.goal_id, todo_id: input.todo_id, todo, + requested_no_followup: input.requested_no_followup, + requested_completion_turn_key: input.requested_completion_turn_key, + requested_completion_identity_source: input.requested_completion_identity_source}); + } catch (error) { + return terminalFailure("invalid_todo_completion_transaction", + error instanceof Error ? error.message : "Invalid original completion identity"); + } + if (fence.outcome === "continue" && fence.reason === "same_turn_terminal_upgrade") { + const originalInput = {...input, + ...(normalized.claim_operation_id === undefined ? {} : {claim_operation_id: normalized.claim_operation_id, + lease_idempotency_key: normalized.lease_idempotency_key, lease_expected_version: normalized.lease_expected_version}), + requested_no_followup: false, + operation_id: completionTurnOperationId(input, false)}; + const original = await terminalReceipt(originalInput, terminalRequestSha(originalInput)).read(store); + if (original === null) return terminalFailure("terminal_completion_receipt_required", + "Same-Turn closeout requires the original accepted ordinary completion receipt.", {}, "decision_rejection"); + if (original.status !== "replayed") return original; + const originalValidation = original.validation_receipt as JsonObject | null; + const receiptDigest = originalValidation?.validation_declaration_sha256; + // Revision-zero receipts may omit the digest. The exact recovered + // request already binds the canonical declaration, actor and lease; + // later declaration revisions still require the receipt's own digest. + const requiresDigest = Number.isSafeInteger(todo.completion_validation_revision) && + Number(todo.completion_validation_revision) > 0; + if (original.completion_identity_key !== input.requested_completion_turn_key || + (validationRequired && (originalValidation === null || + typeof originalValidation !== "object" || originalValidation.passed !== true || + (receiptDigest != null && receiptDigest !== validationSha256) || + (requiresDigest && receiptDigest !== validationSha256))) || + (input.validation_receipt !== null && + canonicalAuthoritySha256(input.validation_receipt) !== canonicalAuthoritySha256(original.validation_receipt))) { + return terminalFailure("terminal_completion_receipt_mismatch", + "Original completion validation and identity must still match the completed Todo.", {}, "decision_rejection"); + } + return original; + } + return null; +} + /** * Execute complete/supersede as one provider-neutral CAS transaction. * @@ -1239,6 +1301,12 @@ export async function executeCoordinationTodoTerminalLifecycle( const receipt = terminalReceipt(input, requestSha); const replay = await receipt.read(store); if (replay !== null) { + // New archived closeout receipts preserve accepted evidence separately + // from the historical request fingerprint. Older receipts still replay. + if (Object.hasOwn(replay, "evidence")) { + const rejection = completionEvidenceRejection(input, replay.evidence); + if (rejection !== null) return rejection; + } // Older accepted lifecycle receipts lack the original quota binding. // Add it to this response from verified history; never rewrite the receipt. if (replay.status === "replayed" && isLifecycleCloseout(input) && @@ -1267,7 +1335,9 @@ export async function executeCoordinationTodoTerminalLifecycle( if (!await authoritySourcesCurrent()) return terminalFailure(AUTHORITY_SOURCE_CHANGED.code, AUTHORITY_SOURCE_CHANGED.reason, {}, "decision_rejection"); const observation = await receipt.observe(store); - if (observation.kind === "receipt") return observation.result; + if (observation.kind === "receipt") return Object.hasOwn(observation.result, "evidence") + ? completionEvidenceRejection(input, observation.result.evidence) ?? observation.result + : observation.result; if (observation.authority.status !== "loaded") { return { schema_version: COORDINATION_TODO_TERMINAL_LIFECYCLE_RESULT_SCHEMA, @@ -1360,13 +1430,19 @@ export async function executeCoordinationTodoTerminalLifecycle( "Todo requirements changed after Turn admission; reread the task and run a fresh Turn", {todo_id: input.todo_id}, "decision_rejection"); } + let terminalUpgradeReceipt: CoordinationTodoTerminalLifecycleResult | null = null; if (todo.archive_state !== "active") { - return terminalFailure( - "todo_archived", - "Todo terminal lifecycle requires an active Todo", - {}, - "decision_rejection", - ); + // Ordinary completion may archive this Todo before its original Turn + // settles. Only its exact accepted parent permits this second phase; + // archive alone grants no new execution or completion authority. + terminalUpgradeReceipt = await readSameTurnCompletionReceipt(store, input, normalized, todo); + if (terminalUpgradeReceipt === null) { + return terminalFailure("todo_archived", "Todo terminal lifecycle requires an active Todo", + {}, "decision_rejection"); + } + if (terminalUpgradeReceipt.status !== "replayed") return terminalUpgradeReceipt; + const rejection = completionEvidenceRejection(input, todo.evidence); + if (rejection !== null) return rejection; } const authorityTodo = todo; let edit: ReturnType | null = null; @@ -1607,50 +1683,13 @@ export async function executeCoordinationTodoTerminalLifecycle( } } let completion: ReturnType | null = null; - let terminalUpgradeReceipt: JsonObject | null = null; if (input.command === "complete") { const validationRequired = todo.completion_validation_required === true; const validationSha256 = todo.completion_validation_sha256; - if (normalized.operation_identity.kind === "completion_turn" && input.requested_no_followup) { - let fence; - try { - fence = evaluateTodoCompletionFence({schema_version: TODO_COMPLETION_FENCE_REQUEST_SCHEMA, - projection_source: "materialized", goal_id: input.goal_id, todo_id: input.todo_id, todo, - requested_no_followup: input.requested_no_followup, - requested_completion_turn_key: input.requested_completion_turn_key, - requested_completion_identity_source: input.requested_completion_identity_source}); - } catch (error) { - return terminalFailure("invalid_todo_completion_transaction", - error instanceof Error ? error.message : "Invalid original completion identity"); - } - if (fence.outcome === "continue" && fence.reason === "same_turn_terminal_upgrade") { - const originalInput = {...input, - ...(normalized.claim_operation_id === undefined ? {} : {claim_operation_id: normalized.claim_operation_id, - lease_idempotency_key: normalized.lease_idempotency_key, lease_expected_version: normalized.lease_expected_version}), - requested_no_followup: false, - operation_id: completionTurnOperationId(input, false)}; - const original = await terminalReceipt(originalInput, terminalRequestSha(originalInput)).read(store); - if (original === null) return terminalFailure("terminal_completion_receipt_required", - "Same-Turn closeout requires the original accepted ordinary completion receipt.", {}, "decision_rejection"); - if (original.status !== "replayed") return original; - const originalValidation = original.validation_receipt as JsonObject | null; - const receiptDigest = originalValidation?.validation_declaration_sha256; - // Revision-zero receipts may omit the digest. The exact recovered - // request already binds the canonical declaration, actor and lease; - // later declaration revisions still require the receipt's own digest. - const requiresDigest = Number.isSafeInteger(todo.completion_validation_revision) && - Number(todo.completion_validation_revision) > 0; - if (original.completion_identity_key !== input.requested_completion_turn_key || - (validationRequired && (originalValidation === null || - typeof originalValidation !== "object" || originalValidation.passed !== true || - (receiptDigest != null && receiptDigest !== validationSha256) || - (requiresDigest && receiptDigest !== validationSha256))) || - (input.validation_receipt !== null && - canonicalAuthoritySha256(input.validation_receipt) !== canonicalAuthoritySha256(original.validation_receipt))) { - return terminalFailure("terminal_completion_receipt_mismatch", - "Original completion validation and identity must still match the completed Todo.", {}, "decision_rejection"); - } - terminalUpgradeReceipt = original; + if (terminalUpgradeReceipt === null) { + terminalUpgradeReceipt = await readSameTurnCompletionReceipt(store, input, normalized, todo); + if (terminalUpgradeReceipt !== null && terminalUpgradeReceipt.status !== "replayed") { + return terminalUpgradeReceipt; } } if (validationRequired && !implicitMonitorNoChange && @@ -1985,6 +2024,10 @@ export async function executeCoordinationTodoTerminalLifecycle( todo_id: input.todo_id, command: input.command, changed, + // Freeze this archived phase's evidence for receipt-only recovery without + // adding a fresh authority read or changing legacy operation fingerprints. + ...(authorityTodo.archive_state !== "active" && terminalUpgradeReceipt !== null + ? {evidence: authorityTodo.evidence ?? null} : {}), ...(input.decision_outcome === null ? {} : {decision_outcome: input.decision_outcome}), ...(followthrough?.unblock_resume ? {unblock_resume: followthrough.unblock_resume} : {}), ...(followthrough?.decision_scope_resolution ? {decision_scope_resolution: followthrough.decision_scope_resolution} : {}), diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index a20fdcbab7..3e5518e800 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -8059,6 +8059,196 @@ def test_completed_todo_reentry_preserves_original_settlement_commands( assert settled["interaction_contract"]["agent_channel"]["must_attempt"] is False +@pytest.mark.parametrize("goal_commitment", [False, True]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_original_turn_closes_automatically_archived_todo_once( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, goal_commitment: bool, +) -> None: + from canonical_authority_fixture import isolate_sqlite_runtime + + # A real validator checks a synthetic result; its counter detects any + # repeated validation during closeout or receipt recovery. + project, runtime, registry_path = _write_fixture(tmp_path, claimed=False) + state_path = _configure_read_only_todo(project) + artifact = project / "result.json" + artifact.write_text('{"sum":45}\n', encoding="utf-8") + counter = project / "validator-count.txt" + validator = ( + "import json; from pathlib import Path; " + f"assert json.loads(Path({str(artifact)!r}).read_text()) == {{'sum':45}}; " + f"p=Path({str(counter)!r}); " + "p.write_text(str(int(p.read_text())+1 if p.exists() else 1))" + ) + argv = quote(json.dumps([sys.executable, "-c", validator]), safe="") + state_path.write_text( + state_path.read_text(encoding="utf-8").replace( + "action_kind=validate ", + f"action_kind=validate validation_command_argv={argv} ", 1, + ) + "".join( + f"\n- [x] [P2] Completed synthetic history {index}.\n" + f" \n" + for index in range(100) + ), + encoding="utf-8", + ) + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + _initialize_fixture_authority(project, runtime, provider=provider, + handoff_mode="hard_lease", leases=[]) + goal_counter = project / "goal-validator-count.txt" + if goal_commitment: + # A failed cross-task Goal check cannot replace or block the local + # validator, nor become accepted just because this Todo closes. + checks = tmp_path / "goal-checks.json" + checks.write_text(json.dumps({ + "scope": {"kind": "goal"}, "objective": "Fulfil the overall commitment", + "non_goals": [], "bindings": [], "criteria": [{ + "id": "overall-commitment", "description": "Overall work remains incomplete", + "validation_argv": [sys.executable, "-c", + "from pathlib import Path; " + f"p=Path({str(goal_counter)!r}); " + "p.write_text(str(int(p.read_text())+1 if p.exists() else 1)); " + "raise SystemExit(1)"], + }], + }), encoding="utf-8") + rc, inspected = _run_cli(registry_path, runtime, "goal-acceptance", "inspect", + "--goal-id", GOAL_ID) + assert rc == 0, inspected + rc, configured = _run_cli( + registry_path, runtime, "goal-acceptance", "configure", "--goal-id", GOAL_ID, + "--document", str(checks), "--operation-id", "overall-check", + "--expected-provider-revision", inspected["provider_revision"], "--execute", + ) + assert rc == 0, configured + rc, failed = _run_cli(registry_path, runtime, "goal-acceptance", "verify", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--execute") + assert rc == 1 and failed["checks_passed"] is False, failed + assert goal_counter.read_text() == "1" + claim_id = "original-archive-closeout-claim" + rc, claim = _run_cli(registry_path, runtime, "todo", "claim", + "--goal-id", GOAL_ID, "--todo-id", TODO_ID, + "--agent-id", AGENT_ID, "--claimed-by", AGENT_ID, + "--claim-operation-id", claim_id) + assert rc == 0, claim + binding = ("--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", TODO_ID, "--turn-instance-id", TURN_ID) + rc, guard = _run_cli(registry_path, runtime, "quota", "should-run", + "--codex-app", *binding) + assert rc == 0 and guard["normal_delivery_allowed"], guard + identity = guard["heartbeat_receipt"]["settlement_identity"] + ordinary_args = ("todo", "complete", *binding, "--claim-operation-id", claim_id, + "--evidence", "artifact:verified-sum") + rc, ordinary = _run_cli(registry_path, runtime, *ordinary_args) + assert rc == 0 and ordinary["validation_receipt"]["passed"], ordinary + rc, archived = _run_cli(registry_path, runtime, "todo", "list", + "--goal-id", GOAL_ID, "--todo-id", TODO_ID, + "--include-archived") + assert rc == 0, archived + assert archived["todo"]["archive_state"] == "archive" + assert archived["todo"]["status"] == "done" + completed_at = archived["todo"]["completed_at"] + terminal_args = (*ordinary_args, "--no-follow-up") + rc, premature = _run_cli(registry_path, runtime, *terminal_args) + assert rc == 1, premature + assert "requires matching writeback and quota spend receipts" in premature["error"] + assert _spend_run_count(runtime) == 0 + + rc, written = _run_cli( + registry_path, runtime, "refresh-state", *binding, + "--classification", "archived_result_verified", + "--delivery-batch-scale", "single_surface", + "--delivery-outcome", "outcome_progress", + "--vision-state", "vision_closed", + "--vision-acceptance", "The independently verified sum is durably recorded.", + "--no-global-sync", "--suppress-external-sinks", + ) + assert rc == 0, written + spend_args = ("quota", "spend-slot", *binding, "--slots", "1", + "--source", "heartbeat", "--execute") + for _ in range(2): + rc, spent = _run_cli(registry_path, runtime, *spend_args) + assert rc == 0, spent + validation_runs = counter.read_text() + assert validation_runs == "1" + + # A reused validation receipt cannot authorize replacing the artifact that + # the archived Todo accepted. Changing only evidence must fail before the + # terminal receipt exists and after that receipt can be replayed. + replacement_args = tuple( + "artifact:unvalidated-replacement" if arg == "artifact:verified-sum" else arg + for arg in terminal_args + ) + rc, replacement = _run_cli(registry_path, runtime, *replacement_args) + assert rc == 1, replacement + assert replacement["error_code"] == "terminal_completion_receipt_mismatch", replacement + rc, preserved = _run_cli(registry_path, runtime, "todo", "list", + "--goal-id", GOAL_ID, "--todo-id", TODO_ID, + "--include-archived") + assert rc == 0, preserved + assert preserved["todo"]["evidence"] == "artifact:verified-sum" + assert counter.read_text() == validation_runs + assert _spend_run_count(runtime) == 1 + + # An unrelated archived task, another actor, and a different claim or Turn + # cannot borrow the completed task's original receipt or settlement. + attempts = [ + ("todo", "complete", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", "todo_history_0000"), + tuple("unregistered-actor" if arg == AGENT_ID else arg for arg in terminal_args), + tuple("unrelated-claim" if arg == claim_id else arg for arg in terminal_args), + tuple("different-turn" if arg == TURN_ID else arg for arg in terminal_args), + (*terminal_args, "--clear-claim"), + ] + for attempt in attempts: + rc, refused = _run_cli(registry_path, runtime, *attempt) + assert rc == 1, refused + assert _spend_run_count(runtime) == 1 + assert counter.read_text() == validation_runs + assert refused["error_code"] == "coordination_operation_identity_mismatch", refused + + rc, terminal = _run_cli(registry_path, runtime, *terminal_args) + assert rc == 0, terminal + assert terminal["completion_continuation"] == "no_followup" + assert terminal["completion_recovery"] == "same_turn_terminal_closeout" + assert terminal["completion_identity_key"] == ordinary["completion_identity_key"] + assert terminal["validation_receipt"] == ordinary["validation_receipt"] + assert [row["step_kind"] for row in terminal["settlement_result"]["receipts"]] == [ + "validation", "durable_writeback", "quota_spend", "terminal_closeout", + ] + evidence_index = terminal_args.index("--evidence") + omitted_evidence = terminal_args[:evidence_index] + terminal_args[evidence_index + 2:] + empty_evidence = (*terminal_args[:evidence_index + 1], "", *terminal_args[evidence_index + 2:]) + for args in (omitted_evidence, empty_evidence): + rc, refused = _run_cli(registry_path, runtime, *args) + assert rc == 1 and "requires --note or --evidence" in refused["error"], refused + completion_note = ("--note", "Close the original independently verified result.") + for args in (terminal_args, ordinary_args, + (*omitted_evidence, *completion_note), (*empty_evidence, *completion_note)): + rc, replay = _run_cli(registry_path, runtime, *args) + assert rc == 0 and replay["idempotent_replay"], replay + rc, replacement = _run_cli(registry_path, runtime, *replacement_args) + assert rc == 1, replacement + assert replacement["error_code"] == "terminal_completion_receipt_mismatch", replacement + rc, final = _run_cli(registry_path, runtime, "todo", "list", + "--goal-id", GOAL_ID, "--todo-id", TODO_ID, + "--include-archived") + assert rc == 0, final + assert final["todo"]["archive_state"] == "archive" + assert final["todo"]["completed_at"] == completed_at + assert final["todo"]["evidence"] == "artifact:verified-sum" + assert final["todo"]["completion_turn_key"] == identity["effect_id"] + assert counter.read_text() == validation_runs + assert _spend_run_count(runtime) == 1 + assert _classification_count(runtime, "archived_result_verified") == 1 + if goal_commitment: + rc, overall = _run_cli(registry_path, runtime, "goal-acceptance", "inspect", + "--goal-id", GOAL_ID) + assert rc == 0, overall + assert overall["goal_acceptance_contract"]["status"] == "failed" + assert goal_counter.read_text() == "1" + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_read_only_settlement_omits_non_causal_delivery_workspace( tmp_path: Path,