From 4eb28e8325fbf77cfe6569b93bf11bc3ce5dc07b Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Wed, 9 Sep 2026 21:29:20 +0800 Subject: [PATCH 1/2] fix(governance): bind alignment and amendments to canonical work snapshots Signed-off-by: huangruiteng --- examples/shared-goal-authority-e2e/mutants.py | 9 + .../coordination/local_authority.py | 12 +- .../coordination/local_authority_runtime.ts | 8 + .../goals/goal_amendment_proposal.py | 71 +----- .../goals/goal_amendment_proposal.ts | 33 ++- .../goals/shared_goal_alignment.py | 207 +++--------------- .../goals/shared_goal_alignment.ts | 37 +++- loopx/control_plane/goals/shared_goal_work.ts | 72 ++++++ .../goals/shared_goal_work_source.py | 71 ++++++ .../test_canonical_goal_governance.py | 164 ++++++++++++++ .../test_shared_goal_alignment.py | 63 +++++- .../authority_store_conformance.ts | 50 +++++ .../goal_amendment_proposal.test.ts | 17 ++ .../shared_goal_alignment.test.ts | 12 + .../control_plane_ts/shared_goal_work.test.ts | 54 +++++ 15 files changed, 621 insertions(+), 259 deletions(-) create mode 100644 loopx/control_plane/goals/shared_goal_work.ts create mode 100644 loopx/control_plane/goals/shared_goal_work_source.py create mode 100644 tests/control_plane/test_canonical_goal_governance.py create mode 100644 tests/control_plane_ts/shared_goal_work.test.ts diff --git a/examples/shared-goal-authority-e2e/mutants.py b/examples/shared-goal-authority-e2e/mutants.py index 33a9bf91d..8d4e0fc83 100644 --- a/examples/shared-goal-authority-e2e/mutants.py +++ b/examples/shared-goal-authority-e2e/mutants.py @@ -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'), diff --git a/loopx/control_plane/coordination/local_authority.py b/loopx/control_plane/coordination/local_authority.py index 0fbef2ef0..10da731c2 100644 --- a/loopx/control_plane/coordination/local_authority.py +++ b/loopx/control_plane/coordination/local_authority.py @@ -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. @@ -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): @@ -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 diff --git a/loopx/control_plane/coordination/local_authority_runtime.ts b/loopx/control_plane/coordination/local_authority_runtime.ts index aa70dee4b..6eaa40390 100644 --- a/loopx/control_plane/coordination/local_authority_runtime.ts +++ b/loopx/control_plane/coordination/local_authority_runtime.ts @@ -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) ?? @@ -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", diff --git a/loopx/control_plane/goals/goal_amendment_proposal.py b/loopx/control_plane/goals/goal_amendment_proposal.py index d0aae2622..eb16287b6 100644 --- a/loopx/control_plane/goals/goal_amendment_proposal.py +++ b/loopx/control_plane/goals/goal_amendment_proposal.py @@ -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, @@ -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" @@ -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 @@ -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): @@ -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( @@ -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, *, diff --git a/loopx/control_plane/goals/goal_amendment_proposal.ts b/loopx/control_plane/goals/goal_amendment_proposal.ts index f4814b94b..d00d0beef 100644 --- a/loopx/control_plane/goals/goal_amendment_proposal.ts +++ b/loopx/control_plane/goals/goal_amendment_proposal.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import type { JsonObject } from "../effect_program.ts"; +import { sharedGoalWorkFacts } from "./shared_goal_work.ts"; import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { optionalNonEmptyString, @@ -41,7 +42,8 @@ import { * * The derived goal basis facts arrive from the Python adapter via the Stage 1 * alignment projection (`state_event_log` head = last append sequence). When - * no event log exists the basis is `markdown_active_state` with sequence 0 — + * no event log exists the basis is `markdown_active_state` before promotion + * or `canonical_todo_snapshot` afterwards, both with sequence 0 — * the only sequence the markdown producer can emit — and a proposal binding * that real 0 is admitted as unverifiable instead of being forced to * fabricate a positive sequence. @@ -49,7 +51,7 @@ import { * The proposal declares its own `base_revision_basis` — the type of the * basis it was actually produced against — so sequence producibility is * validated against the *claimed* basis, never inferred from the current - * derived basis: 0 is only producible under `markdown_active_state`, and a + * derived basis: 0 is only producible under a non-event basis, and a * positive sequence is only producible under `state_event_log` (a fabricated * value under either claim fails closed as a request rejection). When a * Goal's basis evolves from markdown to a typed event log, a proposal still @@ -108,6 +110,7 @@ export type GoalAmendmentAdmissionFact = export const REVISION_BASIS_VALUES = [ "state_event_log", "markdown_active_state", + "canonical_todo_snapshot", ] as const; export type AmendmentRevisionBasis = (typeof REVISION_BASIS_VALUES)[number]; @@ -443,11 +446,11 @@ function requireProducibleBaseSequence( // event log — that transition is admissionOutcome's superseded branch, // not a fabricated history (review round 8). if ( - proposal.base_revision_basis === "markdown_active_state" && + proposal.base_revision_basis !== "state_event_log" && proposal.base_state_event_basis_sequence !== 0 ) { throw new EffectRuntimeRequestError( - "goal_amendment_proposal.base_state_event_basis_sequence must be 0 when the proposal's base_revision_basis is markdown_active_state (the real markdown basis has no event append sequence; do not fabricate one)", + `goal_amendment_proposal.base_state_event_basis_sequence must be 0 when the proposal\'s base_revision_basis is ${proposal.base_revision_basis} (no event append sequence; do not fabricate one)`, ); } if ( @@ -482,9 +485,9 @@ function decodeDerivedBasis(value: unknown): DerivedGoalBasisFacts { "goal_amendment_proposal.derived_basis.state_event_basis_sequence must be a positive event append sequence when revision_basis is state_event_log", ); } - if (revisionBasis === "markdown_active_state" && basisSequence !== 0) { + if (revisionBasis !== "state_event_log" && basisSequence !== 0) { throw new EffectRuntimeRequestError( - "goal_amendment_proposal.derived_basis.state_event_basis_sequence must be 0 when revision_basis is markdown_active_state", + `goal_amendment_proposal.derived_basis.state_event_basis_sequence must be 0 when revision_basis is ${revisionBasis}`, ); } return { @@ -697,7 +700,7 @@ function admissionOutcome( ); } if ( - proposal.base_revision_basis === "markdown_active_state" && + proposal.base_revision_basis !== "state_event_log" && derived.revision_basis === "state_event_log" ) { // A real markdown base that has since been superseded by the goal's @@ -711,6 +714,13 @@ function admissionOutcome( facts: ["base_revision_basis_superseded"], }; } + if (derived.revision_basis === "canonical_todo_snapshot") { + const facts: GoalAmendmentAdmissionFact[] = []; + if (proposal.base_revision_basis !== derived.revision_basis) facts.push("base_revision_basis_superseded"); + if (proposal.base_source_basis_digest !== derived.source_basis_digest) facts.push("base_source_basis_digest_mismatch"); + return facts.length ? {admission: "needs_rebase", facts} + : {admission: "admitted", facts: ["base_source_basis_unverifiable"]}; + } if (derived.revision_basis === "markdown_active_state") { // No event log to compare against: report unverifiable, never a // fabricated stale verdict (same policy as Stage 1). @@ -748,14 +758,19 @@ export function decodeGoalAmendmentProposalRequest( "goal amendment proposal request schema mismatch", ); } + if (request.work_items !== undefined && request.goal_todo_inventory !== undefined) { + throw new EffectRuntimeRequestError("work_items cannot mix with preselected amendment inventory"); + } + const proposal = decodeProposal(request.proposal); return { schema_version: GOAL_AMENDMENT_PROPOSAL_REQUEST_SCHEMA_VERSION, - proposal: decodeProposal(request.proposal), + proposal, derived_basis: decodeDerivedBasis(request.derived_basis), open_replan_obligations: decodeOpenReplanObligations( request.open_replan_obligations, ), - goal_todo_inventory: decodeGoalTodoInventory(request.goal_todo_inventory), + goal_todo_inventory: decodeGoalTodoInventory(request.work_items === undefined + ? request.goal_todo_inventory : sharedGoalWorkFacts(request.work_items, proposal.proposer_agent_id, request.observed_at).goal_todo_inventory), }; } diff --git a/loopx/control_plane/goals/shared_goal_alignment.py b/loopx/control_plane/goals/shared_goal_alignment.py index 62d6f16db..c17b8252b 100644 --- a/loopx/control_plane/goals/shared_goal_alignment.py +++ b/loopx/control_plane/goals/shared_goal_alignment.py @@ -1,7 +1,7 @@ """Read-only shared goal alignment projection adapter (RFC Stage 1). This adapter collects typed facts for one registered Agent around one shared -Goal — registry identity, the markdown active state, the append-only state +Goal — registry identity, the selected Todo/lease source, the append-only state event log, Todo claim/lease fields, and recorded replan obligations — and asks the TypeScript-owned reducer (``goal.shared_goal_alignment.project``) to project ``shared_goal_alignment_v0``. @@ -13,7 +13,7 @@ The projection is strictly read-only: no writer path is touched, and no approval or escalation semantics exist here. ``source_basis_digest`` is a typed source-facts basis summary (goal status, registered agents, and -event-log basis facts), not a canonical intent-envelope digest — the full +event-log basis facts, and canonical Todo revision when promoted), not a canonical intent-envelope digest — the full RFC §3.1 envelope (objective, non-goals, acceptance, permission scope, terminal conditions) has no typed storage yet, so nothing here claims canonical intent identity. @@ -22,8 +22,9 @@ codebase is the state event log's ``append_sequence``, so ``state_event_basis_sequence`` reports that event projection basis — it is NOT a canonical goal/intent revision. Goals without a parsable -``events.jsonl`` project ``revision_basis="markdown_active_state"`` with -``state_event_basis_sequence=0`` and every Agent frontier ``unbound``; +``events.jsonl`` use sequence 0 and an unbound Agent frontier. Before promotion +this is ``markdown_active_state``; after promotion it is ``canonical_todo_snapshot`` +with a separate ``todo_basis`` token, never a fabricated event sequence; drift is then reported as ``frontier_basis_unverifiable`` instead of a fabricated behind fact. """ @@ -43,21 +44,10 @@ build_state_projection, event_sort_key, ) -from ...registry import registry_goals, resolve_state_file +from ...registry import registry_goals from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result -from ..todos.active_state_todo_parser import parse_active_state_todos -from ..todos.contract import ( - TODO_TASK_CLASS_ADVANCEMENT, - normalize_todo_bound_agent, - normalize_todo_claimed_by, - normalize_todo_id, -) -from ..todos.projection import ( - todo_advancement_frontier_counts, - todo_item_is_actionable_open, -) -from ..work_items.local_lease_record import lease_epoch, read_lease -from ..work_items.task_lease import lease_is_active, task_lease_path +from ..todos.contract import normalize_todo_claimed_by +from .shared_goal_work_source import SharedGoalWorkSource, read_shared_goal_work_source from .active_state_event_projection import state_event_log_candidates from .active_state_metadata import parse_state_frontmatter from .goal_frontier import ( @@ -183,6 +173,7 @@ def _source_basis_facts_envelope( last_append_sequence: int | None, source_checksum: str | None, state_updated_at: str | None, + todo_basis: dict[str, Any] | None = None, ) -> dict[str, Any]: return { "goal_id": goal_id, @@ -192,107 +183,23 @@ def _source_basis_facts_envelope( "last_append_sequence": last_append_sequence, "source_checksum": source_checksum, "state_updated_at": state_updated_at, + **({"todo_basis": todo_basis} if todo_basis is not None else {}), } -def _parsed_active_state( - state_text: str, - *, - goal: Mapping[str, Any], - state_path: Path, -) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]: - parsed = parse_active_state_todos( - state_text, - goal=dict(goal), - state_path=state_path, - item_limit=None, - ) - summary = parsed.get("agent_todos") if isinstance(parsed, dict) else None - items = summary.get("items") if isinstance(summary, dict) else None - if not isinstance(summary, dict): - return None, [] - if not isinstance(items, list): - return summary, [] - return summary, [item for item in items if isinstance(item, dict)] - - -def _frontier_claim_items( - items: list[dict[str, Any]], - *, - agent_id: str, -) -> list[dict[str, Any]]: - return [ - item - for item in items - if todo_item_is_actionable_open(item) - and item.get("task_class") == TODO_TASK_CLASS_ADVANCEMENT - and normalize_todo_claimed_by(item.get("claimed_by")) == agent_id - ] - - -def _unclaimed_eligible_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: - return [ - item - for item in items - if todo_item_is_actionable_open(item) - and item.get("task_class") == TODO_TASK_CLASS_ADVANCEMENT - and not normalize_todo_claimed_by(item.get("claimed_by")) - ] - - -def _peer_claimed_bound_todo_ids( - items: list[dict[str, Any]], - *, - agent_id: str, -) -> list[str]: - todo_ids: list[str] = [] - for item in items: - claimed_by = normalize_todo_claimed_by(item.get("claimed_by")) - if not claimed_by or claimed_by == agent_id: - continue - if not todo_item_is_actionable_open(item): - continue - if item.get("task_class") != TODO_TASK_CLASS_ADVANCEMENT: - continue - if normalize_todo_bound_agent(item.get("bound_agent")) != agent_id: - continue - todo_id = normalize_todo_id(item.get("todo_id")) - if todo_id and todo_id not in todo_ids: - todo_ids.append(todo_id) - return todo_ids - - -def _claim_lease_facts( - todo_id: str, - *, - runtime_root: Path | None, - goal_id: str, +def project_shared_goal_alignment( + *, goal_id: str, agent_id: str | None, project: Path, + registry_path: Path | None = None, runtime_root: Path | None = None, + status_item: Mapping[str, Any] | None = None, + project_asset: Mapping[str, Any] | None = None, ) -> dict[str, Any]: - if runtime_root is None: - return {"lease_epoch": None, "lease_owner": None} - lease = read_lease( - task_lease_path( - runtime_root=runtime_root, - goal_id=goal_id, - todo_id=todo_id, - ) - ) - if not lease or not lease_is_active(lease): - return {"lease_epoch": None, "lease_owner": None} - owner = normalize_todo_claimed_by(lease.get("owner")) - if not owner: - # An active hard lease without a valid owner is corrupt authority. - # Projecting it as lease facts with a null owner would let the - # reducer treat the broken lease as "no conflict"; fail closed - # before the typed request is built instead. - raise ValueError( - "active task lease has no valid owner: " - f"goal={goal_id} todo={todo_id}" - ) - return {"lease_epoch": lease_epoch(lease), "lease_owner": owner} + """Public read-only entrypoint; callers cannot inject the Todo snapshot.""" + return _project_shared_goal_alignment(goal_id=goal_id, agent_id=agent_id, + project=project, registry_path=registry_path, runtime_root=runtime_root, + status_item=status_item, project_asset=project_asset) -def project_shared_goal_alignment( +def _project_shared_goal_alignment( *, goal_id: str, agent_id: str | None, @@ -301,6 +208,7 @@ def project_shared_goal_alignment( runtime_root: Path | None = None, status_item: Mapping[str, Any] | None = None, project_asset: Mapping[str, Any] | None = None, + work_source: SharedGoalWorkSource | None = None, ) -> dict[str, Any]: """Project the read-only ``shared_goal_alignment_v0`` view for one Agent.""" @@ -334,12 +242,13 @@ def project_shared_goal_alignment( f"{normalized_agent_id}" ) - state_file = resolve_state_file(project, goal.get("state_file")) - if state_file is None or not state_file.is_file(): - raise ValueError( - f"goal state file is missing for {normalized_goal_id}" - ) - state_text = state_file.read_text(encoding="utf-8") + effective_runtime_root = runtime_root if runtime_root is not None else _runtime_root_from_registry(registry_payload) + source = work_source or read_shared_goal_work_source( + goal=goal, project=project, runtime_root=effective_runtime_root, + ) + if source.goal_id != normalized_goal_id: + raise ValueError("shared work snapshot belongs to another Goal") + state_file, state_text = source.state_path, source.state_text event_facts = _load_state_event_facts(goal, state_path=state_file) frontmatter = parse_state_frontmatter(state_text) @@ -359,7 +268,8 @@ def project_shared_goal_alignment( str(projection.get("source_checksum") or "").strip() or None ) else: - revision_basis = REVISION_BASIS_MARKDOWN_ACTIVE_STATE + revision_basis = ("canonical_todo_snapshot" if source.canonical_basis is not None + else REVISION_BASIS_MARKDOWN_ACTIVE_STATE) basis_sequence = 0 source_checksum = None @@ -376,6 +286,7 @@ def project_shared_goal_alignment( ), source_checksum=source_checksum, state_updated_at=state_updated_at, + todo_basis=source.canonical_basis, ) ) source_basis = { @@ -383,6 +294,7 @@ def project_shared_goal_alignment( "source_basis_digest": source_basis_digest, "revision_basis": revision_basis, "state_updated_at": state_updated_at, + **({"todo_basis": source.canonical_basis} if source.canonical_basis is not None else {}), } frontier_basis = _agent_frontier_basis( @@ -390,52 +302,6 @@ def project_shared_goal_alignment( agent_id=normalized_agent_id, ) - agent_summary, items = _parsed_active_state( - state_text, - goal=goal, - state_path=state_file, - ) - frontier_counts = todo_advancement_frontier_counts( - agent_summary, - agent_id=normalized_agent_id, - ) - - effective_runtime_root = ( - runtime_root - if runtime_root is not None - else _runtime_root_from_registry(registry_payload) - ) - claims = [] - for item in _frontier_claim_items(items, agent_id=normalized_agent_id): - todo_id = normalize_todo_id(item.get("todo_id")) - if not todo_id: - continue - claims.append( - { - "todo_id": todo_id, - "claimed_by": normalized_agent_id, - **_claim_lease_facts( - todo_id, - runtime_root=effective_runtime_root, - goal_id=normalized_goal_id, - ), - } - ) - - unclaimed_eligible = [ - { - "todo_id": normalize_todo_id(item.get("todo_id")), - "task_class": TODO_TASK_CLASS_ADVANCEMENT, - **( - {"action_kind": str(item.get("action_kind"))} - if str(item.get("action_kind") or "").strip() - else {} - ), - } - for item in _unclaimed_eligible_items(items) - if normalize_todo_id(item.get("todo_id")) - ] - replan_obligation = select_autonomous_replan_obligation( dict(status_item) if isinstance(status_item, Mapping) else {}, dict(project_asset) if isinstance(project_asset, Mapping) else None, @@ -448,13 +314,8 @@ def project_shared_goal_alignment( "agent_id": normalized_agent_id, "source_basis": source_basis, "frontier_basis": frontier_basis, - "frontier_counts": frontier_counts, - "claims": claims, - "unclaimed_eligible": unclaimed_eligible, - "peer_claimed_bound_todo_ids": _peer_claimed_bound_todo_ids( - items, - agent_id=normalized_agent_id, - ), + "work_items": source.items, + "observed_at": source.observed_at, "open_lane_replan_obligation_required": ( autonomous_replan_is_required(replan_obligation) ), diff --git a/loopx/control_plane/goals/shared_goal_alignment.ts b/loopx/control_plane/goals/shared_goal_alignment.ts index b4d863a28..32fd903f9 100644 --- a/loopx/control_plane/goals/shared_goal_alignment.ts +++ b/loopx/control_plane/goals/shared_goal_alignment.ts @@ -1,4 +1,5 @@ import type { JsonObject } from "../effect_program.ts"; +import { sharedGoalWorkFacts } from "./shared_goal_work.ts"; import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { optionalNonEmptyString, @@ -21,10 +22,12 @@ import { * computed once on the Python side so both runtimes observe one value. * * Basis semantics: `source_basis` is an event-log-derived projection basis, - * NOT a canonical intent revision. `state_event_basis_sequence` is the state + * NOT a canonical intent revision. With no event log, canonical Todo reads + * use `canonical_todo_snapshot`, sequence 0 and an unbound Agent frontier. + * The separate `todo_basis` identifies the single Todo/lease snapshot. `state_event_basis_sequence` is the state * event log's append sequence (or 0 with the markdown fallback), and - * `source_basis_digest` hashes goal status, registered agents, and event-log - * basis facts — the RFC §3.1 canonical intent envelope (objective, + * `source_basis_digest` hashes goal status, registered agents, event-log + * basis facts and the canonical Todo revision when promoted — the RFC §3.1 canonical intent envelope (objective, * non-goals, acceptance, permissions, terminal conditions) has no typed * storage yet, so no field here claims canonical intent identity. This * contract has no writer surface: it projects drift and conflict facts only @@ -53,6 +56,7 @@ export type SharedGoalAlignmentConflictFact = const REVISION_BASIS_VALUES = [ "state_event_log", "markdown_active_state", + "canonical_todo_snapshot", ] as const; const BASIS_SOURCE_VALUES = ["state_event_log", "unbound"] as const; @@ -74,6 +78,7 @@ export interface SourceBasisFacts extends JsonObject { source_basis_digest: string; revision_basis: RevisionBasis; state_updated_at: string | null; + todo_basis?: JsonObject; } export interface FrontierBasisFacts extends JsonObject { @@ -185,9 +190,9 @@ function decodeSourceBasis(value: unknown): SourceBasisFacts { "shared_goal_alignment source_basis state_event_basis_sequence must be a positive event append sequence when revision_basis is state_event_log", ); } - if (revisionBasis === "markdown_active_state" && basisSequence !== 0) { + if (revisionBasis !== "state_event_log" && basisSequence !== 0) { throw new EffectRuntimeRequestError( - "shared_goal_alignment source_basis state_event_basis_sequence must be 0 when revision_basis is markdown_active_state", + `shared_goal_alignment source_basis state_event_basis_sequence must be 0 when revision_basis is ${revisionBasis}`, ); } const sourceBasisDigest = requireNonEmptyString( @@ -199,6 +204,19 @@ function decodeSourceBasis(value: unknown): SourceBasisFacts { "shared_goal_alignment.source_basis.source_basis_digest must be a sha256: digest computed from typed source facts", ); } + let todoBasis: JsonObject | undefined; + if (raw.todo_basis !== undefined) { + const basis = requireJsonObject(raw.todo_basis, "todo_basis"); + if (basis.source_authority !== "file_v0" || + typeof basis.records_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(basis.records_sha256)) { + throw new EffectRuntimeRequestError("invalid canonical Todo basis"); + } + todoBasis = {source_authority: basis.source_authority, records_sha256: basis.records_sha256, + provider_revision: requireNonEmptyString(basis.provider_revision, "todo_basis.provider_revision")}; + } + if (revisionBasis === "canonical_todo_snapshot" && todoBasis === undefined) { + throw new EffectRuntimeRequestError("canonical_todo_snapshot requires a canonical Todo basis"); + } return { state_event_basis_sequence: basisSequence, source_basis_digest: sourceBasisDigest, @@ -207,6 +225,7 @@ function decodeSourceBasis(value: unknown): SourceBasisFacts { raw.state_updated_at, "shared_goal_alignment.source_basis.state_updated_at", ), + ...(todoBasis === undefined ? {} : {todo_basis: todoBasis}), }; } @@ -478,7 +497,7 @@ function conflictFacts( export function decodeSharedGoalAlignmentRequest( value: unknown, ): SharedGoalAlignmentRequest { - const request = requireJsonObject(value, "shared_goal_alignment request"); + let request = requireJsonObject(value, "shared_goal_alignment request"); if ( request.schema_version !== SHARED_GOAL_ALIGNMENT_REQUEST_SCHEMA_VERSION ) { @@ -496,6 +515,12 @@ export function decodeSharedGoalAlignmentRequest( ); } const agentIdValue = agentId(request.agent_id, "shared_goal_alignment.agent_id"); + if (request.work_items !== undefined) { + for (const key of ["claims", "frontier_counts", "unclaimed_eligible", "peer_claimed_bound_todo_ids"]) { + if (request[key] !== undefined) throw new EffectRuntimeRequestError("work_items cannot mix with preselected alignment facts"); + } + request = {...request, ...sharedGoalWorkFacts(request.work_items, agentIdValue, request.observed_at)}; + } const sourceBasis = decodeSourceBasis(request.source_basis); const frontier = decodeFrontierBasis(request.frontier_basis, sourceBasis); const claims = decodeClaims(request.claims, agentIdValue); diff --git a/loopx/control_plane/goals/shared_goal_work.ts b/loopx/control_plane/goals/shared_goal_work.ts new file mode 100644 index 000000000..5c653e99f --- /dev/null +++ b/loopx/control_plane/goals/shared_goal_work.ts @@ -0,0 +1,72 @@ +/** Read lenses over one full Todo snapshot; never an execution grant. */ +import type { JsonObject } from "../effect_program.ts"; +import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; +import { requireJsonObject, optionalNonEmptyString, requireStringArray } from "../runtime_decode.ts"; +import { leaseIsActive, leaseEpoch, TaskLeaseAcquireError } from "../work_items/task_lease_acquire.ts"; +import { normalizeTodoAgent } from "../coordination/todo_agents.ts"; +import { AuthorityStoreProtocolError } from "../coordination/authority_store_codec.ts"; + +export function sharedGoalWorkFacts(value: unknown, agent: string, observedAt: unknown): JsonObject { + if (typeof observedAt !== "string" || !Number.isFinite(Date.parse(observedAt))) { + throw new EffectRuntimeRequestError("shared goal work requires a valid observed_at"); + } + const now = new Date(observedAt); + if (!Array.isArray(value)) throw new EffectRuntimeRequestError("shared goal work_items must be an array"); + const seen = new Set(); + const current: JsonObject[] = [], unclaimed: JsonObject[] = [], peers: JsonObject[] = []; + const inventory: JsonObject[] = []; + for (const entry of value) { + const item = requireJsonObject(entry, "shared goal work item"); + const id = optionalNonEmptyString(item.todo_id, "todo_id"); + if (!id) continue; // Historical Markdown can contain unaddressable rows. + if (!/^todo_[a-z0-9_-]{3,64}$/.test(id)) throw new EffectRuntimeRequestError("invalid shared goal Todo id"); + if (seen.has(id)) throw new EffectRuntimeRequestError("duplicate Todo in shared goal snapshot"); + seen.add(id); + const status = optionalNonEmptyString(item.status, "status") ?? "open"; + if (!["open", "done", "blocked", "deferred"].includes(status)) { + throw new EffectRuntimeRequestError("invalid shared goal Todo status"); + } + for (const field of ["done", "resume_ready"]) { + if (item[field] != null && typeof item[field] !== "boolean") throw new EffectRuntimeRequestError(`${field} must be boolean`); + } + if (item.archive_state === "archive" || item.done === true || status !== "open" || + (item.resume_when && item.resume_ready !== true)) continue; + const claim = optionalNonEmptyString(item.claimed_by, "claimed_by"); + const bound = optionalNonEmptyString(item.bound_agent, "bound_agent"); + const taskClass = optionalNonEmptyString(item.task_class, "task_class"); + inventory.push({todo_id: id, status: "open", task_class: taskClass, claimed_by: claim, bound_agent: bound}); + if (taskClass !== "advancement_task") continue; + const excluded = requireStringArray(item.excluded_agents ?? [], "excluded_agents").includes(agent); + if (claim && claim !== agent) peers.push(item); + else if (!excluded) (claim ? current : unclaimed).push(item); + } + return { + frontier_counts: {current_agent_claimed_advancement_count: current.length, + unclaimed_advancement_count: unclaimed.length, other_agent_claimed_advancement_count: peers.length}, + claims: current.map((item) => { + if (item.lease_read_error != null) { + throw new EffectRuntimeRequestError(`cannot read selected claim lease: ${item.todo_id}`); + } + const lease = item.lease == null ? null : requireJsonObject(item.lease, "claim lease"); + let epoch: number | null = null, owner: string | null = null; + try { + if (leaseIsActive(lease, now)) { + owner = normalizeTodoAgent(lease?.owner, "active task lease owner"); + epoch = leaseEpoch(lease); + } + } catch (error) { + if (error instanceof AuthorityStoreProtocolError) { + throw new EffectRuntimeRequestError("active task lease has no valid owner"); + } + if (error instanceof TaskLeaseAcquireError) { + throw new EffectRuntimeRequestError(error.message); + } + throw error; + } + return {todo_id: item.todo_id, claimed_by: item.claimed_by, lease_epoch: epoch, lease_owner: owner}; + }), + unclaimed_eligible: unclaimed.map((item) => ({todo_id: item.todo_id, task_class: "advancement_task"})), + peer_claimed_bound_todo_ids: peers.filter((item) => item.bound_agent === agent).map((item) => item.todo_id), + goal_todo_inventory: inventory, + }; +} diff --git a/loopx/control_plane/goals/shared_goal_work_source.py b/loopx/control_plane/goals/shared_goal_work_source.py new file mode 100644 index 000000000..6d17ddf3c --- /dev/null +++ b/loopx/control_plane/goals/shared_goal_work_source.py @@ -0,0 +1,71 @@ +"""One read-only Todo/lease source per alignment or amendment decision. + +Before promotion, parse the existing display and lease files. After promotion, +read both collections at one provider revision; never consult or repair display. +This is an adapter over the canonical summary, not a second Todo inventory. +""" +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from ...registry import resolve_state_file +from ..coordination.local_authority import canonical_todo_summary_fields, read_canonical_todos_if_promoted +from ..todos.active_state_todo_parser import parse_active_state_todos +from ..todos.contract import normalize_todo_excluded_agents +from ..work_items.local_lease_record import TaskLeaseError, read_lease +from ..work_items.task_lease import task_lease_path + + +@dataclass(frozen=True) +class SharedGoalWorkSource: + goal_id: str + state_path: Path + state_text: str + items: list[dict[str, Any]] + canonical_basis: dict[str, Any] | None + observed_at: str + + +def read_shared_goal_work_source(*, goal: dict[str, Any], project: Path, + runtime_root: Path | None) -> SharedGoalWorkSource: + goal_id = str(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 {goal_id}") + canonical = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id, + include_leases=True) if runtime_root is not None else None + if canonical is None: + if not state_path.is_file(): + raise ValueError(f"goal state file is missing for {goal_id}") + state_text = state_path.read_text(encoding="utf-8") + fields = parse_active_state_todos(state_text, goal=goal, state_path=state_path, item_limit=None) + basis = None + leases = None + else: + state_text = "" + fields = canonical_todo_summary_fields(canonical["todos"]) + basis = {"source_authority": canonical["source_authority"], + "provider_revision": canonical["provider_revision"], + "records_sha256": canonical["todo_read_model"]["records_sha256"]} + leases = {lease["todo_id"]: lease for lease in canonical["leases"]} + summary = fields.get("agent_todos") or {} + items = [] + for source in summary.get("items") or []: + item = {key: source.get(key) for key in ("todo_id", "status", "done", "task_class", + "archive_state", "claimed_by", "bound_agent", "resume_when", "resume_ready", "action_kind")} + item["excluded_agents"] = normalize_todo_excluded_agents(source.get("excluded_agents")) + item["lease"] = None + todo_id = item["todo_id"] + if todo_id and source.get("claimed_by"): + try: + item["lease"] = leases.get(todo_id) if leases is not None else ( + read_lease(task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)) + if runtime_root is not None else None) + except TaskLeaseError: + # Let the typed selector reject a corrupt *selected* claim. + # Unrelated historical/peer lease files must not block this Agent. + item["lease_read_error"] = "corrupt_lease" + items.append(item) + return SharedGoalWorkSource(goal_id, state_path, state_text, items, basis, + datetime.now(timezone.utc).isoformat()) diff --git a/tests/control_plane/test_canonical_goal_governance.py b/tests/control_plane/test_canonical_goal_governance.py new file mode 100644 index 000000000..88148e2a2 --- /dev/null +++ b/tests/control_plane/test_canonical_goal_governance.py @@ -0,0 +1,164 @@ +"""Real provider and public-entrypoint checks; no live Goal or display repair.""" +import json +import subprocess +from pathlib import Path + +import pytest + +from canonical_authority_fixture import initialize_canonical_authority +from test_goal_amendment_proposal import _write_fixture, _proposal, _admit, _default_events, GOAL_ID +from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection +from loopx.control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable +from loopx.control_plane.goals.shared_goal_alignment import project_shared_goal_alignment +from loopx.control_plane.goals import shared_goal_work_source +from loopx.control_plane.testing.canary_harness import run_json_cli_result + + +def _record(todo_id="todo_current", **fields): + return {"schema_version": "todo_item_v0", "todo_id": todo_id, "role": "agent", + "status": "open", "done": False, "text": "Canonical work", "task_class": "advancement_task", + "archive_state": "active", "source_section": "Agent Todo", "index": 1, **fields} + + +def _canonical(tmp_path, records=None, *, events=None, leases=None, native=False): + paths = _write_fixture(tmp_path, events=events) + projection = build_todo_runtime_shadow_projection(goal_id=GOAL_ID, + todos=[_record()] if records is None else records, leases=leases or [], handoff_mode="soft_claim") + if native: + from hashlib import sha256 + from loopx.control_plane.coordination.coordination_state_contract import ( + TODO_DOMAIN_RECORD_FIELDS, TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + ) + from loopx.control_plane.coordination.local_authority_shadow_projection import canonical_bytes + for record in projection["todos"]: + record["schema_version"] = "todo_domain_record_v0" + record.pop("index", None) + record.pop("source_section", None) + projection["todo_read_model"] = {"schema_version": TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + "contract_fields": list(TODO_DOMAIN_RECORD_FIELDS), "todo_count": len(projection["todos"]), + "records_sha256": sha256(canonical_bytes(projection["todos"])).hexdigest()} + initialize_canonical_authority(paths["runtime"], GOAL_ID, projection, state_path=paths["state_file"]) + return paths + + +def _alignment(paths): + return project_shared_goal_alignment(goal_id=GOAL_ID, agent_id="agent-a", project=paths["project"]) + + +def _mutate_provider(paths): + root = Path(__file__).resolve().parents[2] + store = (root / "loopx/control_plane/coordination/file_authority_store.ts").as_uri() + codec = (root / "loopx/control_plane/coordination/authority_store_codec.ts").as_uri() + script = (f"import {{FileAuthorityStore}} from {json.dumps(store)};" + f"import {{canonicalAuthoritySha256}} from {json.dumps(codec)};" + "const s=new FileAuthorityStore(process.argv[1],process.argv[2]);const h=await s.loadAuthority();" + "h.head.todos[0].text='Changed canonical work';" + "h.head.todo_read_model.records_sha256=canonicalAuthoritySha256(h.head.todos);" + "const r=await s.commitAuthority({expected_provider_revision:h.provider_revision," + "operation_id:'fixture-revision-change',events:[],receipts:[],next_projection:h.head});" + "if(r.status!=='applied')throw Error(JSON.stringify(r));") + subprocess.run(["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", script, + str(paths["runtime"] / "authority/file-v0"), GOAL_ID], check=True, capture_output=True, text=True, timeout=30) + + +def test_empty_canonical_is_authoritative_and_missing_display_is_not_repaired(tmp_path): + paths = _canonical(tmp_path, []) + paths["state_file"].unlink() + code, result = run_json_cli_result("shared-goal-alignment", "--goal-id", GOAL_ID, + "--agent-id", "agent-a", "--project", str(paths["project"]), registry_path=paths["registry"]) + assert code == 0, result + assert result["unclaimed_eligible_work"] == [] + assert result["source_basis"]["revision_basis"] == "canonical_todo_snapshot" + assert result["source_basis"]["state_event_basis_sequence"] == 0 + assert result["frontier_basis"]["basis_source"] == "unbound" + assert not paths["state_file"].exists() + + +@pytest.mark.parametrize("events", [None, _default_events()]) +def test_canonical_revision_changes_proposal_basis_without_changing_event_sequence(tmp_path, events): + paths = _canonical(tmp_path, events=events) + proposal = _proposal(paths, {"affected_todo_ids": ["todo_current"]}) + before = _alignment(paths)["source_basis"] + _mutate_provider(paths) + after = _alignment(paths)["source_basis"] + assert before["state_event_basis_sequence"] == after["state_event_basis_sequence"] + assert before["source_basis_digest"] != after["source_basis_digest"] + result = _admit(paths, proposal) + assert result["admission"] == "needs_rebase" + assert "base_source_basis_digest_mismatch" in result["admission_facts"] + assert result["canonical_effect"] == "none" + + +def test_amendment_uses_one_snapshot_and_cannot_revive_a_display_only_todo(tmp_path, monkeypatch): + paths = _canonical(tmp_path) + proposal = _proposal(paths, {"affected_todo_ids": ["todo_stage2_a"]}) + with pytest.raises(ValueError, match="not open"): + _admit(paths, proposal) + proposal = _proposal(paths, {"affected_todo_ids": ["todo_current"]}) + original = shared_goal_work_source.read_canonical_todos_if_promoted + reads = [] + def capture(**kwargs): + result = original(**kwargs) + reads.append(result["provider_revision"]) + # A concurrent change after this read must not cause a second snapshot + # for inventory after alignment has already selected a different basis. + _mutate_provider(paths) + return result + monkeypatch.setattr(shared_goal_work_source, "read_canonical_todos_if_promoted", capture) + result = _admit(paths, proposal) + assert len(reads) == 1 + assert result["admission"] == "admitted" + assert result["canonical_effect"] == "none" # Snapshot admission is not a commit-time CAS. + + +@pytest.mark.parametrize("native", [False, True]) +def test_display_changes_do_not_change_canonical_basis(tmp_path, native): + paths = _canonical(tmp_path, native=native) + before = _alignment(paths) + paths["state_file"].write_text("---\nstatus: completed\nupdated_at: 2099-01-01\n---\nnot valid Todo data") + assert _alignment(paths) == before + paths["state_file"].unlink() + assert _alignment(paths) == before + + +def test_provider_failure_never_falls_back_to_readable_markdown(tmp_path): + paths = _canonical(tmp_path) + before = paths["state_file"].read_bytes() + provider = paths["runtime"] / "authority/file-v0" + provider.rename(provider.with_name("offline-fixture")) + with pytest.raises(LocalCoordinationAuthorityUnavailable): + _alignment(paths) + assert paths["state_file"].read_bytes() == before + + +def test_canonical_lease_conflict_wins_over_obsolete_lease_file(tmp_path): + lease = {"schema_version": "task_lease_v0", "goal_id": GOAL_ID, "todo_id": "todo_current", + "status": "active", "expires_at": "2099-01-01T00:00:00Z", "owner": "agent-b", + "lease_epoch": 2, "version": 1} + paths = _canonical(tmp_path, [_record(claimed_by="agent-a")], leases=[lease]) + legacy_path = paths["runtime"] / "goals" / GOAL_ID / "task-leases/todo_current.json" + legacy_path.parent.mkdir(parents=True, exist_ok=True) + legacy_path.write_text(json.dumps({**lease, "owner": "agent-a"})) + result = _alignment(paths) + assert "lease_owner_mismatch" in result["conflict_facts"] + legacy_path.write_text("malformed obsolete display") + assert _alignment(paths) == result + + +def test_real_cli_reads_production_scale_snapshot_without_display(tmp_path): + paths = _write_fixture(tmp_path) + module = (Path(__file__).resolve().parents[2] / "tests/control_plane_ts/production_scale_coordination_fixture.ts").as_uri() + process = subprocess.run(["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", + f"import {{productionScaleCoordinationFixture}} from {json.dumps(module)};" + "process.stdout.write(JSON.stringify(productionScaleCoordinationFixture(process.argv[1])));", GOAL_ID], + check=True, capture_output=True, text=True, timeout=30) + fixture = json.loads(process.stdout) + initialize_canonical_authority(paths["runtime"], GOAL_ID, fixture["projection"], state_path=paths["state_file"]) + paths["state_file"].unlink() + code, result = run_json_cli_result("shared-goal-alignment", "--goal-id", GOAL_ID, + "--agent-id", "agent-a", "--project", str(paths["project"]), registry_path=paths["registry"]) + assert code == 0, result + assert result["frontier_counts"] == {"current_agent_claimed_advancement_count": 13, + "unclaimed_advancement_count": 0, "other_agent_claimed_advancement_count": 24} + assert result["read_only"] is True + assert not paths["state_file"].exists() diff --git a/tests/control_plane/test_shared_goal_alignment.py b/tests/control_plane/test_shared_goal_alignment.py index 836deeb37..911705771 100644 --- a/tests/control_plane/test_shared_goal_alignment.py +++ b/tests/control_plane/test_shared_goal_alignment.py @@ -35,6 +35,55 @@ AGENTS = ("agent-a", "agent-b") EVENT_LOG_NAME = "events.jsonl" + +def test_excluded_unclaimed_work_is_not_offered_to_the_agent(tmp_path): + specs = _default_todo_specs() + specs[1]["excluded_agents"] = "agent-a" + fixture = _write_fixture(tmp_path, todo_specs=specs) + result = project_shared_goal_alignment(goal_id=GOAL_ID, agent_id="agent-a", + project=fixture["project"], registry_path=fixture["registry"], runtime_root=fixture["runtime"]) + assert result["frontier_counts"]["unclaimed_advancement_count"] == 0 + assert result["unclaimed_eligible_work"] == [] + + +def test_corrupt_legacy_lease_only_blocks_a_selected_claim(tmp_path): + from loopx.control_plane.work_items.task_lease import task_lease_path + + fixture = _write_fixture(tmp_path, todo_specs=_default_todo_specs()) + for todo_id in ("todo_blocked", "todo_lane_a"): + path = task_lease_path(runtime_root=fixture["runtime"], goal_id=GOAL_ID, todo_id=todo_id) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("{broken") + if todo_id == "todo_blocked": + result = project_shared_goal_alignment(goal_id=GOAL_ID, agent_id="agent-a", + project=fixture["project"], registry_path=fixture["registry"], runtime_root=fixture["runtime"]) + assert result["frontier_counts"]["current_agent_claimed_advancement_count"] == 1 + else: + with pytest.raises(ValueError, match="cannot read selected claim lease"): + project_shared_goal_alignment(goal_id=GOAL_ID, agent_id="agent-a", + project=fixture["project"], registry_path=fixture["registry"], runtime_root=fixture["runtime"]) + + +@pytest.mark.parametrize("display", ["missing", "stale", "empty"]) +def test_promoted_alignment_does_not_use_the_display(tmp_path, display): + from canonical_authority_fixture import initialize_canonical_authority + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + + fixture = _write_fixture(tmp_path, todo_specs=_default_todo_specs()) + record = {"schema_version": "todo_item_v0", "todo_id": "todo_canonical", "role": "agent", + "status": "open", "done": False, "text": "Canonical work", "task_class": "advancement_task", + "archive_state": "active", "source_section": "Agent Todo", "index": 1} + projection = build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=[record], handoff_mode="soft_claim") + initialize_canonical_authority(fixture["runtime"], GOAL_ID, projection, state_path=fixture["state_file"]) + if display == "missing": + fixture["state_file"].unlink() + elif display == "empty": + fixture["state_file"].write_text("") + result = project_shared_goal_alignment(goal_id=GOAL_ID, agent_id="agent-a", + project=fixture["project"], registry_path=fixture["registry"], runtime_root=fixture["runtime"]) + assert result["unclaimed_eligible_work"] == [{"todo_id": "todo_canonical", "claim_required_before_work": True}] + assert fixture["state_file"].exists() is (display != "missing") + STATE_HEADER_LINES = [ "---", "status: active", @@ -895,15 +944,11 @@ def call(method: str, params: dict[str, object]) -> dict[str, object]: assert request["agent_id"] == "agent-a" assert request["source_basis"]["state_event_basis_sequence"] == 3 assert request["frontier_basis"]["based_on_state_event_sequence"] == 3 - assert request["claims"] == [ - { - "todo_id": "todo_lane_a", - "claimed_by": "agent-a", - "lease_epoch": None, - "lease_owner": None, - } - ] - assert request["peer_claimed_bound_todo_ids"] == [] + assert "claims" not in request # Selection now belongs to TS, not the adapter. + own = next(item for item in request["work_items"] if item["todo_id"] == "todo_lane_a") + assert own["claimed_by"] == "agent-a" + assert own["lease"] is None + assert "text" not in own assert request["open_lane_replan_obligation_required"] is False assert projection["schema_version"] == "shared_goal_alignment_v0" diff --git a/tests/control_plane_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts index e3fc4cbd4..0221b3e8d 100644 --- a/tests/control_plane_ts/authority_store_conformance.ts +++ b/tests/control_plane_ts/authority_store_conformance.ts @@ -22,6 +22,9 @@ import { prepareCoordinationProjectionCommit } from "../../loopx/control_plane/c import { executeCoordinationTodoClaim } from "../../loopx/control_plane/coordination/todo_claim.ts"; import { executeCoordinationTodoCreate } from "../../loopx/control_plane/coordination/todo_create.ts"; import { executeCoordinationTodoUpdate } from "../../loopx/control_plane/coordination/todo_update.ts"; +import { listLocalCoordinationTodos, LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA } + from "../../loopx/control_plane/coordination/local_authority_runtime.ts"; +import { sharedGoalWorkFacts } from "../../loopx/control_plane/goals/shared_goal_work.ts"; import { executeCoordinationTodoArchiveCompleted, executeCoordinationTodoTerminalLifecycle, @@ -212,6 +215,53 @@ export function registerAuthorityStoreConformance( providerName: string, factory: AuthorityStoreConformanceFactory, ): void { + for (const native of [false, true]) test(`${providerName} conformance: governance reads one full Todo/lease snapshot (${native ? "native" : "legacy"})`, async (t) => { + const {store} = await factory(t); + const goal = "goal-governance"; + const fixture = productionScaleCoordinationFixture(goal); + const projection = structuredClone(fixture.projection); + const records = projection.todos as Record[]; + if (native) { + for (const record of records) { + record.schema_version = TODO_DOMAIN_ITEM_SCHEMA; + delete record.source_section; delete record.index; + } + projection.todo_read_model = {schema_version: TODO_DOMAIN_READ_RECORD_SCHEMA, + todo_count: records.length, records_sha256: canonicalAuthoritySha256(records), + contract_fields: [...TODO_DOMAIN_RECORD_CONTRACT.fields]}; + } + const seeded = await store.commitAuthority({operation_id: "governance-fixture", + expected_provider_revision: null, events: [], receipts: [], next_projection: projection}); + assert.equal(seeded.status, "applied"); + const request = {schema_version: LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA, + runtime_root: "/synthetic-runtime", goal_id: goal}; + const plain = await listLocalCoordinationTodos(request, {createStore: () => store}); + assert.equal(plain.status, "loaded"); + assert.equal(Object.hasOwn(plain, "leases"), false); // Default-off response parity. + assert.deepEqual(await listLocalCoordinationTodos({...request, include_leases: false}, + {createStore: () => store}), plain); + assert.notEqual((await listLocalCoordinationTodos({...request, include_leases: "true"}, + {createStore: () => store})).status, "loaded"); + const load = store.loadAuthority.bind(store); + let reads = 0; + store.loadAuthority = async () => { reads++; return load(); }; + const full = await listLocalCoordinationTodos({...request, include_leases: true}, {createStore: () => store}); + store.loadAuthority = load; + assert.equal(full.status, "loaded"); + assert.equal(reads, 1); + assert.equal(full.provider_revision, plain.provider_revision); + assert.deepEqual(full.todos, plain.todos); + assert.equal((full.todos as unknown[]).length, fixture.expected_initial_todo_count); + assert.equal((full.leases as unknown[]).length, fixture.expected_current_lease_count); + const leases = new Map((full.leases as Record[]).map((lease) => [lease.todo_id, lease])); + const items = (full.todos as Record[]).filter((item) => item.role === "agent") + .map((item) => ({...item, lease: leases.get(item.todo_id) ?? null})); + const facts = sharedGoalWorkFacts(items, "agent-a", "2026-09-09T12:00:00Z"); + // 48 open rows: 12 monitors, with the completion target converted to advancement. + assert.deepEqual(facts.frontier_counts, {current_agent_claimed_advancement_count: 13, + unclaimed_advancement_count: 0, other_agent_claimed_advancement_count: 24}); + assert.equal((facts.goal_todo_inventory as unknown[]).length, 48); + }); test(`${providerName} conformance: atomic transition, projection, and receipt`, async (t) => { const { store } = await factory(t); assert.deepEqual(await store.loadAuthority(), { status: "missing" }); diff --git a/tests/control_plane_ts/goal_amendment_proposal.test.ts b/tests/control_plane_ts/goal_amendment_proposal.test.ts index 8dad8cd27..a6cf27c5f 100644 --- a/tests/control_plane_ts/goal_amendment_proposal.test.ts +++ b/tests/control_plane_ts/goal_amendment_proposal.test.ts @@ -11,6 +11,23 @@ import { const DIGEST = "sha256:" + "a".repeat(64); const MISMATCHED_DIGEST = "sha256:" + "b".repeat(64); +test("canonical Todo bases cannot be declared fresh merely because both event sequences are zero", () => { + const request = baseRequest(); + request.proposal.base_revision_basis = "canonical_todo_snapshot"; + request.proposal.base_state_event_basis_sequence = 0; + request.derived_basis.revision_basis = "canonical_todo_snapshot"; + request.derived_basis.state_event_basis_sequence = 0; + const fresh = admitGoalAmendmentProposal(request); + assert.equal(fresh.admission, "admitted"); + assert.deepEqual(fresh.admission_facts, ["base_source_basis_unverifiable"]); + request.derived_basis.source_basis_digest = MISMATCHED_DIGEST; + const stale = admitGoalAmendmentProposal(request); + assert.equal(stale.admission, "needs_rebase"); + assert.deepEqual(stale.admission_facts, ["base_source_basis_digest_mismatch"]); + request.proposal.base_revision_basis = "markdown_active_state"; + assert.ok(admitGoalAmendmentProposal(request).admission_facts.includes("base_revision_basis_superseded")); +}); + function baseRequest(overrides: Record = {}) { return { schema_version: "goal_amendment_proposal_request_v0", diff --git a/tests/control_plane_ts/shared_goal_alignment.test.ts b/tests/control_plane_ts/shared_goal_alignment.test.ts index 246de709b..6bd813f43 100644 --- a/tests/control_plane_ts/shared_goal_alignment.test.ts +++ b/tests/control_plane_ts/shared_goal_alignment.test.ts @@ -8,6 +8,18 @@ import { const DIGEST = "sha256:" + "a".repeat(64); +test("canonical Todo basis does not fabricate an event frontier or accept mixed selectors", () => { + const request = baseRequest({source_basis: {revision_basis: "canonical_todo_snapshot", + state_event_basis_sequence: 0, source_basis_digest: DIGEST, state_updated_at: null, + todo_basis: {source_authority: "file_v0", provider_revision: "opaque-provider-token", records_sha256: "a".repeat(64)}}, + frontier_basis: {basis_source: "unbound", based_on_state_event_sequence: null, last_agent_event_id: null}}); + const result = projectSharedGoalAlignment(request); + assert.deepEqual(result.drift_facts, []); + assert.ok(result.conflict_facts.includes("frontier_basis_unverifiable")); + assert.equal(result.source_basis.todo_basis?.provider_revision, "opaque-provider-token"); + assert.throws(() => projectSharedGoalAlignment({...request, work_items: [], observed_at: "2026-09-09T00:00:00Z"}), /cannot mix/); +}); + function baseRequest(overrides: Record = {}) { return { schema_version: "shared_goal_alignment_request_v0", diff --git a/tests/control_plane_ts/shared_goal_work.test.ts b/tests/control_plane_ts/shared_goal_work.test.ts new file mode 100644 index 000000000..4446a5f90 --- /dev/null +++ b/tests/control_plane_ts/shared_goal_work.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { sharedGoalWorkFacts } from "../../loopx/control_plane/goals/shared_goal_work.ts"; + +const at = "2026-09-09T12:00:00Z"; +const item = (todo_id: string, extra = {}) => ({todo_id, status: "open", task_class: "advancement_task", ...extra}); + +test("alignment selection respects exclusions without narrowing amendment impact", () => { + const result = sharedGoalWorkFacts([ + item("todo_own", {claimed_by: "agent-a"}), item("todo_free"), + item("todo_excluded", {excluded_agents: ["agent-a"]}), + item("todo_peer", {claimed_by: "agent-b"}), + item("todo_own_excluded", {claimed_by: "agent-a", excluded_agents: ["agent-a"]}), + ], "agent-a", at); + assert.deepEqual(result.frontier_counts, {current_agent_claimed_advancement_count: 1, + unclaimed_advancement_count: 1, other_agent_claimed_advancement_count: 1}); + assert.deepEqual(result.unclaimed_eligible, [{todo_id: "todo_free", task_class: "advancement_task"}]); + assert.equal((result.goal_todo_inventory as unknown[]).length, 5); +}); + +test("closed, archived and unsatisfied waits cannot reenter the open inventory", () => { + const result = sharedGoalWorkFacts([ + item("todo_done", {done: true}), item("todo_archive", {archive_state: "archive"}), + item("todo_blocked", {status: "blocked"}), item("todo_deferred", {status: "deferred", resume_ready: true}), + item("todo_wait", {resume_when: "todo_done:todo_target", resume_ready: false}), + item("todo_ready", {resume_when: "todo_done:todo_target", resume_ready: true}), + item("todo_monitor", {task_class: "continuous_monitor"}), + ], "agent-a", at); + assert.deepEqual((result.goal_todo_inventory as {todo_id: string}[]).map((x) => x.todo_id), ["todo_ready", "todo_monitor"]); + assert.deepEqual(result.unclaimed_eligible, [{todo_id: "todo_ready", task_class: "advancement_task"}]); +}); + +test("lease facts use the existing typed epoch and activity rules only for selected claims", () => { + const lease = {schema_version: "task_lease_v0", status: "active", expires_at: "2099-01-01T00:00:00Z", + owner: "agent-b", lease_epoch: 3}; + const result = sharedGoalWorkFacts([item("todo_own", {claimed_by: "agent-a", lease}), + item("todo_peer", {claimed_by: "agent-b", lease: {...lease, lease_epoch: 0}})], "agent-a", at); + assert.deepEqual(result.claims, [{todo_id: "todo_own", claimed_by: "agent-a", lease_epoch: 3, lease_owner: "agent-b"}]); + for (const bad of [{lease_epoch: true}, {lease_epoch: 0}, {expires_at: "broken"}, {owner: ""}]) { + assert.throws(() => sharedGoalWorkFacts([item("todo_own", {claimed_by: "agent-a", lease: {...lease, ...bad}})], "agent-a", at)); + } + const expired = sharedGoalWorkFacts([item("todo_own", {claimed_by: "agent-a", + lease: {...lease, expires_at: at}})], "agent-a", at); + assert.deepEqual(expired.claims, [{todo_id: "todo_own", claimed_by: "agent-a", lease_epoch: null, lease_owner: null}]); +}); + +test("full input is not limited by display budgets and contradictory records fail closed", () => { + assert.equal((sharedGoalWorkFacts(Array.from({length: 400}, (_, i) => item(`todo_item_${i}`)), "agent-a", at) + .unclaimed_eligible as unknown[]).length, 400); + assert.throws(() => sharedGoalWorkFacts([item("todo_same"), item("todo_same")], "agent-a", at), /duplicate/); + assert.throws(() => sharedGoalWorkFacts([item("todo_bad", {done: "false"})], "agent-a", at), /boolean/); + assert.throws(() => sharedGoalWorkFacts([item("todo_bad", {status: "invented"})], "agent-a", at), /status/); + assert.throws(() => sharedGoalWorkFacts([], "agent-a", "not a date"), /observed_at/); +}); From ed4f794764600510f38e538a2f9d1556c4557327 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Wed, 9 Sep 2026 21:29:43 +0800 Subject: [PATCH 2/2] docs(rfc): record bounded canonical governance consumer closure Signed-off-by: huangruiteng --- ...d-goal-alignment-and-governed-amendment-v0.md | 16 +++++++++++++++- ...-alignment-and-governed-amendment-v0.zh-CN.md | 13 ++++++++++++- .../shared-goal-authority-state-provider-v0.md | 7 +++++++ ...red-goal-authority-state-provider-v0.zh-CN.md | 6 ++++++ .../typescript-control-plane-migration-v0.md | 14 ++++++++++++++ ...ypescript-control-plane-migration-v0.zh-CN.md | 11 +++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) diff --git a/docs/architecture/rfcs/shared-goal-alignment-and-governed-amendment-v0.md b/docs/architecture/rfcs/shared-goal-alignment-and-governed-amendment-v0.md index 35b8807e3..56464737f 100644 --- a/docs/architecture/rfcs/shared-goal-alignment-and-governed-amendment-v0.md +++ b/docs/architecture/rfcs/shared-goal-alignment-and-governed-amendment-v0.md @@ -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 @@ -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: diff --git a/docs/architecture/rfcs/shared-goal-alignment-and-governed-amendment-v0.zh-CN.md b/docs/architecture/rfcs/shared-goal-alignment-and-governed-amendment-v0.zh-CN.md index 2c8fe3079..18db05086 100644 --- a/docs/architecture/rfcs/shared-goal-alignment-and-governed-amendment-v0.zh-CN.md +++ b/docs/architecture/rfcs/shared-goal-alignment-and-governed-amendment-v0.zh-CN.md @@ -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 决策 @@ -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**; 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 4bfc84557..3c0574728 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -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. 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 acb69de34..cd71ce751 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 @@ -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-合入后的执行卡) 收口规则并删除重复决策。 diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 127e423fd..647f4e9b9 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -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. 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 4f26f45c4..595aaf47d 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 @@ -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。