diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md index 6758ccb34b..d0b8a4ef45 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -2928,6 +2928,11 @@ an equal-byte retry syncs file and directory before reporting `current`. Narrati canonical records stay intact. This converges the retained Python presentation/legacy input adapter; it adds no RPC or business state machine and does not change TS authority transactions, provider defaults, SQLite D2 or D3 promotion requirements. +Objective examples are now isolated by both document producers, and display +readback shares the Goal codec. Canonical Todo reads remain independent of +malformed display; Objective narrative is not added to the Todo store or its +recovery scope. This is a bounded D1 adapter correction, not D1–D3 qualification. +See the [document boundary](../../reference/protocols/active-state-structured-projection-v0.md#markdown-ownership-boundary). 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). T2 now commits a lease-free native Monitor observation and its independent diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md index f8b9c83fdb..5e9d3dd8cb 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md @@ -2318,6 +2318,10 @@ fenced 示例被当成真实任务、归档 end marker 后叙述进入历史、 这是永久 Python 展示/legacy 输入适配层的收敛:TS authority transaction、provider 默认值、SQLite D2 与 D3 promotion 合同不变,不增加 RPC 或另一份业务状态机。 Canonical handoff-mode show/set 不再依赖 Markdown frontmatter 或本地 lease;一笔 TS 事务把空闲检查、mode 与耐久操作回执绑定到同一 revision,包括未改值请求的回执。该命令边界不切换默认 provider、不晋升整 Goal;frontmatter 仍不属于 Todo-section renderer。操作与恢复见 [handoff-mode](../../reference/handoff-mode.md)。 +两个文档生成入口现在隔离 Objective 示例,展示读回复用 Goal codec。Canonical Todo +读取仍独立于畸形展示;Objective 叙述不进入 Todo store,也不扩大其恢复范围。 +这是有界 D1 适配层修正,不代表 D1–D3 资格化。 +见[文档边界](../../reference/protocols/active-state-structured-projection-v0.md#markdown-ownership-boundary)。 能力缺口 consumer 在 legacy/canonical 输入上共用 TS requirement/resolution owner, 包括 quota 的 Monitor 能力分流。删除 Python missing-set 与 owner/repair 决策 builder, diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index c5caf6badb..8c7d7f8fb0 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -397,6 +397,11 @@ owns durable truth, recovery, cutover, and projection delivery. Neither a fully TypeScript CLI nor `loopxd` is a prerequisite for removing Python decisions. An input adapter or external-effect executor may remain Python. +Objective serialization and readback now share the existing Goal metadata and +section owners; legacy registration compares decoded values and exact narrative. +This closes a presentation boundary without adding TS transport or moving business +authority. See the [document boundary](../../reference/protocols/active-state-structured-projection-v0.md#markdown-ownership-boundary). + The lifecycle-admission slice now uses `todo_lifecycle_decision.ts` for legacy claim/update admission, delegated action/reason checks, ownership-holder routing, and native complete/supersede. Native text/note edits and terminal transitions diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md index ff5921f317..88bba64be1 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md @@ -313,6 +313,10 @@ Markdown renderer 长期保留。 负责 durable truth、恢复、cutover 与投影交付。删除 Python decision 不以前端 CLI 全部改成 TypeScript 或 `loopxd` 落地为前提;输入适配和外部 effect 执行可以保留 Python。 +Objective 编码与读回复用既有 Goal metadata/section owner;旧注册按解码后的值与 +精确叙述正文比较。这是展示边界闭合,不增加 TS transport,也不迁移业务权威。 +见[文档边界](../../reference/protocols/active-state-structured-projection-v0.md#markdown-ownership-boundary)。 + 本次 lifecycle-admission 切片将 legacy claim/update 准入、委托 action/reason 检查、 ownership-holder 路由及 native complete/supersede 统一到 `todo_lifecycle_decision.ts`。Native text/note 编辑与 terminal transition 在进程内 diff --git a/docs/reference/protocols/active-state-structured-projection-v0.md b/docs/reference/protocols/active-state-structured-projection-v0.md index b213aecec8..845340bc11 100644 --- a/docs/reference/protocols/active-state-structured-projection-v0.md +++ b/docs/reference/protocols/active-state-structured-projection-v0.md @@ -119,6 +119,25 @@ Directly editing a projection is not a state transition. ## Markdown Ownership Boundary +New bootstrap and project-registration documents quote each Objective line and +escape HTML metacharacters. Fences, comments, headings, and Todo markers in the +objective remain content rather than document structure. Frontmatter string +encoding and readback share JSON semantics, including escaped Unicode line +separators; only complete delimiter lines terminate frontmatter. Objective +readback composes the existing section reader and decodes generated quotation. +Registration compares metadata values and exact remaining narrative, accepting +legacy Objective presentation without rewriting it; changed content still conflicts. + +This is the permanent Python presentation/legacy-input adapter described by the +[TypeScript RFC](../../architecture/rfcs/typescript-control-plane-migration-v0.md#next-delivery-sequence) +and [shared-authority RFC](../../architecture/rfcs/shared-goal-authority-state-provider-v0.md#next-delivery-and-parallel-provider-work). +It adds no business rule, RPC, provider, or authority write. Post-cutover Todo +consumers still read canonical state when Markdown is absent or malformed; +rendering never imports Objective examples into that state. Before cutover, +the existing legacy writer remains subject to its normal fence. Objective is +independent Goal narrative, outside the Todo store and Todo-section recovery. +Existing malformed documents are not automatically repaired. + Markdown is not one undifferentiated database row. Agents generate and maintain both its structured sections and narrative through LoopX. The distinction is canonical ownership, not human versus Agent authorship: after promotion, diff --git a/loopx/bootstrap.py b/loopx/bootstrap.py index 4138655e83..4085fe36e6 100644 --- a/loopx/bootstrap.py +++ b/loopx/bootstrap.py @@ -40,6 +40,7 @@ MULTI_SUBAGENT_ORCHESTRATION_MODE, ) from .paths import rel_or_abs, resolve_runtime_root +from .control_plane.goals.active_state_metadata import markdown_blockquote, markdown_frontmatter_string from .registry_writability import probe_registry_write_path from .todos import add_todo_to_lines @@ -481,7 +482,7 @@ def render_state_markdown( include_connection_validation: bool = True, handoff_mode: str = HANDOFF_MODE_LEGACY, ) -> str: - safe_objective = objective.replace('"', '\\"') + safe_objective = markdown_frontmatter_string(objective) profile_summary = execution_profile_summary(execution_profile) onboarding_markdown = render_onboarding_state_markdown( onboarding_scan=onboarding_scan, @@ -507,7 +508,7 @@ def render_state_markdown( state_text = f"""--- status: active owner_mode: goal -objective: "{safe_objective}" +objective: {safe_objective} updated_at: {updated_at} adapter_id: {goal_id} {handoff_mode_line}--- @@ -516,7 +517,7 @@ def render_state_markdown( ## Objective -{objective} +{markdown_blockquote(objective)} ## Authority Sources diff --git a/loopx/chat_server.py b/loopx/chat_server.py index cfa13adf94..b7567e28bd 100644 --- a/loopx/chat_server.py +++ b/loopx/chat_server.py @@ -38,6 +38,7 @@ from .chat_store import ChatSessionStore from .capabilities.manager_runtime import manager_runtime_capability_projection from .capabilities.manager_context.roundtrip import project_chat_session_snapshot +from .control_plane.goals.active_state_metadata import active_state_section_text from .control_plane.status.ssh_host_catalog import ( SSH_HOST_CATALOG_PATH, ssh_host_catalog_payload, @@ -123,22 +124,6 @@ def _compact_text(value: Any, *, limit: int = 600) -> str: return " ".join(str(value or "").split())[:limit].strip() -def _active_state_section(state_text: str, heading: str) -> str: - marker = f"## {heading}" - start = state_text.find(marker) - if start < 0: - return "" - content_start = start + len(marker) - end = state_text.find("\n## ", content_start) - section = state_text[content_start : end if end >= 0 else None] - lines = [ - line.strip().removeprefix("- ").strip() - for line in section.splitlines() - if line.strip() and not line.lstrip().startswith("", + id="todo-example", + ), + pytest.param( + "```markdown\n## Agent Todo\n" + "\n" + "- [ ] Example only.\n" + "\n```", + id="fenced-region-example", + ), + ], +) +def test_bootstrap_keeps_objective_separate_from_todo_sources( + tmp_path: Path, objective: str, +) -> None: + state_text = render_state_markdown( + project=tmp_path, + goal_id=GOAL_ID, + adapter_kind="read_only_project_map_v0", + objective=objective, + updated_at="2026-08-21T00:00:00+08:00", + goal_doc=None, + execution_profile=None, + ) + + items, archive, sources = parse_todo_source(state_text) + + assert sources == { + "user": "User Todo / Owner Review Reading Queue", + "agent": "Agent Todo", + } + assert items["user"] == [] + assert archive == [] + assert len(items["agent"]) == 1 + assert items["agent"][0]["action_kind"] == "onboarding_connection_validation" + assert active_state_next_action_entries(state_text) == [items["agent"][0]["text"]] + assert active_state_section_text(state_text, "Objective") == " ".join(objective.split()) + objective_line = next(line for line in state_text.splitlines() if line.startswith("objective: ")) + assert json.loads(objective_line.removeprefix("objective: ")) == objective + assert parse_state_frontmatter(state_text)["objective"] == objective + + +@pytest.mark.parametrize("heading", ["## Objective", "## Objective ", "## Objective\t"]) +def test_objective_readback_preserves_existing_heading_whitespace(heading: str) -> None: + state = f"{heading}\n\nKeep the original objective.\n\n## Next Action\n\nContinue." + assert active_state_section_text(state, "Objective") == "Keep the original objective." + assert active_state_section_text(state, "Missing") == "" + + + +@pytest.mark.parametrize("objective", ["```text\nImplement the task.\n```", "