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
17 changes: 12 additions & 5 deletions docs/quota-allocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1465,13 +1465,20 @@ Post-turn accounting protocol:
`--delivery-workspace-path <delivery-worktree>`; the path is validated locally
and omitted from persisted history. Do not point this option at the canonical
checkout for peer work.
- delivery attribution is not synonymous with Git. A registered single-agent
goal whose project has no Git origin records a path-free `local_goal`
- delivery attribution is not synonymous with Git. A registered non-Git
project records a path-free `local_goal`
workspace identity (`loopx:<goal-id>`) when refresh runs inside that
registered project root. This lets validated non-repository work settle
without inventing a repository. It does not weaken peer isolation: a peer
repository write still requires an `independent_git_worktree`, and a local
goal workspace is rejected when that requirement is active.
without inventing a repository, including peer research and material work.
The existing Todo claim/lease and completion validator still apply; local
delivery is not `same_agent_non_delivery`. A Git peer delivery still requires
an `independent_git_worktree`. An explicit Git task repository or an explicit
owner isolation requirement rejects a local Goal receipt. An outside-root
workspace cannot produce that local receipt.
- `todo complete --evidence <pointer>` can record a validated local artifact.
`--result-file` additionally requires approved Goal acceptance criteria bound
to that Todo. A standalone Todo validator does not establish Goal acceptance;
an unsupported result binding is rejected before executing the validator.
- autonomous replans follow the same accountable-outcome rule: spend after a
concrete successor, blocker, or `outcome_progress`/`primary_goal_outcome`
writeback, but do not spend for a `surface_only` watch-lane continuation or
Expand Down
9 changes: 9 additions & 0 deletions docs/reference/protocols/peer-agent-runtime-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ workspace isolation only: it is not an agent scope, write scope, permission
grant, or replacement for claim/lease and goal-boundary checks. Without the
field, the goal repository remains authoritative.

Accountable refresh applies Git isolation to Git delivery. A registered non-Git
Goal can instead record the existing path-free `local_goal_workspace` receipt
for peer research or material work. Capture must occur inside that Goal root;
an explicit Git task repository or owner isolation requirement cannot use this
route. Completion validation remains independent, and in-flight writeback and
spend leave the Todo open. Do not reclassify a local deliverable as
`same_agent_non_delivery` to settle it. `--evidence` records a local pointer;
`--result-file` requires approved Goal acceptance criteria bound to the Todo.

## Task-Scoped Coordination

