Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
72f8c17
test(control-plane): reconcile generated binding and hook read contracts
huangruiteng Sep 29, 2026
4729e32
refactor(cli): reuse selected dispatch for doctor and command reference
huangruiteng Sep 29, 2026
9c5048b
perf(install): exclude development output before snapshot copying
huangruiteng Sep 29, 2026
6883506
chore(semantics): refresh moved CLI registry read location
huangruiteng Sep 29, 2026
0bd036c
test: exclude private snapshots from native collection
huangruiteng Sep 29, 2026
b0f78b6
Merge remote-tracking branch 'origin/main' into codex/install-filter-…
huangruiteng Sep 30, 2026
198a58e
perf(cli): dispatch archive and installer tools through their owners
huangruiteng Sep 30, 2026
f98e2b0
test(install): combine independent upgrade scenarios in three installs
huangruiteng Sep 30, 2026
15a72c2
fix(change-quality): separate review repair limits from implementatio…
huangruiteng Sep 30, 2026
85991ec
Merge remote-tracking branch 'origin/main' into codex/install-filter-…
huangruiteng Sep 30, 2026
58a470d
refine(install): keep review authority outside the performance scope
huangruiteng Sep 30, 2026
0c2794c
test(cli): retain selected admin dispatch parity and read-only previews
huangruiteng Sep 30, 2026
b517dbd
fix(install): reject blocked entry targets before staging and migrations
huangruiteng Sep 30, 2026
cb64bd4
Merge remote-tracking branch 'origin/main' into codex/install-filter-…
huangruiteng Sep 30, 2026
a2cadd7
Merge commit '7e60e69999d9dd00c6d19a93221982a2f1f736b1' into codex/in…
huangruiteng Sep 30, 2026
7abc4f4
Merge commit 'fd5f31bb3ad57448c32df6c7cddde36d07446934' into codex/in…
huangruiteng Sep 30, 2026
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
13 changes: 12 additions & 1 deletion examples/install-local-overwrite-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import subprocess
import sys
import tempfile
import time
from pathlib import Path


