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/4] 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/4] 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/4] 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/4] 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"]