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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Global Todo readiness owner cutover — 2026-11-02

## Boundary and economics

Baseline: `fb16c6cb1369efce52a673030c914b38e1dac166`. This bounded slice
moves the complete Global Todo readiness decision from `global_todos.py` to
`projectGlobalTodoReadiness` in the existing `todos/decision_scope.ts` owner,
registered as `todo.global_readiness.project`. Both `loopx global-todos` and
`loopx goal-portfolio` use it through their shared display adapter; portfolio
classifies each registered Agent independently.

TypeScript owns ordered selected/lane/summary/frontier traversal, first-source
merge with missing-field fill, source/status provenance, in-process `todoGateRelation`, blocked/deferred/runnable
precedence, and warning ordering. `normal_delivery_allowed` remains a readiness
projection signal and grants no execution permission. Existing readiness values
are reused; the new versioned request/result describe this bounded projection,
not a new lifecycle or authority vocabulary.

Instrumentation pins all cross-runtime requests, including envelope sealing:

| Operation | Baseline | Candidate |
| --- | --- | --- |
| One classification, gates and candidates present | 1 relation request | 1 complete readiness request |
| One classification, no gates | 0 | 1 |
| One classification, no candidates (including empty payload) | 0 | 1 |
| Global command, one classified Goal with gates | 2 including envelope | 2 including envelope |
| Global command, gate-free or empty classification | 1 envelope | 2 including envelope |
| Portfolio, two Agents with gates | 2 | 2 |
| Portfolio, two gate-free/empty Agent classifications | 0 | 2 |

The gate-free/empty increase is intentional and disclosed. No latency or
long-horizon effectiveness/efficiency benefit is claimed.

Product physical diff: +163/−166 lines, net −3. Python removes source
traversal/merge helpers, blocked/deferred classification, readiness precedence
composition and its separate safety-rank table. The uncalled Python scalar
`todo_gate_relation` bridge and its TS scalar dispatch are deleted. Its synthetic
smoke and decoder tests use the production batched bridge retained for summary.

Python retains `open_user_gate_todo_items` as the single gate-selection owner:
summary, stall repair, Todo summary and agent scope still call it. Its ordered
selection, done filtering and Python-equality deduplication must not be copied
into TypeScript. Selected gates cross as decoded facts with target text.
Shared `_facts` retains persisted ID/scope normalization and legacy task-class
interpretation. The bounded request carries only selected/lane records, delivery
signal, and list-valued summary/frontier fields. Python decodes dict elements of
every such list generically; TypeScript alone owns candidate list-key traversal.
There is no recursive payload walk. The largest frozen case's compact UTF-8 JSON
request shrinks from 9,749 to 6,447 bytes (33.9%); outputs remain byte-identical.
Python also retains status/quota IO, public text redaction, `_work_kind` display
interpretation, final grouping/deduplication/display sorting, envelope sealing
and Markdown. These compatibility and presentation boundaries are not a second
candidate-readiness owner.

Facade exit: delete `project_global_todo_readiness` and its decoded-field
sidecar once both global and portfolio host/read models consume the typed Todo
snapshot and projection directly, with native legacy metadata decoding and
public-safe rendering at their owning boundaries. Delete the shared display
adapter when neither consumer needs Python normalization. Retire the temporary
characterization corpus/archive comparison at that exit after durable semantic
and public-entrypoint tests cover the retained contracts.

## Qualification and limits

Seven synthetic characterization cases passed on unchanged Python before the
cutover; their complete JSON/Markdown bytes and baseline/candidate crossing
lists were then frozen. The fixture SHA-256 is
`eaf17921f81357329ba7bc3c7dcb34253eed83b53d31c31d6dba0fdb5c547641`.
Cases cover complementary duplicate fields, conflicting statuses, delivery
denial, exact/scope conflict, unrelated gates, blocked/deferred overlap,
missing identities, two Agents, warning order, empty inputs and public-text
redaction. Independent TS tests assert precedence, denial and exact/scope
conflict semantics, plus malformed protocol rejection. Python decoder tests
reject malformed results without fallback.