Expand Down Expand Up @@ -40,14 +41,18 @@ def run_install(
*,
check: bool = True,
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
print(f"install-local-overwrite-smoke: installing {release_id}", flush=True)
started = time.monotonic()
result = subprocess.run(
[str(INSTALL_SCRIPT)],
cwd=REPO_ROOT,
env={**env, "LOOPX_RELEASE_ID": release_id},
check=check,
capture_output=True,
text=True,
)
print(f"install-local-overwrite-smoke: {release_id} in {time.monotonic() - started:.2f}s", flush=True)
return result


def assert_loopx_link_points_to(wrapper: Path, release_id: str) -> None:
Expand Down Expand Up @@ -90,13 +95,19 @@ def assert_directory_is_not_overwritten() -> None:
profile = home / ".zshrc"
wrapper = bin_dir / "loopx"
wrapper.mkdir(parents=True)
sentinel = wrapper / "user.txt"
sentinel.write_text("keep user content", encoding="utf-8")

env = install_env(root, bin_dir, profile)
failed = run_install(env, "directory-conflict", check=False)
assert failed.returncode != 0, failed.stdout
assert wrapper.is_dir(), wrapper
assert "loopx installer error:" in failed.stderr, failed.stderr
assert f"{wrapper} is a directory; remove it before installing" in failed.stderr, failed.stderr
assert sentinel.read_text(encoding="utf-8") == "keep user content"
assert not (home / ".local" / "share" / "loopx" / "releases").exists()
assert not (home / ".codex").exists()
assert not profile.exists()


def main() -> int:
Expand Down
64 changes: 36 additions & 28 deletions examples/install-local-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import subprocess
import sys
import tempfile
import time
from pathlib import Path


Expand Down Expand Up @@ -71,7 +72,9 @@ def run_install(
cwd: Path = REPO_ROOT,
revalidate_extensions: bool = True,
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
print(f"install-local-smoke: installing {release_id}", flush=True)
started = time.monotonic()
result = subprocess.run(
[str(INSTALL_SCRIPT)],
cwd=cwd,
env={
Expand All @@ -85,6 +88,11 @@ def run_install(
capture_output=True,
text=True,
)
print(
f"install-local-smoke: installed {release_id} in {time.monotonic() - started:.2f}s",
flush=True,
)
return result


def write_promotion_readiness(
Expand Down Expand Up @@ -859,60 +867,60 @@ def main() -> int:
assert "```bash\n" in canary_task_body and "LOOPX_TURN=<current_time_iso>" in canary_task_body, canary_payload
assert "not a command-prefix assignment" in canary_task_body, canary_payload

# The initial install exercises the default post-install extension
# revalidation. Repeated fixture installs do not add coverage for that
# same provider scan, so skip the optional pass to keep this smoke
# inside the public-suite timeout budget.
fresh_install = run_install(
env, "install-smoke-fresh", revalidate_extensions=False
)
assert "loopx installed locally" in fresh_install.stdout, fresh_install.stdout
assert "loopx install warning" not in fresh_install.stderr, fresh_install.stderr

stale_generated_at = (datetime.now(timezone.utc) - timedelta(hours=25)).replace(microsecond=0).isoformat()
write_promotion_readiness(runtime_run_dir, generated_at=stale_generated_at, label="stale")
stale_install = run_install(
env, "install-smoke-stale", revalidate_extensions=False
)
assert "loopx installed locally" in stale_install.stdout, stale_install.stdout
assert "promotion-readiness evidence is stale" in stale_install.stderr, stale_install.stderr
assert "age_hours=" in stale_install.stderr, stale_install.stderr
assert "non-blocking" in stale_install.stderr, stale_install.stderr

# Exercise two actual upgrades after the default install. Readiness
# freshness and optional Host configuration are independent inputs:
# pair fresh evidence with a broken OpenCode config, then stale evidence
# with a healthy config. Keep every warning/Host assertion without
# paying for two extra copies and complete candidate checks.
# The initial install covers post-install extension revalidation;
# repeating that same provider scan does not add upgrade coverage.
blocked_opencode_root = home / ".config" / "opencode-blocked"
blocked_opencode_root.mkdir(parents=True)
(blocked_opencode_root / "opencode.jsonc").write_text(
"{ invalid\n",
encoding="utf-8",
)
blocked_opencode_install = run_install(
fresh_install = run_install(
{
**env,
"LOOPX_INSTALL_OPENCODE": "1",
"OPENCODE_CONFIG_DIR": str(blocked_opencode_root),
},
"install-smoke-opencode-blocked",
"install-smoke-fresh",
revalidate_extensions=False,
)
assert "loopx installed locally" in fresh_install.stdout, fresh_install.stdout
assert "loopx install warning" not in fresh_install.stderr, fresh_install.stderr
assert (
"loopx OpenCode bridge: install attempted; run manually:"
in blocked_opencode_install.stdout
), blocked_opencode_install.stdout
in fresh_install.stdout
), fresh_install.stdout
assert (blocked_opencode_root / "commands" / "loopx.md").is_file()
assert not (blocked_opencode_root / "plugins" / "loopx-goal.js").exists()
assert not (blocked_opencode_root / "loopx" / "goal-bridge-runtime.mjs").exists()
assert not (blocked_opencode_root / "package.json").exists()
assert wrapper.resolve().parents[1].name == "install-smoke-fresh"
assert fallback_wrapper.resolve().parents[1] == wrapper.resolve().parents[1]

opencode_install = run_install(
stale_generated_at = (datetime.now(timezone.utc) - timedelta(hours=25)).replace(microsecond=0).isoformat()
write_promotion_readiness(runtime_run_dir, generated_at=stale_generated_at, label="stale")
stale_install = run_install(
{**env, "LOOPX_INSTALL_OPENCODE": "1"},
"install-smoke-opencode",
"install-smoke-stale",
revalidate_extensions=False,
)
assert "loopx OpenCode bridge:" in opencode_install.stdout, opencode_install.stdout
assert "loopx installed locally" in stale_install.stdout, stale_install.stdout
assert "promotion-readiness evidence is stale" in stale_install.stderr, stale_install.stderr
assert "age_hours=" in stale_install.stderr, stale_install.stderr
assert "non-blocking" in stale_install.stderr, stale_install.stderr

assert "loopx OpenCode bridge:" in stale_install.stdout, stale_install.stdout
assert (opencode_root / "commands" / "loopx.md").is_file()
assert (opencode_root / "plugins" / "loopx-goal.js").is_file()
assert (opencode_root / "loopx" / "goal-bridge-runtime.mjs").is_file()
assert (opencode_root / "package.json").is_file()
assert wrapper.resolve().parents[1].name == "install-smoke-stale"
assert fallback_wrapper.resolve().parents[1] == wrapper.resolve().parents[1]

print("install-local-smoke ok")
return 0
Expand Down
48 changes: 2 additions & 46 deletions loopx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,9 @@
handle_bootstrap_connect_command,
handle_canary_command,
handle_coordination_shadow_command,
handle_authority_archive_command,
handle_capability_command,
handle_doctor_command,
handle_dreaming_command,
handle_evidence_log_command,
handle_extension_command,
handle_explore_command,
handle_first_run_report_command,
handle_goal_channel_command,
Expand All @@ -120,7 +117,6 @@
handle_ready_score_command,
handle_review_batch_command,
handle_registry_admin_command,
handle_slash_commands_command,
handle_starter_command,
handle_summary_all_command,
handle_support_control_command,
Expand Down Expand Up @@ -207,8 +203,7 @@
register_lark_periodic_report_commands,
)
from .help_surface import (
build_command_reference_payload,
render_command_reference_markdown,
register_command_reference,
render_concise_help,
top_level_help_requested,
)
Expand Down Expand Up @@ -271,11 +266,7 @@ def build_parser() -> LoopXArgumentParser:

register_version_command(sub, add_subcommand_format)

commands_parser = sub.add_parser(
"commands",
help="Show grouped LoopX command reference for operators and contributors.",
)
add_subcommand_format(commands_parser)
register_command_reference(sub)

register_bootstrap_connect_command(sub)

Expand Down Expand Up @@ -424,14 +415,6 @@ def main(argv: list[str] | None = None) -> int:
if version_result is not None:
return version_result

if args.command == "commands":
print_payload(
build_command_reference_payload(),
output_format(args),
render_command_reference_markdown,
)
return 0

bootstrap_connect_result = handle_bootstrap_connect_command(
args,
registry_path=registry_path,
Expand All @@ -444,9 +427,6 @@ def main(argv: list[str] | None = None) -> int:
if starter_result is not None:
return starter_result

if args.command == "doctor":
return handle_doctor_command(args, print_payload)

workflow_skills_result = handle_workflow_skills_command(
args,
output_format=output_format,
Expand Down Expand Up @@ -513,15 +493,6 @@ def main(argv: list[str] | None = None) -> int:
if reliability_diagnostics_result is not None:
return reliability_diagnostics_result

extension_result = handle_extension_command(
args,
runtime_root_arg=args.runtime_root,
output_format=output_format,
print_payload=print_payload,
)
if extension_result is not None:
return extension_result

change_quality_result = handle_change_quality_command(
args,
registry_path=registry_path,
Expand Down Expand Up @@ -880,14 +851,6 @@ def main(argv: list[str] | None = None) -> int:
if deepresearch_result is not None:
return deepresearch_result

slash_commands_result = handle_slash_commands_command(
args,
output_format=output_format,
print_payload=print_payload,
)
if slash_commands_result is not None:
return slash_commands_result

if args.command == "dreaming":
return handle_dreaming_command(
args,
Expand Down Expand Up @@ -924,13 +887,6 @@ def main(argv: list[str] | None = None) -> int:
)
if cadence_result is not None:
return cadence_result
authority_archive_result = handle_authority_archive_command(
args, registry_path=registry_path, runtime_root_arg=args.runtime_root,
output_format=output_format, print_payload=print_payload,
)
if authority_archive_result is not None:
return authority_archive_result

coordination_shadow_result = handle_coordination_shadow_command(
args,
registry_path=registry_path,
Expand Down
57 changes: 56 additions & 1 deletion loopx/cli_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@

_STATUS_COMMANDS = frozenset({"check", "status", "diagnose", "review-packet"})
_SELECTED_COMMANDS = _STATUS_COMMANDS | {
"todo", "quota", "change-window", "delegation", "turn",
"todo", "quota", "change-window", "delegation", "turn", "doctor", "commands",
"authority-archive", "extension", "slash-commands",
}


Expand Down Expand Up @@ -228,6 +229,26 @@ def _build_selected_parser(command: str) -> LoopXArgumentParser:
from .cli_commands.turn_registration import register_turn_commands

register_turn_commands(subparsers, add_subcommand_format)
elif command == "authority-archive":
from .cli_commands.authority_archive import register_authority_archive_command

register_authority_archive_command(subparsers, add_subcommand_format)
elif command == "extension":
from .cli_commands.extension import register_extension_commands

register_extension_commands(subparsers, add_subcommand_format)
elif command == "slash-commands":
from .cli_commands.slash_commands import register_slash_commands_command

register_slash_commands_command(subparsers, add_subcommand_format)
elif command == "doctor":
from .cli_commands.doctor import register_doctor_command

register_doctor_command(subparsers, add_subcommand_format)
elif command == "commands":
from .help_surface import register_command_reference

register_command_reference(subparsers)
else: # pragma: no cover - caller guards the private interface
raise ValueError(f"unsupported selected command: {command}")
return parser
Expand All @@ -239,6 +260,40 @@ def _dispatch_common_command(
registry_path: Path,
allow_missing_registry: bool,
) -> int | None:
if args.command == "authority-archive":
from .cli_commands.authority_archive import handle_authority_archive_command

return handle_authority_archive_command(
args, registry_path=registry_path, runtime_root_arg=args.runtime_root,
output_format=output_format, print_payload=print_payload,
)
if args.command == "extension":
from .cli_commands.extension import handle_extension_command

return handle_extension_command(
args, runtime_root_arg=args.runtime_root,
output_format=output_format, print_payload=print_payload,
)
if args.command == "slash-commands":
from .cli_commands.slash_commands import handle_slash_commands_command

return handle_slash_commands_command(
args, output_format=output_format, print_payload=print_payload,
)
if args.command == "doctor":
from .cli_commands.doctor import handle_doctor_command

return handle_doctor_command(args, print_payload)
if args.command == "commands":
from .help_surface import (
build_command_reference_payload, render_command_reference_markdown,
)

print_payload(
build_command_reference_payload(), output_format(args),
render_command_reference_markdown,
)
return 0
if args.command == "delegation":
from .cli_commands.delegation import handle_delegation
from .control_plane.coordination.local_authority_shadow_adapter import (
Expand Down
13 changes: 12 additions & 1 deletion loopx/help_surface.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
from __future__ import annotations

import argparse
import re
from typing import Any

from . import __version__
from .cli_runtime import GLOBAL_OPTIONS_WITH_EQUALS, GLOBAL_OPTIONS_WITH_VALUE
from .cli_runtime import (
GLOBAL_OPTIONS_WITH_EQUALS, GLOBAL_OPTIONS_WITH_VALUE, add_subcommand_format,
)

HELP_FLAGS = {"-h", "--help"}


def register_command_reference(subparsers: argparse._SubParsersAction) -> None:
parser = subparsers.add_parser(
"commands",
help="Show grouped LoopX command reference for operators and contributors.",
)
add_subcommand_format(parser)


COMMAND_GROUPS: list[dict[str, object]] = [
{
"title": "Start here",
Expand Down
2 changes: 1 addition & 1 deletion loopx/semantics/project_registry_io_manifest_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@
},
{
"site": "loopx/cli.py::<module>.main::codec_read:load_project_registry#1",
"line": 836,
"line": 807,
"column": 17,
"kind": "codec_read",
"api": "load_project_registry",
Expand Down
Loading
Loading