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
15 changes: 12 additions & 3 deletions docs/reference/canonical-terminal-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,23 @@ command or transport in this slice.

A declared deliverable passing its validator is not the same event as its Turn
being settled. The shared TypeScript settlement plan orders ordinary Todo
completion, durable `refresh-state`, one `quota spend-slot`, then final
`todo complete --no-follow-up`, all bound to the original Goal, Agent, Todo and
Turn. Ordinary completion retains `active_goal` continuation without creating
completion, durable `refresh-state` and one `quota spend-slot`, all bound to the
original Goal, Agent, Todo and Turn. Final `todo complete --no-follow-up` applies
only when the current contract permits final scope closeout. Ordinary completion
retains `active_goal` continuation without creating
an artificial successor. Its command has the condition
`todo_deliverable_complete`; required validation is never conditional or waived.
Qualified `in_flight_continuation` leaves unfinished work open and omits ordinary
completion rather than pretending the deliverable passed.

The shared Todo summary keeps missing-successor diagnostics and counts, but its
warning is review guidance, not an obligation to create a successor or terminate
a completed stage. Review remaining authorized Goal acceptance and the runnable
frontier; continue existing work or replan while scope remains. An empty queue
alone does not certify final scope closeout. Graph validation, empty-frontier
replan and terminal proof remain governed by their existing typed rules. This
guidance change is shared by CLI, status and quota readback.

If final closeout is attempted before writeback/accounting, CLI JSON and Markdown
return that same recovery plan. Supplied registry/runtime, project/state routes
and original lease proof are retained where the corresponding command accepts
Expand Down
10 changes: 8 additions & 2 deletions docs/reference/canonical-terminal-review.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,18 @@ legacy unscoped 回执属于该周期。三种意图共用同一个终结事务
## 受配额约束的 CLI 完成

交付通过原 validator,不等于该 Turn 已结算。共享 TS settlement plan 依次引导普通
Todo 完成、永久 `refresh-state` 写回、一次 `quota spend-slot` 扣额,以及最终
`todo complete --no-follow-up`;全部绑定原 Goal、Agent、Todo 和 Turn。普通完成
Todo 完成、永久 `refresh-state` 写回和一次 `quota spend-slot` 扣额;全部绑定原
Goal、Agent、Todo 和 Turn。只有当前契约允许最终范围收尾时,才执行
`todo complete --no-follow-up`。普通完成
保留 `active_goal` continuation,不制造虚假后继。该命令的执行条件是
`todo_deliverable_complete`,必需的验证不会随条件省略。符合准入的
`in_flight_continuation` 保留未交付 Todo 为 open,不会伪造通过验证的完成。

共享 Todo 摘要仍保留缺少后继的诊断和计数,但告警是复核引导,不要求制造后继或
终结已完成阶段。先复核剩余的授权 Goal 验收与可执行任务;范围内仍有工作时继续
已有任务或重规划。队列为空本身不能证明最终范围可以收尾。图校验、空前沿重规划
与终态证明仍由已有 typed 规则决定;这次提示调整覆盖 CLI、status 和 quota 读回。