`tests/test_global_todo_readiness_characterization.py::test_production_cli_archive_parity`
archives `HEAD` without stash and compares both public commands, JSON and
Markdown, on the same synthetic temporary home with real File and SQLite
providers. Only clocks are frozen before importing the production CLI; field
presence, diagnostics, source versions and hashes are compared unchanged.
All eight command/provider/format pairs match and require populated Todo and
Agent results. This qualifies a source checkout, not installed packaging or
Comment thread
loopx-agent marked this conversation as resolved.
Resolved
frontend behavior; no caller capability or user journey was added.

The focused scheduler-state readback test
`test_real_cli_quota_should_run_discloses_unreadable_scheduler_state` fails on
both the changed checkout and the archived baseline: its quota command omits
Turn identity and returns `QUOTA_VALIDATION_FAILED` with the requirement for
`--turn-instance-id` or `--begin-turn`. This is a pre-existing caller/validation
mismatch, outside readiness ownership. Preserve that failure; do not relax the
argument gate or rewrite this migration's expected outputs to conceal it.

Semantic drift remains `independently_maintained=45/45`; there are no new
control-plane source files or same-basename pairs. Maintainability and interface
budgets pass unchanged. The integrated canary passes File and SQLite. The
Comment thread
loopx-agent marked this conversation as resolved.
Resolved
diff-scoped advisory detects no supported new vocabulary carriers across four
changed product source files; its supported scope excludes test sources.

## Review correction validation

After removing duplicate gate selection and bounding the request: requested
Python suite 151 passed / 1 failed (the scheduler caller mismatch above);
native Node tests 19 passed / 0 failed; boundary suite 28 passed / 0 failed.
Ruff on all four changed Python files, control-plane typecheck, both requested
smokes and diff whitespace checks pass. Seven frozen characterization cases
remain byte-identical, with the fixture hash unchanged. File/SQLite production
CLI parity covers all eight command/format/provider pairs. A focused boundary
regression proves generic decoding of unknown list fields, omission of unrelated
objects and reuse of shared gate deduplication. No commit, staging or push was
performed; generated uv.lock was removed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Global Todo 就绪分类归属切换 — 2026-11-02

## 边界与经济性

基线:`fb16c6cb1369efce52a673030c914b38e1dac166`。本次有界切片将
完整 Global Todo 就绪决策从 `global_todos.py` 移至现有
`todos/decision_scope.ts` owner 的 `projectGlobalTodoReadiness`,注册为
`todo.global_readiness.project`。`loopx global-todos` 与
`loopx goal-portfolio` 均通过共享展示适配器使用它;portfolio 对每个已注册
Agent 分别分类。

TypeScript 负责 selected/lane/summary/frontier 的有序遍历、首个来源优先并补齐
缺失字段的合并、来源/状态溯源、进程内
`todoGateRelation`、blocked/deferred/runnable 优先级,以及警告顺序。
`normal_delivery_allowed` 仍只是就绪投影信号,不授予执行权限。复用现有就绪值;
新增版本化请求/结果仅描述本次有界投影,不构成新的生命周期或权限词汇。

测试插桩固定所有跨运行时请求,包括 envelope sealing:

| 操作 | 基线 | 候选 |
| --- | --- | --- |
| 单次分类,同时存在 gate 与候选 | 1 次关系请求 | 1 次完整就绪请求 |
| 单次分类,无 gate | 0 | 1 |
| 单次分类,无候选(含空 payload) | 0 | 1 |
| Global 命令,分类一个有 gate 的 Goal | 2,含 envelope | 2,含 envelope |
| Global 命令,无 gate 或空分类 | 1 次 envelope | 2,含 envelope |
| Portfolio,两个有 gate 的 Agent | 2 | 2 |
| Portfolio,两个无 gate/空分类的 Agent | 0 | 2 |

