Skip to content

Commit f172435

Browse files
authored
Merge pull request #4771 from loopx-project/codex/todo-authoring-priority-hint-20260920
fix(todos): make priority explicit across CLI, reviewed edits and authority stores
2 parents a1d5113 + a739abb commit f172435

58 files changed

Lines changed: 710 additions & 165 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
138138
const lastAuthoritativeSubagentConfigurationRef = useRef<WorkspaceGoalSubagentConfiguration | null>(null);
139139
const verifiedSubagentBaselineRef = useRef<WorkspaceGoalSubagentConfiguration | null>(null);
140140
const [todoAgentId, setTodoAgentId] = useState(agents.find((agent) => agent.available)?.agentId ?? "codex");
141+
const [todoPriority, setTodoPriority] = useState("");
141142
const [todoResumeWhen, setTodoResumeWhen] = useState("");
142143
const closeRef = useRef<HTMLButtonElement>(null);
143144
const drawerRef = useRef<HTMLDivElement>(null);
@@ -607,6 +608,21 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
607608
summary: t("drawer.reassignSummary", { task: selection.item.text }),
608609
})} type="button">{t("timeline.review")}</button>
609610
</label>
611+
<label className="personal-inline-agent-select">{t("drawer.taskPriority")}
612+
<select aria-label={t("drawer.taskPriority")} value={todoPriority} onChange={(event) => setTodoPriority(event.target.value)}>
613+
<option value="">{t("drawer.taskPriorityChoose")}</option>
614+
{["P0", "P1", "P2", "P3", "P4"].map((priority) => <option key={priority} value={priority}>{priority}</option>)}
615+
<option value="clear">{t("drawer.taskPriorityClear")}</option>
616+
</select>
617+
<button className="personal-secondary-action" disabled={!todoPriority} onClick={() => void callbacks.onPreviewAction?.({
618+
actionKind: "todo.update",
619+
context: {goal_id: selection.item.goalId, kind: "todo", todo_id: selection.item.todoId},
620+
idempotencyKey: `workspace-todo-${selection.item.todoId}-priority-${todoPriority}-${Date.now().toString(36)}`,
621+
normalizedParameters: {goal_id: selection.item.goalId, todo_id: selection.item.todoId, operation: "edit",
622+
...(todoPriority === "clear" ? {clear_priority: true} : {priority: todoPriority})},
623+
summary: `${t("drawer.taskPriority")}: ${todoPriority === "clear" ? t("drawer.taskPriorityClear") : todoPriority}`,
624+
})} type="button">{t("timeline.review")}</button>
625+
</label>
610626
<strong>{t("drawer.taskDeferUntil")}</strong>
611627
<label className="personal-inline-agent-select personal-inline-resume-when">{t("drawer.taskDeferUntil")}
612628
<input

