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
17 changes: 14 additions & 3 deletions docs/architecture/rfcs/loopx-overall-roadmap-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -967,9 +967,20 @@ Goal path resolver; real File/SQLite import and original-receipt recovery pass
with the four old normal producer modules physically absent in a disposable
package. This qualifies that command's loading boundary, not every other CLI
caller or removal of those modules. Keep T4 retirement on actual callers: the
retained Python prose-write guard still serves live callers and its obligation
must survive adapter removal. This partial stage does not retire the supported
old writer or qualify a released default.
retained prose-write guard now belongs to the existing source/fence boundary;
the source partition projector has one shared owner, with compatible old
imports. Configuration and direct guard checks run without both capture modules.
Pure runtime-root routing and its actual canonical Todo, acceptance, Chat and
CLI callers now use the existing paths owner, with an identical compatibility
reexport for capture callers. Source and fresh-wheel File/SQLite App HTTP import,
process restart, original-operation recovery, later canonical writes and prose
checks pass the same oracle with both capture modules present and physically
absent. Source/maintenance identity, Todo/handoff invariance, JSON types,
project-relative routing and failed-write behavior remain unchanged. Historical
outbox disposition and independent data/history recovery retain their separate
qualifications below; authorized reactivation and remaining App writer callers
stay open. This stage does not retire the supported old writer or qualify a
released default.
The shared TS source check admits runtime directory aliases by physical identity,
preserving the original snapshot and foreign-source rejection. File/SQLite HTTP
joins alias-bound import, externally configured registry backup, later writes and original-operation recovery; a
Expand Down
11 changes: 11 additions & 0 deletions docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,17 @@ owned Host 进程、未晋升源的原生租约和 File/SQLite 验证:进程
完整状态重新激活继续开放;协调源备份验证不结算完整恢复。详见
[冷源导入与支持边界](../../reference/local-authority-provider-selection.md)。

仍有调用方的 prose 写入保护现归属于既有 source/fence 边界,source partition
projector 共用同一 owner,并保留旧导入兼容。配置和直接 guard 可在两个 capture
模块物理缺席时运行。纯 runtime-root 解析及其真实 canonical Todo、terminal
lifecycle、acceptance、Chat 和 CLI 调用方现复用既有 paths owner;旧 capture adapter
兼容导出同一个函数。源码与 fresh wheel 的 File/SQLite App HTTP 使用同一 oracle,
在两个 capture 模块均存在/均物理缺席时验证导入、进程重启、原操作恢复、后续
canonical 写入及 prose 保护。保留相对项目路由、显式 override 优先、source/maintenance
身份、Todo/handoff 不变、JSON 类型与失败不写;历史 outbox 处置和独立数据/历史
恢复保留下述独立资格,授权重新激活及其余 App writer 调用方继续开放,不据此
删除受支持旧 writer 或结算发布默认资格。

冷旧源保留检查点:本项目全量备份现盘点注册的自定义状态与来源 registry 路由。
真实 CLI 冷导入→新增 canonical 写入→全量备份→独立静态解包现已串联验证
File/SQLite 完整 head、全部 committed journal、未引用的归档要求、存储配置和原始
Expand Down
20 changes: 15 additions & 5 deletions docs/reference/local-authority-provider-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,11 +406,21 @@ the existing Goal path resolver; its File/SQLite import and original-receipt
recovery run with `todos.py`, `bootstrap.py`, `runtime_shadow_writer_adapter.py`
and `local_authority_shadow_outbox.py` physically absent in a disposable package.
This proves that command's independence, not that other commands or supported
writers can lose those files. Retained prose-write guards have live callers and
must move to their owning boundary before their adapter is removed.
The packaged App uses the real File/SQLite backend; App loading with those old
modules absent remains a separate acceptance. This stage does not qualify
supported old-writer retirement,
writers can lose those files. The retained prose-write guard now belongs to the
existing source/fence boundary and shares the source partition projector with
capture. Its old import remains compatible. Configuration and the guard load
with both capture modules absent. Pure runtime-root routing now belongs to
`paths.effective_runtime_root`; canonical Todo, terminal lifecycle, acceptance,
Chat and CLI callers import that owner directly. The old adapter reexports the
same function for supported capture callers. Source and fresh-wheel File/SQLite
HTTP import, process restart, original-operation recovery, later writes and
canonical prose checks use the same oracle with both capture modules present
and physically absent. Project-relative routes, explicit override precedence,
source identity, maintenance, Todo/handoff, JSON type and failed-write protections
remain intact. Historical outbox disposition and independent data/history
recovery retain their separate qualifications; authorized reactivation and
remaining App writer callers still need their original acceptance. This stage
does not qualify supported old-writer retirement,
release default or historical support cutoff.
Source checks accept registered runtime directory aliases only when both paths
resolve to the same physical directory. Retargeting an alias after preview still
Expand Down
2 changes: 1 addition & 1 deletion loopx/chat_todo_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .control_plane.coordination.local_authority import (
LocalCoordinationAuthorityUnavailable, read_canonical_todos_if_promoted,
)
from .control_plane.coordination.local_authority_shadow_adapter import effective_runtime_root
from .paths import effective_runtime_root
from .control_plane.todos.provider_projection import projection_delivery_requires_ack