When bounded multi-agent orchestration is enabled, LoopX hashes the canonical
Expand Down
2 changes: 1 addition & 1 deletion loopx/cli_commands/todo_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def register_todo_command(
todo_parser.add_argument("--status", choices=["open", "done", "blocked", "deferred"], help="For todo add/update, set the lifecycle status.")
todo_parser.add_argument("--note", help="Public-safe note to attach to a lifecycle transition.")
todo_parser.add_argument("--evidence", help="Public-safe evidence pointer or short result for complete/update.")
todo_parser.add_argument("--result-file", help="For todo complete, bind a bounded local .json, .md or .txt result to the independently accepted completion.")
todo_parser.add_argument("--result-file", help="For todo complete with bound Goal acceptance criteria, bind a bounded local .json, .md or .txt result. A Todo validator alone is insufficient; use --evidence for a local artifact pointer.")
todo_parser.add_argument(
"--validation-command",
help=(
Expand Down
19 changes: 19 additions & 0 deletions loopx/control_plane/agents/delivery_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,22 @@ def normalize_delivery_workspace_snapshot(value: Any) -> dict[str, Any] | None:
),
}
return _workspace_result(_runtime_result("normalize", workspace=prepared))


def qualify_delivery_workspace_isolation(
workspace: dict[str, Any] | None,
*,
multi_agent_goal: bool,
explicit_peer_worktree_requirement: bool | None,
task_repository: str | None = None,
) -> tuple[dict[str, Any] | None, bool]:
"""Delegate Git/local isolation policy to the existing typed owner."""
result = _runtime_result(
"isolation", workspace=workspace, multi_agent_goal=multi_agent_goal,
explicit_peer_worktree_requirement=explicit_peer_worktree_requirement,
task_repository=task_repository,
)
required = result.get("peer_independent_worktree_required")
if not isinstance(required, bool):
raise RuntimeError("TypeScript delivery workspace isolation result shape mismatch")
return _workspace_result(result), required
32 changes: 30 additions & 2 deletions loopx/control_plane/agents/delivery_workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface DeliveryWorkspaceSnapshot extends JsonObject {
peer_independent_worktree_required: boolean;
}

type DeliveryWorkspaceOperation = "build" | "normalize";
type DeliveryWorkspaceOperation = "build" | "normalize" | "isolation";

const GIT_IDENTITY_PATTERN =
/^git:[a-z0-9.-]+(?::[0-9]{1,5})?\/[A-Za-z0-9._~+/-]+$/i;
Expand All @@ -60,7 +60,7 @@ const GIT_REVISION_DIGEST_PATTERN = /^[0-9a-f]{64}$/i;
function operation(value: unknown): DeliveryWorkspaceOperation {
return requireStringLiteral(
value,
["build", "normalize"] as const,
["build", "normalize", "isolation"] as const,
"delivery workspace operation",
"delivery workspace operation is unsupported",
);
Expand Down Expand Up @@ -249,6 +249,34 @@ export function normalizeDeliveryWorkspaceSnapshot(
export function evaluateDeliveryWorkspace(value: unknown): JsonObject {
const request = requestObject(value);
const selectedOperation = operation(request.operation);
if (selectedOperation === "isolation") {
const workspace = normalizeDeliveryWorkspaceSnapshot(request.workspace);
const multiAgent = requireBoolean(request.multi_agent_goal, "multi_agent_goal");
const explicit = request.explicit_peer_worktree_requirement == null
? null
: requireBoolean(request.explicit_peer_worktree_requirement, "explicit_peer_worktree_requirement");
const repository = optionalNonEmptyString(request.task_repository, "task_repository");
if (repository !== null && canonicalGitIdentity(repository, "task_repository") === null) {
throw new EffectRuntimeRequestError("task_repository must identify a Git repository");
}
// A local Goal receipt proves the registered non-Git workspace. It is a
// delivery, so keep independent Todo acceptance; do not relabel it as
// non-delivery just because there is no Git worktree to isolate.
const required = multiAgent && explicit !== false && (
explicit === true || repository !== null || workspace?.identity_kind !== "local_goal"
);
return {
schema_version: DELIVERY_WORKSPACE_RESULT_SCHEMA,
peer_independent_worktree_required: required,
workspace: workspace === null || (
repository !== null && workspace.task_repository !== canonicalGitIdentity(repository, "task_repository")
) ? null : snapshot(
workspace.workspace_identity, workspace.identity_kind,
workspace.workspace_revision_digest ?? null, workspace.repository_source,
workspace.workspace_kind, required,
),
};
}
return {
schema_version: DELIVERY_WORKSPACE_RESULT_SCHEMA,
workspace: selectedOperation === "build"
Expand Down
7 changes: 7 additions & 0 deletions loopx/control_plane/coordination/todo_terminal_lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,13 @@ export async function executeCoordinationTodoTerminalLifecycle(
{goal_acceptance_guard: acceptance}, "decision_rejection");
}
const acceptanceRequirements = acceptanceCompletionRequirements(completionHead, input.goal_id, input.todo_id);
if (input.completion_result != null && acceptanceRequirements === null) {
return terminalFailure("completion_result_rejected",
"--result-file requires Goal acceptance criteria bound to this Todo; a Todo validator alone does not establish Goal acceptance. " +
"Use --evidence for a local artifact pointer, or bind approved Goal acceptance criteria before retrying --result-file.",
{next_action: "Keep the same Todo/Turn and complete with --evidence, or configure approved bound Goal acceptance criteria."},
"decision_rejection");
}
const acceptanceBinding = acceptanceRequirements === null ? null
: acceptanceSourceBinding(input, acceptanceRequirements, head.provider_revision);
let acceptanceEvidence: JsonObject | null = null;
Expand Down
8 changes: 6 additions & 2 deletions loopx/control_plane/work_items/interaction_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1487,10 +1487,14 @@ def _build_interaction_cli_channel(
if isinstance(payload.get("selected_todo"), Mapping)
else {}
)
if spend_after_selection and selected_todo.get("task_repository"):
if spend_after_selection and selected_todo.get("todo_id"):
channel["delivery_workspace_causality"] = {
"schema_version": "delivery_workspace_causality_v0",
"refresh": "delivery_workspace; otherwise --delivery-workspace-path",
"refresh": (
"delivery_workspace; otherwise --delivery-workspace-path"
if selected_todo.get("task_repository")
else "registered local Goal workspace; Git peer delivery requires an independent worktree"
),
"spend": "recorded_delivery_workspace",
"mismatch": "fail_closed",
}
Expand Down
2 changes: 1 addition & 1 deletion loopx/semantics/project_registry_io_manifest_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -2127,7 +2127,7 @@
},
{
"site": "loopx/state_refresh.py::<module>.refresh_state_run::codec_read:load_registry#1",
"line": 802,
"line": 803,
"column": 16,
"kind": "codec_read",
"api": "load_registry",
Expand Down
30 changes: 22 additions & 8 deletions loopx/state_refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .control_plane.agents.workspace_guard import (
capture_delivery_workspace,
)
from .control_plane.agents.delivery_workspace import qualify_delivery_workspace_isolation
from .control_plane.quota.refresh_external_delivery import (
finish_external_delivery_refresh, refresh_recovery_payload,
)
Expand Down Expand Up @@ -943,10 +944,6 @@ def refresh_state_run(
explicit_peer_worktree_requirement = workspace_guard_policy.get(
"peer_independent_worktree_required"
)
peer_independent_worktree_required = multi_agent_goal and (
explicit_peer_worktree_requirement is None
or explicit_peer_worktree_requirement is True
)
if normalized_agent_id and known_agents and normalized_agent_id not in known_agents:
raise ValueError(
f"agent_id {normalized_agent_id!r} is not registered for goal {safe_goal_id!r}"
Expand Down Expand Up @@ -1160,7 +1157,7 @@ def refresh_state_run(
):
delivery_workspace = capture_delivery_workspace(
current_path=delivery_workspace_path,
peer_independent_worktree_required=peer_independent_worktree_required,
peer_independent_worktree_required=False,
local_goal_id=safe_goal_id,
local_project_root=resolved_project,
repository_source=(
Expand All @@ -1169,6 +1166,22 @@ def refresh_state_run(
else None
),
)
workspace_todo_fields = todo_fields
if workspace_todo_fields is None:
workspace_todo_fields = parse_active_state_todos(
state_text, goal=registry_goal, state_path=resolved_state_file,
preferred_todo_ids={settlement_identity.todo_id or ""},
rollout_events=planning_events, item_limit=None,
)
selected_contract = next((
item for item in workspace_todo_fields.get("agent_todos", {}).get("items", [])
if item.get("todo_id") == settlement_identity.todo_id
), {})
delivery_workspace, peer_independent_worktree_required = qualify_delivery_workspace_isolation(
delivery_workspace, multi_agent_goal=multi_agent_goal,
explicit_peer_worktree_requirement=explicit_peer_worktree_requirement,
task_repository=selected_contract.get("task_repository"),
)
if (
peer_independent_worktree_required
and (
Expand All @@ -1182,10 +1195,11 @@ def refresh_state_run(
"git worktree that produced it, or name that worktree with "
"--delivery-workspace-path"
)
if delivery_workspace_path is not None and delivery_workspace is None:
if delivery_workspace is None:
raise ValueError(
"--delivery-workspace-path must identify the registered local goal "
"workspace or a git checkout with a credential-free origin repository"
"delivery workspace could not be verified; run from the registered "
"local Goal workspace or the selected repository worktree, or name "
"that workspace with --delivery-workspace-path"
)
if checkpoint_supplement:
# The supplemental row must not reattribute the original delivery to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ def test_non_delivery_contract_omits_workspace_causality() -> None:
assert "delivery_workspace_causality" not in contract["cli_channel"]


def test_delivery_without_task_repository_keeps_default_hot_path_compact() -> None:
def test_local_delivery_packet_explains_workspace_without_non_delivery_relabel() -> None:
payload = _payload(should_run=True)
payload["selected_todo"].pop("task_repository")

contract = build_interaction_contract(payload)

assert "delivery_workspace_causality" not in contract["cli_channel"]
causality = contract["cli_channel"]["delivery_workspace_causality"]
assert causality["refresh"] == "registered local Goal workspace; Git peer delivery requires an independent worktree"
assert causality["spend"] == "recorded_delivery_workspace"
assert causality["mismatch"] == "fail_closed"


def test_turn_envelope_preserves_workspace_causality() -> None:
Expand Down
Loading
Loading