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
29 changes: 29 additions & 0 deletions docs/architecture/rfcs/monorepo-distribution-split-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3780,3 +3780,32 @@ Unicode readback through the real LoopX reader with a synthetic Desktop peer.
This qualifies optional record reading, not model-context retention, managed
V4/R1, split-package M3, installed-App adoption or complete costs. The original
Goal regression, acceptance gaps, authority and frozen failures remain intact.

### 2026-11-13 — Standalone continuation retains its installation

Standalone startup previously returned an ambient `loopx` command even when
the caller selected an isolated core interpreter. Executing that complete
command could import another installation through PATH/PYTHONPATH. Earlier
installed tests discarded the executable and replayed its arguments, hiding
the failure. Core-generated commands now retain the same environment's Python
with `-I -m loopx.entrypoint` through the existing invocation owner. An explicit
global `--cli-bin` and ordinary-distribution defaults retain their behavior;
invalid arguments retain complete parser diagnostics. Multi-Goal selection
uses the same command renderer.

Core module diagnosis verifies its actual imported distribution, recorded
module and present recorded console entrypoints. Version/catalog/help probes
use the same isolated module invocation, so a foreign PYTHONPATH cannot
silently change the artifact being checked. Missing or redirected module
records, absent console records/files, unrecorded launchers and missing runtime
files remain failures. The installed qualification executes whole returned
activation and diagnosis commands, with a foreign-package sentinel and an
explicit-route control, alongside existing SQLite/File recovery and context
checks. This repairs command provenance without adding a lifecycle, authority,
configuration or protocol owner.

Full R1/V1–V5/V10, real model recovery/adoption, progressive product control,
minimal dependency closure, M3/Standard/workspace/meta packaging and complete
cost acceptance remain open. This does not upgrade an installed host, start a
model service or establish effectiveness from protocol fixtures. Preserve all
nine original requirements, the original-Goal regression and earlier failures.
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 @@ -2949,3 +2949,24 @@ Python 3.11 wheel 现已通过真实 LoopX reader 和合成 Desktop peer,验
成功响应断流恢复、拒绝响应断流不重试及完整 Unicode 回读。这只资格化可选记录
读取,不证明模型上下文保留、受管理 V4/R1、拆包 M3、已安装 App 采用或完整成本。
原目标回归、未完成验收、权限边界与冻结失败全部保持。

### 2026-11-13 — 独立 core 续接保留原安装

此前即使调用者选择隔离的 core interpreter,独立启动仍返回环境中的 `loopx`
命令;执行完整命令可能通过 PATH/PYTHONPATH 导入其他安装。旧安装测试丢弃
executable 后重放参数,掩盖了失败。Core 生成的命令现在通过既有 invocation
owner 保留同一环境的 Python,并使用 `-I -m loopx.entrypoint`。显式全局
`--cli-bin` 与普通发行默认行为保持;非法参数保留完整 parser 诊断。
多 Goal 选择也复用同一命令 renderer。

Core module 诊断核验实际导入的 distribution、记录中的 module,以及真实存在
且有记录的 console 入口。Version/catalog/help probe 使用同一隔离 module
调用,foreign PYTHONPATH 无法静默改变被检查的 artifact。缺失或重定向的 module
记录、缺失 console 记录/文件、未记录 launcher 与缺失 runtime 文件仍拒绝。
安装资格测试执行完整的返回激活与诊断命令,使用 foreign-package sentinel 和
显式 route 对照,并覆盖既有 SQLite/File 恢复与 context 检查。本轮修复命令来源,
不新增生命周期、权限、配置或 protocol owner。