无 gate/空路径的增加是有意且已披露的。不声称延迟或长程效果/效率收益。

产品物理行差异:+163/−166 行,净 −3。Python 删除来源遍历/合并 helper、
blocked/deferred 分类、就绪优先级组合及独立 safety-rank 表。删除无生产调用方的
Python 标量 `todo_gate_relation` 桥及其 TS dispatch;对应 smoke 和 decoder 测试
使用 summary 仍在调用的批量关系桥。

Python 保留 `open_user_gate_todo_items` 为唯一 gate 选择 owner:summary、
stall repair、Todo summary 和 agent scope 仍依赖它。有序选择、done 过滤及
Python 相等去重不可复制到 TypeScript。已选 gate 以解码 facts 和目标文本传输。
共享 `_facts` 保留持久化 ID/scope 归一化与旧 task-class 解释。有界请求仅传输
selected/lane 记录、delivery 信号以及 summary/frontier 的 list 字段。Python
通用解码每个 list 的 dict 元素;TypeScript 独占候选 list key 遍历规则,不再递归
遍历整个 payload。最大冻结用例的紧凑 UTF-8 JSON 请求从 9,749 降至 6,447 字节
(减少 33.9%),输出字节不变。Python 还保留 status/quota IO、公开文本脱敏、
`_work_kind` 展示解释、最终分组/去重/展示排序、envelope sealing 和 Markdown。
这些兼容与展示边界不是第二个候选就绪 owner。

Facade 退出条件:当 global 与 portfolio 的宿主/读模型都直接消费 typed Todo
snapshot 和投影,且各自边界已原生承接旧元数据解码及公开安全渲染时,删除
`project_global_todo_readiness` 及其字段解码 sidecar。当两个消费者均不再需要
Python 归一化时,删除共享展示适配器。在该退出点,持久语义与公开入口测试覆盖
保留契约后,移除临时 characterization corpus/archive 对照。

## 资格与限制

七个合成 characterization 用例先在未改动的 Python 上通过,然后冻结完整
JSON/Markdown 字节及基线/候选 crossing 列表。fixture SHA-256 为
`eaf17921f81357329ba7bc3c7dcb34253eed83b53d31c31d6dba0fdb5c547641`。
覆盖互补重复字段、冲突状态、delivery 拒绝、精确目标/scope 冲突、无关 gate、
blocked/deferred 重叠、缺失身份、两个 Agent、警告顺序、空输入与公开文本脱敏。
独立 TS 测试断言优先级、拒绝及精确目标/scope 冲突语义,并拒绝畸形协议。
Python decoder 测试拒绝畸形结果且不作回退。

`tests/test_global_todo_readiness_characterization.py::test_production_cli_archive_parity`
通过 archive `HEAD`、不用 stash,在同一合成临时 home 上以真实 File 和 SQLite
provider 对比两个公开命令的 JSON 与 Markdown。仅在导入生产 CLI 前冻结时钟;
字段存在性、诊断、来源版本与哈希均原样比较。八组命令/provider/格式均一致,
并要求结果实际包含 Todo 与 Agent。此证据资格为 source checkout,不覆盖安装包
或前端行为;没有新增调用方能力或用户旅程。

聚焦 scheduler-state 读回测试
`test_real_cli_quota_should_run_discloses_unreadable_scheduler_state` 在修改树与
archive 基线上都失败:其 quota 命令缺少 Turn 身份,返回
`QUOTA_VALIDATION_FAILED`,要求 `--turn-instance-id` 或 `--begin-turn`。
这是已有的调用方/验证不匹配,位于就绪 owner 之外。保留该失败;不得放宽参数门禁,
也不得改写本次迁移的期望输出来掩盖它。

语义漂移仍为 `independently_maintained=45/45`;未新增 control-plane 源文件或
同名配对。Maintainability 与接口预算保持不变并通过。集成 canary 的 File 和
SQLite 均通过。diff 范围 advisory 在四个变更产品源文件中未发现受支持的新词汇
载体;该工具的支持范围不包含测试源文件。

