From 012470a97eb04d6910947782f4560b2959b3801e Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Wed, 9 Sep 2026 13:59:34 +0800 Subject: [PATCH 1/5] fix(delivery): reconcile history supervision with canonical waiting Signed-off-by: huangruiteng --- .../typescript-control-plane-migration-v0.md | 9 +++ ...script-control-plane-migration-v0.zh-CN.md | 6 ++ .../control_plane/effect_runtime_handlers.ts | 3 +- .../handoff/delivery_contract.py | 6 ++ loopx/control_plane/todos/resume_condition.ts | 22 +++++ .../work_items/delivery_history.py | 38 +++++++++ .../work_items/delivery_history.ts | 37 +++++++++ .../work_items/work_lane_context.py | 10 ++- loopx/quota.py | 7 ++ loopx/status.py | 4 +- tests/control_plane/test_delivery_response.py | 80 +++++++++++++++++++ .../delivery_response.test.ts | 77 ++++++++++++++++++ 12 files changed, 293 insertions(+), 6 deletions(-) create mode 100644 tests/control_plane/test_delivery_response.py create mode 100644 tests/control_plane_ts/delivery_response.test.ts diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 29ae29a956..37a28f34d0 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -194,6 +194,15 @@ label; no legacy prediction is retained without a concrete display consumer. the normalized claim before registry access or lock creation. Invalid input therefore takes precedence over store errors, including in dry-run mode; state-dependent admission and writeback still share the same runtime lock. +- Delivery response is a separate typed read decision consumed by quota, + handoff and work-lane projection. A scoped blocked observation exempts the + historical outcome floor only while its canonical Todo has a positively + identified pending resume target. Missing/invalid source, another actor's + claim, exclusions and unbound legacy blocker labels cannot establish that + exemption. Other runnable work remains selectable by the canonical planner. + Unknown refreshes interrupt statistics, not Todo/replan obligations; no new + persistent delivery ledger is added. Surface-only supervision and the + independent small-delivery rule remain unchanged. - Legacy outcome-marker/hint configuration remains readable and preserves whether an outcome floor is configured. Its words no longer classify runs. No persisted history is rewritten and no new default-off flag restores the 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 9ab7952d7b..14bd19526e 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 @@ -149,6 +149,12 @@ classification 保留为历史标签;没有明确展示消费者时,不保 Refresh 先按既有顺序校验各字段,再以归一化结果检查组合语义,之后才读取 registry 和创建锁。因此非法输入优先于存储错误返回,dry-run 也一致;依赖 当前状态的准入与写回仍在同一 runtime 锁内完成。 +- delivery response 是 quota、handoff 和 work-lane 共用的 TS 只读决策:只有 + 绑定的 blocked observation 与当前 canonical Todo 的明确、合法等待条件一致, + 才不施加历史 outcome floor。来源缺失/非法、其他 actor claim、exclusion 和 + 无绑定的旧 blocker 标签不能建立该例外;其他可执行工作仍由 canonical planner + 选择。unknown 刷新中断统计,不清除 Todo/replan 义务,不新增持久化交付账本。 + 连续表层交付监督与独立的小规模交付规则保持不变。 - 新交付声明通过现有 writer API 写显式 enum,例如 `refresh-state --delivery-outcome ... --delivery-batch-scale ...`。 纯状态刷新仍可不声明交付;本批不强迫每次刷新声明进展。既有写入 enum 校验、 diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index e94029f924..10461959b2 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -95,7 +95,7 @@ import { qualifyActionSelection, } from "./work_items/action_portfolio.ts"; import { projectQuotaPlanningHorizon } from "./work_items/planning_horizon.ts"; -import { projectDeliveryHistory } from "./work_items/delivery_history.ts"; +import { projectDeliveryHistory, projectDeliveryResponse } from "./work_items/delivery_history.ts"; import { validateDeliveryClaim } from "./work_items/delivery_outcome.ts"; import { evaluateTaskLeaseAcquireDecision, @@ -411,6 +411,7 @@ export function createEffectRuntimeHandlers( ["work_item.planning_inventory.detail", projectTodoPlanningInventoryDetail], ["work_item.refresh_recommendation.resolve", resolveRefreshRecommendation], ["work_item.delivery_history.project", projectDeliveryHistory], + ["work_item.delivery_response.project", projectDeliveryResponse], ["work_item.delivery_claim.validate", validateDeliveryClaim], ["goal.vision_checkpoint.evaluate", buildVisionCheckpoint], ["goal.vision_wait.coverage", projectVisionWaitCoverage], diff --git a/loopx/control_plane/handoff/delivery_contract.py b/loopx/control_plane/handoff/delivery_contract.py index 1a39f02738..123c355cd5 100644 --- a/loopx/control_plane/handoff/delivery_contract.py +++ b/loopx/control_plane/handoff/delivery_contract.py @@ -10,6 +10,7 @@ outcome_floor_threshold, ) from ..runtime.public_safety import compact_text +from ..work_items.delivery_history import project_delivery_response def compact_packet_text(value: str, limit: int = 180) -> str: @@ -61,6 +62,11 @@ def handoff_delivery_contract(item: dict[str, Any] | None) -> dict[str, Any] | N isinstance(outcome_gap_streak, int) and outcome_gap_streak >= outcome_threshold ) + latest = readiness.get("post_handoff_latest_run") + if outcome_degraded and isinstance(latest, dict): + outcome_degraded = project_delivery_response( + latest, item.get("agent_todos") or project_asset.get("agent_todos"), + )["outcome_floor_applicable"] if not small_degraded and not outcome_degraded: return None recent_runs = readiness.get("post_handoff_recent_runs") diff --git a/loopx/control_plane/todos/resume_condition.ts b/loopx/control_plane/todos/resume_condition.ts index 72342906fc..7a5601719d 100644 --- a/loopx/control_plane/todos/resume_condition.ts +++ b/loopx/control_plane/todos/resume_condition.ts @@ -430,6 +430,28 @@ function diagnosedCondition(condition: JsonObject, waitingTodoId: string): JsonO }; } +/** Positive wait proof for consumers that may relax supervision. Historical + * absence of an invalid marker is not proof of a valid, identified target. */ +export function resumeConditionHasKnownPendingTarget(condition: JsonObject): boolean { + if (condition.schema_version !== "todo_resume_condition_v0" || condition.satisfied !== false + || resumeAvailabilityReason(condition) !== "resume_condition_pending") return false; + switch (condition.kind) { + case "todo_done": + return ["open", "deferred"].includes(String(condition.target_status)) + && typeof condition.target_task_class === "string" && condition.target_task_class !== "continuous_monitor" + && (condition.target_archive_state === null || condition.target_archive_state === "active"); + case "monitor_changed": + return condition.target_task_class === "continuous_monitor" && condition.target_status === "open" + && typeof condition.baseline_generation === "number" && condition.baseline_generation >= 0 + && typeof condition.material_change_generation === "number" + && condition.material_change_generation <= condition.baseline_generation; + case "capacity_available": return condition.provider_required === false; + case "pr_merged": return typeof condition.pr_repo === "string" && condition.pr_repo.length > 0 + && condition.repository_binding_state !== "ambiguous"; + default: return false; + } +} + export function evaluateTodoResumeConditions(value: unknown): JsonObject { const request = requireJsonObject(value, "todo_resume_evaluation_request"); if (request.schema_version !== TODO_RESUME_EVALUATION_REQUEST_SCHEMA_VERSION) { diff --git a/loopx/control_plane/work_items/delivery_history.py b/loopx/control_plane/work_items/delivery_history.py index 40960b3157..b667af3e2e 100644 --- a/loopx/control_plane/work_items/delivery_history.py +++ b/loopx/control_plane/work_items/delivery_history.py @@ -81,3 +81,41 @@ def require_consistent_delivery_claim(record: Mapping[str, Any]) -> None: raise RuntimeError("TypeScript delivery claim validation shape mismatch") if result.get("valid") is not True: raise ValueError("contradictory delivery claim: " + ", ".join(result.get("conflicts") or [])) + + +def project_delivery_response( + run: Mapping[str, Any], summary: dict[str, Any] | None, +) -> dict[str, Any]: + """Select a canonical source row; TS alone decides its supervision meaning.""" + from ..todos.summary_item import todo_planning_source_items + from ..todos.projection import todo_summary_claim_scope_agent_id + + source = next((item for item in todo_planning_source_items(summary, include_terminal=True) + if item.get("todo_id") == run.get("todo_id")), None) if summary else None + fields = ("todo_id", "role", "status", "task_class", "archive_state", "claimed_by", + "excluded_agents", "resume_when", "resume_ready", "resume_condition") + todo = {key: source[key] for key in fields if key in source} if source else None + if todo and isinstance(todo.get("resume_condition"), dict): + condition = todo["resume_condition"] + todo["resume_condition"] = {key: condition[key] for key in ( + "schema_version", "resume_when", "satisfied", "invalid_target", "invalid_state", + "kind", "target_status", "target_task_class", "target_archive_state", + "baseline_generation", "material_change_generation", "provider_required", "pr_repo", "repository_binding_state", + ) if key in condition} + result = effect_runtime_result("work_item.delivery_response.project", { + "run": _run_facts(run), "todo": todo, + "agent_id": todo_summary_claim_scope_agent_id(summary), + "run_agent_id": run.get("agent_id"), + }) + if not isinstance(result, dict) or result.get("schema_version") != "delivery_response_v0": + raise RuntimeError("TypeScript delivery response shape mismatch") + if result["outcome_followthrough"] is not None: + result["outcome_followthrough"]["latest_classification"] = _text(run.get("classification")).strip() + return result + + +def compact_delivery_binding(run: Mapping[str, Any]) -> dict[str, Any]: + """Retain compact evidence identity, never evidence bodies, for read decisions.""" + facts = _run_facts(run) + return {key: facts[key] for key in ("todo_id", "replan_obligation_id", "progress_observation") + if facts[key]} | ({"agent_id": _bounded_text(_text(run["agent_id"]))} if run.get("agent_id") else {}) diff --git a/loopx/control_plane/work_items/delivery_history.ts b/loopx/control_plane/work_items/delivery_history.ts index ade5581188..dfabd29c98 100644 --- a/loopx/control_plane/work_items/delivery_history.ts +++ b/loopx/control_plane/work_items/delivery_history.ts @@ -1,6 +1,7 @@ import type { JsonObject } from "../effect_program.ts"; import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { requireJsonObject } from "../runtime_decode.ts"; +import { resumeConditionHasKnownPendingTarget } from "../todos/resume_condition.ts"; import { DELIVERY_OUTCOMES, diagnoseDeliveryClaim, isTurnScopedSettlementOutcome, type DeliveryOutcome } from "./delivery_outcome.ts"; const TURN_KINDS = [ @@ -107,6 +108,42 @@ function prefixLength(items: readonly T[], matches: (item: T) => boolean): nu return boundary < 0 ? items.length : boundary; } +/** Historical supervision may defer to a positively established current wait, + * never to a prose blocker, missing source row, or another actor's work. + * This read decision does not settle work or choose an alternative Todo. */ +export function projectDeliveryResponse(value: unknown): JsonObject { + const input = requireJsonObject(value, "delivery response"); + const run = decodeRun(input.run); + const signal = (projectDeliveryHistory({ schema_version: "delivery_history_request_v0", + runs: [input.run], outcome_floor_configured: true }).runs as DeliverySignal[])[0]; + const todo = input.todo === null ? null : requireJsonObject(input.todo, "bound Todo"); + const runAgent = typeof input.run_agent_id === "string" ? input.run_agent_id.trim() : ""; + const agentId = typeof input.agent_id === "string" ? input.agent_id.trim() : runAgent; + const owner = typeof todo?.claimed_by === "string" ? todo.claimed_by.trim() : ""; + const excluded = Array.isArray(todo?.excluded_agents) ? todo.excluded_agents : []; + const condition = todo?.resume_condition && typeof todo.resume_condition === "object" + && !Array.isArray(todo.resume_condition) ? todo.resume_condition as JsonObject : null; + const boundBlocker = Boolean(run.todo_id) && !run.replan_obligation_id + && run.delivery_outcome.trim() === "outcome_gap" + && !signal.delivery_claim_conflicts + && isTurnScopedSettlementOutcome(run.delivery_outcome, run.progress_observation, run.todo_id.trim()); + const canonicalWait = boundBlocker && todo?.todo_id === run.todo_id.trim() + && todo.role === "agent" && todo.task_class === "advancement_task" + && ["open", "deferred"].includes(String(todo.status)) + && (todo.archive_state === undefined || todo.archive_state === "active") + && agentId === runAgent && (!owner || owner === agentId) && !excluded.includes(agentId) + && condition?.schema_version === "todo_resume_condition_v0" + && condition.resume_when === todo.resume_when && Boolean(todo.resume_when) + && condition.satisfied === false && todo.resume_ready !== true + && resumeConditionHasKnownPendingTarget(condition); + return { + schema_version: "delivery_response_v0", + outcome_floor_applicable: !canonicalWait, + outcome_followthrough: canonicalWait ? null : signal.outcome_followthrough, + reason: canonicalWait ? "canonical_todo_wait" : "history_supervision", + }; +} + /** One pure batch projection. History selection/order remains the caller's job; * this read model grants neither progress nor a durable settlement receipt. */ export function projectDeliveryHistory(value: unknown): JsonObject { diff --git a/loopx/control_plane/work_items/work_lane_context.py b/loopx/control_plane/work_items/work_lane_context.py index d562826cea..94aabf3bd0 100644 --- a/loopx/control_plane/work_items/work_lane_context.py +++ b/loopx/control_plane/work_items/work_lane_context.py @@ -14,7 +14,7 @@ todo_summary_monitor_schedule_gap_items, todo_summary_open_task_counts, ) -from .delivery_history import project_delivery_history +from .delivery_history import project_delivery_response from .work_lane import ( build_work_lane_contract, due_monitor_preempts_advancement, @@ -72,9 +72,11 @@ def post_handoff_latest_run(item: dict[str, Any]) -> dict[str, Any]: return latest_run -def outcome_followthrough_hint(item: dict[str, Any]) -> dict[str, Any] | None: +def outcome_followthrough_hint( + item: dict[str, Any], summary: dict[str, Any] | None = None, +) -> dict[str, Any] | None: run = post_handoff_latest_run(item) - return project_delivery_history([run])["runs"][0]["outcome_followthrough"] if run else None + return project_delivery_response(run, summary)["outcome_followthrough"] if run else None def next_action_requires_advancement(item: dict[str, Any]) -> bool: @@ -149,7 +151,7 @@ def build_work_lane_context_contract( first_advancement=first_advancement, ), outcome_followthrough=( - outcome_followthrough_hint(item) if advancement_allowed else None + outcome_followthrough_hint(item, agent_todo_summary) if advancement_allowed else None ), next_action_requires_advancement=( next_action_requires_advancement(item) if advancement_allowed else False diff --git a/loopx/quota.py b/loopx/quota.py index 91d39a9789..e84daed8d8 100644 --- a/loopx/quota.py +++ b/loopx/quota.py @@ -247,6 +247,13 @@ def quota_with_handoff_outcome_floor( threshold = outcome_floor_threshold(profile) if outcome_gap_streak < threshold: return quota + from .control_plane.work_items.delivery_history import project_delivery_response + + latest = handoff_readiness.get("post_handoff_latest_run") + if isinstance(latest, dict) and not project_delivery_response( + latest, project_asset.get("agent_todos") if isinstance(project_asset, dict) else None, + )["outcome_floor_applicable"]: + return quota state = str(quota.get("state") or "eligible") if state in {"blocked_health", "operator_gate", "waiting", "paused", "throttled"}: return quota diff --git a/loopx/status.py b/loopx/status.py index b789faa0a9..5f605fcecf 100644 --- a/loopx/status.py +++ b/loopx/status.py @@ -88,7 +88,7 @@ from .control_plane.work_items.backlog_hygiene import ( MAX_BACKLOG_HYGIENE_EVIDENCE_ITEMS as _MAX_BACKLOG_HYGIENE_EVIDENCE_ITEMS_READ_MODEL, ) -from .control_plane.work_items.delivery_history import project_delivery_history +from .control_plane.work_items.delivery_history import compact_delivery_binding, project_delivery_history from .control_plane.runtime.run_compaction import ( RUN_BASE_COMPACT_FIELDS, attach_run_summary_projections as _attach_run_summary_projections_read_model, @@ -588,6 +588,8 @@ def project_post_handoff_history( compact.update({field: signal[field] for field in ("delivery_batch_scale", "delivery_turn_kind")}) if signal.get("delivery_claim_conflicts"): compact["delivery_claim_conflicts"] = signal["delivery_claim_conflicts"] + if signal["delivery_turn_kind"] == "blocker_writeback": + compact.update(compact_delivery_binding(run)) if signal["delivery_outcome"] != "not_configured": compact["delivery_outcome"] = signal["delivery_outcome"] compact_runs.append(_attach_run_summary_projections_read_model( diff --git a/tests/control_plane/test_delivery_response.py b/tests/control_plane/test_delivery_response.py new file mode 100644 index 0000000000..9587459b9d --- /dev/null +++ b/tests/control_plane/test_delivery_response.py @@ -0,0 +1,80 @@ +"""Current canonical waits constrain historical supervision, not vice versa.""" +from copy import deepcopy + +from loopx.control_plane.handoff.delivery_contract import handoff_delivery_contract +from loopx.control_plane.testing.quota_fixtures import quota_status_payload, quota_todo_item, quota_todo_summary +from loopx.control_plane.work_items.delivery_history import project_delivery_response +from loopx.control_plane.work_items.work_lane_context import build_work_lane_context_contract +from loopx.quota import build_quota_should_run, quota_with_handoff_outcome_floor +from loopx.status import project_post_handoff_history + + +PROFILE = {"outcome_floor": {"outcome_markers": ["outcome"], "surface_only_hints": ["surface"]}} + + +def blocked_run(): + return {"agent_id": "agent-a", "todo_id": "todo_delivery", "delivery_outcome": "outcome_gap", + "delivery_batch_scale": "implementation", "progress_observation": { + "schema_version": "typed_progress_observation_v0", "result_class": "blocked", + "work_item_id": "todo_delivery", "blocker_id": "blocker_dependency", "evidence_ids": ["evidence_dependency"]}} + + +def waiting_todo(): + return quota_todo_item(todo_id="todo_delivery", title="Wait for the dependency", status="deferred", + claimed_by="agent-a", resume_when="todo_done:todo_dependency", resume_ready=False, + resume_condition={"schema_version": "todo_resume_condition_v0", "resume_when": "todo_done:todo_dependency", + "satisfied": False, "kind": "todo_done", "target_status": "open", + "target_task_class": "advancement_task", "target_archive_state": "active"}) + + +def dependency_todo(): + return quota_todo_item(todo_id="todo_dependency", title="Complete prerequisite", claimed_by="agent-b") + + +def test_status_compaction_preserves_binding_and_all_consumers_defer_to_current_wait(): + readiness = project_post_handoff_history([blocked_run()] * 3, PROFILE) + summary = quota_todo_summary([waiting_todo(), dependency_todo()], claim_scope_agent_id="agent-a") + asset = {"execution_profile": PROFILE, "agent_todos": summary} + item = {"handoff_readiness": readiness, "agent_todos": summary, "project_asset": asset} + assert project_delivery_response(readiness["post_handoff_latest_run"], summary)["reason"] == "canonical_todo_wait" + quota = {"state": "eligible", "reason": "fixture"} + assert quota_with_handoff_outcome_floor(quota, waiting_on="codex", project_asset=asset, + handoff_readiness=readiness) == quota + assert handoff_delivery_contract(item) is None + for state in ("paused", "operator_gate", "blocked_health", "throttled", "waiting"): + hard = {"state": state, "reason": "existing gate"} + assert quota_with_handoff_outcome_floor(hard, waiting_on="codex", project_asset=asset, + handoff_readiness=readiness) == hard + small = {**item, "handoff_readiness": {**readiness, "post_handoff_small_scale_streak": 3}} + assert handoff_delivery_contract(small)["mode"] == "expand_after_repeated_small_delivery" + extra = quota_todo_item(todo_id="todo_alternative", title="Implement independent work", claimed_by="agent-a") + with_work = quota_todo_summary([waiting_todo(), dependency_todo(), extra], claim_scope_agent_id="agent-a") + lane = build_work_lane_context_contract(item, agent_todo_summary=with_work) + assert lane["must_attempt_work"] is True + assert lane["obligation"] == "advance_one_bounded_segment" + assert "outcome_followthrough" not in lane + + +def test_surface_supervision_remains_and_invalid_wait_does_not_clear_floor(): + summary = quota_todo_summary([waiting_todo()], claim_scope_agent_id="agent-a") + asset = {"execution_profile": PROFILE, "agent_todos": summary} + for run in ({"delivery_outcome": "surface_only"}, {"delivery_outcome": "outcome_gap"}): + readiness = project_post_handoff_history([run] * 3, PROFILE) + assert quota_with_handoff_outcome_floor({"state": "eligible"}, waiting_on="codex", + project_asset=asset, handoff_readiness=readiness)["state"] == "focus_wait" + assert handoff_delivery_contract({"handoff_readiness": readiness, "project_asset": asset}) is not None + + +def test_unknown_refreshes_do_not_close_canonical_work_or_mutate_sources(): + rows = [quota_todo_item(todo_id="todo_active", title="Implement remaining work", claimed_by="agent-a")] + payload = quota_status_payload(goal_id="delivery-response", status="ready", agent_todo_items=rows, + claim_scope_agent_id="agent-a", recommended_action="Advance remaining work", latest_runs=[], + coordination={"registered_agents": ["agent-a"]}) + before = deepcopy(payload) + baseline = build_quota_should_run(payload, goal_id="delivery-response", agent_id="agent-a") + payload["run_history"]["goals"][0]["latest_runs"] = [ + {"agent_id": "agent-a", "classification": "state_refreshed"} for _ in range(50)] + actual = build_quota_should_run(payload, goal_id="delivery-response", agent_id="agent-a") + assert actual["work_lane_contract"]["must_attempt_work"] is True + assert actual["work_lane_contract"] == baseline["work_lane_contract"] + assert payload["attention_queue"] == before["attention_queue"] diff --git a/tests/control_plane_ts/delivery_response.test.ts b/tests/control_plane_ts/delivery_response.test.ts new file mode 100644 index 0000000000..37397c17e5 --- /dev/null +++ b/tests/control_plane_ts/delivery_response.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { projectDeliveryResponse } from "../../loopx/control_plane/work_items/delivery_history.ts"; +import { evaluateTodoResumeConditions, TODO_RESUME_EVALUATION_REQUEST_SCHEMA_VERSION } from "../../loopx/control_plane/todos/resume_condition.ts"; +import type { JsonObject } from "../../loopx/control_plane/effect_program.ts"; + +const run = { delivery_outcome: "outcome_gap", delivery_batch_scale: "implementation", + delivery_turn_kind: "", todo_id: "todo_delivery", replan_obligation_id: "", + outcome_followthrough_required: true, + progress_observation: { schema_version: "typed_progress_observation_v0", result_class: "blocked", + work_item_id: "todo_delivery", blocker_id: "blocker_dependency", evidence_ids: ["evidence_dependency"] } }; +const waiting = { todo_id: "todo_delivery", role: "agent", status: "deferred", + task_class: "advancement_task", claimed_by: "agent-a", resume_when: "todo_done:todo_dependency", + resume_ready: false, resume_condition: { schema_version: "todo_resume_condition_v0", + resume_when: "todo_done:todo_dependency", satisfied: false, kind: "todo_done", + target_status: "open", target_task_class: "advancement_task", target_archive_state: "active" } }; +const input = { run, todo: waiting, run_agent_id: "agent-a", agent_id: "agent-a" }; + +test("wait proof consumes the real resume evaluator for all four condition kinds", () => { + const dependency = { todo_id: "todo_dependency", role: "agent", status: "open", task_class: "advancement_task" }; + for (const [resume, source, capabilities, valid] of [ + ["todo_done:todo_dependency", [dependency], [], true], + ["todo_done:todo_dependency", [], [], false], + ["monitor_changed:todo_dependency", [{ ...dependency, task_class: "continuous_monitor", material_change_generation: 0 }], [], true], + ["monitor_changed:todo_dependency", [], [], false], + ["capacity_available:network", [], [], true], + ["capacity_available:network", [], null, false], + ["pr_merged:example/project#1", [], [], true], + ["pr_merged:#1", [], [], false], + ] as const) { + const todo = { ...waiting, resume_when: resume, resume_monitor_generation: 0 }; + const evaluated = evaluateTodoResumeConditions({ schema_version: TODO_RESUME_EVALUATION_REQUEST_SCHEMA_VERSION, + items: [todo], source_items: source, rollout_events: [], available_capabilities: capabilities }); + const condition = (evaluated.conditions as JsonObject[])[0].condition; + assert.equal(projectDeliveryResponse({ ...input, todo: { ...todo, resume_condition: condition } }).outcome_floor_applicable, + !valid, resume + JSON.stringify(source)); + } +}); + +test("a bound blocked observation delegates a current legal wait to canonical planning", () => { + const before = structuredClone(input); + const result = projectDeliveryResponse(input); + assert.equal(result.outcome_floor_applicable, false); + assert.equal(result.outcome_followthrough, null); + assert.equal(result.reason, "canonical_todo_wait"); + assert.deepEqual(input, before); +}); + +test("history alone, missing source, invalid wait, and other actors cannot exempt the floor", () => { + for (const patch of [ + { todo: null }, { agent_id: "agent-b" }, { run_agent_id: "agent-b" }, + { todo: { ...waiting, todo_id: "todo_other" } }, + { todo: { ...waiting, claimed_by: "agent-b" } }, + { todo: { ...waiting, excluded_agents: ["agent-a"] } }, + { todo: { ...waiting, status: "done" } }, + { todo: { ...waiting, resume_ready: true } }, + { todo: { ...waiting, resume_condition: null } }, + { todo: { ...waiting, resume_condition: { ...waiting.resume_condition, target_status: null } } }, + { todo: { ...waiting, resume_condition: { ...waiting.resume_condition, target_task_class: "continuous_monitor" } } }, + { todo: { ...waiting, resume_condition: { ...waiting.resume_condition, invalid_state: "target_missing" } } }, + { todo: { ...waiting, resume_condition: { ...waiting.resume_condition, satisfied: true } } }, + { run: { ...run, progress_observation: null, delivery_turn_kind: "blocker_writeback" } }, + { run: { ...run, replan_obligation_id: "replan_other" } }, + { run: { ...run, progress_observation: { ...run.progress_observation, evidence_ids: [] } } }, + ]) assert.equal(projectDeliveryResponse({ ...input, ...patch }).outcome_floor_applicable, true, JSON.stringify(patch)); +}); + +test("surface-only supervision and unknown remain distinct from durable work state", () => { + const surface = projectDeliveryResponse({ ...input, run: { ...run, delivery_outcome: "surface_only" } }); + assert.equal(surface.outcome_floor_applicable, true); + assert.ok(surface.outcome_followthrough); + const unknown = projectDeliveryResponse({ ...input, run: { ...run, delivery_outcome: "unknown", + outcome_followthrough_required: false, progress_observation: null } }); + assert.equal(unknown.outcome_floor_applicable, true); + assert.equal(unknown.outcome_followthrough, null); + assert.equal("resolved_todo_id" in unknown, false); +}); From 61a3e39320dea5918ca3999d4972e2a704cf90f7 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Wed, 9 Sep 2026 14:11:57 +0800 Subject: [PATCH 2/5] fix(delivery): reject self-dependent historical wait exemptions Signed-off-by: huangruiteng --- loopx/control_plane/work_items/delivery_history.py | 2 +- loopx/control_plane/work_items/delivery_history.ts | 1 + tests/control_plane_ts/delivery_response.test.ts | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/loopx/control_plane/work_items/delivery_history.py b/loopx/control_plane/work_items/delivery_history.py index b667af3e2e..ebd7c61e63 100644 --- a/loopx/control_plane/work_items/delivery_history.py +++ b/loopx/control_plane/work_items/delivery_history.py @@ -99,7 +99,7 @@ def project_delivery_response( condition = todo["resume_condition"] todo["resume_condition"] = {key: condition[key] for key in ( "schema_version", "resume_when", "satisfied", "invalid_target", "invalid_state", - "kind", "target_status", "target_task_class", "target_archive_state", + "kind", "target_todo_id", "target_status", "target_task_class", "target_archive_state", "baseline_generation", "material_change_generation", "provider_required", "pr_repo", "repository_binding_state", ) if key in condition} result = effect_runtime_result("work_item.delivery_response.project", { diff --git a/loopx/control_plane/work_items/delivery_history.ts b/loopx/control_plane/work_items/delivery_history.ts index dfabd29c98..6700e70ea0 100644 --- a/loopx/control_plane/work_items/delivery_history.ts +++ b/loopx/control_plane/work_items/delivery_history.ts @@ -134,6 +134,7 @@ export function projectDeliveryResponse(value: unknown): JsonObject { && agentId === runAgent && (!owner || owner === agentId) && !excluded.includes(agentId) && condition?.schema_version === "todo_resume_condition_v0" && condition.resume_when === todo.resume_when && Boolean(todo.resume_when) + && condition.target_todo_id !== todo.todo_id && condition.satisfied === false && todo.resume_ready !== true && resumeConditionHasKnownPendingTarget(condition); return { diff --git a/tests/control_plane_ts/delivery_response.test.ts b/tests/control_plane_ts/delivery_response.test.ts index 37397c17e5..83a06cb3e0 100644 --- a/tests/control_plane_ts/delivery_response.test.ts +++ b/tests/control_plane_ts/delivery_response.test.ts @@ -55,6 +55,8 @@ test("history alone, missing source, invalid wait, and other actors cannot exemp { todo: { ...waiting, status: "done" } }, { todo: { ...waiting, resume_ready: true } }, { todo: { ...waiting, resume_condition: null } }, + { todo: { ...waiting, resume_when: "todo_done:todo_delivery", resume_condition: { + ...waiting.resume_condition, resume_when: "todo_done:todo_delivery", target_todo_id: "todo_delivery" } } }, { todo: { ...waiting, resume_condition: { ...waiting.resume_condition, target_status: null } } }, { todo: { ...waiting, resume_condition: { ...waiting.resume_condition, target_task_class: "continuous_monitor" } } }, { todo: { ...waiting, resume_condition: { ...waiting.resume_condition, invalid_state: "target_missing" } } }, From 1046ea4dd1eab6da945123068254ec0c7d1fe97a Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Wed, 9 Sep 2026 14:12:25 +0800 Subject: [PATCH 3/5] docs(rfcs): define executable convergence and retirement stages Signed-off-by: huangruiteng --- ...shared-goal-authority-state-provider-v0.md | 159 +++++++++++------- ...-goal-authority-state-provider-v0.zh-CN.md | 121 ++++++++----- .../typescript-control-plane-migration-v0.md | 153 ++++++++++++----- ...script-control-plane-migration-v0.zh-CN.md | 113 +++++++++---- 4 files changed, 365 insertions(+), 181 deletions(-) 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 e688527089..3783d7675d 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -2522,69 +2522,100 @@ one-way projections. Do not add a third TS-Markdown backend, bidirectional live synchronization, or per-command split authority. Unsupported post-cutover commands fail closed; they do not fall back to the old writer. -The next complete stage packages are: - -1. **Command/consumer closure.** Reuse the merged create, claim, update and - #4053 terminal/successor/archive paths. Inventory remaining public mutations - and reads against actual callers. Status/attention now joins `todo list` in - reading canonical Todo summaries after promotion, without requiring the - Markdown file. Missing providers fail closed and empty canonical collections - never revive legacy Todos. Refresh recommendation, repair/replan qualification, - completion-validation accountability, Todo-add replan binding and guided-start - frontier now share that canonical source. A refresh reads one snapshot and - passes it through its decisions rather than rereading a changing provider or - Markdown at each gate. Provider failure aborts; an empty snapshot is not a - fallback signal. This is consumer progress, not promotion proof: Turn/quota, - standing decisions, leases, monitor writeback, shared-goal alignment and - amendment revision bases still need their own parity inventory. Read authority - does not grant writeback. Source/display independence is tested with the - shared production-scale fixture and real FileAuthorityStore; these reads do - not establish freshness/CAS for a later business commit or change provider - defaults. Next Action narrative remains independent of Todo authority. - - Lifecycle admission and the preauthorized terminal fence now share the TS - owner across legacy writers and native terminal transactions; the replaced - Python rules are removed without changing provider defaults or promotion. - This is not full native field-edit support: retain the strict text/note - transaction boundary until update's fields, ownership, validation and - monitor/resume effects close together. Neither an admission result nor a - lease-fence result is a commit receipt. Keep provider CAS/replay and existing - writer lock lifetimes unchanged while collecting this deletion payoff. - Waiting/resume lane selection is now one TS read-policy owner shared by quota, - vision-wait, agent-scope and replan. The obsolete Python selector module is - deleted; the adapter accepts the same canonical summary after promotion and - legacy summary before it. Real CLI coverage includes capacity changes and - missing promoted display without writing it. This does not close all quota - source paths, authorize monitor writeback, or change provider/promotion holds. -2. **Permanent projection closure.** Reuse `provider_projection.py`, the - Todo-section renderer and existing journal/outbox. Preserve non-owned human - narrative; render owned sections from a known canonical revision, with - idempotent repair and freshness/readback evidence. Pending projection delivery - is independent of business commit/replay. Direct Markdown edits must never - import themselves into authority. Explicit validated edit/import tooling is - a separate proposal, not a second writer hidden inside rendering. Missing - displays now automatically recover Todo-only sections from canonical state - during normal projection delivery - ([#4097](https://github.com/huangruiteng/loopx/pull/4097)); recovery reports - `recovery_scope=todo_sections_only` and does not restore lost Goal narrative. - The [active-state projection contract](../../reference/protocols/active-state-structured-projection-v0.md) - defines the shipped recovery boundary. Validate stale/missing/malformed - display, crash/retry, revision races, narrative preservation and private-field - boundaries. -3. **One qualified local profile and fenced cutover.** Section 7.2's embedded - candidate must prove bounded head/index growth, historical receipts, crash - recovery, real CLI readback, capacity and >=10-day soak. File-v0 conformance - is not that evidence. Bind one exact lineage/revision/manifest, drain capture, - reconcile consumers, fence writers and verify projection recovery plus fenced - export/rollback before explicit promotion approval. Do not promote active - goals for development tests. PostgreSQL deployment and NoKV qualification - proceed independently; changed shared transactions still qualify each - affected provider, including a real isolated PostgreSQL server. -4. **Retirement with named callers.** Remove old Markdown business writers and - capture/reference/bridge code only when their final callers and migration - windows close. Keep the permanent renderer, qualified import/export, and - durable regression coverage. Publish the retained-seam inventory and next - deletion condition, rather than indefinitely expanding dual paths. +#### Durability execution cards + +Use the [TS execution cards](typescript-control-plane-migration-v0.md#execution-cards-after-the-current-stack) +for command inventory, update/monitor transactions and consumer deletion. Do not +repeat that plan in a second implementation or treat a merged read-policy PR +as storage readiness. Its T0 checkpoint is the entry condition for these cards. + +Lifecycle admission and the preauthorized terminal fence now share the TS +owner across legacy writers and native terminal transactions; the replaced +Python rules are removed without changing provider defaults or promotion. +This is not full native field-edit support: retain the strict text/note +transaction boundary until update's fields, ownership, validation and +monitor/resume effects close together. Neither an admission result nor a +lease-fence result is a commit receipt. Keep provider CAS/replay and existing +writer lock lifetimes unchanged while collecting this deletion payoff. +Waiting/resume lane selection is now one TS read-policy owner shared by quota, +vision-wait, agent-scope and replan. The obsolete Python selector module is +deleted; the adapter accepts the same canonical summary after promotion and +legacy summary before it. Real CLI coverage includes capacity changes and +missing promoted display without writing it. This does not close all quota +source paths, authorize monitor writeback, or change provider/promotion holds. + +**D1 — qualify permanent projection delivery; may overlap T1/T2.** + +- Start from `loopx/control_plane/todos/provider_projection.py`, the existing + Todo-section renderer and canonical journal/outbox. #4097 already recovers + missing Todo sections with `recovery_scope=todo_sections_only`; reuse it. + It cannot recover lost independent Goal narrative. +- Evaluate #4101's receipt-retention candidate against its actual merged head; + do not replace it with another delivery queue. A committed business result + and pending projection delivery must remain separately observable. +- Prove crash/retry, concurrent revisions, absent/stale/malformed display, + receipt lifetime until delivery, non-owned narrative preservation and + private-field boundaries. Recovery must not rerun the business operation. +- Delete obsolete projection repair/receipt paths only after their callers + move. Exit with deterministic freshness/readback and an actionable repair + path; a successful render once is insufficient. + +**D2 — qualify exactly one local profile; independent of PostgreSQL deployment.** + +- Reconcile the SQLite candidate #4121 with Section 7.2 before adding code. + Keep it opt-in until qualified and approved. If it does not meet the contract, + record the concrete gap rather than building a second store or changing + defaults. Keep File/NoKV as existing conformance references. +- On a disposable real backend, prove atomic head/event/receipt commit, + concurrent access, historical cursor/replay compatibility, bounded live-head/ + index growth, crash/restore recovery and public CLI readback using the shared + production-scale fixture and accelerated growth cases. +- Separately obtain the existing >=10-day synthetic-goal soak evidence. + Accelerated volume is not elapsed time; code may merge with promotion held. + Starting a scheduled soak or touching live Goals requires separate authority. +- Exit with one exact provider/profile revision and a qualification ledger + naming passed, failed and missing evidence. NoKV and PostgreSQL retain their + own qualification; a pass on SQLite cannot waive another affected provider. + +**D3 — integrate and request a whole-Goal cutover.** + +- Requires T1–T3, D1/D2 and qualified capture; production cutover additionally + requires explicit approval. Transaction- + bound capture from merged #3870 is the starting point, not a new subsystem. + Audit sustained mixed-writer and event-only coverage against the final command + matrix; unresolved rows block promotion. +- Bind one lineage, source revision, field manifest, digest and cursor; drain + capture, fence old writers and read back canonical state plus projection. + Rehearse legacy/canonical/selected-provider parity on disposable inputs, + including empty state, ordering, archived records, leases and receipts. +- Demonstrate fenced export/rollback. Never simply disable a fence and revive + an older Markdown snapshot after canonical writes. A failed cutover leaves + one known authority or an explicit blocked state, never two writable stores. +- Promote only the approved profile/cohort. Keep unsupported commands fail- + closed. After the declared migration window and final legacy caller close, + hand off exact retired paths to T4; keep renderer and validated import/export. + +#### Execution handoff and integration order + +| Ready condition | Next action | What it does not authorize | +| --- | --- | --- | +| Current refactor stack is reconciled | T1; D1 and D2 may proceed independently | Default provider changes or another generic migration framework | +| T1 closes field semantics | T2; close T3 consumers as their contracts become available | Per-command split authority within one Goal | +| T1–T3 and D1/D2 plus capture qualify | D3 rehearsal, then explicit promotion request | Skipping soak, bypassing failed evidence, or production promotion by the agent | +| Approved cutover and legacy window finish | T4 full-writer retirement | Deleting permanent Markdown presentation or historical receipts still needed for replay | + +Expect roughly **five to seven cohesive implementation/qualification batches** +after reconciling the current stack, not a fixed PR quota: T1, T2, T3, D1, D2, +D3 and T4 can share a PR only when their dependencies, review and rollback +remain clear. Semantic deletion starts in T1; full legacy-writer deletion waits +for D3/T4. Elapsed-time soak is separate and is not shortened by splitting PRs. + +For each handoff, record the exact base/head, selected card, actual callers +removed, changed authority/observable semantics, real-backend results, remaining +holds and one next executable action. If an earlier stage already landed, +verify its evidence and skip its implementation; if prerequisites fail, stop +that dependent stage. Do not turn hypothetical post-merge readiness into an +automatic promotion, automation, merge or release permission. The current default and Appendix C promotion holds remain unchanged. This plan does not declare the whole Todo family, long-goal profile, or shared deployment @@ -2597,6 +2628,6 @@ second Todo state machine. | --- | --- | --- | --- | | L. Long-goal local persistence | Now, alongside the Todo caller | Section 7.2: embedded-store candidate, bounded live head and receipt index, historical scan compatibility, crash-safe checkpoints, real CLI readback, accelerated capacity and >=10-day soak. | Reuses the TS authority owner; required for local long-goal promotion, independent of P. | | P. PostgreSQL provider plane | Now, from current `main` | Keep the existing `AuthorityStore` contract; finish schema migration/install ownership, authenticated service and tenant authorization, restore-incarnation rotation, pool/cancellation/failover behavior, and reviewed indexes, partitioning, retention, and measured capacity. Live PostgreSQL conformance remains mandatory. | Does not depend on #3870 and must not stack on its branch. This lane alone creates no runtime caller or promotion claim. | -| C. Canonical transaction capture | In implementation, based on #3870 | Transaction-bound outbox capture now targets the one `coordination.runtime_shadow` lineage and retains complete versioned Todo/lease records. Finish sustained mixed-writer parity, explicit-clear/omission coverage, and event-only Todo recovery evidence. | Can run in parallel with P, but both C and the selected provider profile must finish before parity or promotion integration. | +| C. Canonical transaction capture | Qualify the implementation merged in #3870 | Transaction-bound outbox capture targets the one `coordination.runtime_shadow` lineage and retains complete versioned Todo/lease records. Finish sustained mixed-writer parity, explicit-clear/omission coverage, and event-only Todo recovery evidence. | Can run in parallel with P, but both C and the selected provider profile must finish before parity or promotion integration. | | I. Binding and qualification integration | After C and the selected profile's qualification | Bind one exact provider lineage, field manifest, source revision, digest, and cursor; qualify explicit v0 import, ordering/archival/consumer parity, and recovery/capacity without consulting legacy state for missing fields. | Long-goal local integration requires L and does not wait for P. PostgreSQL joins only when its own P holds pass. | | F. Promotion and cleanup | After I and explicit maintainer approval | Complete provider-first CLI routing, the lock-owning promotion orchestrator, compatibility projection outbox, post-promotion fenced export/rollback, then delete duplicate reference aggregates and flip the reviewed stage/hold declarations. | Each profile must pass C, I, and its own provider qualification; long-goal local promotion additionally requires L, and PostgreSQL requires P. | 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 ba232593f0..a16fad1ed0 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 @@ -2000,50 +2000,81 @@ CLI / Agent / Dashboard → 唯一 TS Todo 事务 owner → canonical authority 供数;cutover 后,所选 canonical provider 向单向投影供数。不增加第三种 TS-Markdown backend、实时双向同步或按命令拆开的权威;晋升后不支持的命令 fail closed,不能回退旧 writer。 -后续完整阶段包按以下顺序推进: - -1. **命令/consumer 闭合。** 复用已合入的 create、claim、update 和 #4053 - terminal/successor/archive 路径。按实际 caller 盘点剩余公开 mutation 和 read。 - status/attention 现在与 `todo list` 一样,在 promotion 后读 canonical Todo summary, - 不要求 Markdown 文件存在;provider 缺失 fail closed,canonical 空集合不能复活旧 - Todo。Refresh 推荐、repair/replan 验收、completion-validation 问责、Todo-add replan - 绑定和 guided-start frontier 现已复用该 canonical 来源。一次 refresh 读取一份快照, - 传给各项决策,不在每个门禁重新读取变化中的 provider 或 Markdown;provider 故障 - 直接中止,空快照不是 fallback 信号。这是 consumer 进展,不是 promotion 证明: - Turn/quota、standing decision、lease、monitor writeback、shared-goal alignment 与 - amendment revision basis 仍需各自 parity 清单。读权威不授予写回能力。共用的复杂 - fixture 和真实 FileAuthorityStore 验证 source/display 独立性,但不证明后续业务 - commit 的 freshness/CAS,也不改变 provider 默认值。Next Action 正文仍独立于 Todo 权威。 - - Lifecycle 准入及预授权 terminal fence 现由 legacy writer 与 native terminal - transaction 共用 TS owner;删除对应 Python 规则,不改变 provider 默认或 promotion。 - 这不是完整 native 字段编辑:在 update 的字段、ownership、validation 和 monitor/resume - effect 一起闭合前,保留严格 text/note 事务边界。准入结果和 lease-fence 结果都不是 - commit receipt;兑现删除收益时,provider CAS/replay 与既有 writer 持锁生命周期不变。 - 等待/恢复 lane 选择现由 quota、vision-wait、agent-scope、replan 共用一个 TS 读取 - 策略 owner,删除旧 Python selector 模块。适配层在 promotion 后消费同一 canonical - summary,之前消费 legacy summary;真实 CLI 覆盖容量变化和 promoted display - 缺失且不写回的场景。这不代表所有 quota source 路径已闭合,不授予 monitor 写回 - 权限,也不改变 provider 默认与 promotion hold。 -2. **永久投影闭合。** 复用 `provider_projection.py`、Todo-section renderer 和既有 - journal/outbox。保留非托管的人工叙述,从已知 canonical revision 渲染托管 section, - 提供幂等修复与 freshness/readback 证据。投影 pending 独立于业务 commit/replay。 - 直接编辑 Markdown 不能自动导入 authority;显式验证的 edit/import 工具另提方案, - 不能藏在 renderer 内成为第二个 writer。缺失 display 现已在正常投影交付时自动从 - canonical state 恢复 Todo-only section([#4097](https://github.com/huangruiteng/loopx/pull/4097)); - 恢复结果声明 `recovery_scope=todo_sections_only`,不恢复丢失的完整 Goal 叙述。 - 已交付边界以 [active-state projection contract](../../reference/protocols/active-state-structured-projection-v0.md) - 为准。验证陈旧/缺失/非法展示、crash/retry、revision race、叙述保留与私有字段边界。 -3. **一个已资格化的本地 profile 与 fenced cutover。** 第 7.2 节嵌入式候选证明 - head/index 增长有界、历史 receipt、crash recovery、真实 CLI readback、容量和 - >=10 天 soak;file-v0 conformance 不能代替这些证据。绑定精确 lineage/revision/ - manifest、排空 capture、对齐 consumer、fence writer,并验证投影恢复及 fenced - export/rollback 后,才请求显式 promotion 批准。不能拿活跃 goal 做开发晋升测试。 - PostgreSQL 部署与 NoKV 资格化独立推进;shared transaction 改动仍验证每个受影响 - provider,包括真实隔离 PostgreSQL server。 -4. **列明 caller 后退役。** 最后 caller 和迁移窗口关闭后,删除旧 Markdown 业务 - writer 与 capture/reference/bridge;保留永久 renderer、已资格化 import/export 和 - 持久回归覆盖。公布保留 seam 清单及下一删除条件,不无限扩大双路径。 +#### 持久化执行卡 + +命令清单、update/monitor 事务和 consumer 删除统一按 +[TS 执行卡](typescript-control-plane-migration-v0.zh-CN.md#当前-stack-合入后的执行卡) +推进,不在这里复制第二套实现路线,也不把 read-policy PR 合并视为存储就绪。 +进入本节前先完成 T0 基线核对。 + +Lifecycle 准入及预授权 terminal fence 现由 legacy writer 与 native terminal +transaction 共用 TS owner;删除对应 Python 规则,不改变 provider 默认或 promotion。 +这不是完整 native 字段编辑:在 update 的字段、ownership、validation 和 monitor/resume +effect 一起闭合前,保留严格 text/note 事务边界。准入结果和 lease-fence 结果都不是 +commit receipt;兑现删除收益时,provider CAS/replay 与既有 writer 持锁生命周期不变。 +等待/恢复 lane 选择现由 quota、vision-wait、agent-scope、replan 共用一个 TS 读取 +策略 owner,删除旧 Python selector 模块。适配层在 promotion 后消费同一 canonical +summary,之前消费 legacy summary;真实 CLI 覆盖容量变化和 promoted display +缺失且不写回的场景。这不代表所有 quota source 路径已闭合,不授予 monitor 写回 +权限,也不改变 provider 默认与 promotion hold。 + +**D1 — 资格化永久投影交付,可与 T1/T2 重叠推进。** + +- 从 `loopx/control_plane/todos/provider_projection.py`、既有 Todo-section renderer、 + canonical journal/outbox 入手。复用 #4097 已有的缺失 Todo section 恢复及 + `recovery_scope=todo_sections_only`;它不能恢复丢失的独立 Goal 正文。 +- 按 #4101 实际合入 head 评估 receipt-retention 候选,不再建一条 delivery queue。 + 业务已提交与投影尚 pending 必须分别可观测。 +- 验证 crash/retry、并发 revision、缺失/陈旧/非法 display、交付前 receipt + 生命周期、非托管正文保留和私有字段边界;恢复不得重新执行业务操作。 +- caller 迁走后才删除旧 projection repair/receipt 路径。退出条件是可复核的 + freshness/readback 和可操作修复路径,不能只证明成功渲染过一次。 + +**D2 — 资格化一个本地 profile,不等待 PostgreSQL 部署。** + +- 写代码前对齐 #4121 SQLite 候选与第 7.2 节;资格化及批准前保持 opt-in。 + 不符合合同则记录具体缺口,不另建第二套 store 或修改默认值。 + File/NoKV 继续作为既有 conformance 参照。 +- 使用一次性真实 backend、共享复杂 fixture 和加速增长样例,验证 head/event/ + receipt 原子提交、并发、历史 cursor/replay、有界 live head/index、crash/restore + 和公开 CLI readback。 +- 另外取得既有 >=10 天 synthetic-goal soak 证据;加速数据量不能替代真实经过时间。 + 代码可以先合并,promotion 保持 hold。启动定时 soak 或操作活跃 Goal 另需授权。 +- 退出时指定唯一 provider/profile revision,列明 passed、failed、missing evidence。 + NoKV/PostgreSQL 各自保留资格要求,SQLite 通过不能豁免其他受影响 provider。 + +**D3 — 集成并申请整 Goal cutover。** + +- 前提为 T1–T3、D1/D2 和 capture 资格化;生产 cutover 另需显式批准。从已合并 #3870 的 transaction- + bound capture 出发,不重新建设;对照最终命令表验证 sustained mixed-writer 与 + event-only 覆盖,任何未闭合行都阻塞 promotion。 +- 绑定唯一 lineage、source revision、field manifest、digest、cursor,排空 capture, + fence 旧 writer,读回 canonical state 与投影。一次性输入上演练 legacy/canonical/ + 所选 provider parity,覆盖空集合、排序、archive、lease、receipt。 +- 证明 fenced export/rollback;canonical 写入后,不能简单关闭 fence 并复活旧 + Markdown 快照。失败时保留唯一已知 authority 或明确 blocked 状态,不能产生双写库。 +- 仅晋升批准的 profile/cohort;不支持的命令保持 fail closed。声明的迁移窗口结束、 + 最后 legacy caller 退出后,把精确退役路径交给 T4;永久 renderer 和合法 import/ + export 保留。 + +#### 执行交接与汇合顺序 + +| 就绪条件 | 下一动作 | 不授予的权限 | +| --- | --- | --- | +| 当前 refactor stack 已核对 | T1;D1、D2 可独立推进 | 修改默认 provider 或新增通用迁移框架 | +| T1 字段语义闭合 | T2;所需合同就绪后推进 T3 consumer | 同一 Goal 按命令拆分 authority | +| T1–T3、D1/D2 和 capture 均合格 | D3 演练,再请求 promotion 批准 | 跳过 soak、绕过失败证据或自行生产晋升 | +| 批准的 cutover 和 legacy 窗口结束 | T4 完整 writer 退役 | 删除永久 Markdown 展示或 replay 仍需的历史 receipt | + +核对当前 stack 后,预估还需**五到七个完整实现/资格化批次**,不是固定 PR 配额: +T1、T2、T3、D1、D2、D3、T4 仅在依赖、评审和回滚清晰时可同 PR 交付。 +T1 就开始删除重复语义;完整 legacy writer 删除等待 D3/T4。Soak 的真实经过时间 +独立计算,不能靠拆 PR 缩短。 + +每次交接记录精确 base/head、执行卡、实际删除的 caller、authority/可观察语义变化、 +真实 backend 结果、剩余 hold 和一个可执行的下一动作。前序已合入则验证证据后跳过 +重复实现;前提不满足就暂停依赖阶段。不能把“假设合并后”的就绪状态当成自动 +promotion、automation、merge 或 release 授权。 当前默认和附录 C promotion hold 均不改变。这份计划不宣称完整 Todo 命令族、长程 profile 或 shared deployment 已生产就绪。provider 负责 durable CAS/transaction, @@ -2055,6 +2086,6 @@ profile 或 shared deployment 已生产就绪。provider 负责 durable CAS/tran | --- | --- | --- | --- | | L. 长程本地持久化 | 现在,与 Todo caller 同期 | 第 7.2 节:嵌入式候选、有界 live head/receipt index、历史 scan 兼容、crash-safe checkpoint、真实 CLI readback、加速容量与 >=10 天 soak。 | 复用 TS authority owner;本地长程晋升必需,不依赖 P。 | | P. PostgreSQL provider plane | 现在,从当前 `main` 开始 | 保持既有 `AuthorityStore` 合同;完成 schema migration/install ownership、authenticated service 与 tenant authorization、restore-incarnation rotation、pool/cancellation/failover 行为,以及经评审的 index、partition、retention 与实测 capacity。真实 PostgreSQL conformance 始终是强制门禁。 | 不依赖 #3870,也不得叠在其分支上。仅完成本 lane 不产生 runtime caller 或 promotion 声明。 | -| C. Canonical transaction capture | 实现中,基于 #3870 | transaction-bound outbox 已指向唯一 `coordination.runtime_shadow` lineage,并保留完整带版本的 Todo/lease record;继续完成 sustained mixed-writer parity、explicit-clear/omission 与 event-only Todo recovery 证据。 | 可与 P 并行;但 C 与选定 provider profile 都完成后,才能进入 parity 或 promotion 集成。 | +| C. Canonical transaction capture | 资格化 #3870 已合入实现 | transaction-bound outbox 已指向唯一 `coordination.runtime_shadow` lineage,并保留完整带版本的 Todo/lease record;继续完成 sustained mixed-writer parity、explicit-clear/omission 与 event-only Todo recovery 证据。 | 可与 P 并行;但 C 与选定 provider profile 都完成后,才能进入 parity 或 promotion 集成。 | | I. Binding 与资格集成 | C 与选定 profile 的资格化完成后 | 绑定一个精确 provider lineage、field manifest、source revision、digest 与 cursor;资格化显式 v0 import、排序/归档/consumer parity 与 recovery/capacity;缺字段时不得查询 legacy state 补齐。 | 长程本地集成需要 L,不等待 P;PostgreSQL 仅在自己的 P hold 全通过后汇合。 | | F. Promotion 与清理 | I 完成且 maintainer 显式批准后 | 完成 provider-first CLI routing、持锁 promotion orchestrator、兼容投影 outbox、晋升后 fenced export/rollback;随后删除重复 reference aggregate,并翻转经评审的 stage/hold 声明。 | 每个 profile 必须通过 C、I 与自身 provider 资格化;长程本地晋升还需 L,PostgreSQL 还需 P。 | diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 37a28f34d0..bca479dfa4 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -307,49 +307,116 @@ rewrite or new writer admission is implied. General add/update admission and a generic repair action for every invalid condition remain separate scopes; this is not a claim of zero behavior change or full Todo writer closure. -1. **Close the actual command and consumer inventory.** Build on the merged - create/claim/update and #4053 terminal/successor/archive transactions; do not - recreate them. Inventory remaining field-edit, monitor, lease, and event - callers against their real contracts. Move their decisions to the existing - TS owner and delete the replaced decisions in the same slice. Reuse the - canonical Todo summary for reads: `todo list` and status/attention must not - select stale Markdown or event Todos after promotion, even when the display - is missing or the canonical collection is empty. Refresh now loads one - unbounded canonical Todo snapshot for recommendation, repair/replan - qualification and completion-validation accountability; Todo-add's replan - binding and guided-start's existing frontier use the same source adapter. - Their existing decision reducers remain owners: no second planning store or - permission rule is introduced. Legacy callers retain their parser contracts. - Audit Turn/quota, Dashboard, standing decisions, shared-goal alignment and - amendment revision bases separately; this closes the named planning callers, - not every consumer. Prove real-entrypoint parity and unavailable-provider - rejection, not just transport snapshots. Independently authored Next Action - remains narrative, not a Todo import. Missing display does not authorize - reconstructing narrative or weaken an accountable completion fence. -2. **Make the display a recoverable one-way projection.** Reuse the canonical - journal/outbox and Todo-section renderer. Keep human narrative, source - revision, idempotent delivery and actionable pending repair. A failed render - must not undo a committed mutation or authorize a Markdown fallback. - Projection recovery must not replay the business operation. No third - TS-Markdown backend or automatic bidirectional synchronization is needed. -3. **Qualify one local store, then cut over whole goals.** Close import, - ordering/consumer parity, writer fencing, capture/projection recovery, - historical receipts, capacity and >=10-day soak for the selected profile. - File-v0 conformance alone is not long-goal readiness. Do not route complete - to a provider while leaving update authoritative in Markdown for the same - goal. Until qualification and explicit promotion approval, keep the existing - default and fail-closed fences. Local qualification does not wait for the - PostgreSQL service; affected PostgreSQL transactions still require real - integration coverage. -4. **Collect deletion payoff at each closed boundary.** After the last caller - and explicit migration window end, remove old Markdown business writers, - capture-only glue, duplicate reference aggregates and redundant bridges. - Keep the Markdown renderer and qualified import/export tools. List the exact - remaining callers and exit condition for each retained seam; a full native - CLI rewrite must not become a blanket reason to retain duplicate semantics. - Report product LOC removed, bridge LOC added and crossings separately from - tests/generated contracts. Stop and replan after two scaffolding-only slices; - net-negative LOC is useful evidence, not a quota that excuses lost behavior. +#### Execution cards after the current stack + +This is a **conditional execution plan**, not a merged-status declaration. +At the 2026-09-09 checkpoint, #4053, #4117 and #4129 are merged; #4122 +(resume diagnosis/planning), #4134 (delivery history) and #4136 (claim diagnosis) +are open. The canonical delivery-response follow-up is stacked on #4136. +Check their actual merge commits before starting. #4121 (SQLite candidate) +and #4101 (projection receipt retention) are independent candidates, not +implicit prerequisites or approved defaults. + +Execute the first unclosed card below; do not start all cards or rebuild a +completed transaction. Keep the task ledger in LoopX state; this document is +the shared plan, not another per-agent checklist database. + +**T0 — reconcile the landed baseline, inside the next implementation PR.** + +- Fetch the intended remote base; record its SHA and each dependency's actual + merged/not-merged status. Compare code, not just PR titles. If a dependency + is open, use an explicitly selected stacked base or stop that dependent unit. +- Start from `coordination/todo_update.ts`, `todos/field_update.ts`, + `todos/provider_compatibility_edit.py`, `todos/line_update.py`, + `scheduler/monitor_poll_writeback.py` and their public callers. These paths + are under `loopx/control_plane/`. Re-resolve moved symbols instead of + restoring removed compatibility wrappers. +- Produce a compact caller matrix: public operation, authority source before/ + after promotion, TS owner, external effects, retained legacy caller, and + exact deletion condition. Update this section's completion facts with the + implementation; do not deliver an inventory-only framework PR. +- When #4122 and delivery response meet, reconcile pending/invalid condition + diagnosis in the existing resume owner. A missing target is not proof of a + valid wait; readable historical pending state is not permission to relax + supervision. Retire duplicate checks only after both contracts are tested. + +**T1 — close the public Todo update transaction.** + +- Reuse the current provider text/note transaction, lifecycle admission, + field-plan and completion rules. Enumerate actual public metadata edits and + explicit-clear behavior before implementation; this is not permission to + widen `UPDATE_FIELDS` to every stored field or admit terminal transitions + through a generic patch. +- Deliver one coarse typed transaction covering admitted intent, actor/claim/ + exclusion/lease checks, field semantics, final validation, CAS and replay. + Keep external execution/checkpointing outside pure reduction. Monitor effects + that cannot fit safely remain explicitly unsupported until T2; list them. +- Delete replaced Python update decisions and leaf-RPC orchestration in the + same PR. Keep the legacy codec/lock and compatibility writer while + unpromoted callers still need them; do not claim full writer deletion. +- Prove omission versus clear, unclaimed copy correction versus privileged + metadata, other-owner/lease rejection, no-op, invalid-input no-write, + competing revisions, retry and lost-response recovery through the public + command and affected real providers. + +**T2 — close monitor writeback and its atomic follow-up.** + +- Inventory `monitor_poll_writeback.py` and its event/Todo/lease callers. + Reuse existing monitor generation, independent-successor and settlement + owners. Compose one transaction rather than adding a second monitor engine. +- Preserve unchanged polling/reschedule behavior, generation fences, + material-change successor deduplication and accountable settlement. + A monitor remains non-executable delivery context; its independent + advancement Todo is not the monitor itself. +- Delete the replaced Python transition decisions. External polling remains + an effect adapter. Prove duplicate polls, crash between phases, races, + failed effects, another actor's claim, and no-change no-delivery semantics. + If a required command effect is still unsupported, hold whole-Goal promotion; + never fall back to a Markdown business write. + +**T3 — close remaining structured consumers, then remove their old reads.** + +- 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. +- For each migrated caller, delete its post-promotion Markdown/event fallback in that + PR. Prove absent/stale/malformed display, empty canonical state, unavailable + provider, terminal/archive ordering, claim scope and data beyond UI limits. + Canonical absence must not revive legacy data or become successful completion. +- Keep outcome history supervision, canonical obligations and settlement + authority separate. Unknown observations cannot settle Todo/replan work. + Explicitly disclose any semantic correction; do not label it full parity. + +**T4 — collect full-writer retirement after durability cutover.** + +- Depends on T1–T3 and the shared RFC's [D1–D3](shared-goal-authority-state-provider-v0.md#durability-execution-cards), including owner approval + and the explicit legacy migration window. Search remaining imports and + public command routes before deleting old Markdown business writers, + capture-only adapters and duplicate reference aggregates. +- Keep permanent Markdown rendering, validated import/export and external + effect adapters. Every retained bridge names its live caller and exit + condition. No full TS CLI, daemon or remote service is required. + +**Validation and stop rules for every card** + +Use `tests/fixtures/control_plane/coordination_production_scale_v0.json`, +`tests/control_plane/canonical_authority_fixture.py` and the existing provider +conformance suite when their semantic dimensions are affected. Verify their +current schema before reuse; never silently shrink a complex fixture to pass. +Run `npm run typecheck:control-plane`, `npm run test:control-plane`, the +affected public CLI tests and risk-based canary coverage. Shared transaction +changes require affected File/NoKV arms and an isolated real PostgreSQL run; +new local-store claims require that actual backend, not an in-memory substitute. + +Before moving code, assert intended legal and illegal behavior independently. +After moving it, report baseline/head parity, intentional differences, +production versus bridge LOC, and crossings separately from tests/generated +code. Stop on an unknown writer, missing real environment, unexplained +difference, private-data dependency or failed required gate. Do not waive +authority, evidence, fixture or payload budgets to complete a card. +A read-only snapshot or disposable synthetic Goal is allowed; active Goal +promotion, new models/jobs, soak automation, release or merge need their +respective explicit authority. Stacked schema-identifier cleanup is independent maintenance, not a prerequisite for this sequence. Absorb a downstream change only when the selected complete 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 14bd19526e..8470d22a00 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 @@ -237,35 +237,90 @@ generation fence、claim/exclusion、capacity 和 PR 等待语义保持。非法 准入。普通 add/update 准入及覆盖全部非法条件的通用修复动作仍是独立范围;不能宣称 全量零行为变化或全部 Todo writer 已闭合。 -1. **闭合实际命令与 consumer 清单。** 基于已合入的 create/claim/update 和 #4053 - terminal/successor/archive transaction 推进,不重复建设。按真实合同盘点剩余 - 字段编辑、monitor、lease、event caller,把规则迁入既有 TS owner,并在同一切片 - 删除被替代的 decision。读取复用 canonical Todo summary:promotion 后,`todo list` - 和 status/attention 不得选择陈旧 Markdown/event Todo;投影缺失、canonical 集合为空 - 也不例外。Refresh 现在只读一次无截断 canonical Todo 快照,供推荐、repair/replan - 验收和 completion-validation 问责共同使用;Todo-add 的 replan 绑定和 guided-start - 的既有 frontier 也复用同一来源适配器。既有 decision reducer 仍是规则 owner,不增加 - 第二份 planning store 或权限规则;旧模式保留原 parser 合同。Turn/quota、Dashboard、 - standing decision、shared-goal alignment 与 amendment revision basis 另行审计, - 不能将这些具名调用链的闭合等同于全部 consumer 合格。通过真实入口验证 parity 和 - provider 故障拒绝。独立维护的 Next Action 仍是正文,不导入 Todo;展示缺失不授权 - 重建丢失正文,也不能削弱完成验收门禁。 -2. **把展示闭合为可恢复的单向投影。** 复用 canonical journal/outbox 与 Todo-section - renderer,保留人工叙述、来源 revision、幂等交付和可操作的 pending repair。 - 渲染失败不能撤销已提交事务,也不能授权 Markdown fallback;恢复投影不能重跑业务操作。 - 不引入第三种 TS-Markdown backend 或自动双向同步。 -3. **资格化一个本地 store,再按完整 goal 切换。** 所选 profile 闭合 import、排序与 - consumer parity、writer fencing、capture/projection recovery、历史 receipt、容量及 - >=10 天 soak。file-v0 conformance 本身不等于长程就绪。不能让同一 goal 的 complete - 走 provider、update 却仍以 Markdown 为权威。资格化与显式 promotion 批准前,保留 - 当前默认及 fail-closed fence。本地资格化不等待 PostgreSQL service;受影响的 - PostgreSQL transaction 仍需真实集成验证。 -4. **每闭合一个边界就兑现删除。** 最后 caller 与显式迁移窗口结束后,删除旧 Markdown - 业务 writer、仅供 capture 的 glue、重复 reference aggregate 和冗余 bridge;保留 - Markdown renderer 与已资格化 import/export 工具。每条保留 seam 列出具体 caller - 和退出条件,不能用“CLI 尚未全 TS 化”笼统保留重复语义。分别报告 product LOC 删除、 - bridge LOC 增加、crossings,不能把测试/生成合同计为删除收益。连续两个切片只有 - scaffolding 时停止并重规划;净减代码是证据,不是可以牺牲行为的配额。 +#### 当前 stack 合入后的执行卡 + +这是**条件式执行规划**,不是已合并声明。2026-09-09 核查时,#4053、#4117、 +#4129 已合并;#4122(resume 诊断/规划)、#4134(交付历史)、#4136(声明诊断) +仍 open;canonical delivery-response 后续批次叠在 #4136 上。执行前核验实际 merge +commit。#4121(SQLite 候选)和 #4101(投影 receipt 保留)是独立候选,不自动成为 +依赖或已批准的默认配置。 + +只执行下面第一个未闭合阶段,不同时启动所有阶段,不重建已完成事务。具体任务 +写入 LoopX Todo;本节作为共享路线,不再维护另一份 per-agent 状态账本。 + +**T0 — 在下一实现 PR 内对齐已合入基线。** + +- Fetch 目标 remote base,记录 SHA 和每项依赖的实际合并状态。核对代码而非 PR + 标题;依赖未合并时,使用明确选定的 stacked base,或暂停该依赖单元。 +- 从 `loopx/control_plane/` 下的 `coordination/todo_update.ts`、 + `todos/field_update.ts`、`todos/provider_compatibility_edit.py`、 + `todos/line_update.py`、`scheduler/monitor_poll_writeback.py` 及公开 caller + 入手。符号移动后重新定位,不恢复已删除 wrapper。 +- 形成紧凑 caller 表:公开操作、promotion 前后来源、TS owner、外部 effect、 + 保留的 legacy caller、精确删除条件。随实现更新完成事实,不单独交付 inventory + framework PR。 +- #4122 与 delivery response 汇合时,在既有 resume owner 对齐 pending/invalid + 诊断。目标缺失不证明合法等待;历史 pending 可读不等于可放宽监督。 + 两个合同都验证后,再删除重复检查。 + +**T1 — 闭合公开 Todo update 事务。** + +- 复用现有 provider text/note 事务、lifecycle 准入、field-plan 和 completion + 规则。先枚举公开 metadata 编辑与显式 clear,不把 `UPDATE_FIELDS` 扩成所有存储 + 字段,也不让 generic patch 获得 terminal transition 权限。 +- 一次粗粒度 TS 事务覆盖合法 intent、actor/claim/exclusion/lease、字段语义、 + 最终验证、CAS 与 replay;外部执行和 checkpoint 留在 effect adapter。 + 无法安全一起闭合的 monitor effect 留到 T2,并显式列为不支持。 +- 同 PR 删除被替代的 Python update decision 与 leaf-RPC 编排;未 promotion + caller 仍需要的 codec、lock 和 compatibility writer 保留,不宣称完整 writer 退役。 +- 通过公开命令及受影响真实 provider 验证:省略/清空、unclaimed 文案修正与受限 + metadata 的差异、other-owner/lease 拒绝、no-op、非法输入无写入、竞争 revision、 + retry 和丢响应恢复。 + +**T2 — 闭合 monitor 写回及原子后续动作。** + +- 盘点 `monitor_poll_writeback.py` 及 event/Todo/lease caller,复用 monitor + generation、独立 successor 和 settlement owner,组成一笔事务,不建第二套引擎。 +- 保持 unchanged poll/reschedule、generation fence、material-change successor + 去重和可归属 settlement。Monitor 不是 delivery 执行任务;独立 advancement Todo + 不能被 monitor 自身替代。 +- 删除被替代的 Python transition decision,外部轮询保留 effect adapter。 + 验证重复 poll、阶段间 crash、race、effect 失败、其他 actor claim 和 no-change + 不形成交付。必要命令 effect 尚不支持时暂停整 Goal promotion,不能回退 Markdown 写入。 + +**T3 — 闭合剩余 structured consumer,删除各自旧读路径。** + +- 分别审计 Turn/quota、Dashboard、standing decision、shared-goal alignment、 + amendment revision 输入。复用 #4117 canonical source adapter,一次决策传递一份 + snapshot,不新增 Todo inventory。 +- 每迁完 caller,就在该 PR 删除其 promotion 后的 Markdown/event fallback。验证缺失/陈旧/ + 非法 display、canonical 空集合、provider 不可用、terminal/archive 排序、 + claim scope 和超过 UI limit 的数据。来源为空不能复活 legacy 数据或视为任务完成。 +- 区分历史监督、canonical 义务与 settlement 权威;unknown 不能结清 Todo/replan。 + 有意语义修正单独披露,不标成全量 parity。 + +**T4 — durable cutover 后兑现完整 writer 删除。** + +- 前提是 T1–T3 和 shared RFC 的 [D1–D3](shared-goal-authority-state-provider-v0.zh-CN.md#持久化执行卡),包括 owner 批准及明确的 legacy 迁移窗口。 + 搜索剩余 import 和公开路由后,删除旧 Markdown 业务 writer、capture-only adapter、 + 重复 reference aggregate。 +- 保留永久 Markdown renderer、已验证 import/export 和外部 effect adapter。 + 每条保留 bridge 标明真实 caller 与退出条件;不等待全 TS CLI、daemon 或远端服务。 + +**每张执行卡的验证与停止规则** + +涉及对应语义时,复用 `tests/fixtures/control_plane/coordination_production_scale_v0.json`、 +`tests/control_plane/canonical_authority_fixture.py` 和既有 provider conformance; +先核验当前 schema,不能为过测试缩减复杂 fixture。运行 +`npm run typecheck:control-plane`、`npm run test:control-plane`、受影响公开 CLI +测试和按风险选择的 canary。共享事务改动须覆盖受影响 File/NoKV 及真实隔离 PostgreSQL; +本地 store 声明须验证实际 backend,内存替身不能替代。 + +移动代码前独立定义合法/非法行为;移动后分别报告 baseline/head parity、有意差异、 +product/bridge LOC 和 crossings,不混入 test/generated LOC。发现未知 writer、 +真实环境缺失、未解释差异、私有数据依赖或必要门禁失败时停止,不降低 authority、 +证据、fixture 或 payload 预算。允许只读快照和一次性 synthetic Goal;活跃 Goal +promotion、启动模型/任务、soak automation、发布或合并仍需各自明确授权。 stack 中的 schema identifier 清理是独立维护,不是上述路线的前置条件。只吸收所选 完整事务确实依赖的下游改动;base 合并后,其余工作再 rebase。 From 35ef3ba8afefca160690c43592f89ddd4d061329 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Wed, 9 Sep 2026 17:57:03 +0800 Subject: [PATCH 4/5] fix(control-plane): bind wait exemptions to exact current targets Signed-off-by: huangruiteng --- examples/shared-goal-authority-e2e/mutants.py | 7 +++ loopx/control_plane/todos/resume_condition.ts | 29 ++++++++--- .../work_items/delivery_history.py | 8 +-- .../work_items/delivery_history.ts | 8 ++- tests/control_plane/test_delivery_response.py | 52 ++++++++++++++++++- .../delivery_response.test.ts | 45 +++++++++++++++- 6 files changed, 131 insertions(+), 18 deletions(-) diff --git a/examples/shared-goal-authority-e2e/mutants.py b/examples/shared-goal-authority-e2e/mutants.py index 3ad623db37..71684b6cb9 100644 --- a/examples/shared-goal-authority-e2e/mutants.py +++ b/examples/shared-goal-authority-e2e/mutants.py @@ -47,6 +47,13 @@ def command(self) -> list[str]: CASES = [ + 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'), + Case('delivery_wait_unknown_class', (('loopx/control_plane/todos/resume_condition.ts', replacement( + '["advancement_task", "user_gate", "user_action", "blocker"].includes(String(condition.target_task_class))', + 'true')),), + 'tests/control_plane_ts/delivery_response.test.ts', 'exact dependency identity'), Case('rollout_cwd_root', (('loopx/cli_rollout.py', replacement( 'resolve_runtime_root(registry, runtime_root_arg, registry_path=registry_path)', 'resolve_runtime_root(registry, runtime_root_arg)')),), diff --git a/loopx/control_plane/todos/resume_condition.ts b/loopx/control_plane/todos/resume_condition.ts index 7a5601719d..fca195e398 100644 --- a/loopx/control_plane/todos/resume_condition.ts +++ b/loopx/control_plane/todos/resume_condition.ts @@ -432,22 +432,35 @@ function diagnosedCondition(condition: JsonObject, waitingTodoId: string): JsonO /** Positive wait proof for consumers that may relax supervision. Historical * absence of an invalid marker is not proof of a valid, identified target. */ -export function resumeConditionHasKnownPendingTarget(condition: JsonObject): boolean { +export function resumeConditionHasKnownPendingTarget(condition: JsonObject, waitingTodo: JsonObject): boolean { + const spec = parseResumeWhen(waitingTodo.resume_when); + if (!spec || condition.resume_when !== spec.normalized || condition.kind !== spec.kind + || (condition.target !== undefined && condition.target !== spec.target)) return false; if (condition.schema_version !== "todo_resume_condition_v0" || condition.satisfied !== false - || resumeAvailabilityReason(condition) !== "resume_condition_pending") return false; - switch (condition.kind) { + || diagnoseTodoResumeCondition(condition, String(waitingTodo.todo_id)).state !== "pending") return false; + if ((spec.kind === "todo_done" || spec.kind === "monitor_changed") + && (condition.target_todo_id !== spec.target || spec.target === waitingTodo.todo_id)) return false; + switch (spec.kind) { case "todo_done": return ["open", "deferred"].includes(String(condition.target_status)) - && typeof condition.target_task_class === "string" && condition.target_task_class !== "continuous_monitor" + && ["advancement_task", "user_gate", "user_action", "blocker"].includes(String(condition.target_task_class)) && (condition.target_archive_state === null || condition.target_archive_state === "active"); case "monitor_changed": return condition.target_task_class === "continuous_monitor" && condition.target_status === "open" - && typeof condition.baseline_generation === "number" && condition.baseline_generation >= 0 + && typeof condition.baseline_generation === "number" && Number.isSafeInteger(condition.baseline_generation) + && condition.baseline_generation >= 0 && condition.baseline_generation === waitingTodo.resume_monitor_generation && typeof condition.material_change_generation === "number" + && Number.isSafeInteger(condition.material_change_generation) && condition.material_change_generation >= 0 && condition.material_change_generation <= condition.baseline_generation; - case "capacity_available": return condition.provider_required === false; - case "pr_merged": return typeof condition.pr_repo === "string" && condition.pr_repo.length > 0 - && condition.repository_binding_state !== "ambiguous"; + case "capacity_available": return condition.provider_required === false + && condition.provider === "runtime_available_capabilities" && condition.capability === spec.target; + case "pr_merged": { + const ref = normalizedPrRef(spec.target); + const repository = ref?.repo ?? githubRepository(waitingTodo.task_repository); + return ref !== null && repository !== null && condition.pr_repo === repository + && condition.pr_number === ref.number && condition.repository_binding_state !== "ambiguous" + && condition.repository_binding_source === (ref.repo ? "qualified_resume_when" : "task_repository"); + } default: return false; } } diff --git a/loopx/control_plane/work_items/delivery_history.py b/loopx/control_plane/work_items/delivery_history.py index ebd7c61e63..5de7cf52fb 100644 --- a/loopx/control_plane/work_items/delivery_history.py +++ b/loopx/control_plane/work_items/delivery_history.py @@ -93,14 +93,16 @@ def project_delivery_response( source = next((item for item in todo_planning_source_items(summary, include_terminal=True) if item.get("todo_id") == run.get("todo_id")), None) if summary else None fields = ("todo_id", "role", "status", "task_class", "archive_state", "claimed_by", - "excluded_agents", "resume_when", "resume_ready", "resume_condition") + "excluded_agents", "resume_when", "resume_ready", "resume_condition", + "resume_monitor_generation", "task_repository") todo = {key: source[key] for key in fields if key in source} if source else None if todo and isinstance(todo.get("resume_condition"), dict): condition = todo["resume_condition"] todo["resume_condition"] = {key: condition[key] for key in ( "schema_version", "resume_when", "satisfied", "invalid_target", "invalid_state", - "kind", "target_todo_id", "target_status", "target_task_class", "target_archive_state", - "baseline_generation", "material_change_generation", "provider_required", "pr_repo", "repository_binding_state", + "kind", "target", "target_todo_id", "target_status", "target_task_class", "target_archive_state", + "baseline_generation", "material_change_generation", "provider_required", "provider", "capability", + "pr_repo", "pr_number", "repository_binding_state", "repository_binding_source", ) if key in condition} result = effect_runtime_result("work_item.delivery_response.project", { "run": _run_facts(run), "todo": todo, diff --git a/loopx/control_plane/work_items/delivery_history.ts b/loopx/control_plane/work_items/delivery_history.ts index 6700e70ea0..27901f4b61 100644 --- a/loopx/control_plane/work_items/delivery_history.ts +++ b/loopx/control_plane/work_items/delivery_history.ts @@ -118,7 +118,7 @@ export function projectDeliveryResponse(value: unknown): JsonObject { runs: [input.run], outcome_floor_configured: true }).runs as DeliverySignal[])[0]; const todo = input.todo === null ? null : requireJsonObject(input.todo, "bound Todo"); const runAgent = typeof input.run_agent_id === "string" ? input.run_agent_id.trim() : ""; - const agentId = typeof input.agent_id === "string" ? input.agent_id.trim() : runAgent; + const agentId = typeof input.agent_id === "string" ? input.agent_id.trim() : ""; const owner = typeof todo?.claimed_by === "string" ? todo.claimed_by.trim() : ""; const excluded = Array.isArray(todo?.excluded_agents) ? todo.excluded_agents : []; const condition = todo?.resume_condition && typeof todo.resume_condition === "object" @@ -131,12 +131,10 @@ export function projectDeliveryResponse(value: unknown): JsonObject { && todo.role === "agent" && todo.task_class === "advancement_task" && ["open", "deferred"].includes(String(todo.status)) && (todo.archive_state === undefined || todo.archive_state === "active") - && agentId === runAgent && (!owner || owner === agentId) && !excluded.includes(agentId) + && Boolean(agentId) && agentId === runAgent && (!owner || owner === agentId) && !excluded.includes(agentId) && condition?.schema_version === "todo_resume_condition_v0" - && condition.resume_when === todo.resume_when && Boolean(todo.resume_when) - && condition.target_todo_id !== todo.todo_id && condition.satisfied === false && todo.resume_ready !== true - && resumeConditionHasKnownPendingTarget(condition); + && resumeConditionHasKnownPendingTarget(condition, todo); return { schema_version: "delivery_response_v0", outcome_floor_applicable: !canonicalWait, diff --git a/tests/control_plane/test_delivery_response.py b/tests/control_plane/test_delivery_response.py index 9587459b9d..98674f5b8e 100644 --- a/tests/control_plane/test_delivery_response.py +++ b/tests/control_plane/test_delivery_response.py @@ -1,9 +1,12 @@ """Current canonical waits constrain historical supervision, not vice versa.""" from copy import deepcopy +import pytest from loopx.control_plane.handoff.delivery_contract import handoff_delivery_contract from loopx.control_plane.testing.quota_fixtures import quota_status_payload, quota_todo_item, quota_todo_summary from loopx.control_plane.work_items.delivery_history import project_delivery_response +from loopx.control_plane.todos.summary_item import TODO_PLANNING_SOURCE_KEYS +from loopx.control_plane.todos.resume_condition import evaluate_todo_resume_conditions from loopx.control_plane.work_items.work_lane_context import build_work_lane_context_contract from loopx.quota import build_quota_should_run, quota_with_handoff_outcome_floor from loopx.status import project_post_handoff_history @@ -23,7 +26,7 @@ def waiting_todo(): return quota_todo_item(todo_id="todo_delivery", title="Wait for the dependency", status="deferred", claimed_by="agent-a", resume_when="todo_done:todo_dependency", resume_ready=False, resume_condition={"schema_version": "todo_resume_condition_v0", "resume_when": "todo_done:todo_dependency", - "satisfied": False, "kind": "todo_done", "target_status": "open", + "satisfied": False, "kind": "todo_done", "target_todo_id": "todo_dependency", "target_status": "open", "target_task_class": "advancement_task", "target_archive_state": "active"}) @@ -31,6 +34,33 @@ def dependency_todo(): return quota_todo_item(todo_id="todo_dependency", title="Complete prerequisite", claimed_by="agent-b") +@pytest.mark.parametrize("patch", [ + {"target_todo_id": "todo_other"}, {"target_todo_id": None}, + {"target_task_class": "unknown_class"}, {"target_task_class": ""}, + {"kind": "monitor_changed"}, +]) +def test_stale_or_malformed_target_cannot_suppress_any_consumer(patch): + todo = waiting_todo() + extra = quota_todo_item(todo_id="todo_alternative", title="Continue independent work", claimed_by="agent-a") + summary = quota_todo_summary([todo, dependency_todo(), extra], claim_scope_agent_id="agent-a") + # Mutate the evaluated snapshot, not the authoring input: summary building + # legitimately recomputes supplied conditions from its canonical source. + for key in TODO_PLANNING_SOURCE_KEYS: + for row in summary.get(key, []): + if row["todo_id"] == "todo_delivery": + row["resume_condition"].update(patch) + readiness = project_post_handoff_history([blocked_run()] * 3, PROFILE) + asset = {"execution_profile": PROFILE, "agent_todos": summary} + item = {"handoff_readiness": readiness, "agent_todos": summary, "project_asset": asset} + before = deepcopy(item) + assert project_delivery_response(blocked_run(), summary)["reason"] == "history_supervision" + assert quota_with_handoff_outcome_floor({"state": "eligible"}, waiting_on="codex", + project_asset=asset, handoff_readiness=readiness)["state"] == "focus_wait" + assert handoff_delivery_contract(item) is not None + assert build_work_lane_context_contract(item, agent_todo_summary=summary)["must_attempt_work"] is True + assert item == before + + def test_status_compaction_preserves_binding_and_all_consumers_defer_to_current_wait(): readiness = project_post_handoff_history([blocked_run()] * 3, PROFILE) summary = quota_todo_summary([waiting_todo(), dependency_todo()], claim_scope_agent_id="agent-a") @@ -55,6 +85,26 @@ def test_status_compaction_preserves_binding_and_all_consumers_defer_to_current_ assert "outcome_followthrough" not in lane +@pytest.mark.parametrize("resume, patch", [ + ("monitor_changed:todo_dependency", {"baseline_generation": 1}), + ("capacity_available:network", {"capability": "other"}), + ("pr_merged:#1", {"pr_number": 2}), +]) +def test_real_resume_projection_identity_survives_python_transport(resume, patch): + todo = {**waiting_todo(), "resume_when": resume, "resume_monitor_generation": 0, + "task_repository": "git:github.com/example/project"} + dependency = {**dependency_todo(), "task_class": "continuous_monitor", "material_change_generation": 0} + condition = evaluate_todo_resume_conditions([todo], source_items=[dependency], available_capabilities=[])[todo["todo_id"]] + summary = quota_todo_summary([todo, dependency], claim_scope_agent_id="agent-a") + for key in TODO_PLANNING_SOURCE_KEYS: + for row in summary.get(key, []): + if row["todo_id"] == todo["todo_id"]: + row["resume_condition"] = condition + assert project_delivery_response(blocked_run(), summary)["reason"] == "canonical_todo_wait" + condition.update(patch) + assert project_delivery_response(blocked_run(), summary)["reason"] == "history_supervision" + + def test_surface_supervision_remains_and_invalid_wait_does_not_clear_floor(): summary = quota_todo_summary([waiting_todo()], claim_scope_agent_id="agent-a") asset = {"execution_profile": PROFILE, "agent_todos": summary} diff --git a/tests/control_plane_ts/delivery_response.test.ts b/tests/control_plane_ts/delivery_response.test.ts index 83a06cb3e0..0ba580375f 100644 --- a/tests/control_plane_ts/delivery_response.test.ts +++ b/tests/control_plane_ts/delivery_response.test.ts @@ -13,7 +13,7 @@ const waiting = { todo_id: "todo_delivery", role: "agent", status: "deferred", task_class: "advancement_task", claimed_by: "agent-a", resume_when: "todo_done:todo_dependency", resume_ready: false, resume_condition: { schema_version: "todo_resume_condition_v0", resume_when: "todo_done:todo_dependency", satisfied: false, kind: "todo_done", - target_status: "open", target_task_class: "advancement_task", target_archive_state: "active" } }; + target_todo_id: "todo_dependency", target_status: "open", target_task_class: "advancement_task", target_archive_state: "active" } }; const input = { run, todo: waiting, run_agent_id: "agent-a", agent_id: "agent-a" }; test("wait proof consumes the real resume evaluator for all four condition kinds", () => { @@ -34,9 +34,37 @@ test("wait proof consumes the real resume evaluator for all four condition kinds const condition = (evaluated.conditions as JsonObject[])[0].condition; assert.equal(projectDeliveryResponse({ ...input, todo: { ...todo, resume_condition: condition } }).outcome_floor_applicable, !valid, resume + JSON.stringify(source)); + if (valid) { + const proof = condition as JsonObject; + const mutations: JsonObject[] = [{ kind: "unknown" }, { target: "other_target" }]; + if (proof.kind === "todo_done" || proof.kind === "monitor_changed") { + mutations.push({ target_todo_id: "todo_other" }, { target_todo_id: null }, { target_task_class: "unknown" }); + } + if (proof.kind === "monitor_changed") mutations.push( + { baseline_generation: 1 }, { material_change_generation: -1 }, { material_change_generation: 0.5 }); + if (proof.kind === "capacity_available") mutations.push({ capability: "other" }, { provider_required: true }); + if (proof.kind === "pr_merged") mutations.push({ pr_number: 2 }, { pr_repo: "example/other" }); + for (const patch of mutations) assert.equal(projectDeliveryResponse({ ...input, + todo: { ...todo, resume_condition: { ...proof, ...patch } } }).reason, + "history_supervision", resume + JSON.stringify(patch)); + } } }); +test("known non-monitor completion classes and repository-bound PR waits remain supported", () => { + for (const task_class of ["advancement_task", "user_gate", "user_action", "blocker"]) { + assert.equal(projectDeliveryResponse({ ...input, todo: { ...waiting, + resume_condition: { ...waiting.resume_condition, target_task_class: task_class } } }).reason, "canonical_todo_wait"); + } + const todo = { ...waiting, resume_when: "pr_merged:#1", task_repository: "git:github.com/example/project" }; + const evaluated = evaluateTodoResumeConditions({ schema_version: TODO_RESUME_EVALUATION_REQUEST_SCHEMA_VERSION, + items: [todo], source_items: [], rollout_events: [], available_capabilities: [] }); + const condition = (evaluated.conditions as JsonObject[])[0].condition; + assert.equal(projectDeliveryResponse({ ...input, todo: { ...todo, resume_condition: condition } }).reason, "canonical_todo_wait"); + assert.equal(projectDeliveryResponse({ ...input, todo: { ...todo, task_repository: "git:github.com/example/other", + resume_condition: condition } }).reason, "history_supervision"); +}); + test("a bound blocked observation delegates a current legal wait to canonical planning", () => { const before = structuredClone(input); const result = projectDeliveryResponse(input); @@ -46,9 +74,24 @@ test("a bound blocked observation delegates a current legal wait to canonical pl assert.deepEqual(input, before); }); +test("exact dependency identity and a supported completion class are required", () => { + for (const patch of [ + { target_todo_id: "todo_other" }, { target_todo_id: null }, + { target: "todo_other" }, { kind: "capacity_available" }, + { target_task_class: "unknown_class" }, { target_task_class: "" }, + ]) { + const result = projectDeliveryResponse({ ...input, todo: { ...waiting, + resume_condition: { ...waiting.resume_condition, ...patch } } }); + assert.equal(result.reason, "history_supervision", JSON.stringify(patch)); + assert.equal(result.outcome_floor_applicable, true); + assert.equal((result.outcome_followthrough as JsonObject).required, true); + } +}); + test("history alone, missing source, invalid wait, and other actors cannot exempt the floor", () => { for (const patch of [ { todo: null }, { agent_id: "agent-b" }, { run_agent_id: "agent-b" }, + { agent_id: null }, { run_agent_id: null }, { agent_id: "", run_agent_id: "" }, { todo: { ...waiting, todo_id: "todo_other" } }, { todo: { ...waiting, claimed_by: "agent-b" } }, { todo: { ...waiting, excluded_agents: ["agent-a"] } }, From 788d915b373e8e52dcbcda754bdecaf1ca2cfa87 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Wed, 9 Sep 2026 17:57:03 +0800 Subject: [PATCH 5/5] docs(rfc): retain roadmap overview and clarify execution cards Signed-off-by: huangruiteng --- .../rfcs/shared-goal-authority-state-provider-v0.md | 11 +++++++++++ .../shared-goal-authority-state-provider-v0.zh-CN.md | 11 +++++++++++ .../rfcs/typescript-control-plane-migration-v0.md | 8 ++++++-- .../typescript-control-plane-migration-v0.zh-CN.md | 9 ++++++--- 4 files changed, 34 insertions(+), 5 deletions(-) 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 3783d7675d..12e7d5f247 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -2522,6 +2522,15 @@ one-way projections. Do not add a third TS-Markdown backend, bidirectional live synchronization, or per-command split authority. Unsupported post-cutover commands fail closed; they do not fall back to the old writer. +#### Refactoring roadmap overview + +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. +2. **Permanent projection closure.** D1 below retains Markdown as a long-lived one-way display, never a second business authority. +3. **One qualified local profile and fenced cutover.** D2/D3 require the real backend, capacity, soak and explicit promotion approval. +4. **Retirement with named callers.** T4 removes obsolete business writers after their callers exit; permanent rendering and required import/export remain. + #### Durability execution cards Use the [TS execution cards](typescript-control-plane-migration-v0.md#execution-cards-after-the-current-stack) @@ -2550,6 +2559,8 @@ source paths, authorize monitor writeback, or change provider/promotion holds. Todo-section renderer and canonical journal/outbox. #4097 already recovers missing Todo sections with `recovery_scope=todo_sections_only`; reuse it. It cannot recover lost independent Goal narrative. + The shipped boundary is the [active-state projection contract](../../reference/protocols/active-state-structured-projection-v0.md). + Direct Markdown edits must not import themselves into authority; validated edit/import tooling is a separate proposal, never a second writer hidden inside rendering. - Evaluate #4101's receipt-retention candidate against its actual merged head; do not replace it with another delivery queue. A committed business result and pending projection delivery must remain separately observable. 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 a16fad1ed0..64977516ae 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 @@ -2000,6 +2000,15 @@ CLI / Agent / Dashboard → 唯一 TS Todo 事务 owner → canonical authority 供数;cutover 后,所选 canonical provider 向单向投影供数。不增加第三种 TS-Markdown backend、实时双向同步或按命令拆开的权威;晋升后不支持的命令 fail closed,不能回退旧 writer。 +#### 重构主线总览 + +以下规划保留原有方向;执行卡是它们的展开,不是替代或取消: + +1. **闭合 TS 事务与 consumer。** 按 [T0–T3](typescript-control-plane-migration-v0.zh-CN.md#当前-stack-合入后的执行卡) 收口规则并删除重复决策。 +2. **永久投影闭合。** 见下方 D1:Markdown 长期保留为单向展示,不恢复为业务权威。 +3. **一个已资格化的本地 profile 与 fenced cutover。** 见 D2、D3:真实 backend、容量、soak 与显式 promotion 批准缺一不可。 +4. **列明 caller 后退役。** 按 T4 删除无调用者的旧业务 writer;永久 renderer 和必要 import/export 保留。 + #### 持久化执行卡 命令清单、update/monitor 事务和 consumer 删除统一按 @@ -2023,6 +2032,8 @@ summary,之前消费 legacy summary;真实 CLI 覆盖容量变化和 promote - 从 `loopx/control_plane/todos/provider_projection.py`、既有 Todo-section renderer、 canonical journal/outbox 入手。复用 #4097 已有的缺失 Todo section 恢复及 `recovery_scope=todo_sections_only`;它不能恢复丢失的独立 Goal 正文。 + 已交付边界以 [active-state projection contract](../../reference/protocols/active-state-structured-projection-v0.md) 为准。 + 直接编辑 Markdown 不得自动导入 authority;显式验证的 edit/import 工具另提方案,不能隐藏在 renderer 中成为第二个 writer。 - 按 #4101 实际合入 head 评估 receipt-retention 候选,不再建一条 delivery queue。 业务已提交与投影尚 pending 必须分别可观测。 - 验证 crash/retry、并发 revision、缺失/陈旧/非法 display、交付前 receipt diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index bca479dfa4..334f6d4281 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -203,6 +203,10 @@ label; no legacy prediction is retained without a concrete display consumer. Unknown refreshes interrupt statistics, not Todo/replan obligations; no new persistent delivery ledger is added. Surface-only supervision and the independent small-delivery rule remain unchanged. + This exemption requires the parsed target identity and a supported task class; + monitor baseline, capability and PR repository/number also bind to the current + Todo. Missing actors or stale/mismatched conditions cannot relax supervision. + Incomplete legacy conditions remain readable, but are not positive wait proof. - Legacy outcome-marker/hint configuration remains readable and preserves whether an outcome floor is configured. Its words no longer classify runs. No persisted history is rewritten and no new default-off flag restores the @@ -310,9 +314,9 @@ is not a claim of zero behavior change or full Todo writer closure. #### Execution cards after the current stack This is a **conditional execution plan**, not a merged-status declaration. -At the 2026-09-09 checkpoint, #4053, #4117 and #4129 are merged; #4122 +At the 2026-09-09 checkpoint, #4053, #4117, #4129, #4122 (resume diagnosis/planning), #4134 (delivery history) and #4136 (claim diagnosis) -are open. The canonical delivery-response follow-up is stacked on #4136. +are merged. The canonical delivery-response follow-up targets that landed main. Check their actual merge commits before starting. #4121 (SQLite candidate) and #4101 (projection receipt retention) are independent candidates, not implicit prerequisites or approved defaults. 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 8470d22a00..2691b06b68 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 @@ -155,6 +155,9 @@ classification 保留为历史标签;没有明确展示消费者时,不保 无绑定的旧 blocker 标签不能建立该例外;其他可执行工作仍由 canonical planner 选择。unknown 刷新中断统计,不清除 Todo/replan 义务,不新增持久化交付账本。 连续表层交付监督与独立的小规模交付规则保持不变。 + 该例外必须匹配解析后的 target identity 和合法 task class;monitor baseline、 + capability、PR repository/number 也绑定当前 Todo。缺失 actor 或陈旧/错配的 + condition 不能解除监督。旧式不完整条件仍可读取,但不构成正向等待证明。 - 新交付声明通过现有 writer API 写显式 enum,例如 `refresh-state --delivery-outcome ... --delivery-batch-scale ...`。 纯状态刷新仍可不声明交付;本批不强迫每次刷新声明进展。既有写入 enum 校验、 @@ -239,9 +242,9 @@ generation fence、claim/exclusion、capacity 和 PR 等待语义保持。非法 #### 当前 stack 合入后的执行卡 -这是**条件式执行规划**,不是已合并声明。2026-09-09 核查时,#4053、#4117、 -#4129 已合并;#4122(resume 诊断/规划)、#4134(交付历史)、#4136(声明诊断) -仍 open;canonical delivery-response 后续批次叠在 #4136 上。执行前核验实际 merge +这是**条件式执行规划**,不是所有阶段已完成的声明。2026-09-09 核查时,#4053、#4117、 +#4129、#4122(resume 诊断/规划)、#4134(交付历史)、#4136(声明诊断)均已合并; +canonical delivery-response 后续批次基于这些已合入的 main。执行前核验实际 merge commit。#4121(SQLite 候选)和 #4101(投影 receipt 保留)是独立候选,不自动成为 依赖或已批准的默认配置。