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
Expand Up @@ -2698,6 +2698,13 @@ lease status changes and Monitor planning/effects remain unsupported. 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.
The same transaction now accepts bounded work-requirement declarations through
the shared public TS planner (field list and intentional rejection changes are
in T1). File, NoKV, SQLite and PostgreSQL conformance exercise aliases, explicit
clear, replay after a later edit, invalid-input atomicity and lease rejection.
The production-scale fixture carries requirements across unrelated lifecycle
operations. This does not qualify a new profile, widen an execution grant, or
change D1–D3/promotion holds; Markdown remains an independent permanent projection.
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2125,6 +2125,11 @@ wire,不改变 provider 默认或 promotion。这仍是有界的非 terminal p
不是通用 native metadata 支持;Active lease 下的状态变化及 Monitor 规划/effect 仍不
支持。准入结果和 lease-fence 结果都不是 commit receipt;兑现删除收益时,provider
CAS/replay 与既有 writer 持锁生命周期不变。
同一事务现通过共享公开 TS planner 接受有界工作要求声明,字段清单和有意拒绝变化见
T1。File、NoKV、SQLite 与 PostgreSQL conformance 覆盖别名、显式清空、后续编辑后
的旧操作重放、非法输入原子性和 lease 拒绝;复杂容量 fixture 携带工作要求验证其他
lifecycle 操作不会丢字段。这不资格化新 profile、不扩大 execution grant,也不改变
D1–D3/promotion hold;Markdown 继续作为独立的永久投影。
等待/恢复 lane 选择现由 quota、vision-wait、agent-scope、replan 共用一个 TS 读取
策略 owner,删除旧 Python selector 模块。适配层在 promotion 后消费同一 canonical
summary,之前消费 legacy summary;真实 CLI 覆盖容量变化和 promoted display
Expand Down
21 changes: 21 additions & 0 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,27 @@ atomic follow-up are not fully closed. Lease-edit PR #4152 is merged; bounded
planning updates now reuse that fence and the existing CAS/receipt transaction.
Continue with the remaining field/effect inventory, not another update engine.

Work-requirement editing is now closed for non-Monitor Agent Todos without a
retained lease: `action_kind`, `task_domain`, `task_repository`,
`required_write_scopes`, `required_capabilities`, `target_capabilities` and
`explore_result_node_refs` use the existing v1 planning transaction. Public
legacy edits and native planning share `todos/work_requirements.ts`; Monitor
successor authoring and receipt verification reuse its repository/capability
codecs instead of retaining scheduler-owned copies. No new RPC or store is added.
Omitted/blank scalar input preserves state; explicit empty collections clear
requirements. Deliberate correction: invalid members, unsafe repository routes
and over-capacity Explore references reject the whole public update rather than
silently dropping requirements or truncating references.
SCP-style password-bearing userinfo is rejected too, including Monitor successor
routes; username-only Git transports remain valid. Unrelated historical
fields are not revalidated by a copy edit. Repository/capability aliases retain
one normalized replay identity. Requirements declare needed work, not a grant:
ownership, decision outcomes, generic raw patches, Monitor edits and leased
requirement changes remain fenced. The Python reader/bootstrap codec and legacy
writer still have real callers; this slice does not retire them or complete T1.
Next close ownership/decision metadata with their lifecycle admission and
validation effects, then the remaining leased Monitor transaction in T2.

- 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,21 @@ field codec 仍有真实 caller,不引入公开 update 限制。Native metadat
T2 原子后续动作尚未全部闭合。Lease-edit PR #4152 已合入;有界规划更新复用该
fence 及既有 CAS/receipt 事务。下一步继续剩余字段/effect 清单,不另建 update engine。

工作要求编辑现已闭合:没有保留 lease 的非 Monitor Agent Todo,可通过既有 v1
planning 事务更新 `action_kind`、`task_domain`、`task_repository`、
`required_write_scopes`、`required_capabilities`、`target_capabilities` 和
`explore_result_node_refs`。公开 legacy 编辑与 native planning 共用
`todos/work_requirements.ts`;Monitor successor authoring 与 receipt verification
复用其仓库/capability codec,删除 scheduler 私有副本,不增加 RPC 或 store。
省略/空白标量保留原值,显式空集合清除要求。有意修正:非法成员、不安全仓库和超出
容量的 Explore 引用使整笔公开更新拒绝,不再静默丢掉要求或截断引用;纯文案编辑不会
重新审查无关历史字段。SCP 风格的含密码 userinfo 同样拒绝,包括 Monitor 后继路由;
仅带用户名的 Git transport 仍合法。仓库/capability 别名保持同一规范化 replay identity。
要求不是授权:ownership、决策结果、任意 raw patch、Monitor 编辑及带 lease 的要求
变化仍受限。Python 读取/bootstrap codec 与 legacy writer 仍有真实调用者,本批
不退役它们,也不宣称完整 T1。下一步结合 lifecycle admission 与 validation effect
闭合 ownership/decision metadata,再推进 T2 剩余带 lease Monitor 事务。

