From 3bd3945b8024865fc50e7a04753bd49e4fce0c91 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:54:52 +0800 Subject: [PATCH] fix(context): scope acceptance observations to selected work Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../rfcs/agent-loop-effect-interpreter-v0.md | 8 + .../agent-loop-effect-interpreter-v0.zh-CN.md | 5 + docs/reference/required-work-context.md | 15 ++ loopx/control_plane/goals/acceptance.py | 4 +- .../goals/acceptance_authority.ts | 14 +- .../work_items/context_readback.py | 6 +- .../test_selected_work_requirements.py | 7 +- .../test_work_acceptance_context.py | 180 ++++++++++++++++++ .../goal_acceptance_authority.test.ts | 10 + 9 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 tests/control_plane/test_work_acceptance_context.py diff --git a/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md b/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md index 840b67f9df..096082a4e0 100644 --- a/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md +++ b/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md @@ -611,6 +611,14 @@ coverage or make a rendered view a substitute for the original capture. Legacy-field access stays in the existing packet owner; the CLI invokes its projection function without adding another legacy migration consumer. +The acceptance-context adapter now uses the existing exact Todo inspection for +selected work. It retains the complete Goal acceptance basis and cumulative +outcomes, while avoiding a second inventory of unrelated task-binding +observations and a second selected-Todo body. File/SQLite CLI and actual DSH +protocol input cover configured checks and outcomes without checks; Todo-less +inspection still returns its original inventory. This qualifies the scoped IO +boundary, not live model benefit, installed adoption or total Turn cost. + Next, inventory authorized real Goal captures across active work, peer/replan, wait/recovery and optional-capability shapes, then map each retained clause to its shared typed owner and actual consumer. Migrate one complete caller family diff --git a/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.zh-CN.md b/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.zh-CN.md index de13ceb6f1..1ffa803ab6 100644 --- a/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.zh-CN.md +++ b/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.zh-CN.md @@ -421,6 +421,11 @@ fixture。只读规划与可选 quota capture 默认值不变,原生执行 Tur 覆盖,也不让渲染视图替代原始 capture。 旧字段访问保留在既有 packet owner;CLI 调用其投影函数,不增加待迁移的旧字段消费者。 +验收上下文 adapter 现在对已选定工作复用既有精确 Todo 读取,保留完整 Goal 验收依据 +和累计 outcome,避免再次搬运无关任务的关联观察及选定 Todo 正文。真实 File/SQLite +CLI 与 DSH 协议输入覆盖已配置检查、仅声明 outcome 两种情形;无选定工作的读取 +仍返回原列表。这只验证局部 IO 边界,不能替代真实模型收益、安装采用或整轮成本。 + 下一步先调查获准真实 Goal 的活跃工作、peer/replan、等待/恢复和可选能力形态, 将每条保留义务映射到共享 typed owner 与实际消费者,再完成一族调用方迁移,最后 退役其旧投影。用同次捕获决策比较 full/thin heartbeat 与 Turn,将必需详情补读、 diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index f3cf2044c0..41fb553904 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -40,6 +40,15 @@ Source failure retains the unresolved read, identifies the unavailable source, and blocks dependent delivery until recovery and a fresh guard. Reading current content is not an execution grant or proof of Goal completion. +For selected work, the acceptance adapter reuses the canonical owner's exact +Todo inspection. Its top-level `tasks` observations cover that Todo; the full +Goal contract, configured scope, checks, held work and cumulative outcomes stay +intact, including other owners' requirements. The separate selected-Todo source +retains its complete body and declaration. Todo-less reads and the explicit +`goal-acceptance inspect` command keep their existing full inventory. This +reduces repeated binding observations without treating a shorter context as +acceptance, authority or a measured model benefit. + For an exact Todo, the existing command now returns one complete record: ```sh @@ -102,6 +111,12 @@ transport flag does not restore the old exact Todo schema. 读取范围由 capability owner 决定。来源失败保留未完成必读项并阻止依赖交付,恢复后 重新准入;User gate 仍生效。大小告警不截断验收或移除剩余必读项。 +已选定工作时,验收 adapter 复用 canonical owner 的精确 Todo 读取,顶层 `tasks` +只携带该工作的关联观察;完整 Goal 合同、已配置 scope、检查、受阻工作和累计 outcome +全部保留,包括其他 owner 的要求。选定 Todo 的完整正文与声明仍由独立来源携带。 +没有选定工作的读取及显式 `goal-acceptance inspect` 保留完整列表;正文减少不等于 +验收、执行权限或已经测得模型收益。 + `todo list --todo-id …` 默认只返回一份完整 `todo.text`,保留来源、修订、身份、 状态和关系。精确响应移除列表及角色视图,不保留兼容开关;`--thin` 只用于概览。 缺失/过滤返回未匹配,歧义/来源故障报错,不退回旧摘要。读到记录不等于获得执行 diff --git a/loopx/control_plane/goals/acceptance.py b/loopx/control_plane/goals/acceptance.py index ad03aa9cad..96ec2a7b28 100644 --- a/loopx/control_plane/goals/acceptance.py +++ b/loopx/control_plane/goals/acceptance.py @@ -149,6 +149,7 @@ def inspect_goal_acceptance( runtime_root: str | None = None, agent_id: str | None = None, todo_id: str | None = None, + include_work_details: bool = True, delivery_workspace: Mapping[str, Any] | None = None, validation_workspace_path: Path | None = None, ) -> dict[str, Any]: @@ -156,7 +157,8 @@ def inspect_goal_acceptance( basis = _result( _INSPECT_METHOD, {**_routing(registry_path, goal_id, runtime_root, agent_id), - **({"todo_id": todo_id} if todo_id is not None else {})}, + **({"todo_id": todo_id} if todo_id is not None else {}), + **({"include_work_details": False} if not include_work_details else {})}, ) basis["goal_acceptance_contract"] = observe_goal_acceptance_oracles( registry_path=registry_path, goal_id=goal_id, diff --git a/loopx/control_plane/goals/acceptance_authority.ts b/loopx/control_plane/goals/acceptance_authority.ts index 56e205bbf7..4afbce7968 100644 --- a/loopx/control_plane/goals/acceptance_authority.ts +++ b/loopx/control_plane/goals/acceptance_authority.ts @@ -508,6 +508,7 @@ export async function inspectGoalAcceptance( goalId: string, todoId?: string, goalRef?: WireExactGoalRef, + includeWorkDetails = true, ): Promise { const head = await store.loadAuthority(); if (head.status !== "loaded") return source(store, {...head}); @@ -531,7 +532,7 @@ export async function inspectGoalAcceptance( return source(store, {status: "loaded", provider_revision: head.provider_revision, revision: state?.revision ?? null, contract_digest: state?.digest ?? null, contract: state ? state.document : null, tasks, - ...(todoId === undefined ? {} : {todo: todos.get(todoId) ?? null, + ...(todoId === undefined || !includeWorkDetails ? {} : {todo: todos.get(todoId) ?? null, completion_requirements: acceptanceCompletionRequirements(head.head, goalId, todoId)}), goal_acceptance_contract: projectGoalAcceptance(head.head, goalId)}); } @@ -545,9 +546,14 @@ async function local(value: unknown, kind: "inspect" | "configure" | "verify" | const goalId = requireAuthorityStoreId(request.goal_id, "goal_id"); const run = async () => { store = await openLocalAuthorityStore(root, goalId); - if (kind === "inspect") return inspectGoalAcceptance(store, goalId, - request.todo_id == null ? undefined : requireAuthorityStoreId(request.todo_id, "todo_id"), - request.goal_ref === undefined ? undefined : parseWireExactGoalRef(request.goal_ref, "acceptance request goal_ref")); + if (kind === "inspect") { + acceptanceRequire(request.include_work_details === undefined || typeof request.include_work_details === "boolean", + "include_work_details must be boolean"); + return inspectGoalAcceptance(store, goalId, + request.todo_id == null ? undefined : requireAuthorityStoreId(request.todo_id, "todo_id"), + request.goal_ref === undefined ? undefined : parseWireExactGoalRef(request.goal_ref, "acceptance request goal_ref"), + request.include_work_details !== false); + } if (kind === "configure") return configureGoalAcceptance(store, request); if (kind === "refine") return refineGoalAcceptanceOutcome(store, request); return kind === "verify" diff --git a/loopx/control_plane/work_items/context_readback.py b/loopx/control_plane/work_items/context_readback.py index 8a5f5cc8cd..631882f4f4 100644 --- a/loopx/control_plane/work_items/context_readback.py +++ b/loopx/control_plane/work_items/context_readback.py @@ -31,8 +31,12 @@ def _source_content(read, *, registry_path, runtime_root, goal_id, todo_id, return {"state_file": str(state_file), "text": text, "revision": "sha256:" + sha256(source_bytes).hexdigest()} if source == "goal_acceptance": + # The typed acceptance owner scopes task-binding observations while + # retaining the complete Goal contract and cumulative outcomes. The + # selected-Todo read owns its body; do not transport it a second time + # through the bounded acceptance RPC before discarding it here. return inspect_goal_acceptance(registry_path=registry_path, goal_id=goal_id, - runtime_root=str(runtime_root)) + runtime_root=str(runtime_root), todo_id=todo_id, include_work_details=False) if source == "selected_todo": if not todo_id: raise ValueError("selected Todo identity is unavailable") diff --git a/tests/control_plane/test_selected_work_requirements.py b/tests/control_plane/test_selected_work_requirements.py index b0a7b40259..7d4ce1ff45 100644 --- a/tests/control_plane/test_selected_work_requirements.py +++ b/tests/control_plane/test_selected_work_requirements.py @@ -83,11 +83,11 @@ def test_inline_user_context_preserves_full_scoped_gates_without_granting_delive build_todo_runtime_shadow_projection(goal_id="requirements-goal", todos=items, handoff_mode="soft_claim"), state_path=state, provider=provider) - def guard(*extra): + def guard(*extra, expected_exit=0): code, packet = run_json_cli_result("quota", "should-run", "--goal-id", "requirements-goal", "--agent-id", "agent-a", "--scan-path", str(tmp_path), *extra, registry_path=registry, runtime_root=runtime) - assert code == 0, json.dumps(packet, indent=2) + assert code == expected_exit, json.dumps(packet, indent=2) return packet packet = guard() @@ -107,7 +107,8 @@ def guard(*extra): # Canonical tasks still exist: the full Goal source must fail closed, # rather than silently substituting a Todo summary for missing intent. state.unlink() - failed = guard() + failed = guard(expected_exit=1) + assert failed["ok"] is False and failed["status_health_ok"] is False failed_channel = failed["interaction_contract"]["agent_channel"] assert failed_channel["delivery_allowed"] is False assert failed_channel["work_context"]["complete"] is False diff --git a/tests/control_plane/test_work_acceptance_context.py b/tests/control_plane/test_work_acceptance_context.py new file mode 100644 index 0000000000..35e809a4f8 --- /dev/null +++ b/tests/control_plane/test_work_acceptance_context.py @@ -0,0 +1,180 @@ +"""Selected work retains the complete Goal basis without a second task inventory.""" +from __future__ import annotations + +from copy import deepcopy +import contextlib +import io +import json +import subprocess +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.goals.acceptance import inspect_goal_acceptance +from loopx.control_plane.testing.canary_harness import run_json_cli_result, write_fixture_registry +from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan +from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request +from loopx.control_plane.work_items.context_readback import _source_content + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("checks", [False, True]) +def test_selected_acceptance_read_keeps_global_requirements_and_exact_binding( + tmp_path, monkeypatch, provider, checks, +): + isolate_sqlite_runtime(tmp_path, monkeypatch) + goal, selected = "acceptance-context", "todo_selected" + runtime, registry, state = tmp_path / "runtime", tmp_path / "registry.json", tmp_path / "state.md" + body = "Retain every accepted export guarantee. " * 30 + "Stop before publication." + state.write_text("---\nstatus: active\n---\n# Goal\n## Objective\nDeliver a compatible export.\n") + write_fixture_registry(project=tmp_path, runtime_root=runtime, registry_path=registry, + goal_id=goal, domain="software", adapter_kind="generic_project_goal_v0", + state_file=str(state), registered_agents=["agent-a", "agent-b"], quota_allowed_slots=None) + todos = [{"schema_version": "todo_item_v0", "todo_id": selected, "role": "agent", + "text": body, "status": "open", "done": False, "task_class": "advancement_task", + "action_kind": "implement", "index": 1, "source_section": "Agent Todo", + "archive_state": "active", "claimed_by": "agent-a"}] + todos += [{**todos[0], "todo_id": f"todo_peer_{index}", "text": "Independent peer work.", + "index": index + 2, "claimed_by": "agent-b"} for index in range(32)] + initialize_canonical_authority(runtime, goal, + build_todo_runtime_shadow_projection(goal_id=goal, todos=todos, handoff_mode="soft_claim"), + state_path=state, provider=provider) + + def cli(*arguments): + code, result = run_json_cli_result(*arguments, registry_path=registry, runtime_root=runtime) + assert code == 0, result + return result + + # Two owners' cumulative outcomes remain Goal requirements. Selection must + # not silently narrow those requirements or their work/evidence references. + for agent, todo in (("agent-a", selected), ("agent-b", "todo_peer_0")): + document = tmp_path / f"{agent}.json" + document.write_text(json.dumps({"outcome_id": f"export-{agent}", "owner_agent_id": agent, + "objective": f"Deliver the accepted {agent} export.", "todo_ids": [todo], + "requirements": [{"id": "content", "description": "Retain the complete export content."}, + {"id": "recovery", "description": "Retain cancellation and recovery at the requirement tail."}], + "source": {"ref": f"message:{agent}", "summary": "The owner requested a compatible export."}})) + before = cli("goal-acceptance", "inspect", "--goal-id", goal) + cli("goal-acceptance", "refine", "--goal-id", goal, "--agent-id", agent, + "--document", str(document), "--expected-provider-revision", before["provider_revision"], + "--operation-id", f"outcome-{agent}", "--execute") + if checks: + document = tmp_path / "checks.json" + document.write_text(json.dumps({"scope": {"kind": "selected_work", "todo_ids": [selected]}, + "objective": "Deliver a compatible export.", "non_goals": ["No publication."], + "criteria": [{"id": "content", "description": "Retain the complete export content.", + "validation_argv": [sys.executable, "-c", "pass"], "validation_timeout_seconds": 5}], + "bindings": [{"todo_id": selected, "criterion_ids": ["content"]}]})) + before = cli("goal-acceptance", "inspect", "--goal-id", goal) + cli("goal-acceptance", "configure", "--goal-id", goal, "--document", str(document), + "--expected-provider-revision", before["provider_revision"], + "--operation-id", "configure-export", "--execute") + + full = inspect_goal_acceptance(registry_path=registry, runtime_root=str(runtime), goal_id=goal) + read = {"source": "goal_acceptance"} + exact = _source_content(read, registry_path=registry, runtime_root=runtime, + goal_id=goal, todo_id=selected) + # The canonical acceptance owner, rather than another prose/role filter, + # determines which binding belongs to this selected work. + assert [row["todo_id"] for row in exact["tasks"]] == [selected] + assert exact["tasks"] == [row for row in full["tasks"] if row["todo_id"] == selected] + assert exact == {**full, "tasks": exact["tasks"]} + assert "todo" not in exact and "completion_requirements" not in exact + assert exact["goal_acceptance_contract"]["outcome_requirements"] == full["goal_acceptance_contract"]["outcome_requirements"] + if checks: + assert exact["contract"]["non_goals"] == ["No publication."] + assert exact["tasks"][0]["state"] == "ready" + else: + assert exact["tasks"][0]["state"] == "unbound" + # A Todo-less planning read still observes the whole existing inventory. + assert _source_content(read, registry_path=registry, runtime_root=runtime, + goal_id=goal, todo_id=None) == full + missing = _source_content(read, registry_path=registry, runtime_root=runtime, + goal_id=goal, todo_id="todo_missing") + assert missing == {**full, "tasks": []}, "missing work must not fall back to another binding" + + packet = cli("quota", "should-run", "--goal-id", goal, "--agent-id", "agent-a", + "--scan-path", str(tmp_path)) + channel = packet["interaction_contract"]["agent_channel"] + assert channel["delivery_allowed"] is True and channel["work_context"]["complete"] is True + observed = next(row["content"] for row in channel["work_context"]["sources"] + if row.get("source") == "goal_acceptance") + assert observed == exact + assert any(row.get("source") == "goal_state" for row in channel["required_reads"]) + # The normal guard's complete captured basis is the envelope input; no + # second guard may substitute a different observation for the host. + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + + envelope = build_turn_envelope(packet) + assert envelope["work_context"] == channel["work_context"] + request = build_loopx_turn_host_request(build_loopx_turn_plan(envelope, + host="codex-cli", execution_mode="isolated-headless", turn_instance_id="export-context-turn")) + runner = tmp_path / "runner.py" + runner.write_text("import json\nfrom pathlib import Path\n" + "def run_dsh_turn(*, prompt, workspace, **kwargs):\n" + " Path(workspace, 'received.txt').write_text(prompt)\n" + " return json.dumps({'result_kind':'wait','summary':'Protocol input recorded.'})\n") + before = {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} + command = [sys.executable, "-m", "loopx.dsh_goal_mode", "--workspace", str(tmp_path), + "--dsh-home", str(tmp_path / "host-home"), "--dsh-runner", str(runner)] + result = subprocess.run(command, + input=json.dumps(request), capture_output=True, text=True, timeout=30) + assert result.returncode == 0, result.stdout + result.stderr + prompt = (tmp_path / "received.txt").read_text() + authority = json.loads(prompt.split("Turn authority JSON:\n", 1)[1].split("\n\nWhen finished", 1)[0]) + assert authority["work_context"] == envelope["work_context"] + changed = deepcopy(request) + acceptance = next(row["content"] for row in changed["turn_envelope"]["work_context"]["sources"] + if row.get("source") == "goal_acceptance") + acceptance["goal_acceptance_contract"]["outcome_requirements"]["outcomes"][-1]["requirements"][-1]["description"] = "Omit recovery." + rejected = subprocess.run(command, input=json.dumps(changed), capture_output=True, text=True, timeout=30) + assert rejected.returncode != 0 and "action signature" in rejected.stderr + assert (tmp_path / "received.txt").read_text() == prompt + assert {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} == before + + # A failed exact acceptance read is still a pending obligation, never an + # empty/successful basis. Exercise the real CLI with only that IO fault. + from loopx.cli import main + from loopx.control_plane.work_items import context_readback + + def unavailable(**_kwargs): + raise OSError("acceptance source unavailable") + + with monkeypatch.context() as fault: + fault.setattr(context_readback, "inspect_goal_acceptance", unavailable) + output = io.StringIO() + with contextlib.redirect_stdout(output): + code = main(["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", + "quota", "should-run", "--goal-id", goal, "--agent-id", "agent-a", "--scan-path", str(tmp_path)]) + failed = json.loads(output.getvalue())["interaction_contract"]["agent_channel"] + assert code == 0 and failed["delivery_allowed"] is False + assert failed["work_context"]["complete"] is False + assert any(row.get("source") == "goal_acceptance" for row in failed["required_reads"]) + recovered = cli("quota", "should-run", "--goal-id", goal, "--agent-id", "agent-a", "--scan-path", str(tmp_path)) + assert recovered["interaction_contract"]["agent_channel"]["work_context"]["complete"] is True + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_acceptance_observation_does_not_transport_the_large_selected_body(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + runtime, registry, state = tmp_path / "runtime", tmp_path / "registry.json", tmp_path / "state.md" + state.write_text("---\nstatus: active\n---\n# Goal\n## Objective\nRetain complete work requirements.\n") + write_fixture_registry(project=tmp_path, runtime_root=runtime, registry_path=registry, + goal_id="large-acceptance-context", domain="software", adapter_kind="generic_project_goal_v0", + state_file=str(state), registered_agents=["agent-a"], quota_allowed_slots=None) + todo = {"schema_version": "todo_item_v0", "todo_id": "todo_large", "role": "agent", + "text": "Retain this complete requirement. " * 100_000 + "Do not omit this tail.", + "status": "open", "done": False, "task_class": "advancement_task", "action_kind": "implement", + "index": 1, "source_section": "Agent Todo", "archive_state": "active", "claimed_by": "agent-a"} + from loopx.control_plane.effect_runtime import MAX_REQUEST_BYTES + + assert len(todo["text"].encode()) > MAX_REQUEST_BYTES + initialize_canonical_authority(runtime, "large-acceptance-context", + build_todo_runtime_shadow_projection(goal_id="large-acceptance-context", todos=[todo], handoff_mode="soft_claim"), + state_path=state, provider=provider) + basis = _source_content({"source": "goal_acceptance"}, registry_path=registry, + runtime_root=runtime, goal_id="large-acceptance-context", todo_id="todo_large") + assert basis["status"] == "loaded" and [row["todo_id"] for row in basis["tasks"]] == ["todo_large"] + assert "todo" not in basis and "completion_requirements" not in basis diff --git a/tests/control_plane_ts/goal_acceptance_authority.test.ts b/tests/control_plane_ts/goal_acceptance_authority.test.ts index e0ab8fa5c8..834f9ded16 100644 --- a/tests/control_plane_ts/goal_acceptance_authority.test.ts +++ b/tests/control_plane_ts/goal_acceptance_authority.test.ts @@ -900,10 +900,20 @@ for (const provider of ["file", "sqlite"] as const) { assert.equal(inspect.source_authority, `${provider}_v0`); assert.equal((inspect.tasks as JsonObject[]).length, 1); assert.equal((inspect.todo as JsonObject).todo_id, "todo_first"); + const context = await inspectLocalGoalAcceptance({runtime_root: root, goal_id: goal, + todo_id: "todo_first", include_work_details: false}); + const {todo: _todo, completion_requirements: _completion, ...basis} = inspect; + assert.deepEqual(context, basis, "context omits work details before the RPC, preserving the entire acceptance basis"); + assert.equal((await inspectLocalGoalAcceptance({runtime_root: root, goal_id: goal, + todo_id: "todo_first", include_work_details: "false"})).status, "failed"); const heldDoc = {...document(), bindings: [{todo_id: "todo_first", criterion_ids: ["prerequisite"]}]}; assert.equal((await commitLocalGoalAcceptance({...await configureRequest(store, {document: heldDoc}), runtime_root: root})).status, "applied"); assert.equal((await inspectLocalGoalAcceptance({runtime_root: root, goal_id: goal, todo_id: "todo_second"})).reason_code, "goal_acceptance_unbound", "canonical task diagnosis survives the private effect adapter"); + const unboundContext = await inspectLocalGoalAcceptance({runtime_root: root, goal_id: goal, + todo_id: "todo_second", include_work_details: false}); + assert.equal((unboundContext.tasks as JsonObject[])[0].state, "unbound"); + assert.equal((unboundContext.goal_acceptance_contract as JsonObject).status, "held"); await update(store, "todo_first", {text: "Changed owner-bound work"}); assert.equal((await inspectLocalGoalAcceptance({runtime_root: root, goal_id: goal, todo_id: "todo_first"})).reason_code, "goal_acceptance_stale");