完整 R1/V1–V5/V10、真实模型恢复/采用、渐进产品控制、最小依赖闭包、
M3/Standard/workspace/meta 拆包与完整成本验收仍开放。本轮不升级已安装 Host、
启动模型服务或从协议夹具推断效果。原九项要求、原 Goal 回归及此前失败全部保留。
4 changes: 2 additions & 2 deletions loopx/bootstrap_command_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -1299,8 +1299,8 @@ def _build_multi_goal_start_selection_packet(
fine_grained=fine_grained,
),
"commands": {
"doctor": f"{shell_arg_for_host(cli_bin)} doctor",
"status": _project_command(resolved_project, f"{shell_arg_for_host(cli_bin)} status"),
"doctor": f"{render_cli_command_prefix(cli_bin=cli_bin)} doctor",
"status": _project_command(resolved_project, f"{render_cli_command_prefix(cli_bin=cli_bin)} status"),
"goal_selection_choices": choices,
},
"safety_contract": {
Expand Down
6 changes: 5 additions & 1 deletion loopx/cli_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,11 @@ def cli_invocation_route(argv: list[str]):
if option == "--cli-bin":
program = argument
index += 1
with loopx_cli_route(program):
# A standalone core may be invoked without activating its environment. Its
# returned commands must stay on this artifact, even when PATH/PYTHONPATH
# names another LoopX installation. Explicit routes remain caller-owned.
default_argv = [sys.executable, "-I", "-m", "loopx.entrypoint"] if _standalone_core() else None
with loopx_cli_route(program, default_argv=default_argv):
yield


Expand Down
20 changes: 16 additions & 4 deletions loopx/command_invocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import Iterator, Mapping, Sequence


_loopx_cli_bin: ContextVar[str] = ContextVar("loopx_cli_bin", default="loopx")
_loopx_cli_bin: ContextVar[tuple[str, ...]] = ContextVar("loopx_cli_bin", default=("loopx",))


def validate_cli_bin(value: str) -> str:
Expand All @@ -20,17 +20,29 @@ def validate_cli_bin(value: str) -> str:


@contextmanager
def loopx_cli_route(cli_bin: str | None) -> Iterator[None]:
def loopx_cli_route(
cli_bin: str | None, *, default_argv: Sequence[str] | None = None,
) -> Iterator[None]:
"""Bind only this invocation's command rendering; retain no host policy."""
token = _loopx_cli_bin.set(cli_bin if cli_bin is not None else "loopx")
program = cli_bin if cli_bin is not None else "loopx"
route = tuple(default_argv) if cli_bin is None and default_argv is not None else (
(program,) if program == "loopx" else (program, "--cli-bin", program)
)
if not route:
raise ValueError("the default CLI invocation must not be empty")
token = _loopx_cli_bin.set(route)
try:
yield
finally:
_loopx_cli_bin.reset(token)


def loopx_cli_argv(cli_bin: str | None = None) -> list[str]:
program = validate_cli_bin(cli_bin if cli_bin is not None else _loopx_cli_bin.get())
if cli_bin is None:
route = _loopx_cli_bin.get()
validate_cli_bin(route[0])
return list(route)
program = validate_cli_bin(cli_bin)
# The executable selects this process; the option preserves that selection
# when its returned commands start another process. Neither grants authority.
return [program] if program == "loopx" else [program, "--cli-bin", program]
Expand Down
14 changes: 12 additions & 2 deletions loopx/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def command_release_root(command_realpath: Path | None) -> Path | None:


def current_script_invocation_path() -> Path | None:
"""Return the active LoopX executable when invoked through its console script."""
"""Return the active console script or installed standalone-core module."""
if not sys.argv or not sys.argv[0]:
return None
path = Path(sys.argv[0]).expanduser()
Expand All @@ -136,6 +136,12 @@ def current_script_invocation_path() -> Path | None:
path = path.absolute()
if path.exists() and path.name.lower() in {"loopx", "loopx.exe"}:
return path
if path == Path(__file__).with_name("entrypoint.py").resolve():
# Module invocation is the core's isolated continuation route. Verify
# the imported distribution before replacing ambient PATH discovery.
installed = python_distribution_install(Path(__file__).resolve())
if installed.get("distribution_name") == "loopx-core":
return path
return None


Expand Down Expand Up @@ -731,7 +737,11 @@ def collect_doctor(
loopx_path = resolve_command_path("loopx")
invocation_path = current_script_invocation_path()
loopx_canary_path = resolve_command_path("loopx-canary")
command_path_primary = loopx_path or invocation_path
command_path_primary = (
invocation_path
if invocation_path == Path(__file__).with_name("entrypoint.py").resolve()
else loopx_path or invocation_path
)
canary_path = loopx_canary_path
command_path = command_path_primary
command_realpath = command_path.resolve() if command_path else None
Expand Down
26 changes: 21 additions & 5 deletions loopx/release_candidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,17 @@ def _import_summary(module_names: Sequence[str] = REPRESENTATIVE_CLI_IMPORTS) ->
}


def _command_summary(command_path: Path | None) -> dict[str, Any]:
def _command_summary(
command_path: Path | None, *, invocation_argv: Sequence[str] | None = None,
) -> dict[str, Any]:
if command_path is None:
return {"ok": False, "results": {}, "failed": ["command_missing"]}

def probe(item: tuple[str, tuple[str, ...]]) -> tuple[str, dict[str, Any]]:
probe_name, args = item
try:
result = subprocess.run(
command_argv(command_path, args),
[*invocation_argv, *args] if invocation_argv is not None else command_argv(command_path, args),
check=False,
capture_output=True,
text=True, encoding="utf-8", errors="replace",
Expand Down Expand Up @@ -130,7 +132,15 @@ def _distribution_command_summary(command_path: Path | None) -> dict[str, Any]:
}

recorded_commands: list[Path] = []
recorded_module = False
resolved_command = command_path.resolve()
standalone_core = getattr(installed, "name", None) == "loopx-core"
for item in installed.files or ():
if standalone_core and str(item) == "loopx/entrypoint.py":
recorded_module = (
Path(item.locate()).resolve() == resolved_command
== Path(__file__).with_name("entrypoint.py").resolve()
)
if Path(str(item)).name.lower() not in {
"loopx",
"loopx.exe",
Expand All @@ -144,9 +154,10 @@ def _distribution_command_summary(command_path: Path | None) -> dict[str, Any]:
if resolved_path not in recorded_commands:
recorded_commands.append(resolved_path)
recorded_commands.sort(key=str)
resolved_command = command_path.resolve()
return {
"ok": resolved_command in recorded_commands,
"ok": resolved_command in recorded_commands or (
recorded_module and any(path.is_file() for path in recorded_commands)
),
**({"distribution_name": "loopx-core"} if getattr(installed, "name", None) == "loopx-core" else {}),
"command": str(resolved_command),
"recorded_commands": [str(path) for path in recorded_commands],
Expand Down Expand Up @@ -227,7 +238,12 @@ def collect_python_distribution_checks(
distribution_command = _distribution_command_summary(command_path)
standalone_core = distribution_command.get("distribution_name") == "loopx-core"
imports = _import_summary(CORE_CLI_IMPORTS) if standalone_core else _import_summary()
commands = _command_summary(command_path)
if standalone_core and command_path == Path(__file__).with_name("entrypoint.py").resolve():
commands = _command_summary(
command_path, invocation_argv=[sys.executable, "-I", "-m", "loopx.entrypoint"],
)
else:
commands = _command_summary(command_path)
package_paths = _package_path_summary(
package_root,
CORE_DISTRIBUTION_PATHS if standalone_core else REPRESENTATIVE_DISTRIBUTION_PATHS,
Expand Down
7 changes: 6 additions & 1 deletion packages/loopx-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ loopx start-goal --guided --project . --goal-text "Create and check an inventory
Follow its returned host choice and ordered transaction. The preview preserves
the exact goal text and writes no state. Its project bootstrap, fresh identity
registration, session binding and subsequent Todo/guard commands execute in
this standalone distribution through their existing owners. The host supplies
this standalone distribution through their existing owners. Generated commands
use this environment's Python with `-I -m loopx.entrypoint`, so continuation and
installation diagnosis keep using the same core even when PATH or PYTHONPATH
points to another installation. An explicit global `--cli-bin` retains the
caller's selected executable. Deep diagnosis still requires recorded module and
console entrypoints; redirected or missing records fail. The host supplies
the model, tools and scheduler; the wheel does not provide them or activate an
optional capability from goal wording. `connect` and `configure-goal` remain
available for explicit setup. Create and claim work through the existing `todo`
Expand Down
107 changes: 107 additions & 0 deletions tests/test_core_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from __future__ import annotations

import importlib.util
import csv
import json
import os
import re
Expand Down Expand Up @@ -184,6 +185,70 @@ def command(*arguments, expected=0):
assert pack["goal_start_contract"]["explicit_invocation_confirms_project_local_state_writes"] is True


@pytest.mark.parametrize("explicit_route", [False, True])
def test_installed_core_generated_commands_keep_their_distribution(
core_python, tmp_path, monkeypatch, explicit_route,
):
from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime

isolate_sqlite_runtime(tmp_path, monkeypatch)
project, runtime = tmp_path / "project", tmp_path / "runtime"
project.mkdir()
prefix = ["--runtime-root", str(runtime), "--format", "json"]
environment = {"TRAECLI_THREAD_ID": "core-session"}

def command(*arguments):
return json.loads(_run(core_python, list(arguments), project,
environment=environment).stdout)

command(*prefix, "bootstrap", "--project", str(project), "--goal-id", GOAL,
"--objective", "Verify the inventory report.", "--write-scope", "docs/**")
command(*prefix, "register-agent", "--goal-id", GOAL,
"--agent-id", AGENT, "--require-new", "--execute")
command(*prefix, "bind-agent-thread", "--goal-id", GOAL, "--agent-id", AGENT,
"--thread-id", "core-session", "--host-surface", "traex-cli", "--execute")
route = ["--cli-bin", "loopx"] if explicit_route else []
pack = command(*prefix, *route, "bootstrap-command-pack", "--project", str(project),
"--goal-id", GOAL, "--host-surface", "traex-cli")
assert pack["agent_id"] == AGENT
# Execute the complete returned command, rather than discarding its
# executable and replaying only the arguments through the test's Python.
arguments = shlex.split(pack["host_loop_activation"]["activation_input_command"])
if os.name == "nt" and arguments[0] == "&":
arguments = arguments[1:]
foreign = tmp_path / "foreign" / "loopx"
foreign.mkdir(parents=True)
(foreign / "__init__.py").write_text("", encoding="utf-8")
(foreign / "entrypoint.py").write_text(
"from pathlib import Path\n"
"Path('foreign-cli-used').write_text('foreign package', encoding='utf-8')\n"
"def main(argv=None): return 91\n",
encoding="utf-8",
)
inherited = _environment(core_python, environment)
inherited["PYTHONPATH"] = str(foreign.parent)
result = subprocess.run(arguments, cwd=project, env=inherited,
capture_output=True, text=True, timeout=45)
marker = project / "foreign-cli-used"
if explicit_route:
assert result.returncode == 91 and marker.exists(), (result.stdout, result.stderr)
else:
assert result.returncode == 0, (result.stdout, result.stderr)
assert not marker.exists()
payload = json.loads(result.stdout)
assert payload["ok"] and payload["goal_id"] == GOAL and payload["agent_id"] == AGENT
doctor_command = shlex.split(pack["commands"]["doctor"])
if os.name == "nt" and doctor_command[0] == "&":
doctor_command = doctor_command[1:]
diagnosed = subprocess.run(
[*doctor_command, "--installation-only", "--deep", "--format", "json"],
cwd=project, env=inherited, capture_output=True, text=True, timeout=90,
)
assert diagnosed.returncode == 0, (diagnosed.stdout, diagnosed.stderr)
assert json.loads(diagnosed.stdout)["ok"]
assert not marker.exists()


@pytest.mark.parametrize("route", ["help", "preview", "catalog"])
def test_installed_core_start_metadata_defers_issue_fix_execution(core_python, tmp_path, route):
arguments = ["start-goal", "--help"] if route == "help" else [
Expand Down Expand Up @@ -268,6 +333,48 @@ def test_installed_core_deep_doctor_rejects_missing_runtime_file(core_python, tm
backup.rename(original)


@pytest.mark.parametrize("damage", ["missing_module", "redirected_module", "missing_console", "missing_console_file"])
def test_installed_core_module_doctor_requires_recorded_entrypoints(core_python, tmp_path, monkeypatch, damage):
from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime

isolate_sqlite_runtime(tmp_path, monkeypatch)
probe = subprocess.run([core_python, "-I", "-c",
"from importlib.metadata import distribution;"
"print(next(str(p.locate()) for p in distribution('loopx-core').files "
"if str(p).endswith('.dist-info/RECORD')))"],
capture_output=True, text=True, check=True)
record = Path(probe.stdout.strip())
original = record.read_bytes()
rows = list(csv.reader(original.decode("utf-8").splitlines()))
changed = []
for row in rows:
if damage == "missing_console" and Path(row[0]).name.lower() in {"loopx", "loopx.exe", "loopx-script.py"}:
continue
if row[0] == "loopx/entrypoint.py" and damage in {"missing_module", "redirected_module"}:
if damage == "missing_module":
continue
row[0] = "loopx/unrecorded-entrypoint.py"
changed.append(row)
try:
console = Path(core_python).parent / ("loopx.exe" if os.name == "nt" else "loopx")
backup = console.with_suffix(".core-test-backup")
if damage == "missing_console_file":
console.rename(backup)
with record.open("w", encoding="utf-8", newline="") as stream:
csv.writer(stream).writerows(changed)
result = subprocess.run(
[core_python, "-I", "-m", "loopx.entrypoint", "doctor", "--installation-only", "--deep", "--format", "json"],
cwd=tmp_path, env=_environment(core_python), capture_output=True, text=True, timeout=90,
)
assert result.returncode == 1, (result.stdout, result.stderr)
checks = {check["id"]: check for check in json.loads(result.stdout)["checks"]}
assert checks["command_package_same_distribution"]["ok"] is False
finally:
record.write_bytes(original)
if damage == "missing_console_file" and backup.exists():
backup.rename(console)


@pytest.fixture(params=["sqlite", "file"])
def task(tmp_path, monkeypatch, core_python, request):
from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime
Expand Down
Loading