- 复用现有 provider text/note 事务、lifecycle 准入、field-plan 和 completion
规则。先枚举公开 metadata 编辑与显式 clear,不把 `UPDATE_FIELDS` 扩成所有存储
字段,也不让 generic patch 获得 terminal transition 权限。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ machine-owned `Completed Work Archive` region (created when needed) and retain
their original `role`. Unknown canonical fields and unsafe region ownership
continue to fail closed.

For promoted provider-first Todo create, claim, and narrow text/note update,
For promoted provider-first Todo create, claim, and supported text/planning updates,
the committed authority journal is the transaction-bound projection outbox:
the canonical mutation, complete head, cursor, revision, and receipt land in
one provider transaction. After that commit, the Python compatibility adapter
Expand All @@ -209,6 +209,18 @@ evidence without reversing or hiding the canonical commit. A later successful
mutation or `todo project-markdown --execute` replays the current head
idempotently. This is projection recovery, not a second authority path.

Supported non-Monitor Agent updates include action/domain/repository and required
write scopes, required/target capabilities and Explore node references. These
declarations use the same canonical planning transaction, not a direct Markdown
edit. Invalid supplied members reject the entire update; empty collections clear
the declaration. They do not grant execution rights, change a lease, or approve
a User decision. Work-requirement edits with a retained lease remain unsupported.

非 Monitor Agent Todo 的 action/domain/repository、写入范围、required/target
capability 和 Explore 引用声明复用同一 canonical planning 事务,不直接编辑
Markdown。非法输入整笔拒绝,空集合明确清除;声明不授予执行权、不变更 lease,
也不批准 User 决策。带保留 lease 的工作要求编辑仍不支持。

### Generated display recovery / 生成式展示恢复