## 评审修正验证

删除重复 gate 选择并收窄请求后:指定 Python 套件 151 通过 / 1 失败(上述
scheduler 调用参数不匹配);原生 Node 测试 19 通过 / 0 失败;边界套件
28 通过 / 0 失败。四个已修改 Python 文件的 Ruff、control-plane typecheck、
两个指定 smoke 和 diff 空白检查均通过。七个冻结 characterization 用例的输出
字节不变,fixture 哈希不变。File/SQLite 生产 CLI 对照覆盖八组命令/格式/provider。
聚焦边界回归验证未知 list 字段的通用解码、排除无关对象及复用共享 gate 去重。
未 commit、stage 或 push;已删除生成的 uv.lock。
Original file line number Diff line number Diff line change
Expand Up @@ -2845,3 +2845,4 @@ with shared write/read status-phase checks. See the [2026-10-02 checkpoint](ledg
- Complete durable-completion evidence read: [2026-11-02 owner and crossings ledger](ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.md).
- Complete adaptive child admission decision: [2026-11-02 checkpoint](ledger/typescript-control-plane-migration-v0/2026-11-02-adaptive-child-admission.md).
- Progress-review receipt qualification: [2026-11-02 checkpoint](ledger/typescript-control-plane-migration-v0/2026-11-02-progress-review-qualification.md).
- Global Todo readiness complete owner cutover (both consumers): [2026-11-02 ledger](ledger/typescript-control-plane-migration-v0/2026-11-02-global-todo-readiness.md).
Original file line number Diff line number Diff line change
Expand Up @@ -2110,3 +2110,4 @@ SQLite 中位数略升,收益不能外推为全面加速。这只资格化本
- 完整 durable-completion 证据读取:[2026-11-02 owner 与调用边界记录](ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.zh-CN.md)。
- 完整自适应子 Agent 准入决策:[2026-11-02 检查点](ledger/typescript-control-plane-migration-v0/2026-11-02-adaptive-child-admission.zh-CN.md)。
- 进度评审 receipt 资格判定:[2026-11-02 检查点](ledger/typescript-control-plane-migration-v0/2026-11-02-progress-review-qualification.zh-CN.md)。
- Global Todo 就绪分类完整 owner 切换(两个消费者):[2026-11-02 记录](ledger/typescript-control-plane-migration-v0/2026-11-02-global-todo-readiness.zh-CN.md)。
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))

