From 53cefbc046515be26d08ac83f1790a85670832e1 Mon Sep 17 00:00:00 2001 From: EnzhenGu <107248786+Exusty@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:04:58 +0800 Subject: [PATCH] fix(bootstrap): isolate generated objective text from Todo parsing A goal objective is user prose, but both state writers rendered it straight into the document body. An objective that collapsed into one fence line, opened a tilde fence, opened an HTML comment, or merely looked like a Todo row then hid or polluted the generated Todo sections below it: `parse_todo_source` reported no source sections and zero items, and a Todo-shaped objective example was adopted as real work. Wrap generated objective text in an isolated `loopx:objective-v0` region in `render_state_markdown` and in the project registry `_state_markdown`, and teach `visible_markdown_lines` to treat the enclosed lines as prose. Fences and comments outside the region still parse exactly as before, so fence handling is not weakened and Todo authority is unchanged. Legacy state documents without the region keep their current readback. Add `read_objective_text` so the dashboard goal context recovers the objective verbatim, including objectives that begin with an HTML comment, and keep the previous section scan as the fallback for unmarked state. Add tests/control_plane/test_objective_todo_visibility.py covering both writers for fenced, tilde-fenced, HTML-comment, and Todo-shaped objectives plus plain-text and closed-fence controls. Closes #4401 Signed-off-by: Exusty <107248786+Exusty@users.noreply.github.com> --- loopx/bootstrap.py | 6 +- loopx/chat_server.py | 6 +- .../goals/active_state_metadata.py | 42 ++++++ loopx/control_plane/projects/registry.py | 6 +- loopx/control_plane/todos/machine_region.py | 19 ++- .../test_objective_todo_visibility.py | 131 ++++++++++++++++++ 6 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 tests/control_plane/test_objective_todo_visibility.py diff --git a/loopx/bootstrap.py b/loopx/bootstrap.py index 4138655e83..3e501f8416 100644 --- a/loopx/bootstrap.py +++ b/loopx/bootstrap.py @@ -13,6 +13,7 @@ from .control_plane.runtime.time import now_local_iso from .control_plane.runtime.public_safety import public_safe_compact_text +from .control_plane.goals.active_state_metadata import render_objective_block from .control_plane.todos.active_state_editing import ( TODO_SECTION_HEADINGS, atomic_write_state_text, @@ -504,6 +505,9 @@ def render_state_markdown( handoff_mode_line = ( f"handoff_mode: {handoff_mode}\n" if handoff_mode != HANDOFF_MODE_LEGACY else "" ) + # The objective is user prose. Isolate it so a fenced or commented example + # inside it cannot hide the generated Todo sections below. + objective_block = render_objective_block(objective) state_text = f"""--- status: active owner_mode: goal @@ -516,7 +520,7 @@ def render_state_markdown( ## Objective -{objective} +{objective_block} ## Authority Sources diff --git a/loopx/chat_server.py b/loopx/chat_server.py index 32ec182d90..31cace85c4 100644 --- a/loopx/chat_server.py +++ b/loopx/chat_server.py @@ -37,6 +37,7 @@ from .chat_ssh_source_api import SshSourceRequestMixin from .chat_store import ChatSessionStore from .capabilities.manager_runtime import manager_runtime_capability_projection +from .control_plane.goals.active_state_metadata import read_objective_text from .control_plane.status.ssh_host_catalog import ( SSH_HOST_CATALOG_PATH, ssh_host_catalog_payload, @@ -149,7 +150,10 @@ def _goal_public_context(registry: dict[str, Any], goal: dict[str, Any]) -> dict if state_path is not None and state_path.exists(): try: state_text = state_path.read_text(encoding="utf-8") - objective = _active_state_section(state_text, "Objective") + objective = ( + _compact_text(read_objective_text(state_text)) + or _active_state_section(state_text, "Objective") + ) title_line = next( (line[2:].strip() for line in state_text.splitlines() if line.startswith("# ")), "", diff --git a/loopx/control_plane/goals/active_state_metadata.py b/loopx/control_plane/goals/active_state_metadata.py index d65b7dc286..38af65b764 100644 --- a/loopx/control_plane/goals/active_state_metadata.py +++ b/loopx/control_plane/goals/active_state_metadata.py @@ -20,6 +20,48 @@ "待办归档", ) +# Generated objective text is presentation data, never machine-owned Markdown. +# Objectives are arbitrary user prose, so they may contain fenced blocks, tilde +# fences, HTML comments, or text that merely looks like a Todo row. Writing one +# straight into the document body lets it open a construct that swallows the +# generated Todo sections below it. The markers below isolate that prose so +# readers keep treating it as text. +OBJECTIVE_REGION_BEGIN = "" +OBJECTIVE_REGION_END = "" + + +def render_objective_block(objective: str) -> str: + """Render generated objective text as an isolated, non-authoritative block. + + The returned text preserves the objective verbatim between two marker + comments, so a direct objective readback still sees the prose while + Markdown readers never let it open a fence, open an HTML comment, or + contribute rows to the Todo sections that follow. + """ + text = str(objective or "").strip("\n") + if not text.strip(): + return "" + return f"{OBJECTIVE_REGION_BEGIN}\n{text}\n{OBJECTIVE_REGION_END}" + + +def read_objective_text(state_text: str) -> str: + """Read back the generated objective text from an isolated region. + + Returns an empty string for legacy state documents written before the + objective region existed, so callers keep their previous fallback. + """ + lines = str(state_text or "").splitlines() + start = next( + (i for i, line in enumerate(lines) if line.strip() == OBJECTIVE_REGION_BEGIN), None + ) + if start is None: + return "" + end = next( + (i for i in range(start + 1, len(lines)) if lines[i].strip() == OBJECTIVE_REGION_END), + len(lines), + ) + return "\n".join(lines[start + 1 : end]).strip() + def parse_state_frontmatter(state_text: str) -> dict[str, str]: if not state_text.startswith("---"): diff --git a/loopx/control_plane/projects/registry.py b/loopx/control_plane/projects/registry.py index b0dd31bcab..e78a6f4b5d 100644 --- a/loopx/control_plane/projects/registry.py +++ b/loopx/control_plane/projects/registry.py @@ -14,6 +14,7 @@ from ...paths import DEFAULT_RUNTIME_ROOT from ...registry import atomic_write_json from ...repository_identity import normalize_repository_identity +from ..goals.active_state_metadata import render_objective_block from .contract import validate_project_record_bindings PROJECT_KINDS = ("work", "personal") @@ -129,6 +130,9 @@ def _state_markdown( def bullets(items: list[str], *, empty: str) -> str: return "\n".join(f"- {item}" for item in items) if items else f"- {empty}" + # The objective is user prose. Isolate it so a fenced or commented example + # inside it cannot hide the generated Todo sections below. + objective_block = render_objective_block(objective) return f"""--- status: active owner_mode: goal @@ -142,7 +146,7 @@ def bullets(items: list[str], *, empty: str) -> str: ## Objective -{objective} +{objective_block} ## Acceptance diff --git a/loopx/control_plane/todos/machine_region.py b/loopx/control_plane/todos/machine_region.py index 381daf74b2..b3b6f4f598 100644 --- a/loopx/control_plane/todos/machine_region.py +++ b/loopx/control_plane/todos/machine_region.py @@ -11,7 +11,12 @@ from dataclasses import dataclass from .contract import TODO_TASK_PATTERN, parse_todo_metadata_line -from ..goals.active_state_metadata import TODO_ARCHIVE_HEADER_MARKERS, todo_role_for_heading +from ..goals.active_state_metadata import ( + OBJECTIVE_REGION_BEGIN, + OBJECTIVE_REGION_END, + TODO_ARCHIVE_HEADER_MARKERS, + todo_role_for_heading, +) TODO_REGION_PREFIX = "