LoopX state documents are generated and maintained by Agents through LoopX.
Expand Down
2 changes: 1 addition & 1 deletion examples/shared-goal-authority-e2e/mutants.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def command(self) -> list[str]:
Case('todo_successor_scope_unbound', (('loopx/control_plane/todos/authoring_scope.ts', replacement(
'if (blocks && (goal || !bound || bound !== blocks)) return "agent_binding_conflict";', '')),),
'tests/control_plane_ts/todo_authoring_scope.test.ts', 'resolved successor scope'),
Case('monitor_route_drops_invalid_capability', (('loopx/control_plane/scheduler/monitor_successor.ts', replacement(
Case('monitor_route_drops_invalid_capability', (('loopx/control_plane/todos/work_requirements.ts', replacement(
' throw new EffectRuntimeRequestError(`${label} must contain public-safe capability tokens; invalid entries cannot be dropped`);',
' continue;')),),
'tests/control_plane_ts/monitor_successor.test.ts', 'invalid successor intent is rejected'),
Expand Down
6 changes: 6 additions & 0 deletions loopx/control_plane/coordination/todo_update.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { JsonObject } from "../effect_program.ts";
import { TODO_WORK_REQUIREMENT_FIELDS } from "../todos/work_requirements.ts";
import type { AuthorityStore, AuthorityStoreCommit, AuthorityStoreReceiptResult } from "./authority_store.ts";
import {
AuthorityStoreProtocolError,
Expand Down Expand Up @@ -230,6 +231,11 @@ function targetRejection(
return failure("update_owner_mismatch", "Leased Todo update requires the current claim owner");
}
const status = input.planning_intent?.status;
if (lease !== undefined && TODO_WORK_REQUIREMENT_FIELDS.some(field =>
Object.hasOwn(input.planning_intent ?? {}, field))) {
return failure("update_lease_requirements_transition_unsupported",
"Changing leased work requirements requires a new execution grant; metadata update leaves the lease unchanged");
}
if (lease !== undefined && typeof status === "string" && status.toLowerCase() !== todo.status) {
return failure("update_lease_status_transition_unsupported",
"Changing a leased Todo status requires an atomic lifecycle operation; planning update leaves the lease unchanged");
Expand Down
7 changes: 4 additions & 3 deletions loopx/control_plane/quota/monitor_poll_commit.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createHash } from "node:crypto";
import { access, readFile, rm } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { monitorSuccessorIntent, monitorSuccessorRoute, monitorSuccessorCapabilities } from "../scheduler/monitor_successor.ts";
import { monitorSuccessorIntent, monitorSuccessorRoute } from "../scheduler/monitor_successor.ts";
import { normalizeTodoCapabilities } from "../todos/work_requirements.ts";

import type { JsonObject } from "../effect_program.ts";
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
Expand Down Expand Up @@ -868,8 +869,8 @@ function requireProviderCapabilityMatch(
expected: readonly string[],
label: string,
): void {
const actualCapabilities = monitorSuccessorCapabilities(actual, label);
const expectedCapabilities = monitorSuccessorCapabilities(expected, label);
const actualCapabilities = normalizeTodoCapabilities(actual, label);
const expectedCapabilities = normalizeTodoCapabilities(expected, label);
if (pythonJson(actualCapabilities) !== pythonJson(expectedCapabilities)) {
throw new EffectRuntimeRequestError(`${label} must match provider plan`);
}
Expand Down
59 changes: 6 additions & 53 deletions loopx/control_plane/scheduler/monitor_successor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import { createHash } from "node:crypto";
import type { JsonObject } from "../effect_program.ts";
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import { optionalNonEmptyString, requireBoolean, requireJsonObject, requireStringArray } from "../runtime_decode.ts";
import { compactPythonWhitespace, normalizeTodoAgent, stripPythonWhitespace } from "../coordination/todo_agents.ts";
import { optionalNonEmptyString, requireBoolean, requireJsonObject } from "../runtime_decode.ts";
import { normalizeTodoAgent, stripPythonWhitespace } from "../coordination/todo_agents.ts";
import { normalizeTodoRepository, normalizeTodoCapabilities } from "../todos/work_requirements.ts";

export const MONITOR_SUCCESSOR_REQUEST_SCHEMA = "loopx_monitor_successor_plan_request_v0";
export const MONITOR_SUCCESSOR_RESULT_SCHEMA = "loopx_monitor_successor_plan_result_v0";
Expand Down Expand Up @@ -47,55 +48,7 @@
return raw === null ? null : stripPythonWhitespace(raw) || null;
}

// The node-independent repository/bootstrap codec remains in repository_identity.py.
// This pure transport codec is characterized against that public contract; do
// not use WHATWG's normalized pathname, which silently removes dot segments.
function repository(value: unknown): string | null {
let raw = text(value, "next_task_repository");
if (!raw) return null;
if (/[\\\s\u0000-\u001f\u007f]/u.test(raw)) {
throw new EffectRuntimeRequestError("--next-task-repository must be a credential-free Git remote without control characters or backslashes");
}
let host: string, path: string;
const canonical = /^git:([a-z0-9.-]+(?::[0-9]{1,5})?)\/([A-Za-z0-9._~+/-]+)$/.exec(raw);
if (canonical) [host, path] = [canonical[1], canonical[2]];
else {
const scp = /^(?:[^@/]+@)?([^:/]+):(.+)$/.exec(raw);
if (scp && !raw.includes("://")) raw = `ssh://${scp[1]}/${scp[2]}`;
try {
const url = new URL(raw);
if (!["git:", "http:", "https:", "ssh:"].includes(url.protocol) ||
!url.hostname || url.password || url.search || url.hash) throw new Error();
host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
const port = Number(url.port);
if (port && !((["http:", "git:"].includes(url.protocol) && port === 80) ||
(["https:", "ssh:"].includes(url.protocol) && [22, 443].includes(port)))) host += `:${port}`;
const pathMatch = /^[^:]+:\/\/[^/?#]*([^?#]*)/.exec(raw);
if (!pathMatch) throw new Error();
path = pathMatch[1];
} catch {
throw new EffectRuntimeRequestError("--next-task-repository must be a credential-free Git remote or canonical git:<host>/<path> identity");
}
}
path = path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
if (!/^[A-Za-z0-9._~+/-]+$/.test(path) || !/^[a-z0-9.-]+(?::[0-9]{1,5})?$/.test(host) ||
path.split("/").some(part => part === "." || part === "..")) {
throw new EffectRuntimeRequestError("--next-task-repository must include a safe repository path");
}
return `git:${host}/${path}`;
}

export function monitorSuccessorCapabilities(value: unknown, label: string): string[] {
const result: string[] = [];
for (const raw of requireStringArray(value ?? [], label)) {
const token = compactPythonWhitespace(raw).toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
if (!/^[a-z][a-z0-9_:-]{0,63}$/.test(token)) {
throw new EffectRuntimeRequestError(`${label} must contain public-safe capability tokens; invalid entries cannot be dropped`);
}
if (!result.includes(token)) result.push(token);
}
return result;
}
// Shared with public Todo metadata updates; no second route codec here.

Check warning on line 51 in loopx/control_plane/scheduler/monitor_successor.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCT1YLZQzi-Va3rZke7&open=AaCT1YLZQzi-Va3rZke7&pullRequest=4252

export interface MonitorSuccessorIntent extends JsonObject {
next_agent_todo: string | null;
Expand All @@ -121,8 +74,8 @@
const policy = text(input.next_continuation_policy, "next_continuation_policy")?.toLowerCase() ?? null;
const target = text(input.next_target_key, "next_target_key");
const claim = text(input.next_claimed_by, "next_claimed_by");
const repo = repository(input.next_task_repository);
const capabilities = monitorSuccessorCapabilities(input.next_required_capabilities, "--next-required-capability");
const repo = normalizeTodoRepository(input.next_task_repository, "--next-task-repository");
const capabilities = normalizeTodoCapabilities(input.next_required_capabilities, "--next-required-capability");
if (!agentTodo && (action || policy || target || claim || repo || capabilities.length)) {
throw new EffectRuntimeRequestError("monitor successor routing options require --next-agent-todo");
}
Expand Down
6 changes: 4 additions & 2 deletions loopx/control_plane/todos/native_update_plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { AuthorityStoreProtocolError } from "../coordination/authority_store_cod
import { compactPythonWhitespace } from "../coordination/todo_agents.ts";
import { normalizeTodoId } from "../work_items/task_lease_acquire.ts";
import { planPublicTodoUpdate, TODO_PUBLIC_UPDATE_REQUEST_SCHEMA } from "./public_update.ts";
import { normalizeTodoWorkRequirements, TODO_WORK_REQUIREMENT_FIELDS } from "./work_requirements.ts";

const STRINGS = new Set(["status", "evidence", "reason", "resume_when", "unblocks_todo_id"]);
const BOOLEANS = new Set(["clear_resume_when", "no_followup"]);
const FIELDS = new Set([...STRINGS, ...BOOLEANS, "successor_todo_ids"]);
const FIELDS = new Set([...STRINGS, ...BOOLEANS, "successor_todo_ids", ...TODO_WORK_REQUIREMENT_FIELDS]);

/** A separate intent namespace preserves the shipped text/note patch and its
* historical receipt encoding. Raw field patches do not gain new authority. */
Expand All @@ -19,6 +20,7 @@ export function normalizeNativePlanningIntent(value: unknown): JsonObject {
const intent: JsonObject = {};
for (const [field, value] of Object.entries(raw)) {
if (!FIELDS.has(field)) throw new AuthorityStoreProtocolError(`Todo planning update does not own ${field}`);
if ((TODO_WORK_REQUIREMENT_FIELDS as readonly string[]).includes(field)) continue;
if (value === null) continue;
if (STRINGS.has(field)) {
if (typeof value !== "string") throw new AuthorityStoreProtocolError(`${field} must be a string`);
Expand All @@ -32,7 +34,7 @@ export function normalizeNativePlanningIntent(value: unknown): JsonObject {
intent[field] = [...new Set(value.map(item => normalizeTodoId(item, "successor_todo_id")))];
}
}
return intent;
return {...intent, ...normalizeTodoWorkRequirements(raw)};
}

export function planNativeTodoUpdate(todo: JsonObject, intent: JsonObject,
Expand Down
6 changes: 5 additions & 1 deletion loopx/control_plane/todos/public_update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import { planTodoAuthoringScope, TODO_AUTHORING_SCOPE_REQUEST_SCHEMA } from "./authoring_scope.ts";
import { planTodoFieldUpdate, TODO_FIELD_UPDATE_REQUEST_SCHEMA } from "./field_update.ts";
import { planTodoExternalWaitTransition, TODO_EXTERNAL_WAIT_REQUEST_SCHEMA_VERSION } from "./resume_condition.ts";
import { normalizeTodoWorkRequirements, TODO_WORK_REQUIREMENT_FIELDS } from "./work_requirements.ts";

export const TODO_PUBLIC_UPDATE_REQUEST_SCHEMA = "todo_public_update_request_v0";

Expand Down Expand Up @@ -42,7 +43,10 @@ export function planPublicTodoUpdate(value: unknown): JsonObject {
throw new EffectRuntimeRequestError("public Todo update schema mismatch");
}
const todo = requireJsonObject(request.todo, "public Todo update source");
const intent = requireJsonObject(request.intent, "public Todo update intent");
const rawIntent = requireJsonObject(request.intent, "public Todo update intent");
const intent: JsonObject = {...rawIntent};
for (const field of TODO_WORK_REQUIREMENT_FIELDS) delete intent[field];
Object.assign(intent, normalizeTodoWorkRequirements(rawIntent));
const context = requireJsonObject(request.context, "public Todo update context");
const scope = planTodoAuthoringScope({schema_version: TODO_AUTHORING_SCOPE_REQUEST_SCHEMA,
command: "update", role: context.role, todo,
Expand Down
Loading