若提前终结、缺少写回或扣额,CLI JSON 和 Markdown 返回同一恢复计划;原 registry/runtime、
project/state 路由和租约证明保留在对应命令支持的参数中。命令模板只是引导,不提供
租约、验证回执或额外执行权限。
Expand Down
5 changes: 3 additions & 2 deletions loopx/control_plane/todos/succession_warning.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ def build_todo_succession_warning_lanes(
"recommended_action": warning.get(
"recommended_action",
(
"run loopx todo complete --no-follow-up for the completed Todo, "
"or add/link a successor Todo; do not invent a user gate"
"Review remaining authorized Goal acceptance and the runnable frontier; "
"follow the current quota settlement/replan contract. "
"A summary warning does not authorize terminal action."
),
),
}
Expand Down
10 changes: 10 additions & 0 deletions loopx/control_plane/todos/summary_projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ export function projectTodoSummary(value: unknown): SummaryProjection {
const gaps = source.filter(index => rows[index].successor_gap === true).sort(byTime(false));
if (gaps.length) {
fields.completed_without_successor_count = gaps.length;
// A missing lineage edge is a review prompt, not authority to terminate an
// ordinary completed stage. Replan and settlement retain their own gates.
fields.todo_succession_warning = {
schema_version: "todo_succession_warning_v0",
reason_code: "completed_advancement_without_successor",
count: gaps.length,
recommended_action: "Review remaining authorized Goal acceptance and the runnable frontier. " +
"Continue existing work or replan while scope remains; ordinary Todo completion needs no artificial successor. " +
"Use --no-follow-up only for final scope closeout allowed by the current settlement contract.",
};
lane("completed_without_successor_items", gaps, 5, "gap");
}
if (selected.watch_only_monitor_items.length) {
Expand Down
24 changes: 7 additions & 17 deletions loopx/control_plane/todos/todo_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@
todo_presentation_sort_key as projection_todo_presentation_sort_key,
todo_projection_sort_key as projection_todo_projection_sort_key,
)
from .succession_warning import (
TODO_SUCCESSION_WARNING_REASON_CODE,
TODO_SUCCESSION_WARNING_SCHEMA_VERSION,
)
from .resume_condition import evaluate_todo_resume_conditions
from ..runtime.time import now_utc, now_utc_iso
from ..work_items.project_asset import build_project_asset_todo_summary
Expand Down Expand Up @@ -930,6 +926,10 @@ def valid_ordinals(value: Any) -> TypeGuard[list[int]]:
summary = result.get("fields")
if not isinstance(summary, dict) or summary.get("schema_version") != "todo_summary_v0":
raise ValueError("invalid typed Todo summary fields")
warning = summary.get("todo_succession_warning")
warning_action = warning.get("recommended_action") if isinstance(warning, dict) else None
if summary.get("completed_without_successor_count") and not isinstance(warning_action, str):
raise ValueError("invalid typed Todo succession warning")
for name, lane in lanes.items():
mode = lane.get("format")
if mode not in {"raw", "active", "compact", "recent", "gap"}:
Expand All @@ -948,11 +948,13 @@ def valid_ordinals(value: Any) -> TypeGuard[list[int]]:
compact.pop(key, None)
if mode == "gap":
compact.update(succession_tracked=True,
recommended_action="record no_followup=true or add/link a successor todo")
recommended_action=warning_action)
else:
raise ValueError("invalid Todo summary display format")
formatted.append(compact)
summary[name] = formatted
if isinstance(warning, dict):
warning["items"] = summary["completed_without_successor_items"]
return {"summary": summary, "items": [items[index] for index in selected],
"succession": [succession[index] for index in selected],
"orchestration": {name: [items[index] for index in indices] for name, indices in orchestration.items()}}
Expand Down Expand Up @@ -1040,16 +1042,4 @@ def compact_evaluated_todo_group(
projected["orchestration"], role=role)
if handoff_gates:
summary["handoff_gates"] = handoff_gates
if summary.get("completed_without_successor_count"):
summary["todo_succession_warning"] = {
"schema_version": TODO_SUCCESSION_WARNING_SCHEMA_VERSION,
"reason_code": TODO_SUCCESSION_WARNING_REASON_CODE,
"count": summary["completed_without_successor_count"],
"items": summary["completed_without_successor_items"],
"recommended_action": (
"run loopx todo complete --no-follow-up for the completed Todo, "
"or add/link a successor Todo before closing the slice; do not "
"invent a user gate"
),
}
return summary
16 changes: 16 additions & 0 deletions tests/control_plane/test_declared_terminal_settlement_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,22 @@ def test_declared_leased_completion_writeback_spend_terminal_and_replay(
assert marker.read_text() == "run\n"
assert not completed.get("successor_todo_ids")
assert cli._spend_run_count(runtime) == 0
# Ordinary completion leaves a genuine lineage gap. Shared read guidance
# must not convert that diagnostic into mandatory terminal mutation.
code, ordinary = run("todo", "list", "--goal-id", cli.GOAL_ID)
assert code == 0, ordinary
warning = ordinary["agent_todos"]["todo_succession_warning"]
assert warning["count"] == 1
assert "ordinary Todo completion needs no artificial successor" in warning["recommended_action"]
assert "terminal_closure_proof" not in ordinary["agent_todos"]
code, continuing_guard = run(*guard_args)
assert code == 0, continuing_guard
assert continuing_guard["agent_todo_summary"]["todo_succession_warning"]["recommended_action"] == warning["recommended_action"]
code, unchanged = run("todo", "list", "--goal-id", cli.GOAL_ID)
assert code == 0, unchanged
assert unchanged["todos"] == ordinary["todos"]
assert marker.read_text() == "run\n"
assert cli._spend_run_count(runtime) == 0
readback = read_heartbeat_settlement(runtime, goal_id=cli.GOAL_ID, agent_id=cli.AGENT_ID,
todo_id=cli.TODO_ID, turn_instance_id=cli.TURN_ID)
assert readback is not None
Expand Down
24 changes: 24 additions & 0 deletions tests/control_plane/test_todo_succession_read_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@ def test_selection_after_warning_display_cap_keeps_exact_gap():
assert selected["completed_without_successor_count"] == 1


def test_warning_consumers_share_typed_guidance_without_mutating_continuation():
from copy import deepcopy
from loopx.control_plane.todos.succession_warning import build_todo_succession_warning_lanes

records = [work("todo_stage", completion_continuation="active_goal"),
work("todo_next", status="open", done=False)]
before = deepcopy(records)
result = summary(records)
warning = result["todo_succession_warning"]
assert warning["count"] == 1
assert "ordinary Todo completion needs no artificial successor" in warning["recommended_action"]
assert result["completed_without_successor_items"][0]["recommended_action"] == warning["recommended_action"]
assert result["first_executable_items"][0]["todo_id"] == "todo_next"
assert "terminal_closure_proof" not in result
for cap in (0, 1, 12):
projected = build_todo_succession_warning_lanes(result, item_limit=cap)
assert projected["completed_without_successor_count"] == 1
assert projected["todo_succession_warning"]["recommended_action"] == warning["recommended_action"]
legacy = build_todo_succession_warning_lanes({"completed_without_successor_count": 1}, item_limit=0)
assert "current quota settlement/replan contract" in legacy["todo_succession_warning"]["recommended_action"]
assert "todo complete" not in legacy["todo_succession_warning"]["recommended_action"]
assert records == before


def test_changed_item_cannot_reuse_old_graph_evaluation():
result = summary([work("todo_closed", no_followup=True)])
result["items"][0]["no_followup"] = False
Expand Down
26 changes: 26 additions & 0 deletions tests/control_plane_ts/todo_summary_projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,32 @@ test("one whole-source projection computes counts, visibility and closure before
}
});

