diff --git a/docs/reference/goal-acceptance-observations.md b/docs/reference/goal-acceptance-observations.md index 32c5459537..c33930b6bd 100644 --- a/docs/reference/goal-acceptance-observations.md +++ b/docs/reference/goal-acceptance-observations.md @@ -569,6 +569,133 @@ Readback validation: Dashboard `npm run smoke:delivery-review`, After the integrated Dashboard build, `npm run smoke:goal-acceptance-contract-packaged` runs the same contract browser check against the shipped assets. + + +### Reconcile obsolete work before supersession / 替代旧任务前协调检查范围 + +Use this owner path when an authorized correction replaces a Todo's behavior, +while the original objective and unaffected acceptance remain valid. For example, +`todo_parse` is already accepted, `todo_transform_old` is obsolete, +`todo_transform_current` is its existing replacement, and `todo_final` still checks +the delivery. Inspect acceptance and Todos on the same registry/runtime route. +Keep the original owner document and correction source with the operation receipt: +public `inspect` deliberately omits executable argv, so it cannot reconstruct the +complete configuration. + +With `all_advancement`, superseding bound work can return +`goal_acceptance_validation_required`. Removing only its binding still leaves it +in scope and can return `goal_acceptance_unbound`. Neither means that an Agent may +disable checks. The authorized local owner must review the **whole replacement +document**, including every retained criterion, command/pin/oracle and binding. +Explicitly narrow coverage to the current accepted work; future Todos will no +longer be included automatically and need owner review if they belong in scope. + +For this standalone contract, save the following as `acceptance-current.json`. +Replace the illustrative IDs and bounded artifact checks with the real reviewed +ones, including the available Python executable. Preserve any existing validator +pins/oracles. The checks below assert actual output, not unconditional success. +Do not add `outcome_id` to this expanded document: the compact outcome authoring +form resolves its work links and supports only omitted scope or explicit `goal` +scope; it cannot express this explicit work subset. + +```json +{ + "scope": {"kind": "selected_work", "todo_ids": ["todo_parse", "todo_transform_current", "todo_final"]}, + "objective": "Deliver validated records", + "non_goals": ["Publish the records"], + "criteria": [ + { + "id": "parse", + "description": "The accepted parsed records remain intact.", + "validation_argv": ["python3", "-c", "from pathlib import Path; assert Path('parsed.txt').read_text() == 'alpha\\nbeta\\n'"], + "validation_timeout_seconds": 5 + }, + { + "id": "transform-current", + "description": "The corrected transform preserves the requested order.", + "validation_argv": ["python3", "-c", "from pathlib import Path; assert Path('transformed.txt').read_text() == 'ALPHA\\nBETA\\n'"], + "validation_timeout_seconds": 5 + }, + { + "id": "final", + "description": "The final delivery contains the corrected records.", + "validation_argv": ["python3", "-c", "from pathlib import Path; assert Path('delivery.txt').read_text() == 'ALPHA\\nBETA\\n'"], + "validation_timeout_seconds": 5 + } + ], + "bindings": [ + {"todo_id": "todo_parse", "criterion_ids": ["parse"]}, + {"todo_id": "todo_transform_current", "criterion_ids": ["transform-current"]}, + {"todo_id": "todo_final", "criterion_ids": ["final"]} + ] +} +``` + +Keep explicit `--registry` / `--runtime-root` arguments on **every** command when +using a nondefault route. Configure without `--agent-id` only under existing local +owner authorization; omission selects a trusted local role, not authentication. +Use the exact current `provider_revision` from `inspect`, never the acceptance +contract's numeric `revision`. Preview and apply the identical reviewed document +with the same operation ID and provider revision: + +```bash +loopx --format json goal-acceptance inspect --goal-id example-goal +loopx --format json goal-acceptance configure --goal-id example-goal \ + --document acceptance-current.json --expected-provider-revision '' \ + --operation-id reconcile-transform-v2 +loopx --format json goal-acceptance configure --goal-id example-goal \ + --document acceptance-current.json --expected-provider-revision '' \ + --operation-id reconcile-transform-v2 --execute +``` + +Read the returned `operation_id`, `provider_revision`, scope, criteria and `tasks` +associations (`todo_id`, `criterion_ids`, `state`); +the accepted parser and final check must remain, and the obsolete Todo must be +absent from both scope and bindings. Retain the reviewed document, correction +source and receipt together. After a lost response retry that exact operation +with its original revision. After a confirmed revision conflict, inspect and +review current state before creating a new operation; do not silently rebase. + +Only then supersede the obsolete Todo. For canonical `hard_lease` work already +claimed by `example-agent`, acquire its ordinary lease (or inspect its existing +lease) and use the exact returned `lease.idempotency_key` and `lease.version`: + +```bash +loopx --format json task-lease acquire --goal-id example-goal \ + --todo-id todo_transform_old --owner example-agent --idempotency-key retire-transform-v1 +loopx --format json todo supersede --goal-id example-goal \ + --todo-id todo_transform_old --agent-id example-agent \ + --reason 'message:owner-correction: use the corrected transform' \ + --successor-todo-id todo_transform_current \ + --task-lease-idempotency-key '' \ + --task-lease-expected-version '' +loopx --format json todo list --goal-id example-goal +loopx --format json goal-acceptance inspect --goal-id example-goal +``` + +This uses the raw valid lease pair; it does not require managed supersede claims. +Do not reuse a released/expired lease or substitute a provider revision for its +version. Read back the original Todo's successor link and the retained work/checks. +Supersession records obsolete history; it does not verify the successor, rerun the +accepted parser, or complete the Goal. Current/final work still needs its normal +fresh checks, claims, leases and continuation handling. + +**保守边界:** 仅在已有明确授权且原目标与未受影响的验收仍有效时,由本地 owner 审阅 +完整替换文档。仅删 binding 不能让旧任务退出 `all_advancement`;改为上述 +`selected_work` 保留已验收 parser、当前替代任务和最终检查,同时明确以后新增任务 +不再自动纳入。每条命令沿用同一 registry/runtime,配置用最新 `provider_revision`, +预览与执行复用同一 operation ID;丢响应原样重试,确认冲突后重新审阅并新建操作。 +保留原配置、用户更正来源、拟用文档和回执,再用实际 lease key/version 替代旧任务。 +配置与 supersede 都不证明新产物通过;后续仍须执行真实检查。 + +This changes executable check coverage only. It does **not** retire or replace +persisted additive `outcome_requirements`, erase their sources/work history, or +resolve a contradictory immutable requirement. Report that conflict as unresolved +unless a supported owner amendment path actually reconciles it; do not claim this +recipe aligns it. 中文:本流程只协调命令检查覆盖,不撤销、覆盖或伪装满足累计的不可变 +成果要求;若已持久化要求互相冲突,在受支持的 owner 修订实际解决前,必须明确保留 +未解决状态。 + ## 中文 打开本机 Goal,选择 **概览 → 交付与依据 → 验收观察**。 @@ -612,6 +739,9 @@ status 同时在独立的 `run_history.goals[].artifact_lifecycle` 和 Markdown ### 默认验收中的命令检查 +旧任务被新任务替代时,使用上方[检查范围协调示例](#owner-check-scope-reconciliation): +完整保留仍有效的检查与工作绑定,由 owner 明确排除旧任务后,再按真实租约执行 supersede。 + 已注册且**已提升到 canonical authority** 的 Goal 可为已有成果要求配置命令检查。 命令不会自动提升 provider,canonical 读取失败也不回退到旧 Markdown。 优先引用 `outcome_id` 和要求 ID,由类型化 owner 解析目标、条件描述和关联任务; diff --git a/loopx/cli_commands/goal_acceptance.py b/loopx/cli_commands/goal_acceptance.py index eebbe63923..3237e0c7de 100644 --- a/loopx/cli_commands/goal_acceptance.py +++ b/loopx/cli_commands/goal_acceptance.py @@ -45,7 +45,18 @@ def register_goal_acceptance_command(subparsers: Any, add_format: Any) -> None: Own work review/adoption needs no human reply or unanimous peer vote. All steps require --goal-id GOAL. Proposal preparation alone requires neither owner input nor adoption; only an applied objective revision needs adoption. -Authority, budgets, stops and existing gated actions remain binding.''', +Authority, budgets, stops and existing gated actions remain binding. + +Owner check-scope reconciliation (configure without --agent-id): + --document is the full replacement: {"scope":{"kind":"selected_work","todo_ids":["todo_current"]}, + "objective":"Retained objective","non_goals":[],"criteria":[{"id":"result","description":"Real check", "validation_argv":["python3","check.py"]}], + "bindings":[{"todo_id":"todo_current","criterion_ids":["result"]}]} + Retain accepted/current/final work and checks; exclude obsolete work from scope + AND bindings. Removing a binding alone leaves all_advancement work held. + Use inspect's provider_revision and one stable --operation-id for preview/apply + and unchanged retries. Configure changes checks, not additive outcome requirements. + Full example, owner authority and lease-fenced supersession: + docs/reference/goal-acceptance-observations.md#owner-check-scope-reconciliation''', ) add_format(parser) parser.add_argument("action", type=lambda value: "clear-checks" if value == "disable" else value, choices=("inspect", "prepare", "configure", "verify", "clear-checks", "refine", "prepare-objective", "inspect-proposal", "apply-proposal", "revise-objective", "adopt-objective")) diff --git a/skills/loopx-project/references/outcome-refinement.md b/skills/loopx-project/references/outcome-refinement.md index 0b72195967..2c0153b739 100644 --- a/skills/loopx-project/references/outcome-refinement.md +++ b/skills/loopx-project/references/outcome-refinement.md @@ -32,6 +32,14 @@ and mark both met, or silently edit an authoritative file. Ask only when a consequential meaning or authority is genuinely missing, not merely because the user gave a correction. +For obsolete work covered by executable checks, follow the +[owner check-scope reconciliation example](../../../docs/reference/goal-acceptance-observations.md#owner-check-scope-reconciliation). +It preserves accepted work and current/final checks in an explicit +`selected_work` replacement before lease-fenced supersession. Removing a binding +alone does not remove work from `all_advancement`. This owner configuration path +does not supersede persisted additive outcome requirements; keep any conflicting +requirement explicit rather than claiming the check update retired it. + ## Current operating objective When recorded explicit user direction makes the Goal objective obsolete, prepare diff --git a/tests/control_plane/test_owner_check_scope_example.py b/tests/control_plane/test_owner_check_scope_example.py new file mode 100644 index 0000000000..0ea30a859d --- /dev/null +++ b/tests/control_plane/test_owner_check_scope_example.py @@ -0,0 +1,126 @@ +"""Execute the documented owner reconciliation against disposable canonical stores.""" +from __future__ import annotations + +from copy import deepcopy +import json +from pathlib import Path +import sys + +import pytest +from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime + +from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection +from loopx.control_plane.testing.canary_harness import run_json_cli_result, write_fixture_registry + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_documented_owner_scope_reconciliation(tmp_path, monkeypatch, provider): + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + project = tmp_path / "project" + project.mkdir() + (project / "parsed.txt").write_text("alpha\nbeta\n") + state = project / "state.md" + state.write_text("---\nstatus: active\n---\n# Fixture\n## Agent Todo\n") + runtime, registry = tmp_path / "runtime", tmp_path / "registry.json" + write_fixture_registry(project=project, runtime_root=runtime, registry_path=registry, + goal_id="example-goal", domain="acceptance", adapter_kind="generic_project_goal_v0", + state_file=str(state), registered_agents=["example-agent"]) + todos = [{"schema_version": "todo_item_v0", "todo_id": todo_id, "role": "agent", + "text": text, "status": "done" if index == 1 else "open", "done": index == 1, + "task_class": "advancement_task", "action_kind": "implement", "index": index, + "source_section": "Agent Todo", "archive_state": "active", "claimed_by": "example-agent"} + for index, (todo_id, text) in enumerate([ + ("todo_parse", "Parse the records"), + ("todo_transform_old", "Transform using the obsolete ordering"), + ("todo_transform_current", "Transform preserving the requested order"), + ("todo_final", "Check the final delivery"), + ], 1)] + initialize_canonical_authority(runtime, "example-goal", + build_todo_runtime_shadow_projection(goal_id="example-goal", todos=todos, handoff_mode="hard_lease"), + state_path=state, provider=provider) + + def run(*args, expected=0): + code, result = run_json_cli_result(*args, registry_path=registry, runtime_root=runtime) + assert code == expected, result + return result + + def acceptance(action, *args, expected=0): + return run("goal-acceptance", action, "--goal-id", "example-goal", *args, expected=expected) + + refinement = project / "outcome.json" + refinement.write_text(json.dumps({"outcome_id": "records", "owner_agent_id": "example-agent", + "objective": "Deliver validated records", "todo_ids": ["todo_parse"], + "requirements": [{"id": "parse", "description": "The accepted parsed records remain intact."}], + "source": {"ref": "message:initial", "summary": "The owner requested validated records"}})) + acceptance("refine", "--agent-id", "example-agent", "--document", str(refinement), + "--expected-provider-revision", acceptance("inspect")["provider_revision"], + "--operation-id", "declare-records", "--execute") + retained = acceptance("inspect")["goal_acceptance_contract"]["outcome_requirements"]["outcomes"] + + docs = Path(__file__).resolve().parents[2] / "docs/reference/goal-acceptance-observations.md" + section = docs.read_text(encoding="utf-8").split('', 1)[1] + document = json.loads(section.split("```json\n", 1)[1].split("```", 1)[0]) + # Use this test process's interpreter; all validator code is the documented code. + for criterion in document["criteria"]: + criterion["validation_argv"][0] = sys.executable + path = project / "acceptance-current.json" + + def configure(value, operation): + path.write_text(json.dumps(value)) + return ("--document", str(path), "--expected-provider-revision", + acceptance("inspect")["provider_revision"], "--operation-id", operation) + + original = deepcopy(document) + original["scope"] = {"kind": "all_advancement"} + original["bindings"][1]["todo_id"] = "todo_transform_old" + original["criteria"][1].update(id="transform-old", description="The obsolete transform reverses the records.", + validation_argv=[sys.executable, "-c", + "from pathlib import Path; assert Path('transformed.txt').read_text() == 'BETA\\nALPHA\\n'"]) + original["bindings"][1]["criterion_ids"] = ["transform-old"] + acceptance("configure", *configure(original, "configure-original"), "--execute") + lease = run("task-lease", "acquire", "--goal-id", "example-goal", "--todo-id", "todo_transform_old", + "--owner", "example-agent", "--idempotency-key", "retire-transform-v1")["lease"] + supersede = ("todo", "supersede", "--goal-id", "example-goal", "--todo-id", "todo_transform_old", + "--agent-id", "example-agent", "--reason", "message:owner-correction: use the corrected transform", + "--successor-todo-id", "todo_transform_current", "--task-lease-idempotency-key", lease["idempotency_key"], + "--task-lease-expected-version", str(lease["version"])) + assert run(*supersede, expected=1)["reason_code"] == "goal_acceptance_validation_required" + + rebind_only = {**document, "scope": {"kind": "all_advancement"}} + acceptance("configure", *configure(rebind_only, "rebind-only"), "--execute") + assert run(*supersede, expected=1)["reason_code"] == "goal_acceptance_unbound" + + args = configure(document, "reconcile-transform-v2") + assert acceptance("configure", *args)["status"] == "planned" + applied = acceptance("configure", *args, "--execute") + assert applied["status"] == "applied" and applied["operation_id"] == "reconcile-transform-v2" + assert applied["source_authority"] == f"{provider}_v0" + replay = acceptance("configure", *args, "--execute") + assert replay["status"] == "replayed" and replay["provider_revision"] == applied["provider_revision"] + assert replay["operation_id"] == applied["operation_id"] + contract = applied["goal_acceptance_contract"] + assert contract["scope"]["kind"] == "selected_work" + assert set(contract["scope"]["todo_ids"]) == set(document["scope"]["todo_ids"]) + assert {item["todo_id"]: item["criterion_ids"] for item in contract["tasks"]} == { + binding["todo_id"]: binding["criterion_ids"] for binding in document["bindings"]} + assert all(item["state"] == "ready" for item in contract["tasks"]) + assert {item["id"] for item in contract["criteria"]} == {"parse", "transform-current", "final"} + assert contract["outcome_requirements"]["outcomes"] == retained + + retired = run(*supersede) + assert retired["superseded"] is True and retired["source_authority"] == f"{provider}_v0" + readback = run("todo", "list", "--goal-id", "example-goal") + records = {todo["todo_id"]: todo for todo in readback["todos"]} + assert records["todo_transform_old"]["superseded_by"] == "todo_transform_current" + assert records["todo_parse"]["done"] is True + assert records["todo_transform_current"]["done"] is False + assert records["todo_final"]["done"] is False + assert acceptance("inspect")["goal_acceptance_contract"]["outcome_requirements"]["outcomes"] == retained + assert acceptance("verify", "--agent-id", "example-agent", "--execute", expected=1)["checks_passed"] is False + for filename, content in [("parsed.txt", "alpha\nbeta\n"), + ("transformed.txt", "ALPHA\nBETA\n"), ("delivery.txt", "ALPHA\nBETA\n")]: + (project / filename).write_text(content) + verified = acceptance("verify", "--agent-id", "example-agent", "--execute") + assert verified["checks_passed"] is True + assert acceptance("inspect")["goal_acceptance_contract"]["status"] == "accepted"