Expand Down
2 changes: 1 addition & 1 deletion loopx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
build_periodic_report_post_writeback_projection,
periodic_report_post_writeback_hooks_for_goal,
)
from .control_plane.coordination.local_authority_shadow_adapter import (
from .paths import (
effective_runtime_root,
)
from .capabilities.semantic_preference.cli import (
Expand Down
2 changes: 1 addition & 1 deletion loopx/cli_commands/authority_shadow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from ..control_plane.coordination.local_authority_shadow_adapter import (
CLI_DRAIN_LOCK_TIMEOUT_SECONDS,
drain_local_authority_shadow_outbox,
effective_runtime_root,
local_authority_shadow_status,
)
from ..control_plane.coordination.local_authority_shadow_outbox import OutboxError
from ..paths import effective_runtime_root
from ..file_lock import LockAcquireTimeoutError


Expand Down
2 changes: 1 addition & 1 deletion loopx/cli_commands/todo_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..file_lock import lock_timeout_error_fields
from ..control_plane.coordination.legacy_writer_fence import LegacyCoordinationWriterFenced
from ..control_plane.coordination.shadow_management import ShadowManagementError
from ..control_plane.coordination.runtime_shadow_writer_adapter import ActiveStateAuthorityMutationError
from ..control_plane.coordination.legacy_writer_fence import ActiveStateAuthorityMutationError
from ..control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable
from .todo_argument_validation import TodoClaimArgumentError

Expand Down
4 changes: 2 additions & 2 deletions loopx/cli_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def _dispatch_common_command(
return 0
if args.command == "delegation":
from .cli_commands.delegation import handle_delegation
from .control_plane.coordination.local_authority_shadow_adapter import (
from .paths import (
effective_runtime_root,
)

Expand Down Expand Up @@ -411,7 +411,7 @@ def _dispatch_common_command(
)
from .cli_commands.todo import handle_todo_command
from .cli_rollout import append_cli_rollout_event
from .control_plane.coordination.local_authority_shadow_adapter import (
from .paths import (
effective_runtime_root,
)

Expand Down
54 changes: 53 additions & 1 deletion loopx/control_plane/coordination/legacy_writer_fence.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from ...file_lock import exclusive_cross_runtime_file_lock
from ..projects.registry_codec import load_registry
from ...paths import resolve_runtime_root
from ...registry import registry_goals, resolve_state_file
from ...registry import find_registry_goal, registry_goals, resolve_state_file
from ..goals.active_state_metadata import parse_state_frontmatter
from .shadow_management import (
ShadowManagementError, read_shadow_bootstrap_source_path, read_shadow_management_state, require_shadow_primary_write_allowed,
Expand Down Expand Up @@ -192,6 +192,58 @@ def require_registry_source_write_allowed(
require_legacy_coordination_write_allowed(runtime_root=registered_root, goal_id=goal_id)


class ActiveStateAuthorityMutationError(ValueError):
"""A prose-only writer attempted to change canonical coordination state."""

code = "active_state_authority_mutation_forbidden"
payload = {"primary_writeback_preserved": True}


def require_prose_state_write_allowed(
*, registry_path: Path, runtime_root: Path, goal_id: str, state_path: Path,
original_text: str, planned_text: str,
) -> None:
"""Under S, enforce source maintenance and the owned prose-only invariant."""

require_registry_source_write_allowed(
registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id,
state_file=state_path, canonical_mutation=False,
)

from ...rollout_event_log import load_rollout_events, rollout_event_log_path
from ..todos.todo_index import MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL
from .runtime_shadow import todo_partition_projector
from .local_authority_shadow_projection import canonical_bytes, partition_comparison_view

try:
goal = find_registry_goal(load_registry(registry_path), goal_id)
events = load_rollout_events(
rollout_event_log_path(runtime_root, goal_id),
limit=MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL,
)
projector = todo_partition_projector(
goal, state_path=state_path, rollout_events=events,
)
# Todo projections include the read-time resume-condition evaluation
# clock. Compare the same semantic view used by shadow continuity
# digests so an owned prose update is not rejected merely because the
# two projections were evaluated milliseconds apart.
original_projection = partition_comparison_view(projector(original_text))
planned_projection = partition_comparison_view(projector(planned_text))
# Python structural equality aliases JSON booleans with integers.
# Use the existing exact-byte identity contract before authorizing IO.
if canonical_bytes(original_projection) != canonical_bytes(planned_projection):
raise ActiveStateAuthorityMutationError(
"prose update would change canonical Todo or handoff state"
)
except ActiveStateAuthorityMutationError:
raise
except Exception as error:
raise ActiveStateAuthorityMutationError(
"prose update cannot prove that canonical coordination state is unchanged"
) from error


def require_legacy_state_replacement_allowed(
*, runtime_root: Path, goal_id: str, goal: dict[str, Any] | None,
) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
from __future__ import annotations

import sys
from collections.abc import Mapping
from dataclasses import asdict, dataclass, field, fields
from pathlib import Path
from typing import Any

from ..projects.registry_codec import load_registry
from ...paths import resolve_runtime_root
from ...paths import effective_runtime_root as effective_runtime_root, resolve_runtime_root
from ...registry import find_registry_goal
from ..effect_runtime import effect_runtime_result
from . import local_authority_shadow_outbox as outbox
Expand All @@ -25,34 +24,18 @@
)
from .local_authority_shadow_projection import (
head_digest,
todo_partition_projection,
)
from .runtime_shadow import resolve_coordination_runtime_shadow_config, capture_todo_archive_dependencies
from .runtime_shadow import (
resolve_coordination_runtime_shadow_config,
todo_partition_projector as todo_partition_projector,
)
from .shadow_management import read_shadow_capture_binding, shadow_management_state_path
from .shadow_goal_scope import shadow_goal_scope


from .runtime_shadow import local_authority_shadow_summary


def effective_runtime_root(
registry_path: Path,
runtime_root_override: str | Path | None,
) -> Path:
"""Resolve the one runtime root every writer hook of a CLI call must share.

``--runtime-root`` wins when given; otherwise the registry's
``common_runtime_root`` applies, and a relative value resolves against the
registry's project root rather than the caller's working directory. Todo,
follow-up, handoff-mode, and task-lease hooks all consume this value so one
goal never splits into two candidate lineages.
"""

registry = load_registry(registry_path)
override = str(runtime_root_override) if runtime_root_override is not None else None
return resolve_runtime_root(registry, override, registry_path=registry_path)


# ---------------------------------------------------------------------------
# Transaction-bound outbox drain (Stage 2C second half plumbing).
#
Expand Down Expand Up @@ -129,34 +112,6 @@ def to_payload(self) -> dict[str, Any]:
return payload


def todo_partition_projector(
goal: Mapping[str, Any] | None,
*,
state_path: Path,
rollout_events: list[dict[str, Any]] | None = None,
) -> outbox.TodoPartitionProjector:
"""Production projector: parse active-state text into the todos partition."""

from ...control_plane.todos.handoff_mode import goal_handoff_mode
from ..todos.goal_todo_projection import project_goal_todo_items

goal_record = dict(goal) if isinstance(goal, Mapping) else None
events = list(rollout_events or [])

def project(state_text: str) -> dict[str, Any]:
return todo_partition_projection(
handoff_mode=goal_handoff_mode(state_text),
todos=capture_todo_archive_dependencies(project_goal_todo_items(
goal_record,
state_text=state_text,
state_path=state_path,
rollout_events=events,
), state_text),
)

return project


def read_local_authority_shadow(
*,
runtime_root: Path,
Expand Down
29 changes: 29 additions & 0 deletions loopx/control_plane/coordination/runtime_shadow.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,35 @@ def capture_todo_archive_dependencies(todos: list[dict[str, Any]], state_text: s
return result


def todo_partition_projector(
goal: Mapping[str, Any] | None,
*,
state_path: Path,
rollout_events: list[dict[str, Any]] | None = None,
) -> Callable[[str], dict[str, Any]]:
"""Production projector: parse active-state text into the todos partition."""

from ..todos.handoff_mode import goal_handoff_mode
from .local_authority_shadow_projection import todo_partition_projection
from ..todos.goal_todo_projection import project_goal_todo_items

goal_record = dict(goal) if isinstance(goal, Mapping) else None
events = list(rollout_events or [])

def project(state_text: str) -> dict[str, Any]:
return todo_partition_projection(
handoff_mode=goal_handoff_mode(state_text),
todos=capture_todo_archive_dependencies(project_goal_todo_items(
goal_record,
state_text=state_text,
state_path=state_path,
rollout_events=events,
), state_text),
)

return project


def build_runtime_shadow_source_snapshot(
*, goal: Mapping[str, Any], runtime_root: Path, state_path: Path,
registry_path: Path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,68 +6,17 @@
from pathlib import Path
from typing import Any

from ...history import load_registry
from ...registry import find_registry_goal
from . import local_authority_shadow_outbox as outbox
from .local_authority_shadow_projection import LEASE_PARTITION
from .legacy_writer_fence import (
ActiveStateAuthorityMutationError as ActiveStateAuthorityMutationError,
require_prose_state_write_allowed as require_prose_state_write_allowed,
)
from .runtime_shadow import resolve_coordination_runtime_shadow_config
from .shadow_goal_scope import shadow_goal_scope
from .shadow_management import ShadowManagementError, read_shadow_capture_binding, require_shadow_primary_write_allowed


class ActiveStateAuthorityMutationError(ValueError):
"""A prose-only writer attempted to change canonical coordination state."""

code = "active_state_authority_mutation_forbidden"
payload = {"primary_writeback_preserved": True}


def require_prose_state_write_allowed(
*, registry_path: Path, runtime_root: Path, goal_id: str, state_path: Path,
original_text: str, planned_text: str,
) -> None:
"""Under S, enforce source maintenance and the owned prose-only invariant."""

from .legacy_writer_fence import require_registry_source_write_allowed
require_registry_source_write_allowed(
registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id,
state_file=state_path, canonical_mutation=False,
)

from ...rollout_event_log import load_rollout_events, rollout_event_log_path
from ..todos.todo_index import MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL
from .local_authority_shadow_adapter import todo_partition_projector
from .local_authority_shadow_projection import canonical_bytes, partition_comparison_view

try:
goal = find_registry_goal(load_registry(registry_path), goal_id)
events = load_rollout_events(
rollout_event_log_path(runtime_root, goal_id),
limit=MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL,
)
projector = todo_partition_projector(
goal, state_path=state_path, rollout_events=events,
)
# Todo projections include the read-time resume-condition evaluation
# clock. Compare the same semantic view used by shadow continuity
# digests so an owned prose update is not rejected merely because the
# two projections were evaluated milliseconds apart.
original_projection = partition_comparison_view(projector(original_text))
planned_projection = partition_comparison_view(projector(planned_text))
# Python structural equality aliases JSON booleans with integers.
# Use the existing exact-byte identity contract before authorizing IO.
if canonical_bytes(original_projection) != canonical_bytes(planned_projection):
raise ActiveStateAuthorityMutationError(
"prose update would change canonical Todo or handoff state"
)
except ActiveStateAuthorityMutationError:
raise
except Exception as error:
raise ActiveStateAuthorityMutationError(
"prose update cannot prove that canonical coordination state is unchanged"
) from error


def begin_todo_runtime_shadow_capture(
*,
registry_path: Path,
Expand Down
Loading
Loading