Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 在进程内
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions loopx/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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}---
Expand All @@ -516,7 +517,7 @@ def render_state_markdown(

## Objective

{objective}
{markdown_blockquote(objective)}

## Authority Sources

Expand Down
19 changes: 2 additions & 17 deletions loopx/chat_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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("<!--")
]
return _compact_text(" ".join(lines))


def _goal_public_context(registry: dict[str, Any], goal: dict[str, Any]) -> dict[str, Any]:
goal_id = str(goal.get("id") or "")
project = Path(str(goal.get("repo") or ".")).expanduser().resolve()
Expand All @@ -148,7 +133,7 @@ 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(active_state_section_text(state_text, "Objective"))
title_line = next(
(line[2:].strip() for line in state_text.splitlines() if line.startswith("# ")),
"",
Expand Down
68 changes: 59 additions & 9 deletions loopx/control_plane/goals/active_state_metadata.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from __future__ import annotations

import json
import re
from html import escape, unescape

from .active_state_sections import active_state_sections


USER_TODO_HEADER_MARKERS = (
"user todo",
Expand All @@ -21,19 +27,63 @@
)


def parse_state_frontmatter(state_text: str) -> dict[str, str]:
if not state_text.startswith("---"):
return {}
parts = state_text.split("---", 2)
if len(parts) < 3:
return {}
def markdown_frontmatter_string(value: str) -> str:
encoded = json.dumps(value, ensure_ascii=False)
for separator in ("\x85", "\u2028", "\u2029"):
encoded = encoded.replace(separator, f"\\u{ord(separator):04x}")
return encoded


def markdown_blockquote(value: str) -> str:
return "\n".join(f"> {escape(line, quote=False)}" for line in value.splitlines())


def active_state_section_text(state_text: str, heading: str) -> str:
_, body = split_state_frontmatter(state_text)
sections = active_state_sections(
body, (heading,), section_heading_pattern=re.compile(r"^## (.+?)[ \t]*$"),
)
section_lines = [line for line in sections[heading] if line]
if heading == "Objective" and section_lines and all(
line.startswith("> ") for line in section_lines
):
text = " ".join(unescape(line[2:]) for line in section_lines)
else:
text = " ".join(
line.strip().removeprefix("- ").strip()
for line in section_lines
if line.strip() and not line.lstrip().startswith("<!--")
)
return " ".join(text.split())


def split_state_frontmatter(state_text: str) -> tuple[dict[str, str], str]:
"""Decode generated string metadata; retain the legacy unquoted input form."""
match = re.match(r"\A---[ \t]*\r?\n(.*?)^---[ \t]*(?:\r?\n|\Z)", state_text, re.M | re.S)
if match is None:
return {}, state_text
result: dict[str, str] = {}
for line in parts[1].splitlines():
# JSON strings may contain Unicode separators: only physical LF ends a field.
for line in match.group(1).split("\n"):
if ":" not in line:
continue
key, value = line.split(":", 1)
result[key.strip()] = value.strip().strip('"')
return result
value = value.strip()
if value.startswith('"'):
try:
decoded = json.loads(value)
except json.JSONDecodeError:
pass # Legacy writers did not escape backslashes as JSON.
else:
if isinstance(decoded, str):
result[key.strip()] = decoded
continue
result[key.strip()] = value.strip('"')
return result, state_text[match.end():]


def parse_state_frontmatter(state_text: str) -> dict[str, str]:
return split_state_frontmatter(state_text)[0]


def todo_role_for_heading(heading: str) -> str | None:
Expand Down
27 changes: 24 additions & 3 deletions loopx/control_plane/projects/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from ..coordination.legacy_writer_fence import legacy_todo_write_transaction, require_legacy_state_replacement_allowed
from ...paths import DEFAULT_RUNTIME_ROOT
from ...registry import atomic_write_json
from ..goals.active_state_metadata import (
markdown_blockquote, markdown_frontmatter_string, split_state_frontmatter,
)
from ...repository_identity import normalize_repository_identity
from .contract import validate_project_record_bindings

Expand Down Expand Up @@ -133,7 +136,7 @@ def bullets(items: list[str], *, empty: str) -> str:
status: active
owner_mode: goal
project_id: {json.dumps(project_id, ensure_ascii=False)}
objective: {json.dumps(objective, ensure_ascii=False)}
objective: {markdown_frontmatter_string(objective)}
updated_at: {updated_at}
adapter_id: {goal_id}
---
Expand All @@ -142,7 +145,7 @@ def bullets(items: list[str], *, empty: str) -> str:

## Objective

{objective}
{markdown_blockquote(objective)}

## Acceptance

Expand Down Expand Up @@ -174,6 +177,22 @@ def bullets(items: list[str], *, empty: str) -> str:
"""


def _registration_state_matches(existing: str, expected: str, *, objective: str) -> bool:
"""Compare metadata values and exact narrative without rewriting old state."""
existing_metadata, existing_body = split_state_frontmatter(existing)
expected_metadata, expected_body = split_state_frontmatter(expected)
if existing_metadata != expected_metadata:
return False
marker = "\n## Objective\n\n"
existing_prefix, separator, existing_section = existing_body.partition(marker)
expected_prefix, _, expected_section = expected_body.partition(marker)
if not separator or existing_prefix != expected_prefix:
return False
quoted = markdown_blockquote(objective)
remainder = expected_section[len(quoted):]
return existing_section in (quoted + remainder, objective + remainder)


def register_project_goal(
*,
registry_path: Path,
Expand Down Expand Up @@ -326,7 +345,9 @@ def register_project_goal(
if existing_updated_at is not None
else None
)
if existing_state != matching_state:
if matching_state is None or not _registration_state_matches(
existing_state, matching_state, objective=objective,
):
raise ValueError(
f"goal state file conflicts with registration: {state_file}"
)
Expand Down
Loading