from loopx.control_plane.todos.decision_scope import todo_gate_relation # noqa: E402
from loopx.control_plane.todos.decision_scope import todo_gate_relations # noqa: E402
from loopx.control_plane.scheduler.execution_context import ( # noqa: E402
GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT,
scheduler_execution_context_for_runtime_profile,
Expand Down Expand Up @@ -967,7 +967,7 @@ def assert_conflicting_exact_and_decision_scope_requires_projection_repair() ->
}
],
}
relation = todo_gate_relation(gate, agent_item)
relation = todo_gate_relations([gate], [agent_item])[0][0]
assert relation["source"] == "unblocks_todo_id+decision_scope", relation
assert relation["state"] == "projection_repair_required", relation
assert relation["reason"] == (
Expand Down
1 change: 1 addition & 0 deletions loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export function createEffectRuntimeHandlers(
["todo.succession.project", lazyHandler(() => import("./todos/succession.ts"), ({projectTodoSuccession}) => projectTodoSuccession)],
["todo.work_counts.project", lazyHandler(() => import("./todos/summary_lanes.ts"), ({projectLegacyTodoWorkCounts}) => projectLegacyTodoWorkCounts)],
["projection.envelope.seal", lazyHandler(() => import("./projection_envelope.ts"), ({sealProjectionEnvelope}) => sealProjectionEnvelope)],
["todo.global_readiness.project", lazyHandler(() => import("./todos/decision_scope.ts"), ({projectGlobalTodoReadiness}) => projectGlobalTodoReadiness)],
["todo.decision_scope.evaluate", lazyHandler(() => import("./todos/decision_scope.ts"), ({evaluateDecisionScope}) => evaluateDecisionScope)],
["todo.user_completion.plan", lazyHandler(() => import("./todos/user_completion.ts"), ({evaluateUserCompletion}) => evaluateUserCompletion)],
["agent.capability_gate.evaluate", lazyHandler(() => import("./agents/capability_gate.ts"), ({evaluateCapabilityGate}) => evaluateCapabilityGate)],
Expand Down
66 changes: 58 additions & 8 deletions loopx/control_plane/todos/decision_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
normalize_todo_id,
normalize_todo_required_decision_scopes,
)
from .user_gate import is_user_gate_todo_item
from .user_gate import is_user_gate_todo_item, open_user_gate_todo_items
from .todo_semantics import todo_projection_sort_key, todo_item_task_class, todo_item_has_removed_continuation_policy

DECISION_SCOPE_CONSISTENCY_SCHEMA_VERSION = "required_decision_scope_consistency_v0"
Expand Down Expand Up @@ -293,13 +293,6 @@ def build_required_decision_scope_repair_hint(
return result


def todo_gate_relation(gate: dict[str, Any], agent_item: dict[str, Any]) -> dict[str, Any] | None:
return _optional_relation(
"relation",
_evaluate("relation", gate=_facts(gate), item=_facts(agent_item)),
)


def todo_gate_relations(gates: list[dict[str, Any]], items: list[dict[str, Any]]) -> list[list[dict[str, Any] | None]]:
"""Evaluate a consumer's candidate set in one RPC, retaining positional identity."""
if not gates or not items:
Expand Down Expand Up @@ -356,3 +349,60 @@ def facts(item: dict[str, Any]) -> dict[str, Any]:
if not isinstance(projections, list) or len(projections) != len(items):
raise TypeError("invalid typed gate scope projection cardinality")
return [_required_projection("gate_scope", row, schema_version="todo_gate_scope_projection_v0") for row in projections]


def _readiness_facts(item: dict[str, Any]) -> dict[str, Any]:
return {
**_facts(item),
"identity_text": str(item.get("todo_id") or ""),
"status_text": str(item.get("status") or ""),
"target_text": str(item.get("unblocks_todo_id") or ""),
}


def _global_readiness_request(payload: dict[str, Any]) -> dict[str, Any]:
"""Bound transport to candidate containers; TS alone selects their list keys."""
bounded = {
key: payload.get(key)
for key in ("selected_todo", "agent_lane_next_action", "normal_delivery_allowed")
}
decoded = {
f"/{key}": _readiness_facts(value)
for key, value in bounded.items()
if isinstance(value, dict)
}
for container_key in ("agent_todo_summary", "agent_scope_frontier"):
container = payload.get(container_key)
lists = {
key: value for key, value in container.items() if isinstance(value, list)
} if isinstance(container, dict) else {}
bounded[container_key] = lists
for key, values in lists.items():
for index, item in enumerate(values):
if isinstance(item, dict):
decoded[f"/{container_key}/{key}/{index}"] = _readiness_facts(item)
return {
"schema_version": "global_todo_readiness_request_v0",
"payload": bounded,
"decoded": decoded,
"gates": [
_readiness_facts(gate)
for gate in open_user_gate_todo_items(payload.get("user_todo_summary"))
],
}


def project_global_todo_readiness(payload: dict[str, Any]) -> dict[str, Any]:
"""Decode persisted fields and reuse the shared Python gate selector."""
result = effect_runtime_result(
"todo.global_readiness.project", _global_readiness_request(payload),
)
projection = _required_projection(
"global_readiness", result, schema_version="global_todo_readiness_v0",
)
for key in ("warnings", "candidates"):
if not isinstance(projection.get(key), list) or any(
not isinstance(row, dict) for row in projection[key]
):
raise TypeError("invalid typed decision scope global readiness rows")
return projection
Loading
Loading