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
22 changes: 22 additions & 0 deletions docs/project-agent-todo-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,28 @@ loopx todo update \
workspace isolation, not write authority; claim/lease, capabilities, the goal
boundary, and repository policy continue to apply.

Completion validation follows the same routing. If `task_repository` differs
from the Goal repository, LoopX binds the caller-approved command to a
turn-bound delivery-workspace receipt (an existing writeback receipt or the
exact Turn's host-verified pre-completion snapshot) and executes it only from a
clean linked worktree whose canonical origin matches that identity. A missing receipt,
canonical checkout, dirty or deleted worktree, and repository mismatch all
fail closed before command execution with a path-free
`validation_blocked_completion` receipt. When no separate repository is
declared, validation keeps the Goal repository as its default workspace. The
CLI and managed Turn use this shared completion effect; frontend and Lark
consume the same receipt projection and do not own a second cwd setting.

完成校验遵循同一套路由规则。若 `task_repository` 与 Goal 仓库不同,LoopX
会把调用方预先声明的校验命令绑定到当前 Turn 的 delivery-workspace receipt(已有
writeback receipt,或同一 Turn 中 host 在完成前验证的 snapshot),且只在
canonical origin 匹配、状态干净的 linked worktree 中执行。receipt 缺失、使用
canonical checkout、worktree 脏或已删除、仓库身份不匹配时,系统都会在命令执行
前 fail closed,并返回不泄露本地路径的 `validation_blocked_completion` receipt。
未声明独立仓库时,仍以 Goal 仓库作为默认校验 workspace。CLI 与 managed Turn
共享同一个 completion effect;前端与 Lark 只消费同源 receipt 投影,不新增 cwd
配置源。

`quota should-run --agent-id <agent-id>` is the preflight for every peer. When
the selected task writes repository state and the peer is in a non-git,
unrelated, or non-isolated workspace, it returns `workspace_guard` and blocks
Expand Down
22 changes: 22 additions & 0 deletions loopx/cli_commands/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path

from ..control_plane.coordination.local_authority import read_canonical_todo_fields_if_promoted
from ..control_plane.agents.workspace_guard import capture_delivery_workspace
from ..control_plane.todos.contract import (
replan_successor_semantic_binding,
)
Expand Down Expand Up @@ -392,6 +393,7 @@ def handle_todo_command(
completion_error = None
completion_turn_key = None
completion_identity_source = None
completion_delivery_workspace = None
if getattr(args, "turn_instance_id", None):
runtime_root = resolve_runtime_root(
load_registry(registry_path),
Expand Down Expand Up @@ -458,6 +460,24 @@ def handle_todo_command(
}
completion_turn_key = identity.effect_id
completion_identity_source = "turn_settlement"
writeback_run = settlement_readback.writeback_run
if isinstance(writeback_run, dict) and isinstance(
writeback_run.get("delivery_workspace"), dict
):
completion_delivery_workspace = dict(
writeback_run["delivery_workspace"]
)
elif todo.get("task_repository"):
# Completion validation precedes accountable refresh, so
# the exact Turn can legitimately have no writeback row
# yet. Bind a freshly verified current-worktree snapshot
# to this already-read settlement identity rather than
# introducing an arbitrary cwd option or a circular gate.
completion_delivery_workspace = capture_delivery_workspace(
Path.cwd(),
peer_independent_worktree_required=True,
repository_source="todo.complete.turn_settlement",
)
elif getattr(args, "completion_identity_key", None):
completion_turn_key = str(args.completion_identity_key)
completion_identity_source = "lifecycle_reentry"
Expand All @@ -472,6 +492,8 @@ def handle_todo_command(
evidence=args.evidence,
completion_turn_key=completion_turn_key,
completion_identity_source=completion_identity_source,
completion_delivery_workspace=completion_delivery_workspace,
completion_validation_workspace_path=Path.cwd(),
task_lease_idempotency_key=args.task_lease_idempotency_key,
task_lease_expected_version=args.task_lease_expected_version,
note=args.note,
Expand Down
14 changes: 14 additions & 0 deletions loopx/cli_commands/turn.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ..capabilities.periodic_report.cadence_runtime import extend_cadence_turn_start_dispatch
from ..capabilities.periodic_report.pending_intent import periodic_report_pending_intent_interaction_hook
from ..control_plane.quota.live_decision import build_live_quota_should_run_decision
from ..control_plane.agents.workspace_guard import capture_delivery_workspace
from ..control_plane.quota.heartbeat_receipt import (
ensure_turn_heartbeat_settlement_receipt,
)
Expand Down Expand Up @@ -473,6 +474,17 @@ def append_settlement_event(
)
else None
)
completion_delivery_workspace = (
capture_delivery_workspace(
delivery_workspace_path,
peer_independent_worktree_required=bool(
selected_todo.get("task_repository")
),
repository_source="turn.delivery_workspace",
)
if delivery_workspace_path is not None
else None
)

def writeback(
result: dict[str, object],
Expand Down Expand Up @@ -590,6 +602,8 @@ def todo_completion(
),
note=str(result["next_action"]),
agent_id=args.agent_id,
completion_delivery_workspace=completion_delivery_workspace,
completion_validation_workspace_path=delivery_workspace_path,
)
# Project the continuation the Todo lifecycle durably recorded,
# never a host-normalized continuation. Contradictory or
Expand Down
7 changes: 6 additions & 1 deletion loopx/cli_commands/turn_todo_writeback.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from __future__ import annotations

from pathlib import Path
from typing import Any
from typing import Any, Mapping

from ..todos import complete_goal_todo, update_goal_todo

Expand Down Expand Up @@ -49,6 +49,8 @@ def write_turn_validated_completion(
evidence: str,
note: str,
agent_id: str | None,
completion_delivery_workspace: Mapping[str, Any] | None = None,
completion_validation_workspace_path: Path | None = None,
) -> dict[str, Any]:
"""Complete one validated Todo under the effective runtime root."""

Expand All @@ -58,6 +60,9 @@ def write_turn_validated_completion(
todo_id=todo_id,
role="agent",
completion_turn_key=completion_turn_key,
completion_identity_source="turn_settlement",
completion_delivery_workspace=completion_delivery_workspace,
completion_validation_workspace_path=completion_validation_workspace_path,
evidence=evidence,
note=note,
agent_id=agent_id,
Expand Down
5 changes: 5 additions & 0 deletions loopx/control_plane/todos/completion_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"completion_continuation",
"completion_turn_key",
"successor_todo_ids",
"task_repository",
"validation_command",
"validation_command_argv",
"validation_label",
Expand Down Expand Up @@ -384,6 +385,10 @@ def _valid_execute_validation_result(result: Mapping[str, Any]) -> bool:
and 1 <= int(effect["validation_timeout_seconds"]) <= 29
)
)
and (
effect.get("task_repository") is None
or isinstance(effect.get("task_repository"), str)
)
)


Expand Down
5 changes: 5 additions & 0 deletions loopx/control_plane/todos/completion_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface TodoCompletionValidationEffect extends JsonObject {
validation_argv: readonly string[] | null;
validation_label: string | null;
validation_timeout_seconds: number | null;
task_repository: string | null;
}

export interface TodoCompletionExecuteValidation
Expand Down Expand Up @@ -410,6 +411,10 @@ export function reduceTodoCompletionTransaction(
validation_label: validationPlan.validation_label,
validation_timeout_seconds:
validationPlan.validation_timeout_seconds,
task_repository: optionalNonEmptyString(
request.todo.task_repository,
"todo.task_repository",
),
},
};
}
Expand Down
Loading