Skip to content

Commit 6fccb53

Browse files
authored
Merge pull request #4402 from ehz0ah/codex/fix-fenced-goal-prompt
Isolate Objective presentation from Todo structure and unify existing Goal metadata/section readback. Preserve legacy registration no-op behavior, reject semantic drift, and document the bounded Python presentation adapter under the TS/shared-authority RFCs. Retain the original contributor commits and the maintainer companion refactor. Exact-head review and integration-tree validation are recorded on PR #4402. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
2 parents a0b5bcb + 6c83dec commit 6fccb53

12 files changed

Lines changed: 331 additions & 34 deletions

‎docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2928,6 +2928,11 @@ an equal-byte retry syncs file and directory before reporting `current`. Narrati
29282928
canonical records stay intact. This converges the retained Python presentation/legacy
29292929
input adapter; it adds no RPC or business state machine and does not change TS authority
29302930
transactions, provider defaults, SQLite D2 or D3 promotion requirements.
2931+
Objective examples are now isolated by both document producers, and display
2932+
readback shares the Goal codec. Canonical Todo reads remain independent of
2933+
malformed display; Objective narrative is not added to the Todo store or its
2934+
recovery scope. This is a bounded D1 adapter correction, not D1–D3 qualification.
2935+
See the [document boundary](../../reference/protocols/active-state-structured-projection-v0.md#markdown-ownership-boundary).
29312936
Canonical handoff-mode show/set no longer depend on Markdown frontmatter or local lease files. One TS transaction binds quiescence, mode and durable operation replay to the same revision, including sealed no-op intents. This adds a provider-neutral command boundary, not a provider default or whole-Goal cutover; frontmatter remains outside the Todo-section renderer. See [operation and recovery](../../reference/handoff-mode.md).
29322937

29332938
T2 now commits a lease-free native Monitor observation and its independent

‎docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2318,6 +2318,10 @@ fenced 示例被当成真实任务、归档 end marker 后叙述进入历史、
23182318
这是永久 Python 展示/legacy 输入适配层的收敛:TS authority transaction、provider
23192319
默认值、SQLite D2 与 D3 promotion 合同不变,不增加 RPC 或另一份业务状态机。
23202320
Canonical handoff-mode show/set 不再依赖 Markdown frontmatter 或本地 lease;一笔 TS 事务把空闲检查、mode 与耐久操作回执绑定到同一 revision,包括未改值请求的回执。该命令边界不切换默认 provider、不晋升整 Goal;frontmatter 仍不属于 Todo-section renderer。操作与恢复见 [handoff-mode](../../reference/handoff-mode.md)。
2321+
两个文档生成入口现在隔离 Objective 示例,展示读回复用 Goal codec。Canonical Todo
2322+
读取仍独立于畸形展示;Objective 叙述不进入 Todo store,也不扩大其恢复范围。
2323+
这是有界 D1 适配层修正,不代表 D1–D3 资格化。
2324+
见[文档边界](../../reference/protocols/active-state-structured-projection-v0.md#markdown-ownership-boundary)。
23212325

23222326
能力缺口 consumer 在 legacy/canonical 输入上共用 TS requirement/resolution owner,
23232327
包括 quota 的 Monitor 能力分流。删除 Python missing-set 与 owner/repair 决策 builder,

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,11 @@ owns durable truth, recovery, cutover, and projection delivery. Neither a fully
397397
TypeScript CLI nor `loopxd` is a prerequisite for removing Python decisions.
398398
An input adapter or external-effect executor may remain Python.
399399

400+
Objective serialization and readback now share the existing Goal metadata and
401+
section owners; legacy registration compares decoded values and exact narrative.
402+
This closes a presentation boundary without adding TS transport or moving business
403+
authority. See the [document boundary](../../reference/protocols/active-state-structured-projection-v0.md#markdown-ownership-boundary).
404+
400405
The lifecycle-admission slice now uses `todo_lifecycle_decision.ts` for legacy
401406
claim/update admission, delegated action/reason checks, ownership-holder routing,
402407
and native complete/supersede. Native text/note edits and terminal transitions

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ Markdown renderer 长期保留。
313313
负责 durable truth、恢复、cutover 与投影交付。删除 Python decision 不以前端 CLI
314314
全部改成 TypeScript 或 `loopxd` 落地为前提;输入适配和外部 effect 执行可以保留 Python。
315315

316+
Objective 编码与读回复用既有 Goal metadata/section owner;旧注册按解码后的值与
317+
精确叙述正文比较。这是展示边界闭合,不增加 TS transport,也不迁移业务权威。
318+
见[文档边界](../../reference/protocols/active-state-structured-projection-v0.md#markdown-ownership-boundary)。
319+
316320
本次 lifecycle-admission 切片将 legacy claim/update 准入、委托 action/reason 检查、
317321
ownership-holder 路由及 native complete/supersede 统一到
318322
`todo_lifecycle_decision.ts`。Native text/note 编辑与 terminal transition 在进程内

‎docs/reference/protocols/active-state-structured-projection-v0.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,25 @@ Directly editing a projection is not a state transition.
119119

120120
## Markdown Ownership Boundary
121121

122+
New bootstrap and project-registration documents quote each Objective line and
123+
escape HTML metacharacters. Fences, comments, headings, and Todo markers in the
124+
objective remain content rather than document structure. Frontmatter string
125+
encoding and readback share JSON semantics, including escaped Unicode line
126+
separators; only complete delimiter lines terminate frontmatter. Objective
127+
readback composes the existing section reader and decodes generated quotation.
128+
Registration compares metadata values and exact remaining narrative, accepting
129+
legacy Objective presentation without rewriting it; changed content still conflicts.
130+
131+
This is the permanent Python presentation/legacy-input adapter described by the
132+
[TypeScript RFC](../../architecture/rfcs/typescript-control-plane-migration-v0.md#next-delivery-sequence)
133+
and [shared-authority RFC](../../architecture/rfcs/shared-goal-authority-state-provider-v0.md#next-delivery-and-parallel-provider-work).
134+
It adds no business rule, RPC, provider, or authority write. Post-cutover Todo
135+
consumers still read canonical state when Markdown is absent or malformed;
136+
rendering never imports Objective examples into that state. Before cutover,
137+
the existing legacy writer remains subject to its normal fence. Objective is
138+
independent Goal narrative, outside the Todo store and Todo-section recovery.
139+
Existing malformed documents are not automatically repaired.
140+
122141
Markdown is not one undifferentiated database row. Agents generate and maintain
123142
both its structured sections and narrative through LoopX. The distinction is
124143
canonical ownership, not human versus Agent authorship: after promotion,

‎loopx/bootstrap.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
MULTI_SUBAGENT_ORCHESTRATION_MODE,
4141
)
4242
from .paths import rel_or_abs, resolve_runtime_root
43+
from .control_plane.goals.active_state_metadata import markdown_blockquote, markdown_frontmatter_string
4344
from .registry_writability import probe_registry_write_path
4445
from .todos import add_todo_to_lines
4546

@@ -481,7 +482,7 @@ def render_state_markdown(
481482
include_connection_validation: bool = True,
482483
handoff_mode: str = HANDOFF_MODE_LEGACY,
483484
) -> str:
484-
safe_objective = objective.replace('"', '\\"')
485+
safe_objective = markdown_frontmatter_string(objective)
485486
profile_summary = execution_profile_summary(execution_profile)
486487
onboarding_markdown = render_onboarding_state_markdown(
487488
onboarding_scan=onboarding_scan,
@@ -507,7 +508,7 @@ def render_state_markdown(
507508
state_text = f"""---
508509
status: active
509510
owner_mode: goal
510-
objective: "{safe_objective}"
511+
objective: {safe_objective}
511512
updated_at: {updated_at}
512513
adapter_id: {goal_id}
513514
{handoff_mode_line}---
@@ -516,7 +517,7 @@ def render_state_markdown(
516517
517518
## Objective
518519
519-
{objective}
520+
{markdown_blockquote(objective)}
520521
521522
## Authority Sources
522523

‎loopx/chat_server.py‎

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from .chat_store import ChatSessionStore
3939
from .capabilities.manager_runtime import manager_runtime_capability_projection
4040
from .capabilities.manager_context.roundtrip import project_chat_session_snapshot
41+
from .control_plane.goals.active_state_metadata import active_state_section_text
4142
from .control_plane.status.ssh_host_catalog import (
4243
SSH_HOST_CATALOG_PATH,
4344
ssh_host_catalog_payload,
@@ -123,22 +124,6 @@ def _compact_text(value: Any, *, limit: int = 600) -> str:
123124
return " ".join(str(value or "").split())[:limit].strip()
124125

125126

126-
def _active_state_section(state_text: str, heading: str) -> str:
127-
marker = f"## {heading}"
128-
start = state_text.find(marker)
129-
if start < 0:
130-
return ""
131-
content_start = start + len(marker)
132-
end = state_text.find("\n## ", content_start)
133-
section = state_text[content_start : end if end >= 0 else None]
134-
lines = [
135-
line.strip().removeprefix("- ").strip()
136-
for line in section.splitlines()
137-
if line.strip() and not line.lstrip().startswith("<!--")
138-
]
139-
return _compact_text(" ".join(lines))
140-
141-
142127
def _goal_public_context(registry: dict[str, Any], goal: dict[str, Any]) -> dict[str, Any]:
143128
goal_id = str(goal.get("id") or "")
144129
project = Path(str(goal.get("repo") or ".")).expanduser().resolve()
@@ -148,7 +133,7 @@ def _goal_public_context(registry: dict[str, Any], goal: dict[str, Any]) -> dict
148133
if state_path is not None and state_path.exists():
149134
try:
150135
state_text = state_path.read_text(encoding="utf-8")
151-
objective = _active_state_section(state_text, "Objective")
136+
objective = _compact_text(active_state_section_text(state_text, "Objective"))
152137
title_line = next(
153138
(line[2:].strip() for line in state_text.splitlines() if line.startswith("# ")),
154139
"",

‎loopx/control_plane/goals/active_state_metadata.py‎

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from __future__ import annotations
22

3+
import json
4+
import re
5+
from html import escape, unescape
6+
7+
from .active_state_sections import active_state_sections
8+
39

410
USER_TODO_HEADER_MARKERS = (
511
"user todo",
@@ -21,19 +27,63 @@
2127
)
2228

2329

24-
def parse_state_frontmatter(state_text: str) -> dict[str, str]:
25-
if not state_text.startswith("---"):
26-
return {}
27-
parts = state_text.split("---", 2)
28-
if len(parts) < 3:
29-
return {}
30+
def markdown_frontmatter_string(value: str) -> str:
31+
encoded = json.dumps(value, ensure_ascii=False)
32+
for separator in ("\x85", "\u2028", "\u2029"):
33+
encoded = encoded.replace(separator, f"\\u{ord(separator):04x}")
34+
return encoded
35+
36+
37+
def markdown_blockquote(value: str) -> str:
38+
return "\n".join(f"> {escape(line, quote=False)}" for line in value.splitlines())
39+
40+
41+
def active_state_section_text(state_text: str, heading: str) -> str:
42+
_, body = split_state_frontmatter(state_text)
43+
sections = active_state_sections(
44+
body, (heading,), section_heading_pattern=re.compile(r"^## (.+?)[ \t]*$"),
45+
)
46+
section_lines = [line for line in sections[heading] if line]
47+
if heading == "Objective" and section_lines and all(
48+
line.startswith("> ") for line in section_lines
49+
):
50+
text = " ".join(unescape(line[2:]) for line in section_lines)
51+
else:
52+
text = " ".join(
53+
line.strip().removeprefix("- ").strip()
54+
for line in section_lines
55+
if line.strip() and not line.lstrip().startswith("<!--")
56+
)
57+
return " ".join(text.split())
58+
59+
60+
def split_state_frontmatter(state_text: str) -> tuple[dict[str, str], str]:
61+
"""Decode generated string metadata; retain the legacy unquoted input form."""
62+
match = re.match(r"\A---[ \t]*\r?\n(.*?)^---[ \t]*(?:\r?\n|\Z)", state_text, re.M | re.S)
63+
if match is None:
64+
return {}, state_text
3065
result: dict[str, str] = {}
31-
for line in parts[1].splitlines():
66+
# JSON strings may contain Unicode separators: only physical LF ends a field.
67+
for line in match.group(1).split("\n"):
3268
if ":" not in line:
3369
continue
3470
key, value = line.split(":", 1)
35-
result[key.strip()] = value.strip().strip('"')
36-
return result
71+
value = value.strip()
72+
if value.startswith('"'):
73+
try:
74+
decoded = json.loads(value)
75+
except json.JSONDecodeError:
76+
pass # Legacy writers did not escape backslashes as JSON.
77+
else:
78+
if isinstance(decoded, str):
79+
result[key.strip()] = decoded
80+
continue
81+
result[key.strip()] = value.strip('"')
82+
return result, state_text[match.end():]
83+
84+
85+
def parse_state_frontmatter(state_text: str) -> dict[str, str]:
86+
return split_state_frontmatter(state_text)[0]
3787

3888

3989
def todo_role_for_heading(heading: str) -> str | None:

‎loopx/control_plane/projects/registry.py‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
from ..coordination.legacy_writer_fence import legacy_todo_write_transaction, require_legacy_state_replacement_allowed
1414
from ...paths import DEFAULT_RUNTIME_ROOT
1515
from ...registry import atomic_write_json
16+
from ..goals.active_state_metadata import (
17+
markdown_blockquote, markdown_frontmatter_string, split_state_frontmatter,
18+
)
1619
from ...repository_identity import normalize_repository_identity
1720
from .contract import validate_project_record_bindings
1821

@@ -133,7 +136,7 @@ def bullets(items: list[str], *, empty: str) -> str:
133136
status: active
134137
owner_mode: goal
135138
project_id: {json.dumps(project_id, ensure_ascii=False)}
136-
objective: {json.dumps(objective, ensure_ascii=False)}
139+
objective: {markdown_frontmatter_string(objective)}
137140
updated_at: {updated_at}
138141
adapter_id: {goal_id}
139142
---
@@ -142,7 +145,7 @@ def bullets(items: list[str], *, empty: str) -> str:
142145
143146
## Objective
144147
145-
{objective}
148+
{markdown_blockquote(objective)}
146149
147150
## Acceptance
148151
@@ -174,6 +177,22 @@ def bullets(items: list[str], *, empty: str) -> str:
174177
"""
175178

176179

180+
def _registration_state_matches(existing: str, expected: str, *, objective: str) -> bool:
181+
"""Compare metadata values and exact narrative without rewriting old state."""
182+
existing_metadata, existing_body = split_state_frontmatter(existing)
183+
expected_metadata, expected_body = split_state_frontmatter(expected)
184+
if existing_metadata != expected_metadata:
185+
return False
186+
marker = "\n## Objective\n\n"
187+
existing_prefix, separator, existing_section = existing_body.partition(marker)
188+
expected_prefix, _, expected_section = expected_body.partition(marker)
189+
if not separator or existing_prefix != expected_prefix:
190+
return False
191+
quoted = markdown_blockquote(objective)
192+
remainder = expected_section[len(quoted):]
193+
return existing_section in (quoted + remainder, objective + remainder)
194+
195+
177196
def register_project_goal(
178197
*,
179198
registry_path: Path,
@@ -326,7 +345,9 @@ def register_project_goal(
326345
if existing_updated_at is not None
327346
else None
328347
)
329-
if existing_state != matching_state:
348+
if matching_state is None or not _registration_state_matches(
349+
existing_state, matching_state, objective=objective,
350+
):
330351
raise ValueError(
331352
f"goal state file conflicts with registration: {state_file}"
332353
)

0 commit comments

Comments
 (0)