test("succession diagnostics guide continuation without authorizing terminal closeout", () => {
const facts = [{todo_id: "todo_stage", status: "done", active: true, advancement: true,
no_followup: false, successors: [], superseded_by: null, unblocks: null, resumes: null,
handoff: false, context_fields: ["claimed_by"]}];
const stage = done({todo_id: "todo_stage", no_followup: false, successor_gap: true});
for (const limit of [null, 0, 1]) {
const carrier = fused([stage], facts);
const source = structuredClone(carrier);
const result = projectTodoSummary({...carrier, item_limit: limit});
assert.deepEqual(result, projectTodoSummary(request([stage], {item_limit: limit})));
assert.equal(result.fields.completed_without_successor_count, 1);
assert.equal(result.fields.terminal_closure_proof, undefined);
const warning = result.fields.todo_succession_warning as JsonObject;
assert.equal(warning.count, 1);
assert.match(String(warning.recommended_action), /Review remaining authorized Goal acceptance/);
assert.match(String(warning.recommended_action), /runnable frontier/);
assert.match(String(warning.recommended_action), /ordinary Todo completion needs no artificial successor/);
assert.match(String(warning.recommended_action), /only for final scope closeout.*current settlement contract/);
assert.deepEqual(carrier, source, "read guidance must not add successor or terminal facts");
}
const continuing = projectTodoSummary(request([stage, row({todo_id: "todo_next"})]));
assert.equal(continuing.fields.completed_without_successor_count, 1);
assert.deepEqual(continuing.lanes.first_executable_items.indices, [1]);
assert.equal(continuing.fields.terminal_closure_proof, undefined);
});

test("recent completion orders true microsecond instants and ignores later edits", () => {
const rows = [done({completed_at: "2026-01-01T10:00:00.000001+08:00", updated_at: "2026-12-01T00:00:00Z"}),
done({completed_at: "2026-01-01T02:00:00.000002Z"}), done({completed_at: "invalid"})];
Expand Down
Loading