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
8 changes: 4 additions & 4 deletions examples/shared-goal-authority-e2e/mutants.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def command(self) -> list[str]:

CASES = [
Case('todo_global_gate_inferred', (('loopx/control_plane/todos/authoring_scope.ts', replacement(
'intent.global_gate ? true : todo.global_gate',
'(intent.global_gate || intent.goal_bound) ? true : todo.global_gate')),),
' : todo.global_gate as boolean | null ?? null;',
' : (todo.global_gate || intent.goal_bound) as boolean | null ?? null;')),),
'tests/control_plane_ts/todo_authoring_scope.test.ts', 'global blocking is never inferred'),
Case('todo_explicit_scope_overwritten', (('loopx/control_plane/todos/authoring_scope.ts', replacement(
'if (requestedBound) fail(', 'if (false) fail(')),),
Expand Down Expand Up @@ -96,8 +96,8 @@ def command(self) -> list[str]:
' if (todo.claimed_by === null || todo.claimed_by !== actor) return "claim_owner_mismatch";')),),
'tests/control_plane/test_shadow_observable_native_e2e.py::test_native_unclaimed_edit_and_explicit_note_clear[disabled]'),
Case('native_diagnostic_truncated', ((COORDINATION + 'todo_update.ts', replacement(
'return failure("update_owner_mismatch", "Todo update cannot edit another claim owner\'s work");',
'return failure("update_owner_mismatch", "Update rejected");')),),
'? "Todo update cannot edit another claim owner\'s work"',
'? "Update rejected"')),),
'tests/control_plane/test_shadow_observable_native_e2e.py::test_canonical_argument_intent_and_atomic_claim[disabled]'),
Case('cursor_baseline_digest', ((COORDINATION + 'local_authority_shadow_adapter.py', replacement(
' return None if marker is None else marker["partition_digest"]',
Expand Down
10 changes: 8 additions & 2 deletions loopx/control_plane/coordination/todo_lifecycle_decision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,14 @@
| CoordinationTodoTerminalDecisionResult {
const { todo, actor_agent_id: actor, registered_agents: registered } = request;
if (registered.length <= 1) {
if (actor !== null && registered.length > 0 && !registered.includes(actor)) {
return result("rejected", "actor_not_registered");
if (actor !== null) {
const rejection = registeredTodoMutationRejection(todo, actor, registered);
if (rejection !== null) return result("rejected", rejection);
} else if (todo.claimed_by !== null || todo.bound_agent !== null ||
todo.blocks_agent !== null || todo.excluded_agents.length > 0) {
// Actorless compatibility is limited to genuinely unowned Todo records;

Check warning on line 373 in loopx/control_plane/coordination/todo_lifecycle_decision.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=AaCah_0bY9lWA-gfgcUU&open=AaCah_0bY9lWA-gfgcUU&pullRequest=4289
// ownership, binding, and exclusion facts require an accountable actor.
return result("rejected", "actor_required");
}
return { mode: "single_agent_compatibility", ownershipGate: "not_required" };
}
Expand Down
73 changes: 57 additions & 16 deletions loopx/control_plane/coordination/todo_update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
import { normalizeRegisteredTodoAgents, normalizeTodoAgent } from "./todo_agents.ts";

import { evaluateCoordinationTerminalFence, COORDINATION_TERMINAL_FENCE_REQUEST_SCHEMA,
registeredTodoMutationRejection }
evaluateCoordinationTodoMutationDecision,
COORDINATION_TODO_MUTATION_DECISION_REQUEST_SCHEMA }
from "./todo_lifecycle_decision.ts";
import { leaseEpoch } from "../work_items/task_lease_acquire.ts";
import { parseIsoTimestamp } from "../runtime_timestamp.ts";
import { normalizeNativePlanningIntent, planNativeTodoUpdate } from "../todos/native_update_plan.ts";
import {CoordinationCommandReceipt} from "./command_receipt.ts";
import { CoordinationCommandReceipt } from "./command_receipt.ts";

export const COORDINATION_TODO_UPDATE_REQUEST_SCHEMA =
"loopx_local_coordination_todo_update_request_v0";
Expand Down Expand Up @@ -167,25 +168,54 @@
if (todo.archive_state !== "active") {
return failure("todo_archived", "Todo update requires an active Todo");
}
if (todo.role !== "agent" || todo.status === "done") {
if (todo.status === "done") {
return failure("unsupported_todo_update_target",
"native metadata update currently requires a non-completed agent Todo");
}
const actorRejection = registeredTodoMutationRejection(todo, input.actor_agent_id, input.registered_agents);
if (actorRejection !== null) {
if (actorRejection === "claim_owner_mismatch") {
// Keep the public adapter's stable diagnostic while the typed predicate
// remains provider-neutral and reusable by lifecycle admission.
return failure("update_owner_mismatch", "Todo update cannot edit another claim owner's work");
}
return failure(actorRejection,
"Todo update requires a registered, non-excluded actor within the existing owner/binding scope");
"native metadata update cannot complete a Todo; use the terminal lifecycle command");
}
const lease = leases.get(input.todo_id);
const mode = head.handoff_mode === undefined ? "legacy" : head.handoff_mode;
if (typeof mode !== "string" || !["legacy", "soft_claim", "hard_lease"].includes(mode)) {
return failure("invalid_handoff_mode", "canonical handoff mode is invalid");
}
const intent = input.planning_intent ?? {};
const ownershipMutation = ["claimed_by", "clear_claim", "excluded_agents", "bound_agent",
"goal_bound", "blocks_agent", "clear_blocks_agent", "global_gate", "clear_global_gate"]
.some(field => Object.hasOwn(intent, field));
const authorityDecision = evaluateCoordinationTodoMutationDecision({
schema_version: COORDINATION_TODO_MUTATION_DECISION_REQUEST_SCHEMA,
command: "update", handoff_mode: mode, registered_agents: input.registered_agents,
lifecycle_grants: [], authority_action: "update",
actor_agent_id: input.actor_agent_id,
requested_claimed_by: intent.claimed_by ?? null,
clear_claim: intent.clear_claim === true,
ownership_mutation: ownershipMutation,
todo: {
...todo, role: todo.role, status: todo.status,
excluded_agents: todo.excluded_agents ?? [],
required_decision_scopes: todo.required_decision_scopes ?? [],
},
});
if (authorityDecision.outcome !== "apply") {
const decisionCode = String(authorityDecision.code ?? "mutation_rejected");

Check warning on line 199 in loopx/control_plane/coordination/todo_update.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'authorityDecision.code ?? "mutation_rejected"' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCWfbrxi2WmFx-_crks&open=AaCWfbrxi2WmFx-_crks&pullRequest=4289
const code = decisionCode === "claim_owner_mismatch"
? "update_owner_mismatch" : decisionCode;
// Keep the public owner-mismatch diagnostic stable while other shared
// admission failures use a provider-neutral explanation.
const reason = code === "update_owner_mismatch"
? "Todo update cannot edit another claim owner's work"
: "Todo update is outside the actor's registered owner/binding scope";
return failure(code, reason);
}
// Preserve the single-agent compatibility path only for genuinely
// unowned work. An empty registry is not evidence that an arbitrary actor
// may rewrite an already-owned Todo.

Check warning on line 211 in loopx/control_plane/coordination/todo_update.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=AaCWfbrxi2WmFx-_crkt&open=AaCWfbrxi2WmFx-_crkt&pullRequest=4289
if (input.registered_agents.length === 0 && input.actor_agent_id !== null) {
return failure("actor_not_registered", "Todo update requires a registered actor");
}
if (input.actor_agent_id === null && (todo.claimed_by !== undefined ||
todo.bound_agent !== undefined || todo.blocks_agent !== undefined)) {
return failure("actor_required", "owned or bound Todo updates require an actor");
}
// A retained lease, even expired/released, has execution lineage. Ownership
// and exclusions must not change beneath it through a metadata operation.
if ((lease !== undefined || mode === "hard_lease") && TODO_OWNERSHIP_INTENT_FIELDS.some(field =>
Expand Down Expand Up @@ -259,7 +289,10 @@
const updates = planNativeTodoUpdate(todo, input.planning_intent!, head,
input.actor_agent_id, input.registered_agents, String(next.updated_at));
for (const [field, value] of Object.entries(updates)) {
if (value === null) { delete next[field]; clearFields.add(field); }
// Markdown compatibility omits empty scalar metadata. Treat an
// explicit empty planning scalar as a clear in the canonical record as
// well; omission and clear are no longer conflated by the planner.
if (value === null || value === "") { delete next[field]; clearFields.add(field); }
else next[field] = value;
}
next.done = next.status === "done" || next.status === "deferred";
Expand All @@ -285,7 +318,7 @@
}

/** Update mutable Todo metadata from the canonical provider head. */
export async function executeCoordinationTodoUpdate(

Check failure on line 321 in loopx/control_plane/coordination/todo_update.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCah_z4Y9lWA-gfgcUT&open=AaCah_z4Y9lWA-gfgcUT&pullRequest=4289
store: AuthorityStore, rawInput: CoordinationTodoUpdateInput,
): Promise<CoordinationTodoUpdateResult> {
let input: CoordinationTodoUpdateInput;
Expand All @@ -297,7 +330,15 @@
const receipt = updateReceipt(input, requestSha);
const replay = await receipt.read(store);
if (replay !== null) return replay;
if (input.actor_agent_id === null || !input.registered_agents.includes(input.actor_agent_id)) {
// Legacy single-agent callers historically omitted actor_agent_id for an
// unowned Todo. Keep that narrow compatibility path, while retaining the

Check warning on line 334 in loopx/control_plane/coordination/todo_update.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=AaCWfbrxi2WmFx-_crku&open=AaCWfbrxi2WmFx-_crku&pullRequest=4289
// registered-actor requirement for multi-agent or explicitly-owned work.
if (input.actor_agent_id === null) {
if (input.registered_agents.length > 1) {
return failure("actor_not_registered", "Todo update requires a registered actor");
}
} else if (input.registered_agents.length === 0 ||
!input.registered_agents.includes(input.actor_agent_id)) {
return failure("actor_not_registered", "Todo update requires a registered actor");
}
const head = await store.loadAuthority();
Expand Down
6 changes: 5 additions & 1 deletion loopx/control_plane/todos/authoring_scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@
}
const creating = command === "create";
let blocks = intent.clear_blocks_agent ? null : requestedBlocks || string(todo.blocks_agent, "blocks_agent");
const global = intent.clear_global_gate ? null : intent.global_gate ? true : todo.global_gate as boolean | null ?? null;
const global = intent.clear_global_gate
? null
: Object.hasOwn(intent, "global_gate")
? intent.global_gate === true
: todo.global_gate as boolean | null ?? null;

Check warning on line 133 in loopx/control_plane/todos/authoring_scope.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCWfbqZi2WmFx-_crkr&open=AaCWfbqZi2WmFx-_crkr&pullRequest=4289
let bound = requestedBound || (intent.goal_bound ? null : string(todo.bound_agent, "bound_agent"));
let goal = intent.goal_bound ? true : requestedBound ? false : todo.goal_bound as boolean | null ?? null;
// Gate scope can determine continuation scope, but never overwrite a
Expand Down
26 changes: 21 additions & 5 deletions loopx/control_plane/todos/field_update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ function bindingUpdates(block: JsonObject, intent: JsonObject, todoId: string):
else if (intent.clear_blocks_agent) updates.blocks_agent = null;
if (present(intent.excluded_agents)) updates.excluded_agents = intent.excluded_agents;
if (intent.clear_global_gate) updates.global_gate = null;
else if (present(intent.global_gate)) updates.global_gate = intent.global_gate;
else if (Object.hasOwn(intent, "global_gate")) {
// The public record is presence-based: false means the gate is cleared,
// never a second persisted state that can shadow a scoped gate.
updates.global_gate = intent.global_gate === true ? true : null;
}
return updates;
}

Expand Down Expand Up @@ -165,10 +169,6 @@ export function planTodoFieldUpdate(value: unknown): TodoFieldUpdatePlan {
const updates: JsonObject = {todo_id: todoId, status: targetStatus};
if (normalizedStatus === "done" && !block.completed_at) updates.completed_at = updatedAt;
else if (normalizedStatus && normalizedStatus !== "done") updates.completed_at = null;
// The public editing contract distinguishes omitted/empty text metadata from
// present collections and booleans. Never turn [] or false into omission.
for (const field of STRING_FIELDS) if (intent[field]) updates[field] = intent[field];
for (const field of PRESENT_FIELDS) if (present(intent[field])) updates[field] = intent[field];
Object.assign(updates, bindingUpdates(block, intent, todoId));
if (intent.unblocks_todo_id) updates.unblocks_todo_id = intent.unblocks_todo_id;
if (present(intent.successor_todo_ids)) updates.successor_todo_ids = intent.successor_todo_ids;
Expand All @@ -182,6 +182,22 @@ export function planTodoFieldUpdate(value: unknown): TodoFieldUpdatePlan {
}
if (present(intent.no_followup)) updates.no_followup = intent.no_followup;
Object.assign(updates, completionUpdates(block, intent, targetStatus, normalizedStatus));
// Presence, rather than truthiness, is the mutation contract. An explicitly
// empty scalar clears the compatibility field; omitted values remain
// untouched. This fixes the old `if (intent[field])` conflation of omission
// and an intentional clear.
for (const field of STRING_FIELDS) {
if (Object.hasOwn(intent, field)) {
// `note` is a display annotation whose historical empty-input contract
// is omission/preservation. Other scalar metadata uses empty text as an
// explicit clear once it crosses this typed boundary.
if (field === "note" && typeof intent[field] === "string" && !intent[field].trim()) continue;
updates[field] = intent[field];
}
}
for (const field of PRESENT_FIELDS) {
if (Object.hasOwn(intent, field)) updates[field] = intent[field];
}
// Public update carries the effective scope and raw observation once. The
// field plan composes validation and generation without another RPC.
const monitorPlan = request.monitor_context == null ? null : planMonitorMetadata({
Expand Down
18 changes: 14 additions & 4 deletions loopx/control_plane/todos/line_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,7 @@ def apply_todo_update_to_lines(
if public_context is not None:
public_context = {**public_context, "items": todo_update_snapshot(lines)
if resume_when or block.get("resume_when") else []}
plan = _field_update_plan(
{**block, "role": resolved_role},
{
raw_intent = {
"status": status,
"note": note,
"evidence": evidence,
Expand Down Expand Up @@ -327,7 +325,19 @@ def apply_todo_update_to_lines(
"monitor_metadata": monitor_metadata,
"clear_claim": clear_claim,
"claim_only": claim_only,
},
}
# Python's compatibility API uses None (and blank note text) for
# omission. Strip those sentinels before crossing the typed planner; an
# actual empty scalar such as reason="" remains an explicit clear.
intent = {
key: value for key, value in raw_intent.items()
if value is not None and not (
key == "note" and isinstance(value, str) and not value.strip()
)
}
plan = _field_update_plan(
{**block, "role": resolved_role},
intent,
updated_at,
monitor_context,
public_context,
Expand Down
24 changes: 21 additions & 3 deletions loopx/control_plane/todos/mutation_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@
return dict(mutation_authority)


def authorize_todo_lifecycle_mutation(

Check failure on line 276 in loopx/control_plane/todos/mutation_authority.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 25 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCah_u4Y9lWA-gfgcUR&open=AaCah_u4Y9lWA-gfgcUR&pullRequest=4289
*,
registry_path: Path,
goal_id: str,
Expand All @@ -298,13 +298,31 @@
infer_status_from_done=True,
)
requested_owner = normalize_todo_claimed_by(requested_claimed_by)
# Legacy single-agent callers historically omitted ``agent_id`` while the
# Todo itself carried the sole registered owner/binding. Attribute those

Check warning on line 302 in loopx/control_plane/todos/mutation_authority.py

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=AaCah_u4Y9lWA-gfgcUS&open=AaCah_u4Y9lWA-gfgcUS&pullRequest=4289
# calls to that unambiguous owner before entering the shared decision
# function; genuinely unowned actorless records remain compatibility-safe.
if normalized_actor is None and len(registered_agents) == 1:
sole_agent = registered_agents[0]
bound_agent = core_todo.bound_agent or (
core_todo.blocks_agent if core_todo.role == "user" else None
)
if requested_owner == sole_agent or core_todo.claimed_by == sole_agent or bound_agent == sole_agent:
normalized_actor = sole_agent
# A pre-coordination goal has no declared registration list, but explicit
# legacy callers still supplied an actor. Preserve that behavior without
# weakening the shared ownership/exclusion checks: the supplied actor is
# the only admissible peer for this decision.
decision_registered_agents = registered_agents or (
[normalized_actor] if normalized_actor is not None else []
)
effective_action = str(authority_action or command).strip().lower()
if command == "claim":
return _authorize_typescript_claim(
goal_id=goal_id,
todo=todo,
core_todo=core_todo,
registered_agents=registered_agents,
registered_agents=decision_registered_agents,
actor=normalized_actor,
requested_owner=requested_owner,
)
Expand All @@ -322,7 +340,7 @@
decision_outcome=normalized_decision_outcome,
)
core_snapshot = CoordinationSnapshot(
registered_agents=tuple(registered_agents),
registered_agents=tuple(decision_registered_agents),
todo=core_todo,
decision_target=core_target,
)
Expand All @@ -334,7 +352,7 @@
coordination.get("todo_lifecycle_authority")
if isinstance(coordination, Mapping)
else None,
registered_agents=registered_agents,
registered_agents=decision_registered_agents,
)
plan = decide(
CoordinationSnapshot(
Expand Down
19 changes: 13 additions & 6 deletions loopx/control_plane/todos/native_update_plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import { planPublicTodoUpdate, TODO_PUBLIC_UPDATE_REQUEST_SCHEMA } from "./publi
import { normalizeTodoWorkRequirements, TODO_WORK_REQUIREMENT_FIELDS } from "./work_requirements.ts";
import {normalizeTodoOwnershipIntent, TODO_OWNERSHIP_INTENT_FIELDS} from "./authoring_scope.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", ...TODO_WORK_REQUIREMENT_FIELDS,
...TODO_OWNERSHIP_INTENT_FIELDS]);
const STRINGS = new Set(["status", "evidence", "reason", "task_class", "continuation_policy",
"resume_when", "unblocks_todo_id", "bound_agent", "blocks_agent"]);
const BOOLEANS = new Set(["clear_resume_when", "no_followup", "goal_bound", "clear_blocks_agent",
"global_gate", "clear_global_gate"]);
const FIELDS = new Set([...STRINGS, ...BOOLEANS, "successor_todo_ids",
...TODO_WORK_REQUIREMENT_FIELDS, ...TODO_OWNERSHIP_INTENT_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 @@ -24,11 +26,16 @@ export function normalizeNativePlanningIntent(value: unknown): JsonObject {
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 ((TODO_OWNERSHIP_INTENT_FIELDS as readonly string[]).includes(field)) continue;
if (value === null) continue;
if (value === null) {
// Null is an explicit clear for scalar planning metadata. Ownership
// fields use their dedicated clear switches and are normalized above.
if (STRINGS.has(field)) intent[field] = null;
continue;
}
if (STRINGS.has(field)) {
if (typeof value !== "string") throw new AuthorityStoreProtocolError(`${field} must be a string`);
const text = compactPythonWhitespace(value);
if (text) intent[field] = field === "unblocks_todo_id" ? normalizeTodoId(text, field) : text;
intent[field] = field === "unblocks_todo_id" && text ? normalizeTodoId(text, field) : text;
} else if (BOOLEANS.has(field)) {
if (typeof value !== "boolean") throw new AuthorityStoreProtocolError(`${field} must be a boolean`);
if (field !== "clear_resume_when" || value) intent[field] = value;
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/todos/provider_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def update_canonical_todo_if_promoted(
)
return settle_canonical_todo_projection(
{"ok": True, "goal_id": goal_id, "todo_id": todo_id,
"role": "agent", "dry_run": dry_run, **result},
"role": role, "dry_run": dry_run, **result},
registry_path=registry_path, runtime_root=runtime_root, goal_id=goal_id,
project=project, state_file=state_file,
)
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 @@ -61,7 +61,11 @@ export function planPublicTodoUpdate(value: unknown): JsonObject {
const role = context.role;
const effectiveIntent = {...intent,
bound_agent: role === "user" ? scope.bound_agent : null,
goal_bound: role === "user" && scope.goal_bound ? true : null,
// Carry an explicit false when a gate changes from goal-wide to an
// agent-bound continuation; omission would leave stale goal_bound=true in
// the canonical record.
goal_bound: role === "user" && scope.goal_bound !== null
? scope.goal_bound : null,
clear_user_binding: scope.clear_user_binding,
resume_when: scope.normalized_resume_when,
resume_monitor_generation: metadata?.resume_monitor_generation ?? null};
Expand Down
Loading