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 @@ -3,7 +3,7 @@
- Status: Draft; under maintainer review
- Tracking issue: [#3836](https://github.com/huangruiteng/loopx/issues/3836)
- Date: 2026-09-02
- Last updated: 2026-09-05
- Last updated: 2026-09-09
- Scope: peer Agents collaborating around one shared Goal while preserving
canonical intent, per-Agent execution frontiers, claim/lease ownership, and
auditable replan/amendment decisions
Expand All @@ -20,6 +20,20 @@

## 1. Summary and decision

Implementation checkpoint (Stages 1/2 only): alignment and amendment admission
share one full Todo/lease source snapshot. Before promotion this remains a
legacy read; afterwards canonical empty state and provider failures never fall
back to Markdown or per-Todo lease files. Typed selection excludes non-open,
archived and unsatisfied-wait work; Agent eligibility also honors exclusions,
while amendment impact may include peer-held or executor-excluded open work.
The source digest binds the canonical provider revision in `source_basis.todo_basis`.
With a state event log, `revision_basis=state_event_log` still names only that
event axis. Without one, promoted reads use `canonical_todo_snapshot` with
event sequence 0 and an unbound Agent frontier. A changed canonical digest
requires `needs_rebase` even at sequence 0. This does not version the full Goal
intent envelope, infer Agent acknowledgement, or turn admission into an approval
or CAS commit; Stage 3 must still revalidate its own exact commit-time basis.

LoopX will distinguish four kinds of state that must not collapse into one
mutable plan:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- 状态:草案;维护者评审中
- 跟踪 Issue:[#3836](https://github.com/huangruiteng/loopx/issues/3836)
- 日期:2026-09-02
- 最后更新:2026-09-05
- 最后更新:2026-09-09
- 范围:多个对等 Agent 围绕同一个共享 Goal 协作,同时保留 canonical
intent、每个 Agent 的执行 frontier、claim/lease 所有权,以及可审计的
replan/amendment 决策
Expand All @@ -19,6 +19,17 @@

## 1. 摘要与决策

实现检查点(仅 Stage 1/2):alignment 与 amendment admission 共用一份完整 Todo/lease
来源快照。Promotion 前仍为 legacy 读取;之后 canonical 空状态和 provider 失败都不
回退 Markdown 或逐 Todo lease 文件。TS 筛选排除非 open、归档和恢复条件未满足的
工作;Agent eligibility 还遵守 exclusion,但 amendment 影响范围可包含其他 Agent
持有或当前 executor 被排除的开放工作。Source digest 通过 `source_basis.todo_basis`
绑定 canonical provider revision。有 state event log 时,`revision_basis=state_event_log`
仍只表示事件轴;没有时,promoted 读取使用 `canonical_todo_snapshot`、事件序号 0 和
unbound Agent frontier。即使事件序号为 0,canonical digest 变化也要求 `needs_rebase`。
这不等于完整 Goal intent envelope 已版本化,不推断 Agent 已确认,也不把 admission
变成审批或 CAS commit;Stage 3 仍须重新验证自己的精确提交时 basis。

LoopX 将区分四类不能坍缩为一份可变计划的状态:

1. **canonical 共享 Goal intent envelope**;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2530,6 +2530,13 @@ agents; `goal_bound` grants no global-gate semantics. This consolidates T1
admission rules without expanding native update fields, changing provider/profile
defaults, or releasing D1–D3 projection, real-backend, soak or promotion holds.

Shared-goal alignment and amendment admission now consume the same canonical
Todo/lease revision after promotion, including authoritative empty state. Their
old display/lease-file reads remain only before promotion. Proposal source
digests include that revision, without treating it as a Goal intent revision or
an amendment commit receipt. This is a bounded T3 consumer closure; the default
provider, permanent projection, D1–D3 qualification and T1/T2 holds are unchanged.

The original direction remains; execution cards expand these stages rather than cancel them:

1. **Close TS transactions and consumers.** Follow [T0–T3](typescript-control-plane-migration-v0.md#execution-cards-after-the-current-stack) to consolidate rules and delete duplicate decisions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,12 @@ Todo authoring scope 已与 terminal successor 共用 TS 最终绑定不变量
这是 T1 准入规则收拢;不扩张 native update 字段权限、不改变 provider/profile 默认值,
也不解除 D1–D3 的投影、真实 backend、soak 或 promotion 条件。

Shared-goal alignment 与 amendment admission 在 promotion 后共用同一个 canonical
Todo/lease revision,包括权威空集合;旧展示/lease 文件读取仅保留在 promotion 前。
Proposal source digest 包含该 revision,但不将它冒充 Goal intent revision 或
amendment commit receipt。这是有边界的 T3 consumer 闭合;默认 provider、永久投影、
D1–D3 资格化和 T1/T2 条件保持不变。

以下规划保留原有方向;执行卡是它们的展开,不是替代或取消:

1. **闭合 TS 事务与 consumer。** 按 [T0–T3](typescript-control-plane-migration-v0.zh-CN.md#当前-stack-合入后的执行卡) 收口规则并删除重复决策。
Expand Down
14 changes: 14 additions & 0 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,20 @@ closure and whole-Goal promotion remain held; do not infer them from a route pla

**T3 — close remaining structured consumers, then remove their old reads.**

Current bounded delivery: shared-goal alignment and amendment admission use one
`shared_goal_work_source.py` snapshot per decision, reusing the canonical Todo
summary after promotion. The same provider read optionally supplies leases at
that revision; absent/empty/stale display and old lease files are not fallback
authority. `shared_goal_work.ts` owns their open-work, claim and exclusion
selection; the old Python selectors and amendment's second Markdown parse are
removed. Excluded work is not recommended to that Agent, but remains available
as amendment impact context. The source digest binds the canonical revision;
`canonical_todo_snapshot` has event sequence 0, not a fabricated Goal intent
revision, and a changed digest requires proposal rebase even without events.
Active malformed lease expiry now fails through the existing typed lease rule.
This independent consumer slice does not depend on open #4142, close T1/T2,
migrate all T3 consumers or grant amendment commit/whole-Goal promotion authority.

- Audit Turn/quota, Dashboard, standing decisions, shared-goal alignment and
amendment revision inputs. Reuse #4117's canonical source adapter and pass
one snapshot through a decision; do not build another Todo inventory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,17 @@ digest 仍绑定原始 wire observation,不能因规范化而悄悄使 pending

**T3 — 闭合剩余 structured consumer,删除各自旧读路径。**

当前有边界交付:shared-goal alignment 与 amendment admission 每次决策共用一份
`shared_goal_work_source.py` 快照,promotion 后复用 canonical Todo summary;同一次
provider 读取可返回同 revision 的 lease。缺失/空/陈旧展示及旧 lease 文件不再是
fallback authority。`shared_goal_work.ts` 统一这两个消费者的开放工作、claim 和
exclusion 筛选,删除旧 Python selector 与 amendment 的第二次 Markdown 解析。
被排除的工作不推荐给该 Agent,但仍可作为 amendment 的影响对象。Source digest
绑定 canonical revision;无事件时 `canonical_todo_snapshot` 的事件序号为 0,不能
冒充 Goal intent revision,digest 变化仍要求 proposal rebase。活动 lease 的非法
到期时间复用现有 TS lease 规则拒绝。本批不依赖仍开放的 #4142,不表示 T1/T2 或全部
T3 完成,也不授予 amendment commit/整 Goal promotion 权限。

- 分别审计 Turn/quota、Dashboard、standing decision、shared-goal alignment、
amendment revision 输入。复用 #4117 canonical source adapter,一次决策传递一份
snapshot,不新增 Todo inventory。
Expand Down
9 changes: 9 additions & 0 deletions examples/shared-goal-authority-e2e/mutants.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ def command(self) -> list[str]:
Case('monitor_route_rewrites_fingerprint', (('loopx/control_plane/quota/monitor_poll_commit.ts', replacement(
' monitorSuccessorIntent(result);', ' Object.assign(result, monitorSuccessorIntent(result));')),),
'tests/control_plane_ts/quota_monitor_poll_commit.test.ts', 'preserves the legacy pending observation fingerprint'),
Case('governance_exclusion_ignored', (('loopx/control_plane/goals/shared_goal_work.ts', replacement(
'else if (!excluded)', 'else if (true)')),),
'tests/control_plane_ts/shared_goal_work.test.ts', 'alignment selection respects exclusions'),
Case('canonical_zero_basis_stale_admitted', (('loopx/control_plane/goals/goal_amendment_proposal.ts', replacement(
'if (proposal.base_source_basis_digest !== derived.source_basis_digest) facts.push("base_source_basis_digest_mismatch");',
'if (false) facts.push("base_source_basis_digest_mismatch");')),),
'tests/control_plane_ts/goal_amendment_proposal.test.ts', 'canonical Todo bases cannot'),
Case('governance_reads_legacy_after_promotion', (('loopx/control_plane/goals/shared_goal_work_source.py', replacement(
'if canonical is None:', 'if True:')),), 'tests/control_plane/test_canonical_goal_governance.py::test_empty_canonical_is_authoritative_and_missing_display_is_not_repaired'),
Case('delivery_wait_target_unbound', (('loopx/control_plane/todos/resume_condition.ts', replacement(
'condition.target_todo_id !== spec.target || ', '')),),
'tests/control_plane_ts/delivery_response.test.ts', 'exact dependency identity'),
Expand Down
12 changes: 11 additions & 1 deletion loopx/control_plane/coordination/local_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def claim_canonical_todo_if_promoted(


def read_canonical_todos_if_promoted(
*, runtime_root: Path, goal_id: str
*, runtime_root: Path, goal_id: str, include_leases: bool = False,
) -> dict[str, Any] | None:
"""Return canonical Todos after cutover, or ``None`` before cutover.

Expand All @@ -196,6 +196,7 @@ def read_canonical_todos_if_promoted(
"schema_version": LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA,
"runtime_root": str(runtime_root.expanduser().resolve(strict=False)),
"goal_id": goal_id,
**({"include_leases": True} if include_leases else {}),
},
)
if not isinstance(result, Mapping):
Expand Down Expand Up @@ -230,6 +231,15 @@ def read_canonical_todos_if_promoted(
payload=payload,
)
payload["todos"] = [dict(item) for item in todos]
if include_leases and (
not isinstance(payload.get("leases"), list)
or any(not isinstance(item, Mapping) for item in payload["leases"])
or not isinstance(payload.get("provider_revision"), str)
):
raise LocalCoordinationAuthorityUnavailable(
"canonical Todo/lease snapshot is incomplete", code="local_authority_snapshot_incomplete",
payload=payload,
)
return payload


Expand Down
8 changes: 8 additions & 0 deletions loopx/control_plane/coordination/local_authority_runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,9 @@ export async function listLocalCoordinationTodos(
if (input.schema_version !== LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA) {
throw new Error("local coordination Todo list request schema mismatch");
}
if (input.include_leases !== undefined && typeof input.include_leases !== "boolean") {
throw new Error("include_leases must be a boolean");
}
const root = runtimeRoot(input.runtime_root);
const goalId = requireAuthorityStoreId(input.goal_id, "goal id");
const store = dependencies.createStore?.(authorityDirectory(root), goalId) ??
Expand All @@ -1006,12 +1009,17 @@ export async function listLocalCoordinationTodos(
}
const projection = indexCoordinationProjectionTodos(head.head, goalId);
const todoReadModel = validateCoordinationTodoReadModel(head.head, goalId);
const leaseIndex = input.include_leases === true
? indexCoordinationProjection(head.head, goalId) : null;
return {
schema_version: LOCAL_COORDINATION_TODO_LIST_RESULT_SCHEMA,
status: "loaded",
todos: projection.todo_ids.map((todoId) => projection.todos.get(todoId)!),
todo_ids: projection.todo_ids,
todo_read_model: todoReadModel,
...(leaseIndex === null ? {} : {
leases: leaseIndex.lease_todo_ids.map((id) => leaseIndex.leases.get(id)!),
}),
provider_revision: head.provider_revision,
cursor: head.cursor,
source_authority: "file_v0",
Expand Down
71 changes: 10 additions & 61 deletions loopx/control_plane/goals/goal_amendment_proposal.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,12 @@
from ...event_sourced_state import now_utc_iso
from ...file_lock import exclusive_file_lock
from ...history import load_index
from ...registry import resolve_state_file
from ...runtime import validate_goal_id_path_segment
from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result
from ..status.autonomous_replan_projection import (
autonomous_replan_obligation_from_runs,
)
from ..todos.contract import (
normalize_todo_bound_agent,
normalize_todo_claimed_by,
normalize_todo_id,
)
from ..todos.projection import todo_item_is_actionable_open
from ..todos.contract import normalize_todo_claimed_by
from ..work_items.autonomous_replan_obligation import (
ensure_replan_novelty_policy,
run_history_agent_id,
Expand All @@ -87,11 +81,11 @@
autonomous_replan_is_required,
autonomous_replan_scope_decision,
)
from .shared_goal_work_source import read_shared_goal_work_source
from .shared_goal_alignment import (
DEFAULT_REGISTRY_RELATIVE_PATH,
_parsed_active_state,
_registered_goal,
project_shared_goal_alignment,
_project_shared_goal_alignment,
)

GOAL_AMENDMENT_PROPOSAL_EFFECT_METHOD = "goal.amendment_proposal.admit"
Expand Down Expand Up @@ -223,10 +217,9 @@ def admit_goal_amendment_proposal(
)

goal = _registered_goal(registry_payload, goal_id=proposal_goal_id)
state_path = resolve_state_file(project, goal.get("state_file"))
if state_path is None:
raise ValueError(f"goal state file is missing for {proposal_goal_id}")
state_text = state_path.read_text(encoding="utf-8")
work_source = read_shared_goal_work_source(goal=goal, project=project,
runtime_root=effective_runtime_root)
state_text = work_source.state_text

# Causal authority is derived, never submitted: the open obligation
# inventory comes from the same run-history projection the quota/status
Expand All @@ -244,13 +237,14 @@ def admit_goal_amendment_proposal(
# and unregistered proposers, and derives the source basis (state event
# log append sequence, or markdown fallback) the proposal's base binds
# against — both its sequence and its digest.
alignment = project_shared_goal_alignment(
alignment = _project_shared_goal_alignment(
goal_id=proposal_goal_id,
agent_id=proposer_agent_id,
project=project,
registry_path=effective_registry_path,
runtime_root=effective_runtime_root,
status_item=derived_status_item,
work_source=work_source,
)
source_basis = alignment.get("source_basis")
if not isinstance(source_basis, Mapping):
Expand All @@ -266,18 +260,13 @@ def admit_goal_amendment_proposal(
registered_agents=registered_agent_ids_for_goal(goal),
status_item=derived_status_item,
)
goal_todo_inventory = _goal_todo_inventory(
state_text=state_text,
goal=goal,
state_path=state_path,
)

request = {
"schema_version": GOAL_AMENDMENT_PROPOSAL_REQUEST_SCHEMA_VERSION,
"proposal": dict(proposal),
"derived_basis": derived_basis,
"open_replan_obligations": open_replan_obligations,
"goal_todo_inventory": goal_todo_inventory,
"work_items": work_source.items,
"observed_at": work_source.observed_at,
}
try:
admission = effect_runtime_result(
Expand Down Expand Up @@ -427,46 +416,6 @@ def _open_replan_obligation_inventory(
return list(inventory.values())


def _goal_todo_inventory(
*,
state_text: str,
goal: Mapping[str, Any],
state_path: Path,
) -> list[dict[str, Any]]:
"""Derive the goal's actionable open Todos as typed facts.

``claimed_by``/``bound_agent`` are diagnostic companions only:
admission checks existence, openness, and goal membership — shared
amendments legitimately affect peer-claimed work, and lease
disposition belongs to the Stage 3 commit step (RFC §5 step 4).
"""

_, items = _parsed_active_state(
state_text,
goal=dict(goal),
state_path=state_path,
)
inventory: list[dict[str, Any]] = []
seen_todo_ids: set[str] = set()
for todo_item in items:
if not todo_item_is_actionable_open(todo_item):
continue
todo_id = normalize_todo_id(todo_item.get("todo_id"))
if not todo_id or todo_id in seen_todo_ids:
continue
seen_todo_ids.add(todo_id)
inventory.append(
{
"todo_id": todo_id,
"status": "open",
"task_class": (str(todo_item.get("task_class") or "").strip() or None),
"claimed_by": normalize_todo_claimed_by(todo_item.get("claimed_by")),
"bound_agent": normalize_todo_bound_agent(todo_item.get("bound_agent")),
}
)
return inventory


def _check_admission_shape(
admission: object,
*,
Expand Down
Loading
Loading