From c984ac2fbaae3d234aad76493c0c8af69df19f93 Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:21:27 +0800 Subject: [PATCH] perf(cli): dispatch global reads through their lightweight owner Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 13 +- ...script-control-plane-migration-v0.zh-CN.md | 11 +- loopx/cli.py | 11 -- loopx/cli_commands/summary_all.py | 126 +--------------- .../cli_commands/summary_all_registration.py | 135 ++++++++++++++++++ loopx/cli_runtime.py | 14 +- tests/test_cli_entrypoint.py | 35 +++++ 7 files changed, 207 insertions(+), 138 deletions(-) create mode 100644 loopx/cli_commands/summary_all_registration.py diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 44368ff77e..655e9d70d8 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -4,7 +4,7 @@ - Supersedes / closes: none - Proposed by: LoopX maintainers - Date: 2026-08-15 -- Last revised: 2026-10-20 +- Last revised: 2026-10-21 - Scope: an incremental, replacement-first migration of the LoopX control-plane core from Python to TypeScript without maintaining two semantic implementations @@ -34,6 +34,17 @@ and follow-up wire contracts, real runtime transport, installed callers and matched complete SQLite CLI performance must qualify independently before delivery. Fewer requests alone do not establish a performance gain. +### Global read CLI caller adoption + +The existing five global read commands now use the selected parser and shared +dispatcher. `summary_all_registration.py` owns their parser-only registration; +the old registration import remains an alias. Full-parser compatibility callers +use the same handler dispatch, and the old full-CLI branch is removed. Help and +diagnostics retain the existing interface; accepted reads retain registry/runtime +resolution, guards, formatting and typed owners. This adds no domain rule, bridge +operation or data cache. Incremental qualification accounts for pending quota +request-scope adoption separately rather than counting its gain again. + ### Product conversations and asynchronous inbox **Receiver Inbox read transaction (2026-10-19).** diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md index 3dee7c57d9..5bb2ccf690 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md @@ -4,7 +4,7 @@ - 替代 / 关闭:无 - Proposed by:LoopX maintainers - Date:2026-08-15 -- Last revised:2026-10-20 +- Last revised:2026-10-21 - Scope:LoopX 控制面核心从 Python 到 TypeScript 的增量、replacement-first 迁移;不长期维护两份语义实现 - Tracking issue:[#3225](https://github.com/loopx-project/loopx/issues/3225) @@ -32,6 +32,15 @@ Python builder 和 App 兼容组合逻辑已退役。Python 保留既有 executi follow-up wire 契约、真实 runtime transport、已安装调用者,以及匹配的完整 SQLite CLI 性能。仅减少请求次数不能证明性能收益。 +### 全局读取 CLI 调用者接入 + +既有五个全局读取命令现在使用按命令 parser 和共享 dispatcher。 +`summary_all_registration.py` 统一持有仅解析参数的注册实现,旧注册 import 保留 +同一函数的 alias。完整 parser 的兼容调用者复用相同 handler 分发,旧完整 CLI +分支已删除。Help 和诊断保留既有 interface;实际读取保留 registry/runtime 解析、 +guard、格式和 typed owner。这不新增领域规则、桥接操作或数据缓存。增量资格验证 +单独计入待合入的 quota 请求作用域接入,避免重复计算其收益。 + ### 产品对话与通用异步 inbox **接收方 Inbox 读取事务(2026-10-19)。** diff --git a/loopx/cli.py b/loopx/cli.py index dbad49fa7b..92b848cb5b 100644 --- a/loopx/cli.py +++ b/loopx/cli.py @@ -121,7 +121,6 @@ handle_review_batch_command, handle_registry_admin_command, handle_starter_command, - handle_summary_all_command, handle_support_control_command, handle_handoff_mode_command, handle_task_lease_command, @@ -836,16 +835,6 @@ def main(argv: list[str] | None = None) -> int: if lark_inbox_result is not None: return lark_inbox_result - summary_all_result = handle_summary_all_command( - args, - registry_path=registry_path, - runtime_root_arg=args.runtime_root, - output_format=output_format, - print_payload=print_payload, - ) - if summary_all_result is not None: - return summary_all_result - pr_review_result = handle_pr_review_command( args, registry_path=registry_path, diff --git a/loopx/cli_commands/summary_all.py b/loopx/cli_commands/summary_all.py index 4fbff24136..e8a71f745d 100644 --- a/loopx/cli_commands/summary_all.py +++ b/loopx/cli_commands/summary_all.py @@ -16,7 +16,6 @@ build_global_todos_error, render_global_todos_markdown, ) -from ..paths import default_public_scan_root from ..summary_all import ( build_global_gates, build_global_gates_error, @@ -25,6 +24,8 @@ render_summary_all_markdown, ) +from .summary_all_registration import register_summary_all_command as register_summary_all_command + PrintPayload = Callable[ [dict[str, object], str, Callable[[dict[str, object]], str]], @@ -36,129 +37,6 @@ def _scan_roots(args: argparse.Namespace) -> list[Path]: return scan_roots or [Path(args.scan_root).expanduser()] -def _add_current_manager_flags( - parser: argparse.ArgumentParser, - *, - agent_help: str, - limit_help: str, -) -> None: - parser.add_argument("--agent-id", help=agent_help) - parser.add_argument("--limit", type=int, default=8, help=limit_help) - parser.add_argument( - "--scan-root", - default=default_public_scan_root(), - help=( - "Public files to scan for obvious private material. " - "Defaults to the LoopX install root." - ), - ) - parser.add_argument( - "--scan-path", - action="append", - default=[], - help=( - "Specific public file or directory to scan. Repeatable. " - "Overrides --scan-root when set." - ), - ) - - -def register_summary_all_command( - subparsers: argparse._SubParsersAction, - add_subcommand_format: Callable[[argparse.ArgumentParser], None], -) -> None: - portfolio = subparsers.add_parser( - "goal-portfolio", help="Read scoped Goal evidence with explicit source coverage." - ) - add_subcommand_format(portfolio) - portfolio.add_argument("--manager-view", choices=("portfolio", "todos", "deliveries", "agents"), help="Export an audience-safe manager evidence page from this registry.") - portfolio.add_argument("--query", default="", help="Search registered Agent identities and responsibilities with --manager-view agents.") - portfolio.add_argument("--todo-id", dest="context_todo_id", help="Read an exact permitted Todo with --manager-view todos and one --goal-id.") - portfolio.add_argument("--offset", type=int, default=0) - portfolio.add_argument("--days", type=int, default=1) - portfolio.add_argument("--include-stopped", action="store_true") - portfolio.add_argument( - "--goal-id", action="append", dest="portfolio_goal_ids", - help="Exact registered Goal to include; repeat to narrow scope.", - ) - portfolio.add_argument( - "--limit", type=int, default=8, - help="Maximum Goals to read, 1..128; omitted Goals remain in coverage.", - ) - portfolio.add_argument( - "--max-age-hours", type=float, default=24, - help="Maximum age of a progress source before it is marked stale.", - ) - parser = subparsers.add_parser( - "global-summary", - help=( - "Read a public-safe /loopx-global-summary progress digest; " - "see `loopx slash-commands` for slash help." - ), - ) - add_subcommand_format(parser) - _add_current_manager_flags( - parser, - agent_help="Registered agent id for agent-lane quota projection.", - limit_help="Maximum items per digest section.", - ) - parser.add_argument( - "--time-range", - default="24h", - help="Recent progress window, e.g. 24h or 7d.", - ) - - gates_parser = subparsers.add_parser( - "global-gates", - help=( - "Read public-safe /loopx-global-gates user/controller decisions; " - "see `loopx slash-commands` for slash help." - ), - ) - add_subcommand_format(gates_parser) - _add_current_manager_flags( - gates_parser, - agent_help="Registered agent id used to narrow gate projection.", - limit_help="Maximum returned gate count.", - ) - - todos_parser = subparsers.add_parser( - "global-todos", - help=( - "Read a public-safe current-state todo inbox across visible goals; " - "see `loopx slash-commands` for slash help." - ), - ) - add_subcommand_format(todos_parser) - _add_current_manager_flags( - todos_parser, - agent_help="Registered agent id used to narrow todo projection.", - limit_help="Maximum returned todo count.", - ) - - risks_parser = subparsers.add_parser( - "global-risks", - help=( - "Read a public-safe current risk inbox across visible goals; " - "see `loopx slash-commands` for slash help." - ), - ) - add_subcommand_format(risks_parser) - _add_current_manager_flags( - risks_parser, - agent_help="Registered agent id used to narrow risk projection.", - limit_help="Maximum returned risk count.", - ) - risks_parser.add_argument( - "--time-range", - default="24h", - help=( - "Risk request window for protocol compatibility, e.g. 24h or 7d; " - "current risks remain visible." - ), - ) - - def handle_summary_all_command( args: argparse.Namespace, *, diff --git a/loopx/cli_commands/summary_all_registration.py b/loopx/cli_commands/summary_all_registration.py new file mode 100644 index 0000000000..ad41969ae5 --- /dev/null +++ b/loopx/cli_commands/summary_all_registration.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +import argparse +from collections.abc import Callable + +from ..paths import default_public_scan_root + + +GLOBAL_READ_COMMANDS = frozenset({ + "goal-portfolio", "global-summary", "global-gates", "global-todos", "global-risks", +}) + + +def _add_current_manager_flags( + parser: argparse.ArgumentParser, + *, + agent_help: str, + limit_help: str, +) -> None: + parser.add_argument("--agent-id", help=agent_help) + parser.add_argument("--limit", type=int, default=8, help=limit_help) + parser.add_argument( + "--scan-root", + default=default_public_scan_root(), + help=( + "Public files to scan for obvious private material. " + "Defaults to the LoopX install root." + ), + ) + parser.add_argument( + "--scan-path", + action="append", + default=[], + help=( + "Specific public file or directory to scan. Repeatable. " + "Overrides --scan-root when set." + ), + ) + + + +def register_summary_all_command( + subparsers: argparse._SubParsersAction, + add_subcommand_format: Callable[[argparse.ArgumentParser], None], +) -> None: + portfolio = subparsers.add_parser( + "goal-portfolio", help="Read scoped Goal evidence with explicit source coverage." + ) + add_subcommand_format(portfolio) + portfolio.add_argument("--manager-view", choices=("portfolio", "todos", "deliveries", "agents"), help="Export an audience-safe manager evidence page from this registry.") + portfolio.add_argument("--query", default="", help="Search registered Agent identities and responsibilities with --manager-view agents.") + portfolio.add_argument("--todo-id", dest="context_todo_id", help="Read an exact permitted Todo with --manager-view todos and one --goal-id.") + portfolio.add_argument("--offset", type=int, default=0) + portfolio.add_argument("--days", type=int, default=1) + portfolio.add_argument("--include-stopped", action="store_true") + portfolio.add_argument( + "--goal-id", action="append", dest="portfolio_goal_ids", + help="Exact registered Goal to include; repeat to narrow scope.", + ) + portfolio.add_argument( + "--limit", type=int, default=8, + help="Maximum Goals to read, 1..128; omitted Goals remain in coverage.", + ) + portfolio.add_argument( + "--max-age-hours", type=float, default=24, + help="Maximum age of a progress source before it is marked stale.", + ) + parser = subparsers.add_parser( + "global-summary", + help=( + "Read a public-safe /loopx-global-summary progress digest; " + "see `loopx slash-commands` for slash help." + ), + ) + add_subcommand_format(parser) + _add_current_manager_flags( + parser, + agent_help="Registered agent id for agent-lane quota projection.", + limit_help="Maximum items per digest section.", + ) + parser.add_argument( + "--time-range", + default="24h", + help="Recent progress window, e.g. 24h or 7d.", + ) + + gates_parser = subparsers.add_parser( + "global-gates", + help=( + "Read public-safe /loopx-global-gates user/controller decisions; " + "see `loopx slash-commands` for slash help." + ), + ) + add_subcommand_format(gates_parser) + _add_current_manager_flags( + gates_parser, + agent_help="Registered agent id used to narrow gate projection.", + limit_help="Maximum returned gate count.", + ) + + todos_parser = subparsers.add_parser( + "global-todos", + help=( + "Read a public-safe current-state todo inbox across visible goals; " + "see `loopx slash-commands` for slash help." + ), + ) + add_subcommand_format(todos_parser) + _add_current_manager_flags( + todos_parser, + agent_help="Registered agent id used to narrow todo projection.", + limit_help="Maximum returned todo count.", + ) + + risks_parser = subparsers.add_parser( + "global-risks", + help=( + "Read a public-safe current risk inbox across visible goals; " + "see `loopx slash-commands` for slash help." + ), + ) + add_subcommand_format(risks_parser) + _add_current_manager_flags( + risks_parser, + agent_help="Registered agent id used to narrow risk projection.", + limit_help="Maximum returned risk count.", + ) + risks_parser.add_argument( + "--time-range", + default="24h", + help=( + "Risk request window for protocol compatibility, e.g. 24h or 7d; " + "current risks remain visible." + ), + ) diff --git a/loopx/cli_runtime.py b/loopx/cli_runtime.py index 2058cda57c..f256f86bee 100644 --- a/loopx/cli_runtime.py +++ b/loopx/cli_runtime.py @@ -8,6 +8,7 @@ from pathlib import Path from . import __version__ +from .cli_commands.summary_all_registration import GLOBAL_READ_COMMANDS from .paths import default_registry_path, global_registry_path, select_default_runtime_root @@ -65,7 +66,7 @@ ) _STATUS_COMMANDS = frozenset({"check", "status", "diagnose", "review-packet"}) -_SELECTED_COMMANDS = _STATUS_COMMANDS | { +_SELECTED_COMMANDS = _STATUS_COMMANDS | GLOBAL_READ_COMMANDS | { "todo", "quota", "task-lease", "change-window", "delegation", "turn", "doctor", "commands", "authority-archive", "coordination-shadow", "extension", "slash-commands", } @@ -262,6 +263,10 @@ def _build_selected_parser(command: str) -> LoopXArgumentParser: from .cli_commands.status_registration import register_status_commands register_status_commands(subparsers, add_subcommand_format) + elif command in GLOBAL_READ_COMMANDS: + from .cli_commands.summary_all_registration import register_summary_all_command + + register_summary_all_command(subparsers, add_subcommand_format) elif command == "todo": from .cli_commands.todo_registration import register_todo_command @@ -396,6 +401,13 @@ def _dispatch_common_command( args, registry_path=registry_path, runtime_root_arg=args.runtime_root, output_format=output_format, print_payload=print_payload, ) + if args.command in GLOBAL_READ_COMMANDS: + from .cli_commands.summary_all import handle_summary_all_command + + return handle_summary_all_command( + args, registry_path=registry_path, runtime_root_arg=args.runtime_root, + output_format=output_format, print_payload=print_payload, + ) if args.command in _STATUS_COMMANDS: from .cli_commands.status import ( handle_check_command, diff --git a/tests/test_cli_entrypoint.py b/tests/test_cli_entrypoint.py index 7d11afb5ac..07c7b6cd15 100644 --- a/tests/test_cli_entrypoint.py +++ b/tests/test_cli_entrypoint.py @@ -279,6 +279,11 @@ def test_command_reference_does_not_require_default_runtime_route( (["task-lease", "--help"], "loopx.cli_commands.task_lease", "loopx.cli_commands.task_lease"), (["todo", "--help"], "loopx.cli_commands.todo_registration", "loopx.cli_commands.todo"), (["delegation", "--help"], "loopx.cli_commands.delegation", "loopx.cli_commands.delegation"), + (["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"), + (["global-todos", "--help"], "loopx.cli_commands.summary_all_registration", "loopx.cli_commands.summary_all"), + (["global-risks", "--help"], "loopx.cli_commands.summary_all_registration", "loopx.cli_commands.summary_all"), (["turn", "--help"], "loopx.cli_commands.turn_registration", "loopx.cli_commands.turn"), (["turn", "run-once", "--help"], "loopx.cli_commands.turn_registration", "loopx.cli_commands.turn"), ], @@ -452,6 +457,9 @@ def test_selected_parser_matches_full_help_and_diagnostics() -> None: ["quota", "unknown-command"], ["--format", "unknown", "status"], ] + for command in ("goal-portfolio", "global-summary", "global-gates", "global-todos", "global-risks"): + argv_cases.extend([[command, "--help"], [command, "--unknown-option"], [command, "--limit", "invalid"]]) + argv_cases.append(["goal-portfolio", "--manager-view", "unknown"]) selected = run_cli_batch("loopx.entrypoint", argv_cases) full = run_cli_batch("loopx.cli", argv_cases) @@ -804,3 +812,30 @@ def test_console_native_scheduler_followup_fails_closed_without_node( assert entrypoint.main() == 2 assert "requires Node.js 22.22.3 or newer" in capsys.readouterr().err + + +@pytest.mark.parametrize("command", [ + "goal-portfolio", "global-summary", "global-gates", "global-todos", "global-risks", +]) +def test_global_read_preserves_empty_coverage_without_loading_full_cli(tmp_path, command): + registry, runtime, public = tmp_path / "registry.json", tmp_path / "runtime", tmp_path / "public" + runtime.mkdir() + public.mkdir() + registry.write_text(json.dumps({"goals": [], "common_runtime_root": str(runtime)})) + argv = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", command] + if command != "goal-portfolio": + argv.extend(["--scan-path", str(public)]) + before = registry.read_bytes() + selected = run_cli_main("loopx.entrypoint", argv, forbidden_modules=("loopx.cli",)) + full = run_cli_main("loopx.cli", argv) + assert selected.returncode == 0, selected.stderr + assert full.returncode == 0, full.stderr + payload, original = json.loads(selected.stdout), json.loads(full.stdout) + assert payload["ok"] is True + for key in ("schema_version", "request", "summary", "coverage", "scope", "boundary", "omissions"): + assert payload.get(key) == original.get(key) + if command == "goal-portfolio": + assert payload["coverage"]["discovered"] == 0 + assert payload["coverage"]["complete"] is False + assert payload["goals"] == [] + assert registry.read_bytes() == before