From 2c4d4eb5d8e60df064393bac25aa9ca558f7811d Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Fri, 23 Oct 2026 22:30:26 +0800 Subject: [PATCH 1/3] fix(cli): print host-runnable onboarding and heartbeat commands Route the onboarding, Codex CLI bootstrap, new-project, and heartbeat preflight text through loopx/install_contract.py so native Windows prints PowerShell instead of Bash. Render the two connect commands as one host-quoted command line instead of POSIX backslash continuations. Refs #7158 Signed-off-by: JasonBuildAI --- examples/codex-cli-bootstrap-message-smoke.py | 35 ++- .../codex-cli-first-run-rehearsal-smoke.py | 13 +- ...ex-cli-tui-bootstrap-smoke-bundle-smoke.py | 21 +- ...cli-no-clone-release-verification-smoke.py | 12 +- loopx/agent_onboarding.py | 61 +++--- loopx/install_contract.py | 132 +++++++++-- loopx/project_prompt.py | 91 +++----- tests/test_install_hint_platform.py | 55 +++++ tests/test_onboarding_command_platform.py | 206 ++++++++++++++++++ 9 files changed, 497 insertions(+), 129 deletions(-) create mode 100644 tests/test_onboarding_command_platform.py diff --git a/examples/codex-cli-bootstrap-message-smoke.py b/examples/codex-cli-bootstrap-message-smoke.py index 42e2ba27c5..1a69c41e14 100644 --- a/examples/codex-cli-bootstrap-message-smoke.py +++ b/examples/codex-cli-bootstrap-message-smoke.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import os import subprocess import sys from pathlib import Path @@ -51,9 +52,15 @@ "--bootstrap", "--codex-app", "Do not spend quota for a setup-only turn", - "python3 -m pip install --upgrade loopx", - "workflow-skills --install", - "loopx-project.github.io/loopx/install.sh", + *( + ("loopx doctor",) + if os.name == "nt" + else ( + "python3 -m pip install --upgrade loopx", + "workflow-skills --install", + "loopx-project.github.io/loopx/install.sh", + ) + ), ) @@ -63,9 +70,17 @@ def assert_message_contract(payload: dict[str, object]) -> None: assert payload["invocation_mode"] == "codex_cli_setup_then_goal_mode", payload assert payload["goal_id"] == GOAL_ID, payload assert payload["agent_id"] == AGENT_ID, payload - assert "python3 -m pip install --upgrade loopx" in str(payload["install_repair_command"]), payload - assert "workflow-skills --install" in str(payload["install_repair_command"]), payload - assert "loopx-project.github.io/loopx/install.sh" in str(payload["install_repair_command"]), payload + repair = str(payload["install_repair_command"]) + assert "loopx doctor" in repair, payload + if os.name == "nt": + # Native Windows has no python3 or Bash; the owner prints the checkout + # installer or the running interpreter's pip repair instead. + assert "| bash" not in repair, payload + assert "install-windows.ps1" in repair or "-m pip install --upgrade loopx" in repair, payload + else: + assert "python3 -m pip install --upgrade loopx" in repair, payload + assert "workflow-skills --install" in repair, payload + assert "loopx-project.github.io/loopx/install.sh" in repair, payload assert payload["existing_goal_probe_command"] == payload["quota_guard_command"], payload assert "heartbeat-prompt --thin" in str(payload["heartbeat_prompt_command"]), payload assert "heartbeat-prompt --thin" in str(payload["heartbeat_prompt_json_command"]), payload @@ -229,8 +244,12 @@ def main() -> int: assert "Fresh Repo Install Repair" in cli_markdown, cli_markdown assert "Post-Bootstrap Loop Loader" in cli_markdown, cli_markdown assert "Transcript-Free Validation Checklist" in cli_markdown, cli_markdown - assert "loopx-project.github.io/loopx/install.sh" in cli_markdown, cli_markdown - assert "python3 -m pip install --upgrade loopx" in cli_markdown, cli_markdown + if os.name == "nt": + assert "loopx doctor" in cli_markdown, cli_markdown + assert "| bash" not in cli_markdown, cli_markdown + else: + assert "loopx-project.github.io/loopx/install.sh" in cli_markdown, cli_markdown + assert "python3 -m pip install --upgrade loopx" in cli_markdown, cli_markdown cli_message_only = run_cli( "codex-cli-bootstrap-message", diff --git a/examples/codex-cli-first-run-rehearsal-smoke.py b/examples/codex-cli-first-run-rehearsal-smoke.py index fdd83e879d..ac64e15fb8 100644 --- a/examples/codex-cli-first-run-rehearsal-smoke.py +++ b/examples/codex-cli-first-run-rehearsal-smoke.py @@ -3,6 +3,7 @@ from __future__ import annotations +import os import subprocess import sys from pathlib import Path @@ -95,8 +96,16 @@ def assert_cli_surfaces_align() -> None: assert not message.startswith("/goal "), message assert "setup/bootstrap instruction" in normalized, message assert "/goal " in normalized, message - assert "python3 -m pip install --upgrade loopx" in normalized, message - assert "workflow-skills --install" in normalized, message + assert "loopx doctor" in normalized, message + if os.name == "nt": + assert "| bash" not in normalized, message + assert ( + "install-windows.ps1" in normalized + or "-m pip install --upgrade loopx" in normalized + ), message + else: + assert "python3 -m pip install --upgrade loopx" in normalized, message + assert "workflow-skills --install" in normalized, message assert "Codex CLI TUI" in normalized, message assert "quota should-run" in normalized, message assert "settlement_plan.ordered_steps" in normalized, message diff --git a/examples/codex-cli-tui-bootstrap-smoke-bundle-smoke.py b/examples/codex-cli-tui-bootstrap-smoke-bundle-smoke.py index 78200a9ad6..2b59e7a1dd 100644 --- a/examples/codex-cli-tui-bootstrap-smoke-bundle-smoke.py +++ b/examples/codex-cli-tui-bootstrap-smoke-bundle-smoke.py @@ -137,9 +137,18 @@ def main() -> None: assert bundle["agent_id"] == AGENT_ID, bundle assert "--message-only" in bundle["message_only_command"], bundle assert str(fresh_repo) in bundle["message_only_command"], bundle - assert "loopx-project.github.io/loopx/install.sh" in bundle["install_repair_command"], bundle - assert "python3 -m pip install --upgrade loopx" in bundle["install_repair_command"], bundle - assert "workflow-skills --install" in bundle["install_repair_command"], bundle + repair = bundle["install_repair_command"] + assert "loopx doctor" in repair, bundle + if os.name == "nt": + assert "| bash" not in repair, bundle + assert ( + "install-windows.ps1" in repair + or "-m pip install --upgrade loopx" in repair + ), bundle + else: + assert "loopx-project.github.io/loopx/install.sh" in repair, bundle + assert "python3 -m pip install --upgrade loopx" in repair, bundle + assert "workflow-skills --install" in repair, bundle assert "quota should-run" in bundle["quota_guard_command"], bundle assert "--agent-id codex-side-bypass" in bundle["quota_guard_command"], bundle assert "refresh-state --goal-id public-fresh-codex-cli-goal" in bundle["refresh_command"], bundle @@ -183,8 +192,10 @@ def main() -> None: assert not message_only.startswith("/goal "), message_only assert "# Codex CLI LoopX Bootstrap Message" not in message_only, message_only assert "Fresh Repo Install Repair" not in message_only, message_only - assert "loopx-project.github.io/loopx/install.sh" in message_only, message_only - assert "python3 -m pip install --upgrade loopx" in message_only, message_only + assert "loopx doctor" in message_only, message_only + if os.name != "nt": + assert "loopx-project.github.io/loopx/install.sh" in message_only, message_only + assert "python3 -m pip install --upgrade loopx" in message_only, message_only assert "/goal " in message_only, message_only assert "quota should-run" in message_only, message_only assert "refresh-state --goal-id public-fresh-codex-cli-goal" in message_only, message_only diff --git a/examples/release/codex-cli-no-clone-release-verification-smoke.py b/examples/release/codex-cli-no-clone-release-verification-smoke.py index a18db94bcb..20671572b6 100644 --- a/examples/release/codex-cli-no-clone-release-verification-smoke.py +++ b/examples/release/codex-cli-no-clone-release-verification-smoke.py @@ -161,8 +161,16 @@ def assert_installed_release( assert not message.startswith("/goal "), message assert "setup/bootstrap instruction" in normalized_message, message assert "/goal " in normalized_message, message - assert "python3 -m pip install --upgrade loopx" in normalized_message, message - assert "workflow-skills --install" in normalized_message, message + assert "loopx doctor" in normalized_message, message + if os.name == "nt": + assert "| bash" not in normalized_message, message + assert ( + "install-windows.ps1" in normalized_message + or "-m pip install --upgrade loopx" in normalized_message + ), message + else: + assert "python3 -m pip install --upgrade loopx" in normalized_message, message + assert "workflow-skills --install" in normalized_message, message assert "Codex CLI TUI" in normalized_message, message assert "quota should-run" in normalized_message, message assert "settlement_plan.ordered_steps" in normalized_message, message diff --git a/loopx/agent_onboarding.py b/loopx/agent_onboarding.py index a3ae62998b..b71c04e60e 100644 --- a/loopx/agent_onboarding.py +++ b/loopx/agent_onboarding.py @@ -30,6 +30,7 @@ render_codex_cli_install_preflight, render_quota_guard_command, shell_arg, + shell_arg_for_host, ) from .registry import registry_goals from .skill_install_readback import ( @@ -46,31 +47,31 @@ def _surface_install_command(agent_type: str, cli_bin: str, project: str) -> str | None: if agent_type in {"codex-app", "codex-app-ssh", "codex-ide-plugin", "codex-cli"}: - return f"{shell_arg(cli_bin)} slash-commands --install --surface codex" + return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface codex" if agent_type == "claude-code": - return f"{shell_arg(cli_bin)} slash-commands --install --surface claude-code" + return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface claude-code" if agent_type == "opencode": return ( - f"{shell_arg(cli_bin)} slash-commands --install --surface opencode " + f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface opencode " "--with-goal-bridge" ) if agent_type == "gemini-cli": - return f"{shell_arg(cli_bin)} slash-commands --install --surface gemini" + return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface gemini" if agent_type == "cursor-agent": - return f"{shell_arg(cli_bin)} slash-commands --install --surface cursor" + return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface cursor" if agent_type == "zcode": - return f"{shell_arg(cli_bin)} slash-commands --install --surface zcode" + return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface zcode" if agent_type == "agy": - return f"{shell_arg(cli_bin)} slash-commands --install --surface agy" + return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface agy" if agent_type == "kiro-cli": - return f"{shell_arg(cli_bin)} slash-commands --install --surface kiro-cli" + return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface kiro-cli" if agent_type == "pi": # The slash-commands installer resolves the Pi extension target through # --pi-project; pass the resolved project so the command stays correct # when agent-onboard runs from any cwd. return ( - f"{shell_arg(cli_bin)} slash-commands --install --surface pi " - f"--pi-project {shell_arg(project)}" + f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface pi " + f"--pi-project {shell_arg_for_host(project)}" ) return None @@ -102,15 +103,15 @@ def _project_skill_command( execute: bool = False, ) -> str: parts = [ - shell_arg(cli_bin), + shell_arg_for_host(cli_bin), "project-skill", command, "--project", - shell_arg(project), + shell_arg_for_host(project), "--skill", - shell_arg(skill_id), + shell_arg_for_host(skill_id), "--surface", - shell_arg(surface), + shell_arg_for_host(surface), ] if execute: parts.append("--execute") @@ -303,21 +304,21 @@ def _bootstrap_pack_command( "other-agent": "other-agent", } parts = [ - shell_arg(cli_bin), + shell_arg_for_host(cli_bin), "bootstrap-command-pack", "--project", - shell_arg(project), + shell_arg_for_host(project), "--goal-id", - shell_arg(goal_id), + shell_arg_for_host(goal_id), "--host-surface", - shell_arg(surface_by_type[agent_type]), + shell_arg_for_host(surface_by_type[agent_type]), ] if agent_id: - parts.extend(["--agent-id", shell_arg(agent_id)]) + parts.extend(["--agent-id", shell_arg_for_host(agent_id)]) if task_text: - parts.extend(["--goal-text", shell_arg(task_text)]) + parts.extend(["--goal-text", shell_arg_for_host(task_text)]) for capability in available_capabilities or []: - parts.extend(["--available-capability", shell_arg(capability)]) + parts.extend(["--available-capability", shell_arg_for_host(capability)]) return " ".join(parts) @@ -514,12 +515,12 @@ def build_agent_onboarding_packet( else None ), "agent_onboard_recheck": ( - f"{shell_arg(cli_bin)} agent-onboard " - f"--agent-type {shell_arg(canonical_agent_type)} " - f"--project {shell_arg(resolved_project)} " - f"--goal-id {shell_arg(resolved_goal_id)}" + f"{shell_arg_for_host(cli_bin)} agent-onboard " + f"--agent-type {shell_arg_for_host(canonical_agent_type)} " + f"--project {shell_arg_for_host(resolved_project)} " + f"--goal-id {shell_arg_for_host(resolved_goal_id)}" + ( - f" --agent-id {shell_arg(str(selected_agent_id))}" + f" --agent-id {shell_arg_for_host(str(selected_agent_id))}" if selected_agent_id else "" ) @@ -530,11 +531,11 @@ def build_agent_onboarding_packet( commands["install_command_facade"] = install_command if canonical_agent_type == "codex-cli": commands["codex_cli_bootstrap_message"] = ( - f"{shell_arg(cli_bin)} codex-cli-bootstrap-message " - f"--project {shell_arg(resolved_project)} " - f"--goal-id {shell_arg(resolved_goal_id)}" + f"{shell_arg_for_host(cli_bin)} codex-cli-bootstrap-message " + f"--project {shell_arg_for_host(resolved_project)} " + f"--goal-id {shell_arg_for_host(resolved_goal_id)}" + ( - f" --agent-id {shell_arg(str(selected_agent_id))}" + f" --agent-id {shell_arg_for_host(str(selected_agent_id))}" if selected_agent_id else "" ) diff --git a/loopx/install_contract.py b/loopx/install_contract.py index ea7469587d..f7cab2d774 100644 --- a/loopx/install_contract.py +++ b/loopx/install_contract.py @@ -1,15 +1,16 @@ """Install, repair, and fallback command text printed to LoopX operators. -One platform-aware owner keeps the bootstrap/connect hint, the doctor freshness -command, and the archive fallback consistent. POSIX keeps the documented public -defaults; native Windows has neither a ``python3`` launcher nor Bash, so it -names the interpreter that runs here and prints the snapshot installer only for a -checkout that actually ships it. +One platform-aware owner keeps the bootstrap/connect hint, the preflight prompt, +the doctor freshness command, and the archive fallback consistent. POSIX keeps +the documented public defaults; native Windows has neither a ``python3`` launcher +nor Bash, so it names the interpreter that runs here and prints the snapshot +installer only for a checkout that actually ships it. """ from __future__ import annotations import os +import re import shlex import sys from pathlib import Path @@ -32,10 +33,21 @@ ) +_POWERSHELL_SAFE_COMMAND = re.compile(r"^[A-Za-z0-9_./-]+$") + + def _powershell_literal(value: str | Path) -> str: return "'" + str(value).replace("'", "''") + "'" +def _powershell_command(value: str) -> str: + """A command name or path PowerShell can invoke from the start of a pipeline.""" + + if _POWERSHELL_SAFE_COMMAND.fullmatch(value): + return value + return f"& {_powershell_literal(value)}" + + def _is_windows_host() -> bool: """Whether printed commands target PowerShell instead of a POSIX shell.""" @@ -57,13 +69,16 @@ def _checkout_snapshot_installer(repo_root: Path) -> Path | None: return None -def _windows_pypi_upgrade_command(*, doctor_agent_arg: str, skip_skills: bool) -> str: +def _windows_pypi_upgrade_command( + *, doctor_agent_arg: str, skip_skills: bool, cli_bin: str = "loopx" +) -> str: """No-clone upgrade through the interpreter that owns this installation.""" + command = _powershell_command(cli_bin) lines = [f"& {_powershell_literal(sys.executable)} -m pip install --upgrade loopx"] if not skip_skills: - lines.append(DEFAULT_WORKFLOW_SKILL_INSTALL_COMMAND) - lines.append(f"loopx doctor{doctor_agent_arg}") + lines.append(f"{command} workflow-skills --install") + lines.append(f"{command} doctor{doctor_agent_arg}") return "\n".join(lines) @@ -84,6 +99,7 @@ def no_clone_upgrade_command( *, doctor_agent_type: str | None = None, skip_skills: bool = False, + cli_bin: str = "loopx", ) -> str: if _is_windows_host(): repo_root = Path(__file__).resolve().parents[1] @@ -95,10 +111,12 @@ def no_clone_upgrade_command( if _checkout_snapshot_installer(repo_root) is not None: return ( f"{local_install_command(repo_root, skip_skills=skip_skills)}\n" - f"loopx doctor{doctor_agent_arg}" + f"{_powershell_command(cli_bin)} doctor{doctor_agent_arg}" ) return _windows_pypi_upgrade_command( - doctor_agent_arg=doctor_agent_arg, skip_skills=skip_skills + doctor_agent_arg=doctor_agent_arg, + skip_skills=skip_skills, + cli_bin=cli_bin, ) ref = str(source_ref or "").strip() installer = f"curl -fsSL {NO_CLONE_INSTALL_URL}" @@ -119,21 +137,101 @@ def no_clone_upgrade_command( return ( f"{installer}\n" 'export PATH="$HOME/.local/bin:$PATH"\n' - f"loopx doctor{doctor_agent_arg}" + f"{shlex.quote(cli_bin)} doctor{doctor_agent_arg}" ) -def install_repair_command() -> str: +def install_repair_command(*, cli_bin: str = "loopx") -> str: """PyPI repair guidance that runs as printed on this host.""" if _is_windows_host(): - return _windows_pypi_upgrade_command(doctor_agent_arg="", skip_skills=False) - return DEFAULT_INSTALL_REPAIR_COMMAND + return _windows_pypi_upgrade_command( + doctor_agent_arg="", skip_skills=False, cli_bin=cli_bin + ) + if cli_bin == "loopx": + return DEFAULT_INSTALL_REPAIR_COMMAND + quoted_cli = shlex.quote(cli_bin) + return ( + f"{DEFAULT_INSTALL_COMMAND}\n" + f"{quoted_cli} workflow-skills --install --cli-bin {quoted_cli}\n" + f"{quoted_cli} doctor" + ) -def archive_fallback_install_command() -> str: +def archive_fallback_install_command(*, cli_bin: str = "loopx") -> str: """Archive fallback guidance that runs as printed on this host.""" if _is_windows_host(): - return no_clone_upgrade_command() - return ARCHIVE_FALLBACK_INSTALL_COMMAND + return no_clone_upgrade_command(cli_bin=cli_bin) + if cli_bin == "loopx": + return ARCHIVE_FALLBACK_INSTALL_COMMAND + return ( + f"curl -fsSL {NO_CLONE_INSTALL_URL} | bash\n" + 'export PATH="$HOME/.local/bin:$PATH"\n' + f"{shlex.quote(cli_bin)} doctor" + ) + + +def doctor_preflight_command(*, cli_bin: str = "loopx") -> str: + """Ensure the CLI is reachable without upgrading on every scheduled turn.""" + + if _is_windows_host(): + return "\n".join( + [ + f"if (-not (Get-Command {_powershell_literal(cli_bin)} -ErrorAction SilentlyContinue)) {{", + f" & {_powershell_literal(sys.executable)} -m pip install --upgrade loopx", + "}", + f"{_powershell_command(cli_bin)} doctor | Out-Null", + ] + ) + cli_bin_arg = shlex.quote(cli_bin) + return f"""export PATH="$HOME/.local/bin:$PATH" +install_script="$HOME/loopx/scripts/install-local.sh" +if ! command -v {cli_bin_arg} >/dev/null 2>&1; then + if [ -x "$install_script" ]; then + "$install_script" + export PATH="$HOME/.local/bin:$PATH" + else + echo "loopx is not on PATH; clone the LoopX repo and run scripts/install-local.sh" >&2 + exit 1 + fi +fi +{cli_bin_arg} doctor >/dev/null""" + + +def codex_cli_install_preflight( + *, + cli_bin: str = "loopx", + doctor_agent_type: str | None = None, +) -> str: + """Install-or-doctor guidance a Codex CLI operator can paste on this host.""" + + if _is_windows_host(): + return no_clone_upgrade_command( + doctor_agent_type=doctor_agent_type, + cli_bin=cli_bin, + ) + cli_bin_arg = shlex.quote(cli_bin) + doctor_agent_arg = ( + f" --agent-type {shlex.quote(doctor_agent_type)}" + if doctor_agent_type + else "" + ) + return f"""export PATH="$HOME/.local/bin:$PATH" +if ! command -v {cli_bin_arg} >/dev/null 2>&1; then + if command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; then + python3 -m pip install --upgrade loopx + if command -v {cli_bin_arg} >/dev/null 2>&1; then + {cli_bin_arg} workflow-skills --install --cli-bin {cli_bin_arg} + fi + fi + if ! command -v {cli_bin_arg} >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then + curl -fsSL {NO_CLONE_INSTALL_URL} | bash + export PATH="$HOME/.local/bin:$PATH" + fi + if ! command -v {cli_bin_arg} >/dev/null 2>&1; then + echo "loopx is not on PATH; use a Python 3.11+ environment, the archive fallback, or a contributor checkout" >&2 + exit 1 + fi +fi +{cli_bin_arg} doctor{doctor_agent_arg} >/dev/null""" diff --git a/loopx/project_prompt.py b/loopx/project_prompt.py index 9d7435c3ed..88724a6d68 100644 --- a/loopx/project_prompt.py +++ b/loopx/project_prompt.py @@ -23,7 +23,7 @@ scheduler_runtime_profile_for_execution_context, ) from .control_plane.todos.contract import normalize_required_capabilities -from .install_contract import NO_CLONE_INSTALL_URL +from . import install_contract DEFAULT_HANDOFF_OBJECTIVE = "" DEFAULT_HANDOFF_DOMAIN = "" @@ -141,19 +141,7 @@ def render_available_capability_args(values: Any) -> str: def render_cli_preflight(*, cli_bin: str = "loopx") -> str: - cli_bin_arg = shell_arg(cli_bin) - return f"""export PATH="$HOME/.local/bin:$PATH" -install_script="$HOME/loopx/scripts/install-local.sh" -if ! command -v {cli_bin_arg} >/dev/null 2>&1; then - if [ -x "$install_script" ]; then - "$install_script" - export PATH="$HOME/.local/bin:$PATH" - else - echo "loopx is not on PATH; clone the LoopX repo and run scripts/install-local.sh" >&2 - exit 1 - fi -fi -{cli_bin_arg} doctor >/dev/null""" + return install_contract.doctor_preflight_command(cli_bin=cli_bin) def render_codex_cli_install_preflight( @@ -161,30 +149,10 @@ def render_codex_cli_install_preflight( cli_bin: str = "loopx", doctor_agent_type: str | None = None, ) -> str: - cli_bin_arg = shell_arg(cli_bin) - doctor_agent_arg = ( - f" --agent-type {shell_arg(doctor_agent_type)}" - if doctor_agent_type - else "" + return install_contract.codex_cli_install_preflight( + cli_bin=cli_bin, + doctor_agent_type=doctor_agent_type, ) - return f"""export PATH="$HOME/.local/bin:$PATH" -if ! command -v {cli_bin_arg} >/dev/null 2>&1; then - if command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; then - python3 -m pip install --upgrade loopx - if command -v {cli_bin_arg} >/dev/null 2>&1; then - {cli_bin_arg} workflow-skills --install --cli-bin {cli_bin_arg} - fi - fi - if ! command -v {cli_bin_arg} >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then - curl -fsSL {NO_CLONE_INSTALL_URL} | bash - export PATH="$HOME/.local/bin:$PATH" - fi - if ! command -v {cli_bin_arg} >/dev/null 2>&1; then - echo "loopx is not on PATH; use a Python 3.11+ environment, the archive fallback, or a contributor checkout" >&2 - exit 1 - fi -fi -{cli_bin_arg} doctor{doctor_agent_arg} >/dev/null""" def render_quota_guard_command( @@ -407,29 +375,24 @@ def render_connect_command( cli_bin: str = "loopx", runtime_root: str | Path | None = None, ) -> str: - lines = [ - f"cd {shell_arg(project)}", - f"{render_cli_command_prefix(cli_bin=cli_bin, runtime_root=runtime_root)} connect \\", - f" --goal-id {shell_arg(goal_id)} \\", - f" --objective {shell_arg(objective)} \\", - f" --domain {shell_arg(domain)} \\", - f" --goal-doc {shell_arg(goal_doc)} \\", - f" --adapter-kind {shell_arg(adapter_kind)} \\", - f" --adapter-status {shell_arg(adapter_status)}", - ] + command = ( + f"{render_cli_command_prefix(cli_bin=cli_bin, runtime_root=runtime_root)} connect" + f" --goal-id {shell_arg_for_host(goal_id)}" + f" --objective {shell_arg_for_host(objective)}" + f" --domain {shell_arg_for_host(domain)}" + f" --goal-doc {shell_arg_for_host(goal_doc)}" + f" --adapter-kind {shell_arg_for_host(adapter_kind)}" + f" --adapter-status {shell_arg_for_host(adapter_status)}" + ) if next_probe: - lines[-1] += " \\" - lines.append(f" --next-probe {shell_arg(next_probe)}") + command += f" --next-probe {shell_arg_for_host(next_probe)}" if spawn_allowed: - lines[-1] += " \\" - lines.append(" --spawn-allowed") + command += " --spawn-allowed" for allowed_domain in allowed_domains: - lines[-1] += " \\" - lines.append(f" --allowed-domain {shell_arg(allowed_domain)}") + command += f" --allowed-domain {shell_arg_for_host(allowed_domain)}" for scope in write_scope: - lines[-1] += " \\" - lines.append(f" --write-scope {shell_arg(scope)}") - return "\n".join(lines) + command += f" --write-scope {shell_arg_for_host(scope)}" + return "\n".join([f"cd {shell_arg_for_host(project)}", command]) def build_new_project_prompt( @@ -546,16 +509,14 @@ def render_codex_cli_bootstrap_connect_command( goal_id: str, cli_bin: str, ) -> str: - return "\n".join( - [ - f"cd {shell_arg(project)}", - f"{shell_arg(cli_bin)} bootstrap \\", - " --project . \\", - f" --goal-id {shell_arg(goal_id)} \\", - f" --adapter-kind {shell_arg(DEFAULT_HANDOFF_ADAPTER_KIND)} \\", - f" --adapter-status {shell_arg(DEFAULT_HANDOFF_ADAPTER_STATUS)}", - ] + command = ( + f"{shell_arg_for_host(cli_bin)} bootstrap" + " --project ." + f" --goal-id {shell_arg_for_host(goal_id)}" + f" --adapter-kind {shell_arg_for_host(DEFAULT_HANDOFF_ADAPTER_KIND)}" + f" --adapter-status {shell_arg_for_host(DEFAULT_HANDOFF_ADAPTER_STATUS)}" ) + return "\n".join([f"cd {shell_arg_for_host(project)}", command]) def build_codex_cli_bootstrap_message( diff --git a/tests/test_install_hint_platform.py b/tests/test_install_hint_platform.py index e39bd4541f..c2a97a37a4 100644 --- a/tests/test_install_hint_platform.py +++ b/tests/test_install_hint_platform.py @@ -227,6 +227,61 @@ def test_windows_installed_fallback_keeps_doctor_options_and_skips_owned_skills( ] +@pytest.mark.parametrize("os_name", ["posix", "nt"]) +def test_preflight_commands_follow_the_host_platform( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, os_name: str +) -> None: + monkeypatch.setattr(install_contract, "_is_windows_host", lambda: os_name == "nt") + if os_name == "nt": + pretend_module_root(monkeypatch, tmp_path / "site-packages") + monkeypatch.setattr(sys, "executable", PLAIN_WINDOWS_INTERPRETER) + doctor = install_contract.doctor_preflight_command(cli_bin="loopx-canary") + codex = install_contract.codex_cli_install_preflight( + cli_bin="loopx-canary", doctor_agent_type="codex-cli" + ) + + if os_name == "nt": + for command in (doctor, codex): + for token in POSIX_ONLY_TOKENS: + assert token not in command, command + assert "Get-Command" in doctor + assert doctor.splitlines()[-1] == "loopx-canary doctor | Out-Null" + assert powershell_literal(PLAIN_WINDOWS_INTERPRETER) in doctor + assert codex.splitlines()[0] == ( + f"& {powershell_literal(PLAIN_WINDOWS_INTERPRETER)} " + "-m pip install --upgrade loopx" + ) + assert "loopx-canary workflow-skills --install" in codex + assert f"loopx-canary doctor --agent-type {powershell_literal('codex-cli')}" in codex + else: + assert doctor.startswith('export PATH="$HOME/.local/bin:$PATH"') + assert "loopx-canary doctor >/dev/null" in doctor + assert "python3 -m pip install --upgrade loopx" in codex + assert "loopx-canary doctor --agent-type codex-cli >/dev/null" in codex + + +@pytest.mark.skipif(PWSH is None, reason="requires pwsh to parse the printed commands") +def test_preflight_commands_parse_as_powershell( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + pwsh = PWSH + assert pwsh is not None + pretend_windows_host(monkeypatch, PLAIN_WINDOWS_INTERPRETER) + pretend_module_root(monkeypatch, tmp_path / "site-packages") + commands = { + "doctor": install_contract.doctor_preflight_command(), + "codex": install_contract.codex_cli_install_preflight(doctor_agent_type="codex-cli"), + } + + for name, command in commands.items(): + script = tmp_path / f"{name}-preflight.ps1" + script.write_text(command, encoding="utf-8") + + errors = powershell_parse_errors(pwsh, script) + + assert errors == "", (name, errors) + + @pytest.mark.skipif(PWSH is None, reason="requires pwsh to parse the printed commands") @pytest.mark.parametrize("layout", ["checkout", "installed"]) @pytest.mark.parametrize("interpreter", WINDOWS_INTERPRETERS) diff --git a/tests/test_onboarding_command_platform.py b/tests/test_onboarding_command_platform.py new file mode 100644 index 0000000000..8c4c1295a8 --- /dev/null +++ b/tests/test_onboarding_command_platform.py @@ -0,0 +1,206 @@ +"""Onboarding and heartbeat packets must print commands the host can run.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +import loopx.project_prompt as project_prompt +from loopx import install_contract +from loopx.agent_onboarding import build_agent_onboarding_packet +from loopx.project_prompt import ( + build_codex_cli_bootstrap_message, + build_new_project_prompt, + render_cli_preflight, +) + +PWSH = shutil.which("pwsh") +GOAL_ID = "windows-onboarding-host" +POSIX_ONLY_TOKENS = ( + "python3 ", + "| bash", + "export PATH=", + "command -v ", + ">/dev/null", +) + + +def pretend_windows_host( + monkeypatch: pytest.MonkeyPatch, + interpreter: str = r"C:\Python311\python.exe", +) -> None: + """Exercise every Windows branch without switching ``os.name``.""" + + monkeypatch.setattr(install_contract, "_is_windows_host", lambda: True) + monkeypatch.setattr(project_prompt, "_is_windows_host", lambda: True) + monkeypatch.setattr(sys, "executable", interpreter) + + +def assert_powershell_ready(command: str) -> None: + assert command.strip(), "empty command" + assert "\\\n" not in command, command + for token in POSIX_ONLY_TOKENS: + assert token not in command, (token, command) + + +def project_with_apostrophe(tmp_path: Path) -> Path: + project = tmp_path / "project's" + project.mkdir() + return project + + +def test_windows_codex_bootstrap_message_prints_powershell_commands( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + pretend_windows_host(monkeypatch) + project = project_with_apostrophe(tmp_path) + payload = build_codex_cli_bootstrap_message( + project=project, + goal_id=GOAL_ID, + agent_id="host-agent", + cli_bin="loopx", + ) + + assert_powershell_ready(str(payload["install_repair_command"])) + connect = str(payload["connect_command"]) + assert_powershell_ready(connect) + assert len(connect.splitlines()) == 2 + assert connect.splitlines()[0].startswith("cd ") + assert connect.splitlines()[1].startswith("loopx bootstrap --project .") + assert "project''s" in connect + assert "'\\''" not in connect + + +def test_windows_agent_onboard_prints_powershell_commands( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + pretend_windows_host(monkeypatch) + project = project_with_apostrophe(tmp_path) + payload = build_agent_onboarding_packet( + project=project, + agent_type="codex-cli", + goal_id=GOAL_ID, + agent_id="host-agent", + ) + commands = payload["commands"] + assert isinstance(commands, dict) + + for key in ("doctor_or_install", "bootstrap_command_pack", "agent_onboard_recheck"): + assert_powershell_ready(str(commands[key])) + assert "project''s" in str(commands["bootstrap_command_pack"]) + assert "project''s" in str(commands["agent_onboard_recheck"]) + + +def test_windows_new_project_prompt_prints_powershell_commands( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + pretend_windows_host(monkeypatch) + project = project_with_apostrophe(tmp_path) + payload = build_new_project_prompt( + project=project, + goal_doc=project / "GOAL.md", + goal_id=GOAL_ID, + objective="Verify host-runnable onboarding", + domain="example", + adapter_kind="read_only_project_map_v0", + adapter_status="connected-read-only", + next_probe=None, + spawn_allowed=True, + allowed_domains=["example.com"], + write_scope=["docs/**"], + ) + + assert_powershell_ready(str(payload["cli_preflight"])) + connect = str(payload["connect_command"]) + assert_powershell_ready(connect) + assert len(connect.splitlines()) == 2 + assert "project''s" in connect + assert "--spawn-allowed" in connect + assert "--allowed-domain example.com" in connect + assert "--write-scope 'docs/**'" in connect + + +def test_windows_heartbeat_preflight_does_not_upgrade_every_turn( + monkeypatch: pytest.MonkeyPatch, +) -> None: + pretend_windows_host(monkeypatch) + command = render_cli_preflight() + + assert_powershell_ready(command) + assert "Get-Command" in command + assert command.count("-m pip install --upgrade loopx") == 1 + assert "doctor | Out-Null" in command + + +def test_posix_preflight_keeps_the_documented_shell( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(install_contract, "_is_windows_host", lambda: False) + monkeypatch.setattr(project_prompt, "_is_windows_host", lambda: False) + + command = render_cli_preflight() + + assert command.startswith('export PATH="$HOME/.local/bin:$PATH"') + assert "install-local.sh" in command + assert "doctor >/dev/null" in command + + +def run_powershell(pwsh: str, script: Path, *, env: dict[str, str], cwd: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [pwsh, "-NoLogo", "-NoProfile", "-File", str(script)], + capture_output=True, + text=True, + timeout=180, + env=env, + cwd=cwd, + ) + + +@pytest.mark.skipif( + os.name != "nt" or PWSH is None, + reason="requires native Windows and PowerShell 7", +) +def test_generated_windows_onboarding_commands_run_in_powershell(tmp_path: Path) -> None: + pwsh = PWSH + assert pwsh is not None + project = tmp_path / "project's" + project.mkdir() + home = tmp_path / "home" + home.mkdir() + environment = os.environ.copy() + environment["PATH"] = ( + f"{Path(sys.executable).parent}{os.pathsep}{environment.get('PATH', '')}" + ) + environment["HOME"] = str(home) + environment["USERPROFILE"] = str(home) + + payload = build_new_project_prompt( + project=project, + goal_doc=project / "GOAL.md", + goal_id=GOAL_ID, + objective="Verify host-runnable onboarding", + domain="example", + adapter_kind="read_only_project_map_v0", + adapter_status="connected-read-only", + next_probe=None, + spawn_allowed=False, + allowed_domains=None, + write_scope=None, + ) + + connect_script = tmp_path / "connect.ps1" + connect_script.write_text(str(payload["connect_command"]), encoding="utf-8") + connect = run_powershell(pwsh, connect_script, env=environment, cwd=project) + assert connect.returncode == 0, connect.stdout + connect.stderr + assert (project / ".loopx" / "registry.json").is_file() + assert (project / ".loopx" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md").is_file() + + preflight_script = tmp_path / "preflight.ps1" + preflight_script.write_text(str(payload["cli_preflight"]), encoding="utf-8") + preflight = run_powershell(pwsh, preflight_script, env=environment, cwd=project) + assert preflight.returncode == 0, preflight.stdout + preflight.stderr From cf503bf3d09d2becc58a018cc205d896a6000156 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Fri, 23 Oct 2026 23:09:41 +0800 Subject: [PATCH 2/3] fix(cli): finish host-runnable heartbeat and executable routing Render the heartbeat Turn bootstrap with PowerShell assignment and pass the Turn id into the quota guard without POSIX parameter expansion. Route quoted executables through the PowerShell call operator, and use the interpreter pip preflight on Windows so Windows PowerShell 5.1 hosts do not require pwsh. Addresses independent review findings on #7160. Signed-off-by: JasonBuildAI --- loopx/agent_onboarding.py | 27 +++--- loopx/control_plane/heartbeat/task_body.py | 22 +++-- loopx/install_contract.py | 96 ++++++++++--------- loopx/project_prompt.py | 28 +++++- tests/test_install_hint_platform.py | 16 ++-- tests/test_onboarding_command_platform.py | 102 +++++++++++++++++++++ 6 files changed, 213 insertions(+), 78 deletions(-) diff --git a/loopx/agent_onboarding.py b/loopx/agent_onboarding.py index b71c04e60e..b63b4bfbe6 100644 --- a/loopx/agent_onboarding.py +++ b/loopx/agent_onboarding.py @@ -26,6 +26,7 @@ SKILLS_ROOT_LABEL as KIRO_CLI_SKILLS_ROOT_LABEL, ) from .project_prompt import ( + host_command, render_available_capability_args, render_codex_cli_install_preflight, render_quota_guard_command, @@ -47,30 +48,30 @@ def _surface_install_command(agent_type: str, cli_bin: str, project: str) -> str | None: if agent_type in {"codex-app", "codex-app-ssh", "codex-ide-plugin", "codex-cli"}: - return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface codex" + return f"{host_command(cli_bin)} slash-commands --install --surface codex" if agent_type == "claude-code": - return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface claude-code" + return f"{host_command(cli_bin)} slash-commands --install --surface claude-code" if agent_type == "opencode": return ( - f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface opencode " + f"{host_command(cli_bin)} slash-commands --install --surface opencode " "--with-goal-bridge" ) if agent_type == "gemini-cli": - return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface gemini" + return f"{host_command(cli_bin)} slash-commands --install --surface gemini" if agent_type == "cursor-agent": - return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface cursor" + return f"{host_command(cli_bin)} slash-commands --install --surface cursor" if agent_type == "zcode": - return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface zcode" + return f"{host_command(cli_bin)} slash-commands --install --surface zcode" if agent_type == "agy": - return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface agy" + return f"{host_command(cli_bin)} slash-commands --install --surface agy" if agent_type == "kiro-cli": - return f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface kiro-cli" + return f"{host_command(cli_bin)} slash-commands --install --surface kiro-cli" if agent_type == "pi": # The slash-commands installer resolves the Pi extension target through # --pi-project; pass the resolved project so the command stays correct # when agent-onboard runs from any cwd. return ( - f"{shell_arg_for_host(cli_bin)} slash-commands --install --surface pi " + f"{host_command(cli_bin)} slash-commands --install --surface pi " f"--pi-project {shell_arg_for_host(project)}" ) return None @@ -103,7 +104,7 @@ def _project_skill_command( execute: bool = False, ) -> str: parts = [ - shell_arg_for_host(cli_bin), + host_command(cli_bin), "project-skill", command, "--project", @@ -304,7 +305,7 @@ def _bootstrap_pack_command( "other-agent": "other-agent", } parts = [ - shell_arg_for_host(cli_bin), + host_command(cli_bin), "bootstrap-command-pack", "--project", shell_arg_for_host(project), @@ -515,7 +516,7 @@ def build_agent_onboarding_packet( else None ), "agent_onboard_recheck": ( - f"{shell_arg_for_host(cli_bin)} agent-onboard " + f"{host_command(cli_bin)} agent-onboard " f"--agent-type {shell_arg_for_host(canonical_agent_type)} " f"--project {shell_arg_for_host(resolved_project)} " f"--goal-id {shell_arg_for_host(resolved_goal_id)}" @@ -531,7 +532,7 @@ def build_agent_onboarding_packet( commands["install_command_facade"] = install_command if canonical_agent_type == "codex-cli": commands["codex_cli_bootstrap_message"] = ( - f"{shell_arg_for_host(cli_bin)} codex-cli-bootstrap-message " + f"{host_command(cli_bin)} codex-cli-bootstrap-message " f"--project {shell_arg_for_host(resolved_project)} " f"--goal-id {shell_arg_for_host(resolved_goal_id)}" + ( diff --git a/loopx/control_plane/heartbeat/task_body.py b/loopx/control_plane/heartbeat/task_body.py index bde85c4628..15d1181f69 100644 --- a/loopx/control_plane/heartbeat/task_body.py +++ b/loopx/control_plane/heartbeat/task_body.py @@ -4,6 +4,7 @@ from typing import Any +from ...project_prompt import render_turn_bootstrap from .rules import ( CODEX_NATIVE_GOAL_UNCHANGED_WAIT_RULE, DEFAULT_MATERIAL_QUEUE_RULE, @@ -53,10 +54,15 @@ def bind_exact_turn_settlement_task_body( ) -> str: if turn_instance_id is None: return task_body - bound = task_body.replace( + bound = task_body + for placeholder in ( "LOOPX_TURN=", - f"LOOPX_TURN={turn_instance_id}", - ) + '$LOOPX_TURN = ""', + ): + bound = bound.replace( + placeholder, + render_turn_bootstrap(value=turn_instance_id), + ) plan_path = ("writeback.settlement_plan.ordered_steps" if captured_envelope else "interaction_contract.cli_channel.settlement_plan.ordered_steps") return ( @@ -103,7 +109,7 @@ def render_heartbeat_task_body( ```bash {cli_preflight} -LOOPX_TURN= +{render_turn_bootstrap()} {pr_review_pre_quota_block}{quota_guard_command} ``` @@ -273,7 +279,7 @@ def render_brief_heartbeat_task_body( ```bash {cli_preflight} -LOOPX_TURN= +{render_turn_bootstrap()} {pr_review_pre_quota_block}{quota_guard_command} ``` @@ -341,7 +347,7 @@ def render_compact_heartbeat_task_body( ```bash {cli_preflight} -LOOPX_TURN= +{render_turn_bootstrap()} {pr_review_pre_quota_block}{quota_guard_command} ``` @@ -638,7 +644,7 @@ def render_thin_heartbeat_task_body( After a workspace or state change, re-enter the guard before continuing work. ```sh -LOOPX_TURN= +{render_turn_bootstrap()} {pr_review_pre_quota_instruction}{quota_guard_command} ``` {SCOPE_BOUNDED_WORK_RULE} @@ -665,7 +671,7 @@ def render_thin_heartbeat_task_body( {HOST_LOOP_QUOTA_DISPATCH_RULE} {HEARTBEAT_TURN_BOOTSTRAP_RULE} ```sh -LOOPX_TURN= +{render_turn_bootstrap()} {pr_review_pre_quota_instruction}{quota_guard_command} ``` {HEARTBEAT_NOTIFICATION_RULE_SHORT} diff --git a/loopx/install_contract.py b/loopx/install_contract.py index f7cab2d774..4fd0703532 100644 --- a/loopx/install_contract.py +++ b/loopx/install_contract.py @@ -48,6 +48,14 @@ def _powershell_command(value: str) -> str: return f"& {_powershell_literal(value)}" +def host_command_name(value: str) -> str: + """Render an executable as the first token of a host-shell command.""" + + if _is_windows_host(): + return _powershell_command(value) + return shlex.quote(value) + + def _is_windows_host() -> bool: """Whether printed commands target PowerShell instead of a POSIX shell.""" @@ -69,16 +77,13 @@ def _checkout_snapshot_installer(repo_root: Path) -> Path | None: return None -def _windows_pypi_upgrade_command( - *, doctor_agent_arg: str, skip_skills: bool, cli_bin: str = "loopx" -) -> str: +def _windows_pypi_upgrade_command(*, doctor_agent_arg: str, skip_skills: bool) -> str: """No-clone upgrade through the interpreter that owns this installation.""" - command = _powershell_command(cli_bin) lines = [f"& {_powershell_literal(sys.executable)} -m pip install --upgrade loopx"] if not skip_skills: - lines.append(f"{command} workflow-skills --install") - lines.append(f"{command} doctor{doctor_agent_arg}") + lines.append(DEFAULT_WORKFLOW_SKILL_INSTALL_COMMAND) + lines.append(f"loopx doctor{doctor_agent_arg}") return "\n".join(lines) @@ -99,7 +104,6 @@ def no_clone_upgrade_command( *, doctor_agent_type: str | None = None, skip_skills: bool = False, - cli_bin: str = "loopx", ) -> str: if _is_windows_host(): repo_root = Path(__file__).resolve().parents[1] @@ -111,12 +115,10 @@ def no_clone_upgrade_command( if _checkout_snapshot_installer(repo_root) is not None: return ( f"{local_install_command(repo_root, skip_skills=skip_skills)}\n" - f"{_powershell_command(cli_bin)} doctor{doctor_agent_arg}" + f"loopx doctor{doctor_agent_arg}" ) return _windows_pypi_upgrade_command( - doctor_agent_arg=doctor_agent_arg, - skip_skills=skip_skills, - cli_bin=cli_bin, + doctor_agent_arg=doctor_agent_arg, skip_skills=skip_skills ) ref = str(source_ref or "").strip() installer = f"curl -fsSL {NO_CLONE_INSTALL_URL}" @@ -137,54 +139,59 @@ def no_clone_upgrade_command( return ( f"{installer}\n" 'export PATH="$HOME/.local/bin:$PATH"\n' - f"{shlex.quote(cli_bin)} doctor{doctor_agent_arg}" + f"loopx doctor{doctor_agent_arg}" ) -def install_repair_command(*, cli_bin: str = "loopx") -> str: +def install_repair_command() -> str: """PyPI repair guidance that runs as printed on this host.""" if _is_windows_host(): - return _windows_pypi_upgrade_command( - doctor_agent_arg="", skip_skills=False, cli_bin=cli_bin - ) - if cli_bin == "loopx": - return DEFAULT_INSTALL_REPAIR_COMMAND - quoted_cli = shlex.quote(cli_bin) - return ( - f"{DEFAULT_INSTALL_COMMAND}\n" - f"{quoted_cli} workflow-skills --install --cli-bin {quoted_cli}\n" - f"{quoted_cli} doctor" - ) + return _windows_pypi_upgrade_command(doctor_agent_arg="", skip_skills=False) + return DEFAULT_INSTALL_REPAIR_COMMAND -def archive_fallback_install_command(*, cli_bin: str = "loopx") -> str: +def archive_fallback_install_command() -> str: """Archive fallback guidance that runs as printed on this host.""" if _is_windows_host(): - return no_clone_upgrade_command(cli_bin=cli_bin) - if cli_bin == "loopx": - return ARCHIVE_FALLBACK_INSTALL_COMMAND - return ( - f"curl -fsSL {NO_CLONE_INSTALL_URL} | bash\n" - 'export PATH="$HOME/.local/bin:$PATH"\n' - f"{shlex.quote(cli_bin)} doctor" - ) + return no_clone_upgrade_command() + return ARCHIVE_FALLBACK_INSTALL_COMMAND -def doctor_preflight_command(*, cli_bin: str = "loopx") -> str: +def doctor_preflight_command( + *, + cli_bin: str = "loopx", + doctor_agent_type: str | None = None, + install_skills: bool = False, +) -> str: """Ensure the CLI is reachable without upgrading on every scheduled turn.""" if _is_windows_host(): - return "\n".join( - [ - f"if (-not (Get-Command {_powershell_literal(cli_bin)} -ErrorAction SilentlyContinue)) {{", - f" & {_powershell_literal(sys.executable)} -m pip install --upgrade loopx", - "}", - f"{_powershell_command(cli_bin)} doctor | Out-Null", - ] + doctor_arg = ( + f" --agent-type {_powershell_literal(doctor_agent_type)}" + if doctor_agent_type + else "" + ) + install_lines = [ + f"if (-not (Get-Command {_powershell_literal(cli_bin)} -ErrorAction SilentlyContinue)) {{", + f" & {_powershell_literal(sys.executable)} -m pip install --upgrade loopx", + ] + if install_skills: + install_lines.append( + f" {_powershell_command(cli_bin)} workflow-skills --install" + ) + install_lines.append("}") + install_lines.append( + f"{_powershell_command(cli_bin)} doctor{doctor_arg} | Out-Null" ) + return "\n".join(install_lines) cli_bin_arg = shlex.quote(cli_bin) + doctor_arg = ( + f" --agent-type {shlex.quote(doctor_agent_type)}" + if doctor_agent_type + else "" + ) return f"""export PATH="$HOME/.local/bin:$PATH" install_script="$HOME/loopx/scripts/install-local.sh" if ! command -v {cli_bin_arg} >/dev/null 2>&1; then @@ -196,7 +203,7 @@ def doctor_preflight_command(*, cli_bin: str = "loopx") -> str: exit 1 fi fi -{cli_bin_arg} doctor >/dev/null""" +{cli_bin_arg} doctor{doctor_arg} >/dev/null""" def codex_cli_install_preflight( @@ -207,9 +214,10 @@ def codex_cli_install_preflight( """Install-or-doctor guidance a Codex CLI operator can paste on this host.""" if _is_windows_host(): - return no_clone_upgrade_command( - doctor_agent_type=doctor_agent_type, + return doctor_preflight_command( cli_bin=cli_bin, + doctor_agent_type=doctor_agent_type, + install_skills=True, ) cli_bin_arg = shlex.quote(cli_bin) doctor_agent_arg = ( diff --git a/loopx/project_prompt.py b/loopx/project_prompt.py index 88724a6d68..026f00b135 100644 --- a/loopx/project_prompt.py +++ b/loopx/project_prompt.py @@ -62,14 +62,30 @@ def render_optional_cli_arg(flag: str, value: str | None) -> str: return f" {flag} {shell_arg_for_host(value)}" +def host_command(value: str) -> str: + """Render an executable as the first token of a host-shell command.""" + + return install_contract.host_command_name(value) + + +def render_turn_bootstrap(*, value: str = "") -> str: + """Assign the per-wake Turn identity with the host shell's syntax.""" + + if _is_windows_host(): + return f'$LOOPX_TURN = "{value}"' + return f"LOOPX_TURN={value}" + + def render_cli_command_prefix( *, cli_bin: str = "loopx", runtime_root: str | Path | None = None, registry_path: str | Path | None = None, ) -> str: - prefix = " ".join(shell_arg_for_host(arg) for arg in loopx_cli_argv( - None if cli_bin == "loopx" else cli_bin)) + argv = loopx_cli_argv(None if cli_bin == "loopx" else cli_bin) + prefix = " ".join( + [host_command(argv[0]), *(shell_arg_for_host(arg) for arg in argv[1:])] + ) if runtime_root is not None: prefix += f" --runtime-root {shell_arg_for_host(str(runtime_root))}" if registry_path is not None: @@ -191,7 +207,11 @@ def render_quota_guard_command( scheduler_execution_context=scheduler_execution_context, ) if heartbeat_turn_receipt: - turn_arg = ' --turn-instance-id "${LOOPX_TURN:?}"' + turn_arg = ( + " --turn-instance-id $LOOPX_TURN" + if _is_windows_host() + else ' --turn-instance-id "${LOOPX_TURN:?}"' + ) elif begin_turn: turn_arg = " --begin-turn" else: @@ -510,7 +530,7 @@ def render_codex_cli_bootstrap_connect_command( cli_bin: str, ) -> str: command = ( - f"{shell_arg_for_host(cli_bin)} bootstrap" + f"{host_command(cli_bin)} bootstrap" " --project ." f" --goal-id {shell_arg_for_host(goal_id)}" f" --adapter-kind {shell_arg_for_host(DEFAULT_HANDOFF_ADAPTER_KIND)}" diff --git a/tests/test_install_hint_platform.py b/tests/test_install_hint_platform.py index c2a97a37a4..df5cccdea2 100644 --- a/tests/test_install_hint_platform.py +++ b/tests/test_install_hint_platform.py @@ -233,7 +233,6 @@ def test_preflight_commands_follow_the_host_platform( ) -> None: monkeypatch.setattr(install_contract, "_is_windows_host", lambda: os_name == "nt") if os_name == "nt": - pretend_module_root(monkeypatch, tmp_path / "site-packages") monkeypatch.setattr(sys, "executable", PLAIN_WINDOWS_INTERPRETER) doctor = install_contract.doctor_preflight_command(cli_bin="loopx-canary") codex = install_contract.codex_cli_install_preflight( @@ -244,15 +243,15 @@ def test_preflight_commands_follow_the_host_platform( for command in (doctor, codex): for token in POSIX_ONLY_TOKENS: assert token not in command, command - assert "Get-Command" in doctor + assert "Get-Command" in command + assert "pwsh" not in command + assert powershell_literal(PLAIN_WINDOWS_INTERPRETER) in command assert doctor.splitlines()[-1] == "loopx-canary doctor | Out-Null" - assert powershell_literal(PLAIN_WINDOWS_INTERPRETER) in doctor - assert codex.splitlines()[0] == ( - f"& {powershell_literal(PLAIN_WINDOWS_INTERPRETER)} " - "-m pip install --upgrade loopx" - ) assert "loopx-canary workflow-skills --install" in codex - assert f"loopx-canary doctor --agent-type {powershell_literal('codex-cli')}" in codex + assert ( + f"loopx-canary doctor --agent-type {powershell_literal('codex-cli')} | Out-Null" + in codex + ) else: assert doctor.startswith('export PATH="$HOME/.local/bin:$PATH"') assert "loopx-canary doctor >/dev/null" in doctor @@ -267,7 +266,6 @@ def test_preflight_commands_parse_as_powershell( pwsh = PWSH assert pwsh is not None pretend_windows_host(monkeypatch, PLAIN_WINDOWS_INTERPRETER) - pretend_module_root(monkeypatch, tmp_path / "site-packages") commands = { "doctor": install_contract.doctor_preflight_command(), "codex": install_contract.codex_cli_install_preflight(doctor_agent_type="codex-cli"), diff --git a/tests/test_onboarding_command_platform.py b/tests/test_onboarding_command_platform.py index 8c4c1295a8..1274d636da 100644 --- a/tests/test_onboarding_command_platform.py +++ b/tests/test_onboarding_command_platform.py @@ -13,13 +13,19 @@ import loopx.project_prompt as project_prompt from loopx import install_contract from loopx.agent_onboarding import build_agent_onboarding_packet +from loopx.control_plane.heartbeat.task_body import ( + bind_exact_turn_settlement_task_body, +) from loopx.project_prompt import ( build_codex_cli_bootstrap_message, build_new_project_prompt, render_cli_preflight, + render_quota_guard_command, + render_turn_bootstrap, ) PWSH = shutil.which("pwsh") +SH = shutil.which("sh") GOAL_ID = "windows-onboarding-host" POSIX_ONLY_TOKENS = ( "python3 ", @@ -92,6 +98,7 @@ def test_windows_agent_onboard_prints_powershell_commands( for key in ("doctor_or_install", "bootstrap_command_pack", "agent_onboard_recheck"): assert_powershell_ready(str(commands[key])) + assert "Get-Command" in str(commands["doctor_or_install"]) assert "project''s" in str(commands["bootstrap_command_pack"]) assert "project''s" in str(commands["agent_onboard_recheck"]) @@ -204,3 +211,98 @@ def test_generated_windows_onboarding_commands_run_in_powershell(tmp_path: Path) preflight_script.write_text(str(payload["cli_preflight"]), encoding="utf-8") preflight = run_powershell(pwsh, preflight_script, env=environment, cwd=project) assert preflight.returncode == 0, preflight.stdout + preflight.stderr + + +def test_windows_executable_path_uses_powershell_call_operator( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + pretend_windows_host(monkeypatch) + cli = r"C:\Program Files\LoopX\loopx.exe" + project = tmp_path / "project" + project.mkdir() + + message = build_codex_cli_bootstrap_message( + project=project, + goal_id=GOAL_ID, + agent_id="host-agent", + cli_bin=cli, + ) + assert message["connect_command"].splitlines()[1].startswith( + f"& '{cli}' bootstrap" + ) + packet = build_agent_onboarding_packet( + project=project, + agent_type="codex-cli", + goal_id=GOAL_ID, + agent_id="host-agent", + cli_bin=cli, + ) + commands = packet["commands"] + assert isinstance(commands, dict) + assert commands["agent_onboard_recheck"].startswith(f"& '{cli}' agent-onboard") + assert f"& '{cli}'" in str(commands["bootstrap_command_pack"]) + assert f"& '{cli}'" in str(commands["doctor_or_install"]) + + +def test_windows_heartbeat_turn_identity_uses_powershell( + monkeypatch: pytest.MonkeyPatch, +) -> None: + pretend_windows_host(monkeypatch) + + assert render_turn_bootstrap() == '$LOOPX_TURN = ""' + guard = render_quota_guard_command("goal", heartbeat_turn_receipt=True) + assert "--turn-instance-id $LOOPX_TURN" in guard + assert "${LOOPX_TURN" not in guard + body = f"```sh\n{render_turn_bootstrap()}\n{guard}\n```" + bound = bind_exact_turn_settlement_task_body( + body, + turn_instance_id="2026-10-23T00:00:00Z", + ) + assert '$LOOPX_TURN = "2026-10-23T00:00:00Z"' in bound + assert "${LOOPX_TURN" not in bound + + +def test_posix_heartbeat_turn_identity_keeps_shell_assignment( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(install_contract, "_is_windows_host", lambda: False) + monkeypatch.setattr(project_prompt, "_is_windows_host", lambda: False) + + assert render_turn_bootstrap() == "LOOPX_TURN=" + guard = render_quota_guard_command("goal", heartbeat_turn_receipt=True) + assert '--turn-instance-id "${LOOPX_TURN:?}"' in guard + + +@pytest.mark.skipif( + os.name == "nt" or SH is None, + reason="requires a POSIX shell to parse the printed command", +) +def test_posix_connect_commands_are_valid_shell(tmp_path: Path) -> None: + sh = SH + assert sh is not None + project = tmp_path / "project's" + project.mkdir() + payload = build_new_project_prompt( + project=project, + goal_doc=project / "GOAL.md", + goal_id=GOAL_ID, + objective="Verify host-runnable onboarding", + domain="example", + adapter_kind="read_only_project_map_v0", + adapter_status="connected-read-only", + next_probe=None, + spawn_allowed=False, + allowed_domains=None, + write_scope=None, + ) + for command in (payload["connect_command"], payload["cli_preflight"]): + text = str(command) + assert "\\\n" not in text + completed = subprocess.run( + [sh, "-n"], + input=text, + text=True, + capture_output=True, + timeout=60, + ) + assert completed.returncode == 0, completed.stderr From b54671c9bc6ab7daca4ff39724697f1adb7b7f0c Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Fri, 23 Oct 2026 23:47:40 +0800 Subject: [PATCH 3/3] test(cli): keep the brief budget ceiling host-shell independent Native Windows prints a shorter PowerShell preflight, so the brief Markdown body is 76 lines instead of 92. Assert the frozen 92-line ceiling and retain the cross-depth equality instead of pinning the POSIX line count. Signed-off-by: JasonBuildAI --- tests/control_plane/test_cli_output_budget.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/control_plane/test_cli_output_budget.py b/tests/control_plane/test_cli_output_budget.py index e928757102..c33ac0f49b 100644 --- a/tests/control_plane/test_cli_output_budget.py +++ b/tests/control_plane/test_cli_output_budget.py @@ -1776,8 +1776,10 @@ def capture_output(command): ) else: # The frozen 94-line body/envelope loses only two diagnostic - # lines: one duplicate route and one absent pre-quota command. - assert len(text.splitlines()) == 92 + # lines. The host shell owns the preflight length: native Windows + # prints a shorter PowerShell block, so the portable invariant is + # the ceiling; the depth equality below still pins semantics. + assert len(text.splitlines()) <= 92 normalized = text.replace(root, "") outputs.append({"format": output_format, "normalized": normalized}) return exit_code, text