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
26 changes: 26 additions & 0 deletions docs/architecture/rfcs/monorepo-distribution-split-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,32 @@ Approval needs a reviewed decision, not implementation progress or silence.
by the guard, and covering it would be a runtime change with its own
acceptance, not a documentation change.

### 2026-11-03 — Selected host-mode discovery loading

- **Baseline:** `6a6a8590e`. Earlier selected routes for `performance-diagnosis`
and `goal-acceptance` are already on this baseline; the Section 4 audit above
remains historical, not the current implementation inventory.
- **Delta:** `host-mode-plan` uses its existing registrar and handler through
`cli_runtime`, without loading the full CLI or Chat graph on valid requests.
Rejected syntax retains the full-parser diagnostic path. The host selector,
preview commands, readiness and authority rules do not move or change.
- **M1 integration:** [#7878](https://github.com/loopx-project/loopx/pull/7878)
is merged at `ffe84a693`: 17 internal-only Chat owners are under `loopx/chat/`,
with the top-level count and budget both 128 (145 before this move group).
This is a structural delta, not a performance or full-M1 qualification.
- **Evidence:** `tests/test_cli_entrypoint.py` checks owner-only help, JSON and
Markdown parity, unavailable-host previews, invalid requests and zero process,
network or file-write activation. A separately installed wheel exercises help,
ready/unavailable previews and rejection without Node or a live registry.
Local paired fresh-process measurements use 20 samples per revision/workload,
one discarded warm-up and alternating order, with telemetry off and warm file
caches. On macOS arm64/Python 3.13.5, the help/preview LoopX closure falls
from 1,149 to 57 modules; byte-identical output is retained. Timing/RSS belong
to the PR's scoped evidence, not a portable threshold or task-outcome claim.
- **Remaining:** this extends R0-style loading qualification to discovery;
it does not complete R0's full V10 census, M1 public-import migration, split
packaging or R1 managed minimal. D1–D7 remain open. No new preset or default.

## Appendix B: Decision log

| Date | Decision | Owner / approval | Alternatives | Normative sections changed |
Expand Down
21 changes: 21 additions & 0 deletions docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,27 @@ R0 可以先于目录移动交付,但不代表内核合格。R3/R4 可选,
待处理的入口收据仍不被该栅栏覆盖;要覆盖它是一次带自有验收的运行时变更,
而不是文档变更。

### 2026-11-03 — 所选宿主模式发现路径的加载

- **基线:** `6a6a8590e`。此前 `performance-diagnosis` 与 `goal-acceptance`
的 selected 路由已在该基线上;第 4 节审计仍为历史记录,不是当前实现清单。
- **变化:** `host-mode-plan` 经 `cli_runtime` 使用原有 registrar 与 handler,
合法请求不再加载完整 CLI 或 Chat 依赖图。非法语法保留完整 parser 的诊断路径。
宿主选择器、预览命令、readiness 与 authority 规则均不移动、不改变。
- **M1 集成:** [#7878](https://github.com/loopx-project/loopx/pull/7878) 已在
`ffe84a693` 合入:17 个仅内部使用的 Chat owner 位于 `loopx/chat/`,顶层模块
数量和预算均为 128(该移动组之前为 145)。这是结构变化,不代表性能或完整 M1 合格。
- **证据:** `tests/test_cli_entrypoint.py` 覆盖仅加载 owner 的 help、JSON 与
Markdown 一致性、宿主不可用预览、非法请求及零进程、网络或文件写入激活。
独立安装的 wheel 在没有 Node 或实时 registry 时验证 help、ready/unavailable
预览及拒绝。每个版本/负载做 20 次本地成对新进程测量,丢弃一次预热、交替
顺序、关闭遥测并保留热文件缓存。在 macOS arm64/Python 3.13.5 上,help/
preview 的 LoopX 加载闭包从 1,149 降到 57 个模块,输出字节保持一致。时间/RSS
留在 PR 的限定证据中,不作为可移植阈值或任务成果收益声明。
- **剩余:** 将 R0 类型的加载验证扩展到发现路径;不关闭 R0 的完整 V10 清点、
M1 公开 import 迁移、发行物拆分或 R1 managed minimal。D1–D7 仍未决,
不新增预设或改变默认值。

## 附录 B:决策日志

| 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 |
Expand Down
11 changes: 11 additions & 0 deletions loopx/cli_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"performance-diagnosis",
"heartbeat-prompt",
"goal-acceptance",
"host-mode-plan",
}


Expand Down Expand Up @@ -404,6 +405,10 @@ def _build_selected_parser(command: str) -> LoopXArgumentParser:
from .cli_commands.goal_acceptance import register_goal_acceptance_command

register_goal_acceptance_command(subparsers, add_subcommand_format)
elif command == "host-mode-plan":
from .cli_commands.host_mode_plan import register_host_mode_plan_command

register_host_mode_plan_command(subparsers, add_subcommand_format)
else: # pragma: no cover - caller guards the private interface
raise ValueError(f"unsupported selected command: {command}")
return parser
Expand All @@ -415,6 +420,12 @@ def _dispatch_common_command(
registry_path: Path,
allow_missing_registry: bool,
) -> int | None:
if args.command == "host-mode-plan":
from .cli_commands.host_mode_plan import handle_host_mode_plan_command

return handle_host_mode_plan_command(
args, output_format=output_format, print_payload=print_payload,
)
if args.command == "heartbeat-prompt":
from .cli_commands.support_control_heartbeat import handle_heartbeat_prompt_command

Expand Down
81 changes: 81 additions & 0 deletions tests/test_cli_entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ def test_command_reference_does_not_require_default_runtime_route(
(["agent-directory", "--help"], "loopx.cli_commands.agent_directory", "loopx.cli_commands.agent_directory"),
(["agent-capabilities", "--help"], "loopx.cli_commands.agent_capabilities", "loopx.cli_commands.agent_capabilities"),
(["goal-acceptance", "--help"], "loopx.cli_commands.goal_acceptance", "loopx.cli_commands.goal_acceptance"),
(["host-mode-plan", "--help"], "loopx.cli_commands.host_mode_plan", "loopx.cli_commands.host_mode_plan"),
(["goal-portfolio", "--help"], "loopx.cli_commands.summary_all_registration", "loopx.cli_commands.summary_all"),
(["global-summary", "--help"], "loopx.cli_commands.summary_all_registration", "loopx.cli_commands.summary_all"),
(["global-gates", "--help"], "loopx.cli_commands.summary_all_registration", "loopx.cli_commands.summary_all"),
Expand Down Expand Up @@ -837,6 +838,86 @@ def test_selected_goal_acceptance_refusals_match_without_full_cli(tmp_path, argu
assert registry.read_bytes() == before


@pytest.mark.parametrize("format_name", ["json", "markdown"])
@pytest.mark.parametrize("arguments, expected_code", [
(["--intent", "watch_each_turn", "--host-capability", "visible_session",
"--host-identity", "codex-cli", "--registered-agent", "fixture-agent"], 0),
(["--intent", "watch_each_turn", "--host-capability", "visible_session"], 0),
(["--intent", "unsupported-intent"], 1),
(["--host-capability", "unsupported-capability"], 1),
])
def test_selected_host_mode_plan_preserves_results_without_activation(
tmp_path, format_name, arguments, expected_code,
):
registry, runtime = tmp_path / "absent.json", tmp_path / "runtime"
argv = ["--registry", str(registry), "--runtime-root", str(runtime),
"--format", "markdown", "host-mode-plan", "--goal-id", "fixture-goal",
"--cli-bin", "fixture-loopx", "--format", format_name, *arguments]
# An explicit absent registry must not turn a read-only selector into an
# authority read. Its preview is not permission to launch the named host.
environment = {"LOOPX_USAGE_PING": "0"}
selected = run_cli_main("loopx.entrypoint", argv,
forbidden_modules=("loopx.cli", "loopx.chat_server", "loopx.chat_runtime",
"loopx.cli_commands.benchmark_dispatch", "loopx.capabilities.content_ops.cli"),
environment=environment)
full = run_cli_main("loopx.cli", argv, environment=environment)
assert selected.returncode == full.returncode == expected_code, (selected.stderr, full.stderr)
assert selected.stdout == full.stdout
assert selected.stderr == full.stderr == ""
assert not registry.exists() and not runtime.exists()
if format_name == "json" and expected_code == 0:
payload = json.loads(selected.stdout)
assert payload["schema_version"] == "host_mode_plan_v0"
assert payload["goal_id"] == "fixture-goal"
if "--host-identity" in arguments:
assert payload["agent_id"] == "fixture-agent"
assert payload["selected_connector_id"] == "codex_cli_tui"
else:
# A valid preview may expose missing readiness; it must not silently
# select or activate a default host to make the plan executable.
assert payload["selected_connector_id"] is None
assert payload["selected_capability_ready"] is False


def test_selected_host_mode_plan_diagnostics_keep_full_parser_fallback():
cases = [
["host-mode-plan"], ["host-mode-plan", "--help"],
["host-mode-plan", "--goal-id", "fixture-goal", "--unknown"],
["host-mode-plan", "--goal-id", "fixture-goal", "--format", "yaml"],
["host-mode-plan", "--goal-id", "fixture-goal", "--host-identity", "unknown"],
["host-mode-plan", "--goal-id", "fixture-goal", "--host-cap"],
]
assert run_cli_batch("loopx.entrypoint", cases) == run_cli_batch("loopx.cli", cases)


def test_selected_host_mode_plan_does_not_start_processes_or_use_network(tmp_path):
script = f"""
import os
import sys
os.environ["LOOPX_USAGE_PING"] = "0"
sys.dont_write_bytecode = True
os.chdir({str(tmp_path)!r})

def reject_activation(event, args):
assert event not in ("subprocess.Popen", "os.system", "os.posix_spawn", "socket.connect"), event
if event == "open":
mode, flags = args[1:3]
assert not (isinstance(mode, str) and any(c in mode for c in "wax+")), args
assert not (isinstance(flags, int) and flags & (os.O_WRONLY | os.O_RDWR | os.O_CREAT)), args

sys.addaudithook(reject_activation)
from loopx.entrypoint import main
assert main(["host-mode-plan", "--goal-id", "fixture-goal", "--intent", "watch_each_turn",
"--host-capability", "visible_session", "--host-identity", "codex-cli",
"--agent-id", "fixture-agent", "--format", "json"]) == 0
assert "loopx.cli" not in sys.modules
"""
result = run_isolated_script(script)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout)["selected_connector_id"] == "codex_cli_tui"
assert not list(tmp_path.iterdir())


@pytest.mark.parametrize("provider", ["file", "sqlite"])
def test_selected_refresh_preview_keeps_canonical_state_without_full_cli(tmp_path, monkeypatch, provider):
from tests.control_plane.canonical_authority_fixture import initialize_canonical_source_fixture, isolate_sqlite_runtime
Expand Down
Loading