diff --git a/tests/control_plane_ts/semantic_request.test.ts b/tests/control_plane_ts/semantic_request.test.ts
new file mode 100644
index 0000000000..18aa3bd10b
--- /dev/null
+++ b/tests/control_plane_ts/semantic_request.test.ts
@@ -0,0 +1,22 @@
+import assert from "node:assert/strict";
+import { normalizeCollaborationRequest } from "../../loopx/control_plane/collaboration/semantic_request.ts";
+
+const target = { goal_id: "allocation", agent_id: "reviewer" };
+const brief = {
+ schema_version: "collaboration_brief_v0", purpose: "Review the allocation",
+ context: "Proportional rounding was rejected. Preserve the reserve.",
+ constraints: ["No real orders"], inputs: [{ ref: "outputs/plan.json", description: "Candidate plan" }],
+ acceptance: ["Budget and shared stock hold"], return_requirement: "Return independent findings",
+};
+assert.deepEqual(normalizeCollaborationRequest(target), target);
+assert.deepEqual(normalizeCollaborationRequest({ ...target, brief }), { ...target, brief });
+for (const request of [
+ { ...target, priority: "P0" }, { ...target, agent_id: "../reviewer" },
+ { ...target, brief: { ...brief, acceptance: [] } },
+ { ...target, brief: { ...brief, authority: "owner" } },
+ { ...target, brief: { ...brief, inputs: [{ ref: "../secrets", description: "Outside" }] } },
+ { ...target, brief: { ...brief, inputs: [{ ref: "file:///secret", description: "Outside" }] } },
+ { ...target, brief: { ...brief, inputs: [{ ref: "inputs/a", description: "Input", sha256: "unverified" }] } },
+ { ...target, brief: { ...brief, context: "字".repeat(6000) } },
+]) assert.throws(() => normalizeCollaborationRequest(request));
+console.log("semantic collaboration contract passed");
diff --git a/tests/test_collaboration_mcp.py b/tests/test_collaboration_mcp.py
new file mode 100644
index 0000000000..af73d53ff1
--- /dev/null
+++ b/tests/test_collaboration_mcp.py
@@ -0,0 +1,105 @@
+"""The real stdio bridge binds identity and rechecks revocation per call."""
+
+import asyncio
+import json
+import sys
+
+from mcp import ClientSession, StdioServerParameters
+from mcp.client.stdio import stdio_client
+
+
+def test_scoped_stdio_tools_do_not_offer_shell_or_sender_override(tmp_path):
+ registry = tmp_path / "registry.json"
+ config = {
+ "goals": [
+ {
+ "id": "delivery",
+ "repo": str(tmp_path),
+ "coordination": {"registered_agents": ["builder", "reviewer"]},
+ }
+ ]
+ }
+ registry.write_text(json.dumps(config))
+ brief = {
+ "schema_version": "collaboration_brief_v0",
+ "purpose": "Review the allocation",
+ "context": "The owner rejected proportional rounding.",
+ "constraints": ["No orders"],
+ "inputs": [],
+ "acceptance": ["Check coupled capacity limits"],
+ "return_requirement": "Findings",
+ }
+
+ async def exercise():
+ params = StdioServerParameters(
+ command=sys.executable,
+ args=[
+ "-m",
+ "loopx.control_plane.collaboration.mcp",
+ "--runtime-root",
+ str(tmp_path),
+ "--registry",
+ str(registry),
+ "--goal-id",
+ "delivery",
+ "--agent-id",
+ "builder",
+ "--workspace",
+ str(tmp_path),
+ ],
+ )
+ async with (
+ stdio_client(params) as (read, write),
+ ClientSession(read, write) as session,
+ ):
+ await session.initialize()
+ tools = await session.list_tools()
+ assert {tool.name for tool in tools.tools} == {
+ "read_context",
+ "assess_request",
+ "request_peer",
+ "return_result",
+ "consume_peer_result",
+ }
+ for tool in tools.tools:
+ assert not {
+ "agent_id",
+ "goal_id",
+ "runtime_root",
+ "command",
+ "path",
+ } & set(tool.inputSchema.get("properties", {}))
+ result = await session.call_tool(
+ "request_peer",
+ {
+ "peer_agent_id": "reviewer",
+ "operation_id": "review-1",
+ "brief": brief,
+ },
+ )
+ assert not result.isError
+ result = await session.call_tool(
+ "request_peer",
+ {
+ "peer_agent_id": "unknown",
+ "operation_id": "review-2",
+ "brief": brief,
+ },
+ )
+ assert result.isError
+ # Changing registration affects this already-running server.
+ config["goals"][0]["coordination"]["registered_agents"] = ["reviewer"]
+ registry.write_text(json.dumps(config))
+ result = await session.call_tool("read_context", {})
+ assert result.isError
+ result = await session.call_tool(
+ "request_peer",
+ {
+ "peer_agent_id": "reviewer",
+ "operation_id": "review-3",
+ "brief": brief,
+ },
+ )
+ assert result.isError
+
+ asyncio.run(exercise())
diff --git a/tests/test_peer_collaboration.py b/tests/test_peer_collaboration.py
new file mode 100644
index 0000000000..0f9ef202f4
--- /dev/null
+++ b/tests/test_peer_collaboration.py
@@ -0,0 +1,541 @@
+"""Semantic delegation, two review rounds and recovery over the real CLI/store."""
+
+import hashlib
+import json
+import subprocess
+import sys
+from concurrent.futures import ThreadPoolExecutor
+
+import pytest
+
+from loopx.capabilities.manager_context import (
+ deliver,
+ pending,
+ normalize_request,
+ turn_start_hook,
+)
+from loopx.control_plane.collaboration.peers import (
+ request,
+ returns,
+ consume_return,
+ input_readiness,
+)
+from loopx.capabilities.manager_context.roundtrip import (
+ drain,
+ project_chat_session_snapshot,
+)
+from loopx.capabilities.manager_context.tracking import query
+from loopx.chat_store import ChatSessionStore
+
+
+@pytest.fixture
+def scenario(tmp_path):
+ registry = tmp_path / "registry.json"
+ registry.write_text(
+ json.dumps(
+ {
+ "goals": [
+ {
+ "id": "delivery",
+ "repo": str(tmp_path),
+ "coordination": {
+ "registered_agents": ["builder", "reviewer", "analyst"]
+ },
+ }
+ ]
+ }
+ )
+ )
+ (tmp_path / "inputs").mkdir()
+ artifact = tmp_path / "inputs" / "demand.csv"
+ artifact.write_text("sku,demand\na,15\nb,0\n")
+ brief = {
+ "schema_version": "collaboration_brief_v0",
+ "purpose": "Produce a stock plan",
+ "context": "The owner rejected using an average. Latest correction: reserve two units.",
+ "constraints": ["No external orders", "Never allocate negative quantities"],
+ "inputs": [
+ {
+ "ref": "inputs/demand.csv",
+ "description": "Demand fixture",
+ "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest(),
+ }
+ ],
+ "acceptance": [
+ "Total allocation is within available stock",
+ "Zero-demand items receive zero",
+ ],
+ "return_requirement": "Return the plan and independent review findings, including unresolved gaps.",
+ }
+ store = ChatSessionStore(tmp_path)
+ session = store.create_session(
+ goal_id="loopx-manager",
+ agent_id="codex",
+ adapter_kind="codex_app_server",
+ upstream_thread_id="fixture",
+ channel_id="manager",
+ )
+ turn, _ = store.create_turn(
+ session["session_id"],
+ client_turn_id="owner-correction",
+ message="Use the correction above and have a peer check it.",
+ origin="web",
+ )
+ receipt = deliver(
+ tmp_path,
+ registry,
+ session=session,
+ turn=turn,
+ request={"goal_id": "delivery", "agent_id": "builder", "brief": brief},
+ )
+ store.update_turn(
+ session["session_id"],
+ turn["turn_id"],
+ status="completing",
+ response={"message": "Delegated", "context_handoff_receipt": receipt},
+ )
+ store.finalize_managed_turn_completion(session["session_id"], turn["turn_id"])
+ return tmp_path, registry, brief, store, session, turn, receipt["request_id"]
+
+
+def cli(root, registry, agent, action, *args, ok=True):
+ proc = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "loopx.cli",
+ "--runtime-root",
+ str(root),
+ "--registry",
+ str(registry),
+ "manager-inbox",
+ action,
+ "--goal-id",
+ "delivery",
+ "--agent-id",
+ agent,
+ *args,
+ ],
+ capture_output=True,
+ text=True,
+ timeout=30,
+ )
+ result = json.loads(proc.stdout)
+ assert proc.returncode == (0 if ok else 1), (proc.stdout, proc.stderr)
+ return result
+
+
+def test_two_peer_review_rounds_return_to_original_conversation_after_restart(scenario):
+ root, registry, brief, store, session, turn, parent = scenario
+ builder = cli(root, registry, "builder", "read")["items"][0]
+ assert builder["message"] == turn["message"]
+ assert builder["brief"] == brief
+ assert builder["input_readiness"][0]["status"] == "available"
+ cli(
+ root,
+ registry,
+ "builder",
+ "acknowledge",
+ "--request-id",
+ parent,
+ "--decision",
+ "adopt",
+ "--reason",
+ "Will use the corrected reserve rule.",
+ )
+ ids = []
+ for number in (1, 2):
+ review = {**brief, "purpose": f"Independently review stock plan round {number}"}
+ path = root / "review.json"
+ path.write_text(json.dumps(review))
+ args = (
+ "--peer-agent-id",
+ "reviewer",
+ "--operation-id",
+ f"review-{number}",
+ "--brief-file",
+ str(path),
+ "--parent-request-id",
+ parent,
+ )
+ rid = cli(root, registry, "builder", "request", *args)["request_id"]
+ ids.append(rid)
+ assert cli(root, registry, "builder", "request", *args)["replayed"]
+ received = cli(root, registry, "reviewer", "read")["items"][0]
+ assert received["inherited_context"]["brief"] == brief
+ assert received["source_agent_id"] == "builder"
+ cli(
+ root,
+ registry,
+ "reviewer",
+ "acknowledge",
+ "--request-id",
+ rid,
+ "--decision",
+ "adopt",
+ "--reason",
+ "Independent artifact check.",
+ )
+ result_text = (
+ "Reserve missing; revise."
+ if number == 1
+ else "Reserve and zero-demand checks passed."
+ )
+ cli(
+ root,
+ registry,
+ "reviewer",
+ "report",
+ "--request-id",
+ rid,
+ "--reply-text",
+ result_text,
+ )
+ # The Chat pump cannot send a peer reply to any conversation.
+ assert drain(root, registry, store, None) == 0
+ assert turn_start_hook(root, registry, "delivery", "builder").producer()[
+ "agent_read_required"
+ ]
+ # New CLI processes reconstruct the work context with no source session.
+ returned = cli(root, registry, "builder", "read")["peer_returns"]["items"][0]
+ assert (
+ returned["text"] == result_text and returned["parent_request_id"] == parent
+ )
+ assert (
+ cli(root, registry, "builder", "read")["peer_returns"]["items"][0]
+ == returned
+ )
+ cli(root, registry, "builder", "acknowledge-return", "--request-id", rid)
+ cli(root, registry, "builder", "acknowledge-return", "--request-id", rid)
+ assert not returns(root, "delivery", "builder")["items"]
+ assert ids[0] != ids[1]
+ cli(
+ root,
+ registry,
+ "builder",
+ "report",
+ "--request-id",
+ parent,
+ "--reply-text",
+ "Revised the reserve after peer review; the second check passed.",
+ )
+ assert drain(root, registry, ChatSessionStore(root), None) == 1
+ assert drain(root, registry, ChatSessionStore(root), None) == 0
+ snapshot = project_chat_session_snapshot(root, store, session["session_id"])
+ assert sum(m.get("origin") == "manager_followup" for m in snapshot["messages"]) == 1
+ card = next(
+ m["collaboration"] for m in snapshot["messages"] if m.get("collaboration")
+ )
+ assert (
+ card["brief"] == brief
+ and card["decision"] == "adopt"
+ and card["read_status"] == "supplied"
+ )
+ assert card["returns"][-1]["status"] == "delivered"
+ assert not pending(root, "delivery", "builder")["items"]
+
+
+def test_operation_replay_conflicts_and_independent_requests(scenario):
+ root, registry, brief, _, _, _, parent = scenario
+
+ def send():
+ return request(
+ root, registry, "delivery", "builder", "reviewer", "review-1", brief, parent
+ )
+
+ with ThreadPoolExecutor(max_workers=4) as pool:
+ receipts = list(pool.map(lambda _: send(), range(4)))
+ assert sum(not row["replayed"] for row in receipts) == 1
+ with pytest.raises(ValueError, match="identity conflict"):
+ request(
+ root, registry, "delivery", "builder", "analyst", "review-1", brief, parent
+ )
+ with pytest.raises(ValueError, match="identity conflict"):
+ request(
+ root,
+ registry,
+ "delivery",
+ "builder",
+ "reviewer",
+ "review-1",
+ {**brief, "purpose": "Changed"},
+ parent,
+ )
+ rid = receipts[0]["request_id"]
+ with pytest.raises(ValueError, match="scope mismatch"):
+ consume_return(root, "delivery", "analyst", rid)
+ with pytest.raises(ValueError, match="read the peer conclusion"):
+ consume_return(root, "delivery", "builder", rid)
+ with pytest.raises(ValueError, match="different"):
+ request(root, registry, "delivery", "builder", "builder", "self", brief)
+ with pytest.raises(ValueError, match="registered"):
+ request(root, registry, "delivery", "builder", "unknown", "other", brief)
+ with pytest.raises((ValueError, OSError)):
+ request(
+ root,
+ registry,
+ "delivery",
+ "analyst",
+ "reviewer",
+ "stolen-parent",
+ brief,
+ parent,
+ )
+
+
+@pytest.mark.parametrize(
+ "change",
+ [
+ {"priority": "P0"},
+ {"acceptance": []},
+ {"constraints": "No orders"},
+ {"inputs": [{"ref": "../private.txt", "description": "Invalid"}]},
+ {"inputs": [{"ref": "/etc/passwd", "description": "Invalid"}]},
+ {
+ "inputs": [
+ {"ref": "inputs/x", "description": "Invalid", "sha256": "guessed"}
+ ]
+ },
+ {"context": "字" * 6000},
+ ],
+)
+def test_semantic_contract_rejects_operational_keys_and_invalid_inputs(
+ scenario, change
+):
+ _, _, brief, *_ = scenario
+ with pytest.raises(ValueError):
+ normalize_request(
+ {"goal_id": "delivery", "agent_id": "builder", "brief": {**brief, **change}}
+ )
+
+
+def test_changed_missing_and_escaping_artifacts_are_explicit(scenario, tmp_path):
+ root, registry, brief, *_ = scenario
+ (root / "inputs/demand.csv").write_text("changed")
+ assert input_readiness(registry, "delivery", brief)[0]["status"] == "changed"
+ (root / "inputs/demand.csv").unlink()
+ assert input_readiness(registry, "delivery", brief)[0]["status"] == "unavailable"
+ (root / "inputs/demand.csv").symlink_to(root.parent / "outside.csv")
+ assert (
+ input_readiness(registry, "delivery", brief)[0]["status"] == "outside_workspace"
+ )
+
+
+def test_peer_route_never_enters_external_audience_projection(scenario):
+ root, registry, brief, store, session, turn, parent = scenario
+ request(root, registry, "delivery", "builder", "reviewer", "review", brief, parent)
+ result = query(
+ root,
+ registry,
+ goal_ids=["delivery"],
+ owner_scope=False,
+ channel_id="manager.external.other",
+ )
+ assert result["rows"] == []
+ external = store.create_session(
+ goal_id="loopx-manager",
+ agent_id="codex",
+ adapter_kind="codex_app_server",
+ upstream_thread_id="external",
+ channel_id="manager.external.other",
+ )
+ assert not any(
+ m.get("collaboration")
+ for m in project_chat_session_snapshot(root, store, external["session_id"])[
+ "messages"
+ ]
+ )
+ assert not returns(root, "delivery", "analyst")["items"]
+
+
+def test_invalid_brief_retry_does_not_create_another_manager_request(scenario):
+ root, registry, brief, _, session, turn, _ = scenario
+ with pytest.raises(ValueError, match="identity conflict"):
+ deliver(
+ root,
+ registry,
+ session=session,
+ turn=turn,
+ request={
+ "goal_id": "delivery",
+ "agent_id": "builder",
+ "brief": {**brief, "purpose": "Different"},
+ },
+ )
+ assert len(pending(root, "delivery", "builder")["items"]) == 1
+
+
+def test_input_versions_follow_receiver_worktree_and_reject_unrelated_workspace(
+ scenario, tmp_path
+):
+ root, registry, brief, *_ = scenario
+
+ def git(*args):
+ subprocess.run(["git", "-C", str(root), *args], check=True, capture_output=True)
+
+ git("init", "-b", "main")
+ git("add", "inputs/demand.csv")
+ git(
+ "-c",
+ "user.name=Fixture",
+ "-c",
+ "user.email=fixture@example.invalid",
+ "commit",
+ "-m",
+ "fixture",
+ )
+ receiver = root.parent / (root.name + "-receiver")
+ git("worktree", "add", "-b", "receiver", str(receiver))
+ (receiver / "inputs/demand.csv").write_text("receiver has a different version")
+ result = input_readiness(registry, "delivery", brief, workspace=receiver)[0]
+ assert result["status"] == "changed" and result["basis"] == "receiver_worktree"
+ unrelated = root / "unrelated"
+ unrelated.mkdir()
+ # A non-repository directory cannot replace the registered source root.
+ outside = root.parent / (root.name + "-outside")
+ outside.mkdir()
+ result = input_readiness(registry, "delivery", brief, workspace=outside)[0]
+ assert result["status"] == "available" and result["basis"] == "goal_workspace"
+
+
+def test_unrelated_damaged_return_route_does_not_break_legacy_inbox(scenario):
+ root, _, _, _, _, _, parent = scenario
+ damaged = root / ".local/manager-context/roundtrips/unrelated.json"
+ damaged.write_text("{broken")
+ assert pending(root, "delivery", "builder")["items"][0]["request_id"] == parent
+
+
+def test_consumption_rejects_corrupt_reply_and_receipt(scenario):
+ from loopx.capabilities.manager_context import _root
+
+ root, registry, brief, _, _, _, parent = scenario
+ rid = request(
+ root, registry, "delivery", "builder", "reviewer", "review", brief, parent
+ )["request_id"]
+ cli(root, registry, "reviewer", "read")
+ cli(
+ root,
+ registry,
+ "reviewer",
+ "acknowledge",
+ "--request-id",
+ rid,
+ "--decision",
+ "adopt",
+ "--reason",
+ "Review",
+ )
+ cli(
+ root,
+ registry,
+ "reviewer",
+ "report",
+ "--request-id",
+ rid,
+ "--reply-text",
+ "Checked",
+ )
+ cli(root, registry, "builder", "read")
+ folder = _root(root) / "replies" / rid
+ reply = folder / "conclusion.json"
+ original = reply.read_text()
+ reply.write_text(json.dumps({**json.loads(original), "source_id": "other"}))
+ with pytest.raises(ValueError, match="identity conflict"):
+ consume_return(root, "delivery", "builder", rid)
+ reply.write_text(original)
+ consume_return(root, "delivery", "builder", rid)
+ consumed = folder / "conclusion.consumed.json"
+ consumed.write_text(
+ json.dumps({**json.loads(consumed.read_text()), "agent_id": "other"})
+ )
+ with pytest.raises(ValueError, match="scope mismatch"):
+ consume_return(root, "delivery", "builder", rid)
+
+
+def test_special_file_read_is_bounded_and_stopped_goal_remains_readable(scenario):
+ import os
+ from loopx.control_plane.collaboration.peers import read_inbox
+
+ root, registry, brief, *_ = scenario
+ (root / "inputs/demand.csv").unlink()
+ os.mkfifo(root / "inputs/demand.csv")
+ assert input_readiness(registry, "delivery", brief)[0]["status"] == "unavailable"
+ config = json.loads(registry.read_text())
+ config["goals"][0]["status"] = "stopped"
+ registry.write_text(json.dumps(config))
+ assert read_inbox(root, registry, "delivery", "builder")["items"]
+ with pytest.raises(ValueError, match="stopped"):
+ request(root, registry, "delivery", "builder", "reviewer", "stopped", brief)
+
+
+def test_nested_coordinators_return_to_each_immediate_requester(scenario):
+ """Worker -> coordinator -> specialist works without a manager root request."""
+ root, registry, brief, *_ = scenario
+ outer = request(
+ root, registry, "delivery", "builder", "reviewer", "delegate", brief
+ )["request_id"]
+ cli(root, registry, "reviewer", "read")
+ cli(
+ root,
+ registry,
+ "reviewer",
+ "acknowledge",
+ "--request-id",
+ outer,
+ "--decision",
+ "adopt",
+ "--reason",
+ "Coordinate the specialist audit",
+ )
+ inner = request(
+ root, registry, "delivery", "reviewer", "analyst", "specialist", brief, outer
+ )["request_id"]
+ received = cli(root, registry, "analyst", "read")["items"][0]
+ assert received["inherited_context"]["request_id"] == outer
+ assert received["source_agent_id"] == "reviewer"
+ cli(
+ root,
+ registry,
+ "analyst",
+ "acknowledge",
+ "--request-id",
+ inner,
+ "--decision",
+ "adopt",
+ "--reason",
+ "Check specialty",
+ )
+ cli(
+ root,
+ registry,
+ "analyst",
+ "report",
+ "--request-id",
+ inner,
+ "--reply-text",
+ "Specialist findings",
+ )
+ assert not returns(root, "delivery", "builder")["items"]
+ assert (
+ cli(root, registry, "reviewer", "read")["peer_returns"]["items"][0][
+ "request_id"
+ ]
+ == inner
+ )
+ consume_return(root, "delivery", "reviewer", inner)
+ cli(
+ root,
+ registry,
+ "reviewer",
+ "report",
+ "--request-id",
+ outer,
+ "--reply-text",
+ "Specialist findings independently assessed",
+ )
+ assert (
+ cli(root, registry, "builder", "read")["peer_returns"]["items"][0]["request_id"]
+ == outer
+ )
+ consume_return(root, "delivery", "builder", outer)
+ assert not returns(root, "delivery", "builder")["items"]
From e715a0c897cb113672d899bac65f9486d255d38f Mon Sep 17 00:00:00 2001
From: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Date: Fri, 18 Sep 2026 02:04:10 +0800
Subject: [PATCH 2/5] docs(collaboration): add reproducible managed Agent
delivery demo
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
---
.../capable-manager-semantic-handoff-v0.md | 14 +
...pable-manager-semantic-handoff-v0.zh-CN.md | 9 +
.../rfcs/loopx-overall-roadmap-v0.md | 1 +
.../rfcs/loopx-overall-roadmap-v0.zh-CN.md | 1 +
examples/collaboration-delivery/README.md | 157 ++++++
.../collaboration-delivery/REQUIREMENTS.md | 20 +
examples/collaboration-delivery/demo.py | 514 ++++++++++++++++++
examples/collaboration-delivery/scenario.json | 65 +++
.../screenshots/before.png | Bin 0 -> 129910 bytes
.../screenshots/desktop.png | Bin 0 -> 134369 bytes
.../screenshots/mobile.png | Bin 0 -> 57502 bytes
examples/collaboration-delivery/tasks.json | 9 +
examples/collaboration-delivery/verify.py | 128 +++++
loopx/capabilities/manager_context/README.md | 142 ++++-
loopx/control_plane/collaboration/README.md | 38 ++
tests/test_collaboration_demo.py | 89 +++
16 files changed, 1186 insertions(+), 1 deletion(-)
create mode 100644 examples/collaboration-delivery/README.md
create mode 100644 examples/collaboration-delivery/REQUIREMENTS.md
create mode 100644 examples/collaboration-delivery/demo.py
create mode 100644 examples/collaboration-delivery/scenario.json
create mode 100644 examples/collaboration-delivery/screenshots/before.png
create mode 100644 examples/collaboration-delivery/screenshots/desktop.png
create mode 100644 examples/collaboration-delivery/screenshots/mobile.png
create mode 100644 examples/collaboration-delivery/tasks.json
create mode 100644 examples/collaboration-delivery/verify.py
create mode 100644 loopx/control_plane/collaboration/README.md
create mode 100644 tests/test_collaboration_demo.py
diff --git a/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.md b/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.md
index 708f7dbbeb..650c6513a2 100644
--- a/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.md
+++ b/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.md
@@ -67,6 +67,20 @@ At `43d362532`, the private `manager_runtime` profile, steward executor configur
The [overall roadmap](loopx-overall-roadmap-v0.md) records verified F1–F7 and R1–R7. Repair R1 commitment preservation, stale basis and recovery first, then qualify R2 small teams; M2/M3 converge through R3, and M4 requires real user journeys. Section 4 retains its older baseline as migration input, not an override of this checkpoint. Partial merges do not complete M1–M4. Parallel joins, pipeline dependencies, peer help/review, execution responsibility continuation and cross-host collaboration between long-running LoopX Agents follow the roadmap Section 5 matrix. R2 requires real inter-Agent handoff; M2/M3 cannot reduce to steward broadcasts or one-turn forwarding.
+The local semantic exchange now extends the existing `manager-context` owner:
+`collaboration_brief_v0` preserves corrections, constraints, relative artifact
+versions, acceptance and return requirements alongside the unchanged owner
+message. Same-Goal peers independently assess requests and return conclusions;
+the requester receives results again until explicit consumption. Owner-local Chat
+shows the brief and live read/decision/return facts in the original conversation.
+The optional identity-scoped stdio tools support sandboxed managed dsh workers.
+See the [three-Agent allocation demo](../../../examples/collaboration-delivery/README.md)
+for dependent artifacts, two review rounds and owner correction. The controller
+selects phases and transfers files; request transport adds no scheduling, lease
+transfer or shared-intent mutation. This is a bounded R3 slice, not M2–M4/G1
+completion; external-audience peer forwarding and cross-host continuation remain
+with their existing roadmap owners.
+
## 4. Current-system contract: audited facts
The baseline already has substantial reusable machinery:
diff --git a/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.zh-CN.md b/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.zh-CN.md
index 6a328c6db4..2fd61bdc52 100644
--- a/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.zh-CN.md
+++ b/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.zh-CN.md
@@ -67,6 +67,15 @@
[整体路线总纲](loopx-overall-roadmap-v0.zh-CN.md) 记录已复核 F1–F7 与 R1–R7 执行卡。优先修 R1 承诺保留、stale basis 和恢复,再验 R2 小团队;M2/M3 按 R3 收敛,M4 需真实用户旅程。第 4 节的旧基线保留为迁移输入,不能覆盖本检查点;不因局部切片合并将 M1–M4 标为完成。 多个长程 LoopX Agent 的并行汇合、流水线依赖、peer 求助/复核、执行责任接续和跨 host 协作,统一按路线第 5 节协作矩阵验收;R2 必须包含真实 Agent 间 handoff,M2/M3 不能退化为管家广播或单轮转发。
+本地语义协作沿用 `manager-context` owner:`collaboration_brief_v0` 在原始用户
+消息旁保留修订、约束、相对产物版本、验收与回传要求。同 Goal 同伴独立判断请求并
+回传结论,请求者在明确消费前会持续收到结果。管家原对话展示交办内容和实时读取、
+判断、回传事实。显式配置的身份限定 stdio 工具支持保留沙箱的 managed dsh。
+[三 Agent 分配器 demo](../../../examples/collaboration-delivery/README.md#中文操作说明)
+覆盖依赖产物、两轮复核与用户修订;控制器选择阶段并交接文件。请求传输不授予调度、
+lease 转交或 shared-intent 修改权限。这是 R3 的有界切片,不代表 M2–M4/G1 完成;
+外部受众同伴转发与跨主机续接继续归现有路线 owner。
+
## 4. 当前系统:已核对的基线事实
已有大量基础应该复用:
diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md
index dfc4ce7bdd..ada3ef8758 100644
--- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md
+++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md
@@ -297,6 +297,7 @@ Progress means another independently reproducible user journey, not more fields
| Shared authority | `AuthorityStore`, File/SQLite candidates, PostgreSQL store/service admission seam, recovery and conformance foundations | A deployed authenticated cross-host service, a promoted default provider or distributed quota |
| Alignment | Stage 1/2 source-basis reader and amendment admission/retention | A full Goal-intent revision in `source_basis_digest`, or Stage 3 automatic commit |
| Managed execution | `turn run-once`, managed step, attached broker and executor fences | Unattended long-horizon supervision from one bounded segment, or a healthy executor for every registered Agent |
+| Semantic peer delivery | Existing Inbox/reply owners now carry immutable briefs, same-Goal peer requests, explicit result consumption and original-conversation readback; [real managed demo](../../../examples/collaboration-delivery/README.md) | Controller-driven phases do not qualify autonomous scheduling, cross-host/Lark peers or full G1/M2–M4 |
### Verified Findings
diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md
index 6d6e350ae3..f0ff11d37f 100644
--- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md
+++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md
@@ -297,6 +297,7 @@ R2 的一条依赖必须通过真实 LoopX Agent 间的请求/产物交接完成
| shared authority | `AuthorityStore`、File/SQLite 候选与 PostgreSQL store/service admission 接缝、恢复及 conformance 基础 | 已部署 authenticated 跨主机服务;已晋升任一默认 provider;分布式 quota 已成立 |
| alignment | Stage 1/2 source-basis reader 与 amendment admission/retention | `source_basis_digest` 是完整 Goal intent revision;Stage 3 自动 commit 已成立 |
| managed | `turn run-once`、managed step、attached broker 及单执行器围栏已有实现 | 有界片段等于无人值守长程监督;每个注册 Agent 都有健康执行器 |
+| 语义同伴协作 | 既有 Inbox/reply owner 承载不可变交办、同 Goal 同伴请求、结果消费及原对话读回;[真实 managed demo](../../../examples/collaboration-delivery/README.md#中文操作说明) | 控制器驱动阶段不证明自主调度、跨主机/Lark 同伴或完整 G1/M2–M4 |
### 已复核的问题
diff --git a/examples/collaboration-delivery/README.md b/examples/collaboration-delivery/README.md
new file mode 100644
index 0000000000..6ac9369e68
--- /dev/null
+++ b/examples/collaboration-delivery/README.md
@@ -0,0 +1,157 @@
+# Allocation delivery with three managed Agents
+
+[中文](#中文操作说明) · [Capability contract](../../loopx/capabilities/manager_context/README.md#semantic-delegation-and-peer-review)
+
+This demo runs real `dsh` workers through `loopx turn run-once`: builder requests
+an analyst's model, consumes its versioned artifact, implements a solver, obtains
+independent review, incorporates an owner correction, obtains a second review,
+and returns both results to the original conversation. Each phase has a fresh
+execution session but retains its registered Agent identity and durable Inbox.
+
+The task has six orders, shared stock, regional capacity, an integer-cent budget,
+a high-value zero-demand order and a deterministic tie-break. The owner rejected
+proportional rounding. Round two adds reserve stock and a minimum East allocation.
+The independent exhaustive oracle never imports the generated solver.
+
+| Input version | Allocation in id order a–f | Value | Cost |
+| --- | --- | --- | --- |
+| Initial | 1, 1, 3, 2, 0, 2 | 84 | 1000 |
+| Reserve 1; East minimum 4 | 2, 1, 3, 1, 0, 2 | 83 | 990 |
+
+These expectations were derived before worker execution. Correctness includes
+the exact allocation, not only the objective value. Do not supply this table or
+`verify.py` to the reviewer as its reasoning; it must derive its own expectations.
+
+## Run
+
+Run from the LoopX source root on a supported dsh host, with Git, Node and the
+selected Python environment available:
+
+```sh
+uv sync --extra test --extra deepseek-harness
+```
+
+Configure `DEEPSEEK_API_KEY` through your normal credential setup. The controller
+only consumes this environment variable; it does not discover credentials or
+store them. Model runs are explicit, billable calls: seven successful phases,
+each capped at 600 seconds. Stop on a failed phase and inspect its private
+receipt before spending more; a retry may require a new Turn identity if the
+original Turn recorded terminal failure. No model calls run in CI.
+
+Define a convenience command in your shell:
+
+```sh
+demo() { uv run --extra test --extra deepseek-harness python examples/collaboration-delivery/demo.py "$@"; }
+demo prepare
+```
+
+`prepare` requires a **new** `.local/allocation-demo` directory. It creates a
+disposable registry, Goal state, synthetic Git repository, three worktrees and
+per-worker Cordis patches. It seeds the owner's typed Chat request explicitly;
+this is a controlled fixture, not a test of natural-language team creation. All
+later work assignments use the existing Todo CLI. No active user Goal is used.
+Pass `--root
` to every command to select another isolated run.
+
+Run the dependency chain one phase at a time (default model
+`deepseek-v4-flash`, reasoning `high`; override with `--model`):
+
+```sh
+demo run --phase builder-1 --execute
+demo run --phase analyst-1 --execute
+demo transfer --phase analyst-1
+demo run --phase builder-2 --execute
+demo transfer --phase builder-2
+demo run --phase reviewer-1 --execute
+uv run --extra test python examples/collaboration-delivery/verify.py .local/allocation-demo/agents/reviewer
+demo transfer --phase reviewer-1
+```
+
+Inspect the review and verifier output. A failure is a failure: do not continue
+as though acceptance passed. After passing round one:
+
+```sh
+demo correct
+demo run --phase builder-3 --execute
+demo transfer --phase builder-3
+demo run --phase reviewer-2 --execute
+uv run --extra test python examples/collaboration-delivery/verify.py .local/allocation-demo/agents/reviewer
+demo transfer --phase reviewer-2
+demo run --phase builder-final --execute
+demo readback
+```
+
+`correct` writes a new input version and a new immutable owner request. The old
+request retains its original digest. `readback` uses the real return pump and
+checks that a second drain sends nothing; `conversation.json` retains the
+result. The packaged frontend can show the same brief, receiver facts and returns:
+
+```sh
+uv run --extra test loopx --registry .local/allocation-demo/registry.json \
+ --runtime-root .local/allocation-demo/runtime chat --no-open
+```
+
+Open the printed URL and choose the manager conversation. The generated
+`outputs/model.json`, `solver.py`, `outputs/review-r1.md`, `outputs/review-r2.md`
+and `outputs/final.md` are real worker artifacts. Inspect them, rather than
+counting tool calls or trusting a success sentence. Turn receipts distinguish
+host execution, artifact/route validation, writeback and settlement. The separate
+oracle checks nine positive/infeasible cases and five malformed inputs.
+
+## Conversation preview
+
+Packaged-browser screenshots use synthetic UI fixtures, separate from the live
+model qualification: [before](screenshots/before.png),
+[desktop brief](screenshots/desktop.png), [mobile brief](screenshots/mobile.png).
+
+## Boundaries and recovery
+
+The controller selects bounded phases, transports an explicit artifact list
+through signed synthetic Git commits, and changes the fixture input. Workers
+independently read/assess requests, author artifacts, request peers and return
+results. This demonstrates dependency adoption and continuation across fresh
+sessions, not autonomous scheduling or full roadmap G1/R2 qualification.
+
+The MCP patch binds Goal, Agent and workspace outside model arguments. It keeps
+dsh `workspace-write`, exposes five Inbox tools and adds no shell, Todo/lease
+writer or network listener. Relative references and hashes do not transfer
+files, certify comprehension or transfer ownership. Same-host, same-Goal peers
+are supported; cross-host authority, Lark peer forwarding, stop/lease takeover,
+24-hour continuity and hundred-Agent scale are not qualified by this demo.
+
+To stop, end the active bounded command and launch no further phase. Remove the
+per-worker Cordis patch from your runtime configuration and restart that worker
+to disable the tools; existing Inbox records remain. The demo's source repository
+and worktrees all live under its disposable root. After processes exit, delete
+only that root when its evidence is no longer needed. Never copy its runtime,
+raw model logs, credential configuration or machine paths into public artifacts.
+
+## 中文操作说明
+
+这个 demo 演示的是一条可检查的交付链:**管家交办 → builder 向 analyst
+求助 → 使用模型产物实现求解器 → reviewer 独立复核 → 用户修订要求 →
+重新计算及第二轮复核 → 回到原对话报告结果**。每个阶段启动全新 dsh 会话,
+持久 Inbox、注册身份与真实文件把前后工作连接起来。
+
+按照上面的命令从仓库根目录运行。先安装 `test` 和 `deepseek-harness` extras,
+通过已有凭据管理方式配置 `DEEPSEEK_API_KEY`,再执行 `demo prepare`。
+它只创建隔离的演示目录,不调用模型;七个 `demo run ... --execute` 才会产生
+真实模型调用,每个阶段最多 600 秒。可用 `--model` 选择模型,用 `--root`
+选择另一全新目录;同一轮所有命令应使用相同目录。
+
+`transfer` 由控制器通过 Git 交接指定文件,并记录提交和实际摘要。Agent 自己
+读取材料、决定是否采纳、发起同伴请求、写实现及复核报告、提交结论。两次
+`verify.py` 是独立的穷举验收,不能以 Agent 自称通过代替。第一轮结果应为
+价值 **84**、成本 **1000**;新增“每组预留一件、东区至少四件”后应为价值
+**83**、成本 **990**,分配向量见上表。失败时停下检查结果,不把失败改写成通过。
+
+`demo readback` 会运行原有结果回传服务,并检查重复执行没有新增投递。
+启动上面的 `loopx chat` 后,在管家原对话即可查看交办说明、接收方判断和
+最终结果。无需新增导航入口或改变 Kanban。
+
+这里的初始管家请求由测试控制器明确写入;不是自然语言创建团队的验收。
+它证明同主机、同 Goal 的真实产物协作与新会话续接,不证明无人值守调度、
+跨主机协作、Lark 同伴转发、停机接管、跨日持续运行或百 Agent 规模。
+MCP 仅提供绑定身份的五个 Inbox 工具,保留 dsh 文件沙箱,不授予额外 shell、
+Todo/lease 或发布权限。停止运行后,不再启动下一阶段即可;移除 Cordis 配置
+并重启相应 worker 可停用工具,已有请求与回复保留。确认进程退出后,只删除
+这次演示的隔离目录。凭据、原始日志和本地运行状态不要提交到仓库。
diff --git a/examples/collaboration-delivery/REQUIREMENTS.md b/examples/collaboration-delivery/REQUIREMENTS.md
new file mode 100644
index 0000000000..32d655dea0
--- /dev/null
+++ b/examples/collaboration-delivery/REQUIREMENTS.md
@@ -0,0 +1,20 @@
+# Allocation planner
+
+Implement `python solver.py inputs/scenario.json outputs/plan.json` using exact
+integer arithmetic. Maximize total value under individual demand, shared group
+stock, region capacity and total integer-cent budget. Among optimal solutions,
+choose the lexicographically smallest allocation vector in sorted order-id order.
+Output `allocation` (id → integer), `total_value` and `total_cost`.
+
+Every declared group retains `reserve_per_group` units: its allocation limit is
+`max(0, stock - reserve_per_group)`. Total East allocation must be at least
+`minimum_east`. A zero-demand order receives zero even if its value is high.
+Reject malformed or infeasible inputs with nonzero exit and no success plan.
+Booleans, fractional numbers, negative quantities, duplicate ids and missing
+stock/region declarations are malformed. All quantities/costs/values are
+nonnegative integers; use no floating-point optimization.
+
+The owner rejected proportional rounding: it can violate coupled constraints
+or miss the optimum. Do not place orders or use external services. Independent
+review must open actual artifacts, derive expectations and run the solver;
+model/builder claims alone do not establish correctness.
diff --git a/examples/collaboration-delivery/demo.py b/examples/collaboration-delivery/demo.py
new file mode 100644
index 0000000000..d502197209
--- /dev/null
+++ b/examples/collaboration-delivery/demo.py
@@ -0,0 +1,514 @@
+"""Bounded real-agent demo controller; private state stays in a fresh ignored root."""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+
+from loopx.capabilities.manager_context import deliver, pending
+from loopx.control_plane.collaboration.peers import returns
+from loopx.capabilities.manager_context.roundtrip import drain
+from loopx.chat_store import ChatSessionStore
+
+HERE = Path(__file__).resolve().parent
+GOAL = "allocation-demo"
+ACTORS = ("builder", "analyst", "reviewer")
+TASKS = json.loads((HERE / "tasks.json").read_text())
+ARTIFACTS = {
+ "analyst-1": ("analyst", "builder", ["outputs/model.json"]),
+ "builder-2": (
+ "builder",
+ "reviewer",
+ ["solver.py", "outputs/model.json", "outputs/plan.json"],
+ ),
+ "reviewer-1": ("reviewer", "builder", ["outputs/review-r1.md"]),
+ "builder-3": (
+ "builder",
+ "reviewer",
+ ["solver.py", "inputs/scenario.json", "outputs/plan.json"],
+ ),
+ "reviewer-2": ("reviewer", "builder", ["outputs/review-r2.md"]),
+}
+
+
+def write(path, value):
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(json.dumps(value, indent=2) + "\n")
+
+
+def git(workspace, *args):
+ return subprocess.check_output(
+ [
+ "git",
+ "-C",
+ str(workspace),
+ "-c",
+ "user.name=Demo Controller",
+ "-c",
+ "user.email=demo@example.invalid",
+ *args,
+ ],
+ text=True,
+ ).strip()
+
+
+def worker(root, actor):
+ return root / "agents" / actor
+
+
+def cli(root, *args, cwd=None):
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "loopx.cli",
+ "--registry",
+ str(root / "registry.json"),
+ "--runtime-root",
+ str(root / "runtime"),
+ "--format",
+ "json",
+ *args,
+ ],
+ capture_output=True,
+ text=True,
+ check=False,
+ cwd=cwd,
+ )
+ if result.returncode:
+ (root / "last-cli-failure.log").write_text(result.stdout + "\n" + result.stderr)
+ raise SystemExit(
+ "CLI failed; inspect last-cli-failure.log in the private demo root"
+ )
+ return json.loads(result.stdout)
+
+
+def owner_request(root, correction=False):
+ store = ChatSessionStore(root / "runtime")
+ meta = json.loads((root / "demo.json").read_text())
+ session = store.load_session(meta["session_id"])
+ source = root / "project/inputs/scenario.json"
+ message = (
+ "Correction: reserve one unit in each stock group and allocate at least four "
+ "units to East. Keep all prior constraints and obtain a second independent review."
+ if correction
+ else "Build the allocation planner. Ask analyst for a model and "
+ "reviewer for independent verification; return the verified result here."
+ )
+ turn, _ = store.create_turn(
+ session["session_id"],
+ client_turn_id="correction" if correction else "initial",
+ message=message,
+ origin="web",
+ )
+ brief = {
+ "schema_version": "collaboration_brief_v0",
+ "purpose": message,
+ "context": "The owner rejected proportional rounding. Use exact integer optimization "
+ "and sorted-id lexicographic tie-breaking. Preserve zero-demand behavior.",
+ "constraints": ["Synthetic local files only; no orders or external services"],
+ "inputs": [
+ {
+ "ref": "inputs/scenario.json",
+ "description": "Current allocation input",
+ "sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
+ },
+ {"ref": "REQUIREMENTS.md", "description": "Acceptance contract"},
+ ],
+ "acceptance": [
+ "Runnable solver with independently verified optimum and negative cases"
+ ],
+ "return_requirement": "Return actual artifacts, independent findings and remaining gaps",
+ }
+ receipt = deliver(
+ root / "runtime",
+ root / "registry.json",
+ session=session,
+ turn=turn,
+ request={"goal_id": GOAL, "agent_id": "builder", "brief": brief},
+ )
+ store.update_turn(
+ session["session_id"],
+ turn["turn_id"],
+ status="completing",
+ response={"message": "Delegation saved.", "context_handoff_receipt": receipt},
+ )
+ store.finalize_managed_turn_completion(session["session_id"], turn["turn_id"])
+ meta["requests"].append(receipt["request_id"])
+ write(root / "demo.json", meta)
+
+
+def prepare(root):
+ # Never point the fixture controller at an existing Goal or runtime.
+ root.mkdir(parents=True, exist_ok=False)
+ (root / ".gitignore").write_text("*\n")
+ project = root / "project"
+ (project / "inputs").mkdir(parents=True)
+ shutil.copy(HERE / "scenario.json", project / "inputs/scenario.json")
+ shutil.copy(HERE / "REQUIREMENTS.md", project / "REQUIREMENTS.md")
+ (project / ".gitignore").write_text(".local/\nACTIVE_GOAL_STATE.md\n__pycache__/\n")
+ (project / "ACTIVE_GOAL_STATE.md").write_text(
+ "---\nstatus: active\n---\n# Allocation demo\n\n## User Todo\n\n## Agent Todo\n\n## Next Action\n\n- Run the assigned bounded phase.\n"
+ )
+ git(project, "init", "-b", "main")
+ git(project, "add", ".gitignore", "inputs/scenario.json", "REQUIREMENTS.md")
+ git(project, "commit", "-s", "-m", "Initialize synthetic allocation scenario")
+ # Identity only: this URL is never contacted by the controller.
+ git(
+ project,
+ "remote",
+ "add",
+ "origin",
+ "https://example.invalid/loopx/allocation-demo.git",
+ )
+ registry = {
+ "schema_version": 1,
+ "common_runtime_root": str(root / "runtime"),
+ "goals": [
+ {
+ "id": GOAL,
+ "domain": "synthetic-allocation",
+ "status": "active",
+ "repo": str(project),
+ "state_file": "ACTIVE_GOAL_STATE.md",
+ "adapter": {"kind": "fixture_v0", "status": "connected-delivery"},
+ "quota": {"compute": 20.0, "window_hours": 24},
+ "coordination": {
+ "agent_model": "peer_v1",
+ "registered_agents": list(ACTORS),
+ "write_scope": ["**"],
+ },
+ }
+ ],
+ }
+ write(root / "registry.json", registry)
+ for actor in ACTORS:
+ workspace = worker(root, actor)
+ workspace.parent.mkdir(exist_ok=True)
+ git(project, "worktree", "add", "-b", actor, str(workspace))
+ (workspace / "outputs").mkdir()
+ (workspace / "tasks").mkdir()
+ args = [
+ "-m",
+ "loopx.control_plane.collaboration.mcp",
+ "--registry",
+ str(root / "registry.json"),
+ "--runtime-root",
+ str(root / "runtime"),
+ "--goal-id",
+ GOAL,
+ "--agent-id",
+ actor,
+ "--workspace",
+ str(workspace),
+ ]
+ # JSON is valid YAML and avoids shell/YAML interpolation of host paths.
+ write(
+ root / f"{actor}-cordis.yml",
+ [
+ {
+ "insert": [
+ {
+ "id": "loopx-collaboration",
+ "name": "@deepseek-ai/dsh-mcp-client",
+ "config": {
+ "transport": "stdio",
+ "serverName": "loopx_collaboration",
+ "command": sys.executable,
+ "args": args,
+ "cwd": str(workspace),
+ "failOnStartupError": True,
+ },
+ }
+ ]
+ }
+ ],
+ )
+ store = ChatSessionStore(root / "runtime")
+ session = store.create_session(
+ goal_id="loopx-manager",
+ agent_id="codex",
+ adapter_kind="codex_app_server",
+ upstream_thread_id="demo-owner",
+ channel_id="manager",
+ )
+ write(
+ root / "demo.json",
+ {
+ "schema": "allocation_demo_v1",
+ "session_id": session["session_id"],
+ "requests": [],
+ },
+ )
+ owner_request(root)
+ print("Prepared isolated fixture; no model call has run.")
+
+
+def validate(root, phase):
+ actor = phase.split("-")[0]
+ workspace = worker(root, actor)
+ runtime = root / "runtime"
+ if phase == "builder-1":
+ assert (workspace / "outputs/build-plan.md").stat().st_size > 0
+ assert pending(runtime, GOAL, "analyst")["items"]
+ elif phase in {"analyst-1", "reviewer-1", "reviewer-2"}:
+ for ref in ARTIFACTS[phase][2]:
+ assert (workspace / ref).stat().st_size > 0
+ assert any(
+ row["agent_id"] == actor
+ for row in returns(runtime, GOAL, "builder")["items"]
+ )
+ elif phase in {"builder-2", "builder-3"}:
+ rows = pending(runtime, GOAL, "reviewer")["items"]
+ assert len(rows) == 1
+ refs = {item["ref"]: item.get("sha256") for item in rows[0]["brief"]["inputs"]}
+ for ref in ("solver.py", "inputs/scenario.json", "outputs/plan.json"):
+ assert (
+ refs[ref] == hashlib.sha256((workspace / ref).read_bytes()).hexdigest()
+ )
+ elif phase == "builder-final":
+ assert (workspace / "outputs/final.md").stat().st_size > 0
+ meta = json.loads((root / "demo.json").read_text())
+ assert len(meta["requests"]) == 2
+ for rid in meta["requests"]:
+ assert (
+ runtime / ".local/manager-context/replies" / rid / "conclusion.json"
+ ).exists()
+ print(
+ "Artifact/route validation passed; this is not independent solver acceptance."
+ )
+
+
+def run(root, phase, model, execute):
+ if not execute:
+ raise SystemExit(
+ "Model execution requires --execute and an externally configured DEEPSEEK_API_KEY"
+ )
+ if not os.environ.get("DEEPSEEK_API_KEY"):
+ raise SystemExit("DEEPSEEK_API_KEY is not configured")
+ actor = phase.split("-")[0]
+ workspace = worker(root, actor)
+ meta = json.loads((root / "demo.json").read_text())
+ (workspace / "OPERATING.md").write_text(
+ f"Your identity is {actor}. Use the scoped loopx_collaboration MCP tools read_context, "
+ "assess_request, request_peer, return_result, consume_peer_result. They bind identity "
+ "outside the file sandbox. Do not use CLI writes or request sandbox escalation. "
+ "Open actual files and verify versions. Requests/replies do not grant Todo/lease authority. "
+ "Never edit registry/state, commit Git, use network or invent another Agent's result. "
+ f"Owner request ids: {', '.join(meta['requests'])}.\n"
+ )
+ (workspace / "tasks" / f"{phase}.md").write_text(TASKS[phase] + "\n")
+ todos = cli(root, "todo", "list", "--goal-id", GOAL)["todos"]
+ owned = next(
+ (
+ t
+ for t in todos
+ if t.get("claimed_by") == actor and t.get("status") == "open"
+ ),
+ None,
+ )
+ text = f"Read OPERATING.md and tasks/{phase}.md and perform that bounded collaboration phase."
+ cli(
+ root,
+ "todo",
+ "update",
+ "--goal-id",
+ GOAL,
+ "--todo-id",
+ owned["todo_id"],
+ "--agent-id",
+ actor,
+ "--text",
+ text,
+ ) if owned else cli(
+ root,
+ "todo",
+ "add",
+ "--goal-id",
+ GOAL,
+ "--role",
+ "agent",
+ "--claimed-by",
+ actor,
+ "--text",
+ text,
+ "--action-kind",
+ "implement",
+ )
+ validator = [
+ sys.executable,
+ str(HERE / "demo.py"),
+ "validate",
+ "--root",
+ str(root),
+ "--phase",
+ phase,
+ ]
+ result = cli(
+ root,
+ "turn",
+ "run-once",
+ "--goal-id",
+ GOAL,
+ "--agent-id",
+ actor,
+ "--turn-instance-id",
+ phase,
+ "--host",
+ "dsh",
+ "--execution-mode",
+ "isolated-headless",
+ "--project",
+ str(workspace),
+ "--dsh-home",
+ str(root / f"home-{phase}"),
+ "--dsh-cordis",
+ str(root / f"{actor}-cordis.yml"),
+ "--dsh-model",
+ model,
+ "--dsh-reasoning-effort",
+ "high",
+ "--validation-command-json",
+ json.dumps(validator),
+ "--validation-failure-kind",
+ "repair_required",
+ "--scan-root",
+ str(workspace),
+ "--no-global-sync",
+ "--timeout-seconds",
+ "600",
+ "--execute",
+ cwd=workspace,
+ )
+ write(root / f"{phase}.json", result)
+ print(
+ json.dumps(
+ {k: result.get(k) for k in ("ok", "status", "result_kind", "validation")}
+ )
+ )
+ if result.get("status") != "committed":
+ raise SystemExit(
+ "Phase did not commit; inspect its private receipt before retrying"
+ )
+
+
+def transfer(root, phase):
+ source, target, refs = ARTIFACTS[phase]
+ src, dst = worker(root, source), worker(root, target)
+ git(src, "add", "--", *refs)
+ if git(src, "diff", "--cached", "--name-only"):
+ git(src, "commit", "-s", "-m", f"Deliver {phase} artifacts")
+ commit = git(src, "rev-parse", "HEAD")
+ for ref in refs:
+ (dst / ref).parent.mkdir(parents=True, exist_ok=True)
+ (dst / ref).write_bytes(
+ subprocess.check_output(["git", "-C", str(src), "show", f"{commit}:{ref}"])
+ )
+ write(
+ root / f"{phase}-transfer.json",
+ {
+ "source": source,
+ "target": target,
+ "commit": commit,
+ "artifacts": {
+ ref: hashlib.sha256((dst / ref).read_bytes()).hexdigest()
+ for ref in refs
+ },
+ },
+ )
+ print(f"Transferred {len(refs)} versioned artifacts: {source} -> {target}")
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "action",
+ choices=["prepare", "run", "validate", "transfer", "correct", "readback"],
+ )
+ parser.add_argument("--root", type=Path, default=Path(".local/allocation-demo"))
+ parser.add_argument("--phase", choices=list(TASKS))
+ parser.add_argument("--model", default="deepseek-v4-flash")
+ parser.add_argument("--execute", action="store_true")
+ args = parser.parse_args()
+ root = args.root.resolve()
+ if args.action == "prepare":
+ prepare(root)
+ return
+ if (
+ json.loads((root / "demo.json").read_text()).get("schema")
+ != "allocation_demo_v1"
+ ):
+ raise SystemExit("Not an isolated demo root")
+ if args.action in {"run", "validate", "transfer"} and not args.phase:
+ parser.error("--phase is required")
+ if args.action == "run":
+ run(root, args.phase, args.model, args.execute)
+ elif args.action == "validate":
+ validate(root, args.phase)
+ elif args.action == "transfer":
+ if args.phase not in ARTIFACTS:
+ parser.error("This phase has no artifact transfer")
+ transfer(root, args.phase)
+ elif args.action == "correct":
+ meta = json.loads((root / "demo.json").read_text())
+ if len(meta["requests"]) != 1:
+ raise SystemExit("Correction already created")
+ source = root / "project/inputs/scenario.json"
+ write(
+ source,
+ {
+ **json.loads(source.read_text()),
+ "reserve_per_group": 1,
+ "minimum_east": 4,
+ },
+ )
+ shutil.copy(source, worker(root, "builder") / "inputs/scenario.json")
+ owner_request(root, correction=True)
+ print(
+ "New immutable owner request created; the first request's input version remains unchanged."
+ )
+ elif args.action == "readback":
+ store = ChatSessionStore(root / "runtime")
+ delivered = drain(root / "runtime", root / "registry.json", store, None)
+ assert (
+ drain(
+ root / "runtime",
+ root / "registry.json",
+ ChatSessionStore(root / "runtime"),
+ None,
+ )
+ == 0
+ )
+ from loopx.capabilities.manager_context.roundtrip import (
+ project_chat_session_snapshot,
+ )
+
+ snapshot = project_chat_session_snapshot(
+ root / "runtime",
+ store,
+ json.loads((root / "demo.json").read_text())["session_id"],
+ )
+ write(root / "conversation.json", snapshot)
+ print(
+ json.dumps(
+ {
+ "new_deliveries": delivered,
+ "repeat_deliveries": 0,
+ "conclusions": [
+ m.get("text", m.get("content"))
+ for m in snapshot["messages"]
+ if m.get("origin") == "manager_followup"
+ ],
+ }
+ )
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/collaboration-delivery/scenario.json b/examples/collaboration-delivery/scenario.json
new file mode 100644
index 0000000000..aae12e4235
--- /dev/null
+++ b/examples/collaboration-delivery/scenario.json
@@ -0,0 +1,65 @@
+{
+ "orders": [
+ {
+ "id": "a",
+ "region": "east",
+ "demand": 4,
+ "stock_group": "x",
+ "cost": 110,
+ "value": 9
+ },
+ {
+ "id": "b",
+ "region": "west",
+ "demand": 5,
+ "stock_group": "x",
+ "cost": 90,
+ "value": 7
+ },
+ {
+ "id": "c",
+ "region": "east",
+ "demand": 3,
+ "stock_group": "y",
+ "cost": 140,
+ "value": 12
+ },
+ {
+ "id": "d",
+ "region": "west",
+ "demand": 4,
+ "stock_group": "y",
+ "cost": 120,
+ "value": 10
+ },
+ {
+ "id": "e",
+ "region": "east",
+ "demand": 0,
+ "stock_group": "z",
+ "cost": 30,
+ "value": 20
+ },
+ {
+ "id": "f",
+ "region": "west",
+ "demand": 3,
+ "stock_group": "z",
+ "cost": 70,
+ "value": 6
+ }
+ ],
+ "stock": {
+ "x": 6,
+ "y": 5,
+ "z": 4
+ },
+ "region_cap": {
+ "east": 5,
+ "west": 7
+ },
+ "budget": 1000,
+ "reserve_per_group": 0,
+ "minimum_east": 0,
+ "tie_break": "lexicographically smallest allocation vector in sorted order id order"
+}
\ No newline at end of file
diff --git a/examples/collaboration-delivery/screenshots/before.png b/examples/collaboration-delivery/screenshots/before.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f5d6007fa966e88b080e8b6f23206b559927b7b
GIT binary patch
literal 129910
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