‎apps/presentation/dashboard/src/features/personal-workspace/goal-tasks-view.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export function GoalTasksView({
140140
.filter((todo) => todo.goalId === goal.goalId)
141141
.map((todo) => ({ ...todo, goalTitle: goal.title }));
142142
const priorityRank = (todo: WorkspaceGoal["agentTodos"][number]) =>
143-
todo.priority === "P0" ? 0 : todo.priority === "P1" ? 1 : todo.priority === "P2" ? 2 : 3;
143+
todo.priority === "P0" ? 0 : todo.priority === "P1" ? 1 : todo.priority === "P2" ? 2 : todo.priority === "P3" ? 3 : todo.priority === "P4" ? 4 : 50;
144144
const agentLanes = useMemo(() => {
145145
const lanes = new Map((goal.agentLanes ?? []).map((lane) => [lane.agentId, lane]));
146146
for (const todo of goal.agentTodos) {

‎apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ const en = {
350350
"drawer.taskDeferPlaceholder": "For example, resume_at:2026-09-14T09:00:00+08:00",
351351
"drawer.taskDeferReview": "Review defer",
352352
"drawer.taskDeferSupported": "Supports todo_done, pr_merged, capacity_available, and timezone-aware resume_at conditions.",
353+
"drawer.taskPriority": "Priority",
354+
"drawer.taskPriorityChoose": "Choose priority",
355+
"drawer.taskPriorityClear": "No priority",
353356
"drawer.taskDeferUntil": "Defer until",
354357
"drawer.taskDetails": "Todo details",
355358
"drawer.taskInfo": "Task information",
@@ -1432,6 +1435,9 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
14321435
"drawer.taskDeferPlaceholder": "例如 resume_at:2026-09-14T09:00:00+08:00",
14331436
"drawer.taskDeferReview": "检查暂缓",
14341437
"drawer.taskDeferSupported": "支持 todo_done、pr_merged、capacity_available 与带时区的 resume_at 条件。",
1438+
"drawer.taskPriority": "优先级",
1439+
"drawer.taskPriorityChoose": "选择优先级",
1440+
"drawer.taskPriorityClear": "不设优先级",
14351441
"drawer.taskDeferUntil": "暂缓至",
14361442
"drawer.taskDetails": "Todo 详情",
14371443
"drawer.taskInfo": "任务信息",

‎docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2759,6 +2759,15 @@ The typed TypeScript `projection_delivery` union also owns the distinction
27592759
between mutation intent (`pending`/`not_required`) and provider readback
27602760
(`delivered`/`current`); unknown states fail closed before acknowledgement.
27612761

2762+
Priority intent now follows the same admitted create/update transaction on File,
2763+
SQLite and PostgreSQL. Explicit set/clear, omission and conflicting legacy text
2764+
are resolved by `todos/priority.ts`; Python reads share the generated grammar.
2765+
Markdown remains compatible display, while native records retain matching
2766+
priority/title. CLI and reviewed Chat edits preserve CAS and historical retry
2767+
identity. Real backend readback and a disposable clone of a long-lived local
2768+
Goal qualify this bounded change. See the [caller contract](../../project-agent-todo-contract.md#priority-intent).
2769+
This does not change provider defaults or close the remaining promotion gates.
2770+
27622771
Presentation is canonical at the projection layer, not in the domain record.
27632772
`source_section` and `index` are the v0 wire shape's display coordinates, while
27642773
native records derive the same display section from role/archive state and use

‎docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,6 +2188,13 @@ Python 生产 caller 直接从 `todos/todo_semantics.py` 导入;`todos/project
21882188
mutation intent(`pending`/`not_required`)与 provider readback(`delivered`/`current`);
21892189
未知状态在 acknowledgement 之前 fail closed。
21902190

2191+
优先级意图现接入 File、SQLite、PostgreSQL 既有的准入 create/update 事务。
2192+
显式设置/清除、参数缺省及与旧文字前缀的冲突由 `todos/priority.ts` 处理,Python
2193+
读取共享生成的语法。Markdown 保留兼容展示,native record 保存一致的 priority/title。
2194+
CLI 与经过审阅的 Chat 编辑保留 CAS 和历史重试身份。真实后端回读及长期本地 Goal 的
2195+
一次性隔离副本验证这条边界,见[调用合同](../../project-agent-todo-contract.md#priority-intent)。
2196+
这不改变 provider 默认,也不关闭其余 promotion 门禁。
2197+
21912198
展示语义属于 projection 层,而不是 domain record。`source_section` 与 `index` 是 v0
21922199
wire shape 的展示坐标;native record 根据 role/archive state 推导相同的展示 section,
21932200
并以时间戳和 Todo identity 做确定性回退,不制造假的持久 index。因此即使 wire shape

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.md‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,23 @@ remaining lifecycle writers have migrated.
5858

5959
The same stage also removes duplicated Python read policy around that boundary.
6060
Task-class resolution, title-aware actionability, dependency readiness, agent
61-
eligibility, priority ordering, and canonical Todo read records now have one
62-
Python semantic owner while TypeScript remains the transaction owner. The old
61+
eligibility and canonical Todo read records have one Python semantic owner
62+
while TypeScript remains the transaction owner. Priority authoring and ordering
63+
now share `todos/priority.ts`; Python compatibility readers consume its generated
64+
vocabulary and legacy grammar instead of maintaining independent patterns. The old
6365
projection module is an import-only compatibility facade. This keeps the
6466
replacement-first rule intact: compatibility remains available, but it cannot
6567
silently become a second semantic implementation.
6668

69+
The priority-intent slice connects CLI add/update/clear, reviewed Chat edits and
70+
the Dashboard selector to the existing typed Todo transaction. Text-only edits
71+
preserve priority; conflicting declarations fail before writes. P3/P4 ordering,
72+
legacy decorated labels and successor inheritance share the same owner. See the
73+
[caller contract](../../project-agent-todo-contract.md#priority-intent). Real CLI
74+
File/SQLite readback, isolated PostgreSQL and the shared complex fixture qualify
75+
this boundary. It retires duplicate priority knowledge, not the remaining T1/T3
76+
callers, Python compatibility IO or the D1–D3 default-cutover gates.
77+
6778
Native update now composes `todos/public_update.ts` for a bounded nonterminal
6879
planning intent (status, evidence/reason, resume/clear and successor links),
6980
against the same complete canonical head used for authority checks and CAS.

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,19 @@ fixture 覆盖。这是一个已完成的交付阶段,不代表 Markdown 晋
3737
lifecycle writer 已全部迁移。
3838

3939
同一阶段也删除了该边界周围重复的 Python read policy。task-class 解析、识别 title
40-
的 actionable 判断、依赖就绪、Agent eligibility、priority 排序和 canonical Todo
41-
read record 现在只有一个 Python 语义 owner,而 TypeScript 仍是事务 owner。旧
40+
的 actionable 判断、依赖就绪、Agent eligibility 和 canonical Todo read record
41+
只有一个 Python 语义 owner,TypeScript 仍是事务 owner。优先级写入意图与排序现共用
42+
`todos/priority.ts`;Python 兼容读取使用生成的词表和旧格式语法,不再独立维护模式。旧
4243
projection 模块只保留 import-only 兼容 facade。这样继续遵守 replacement-first:
4344
兼容路径仍可用,但不能静默形成第二份语义实现。
4445

46+
优先级意图这一批将 CLI add/update/clear、经过审阅的 Chat 编辑和 Dashboard 选择器
47+
接入既有 typed Todo 事务。仅改文字保留优先级,冲突声明在写入前拒绝。P3/P4 排序、
48+
历史装饰标签和 successor 继承使用同一 owner。见
49+
[调用合同](../../project-agent-todo-contract.md#priority-intent)。真实 CLI File/SQLite
50+
回读、隔离 PostgreSQL 和共享复杂 fixture 验证这条边界。本批删除重复的优先级知识,
51+
不代表其余 T1/T3 caller、Python 兼容 IO 或 D1–D3 默认切换门禁已经完成。
52+
4553
Native update 通过 `todos/public_update.ts` 组合非终态 planning intent,在同一份
4654
完整 canonical head 上校验权限并 CAS。独立 intent 命名空间保留 text/note 限制
4755
及旧回执指纹。v2 transport 携带 lifecycle grant、authority reason、registry 来源

‎docs/project-agent-todo-contract.md‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,45 @@ continue it; when it needs to fan out, split, supersede, or create successor
2727
work, it writes the new task through the LoopX todo lifecycle and lets the board
2828
sync catch up.
2929

30+
## Priority intent
31+
32+
Priority is an explicit scheduling field, with `P0` highest and `P4` lowest.
33+
Create or edit it independently of the task description:
34+
35+
```bash
36+
loopx todo add --goal-id <goal-id> --role agent --priority P1 --text '<agent action>'
37+
loopx todo update --goal-id <goal-id> --todo-id <todo-id> --priority P3
38+
loopx todo update --goal-id <goal-id> --todo-id <todo-id> --clear-priority
39+
```
40+
41+
Pass the registered `--agent-id` on updates when the Goal has multiple Agents.
42+
The existing authoring, ownership, lease and review requirements still apply.
43+
Chat `todo.create` accepts `priority`; reviewed `todo.update` edits accept
44+
`priority` or `clear_priority`. The task management panel uses that reviewed
45+
update path for its priority selector. Priority never grants eligibility,
46+
capabilities, a claim or quota; missing priority is allowed and sorts after P4.
47+
The generated agent authoring hint shows an explicit P1 example, not a global
48+
default or a prerequisite for same-turn binding.
49+
50+
Omitting priority from an ordinary text edit preserves the current priority.
51+
Use `--clear-priority` to remove it. Legacy `--text '[P2] Task'` remains supported;
52+
a supplied prefix edits priority for old callers. An explicit parameter and a
53+
conflicting prefix are rejected before writing, as are simultaneous set/clear
54+
instructions. Words such as `P0` inside ordinary prose have no scheduling meaning.
55+
Historical decorated prefixes such as `[P2-review]` read as P2; an edit renders
56+
the normalized `[P2]` prefix. P3/P4 participate in ordering and repair suggestions
57+
without being silently promoted to P1. Existing repair-suggestion and historical
58+
event-replay defaults retain their own contracts; they do not impose a default
59+
on new unprioritized Todos.
60+
61+
`todos/priority.ts` owns authoring intent and ordering. The generated coordination
62+
contract supplies its vocabulary and legacy grammar to Python read adapters.
63+
Markdown keeps the compatible `[Pn] description` display; native authority records
64+
also carry canonical priority/title. File, SQLite and PostgreSQL updates use the
65+
existing admitted head, CAS and operation receipt. A retry retains the original
66+
intent identity and returns its historical result; changing priority under the
67+
same operation ID is a conflict. No provider promotion or receipt rewrite occurs.
68+
3069
## Write Contract
3170

3271
For a caller-owned runtime that already registered its Goal/Agent, generate the

‎examples/personal-workspace-browser/typed-actions.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,6 +1637,16 @@ export const typedActionsScenario = {
16371637
await taskRow.click();
16381638
taskManagement = page.locator("details.personal-task-management");
16391639
await taskManagement.locator("summary").click();
1640+
await page.getByLabel("优先级", {exact: true}).selectOption("P4");
1641+
await page.screenshot({path: resolve(outputDir, "todo-priority-edit.png"), fullPage: false, animations: "disabled"});
1642+
await taskManagement.locator("label", {has: page.getByLabel("优先级", {exact: true})}).getByRole("button").click();
1643+
await page.getByText("确认执行").waitFor({state: "visible"});
1644+
const priorityEdit = api.actionPreviews.findLast(preview => preview.action_kind === "todo.update" && preview.normalized_parameters.priority === "P4");
1645+
if (!priorityEdit || priorityEdit.normalized_parameters.text !== undefined) throw new Error("Priority edit must be structured, without a text rewrite");
1646+
await page.getByRole("button", {name: "关闭", exact: true}).click();
1647+
await taskRow.click();
1648+
taskManagement = page.locator("details.personal-task-management");
1649+
await taskManagement.locator("summary").click();
16401650
await page.getByLabel("Todo 暂缓恢复条件").fill("pr_merged:huangruiteng/loopx#3399");
16411651
await page.screenshot({ path: resolve(outputDir, "todo-defer-resume-condition.png"), fullPage: false, animations: "disabled" });
16421652
await taskManagement.locator(".personal-inline-resume-when").getByRole("button", { name: "检查暂缓" }).click();

‎examples/project/project-agent-adoption-smoke.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def main() -> int:
256256
assert approved_quota["todo_write_hint"]["agent_todo_command_template"].startswith(
257257
f"loopx todo add --goal-id {GOAL_ID} --role agent "
258258
), approved_quota
259+
assert "--priority P1" in approved_quota["todo_write_hint"]["agent_todo_command_template"], approved_quota
259260

260261
packet = run_cli(root, registry_path, "review-packet", "--goal-id", GOAL_ID, "--scan-root", str(project))
261262
assert packet["ok"] is True, packet

0 commit comments

Comments
 (0)