From 60eb1f290bb7590f600b32e0c375748a3128e66f Mon Sep 17 00:00:00 2001 From: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:28:58 +0800 Subject: [PATCH 1/6] fix(goal-start): fence guided bootstrap over orphaned Goal state A project reset that deletes a Goal's registry entry can leave its project-local active-state file behind. inspect_bootstrap_connection classified that as the ordinary registry_without_goal absence, so guided packets kept offering connect_if_needed, fresh-agent registration, Todo writeback, host-loop activation and quota spend for the same human-readable Goal id, and a later diagnose reported a healthy Goal over state an earlier lane left behind. Separate the two absences: when no registry entry matches but a state file for that id still exists under a project goal-state route, the connection is now orphaned_goal_state and the packet carries no mutation continuation. Only inspection and the backup-state preview remain, and the gate records that archiving or adopting project-local orphan state has no shipped command yet. The whole fence lives in the new goals-domain module so the already oversized bootstrap_command_pack keeps only its wiring and stays inside its reviewed module ceiling. Refs #4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> --- loopx/bootstrap_command_pack.py | 33 ++- .../goals/orphaned_goal_state.py | 270 ++++++++++++++++++ 2 files changed, 291 insertions(+), 12 deletions(-) create mode 100644 loopx/control_plane/goals/orphaned_goal_state.py diff --git a/loopx/bootstrap_command_pack.py b/loopx/bootstrap_command_pack.py index efc9b552e7..feedd5d47d 100644 --- a/loopx/bootstrap_command_pack.py +++ b/loopx/bootstrap_command_pack.py @@ -14,6 +14,12 @@ build_issue_fix_goal_command_templates, ) from .control_plane.effect_program import effect_program_from_ordered_steps +from .control_plane.goals.orphaned_goal_state import ( + fence_command_pack, + guided_fence, + registry_missing_goal_connection, + render_guided_lines, +) from .control_plane.goals.start_contract import ( build_goal_start_contract, build_goal_start_prompt, @@ -608,18 +614,13 @@ def inspect_bootstrap_connection( state_file = goal_state_file or fallback_state_file if selected_goal is None: - return { - **base_connection, - "registry_exists": True, - "goal_id": resolved_goal_id, - "goal_found": False, - "known_goal_ids": [str(goal.get("id")) for goal in goals], - "state_file": str(state_file), - "state_file_exists": state_file.exists(), - "connection_state": "registry_without_goal", - "mutation_confirmation_required": True, - "reason": "registry exists but no matching goal entry was found", - } + return registry_missing_goal_connection( + base_connection=base_connection, + project=resolved_project, + goal_id=resolved_goal_id, + known_goal_ids=[str(goal.get("id")) for goal in goals], + state_file=state_file, + ) if not selected_goal.get("state_file"): return { @@ -1076,6 +1077,7 @@ def build_loopx_bootstrap_command_pack( "host_loop_activation_allowed": activation_allowed, }, } + fence_command_pack(payload, command_prefix=command_prefix) if normalized_thread_id: payload["thread_id"] = normalized_thread_id payload["thread_agent_binding"] = thread_binding_projection @@ -1709,6 +1711,10 @@ def rerun_start_goal(selected_agent_id: str) -> str: detail_command=detail_command, ) ) + orphaned_gate = command_pack.get("orphaned_goal_state") + if isinstance(orphaned_gate, dict): + guided_transaction.update(guided_fence(orphaned_gate)) + guided_transaction.pop("identity_selection_gate", None) payload = { "ok": True, "schema_version": GUIDED_START_SCHEMA_VERSION, @@ -1731,6 +1737,7 @@ def rerun_start_goal(selected_agent_id: str) -> str: "spends_quota": False, "mutation_commands_are_previewed": True, "force_bootstrap_allowed": False, + "orphaned_goal_state_blocks_continuation": isinstance(orphaned_gate, dict), }, } if command_pack.get("thread_id"): @@ -1887,6 +1894,7 @@ def actionable_shell_command(value: Any) -> str: + "\n".join(choices) + "\n" ) + orphan_gate_lines = render_guided_lines(transaction) host_gate = transaction.get("host_surface_selection_gate") host_gate = host_gate if isinstance(host_gate, dict) else {} host_gate_lines = "" @@ -1916,6 +1924,7 @@ def actionable_shell_command(value: Any) -> str: {chr(10).join(step_lines)} {host_gate_lines} {goal_gate_lines} +{orphan_gate_lines} {identity_gate_lines} ## Todo Preservation diff --git a/loopx/control_plane/goals/orphaned_goal_state.py b/loopx/control_plane/goals/orphaned_goal_state.py new file mode 100644 index 0000000000..adbb2ad0f8 --- /dev/null +++ b/loopx/control_plane/goals/orphaned_goal_state.py @@ -0,0 +1,270 @@ +"""Guard the goal-start flow when Goal state outlives its registry entry. + +A project ``ACTIVE_GOAL_STATE.md`` for a goal id the project registry no longer +declares is not the ordinary "nothing to connect yet" case: continuing would hand +a fresh lane write authority over state an earlier lane left behind, under the +same human-readable id. The routes below are the project goal-state roots +``loopx.state_backup`` archives, so a reset that moved or kept one of them is +detected rather than silently reconnected. + +This module owns the whole fence so ``loopx.bootstrap_command_pack`` keeps only +its wiring: the detected fact, the operator-facing gate, the packet fields that +must disappear over orphaned state, and the guided transaction's blocking shape. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from ...project_prompt import shell_arg + +ACTIVE_GOAL_STATE_FILENAME = "ACTIVE_GOAL_STATE.md" + +ORPHANED_GOAL_STATE_CONNECTION = "orphaned_goal_state" + +ORPHANED_GOAL_STATE_GATE_SCHEMA_VERSION = "loopx_orphaned_goal_state_gate_v0" + +REGISTRY_WITHOUT_GOAL_CONNECTION = "registry_without_goal" + +# Project-local goal state has been written under each of these roots, so a goal +# absent from the registry can still own durable state in any of them. Ordered +# current route first; every match is reported, never merged or copied. +GOAL_STATE_ROOTS: tuple[tuple[str, ...], ...] = ( + (".loopx", "goals"), + (".codex", "goals"), + (".claude", "goals"), + (".local", "goals"), +) + +ORPHANED_GOAL_STATE_REASON = ( + "the project registry has no entry for this goal id, but goal state written for " + "it still exists; connecting again would create a second authority over that state" +) + +FORBIDDEN_UNTIL_RESOLVED = ( + "bootstrap", + "agent_registration", + "todo_write", + "quota_spend", + "host_loop_activation", +) + +# The command-pack keys that carry a mutation continuation, including the four the +# onboarding entry contract treats as one connect -> writeback -> activate -> quota +# loop. Over orphaned state none of them may be offered, goal text or not. +MUTATION_CONTINUATION_COMMANDS = ( + "goal_start_connect_if_needed", + "bootstrap_dry_run_preview", + "bootstrap_after_user_confirmation", + "goal_start_plan_prompt", + "goal_start_refresh_state", + "goal_start_host_loop_activation", + "goal_start_quota_should_run", +) + + +def orphaned_goal_state_routes(project: Path, goal_id: str) -> list[str]: + """List existing state files for ``goal_id`` as project-relative routes. + + Routes stay relative because this projection reaches host-facing packets; an + absolute path would carry the operator's filesystem into those artifacts. + """ + + return [ + path.relative_to(project).as_posix() + for path in ( + project.joinpath(*root).joinpath(goal_id, ACTIVE_GOAL_STATE_FILENAME) + for root in GOAL_STATE_ROOTS + ) + if path.is_file() + ] + + +def orphaned_goal_state_projection(project: Path, goal_id: str) -> dict[str, Any] | None: + """Return the orphaned-state fact for ``goal_id``, or ``None`` when nothing is orphaned.""" + + routes = orphaned_goal_state_routes(project, goal_id) + if not routes: + return None + return { + "schema_version": ORPHANED_GOAL_STATE_GATE_SCHEMA_VERSION, + "goal_id": goal_id, + "state_file_routes": routes, + "reason": ORPHANED_GOAL_STATE_REASON, + } + + +def registry_missing_goal_connection( + *, + base_connection: dict[str, Any], + project: Path, + goal_id: str, + known_goal_ids: list[str], + state_file: Path, +) -> dict[str, Any]: + """Classify "no registry entry" as orphaned state or as ordinary absence.""" + + shared = { + **base_connection, + "registry_exists": True, + "goal_id": goal_id, + "goal_found": False, + "known_goal_ids": known_goal_ids, + "state_file": str(state_file), + "state_file_exists": state_file.exists(), + "mutation_confirmation_required": True, + } + projection = orphaned_goal_state_projection(project, goal_id) + if projection is None: + return { + **shared, + "connection_state": REGISTRY_WITHOUT_GOAL_CONNECTION, + "reason": "registry exists but no matching goal entry was found", + } + return { + **shared, + "orphaned_goal_state": projection, + "connection_state": ORPHANED_GOAL_STATE_CONNECTION, + "bootstrap_continuation_allowed": False, + "reason": ORPHANED_GOAL_STATE_REASON, + } + + +def orphaned_goal_state_gate( + projection: dict[str, Any], + *, + project: str, + command_prefix: str, + status_command: str, +) -> dict[str, Any]: + """Project the next steps orphaned state allows: inspect, then back up.""" + + return { + **projection, + "resolution_routes": [ + { + "route": "inspect_registry_and_state", + "mutates": False, + "command": status_command, + }, + { + "route": "preview_state_backup", + "mutates": False, + "command": "\n".join( + [ + f"cd {shell_arg(project)}", + f"{command_prefix} backup-state --project . " + "--current-project-only", + ] + ), + }, + ], + "execution_boundary": ( + "backup-state writes nothing until the operator adds --execute, which is " + "the auditable archive the resolution depends on" + ), + "unavailable_resolution_routes": [ + { + "route": "archive_project_local_state", + "reason": ( + "uninstall-project selects goals from the project registry and " + "archive-runtime selects a goal directory under the shared runtime " + "root, so neither reaches state the registry no longer declares" + ), + }, + { + "route": "adopt_orphan_state", + "reason": ( + "re-registering the goal id is the second authority this fence " + "prevents; adoption needs an explicit instance identity first" + ), + }, + ], + "forbidden_until_resolved": list(FORBIDDEN_UNTIL_RESOLVED), + } + + +def fence_command_pack(command_pack: dict[str, Any], *, command_prefix: str) -> None: + """Leave a command pack over orphaned state with no mutation continuation.""" + + projection = command_pack["project_connection"].get("orphaned_goal_state") + if projection is None: + return + gate = orphaned_goal_state_gate( + projection, + project=str(command_pack.get("project") or ""), + command_prefix=command_prefix, + status_command=str(command_pack["commands"]["status"]), + ) + for key in MUTATION_CONTINUATION_COMMANDS: + command_pack["commands"][key] = None + safety = command_pack["safety_contract"] + safety["orphaned_goal_state_blocks_continuation"] = True + safety["mutation_requires_user_confirmation"] = True + safety["explicit_goal_start_may_write_project_local_state"] = False + safety["explicit_goal_start_must_activate_host_loop"] = False + safety["host_loop_activation_allowed"] = False + command_pack["orphaned_goal_state"] = gate + command_pack["recommended_next_step"] = { + "kind": "resolve_orphaned_goal_state", + "requires_user_confirmation": True, + "summary": ORPHANED_GOAL_STATE_REASON, + "orphaned_goal_state_gate": gate, + } + + +def guided_fence(gate: dict[str, Any]) -> dict[str, Any]: + """The guided-transaction fields that replace every continuation step.""" + + return { + "blocked_by": ORPHANED_GOAL_STATE_CONNECTION, + "orphaned_goal_state_gate": gate, + "ordered_steps": [ + { + "id": "inspect_connection", + "kind": "read_only", + "purpose": "resolve the requested project route and confirm the registry and orphaned state disagree", + }, + { + "id": "resolve_orphaned_goal_state", + "kind": "orphaned_goal_state_gate", + "resolution_routes": gate["resolution_routes"], + "forbidden_until_resolved": gate["forbidden_until_resolved"], + "purpose": ( + "inspect the orphaned state and preview its backup, then stop for " + "the operator's explicit resolution; do not bootstrap over it" + ), + }, + ], + } + + +def render_guided_lines(transaction: dict[str, Any]) -> str: + """Render the orphan gate section of the guided Markdown, or nothing.""" + + gate = transaction.get("orphaned_goal_state_gate") + if not isinstance(gate, dict): + return "" + routes = "\n".join( + [ + f"- `{route.get('route')}` (preview only): " + f"`{str(route.get('command')).splitlines()[-1]}`" + for route in gate.get("resolution_routes") or [] + if isinstance(route, dict) + ] + + [ + f"- `{item.get('route')}` is not available yet: {item.get('reason')}" + for item in gate.get("unavailable_resolution_routes") or [] + if isinstance(item, dict) + ] + ) + return ( + "\n## Orphaned Goal State Gate\n\n" + f"{gate.get('reason')}\n\n" + f"- orphaned state: {', '.join(f'`{route}`' for route in gate.get('state_file_routes') or [])}\n" + f"- blocked until resolved: {', '.join(f'`{name}`' for name in gate.get('forbidden_until_resolved') or [])}\n" + f"- {gate.get('execution_boundary')}\n\n" + + routes + + "\n" + ) From 144072cdbee74cb37b7b91ff8d5b8778f7c86366 Mon Sep 17 00:00:00 2001 From: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:29:01 +0800 Subject: [PATCH 2/6] test(goal-start): pin the orphaned Goal state fence Cover the detected fact over the current and legacy project state routes, the blocking guided transaction, the negative twins (plain absence keeps connect_if_needed, and a state file for another Goal id does not block), and that the shipped onboarding entry qualifier reports route=stop with zero actionable commands. Add the same invariant to the owning smoke through the real CLI so a future packet change cannot silently restore a continuation. Refs #4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> --- examples/bootstrap-command-pack-smoke.py | 80 +++++ .../test_start_goal_orphaned_goal_state.py | 292 ++++++++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 tests/control_plane/test_start_goal_orphaned_goal_state.py diff --git a/examples/bootstrap-command-pack-smoke.py b/examples/bootstrap-command-pack-smoke.py index fef295914a..a9d8dc1b7d 100644 --- a/examples/bootstrap-command-pack-smoke.py +++ b/examples/bootstrap-command-pack-smoke.py @@ -420,6 +420,85 @@ def test_start_goal_guided_previews_transaction_without_mutation() -> None: assert_fixture_unchanged(snapshot) +def test_start_goal_guided_blocks_orphaned_goal_state() -> None: + """A reset that deleted the registry entry must not reopen the same goal.""" + + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "reset-project" + project.mkdir() + goal_id = "reset-goal" + snapshot = write_connected_goal_fixture( + project, goal_id=goal_id, agent_id="codex-retired" + ) + state_file = project / ".codex" / "goals" / goal_id / "ACTIVE_GOAL_STATE.md" + registry = project / ".loopx" / "registry.json" + registry.write_text( + json.dumps({"schema_version": "0.1", "goals": []}, indent=2) + "\n", + encoding="utf-8", + ) + + payload = run_json( + "start-goal", + "--guided", + "--project", + str(project), + "--goal-id", + goal_id, + "--host-surface", + "codex-app", + "--goal-text", + "Continue the interrupted refactor", + ) + + connection = payload["project_connection"] + assert connection["connection_state"] == "orphaned_goal_state", connection + assert connection["goal_found"] is False, connection + assert connection["bootstrap_continuation_allowed"] is False, connection + assert connection["orphaned_goal_state"]["state_file_routes"] == [ + f".codex/goals/{goal_id}/ACTIVE_GOAL_STATE.md" + ], connection + + transaction = payload["guided_transaction"] + assert transaction["blocked_by"] == "orphaned_goal_state", transaction + assert [step["id"] for step in transaction["ordered_steps"]] == [ + "inspect_connection", + "resolve_orphaned_goal_state", + ], transaction + + gate = transaction["orphaned_goal_state_gate"] + assert gate["schema_version"] == "loopx_orphaned_goal_state_gate_v0", gate + assert gate["forbidden_until_resolved"] == [ + "bootstrap", + "agent_registration", + "todo_write", + "quota_spend", + "host_loop_activation", + ], gate + for route in gate["resolution_routes"]: + assert route["mutates"] is False, route + assert "--execute" not in route["command"], route + assert route["command"].splitlines()[-1].startswith("loopx "), route + + commands = payload["command_pack"]["commands"] + for key in ( + "goal_start_connect_if_needed", + "bootstrap_after_user_confirmation", + "goal_start_plan_prompt", + ): + assert commands[key] is None, key + + safety = payload["safety_contract"] + assert safety["force_bootstrap_allowed"] is False, safety + assert safety["writes_state_file"] is False, safety + assert safety["orphaned_goal_state_blocks_continuation"] is True, safety + assert_packet_summary_refs( + payload, + packet_kind="guided_start_goal", + compact_projection_default=True, + ) + assert_fixture_unchanged({registry: registry.read_text(), state_file: snapshot[state_file]}) + + def test_start_goal_guided_requires_explicit_goal_for_multi_goal_project() -> None: with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / "multi-goal-project" @@ -816,6 +895,7 @@ def main() -> int: test_missing_project_stops_before_mutation() test_goal_text_invocation_plans_ranked_todos_before_activation() test_start_goal_guided_previews_transaction_without_mutation() + test_start_goal_guided_blocks_orphaned_goal_state() test_start_goal_guided_requires_explicit_goal_for_multi_goal_project() test_connected_project_reuses_existing_state() test_linked_git_worktree_reuses_canonical_source_registry() diff --git a/tests/control_plane/test_start_goal_orphaned_goal_state.py b/tests/control_plane/test_start_goal_orphaned_goal_state.py new file mode 100644 index 0000000000..5ac1a014a4 --- /dev/null +++ b/tests/control_plane/test_start_goal_orphaned_goal_state.py @@ -0,0 +1,292 @@ +"""Guard the guided flow when Goal state outlives its registry entry (#4801). + +A reset that removes a Goal from ``.loopx/registry.json`` but leaves its +``ACTIVE_GOAL_STATE.md`` behind is not ordinary absence. Continuing with the +normal bootstrap, agent-registration, Todo, quota, or host-activation +continuation would let a fresh lane write over the orphaned state under the same +human-readable id, and ``diagnose`` would then report a healthy Goal. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from loopx.bootstrap_command_pack import ( + build_loopx_bootstrap_command_pack, + build_start_goal_guided_packet, + inspect_bootstrap_connection, +) +from loopx.control_plane.goals.orphaned_goal_state import ( + GOAL_STATE_ROOTS, + ORPHANED_GOAL_STATE_CONNECTION, + orphaned_goal_state_routes, +) +from loopx.control_plane.testing.onboarding_model_behavior_qualification import ( + onboarding_entry_contract_violations, + onboarding_entry_semantic_contract, +) + +ORPHANED_GOAL_ID = "reset-goal" +REGISTERED_GOAL_ID = "live-goal" +GOAL_TEXT = "Continue the interrupted refactor." + + +def _project( + root: Path, + *, + orphaned_state_dirs: tuple[str, ...] = (), + orphaned_goal_id: str = ORPHANED_GOAL_ID, +) -> Path: + """Build a project whose registry declares only ``live-goal``.""" + + project = root / "project" + registry = project / ".loopx" / "registry.json" + registry.parent.mkdir(parents=True) + registry.write_text( + json.dumps( + { + "schema_version": "0.1", + "goals": [ + { + "id": REGISTERED_GOAL_ID, + "status": "active", + "repo": str(project), + "state_file": ( + f".codex/goals/{REGISTERED_GOAL_ID}/ACTIVE_GOAL_STATE.md" + ), + "coordination": { + "agent_model": "peer_v1", + "registered_agents": ["codex-live"], + }, + } + ], + } + ) + + "\n", + encoding="utf-8", + ) + registered_state = project / ".codex" / "goals" / REGISTERED_GOAL_ID + registered_state.mkdir(parents=True) + (registered_state / "ACTIVE_GOAL_STATE.md").write_text( + "# Live goal state\n", encoding="utf-8" + ) + for state_dir in orphaned_state_dirs: + orphan = project / state_dir / orphaned_goal_id + orphan.mkdir(parents=True) + (orphan / "ACTIVE_GOAL_STATE.md").write_text( + "# Orphaned goal state written by a retired lane\n", encoding="utf-8" + ) + return project + + +def _guided(project: Path, goal_id: str = ORPHANED_GOAL_ID) -> dict[str, Any]: + return build_start_goal_guided_packet( + project=project, + goal_id=goal_id, + agent_id=None, + cli_bin="loopx", + host_surface="codex-app", + goal_text=GOAL_TEXT, + available_capabilities=["network"], + ) + + +def _command_pack(project: Path, goal_id: str = ORPHANED_GOAL_ID) -> dict[str, Any]: + return build_loopx_bootstrap_command_pack( + project=project, + goal_id=goal_id, + agent_id=None, + cli_bin="loopx", + host_surface="codex-app", + goal_text=GOAL_TEXT, + available_capabilities=["network"], + ) + + +# ---- the detected fact ------------------------------------------------------- + + +def test_orphan_detection_covers_current_and_legacy_routes(tmp_path: Path) -> None: + roots = tuple("/".join(root) for root in GOAL_STATE_ROOTS) + project = _project(tmp_path, orphaned_state_dirs=roots) + + assert orphaned_goal_state_routes(project, ORPHANED_GOAL_ID) == [ + f"{root}/{ORPHANED_GOAL_ID}/ACTIVE_GOAL_STATE.md" for root in roots + ] + + +def test_inspection_separates_orphaned_state_from_plain_absence( + tmp_path: Path, +) -> None: + orphaned = inspect_bootstrap_connection( + _project(tmp_path / "orphaned", orphaned_state_dirs=(".codex/goals",)), + goal_id=ORPHANED_GOAL_ID, + ) + assert orphaned["connection_state"] == ORPHANED_GOAL_STATE_CONNECTION + assert orphaned["goal_found"] is False + assert orphaned["bootstrap_continuation_allowed"] is False + assert orphaned["orphaned_goal_state"]["state_file_routes"] == [ + f".codex/goals/{ORPHANED_GOAL_ID}/ACTIVE_GOAL_STATE.md" + ] + + absent = inspect_bootstrap_connection( + _project(tmp_path / "absent"), + goal_id=ORPHANED_GOAL_ID, + ) + assert absent["connection_state"] == "registry_without_goal" + assert "orphaned_goal_state" not in absent + + +def test_candidate_matching_is_scoped_to_the_requested_goal(tmp_path: Path) -> None: + # Only ``live-goal`` has state, and it is registered, so nothing is orphaned. + project = _project(tmp_path) + + assert orphaned_goal_state_routes(project, ORPHANED_GOAL_ID) == [] + assert ( + inspect_bootstrap_connection(project, goal_id=ORPHANED_GOAL_ID)[ + "connection_state" + ] + == "registry_without_goal" + ) + assert ( + inspect_bootstrap_connection(project, goal_id=REGISTERED_GOAL_ID)[ + "connection_state" + ] + == "connected" + ) + + +# ---- the fence: no activation path over orphaned state ----------------------- + + +def test_guided_packet_offers_only_previews_over_orphaned_state( + tmp_path: Path, +) -> None: + payload = _guided(_project(tmp_path, orphaned_state_dirs=(".codex/goals",))) + transaction = payload["guided_transaction"] + + assert transaction["blocked_by"] == ORPHANED_GOAL_STATE_CONNECTION + assert transaction["writes_now"] is False + assert transaction["spends_quota_now"] is False + assert [step["id"] for step in transaction["ordered_steps"]] == [ + "inspect_connection", + "resolve_orphaned_goal_state", + ] + gate = transaction["orphaned_goal_state_gate"] + assert gate["schema_version"] == "loopx_orphaned_goal_state_gate_v0" + assert gate["forbidden_until_resolved"] == [ + "bootstrap", + "agent_registration", + "todo_write", + "quota_spend", + "host_loop_activation", + ] + assert [route["route"] for route in gate["resolution_routes"]] == [ + "inspect_registry_and_state", + "preview_state_backup", + ] + assert [ + item["route"] for item in gate["unavailable_resolution_routes"] + ] == ["archive_project_local_state", "adopt_orphan_state"] + for route in gate["resolution_routes"]: + assert route["mutates"] is False + assert "--execute" not in route["command"] + assert route["command"].splitlines()[-1].startswith("loopx "), route + contract = payload["safety_contract"] + assert contract["writes_state_file"] is False + assert contract["spends_quota"] is False + assert contract["force_bootstrap_allowed"] is False + assert contract["mutation_commands_are_previewed"] is True + + +def test_guided_packet_carries_no_bootstrap_or_todo_authoring_continuation( + tmp_path: Path, +) -> None: + project = _project(tmp_path, orphaned_state_dirs=(".codex/goals",)) + payload = _guided(project) + commands = payload["command_pack"]["commands"] + + assert commands["goal_start_connect_if_needed"] is None + assert commands["bootstrap_after_user_confirmation"] is None + assert commands["goal_start_plan_prompt"] is None + assert payload["recommended_next_step"]["kind"] == "resolve_orphaned_goal_state" + assert payload["recommended_next_step"]["requires_user_confirmation"] is True + assert "identity_selection_gate" not in payload["guided_transaction"] + + message = payload["message"] + assert "Orphaned Goal State Gate" in message + assert "todo add" not in message + assert "is not available yet" in message + assert not [ + line + for line in message.splitlines() + if line.lstrip().startswith("`loopx ") and "--execute" in line + ], message + # The orphan routes stay project-relative; a projected absolute path would + # carry the operator's filesystem into host-facing artifacts. The preview + # commands still `cd` into the resolved project, as every packet command does. + assert str(project) not in json.dumps( + payload["guided_transaction"]["orphaned_goal_state_gate"]["state_file_routes"] + ) + + +def test_command_pack_fence_matches_the_guided_packet(tmp_path: Path) -> None: + payload = _command_pack(_project(tmp_path, orphaned_state_dirs=(".claude/goals",))) + + assert payload["orphaned_goal_state"]["state_file_routes"] == [ + f".claude/goals/{ORPHANED_GOAL_ID}/ACTIVE_GOAL_STATE.md" + ] + contract = payload["safety_contract"] + assert contract["orphaned_goal_state_blocks_continuation"] is True + assert contract["explicit_goal_start_may_write_project_local_state"] is False + assert contract["host_loop_activation_allowed"] is False + assert contract["mutation_requires_user_confirmation"] is True + + +# ---- the fence did not widen: untouched routes keep their old behavior ------- + + +def test_plain_absence_keeps_the_connect_continuation(tmp_path: Path) -> None: + payload = _guided(_project(tmp_path)) + transaction = payload["guided_transaction"] + + assert transaction.get("blocked_by") != ORPHANED_GOAL_STATE_CONNECTION + step_ids = [step["id"] for step in transaction["ordered_steps"]] + assert step_ids[:2] == ["inspect_connection", "connect_if_needed"] + assert payload["command_pack"]["commands"]["goal_start_connect_if_needed"] + assert payload["safety_contract"]["orphaned_goal_state_blocks_continuation"] is False + + +def test_connected_goal_packet_is_unchanged(tmp_path: Path) -> None: + payload = _guided( + _project(tmp_path, orphaned_state_dirs=(".codex/goals",)), + goal_id=REGISTERED_GOAL_ID, + ) + + assert payload["project_connection"]["connection_state"] == "connected" + assert "orphaned_goal_state_gate" not in payload["guided_transaction"] + assert [step["id"] for step in payload["guided_transaction"]["ordered_steps"]][0] == ( + "inspect_connection" + ) + + +# ---- the shipped onboarding qualifier classifies the fence as a stop -------- + + +def test_obeying_agent_has_no_actionable_command_at_the_fence(tmp_path: Path) -> None: + payload = _guided(_project(tmp_path / "fence", orphaned_state_dirs=(".codex/goals",))) + contract = onboarding_entry_semantic_contract(payload) + + assert contract["route"] == "stop" + assert contract["action_command_ids"] == [] + assert contract["writes_now"] is False + assert contract["spends_quota_now"] is False + assert onboarding_entry_contract_violations(contract) == [] + + unblocked = onboarding_entry_semantic_contract( + _guided(_project(tmp_path / "clear")) + ) + assert unblocked["route"] == "select_agent_identity" + assert unblocked["action_command_ids"] From 49dc852bbb4482581153356209cfe703fc9a9800 Mon Sep 17 00:00:00 2001 From: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:30:32 +0800 Subject: [PATCH 3/6] fix(goal-start): fence orphaned state when the registry is gone too The first version classified orphaned state only inside the readable, non-empty registry branch, so a reset that removed .loopx/registry.json outright still reached the early not_connected return and a guided packet carrying bootstrap, fresh-agent registration, Todo planning and writeback, host-loop activation and quota guard for a Goal id whose state file survived. Move the shared classifier above both absence returns: the ordinary case keeps exactly the fields and connection_state it returned before (registry_exists and the reason text included), and only a surviving state candidate switches it to orphaned_goal_state. The command-pack size ceiling still holds (2233 against 2240), because the classifier now replaces two inline literals rather than adding a third branch. Refs #4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> --- loopx/bootstrap_command_pack.py | 19 +++---- .../goals/orphaned_goal_state.py | 49 ++++++++++++++++--- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/loopx/bootstrap_command_pack.py b/loopx/bootstrap_command_pack.py index feedd5d47d..47ddb97454 100644 --- a/loopx/bootstrap_command_pack.py +++ b/loopx/bootstrap_command_pack.py @@ -19,6 +19,7 @@ guided_fence, registry_missing_goal_connection, render_guided_lines, + unregistered_goal_connection, ) from .control_plane.goals.start_contract import ( build_goal_start_contract, @@ -590,17 +591,12 @@ def inspect_bootstrap_connection( } if not registry: - return { - **base_connection, - "registry_exists": False, - "goal_id": inferred_goal_id, - "goal_found": False, - "state_file": str(state_file), - "state_file_exists": state_file.exists(), - "connection_state": "not_connected", - "mutation_confirmation_required": True, - "reason": "project-local .loopx/registry.json is missing", - } + return unregistered_goal_connection( + base_connection=base_connection, + project=resolved_project, + goal_id=inferred_goal_id, + state_file=state_file, + ) goals = registry_goals(registry) selected_goal_id, selected_goal = _select_goal(goals, goal_id) @@ -620,6 +616,7 @@ def inspect_bootstrap_connection( goal_id=resolved_goal_id, known_goal_ids=[str(goal.get("id")) for goal in goals], state_file=state_file, + registry_exists=True, ) if not selected_goal.get("state_file"): diff --git a/loopx/control_plane/goals/orphaned_goal_state.py b/loopx/control_plane/goals/orphaned_goal_state.py index adbb2ad0f8..3d30f598f0 100644 --- a/loopx/control_plane/goals/orphaned_goal_state.py +++ b/loopx/control_plane/goals/orphaned_goal_state.py @@ -27,6 +27,12 @@ REGISTRY_WITHOUT_GOAL_CONNECTION = "registry_without_goal" +NOT_CONNECTED_CONNECTION = "not_connected" + +REGISTRY_WITHOUT_GOAL_REASON = "registry exists but no matching goal entry was found" + +NOT_CONNECTED_REASON = "project-local .loopx/registry.json is missing" + # Project-local goal state has been written under each of these roots, so a goal # absent from the registry can still own durable state in any of them. Ordered # current route first; every match is reported, never merged or copied. @@ -95,32 +101,40 @@ def orphaned_goal_state_projection(project: Path, goal_id: str) -> dict[str, Any } -def registry_missing_goal_connection( +def goal_connection_without_matching_entry( *, base_connection: dict[str, Any], project: Path, goal_id: str, - known_goal_ids: list[str], state_file: Path, + registry_exists: bool, + absence_connection: str, + absence_reason: str, + known_goal_ids: list[str] | None = None, ) -> dict[str, Any]: - """Classify "no registry entry" as orphaned state or as ordinary absence.""" + """Classify "no matching registry entry" as orphaned state or as ordinary absence. + + The absence half keeps the packet fields it carried before this fence existed, + including which of them are present, so a fresh project is unaffected. + """ shared = { **base_connection, - "registry_exists": True, + "registry_exists": registry_exists, "goal_id": goal_id, "goal_found": False, - "known_goal_ids": known_goal_ids, "state_file": str(state_file), "state_file_exists": state_file.exists(), "mutation_confirmation_required": True, } + if known_goal_ids is not None: + shared["known_goal_ids"] = known_goal_ids projection = orphaned_goal_state_projection(project, goal_id) if projection is None: return { **shared, - "connection_state": REGISTRY_WITHOUT_GOAL_CONNECTION, - "reason": "registry exists but no matching goal entry was found", + "connection_state": absence_connection, + "reason": absence_reason, } return { **shared, @@ -131,6 +145,27 @@ def registry_missing_goal_connection( } +def registry_missing_goal_connection(**fields: Any) -> dict[str, Any]: + """Classify the absence inside a readable, non-empty registry.""" + + return goal_connection_without_matching_entry( + absence_connection=REGISTRY_WITHOUT_GOAL_CONNECTION, + absence_reason=REGISTRY_WITHOUT_GOAL_REASON, + **fields, + ) + + +def unregistered_goal_connection(**fields: Any) -> dict[str, Any]: + """Classify the absence when no readable registry declares any goal.""" + + return goal_connection_without_matching_entry( + registry_exists=False, + absence_connection=NOT_CONNECTED_CONNECTION, + absence_reason=NOT_CONNECTED_REASON, + **fields, + ) + + def orphaned_goal_state_gate( projection: dict[str, Any], *, From a0ca91d1d61341df260aaaf49742314ef9bca4dc Mon Sep 17 00:00:00 2001 From: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:30:34 +0800 Subject: [PATCH 4/6] test(goal-start): cover the fence without a project registry Parametrized twins for a missing and an empty registry pin both halves: surviving state fences the packet with no actionable command, while a genuinely fresh project keeps its ordinary onboarding continuation and its previous connection_state. Add the missing-registry case to the owning smoke through the real CLI so the bypass cannot return silently. Refs #4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> --- examples/bootstrap-command-pack-smoke.py | 44 +++++++ .../test_start_goal_orphaned_goal_state.py | 117 ++++++++++++++---- 2 files changed, 138 insertions(+), 23 deletions(-) diff --git a/examples/bootstrap-command-pack-smoke.py b/examples/bootstrap-command-pack-smoke.py index a9d8dc1b7d..22fff575fb 100644 --- a/examples/bootstrap-command-pack-smoke.py +++ b/examples/bootstrap-command-pack-smoke.py @@ -499,6 +499,49 @@ def test_start_goal_guided_blocks_orphaned_goal_state() -> None: assert_fixture_unchanged({registry: registry.read_text(), state_file: snapshot[state_file]}) +def test_start_goal_guided_blocks_orphaned_state_without_a_registry() -> None: + """The same invariant must hold when the reset deleted the whole registry file.""" + + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "reset-project" + state_file = project / ".codex" / "goals" / "reset-goal" / "ACTIVE_GOAL_STATE.md" + state_file.parent.mkdir(parents=True) + state_text = "# Orphaned goal state written by a retired lane\n" + state_file.write_text(state_text, encoding="utf-8") + + payload = run_json( + "start-goal", + "--guided", + "--project", + str(project), + "--goal-id", + "reset-goal", + "--host-surface", + "codex-app", + "--goal-text", + "Continue the interrupted refactor", + ) + + assert payload["project_connection"]["connection_state"] == "orphaned_goal_state" + transaction = payload["guided_transaction"] + assert transaction["blocked_by"] == "orphaned_goal_state", transaction + assert [step["id"] for step in transaction["ordered_steps"]] == [ + "inspect_connection", + "resolve_orphaned_goal_state", + ], transaction + commands = payload["command_pack"]["commands"] + for key in ( + "goal_start_connect_if_needed", + "goal_start_refresh_state", + "goal_start_host_loop_activation", + "goal_start_quota_should_run", + "goal_start_plan_prompt", + ): + assert commands[key] is None, key + assert state_file.read_text(encoding="utf-8") == state_text + assert not (project / ".loopx" / "registry.json").exists() + + def test_start_goal_guided_requires_explicit_goal_for_multi_goal_project() -> None: with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / "multi-goal-project" @@ -896,6 +939,7 @@ def main() -> int: test_goal_text_invocation_plans_ranked_todos_before_activation() test_start_goal_guided_previews_transaction_without_mutation() test_start_goal_guided_blocks_orphaned_goal_state() + test_start_goal_guided_blocks_orphaned_state_without_a_registry() test_start_goal_guided_requires_explicit_goal_for_multi_goal_project() test_connected_project_reuses_existing_state() test_linked_git_worktree_reuses_canonical_source_registry() diff --git a/tests/control_plane/test_start_goal_orphaned_goal_state.py b/tests/control_plane/test_start_goal_orphaned_goal_state.py index 5ac1a014a4..aa3574f406 100644 --- a/tests/control_plane/test_start_goal_orphaned_goal_state.py +++ b/tests/control_plane/test_start_goal_orphaned_goal_state.py @@ -13,6 +13,8 @@ from pathlib import Path from typing import Any +import pytest + from loopx.bootstrap_command_pack import ( build_loopx_bootstrap_command_pack, build_start_goal_guided_packet, @@ -38,35 +40,39 @@ def _project( *, orphaned_state_dirs: tuple[str, ...] = (), orphaned_goal_id: str = ORPHANED_GOAL_ID, + registry: str = "registered", ) -> Path: - """Build a project whose registry declares only ``live-goal``.""" + """Build a project carrying an orphaned state file for ``orphaned_goal_id``. + + ``registry`` selects how far the reset got: ``registered`` keeps the other + Goal's entry, ``empty`` keeps the file but declares no Goal, and ``missing`` + removes the project registry entirely. + """ project = root / "project" - registry = project / ".loopx" / "registry.json" - registry.parent.mkdir(parents=True) - registry.write_text( - json.dumps( + registry_path = project / ".loopx" / "registry.json" + registry_path.parent.mkdir(parents=True) + entries = ( + [ { - "schema_version": "0.1", - "goals": [ - { - "id": REGISTERED_GOAL_ID, - "status": "active", - "repo": str(project), - "state_file": ( - f".codex/goals/{REGISTERED_GOAL_ID}/ACTIVE_GOAL_STATE.md" - ), - "coordination": { - "agent_model": "peer_v1", - "registered_agents": ["codex-live"], - }, - } - ], + "id": REGISTERED_GOAL_ID, + "status": "active", + "repo": str(project), + "state_file": f".codex/goals/{REGISTERED_GOAL_ID}/ACTIVE_GOAL_STATE.md", + "coordination": { + "agent_model": "peer_v1", + "registered_agents": ["codex-live"], + }, } - ) - + "\n", - encoding="utf-8", + ] + if registry == "registered" + else [] ) + if registry != "missing": + registry_path.write_text( + json.dumps({"schema_version": "0.1", "goals": entries}, indent=2) + "\n", + encoding="utf-8", + ) registered_state = project / ".codex" / "goals" / REGISTERED_GOAL_ID registered_state.mkdir(parents=True) (registered_state / "ACTIVE_GOAL_STATE.md").write_text( @@ -290,3 +296,68 @@ def test_obeying_agent_has_no_actionable_command_at_the_fence(tmp_path: Path) -> ) assert unblocked["route"] == "select_agent_identity" assert unblocked["action_command_ids"] + + +# ---- the same invariant holds when the reset removed the whole registry ----- + + +def _assert_fenced(payload: dict[str, Any]) -> None: + transaction = payload["guided_transaction"] + + assert transaction["blocked_by"] == ORPHANED_GOAL_STATE_CONNECTION, transaction + assert [step["id"] for step in transaction["ordered_steps"]] == [ + "inspect_connection", + "resolve_orphaned_goal_state", + ], transaction + commands = payload["command_pack"]["commands"] + for key in ( + "goal_start_connect_if_needed", + "goal_start_refresh_state", + "goal_start_host_loop_activation", + "goal_start_quota_should_run", + "goal_start_plan_prompt", + ): + assert commands[key] is None, key + assert onboarding_entry_semantic_contract(payload)["action_command_ids"] == [] + + +@pytest.mark.parametrize("registry", ["missing", "empty"]) +def test_reset_without_a_registry_still_fences_surviving_state( + tmp_path: Path, registry: str +) -> None: + project = _project( + tmp_path / registry, + orphaned_state_dirs=(".codex/goals",), + registry=registry, + ) + + connection = inspect_bootstrap_connection(project, goal_id=ORPHANED_GOAL_ID) + assert connection["connection_state"] == ORPHANED_GOAL_STATE_CONNECTION, connection + assert connection["orphaned_goal_state"]["state_file_routes"] == [ + f".codex/goals/{ORPHANED_GOAL_ID}/ACTIVE_GOAL_STATE.md" + ], connection + _assert_fenced(_guided(project)) + + +@pytest.mark.parametrize( + ("registry", "absence_connection"), + [("missing", "not_connected"), ("empty", "registry_without_goal")], +) +def test_reset_without_orphaned_state_is_still_ordinary_onboarding( + tmp_path: Path, registry: str, absence_connection: str +) -> None: + project = _project(tmp_path / f"clear-{registry}", registry=registry) + + connection = inspect_bootstrap_connection(project, goal_id=ORPHANED_GOAL_ID) + assert connection["connection_state"] == absence_connection, connection + assert connection["registry_exists"] is (registry != "missing"), connection + assert "orphaned_goal_state" not in connection, connection + + payload = _guided(project) + transaction = payload["guided_transaction"] + assert transaction.get("blocked_by") != ORPHANED_GOAL_STATE_CONNECTION, transaction + assert "orphaned_goal_state_gate" not in transaction, transaction + assert [step["id"] for step in transaction["ordered_steps"]][1] == ( + "connect_if_needed" + ) + assert payload["command_pack"]["commands"]["goal_start_connect_if_needed"] From a472baedeaa84a004f19ee0bf683d505a977b1e7 Mon Sep 17 00:00:00 2001 From: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:47:54 +0800 Subject: [PATCH 5/6] fix(goal-start): state every absence through one builder The unparseable-registry return built its own connection dict and handed back bootstrap, registration, Todo, activation and quota continuations while Goal state for the same id was still on disk, so the fence was reached only by the absences that happened to call it. All three absence returns now go through absent_goal_connection, which owns the orphan projection once and keeps which absence produced the fence in absent_connection_state and absent_reason. The gate no longer explains why archive and adopt are not reachable yet; that belongs to #4801's remaining work, not to a packet field. Refs #4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> --- examples/bootstrap-command-pack-smoke.py | 95 +++++++++++----- loopx/bootstrap_command_pack.py | 35 +++--- .../goals/orphaned_goal_state.py | 101 +++++------------- .../test_start_goal_orphaned_goal_state.py | 37 +++++-- 4 files changed, 141 insertions(+), 127 deletions(-) diff --git a/examples/bootstrap-command-pack-smoke.py b/examples/bootstrap-command-pack-smoke.py index 22fff575fb..49a3ab62d3 100644 --- a/examples/bootstrap-command-pack-smoke.py +++ b/examples/bootstrap-command-pack-smoke.py @@ -499,15 +499,72 @@ def test_start_goal_guided_blocks_orphaned_goal_state() -> None: assert_fixture_unchanged({registry: registry.read_text(), state_file: snapshot[state_file]}) -def test_start_goal_guided_blocks_orphaned_state_without_a_registry() -> None: - """The same invariant must hold when the reset deleted the whole registry file.""" +def test_start_goal_guided_fences_orphaned_state_for_every_absence_route() -> None: + """No registry authority plus surviving state must fence, however that came about. + + One real CLI run per absence shape: a deleted registry file, a registry that + declares no goal, and a registry that no longer parses each reach the fence + through a different return in the inspection. + """ + + for shape, registry_text, absence in ( + ("missing", None, "not_connected"), + ("empty", '{"schema_version": "0.1", "goals": []}\n', "registry_without_goal"), + ("invalid", '{"schema_version": "0.1", "goals": [', "registry_invalid"), + ): + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "reset-project" + state_file = project / ".codex" / "goals" / "reset-goal" / "ACTIVE_GOAL_STATE.md" + state_file.parent.mkdir(parents=True) + state_text = "# Orphaned goal state written by a retired lane\n" + state_file.write_text(state_text, encoding="utf-8") + if registry_text is not None: + registry = project / ".loopx" / "registry.json" + registry.parent.mkdir(parents=True) + registry.write_text(registry_text, encoding="utf-8") + + payload = run_json( + "start-goal", + "--guided", + "--project", + str(project), + "--goal-id", + "reset-goal", + "--host-surface", + "codex-app", + "--goal-text", + "Continue the interrupted refactor", + ) + + connection = payload["project_connection"] + assert connection["connection_state"] == "orphaned_goal_state", (shape, connection) + assert connection["absent_connection_state"] == absence, (shape, connection) + transaction = payload["guided_transaction"] + assert transaction["blocked_by"] == "orphaned_goal_state", (shape, transaction) + assert [step["id"] for step in transaction["ordered_steps"]] == [ + "inspect_connection", + "resolve_orphaned_goal_state", + ], (shape, transaction) + commands = payload["command_pack"]["commands"] + for key in ( + "goal_start_connect_if_needed", + "goal_start_refresh_state", + "goal_start_host_loop_activation", + "goal_start_quota_should_run", + "goal_start_plan_prompt", + ): + assert commands[key] is None, (shape, key) + assert state_file.read_text(encoding="utf-8") == state_text, shape + + +def test_unparseable_registry_without_orphaned_state_keeps_onboarding() -> None: + """A broken registry is its own repair action; the fence must not stand in for it.""" with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / "reset-project" - state_file = project / ".codex" / "goals" / "reset-goal" / "ACTIVE_GOAL_STATE.md" - state_file.parent.mkdir(parents=True) - state_text = "# Orphaned goal state written by a retired lane\n" - state_file.write_text(state_text, encoding="utf-8") + registry = project / ".loopx" / "registry.json" + registry.parent.mkdir(parents=True) + registry.write_text('{"schema_version": "0.1", "goals": [', encoding="utf-8") payload = run_json( "start-goal", @@ -522,24 +579,11 @@ def test_start_goal_guided_blocks_orphaned_state_without_a_registry() -> None: "Continue the interrupted refactor", ) - assert payload["project_connection"]["connection_state"] == "orphaned_goal_state" - transaction = payload["guided_transaction"] - assert transaction["blocked_by"] == "orphaned_goal_state", transaction - assert [step["id"] for step in transaction["ordered_steps"]] == [ - "inspect_connection", - "resolve_orphaned_goal_state", - ], transaction - commands = payload["command_pack"]["commands"] - for key in ( - "goal_start_connect_if_needed", - "goal_start_refresh_state", - "goal_start_host_loop_activation", - "goal_start_quota_should_run", - "goal_start_plan_prompt", - ): - assert commands[key] is None, key - assert state_file.read_text(encoding="utf-8") == state_text - assert not (project / ".loopx" / "registry.json").exists() + connection = payload["project_connection"] + assert connection["connection_state"] == "registry_invalid", connection + assert "orphaned_goal_state" not in connection, connection + assert connection["reason"], connection + assert payload["command_pack"]["commands"]["goal_start_connect_if_needed"], payload def test_start_goal_guided_requires_explicit_goal_for_multi_goal_project() -> None: @@ -939,7 +983,8 @@ def main() -> int: test_goal_text_invocation_plans_ranked_todos_before_activation() test_start_goal_guided_previews_transaction_without_mutation() test_start_goal_guided_blocks_orphaned_goal_state() - test_start_goal_guided_blocks_orphaned_state_without_a_registry() + test_start_goal_guided_fences_orphaned_state_for_every_absence_route() + test_unparseable_registry_without_orphaned_state_keeps_onboarding() test_start_goal_guided_requires_explicit_goal_for_multi_goal_project() test_connected_project_reuses_existing_state() test_linked_git_worktree_reuses_canonical_source_registry() diff --git a/loopx/bootstrap_command_pack.py b/loopx/bootstrap_command_pack.py index 47ddb97454..a972fd60b2 100644 --- a/loopx/bootstrap_command_pack.py +++ b/loopx/bootstrap_command_pack.py @@ -15,11 +15,10 @@ ) from .control_plane.effect_program import effect_program_from_ordered_steps from .control_plane.goals.orphaned_goal_state import ( + absent_goal_connection, fence_command_pack, guided_fence, - registry_missing_goal_connection, render_guided_lines, - unregistered_goal_connection, ) from .control_plane.goals.start_contract import ( build_goal_start_contract, @@ -578,24 +577,23 @@ def inspect_bootstrap_connection( } if registry_error: - return { - **base_connection, - "registry_exists": registry_exists, - "goal_id": inferred_goal_id, - "goal_found": False, - "state_file": str(state_file), - "state_file_exists": state_file.exists(), - "connection_state": "registry_invalid", - "mutation_confirmation_required": True, - "reason": registry_error, - } + return absent_goal_connection( + base_connection=base_connection, + goal_id=inferred_goal_id, + state_file=state_file, + registry_exists=registry_exists, + absence_connection="registry_invalid", + absence_reason=registry_error, + ) if not registry: - return unregistered_goal_connection( + return absent_goal_connection( base_connection=base_connection, - project=resolved_project, goal_id=inferred_goal_id, state_file=state_file, + registry_exists=False, + absence_connection="not_connected", + absence_reason="project-local .loopx/registry.json is missing", ) goals = registry_goals(registry) @@ -610,13 +608,14 @@ def inspect_bootstrap_connection( state_file = goal_state_file or fallback_state_file if selected_goal is None: - return registry_missing_goal_connection( + return absent_goal_connection( base_connection=base_connection, - project=resolved_project, goal_id=resolved_goal_id, - known_goal_ids=[str(goal.get("id")) for goal in goals], state_file=state_file, registry_exists=True, + absence_connection="registry_without_goal", + absence_reason="registry exists but no matching goal entry was found", + known_goal_ids=[str(goal.get("id")) for goal in goals], ) if not selected_goal.get("state_file"): diff --git a/loopx/control_plane/goals/orphaned_goal_state.py b/loopx/control_plane/goals/orphaned_goal_state.py index 3d30f598f0..1e8a649649 100644 --- a/loopx/control_plane/goals/orphaned_goal_state.py +++ b/loopx/control_plane/goals/orphaned_goal_state.py @@ -8,8 +8,9 @@ detected rather than silently reconnected. This module owns the whole fence so ``loopx.bootstrap_command_pack`` keeps only -its wiring: the detected fact, the operator-facing gate, the packet fields that -must disappear over orphaned state, and the guided transaction's blocking shape. +its wiring: one entry point through which every absence route passes, the +operator-facing gate, the packet fields that must disappear over orphaned state, +and the guided transaction's blocking shape. """ from __future__ import annotations @@ -25,14 +26,6 @@ ORPHANED_GOAL_STATE_GATE_SCHEMA_VERSION = "loopx_orphaned_goal_state_gate_v0" -REGISTRY_WITHOUT_GOAL_CONNECTION = "registry_without_goal" - -NOT_CONNECTED_CONNECTION = "not_connected" - -REGISTRY_WITHOUT_GOAL_REASON = "registry exists but no matching goal entry was found" - -NOT_CONNECTED_REASON = "project-local .loopx/registry.json is missing" - # Project-local goal state has been written under each of these roots, so a goal # absent from the registry can still own durable state in any of them. Ordered # current route first; every match is reported, never merged or copied. @@ -101,10 +94,9 @@ def orphaned_goal_state_projection(project: Path, goal_id: str) -> dict[str, Any } -def goal_connection_without_matching_entry( +def absent_goal_connection( *, base_connection: dict[str, Any], - project: Path, goal_id: str, state_file: Path, registry_exists: bool, @@ -112,13 +104,16 @@ def goal_connection_without_matching_entry( absence_reason: str, known_goal_ids: list[str] | None = None, ) -> dict[str, Any]: - """Classify "no matching registry entry" as orphaned state or as ordinary absence. + """Project "no registry entry carries this goal", fenced if its state survives. - The absence half keeps the packet fields it carried before this fence existed, - including which of them are present, so a fresh project is unaffected. + This is the only way the goal-start flow states an absence, so an unparseable + registry file, a missing or empty one, and a readable registry with no + matching entry cannot drift apart on the safety question. The fields are the + ones the packet carried before this fence existed, so an ordinary absence -- + including a fresh project -- keeps its old onboarding continuation. """ - shared = { + connection: dict[str, Any] = { **base_connection, "registry_exists": registry_exists, "goal_id": goal_id, @@ -126,46 +121,30 @@ def goal_connection_without_matching_entry( "state_file": str(state_file), "state_file_exists": state_file.exists(), "mutation_confirmation_required": True, + "connection_state": absence_connection, + "reason": absence_reason, } if known_goal_ids is not None: - shared["known_goal_ids"] = known_goal_ids - projection = orphaned_goal_state_projection(project, goal_id) + connection["known_goal_ids"] = known_goal_ids + # An empty project would resolve the state roots against the interpreter's + # working directory, which is not a project this packet may speak for. + project = str(base_connection.get("project") or "") + projection = orphaned_goal_state_projection(Path(project), goal_id) if project else None if projection is None: - return { - **shared, - "connection_state": absence_connection, - "reason": absence_reason, - } + return connection return { - **shared, + **connection, "orphaned_goal_state": projection, "connection_state": ORPHANED_GOAL_STATE_CONNECTION, + # Which absence produced the fence stays attributable: a registry that + # cannot be parsed is a different operator action from a deleted entry. + "absent_connection_state": absence_connection, + "absent_reason": absence_reason, "bootstrap_continuation_allowed": False, "reason": ORPHANED_GOAL_STATE_REASON, } -def registry_missing_goal_connection(**fields: Any) -> dict[str, Any]: - """Classify the absence inside a readable, non-empty registry.""" - - return goal_connection_without_matching_entry( - absence_connection=REGISTRY_WITHOUT_GOAL_CONNECTION, - absence_reason=REGISTRY_WITHOUT_GOAL_REASON, - **fields, - ) - - -def unregistered_goal_connection(**fields: Any) -> dict[str, Any]: - """Classify the absence when no readable registry declares any goal.""" - - return goal_connection_without_matching_entry( - registry_exists=False, - absence_connection=NOT_CONNECTED_CONNECTION, - absence_reason=NOT_CONNECTED_REASON, - **fields, - ) - - def orphaned_goal_state_gate( projection: dict[str, Any], *, @@ -199,23 +178,6 @@ def orphaned_goal_state_gate( "backup-state writes nothing until the operator adds --execute, which is " "the auditable archive the resolution depends on" ), - "unavailable_resolution_routes": [ - { - "route": "archive_project_local_state", - "reason": ( - "uninstall-project selects goals from the project registry and " - "archive-runtime selects a goal directory under the shared runtime " - "root, so neither reaches state the registry no longer declares" - ), - }, - { - "route": "adopt_orphan_state", - "reason": ( - "re-registering the goal id is the second authority this fence " - "prevents; adoption needs an explicit instance identity first" - ), - }, - ], "forbidden_until_resolved": list(FORBIDDEN_UNTIL_RESOLVED), } @@ -282,17 +244,10 @@ def render_guided_lines(transaction: dict[str, Any]) -> str: if not isinstance(gate, dict): return "" routes = "\n".join( - [ - f"- `{route.get('route')}` (preview only): " - f"`{str(route.get('command')).splitlines()[-1]}`" - for route in gate.get("resolution_routes") or [] - if isinstance(route, dict) - ] - + [ - f"- `{item.get('route')}` is not available yet: {item.get('reason')}" - for item in gate.get("unavailable_resolution_routes") or [] - if isinstance(item, dict) - ] + f"- `{route.get('route')}` (preview only): " + f"`{str(route.get('command')).splitlines()[-1]}`" + for route in gate.get("resolution_routes") or [] + if isinstance(route, dict) ) return ( "\n## Orphaned Goal State Gate\n\n" diff --git a/tests/control_plane/test_start_goal_orphaned_goal_state.py b/tests/control_plane/test_start_goal_orphaned_goal_state.py index aa3574f406..59511d2c57 100644 --- a/tests/control_plane/test_start_goal_orphaned_goal_state.py +++ b/tests/control_plane/test_start_goal_orphaned_goal_state.py @@ -45,8 +45,9 @@ def _project( """Build a project carrying an orphaned state file for ``orphaned_goal_id``. ``registry`` selects how far the reset got: ``registered`` keeps the other - Goal's entry, ``empty`` keeps the file but declares no Goal, and ``missing`` - removes the project registry entirely. + Goal's entry, ``empty`` keeps the file but declares no Goal, ``missing`` + removes the project registry entirely, and ``invalid`` leaves a file that no + longer parses. """ project = root / "project" @@ -68,7 +69,9 @@ def _project( if registry == "registered" else [] ) - if registry != "missing": + if registry == "invalid": + registry_path.write_text('{"schema_version": "0.1", "goals": [', encoding="utf-8") + elif registry != "missing": registry_path.write_text( json.dumps({"schema_version": "0.1", "goals": entries}, indent=2) + "\n", encoding="utf-8", @@ -193,9 +196,6 @@ def test_guided_packet_offers_only_previews_over_orphaned_state( "inspect_registry_and_state", "preview_state_backup", ] - assert [ - item["route"] for item in gate["unavailable_resolution_routes"] - ] == ["archive_project_local_state", "adopt_orphan_state"] for route in gate["resolution_routes"]: assert route["mutates"] is False assert "--execute" not in route["command"] @@ -224,7 +224,6 @@ def test_guided_packet_carries_no_bootstrap_or_todo_authoring_continuation( message = payload["message"] assert "Orphaned Goal State Gate" in message assert "todo add" not in message - assert "is not available yet" in message assert not [ line for line in message.splitlines() @@ -298,7 +297,7 @@ def test_obeying_agent_has_no_actionable_command_at_the_fence(tmp_path: Path) -> assert unblocked["action_command_ids"] -# ---- the same invariant holds when the reset removed the whole registry ----- +# ---- the same invariant holds whatever shape the reset left the registry in -- def _assert_fenced(payload: dict[str, Any]) -> None: @@ -321,9 +320,16 @@ def _assert_fenced(payload: dict[str, Any]) -> None: assert onboarding_entry_semantic_contract(payload)["action_command_ids"] == [] -@pytest.mark.parametrize("registry", ["missing", "empty"]) +@pytest.mark.parametrize( + ("registry", "absence_connection"), + [ + ("missing", "not_connected"), + ("empty", "registry_without_goal"), + ("invalid", "registry_invalid"), + ], +) def test_reset_without_a_registry_still_fences_surviving_state( - tmp_path: Path, registry: str + tmp_path: Path, registry: str, absence_connection: str ) -> None: project = _project( tmp_path / registry, @@ -336,12 +342,21 @@ def test_reset_without_a_registry_still_fences_surviving_state( assert connection["orphaned_goal_state"]["state_file_routes"] == [ f".codex/goals/{ORPHANED_GOAL_ID}/ACTIVE_GOAL_STATE.md" ], connection + # The fence must not erase which absence it was reached through: a registry + # that cannot be parsed is a different operator action from a deleted entry. + assert connection["absent_connection_state"] == absence_connection, connection + assert connection["absent_reason"], connection + assert connection["reason"] != connection["absent_reason"], connection _assert_fenced(_guided(project)) @pytest.mark.parametrize( ("registry", "absence_connection"), - [("missing", "not_connected"), ("empty", "registry_without_goal")], + [ + ("missing", "not_connected"), + ("empty", "registry_without_goal"), + ("invalid", "registry_invalid"), + ], ) def test_reset_without_orphaned_state_is_still_ordinary_onboarding( tmp_path: Path, registry: str, absence_connection: str From c78705998ddf4d15b38e2362bb1a13bd90b4b7f6 Mon Sep 17 00:00:00 2001 From: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:16:51 +0800 Subject: [PATCH 6/6] fix(goal-start): rebuild the fenced packet from what is safe to run The fence nulled seven top-level command keys, so the same payload still offered `loopx register-agent --require-new --execute` through nested registration and identity-gate fields, the issue-fix post-PR templates, the slash-command catalog and the onboarding hint -- a continuation by instruction, contradicting this very packet's forbidden_until_resolved. `commands` is now rebuilt from an allowlist that keeps only `status`, the verb-bearing subtrees are dropped whole, and a fenced standalone pack renders its own body instead of the connect/plan/activate template. The guard over what a packet may spell out lives in control_plane/testing so the focused test and the shipped CLI smoke enforce one rule across every surface, including the rendered Markdown. Refs #4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com> --- examples/bootstrap-command-pack-smoke.py | 55 +++++++++++++ loopx/bootstrap_command_pack.py | 4 + .../goals/orphaned_goal_state.py | 80 ++++++++++++++----- .../testing/continuation_verb_guard.py | 77 ++++++++++++++++++ .../test_start_goal_orphaned_goal_state.py | 26 ++++++ 5 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 loopx/control_plane/testing/continuation_verb_guard.py diff --git a/examples/bootstrap-command-pack-smoke.py b/examples/bootstrap-command-pack-smoke.py index 49a3ab62d3..85a09a2e36 100644 --- a/examples/bootstrap-command-pack-smoke.py +++ b/examples/bootstrap-command-pack-smoke.py @@ -10,6 +10,12 @@ REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from loopx.control_plane.testing.continuation_verb_guard import ( # noqa: E402 + assert_no_continuation_verb, +) def run_json(*args: str, env: dict[str, str] | None = None) -> dict[str, object]: @@ -586,6 +592,54 @@ def test_unparseable_registry_without_orphaned_state_keeps_onboarding() -> None: assert payload["command_pack"]["commands"]["goal_start_connect_if_needed"], payload +def test_fenced_project_surfaces_offer_no_continuation() -> None: + """No real CLI surface over orphaned state may spell out a runnable mutation. + + The guided packet with its full command pack, and the standalone command pack + with its rendered message, are both executed by hosts. Each carried + ``register-agent --execute`` in nested fields the top-level fence never read. + """ + + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) / "reset-project" + state_file = project / ".codex" / "goals" / "reset-goal" / "ACTIVE_GOAL_STATE.md" + state_file.parent.mkdir(parents=True) + state_file.write_text( + "# Orphaned goal state written by a retired lane\n", encoding="utf-8" + ) + guided = run_json( + "start-goal", + "--guided", + "--include-command-pack-detail", + "--project", + str(project), + "--goal-id", + "reset-goal", + "--host-surface", + "codex-app", + "--goal-text", + "Continue the interrupted refactor", + ) + standalone = run_json( + "bootstrap-command-pack", + "--project", + str(project), + "--goal-id", + "reset-goal", + "--host-surface", + "codex-app", + ) + + assert guided["project_connection"]["connection_state"] == "orphaned_goal_state" + assert standalone["project_connection"]["connection_state"] == "orphaned_goal_state" + assert_no_continuation_verb(guided, source="guided with command pack detail") + assert_no_continuation_verb(standalone, source="standalone command pack") + # Without a surviving read-only route the guard above could pass on a + # packet that tells the operator nothing at all. + assert guided["command_pack"]["commands"]["status"] + assert state_file.is_file() + + def test_start_goal_guided_requires_explicit_goal_for_multi_goal_project() -> None: with tempfile.TemporaryDirectory() as tmp: project = Path(tmp) / "multi-goal-project" @@ -985,6 +1039,7 @@ def main() -> int: test_start_goal_guided_blocks_orphaned_goal_state() test_start_goal_guided_fences_orphaned_state_for_every_absence_route() test_unparseable_registry_without_orphaned_state_keeps_onboarding() + test_fenced_project_surfaces_offer_no_continuation() test_start_goal_guided_requires_explicit_goal_for_multi_goal_project() test_connected_project_reuses_existing_state() test_linked_git_worktree_reuses_canonical_source_registry() diff --git a/loopx/bootstrap_command_pack.py b/loopx/bootstrap_command_pack.py index a972fd60b2..c548df0c82 100644 --- a/loopx/bootstrap_command_pack.py +++ b/loopx/bootstrap_command_pack.py @@ -15,8 +15,10 @@ ) from .control_plane.effect_program import effect_program_from_ordered_steps from .control_plane.goals.orphaned_goal_state import ( + ORPHANED_GOAL_STATE_CONNECTION, absent_goal_connection, fence_command_pack, + fenced_standalone_message, guided_fence, render_guided_lines, ) @@ -1932,6 +1934,8 @@ def actionable_shell_command(value: Any) -> str: def render_loopx_bootstrap_command_pack_message(payload: dict[str, Any]) -> str: connection = payload.get("project_connection") connection = connection if isinstance(connection, dict) else {} + if connection.get("connection_state") == ORPHANED_GOAL_STATE_CONNECTION: + return fenced_standalone_message(payload) commands = payload.get("commands") commands = commands if isinstance(commands, dict) else {} next_step = payload.get("recommended_next_step") diff --git a/loopx/control_plane/goals/orphaned_goal_state.py b/loopx/control_plane/goals/orphaned_goal_state.py index 1e8a649649..3a09dc83e0 100644 --- a/loopx/control_plane/goals/orphaned_goal_state.py +++ b/loopx/control_plane/goals/orphaned_goal_state.py @@ -49,19 +49,26 @@ "host_loop_activation", ) -# The command-pack keys that carry a mutation continuation, including the four the -# onboarding entry contract treats as one connect -> writeback -> activate -> quota -# loop. Over orphaned state none of them may be offered, goal text or not. -MUTATION_CONTINUATION_COMMANDS = ( - "goal_start_connect_if_needed", - "bootstrap_dry_run_preview", - "bootstrap_after_user_confirmation", - "goal_start_plan_prompt", - "goal_start_refresh_state", - "goal_start_host_loop_activation", - "goal_start_quota_should_run", +# Over orphaned state the packet is rebuilt from what is safe to run rather than +# from verbs to suppress: `status` is the only command-pack entry point left, and +# the resolution gate below is the only other source of runnable commands. A key +# a future builder adds to the pack is then withheld by default instead of having +# to be remembered here. +FENCED_COMMAND_KEYS = ("status",) + +# Subtrees whose fields carry a continuation verb -- registration, activation +# input and steps, the heartbeat prompt, and the planner's post-PR routes. They +# are dropped whole; every consumer reads them through an isinstance guard. +# `available_slash_commands` and `onboarding_hint` are dropped for the same +# reason as the rendered text: both spell out the verbs this fence withholds. +FENCED_SUBTREES = ( + "available_slash_commands", + "host_loop_activation", + "onboarding_hint", ) +FENCED_CONTRACT_KEYS = ("activation", "domain_route_hints", "execution_invariants") + def orphaned_goal_state_routes(project: Path, goal_id: str) -> list[str]: """List existing state files for ``goal_id`` as project-relative routes. @@ -194,8 +201,16 @@ def fence_command_pack(command_pack: dict[str, Any], *, command_prefix: str) -> command_prefix=command_prefix, status_command=str(command_pack["commands"]["status"]), ) - for key in MUTATION_CONTINUATION_COMMANDS: - command_pack["commands"][key] = None + commands = command_pack["commands"] + for key in list(commands): + if key not in FENCED_COMMAND_KEYS: + commands[key] = None + for key in FENCED_SUBTREES: + command_pack[key] = None + contract = command_pack.get("goal_start_contract") + if isinstance(contract, dict): + for key in FENCED_CONTRACT_KEYS: + contract.pop(key, None) safety = command_pack["safety_contract"] safety["orphaned_goal_state_blocks_continuation"] = True safety["mutation_requires_user_confirmation"] = True @@ -237,18 +252,45 @@ def guided_fence(gate: dict[str, Any]) -> dict[str, Any]: } +def _preview_route_lines(gate: dict[str, Any]) -> str: + return "\n".join( + f"- `{route.get('route')}` (preview only): " + f"`{str(route.get('command')).splitlines()[-1]}`" + for route in gate.get("resolution_routes") or [] + if isinstance(route, dict) + ) + + +def fenced_standalone_message(command_pack: dict[str, Any]) -> str: + """Render the standalone command pack for an orphaned Goal. + + The shared renderer's body is connect, plan-write and activation guidance -- + exactly what this fence withholds -- so a fenced pack states its own two + read-only routes instead of that text with holes punched in it. + """ + + gate = command_pack.get("orphaned_goal_state") or {} + return ( + "# LoopX Bootstrap Command Pack\n\n" + f"- project: `{command_pack.get('project')}`\n" + f"- goal_id: `{command_pack.get('goal_id')}`\n" + f"- connection_state: `{ORPHANED_GOAL_STATE_CONNECTION}`\n\n" + f"{gate.get('reason')}\n\n" + f"- withheld until an operator resolves it: " + f"{', '.join(str(name) for name in gate.get('forbidden_until_resolved') or [])}\n" + f"- {gate.get('execution_boundary')}\n\n" + "## Read-only routes\n\n" + f"{_preview_route_lines(gate)}\n" + ) + + def render_guided_lines(transaction: dict[str, Any]) -> str: """Render the orphan gate section of the guided Markdown, or nothing.""" gate = transaction.get("orphaned_goal_state_gate") if not isinstance(gate, dict): return "" - routes = "\n".join( - f"- `{route.get('route')}` (preview only): " - f"`{str(route.get('command')).splitlines()[-1]}`" - for route in gate.get("resolution_routes") or [] - if isinstance(route, dict) - ) + routes = _preview_route_lines(gate) return ( "\n## Orphaned Goal State Gate\n\n" f"{gate.get('reason')}\n\n" diff --git a/loopx/control_plane/testing/continuation_verb_guard.py b/loopx/control_plane/testing/continuation_verb_guard.py new file mode 100644 index 0000000000..30dd8e4552 --- /dev/null +++ b/loopx/control_plane/testing/continuation_verb_guard.py @@ -0,0 +1,77 @@ +"""One rule for which commands a blocked packet may spell out, shared by tests and smokes. + +A packet that withholds a continuation is only as safe as every surface that +carries it: the top-level command map, nested registration and activation +gates, the slash-command catalog, and the Markdown a host reads. This module +states the rule once so a unit test and a shipped CLI smoke cannot drift apart +on what counts as a runnable verb. +""" + +from __future__ import annotations + +import re +from typing import Any, Iterator + +COMMAND_FAMILY = re.compile(r"loopx\s+([a-z][a-z-]*)") + +# Every command family that can move Goal state, a lane, a lease, quota or a host +# loop. Read-only inspection stays out: `status`, `doctor`, +# `bootstrap-command-pack`, and a `backup-state` preview without `--execute`. +MUTATION_COMMAND_FAMILIES = frozenset( + { + "agent-onboard", + "bind-agent-thread", + "bootstrap", + "configure-goal", + "connect", + "heartbeat-prompt", + "issue-fix", + "quota", + "refresh-state", + "register-agent", + "start-goal", + "task-lease", + "todo", + } +) + + +def _strings(value: Any, path: str) -> Iterator[tuple[str, str]]: + if isinstance(value, dict): + for key, item in value.items(): + yield from _strings(item, f"{path}.{key}") + elif isinstance(value, list): + for index, item in enumerate(value): + yield from _strings(item, f"{path}[{index}]") + elif isinstance(value, str): + yield path, value + + +def continuation_verb_findings(payload: Any) -> list[tuple[str, str]]: + """Every line in ``payload`` that spells a runnable mutation or ``--execute``. + + Judged per line: a runnable command always carries its own ``--execute`` on + the same line, while prose that only explains a preview boundary mentions the + two apart. + """ + + return [ + (path, line[:120]) + for path, text in _strings(payload, "packet") + for line in text.splitlines() + if "loopx " in line + and ( + "--execute" in line + or any( + family in MUTATION_COMMAND_FAMILIES + for family in COMMAND_FAMILY.findall(line) + ) + ) + ] + + +def assert_no_continuation_verb(payload: Any, *, source: str = "packet") -> None: + """Fail on any command this packet's surfaces still offer for mutation.""" + + findings = continuation_verb_findings(payload) + assert findings == [], (source, findings) diff --git a/tests/control_plane/test_start_goal_orphaned_goal_state.py b/tests/control_plane/test_start_goal_orphaned_goal_state.py index 59511d2c57..f9c60dbf94 100644 --- a/tests/control_plane/test_start_goal_orphaned_goal_state.py +++ b/tests/control_plane/test_start_goal_orphaned_goal_state.py @@ -19,12 +19,14 @@ build_loopx_bootstrap_command_pack, build_start_goal_guided_packet, inspect_bootstrap_connection, + render_loopx_bootstrap_command_pack_message, ) from loopx.control_plane.goals.orphaned_goal_state import ( GOAL_STATE_ROOTS, ORPHANED_GOAL_STATE_CONNECTION, orphaned_goal_state_routes, ) +from loopx.control_plane.testing.continuation_verb_guard import assert_no_continuation_verb from loopx.control_plane.testing.onboarding_model_behavior_qualification import ( onboarding_entry_contract_violations, onboarding_entry_semantic_contract, @@ -297,6 +299,30 @@ def test_obeying_agent_has_no_actionable_command_at_the_fence(tmp_path: Path) -> assert unblocked["action_command_ids"] +# ---- no consumption surface may keep a continuation verb --------------------- + + +def test_every_fenced_consumption_surface_is_read_only(tmp_path: Path) -> None: + project = _project(tmp_path, orphaned_state_dirs=(".codex/goals",)) + + guided = _guided(project) + command_pack = _command_pack(project) + + assert_no_continuation_verb(guided, source="guided_packet") + assert_no_continuation_verb(command_pack, source="standalone_command_pack") + assert_no_continuation_verb( + render_loopx_bootstrap_command_pack_message(command_pack), + source="standalone_rendered_message", + ) + # The guard above must not be able to pass on a packet that has nothing left + # to tell the operator: the read-only half survives the rebuild. + assert command_pack["commands"]["status"] + assert [ + route["route"] + for route in guided["guided_transaction"]["orphaned_goal_state_gate"]["resolution_routes"] + ] == ["inspect_registry_and_state", "preview_state_backup"] + + # ---- the same invariant holds whatever shape the reset left the registry in --