Skip to content

Commit 785a4d0

Browse files
committed
perf(cli): share selected lifecycle command dispatch
Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com>
1 parent e3fa619 commit 785a4d0

9 files changed

Lines changed: 111 additions & 52 deletions

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ resolution, guards, formatting and typed owners. This adds no domain rule, bridg
4545
operation or data cache. Incremental qualification accounts for pending quota
4646
request-scope adoption separately rather than counting its gain again.
4747

48+
### Lifecycle CLI caller adoption
49+
50+
`refresh-state`, `checkpoint-context`, `read-only-map`, `reward` and
51+
`operator-gate` reuse the selected parser and shared dispatcher. Both CLI
52+
entrypoints use the existing lifecycle registration, handlers and writeback-hook
53+
composition; the old full-CLI dispatch branch is removed. The command set lives
54+
with routing and remains available through its lifecycle-module import. Existing
55+
help, diagnostics, registry/runtime selection and native-controller guards stay
56+
on their established paths. This adds no domain rule, bridge operation or state
57+
cache. Complete command cost and real write/recovery behavior qualify separately
58+
from import timing; installed and live-provider behavior need their own evidence.
59+
4860
### Product conversations and asynchronous inbox
4961

5062
**Chat HTTP POST scope.** `ChatRequestHandler.do_POST` now joins the existing

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ SQLite CLI 性能。仅减少请求次数不能证明性能收益。
4141
guard、格式和 typed owner。这不新增领域规则、桥接操作或数据缓存。增量资格验证
4242
单独计入待合入的 quota 请求作用域接入,避免重复计算其收益。
4343

44+
### Lifecycle CLI 调用者接入
45+
46+
`refresh-state`、`checkpoint-context`、`read-only-map`、`reward` 和
47+
`operator-gate` 复用按命令 parser 和共享 dispatcher。两个 CLI 入口使用既有
48+
lifecycle 注册、handler 和 writeback-hook 组合;旧的完整 CLI 分发分支已删除。
49+
命令集合由路由持有,lifecycle module 的旧 import 仍可使用它。Help、诊断、
50+
registry/runtime 选择和 native-controller guard 保持既有路径。这不新增领域规则、
51+
桥接操作或状态缓存。完整命令成本及真实写入/恢复行为与 import 计时分别验证;
52+
安装后行为和真实 provider 行为仍需各自的证据。
53+
4454
### 产品对话与通用异步 inbox
4555

4656
**Chat HTTP POST 作用域。** `ChatRequestHandler.do_POST` 现在为每次完整 HTTP

‎examples/cli-project-lifecycle-command-modularization-smoke.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ def main() -> None:
220220
"PROJECT_LIFECYCLE_COMMANDS",
221221
"register_project_lifecycle_commands",
222222
"handle_project_lifecycle_command",
223-
"refresh-state",
224223
"read-only-map",
225224
"reward",
226225
"operator-gate",
@@ -240,7 +239,9 @@ def main() -> None:
240239
):
241240
require(marker in sink_source, f"project lifecycle sink module missing {marker}")
242241
require("register_project_lifecycle_commands" in cli_source, "cli.py did not register project lifecycle commands")
243-
require("handle_project_lifecycle_command" in cli_source, "cli.py did not dispatch project lifecycle commands")
242+
runtime_source = (CLI.parent / "cli_runtime.py").read_text(encoding="utf-8")
243+
require("dispatch_common_command" in cli_source and "handle_project_lifecycle_command" in runtime_source,
244+
"CLI entrypoints did not share project lifecycle dispatch")
244245
require("register_project_lifecycle_commands" in init_source, "__init__ did not export project lifecycle registration")
245246
require("handle_project_lifecycle_command" in init_source, "__init__ did not export project lifecycle handler")
246247

‎loopx/cli.py‎

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,6 @@
6969
handle_machine_configuration_command,
7070
register_machine_configuration_commands,
7171
)
72-
from .capabilities.periodic_report.post_writeback_hook import (
73-
build_periodic_report_post_writeback_projection,
74-
periodic_report_post_writeback_hooks_for_goal,
75-
)
7672
from .paths import (
7773
effective_runtime_root,
7874
)
@@ -114,7 +110,6 @@
114110
handle_presentation_command,
115111
handle_dash_command,
116112
handle_project_command,
117-
handle_project_lifecycle_command,
118113
handle_pr_review_command,
119114
handle_deepresearch_command,
120115
handle_ready_score_command,
@@ -196,7 +191,6 @@
196191
handle_reliability_diagnostics_command,
197192
register_reliability_diagnostics_commands,
198193
)
199-
from .cli_rollout import append_cli_rollout_event
200194
from .capabilities.project_skill_delivery.cli import (
201195
handle_project_skill_command,
202196
register_project_skill_commands,
@@ -745,33 +739,6 @@ def _main(argv: list[str] | None = None) -> int:
745739
print_payload=print_payload,
746740
)
747741

748-
project_lifecycle_result = handle_project_lifecycle_command(
749-
args,
750-
registry_path=registry_path,
751-
print_payload=print_payload,
752-
output_format=output_format,
753-
append_cli_rollout_event=append_cli_rollout_event,
754-
post_writeback_hooks=(
755-
periodic_report_post_writeback_hooks_for_goal(
756-
registry_path=registry_path,
757-
goal_id=args.goal_id,
758-
runtime_root=effective_runtime_root(
759-
registry_path,
760-
args.runtime_root,
761-
),
762-
)
763-
if args.command == "refresh-state"
764-
else ()
765-
),
766-
post_writeback_projection_builder=(
767-
build_periodic_report_post_writeback_projection
768-
if args.command == "refresh-state"
769-
else None
770-
),
771-
)
772-
if project_lifecycle_result is not None:
773-
return project_lifecycle_result
774-
775742
goal_channel_result = handle_goal_channel_command(
776743
args,
777744
registry_path=registry_path,

‎loopx/cli_commands/project_lifecycle.py‎

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections.abc import Callable, Sequence
55
from pathlib import Path
66

7+
from ..cli_runtime import PROJECT_LIFECYCLE_COMMANDS as PROJECT_LIFECYCLE_COMMANDS
78
from ..control_plane.capability_hooks import (
89
PostWritebackHookRegistration,
910
)
@@ -40,15 +41,6 @@
4041
OutputFormat = Callable[[argparse.Namespace], str]
4142
AppendCliRolloutEvent = Callable[..., dict[str, object]]
4243

43-
PROJECT_LIFECYCLE_COMMANDS = {
44-
"refresh-state",
45-
"checkpoint-context",
46-
"read-only-map",
47-
"reward",
48-
"operator-gate",
49-
}
50-
51-
5244
def register_project_lifecycle_commands(
5345
subparsers: argparse._SubParsersAction,
5446
add_subcommand_format: Callable[[argparse.ArgumentParser], None],

‎loopx/cli_runtime.py‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@
6767
)
6868

6969
_STATUS_COMMANDS = frozenset({"check", "status", "diagnose", "review-packet"})
70-
_SELECTED_COMMANDS = _STATUS_COMMANDS | GLOBAL_READ_COMMANDS | {
70+
PROJECT_LIFECYCLE_COMMANDS = {
71+
"refresh-state", "checkpoint-context", "read-only-map", "reward", "operator-gate",
72+
}
73+
_SELECTED_COMMANDS = _STATUS_COMMANDS | GLOBAL_READ_COMMANDS | PROJECT_LIFECYCLE_COMMANDS | {
7174
"todo", "quota", "task-lease", "change-window", "delegation", "turn", "doctor", "commands",
7275
"authority-archive", "coordination-shadow", "extension", "slash-commands",
7376
"manager-inbox",
@@ -297,6 +300,10 @@ def _build_selected_parser(command: str) -> LoopXArgumentParser:
297300
from .cli_commands.summary_all_registration import register_summary_all_command
298301

299302
register_summary_all_command(subparsers, add_subcommand_format)
303+
elif command in PROJECT_LIFECYCLE_COMMANDS:
304+
from .cli_commands.project_lifecycle import register_project_lifecycle_commands
305+
306+
register_project_lifecycle_commands(subparsers, add_subcommand_format)
300307
elif command == "todo":
301308
from .cli_commands.todo_registration import register_todo_command
302309

@@ -360,6 +367,25 @@ def _dispatch_common_command(
360367
registry_path: Path,
361368
allow_missing_registry: bool,
362369
) -> int | None:
370+
if args.command in PROJECT_LIFECYCLE_COMMANDS:
371+
from .cli_commands.project_lifecycle import handle_project_lifecycle_command
372+
from .cli_rollout import append_cli_rollout_event
373+
from .capabilities.periodic_report.post_writeback_hook import (
374+
build_periodic_report_post_writeback_projection,
375+
periodic_report_post_writeback_hooks_for_goal,
376+
)
377+
from .paths import effective_runtime_root
378+
379+
return handle_project_lifecycle_command(
380+
args, registry_path=registry_path, print_payload=print_payload,
381+
output_format=output_format, append_cli_rollout_event=append_cli_rollout_event,
382+
post_writeback_hooks=(periodic_report_post_writeback_hooks_for_goal(
383+
registry_path=registry_path, goal_id=args.goal_id,
384+
runtime_root=effective_runtime_root(registry_path, args.runtime_root),
385+
) if args.command == "refresh-state" else ()),
386+
post_writeback_projection_builder=(build_periodic_report_post_writeback_projection
387+
if args.command == "refresh-state" else None),
388+
)
363389
if args.command == "manager-inbox":
364390
from .cli_commands.manager_inbox import handle_manager_inbox
365391
from .control_plane.projects.registry_codec import load_project_registry

‎tests/control_plane/test_post_writeback_capability_hooks.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2593,13 +2593,15 @@ def capture_hooks(
25932593
assert captured["runtime_root"] == runtime_override
25942594

25952595

2596+
@pytest.mark.parametrize("entrypoint", ["loopx.cli", "loopx.entrypoint"])
25962597
def test_refresh_state_composition_passes_effective_runtime_root_to_hooks(
25972598
monkeypatch: pytest.MonkeyPatch,
25982599
tmp_path: Path,
2600+
entrypoint: str,
25992601
) -> None:
26002602
"""The refresh-state hook composition reads machine defaults under the override."""
26012603

2602-
from loopx import cli as cli_module
2604+
from importlib import import_module
26032605

26042606
registry_path, _runtime_registry, runtime_override = _split_root_registry(tmp_path)
26052607
captured: dict[str, object] = {}
@@ -2611,17 +2613,15 @@ def capture_hooks(
26112613
return ()
26122614

26132615
monkeypatch.setattr(
2614-
cli_module,
2615-
"periodic_report_post_writeback_hooks_for_goal",
2616+
"loopx.capabilities.periodic_report.post_writeback_hook.periodic_report_post_writeback_hooks_for_goal",
26162617
capture_hooks,
26172618
)
26182619
monkeypatch.setattr(
2619-
cli_module,
2620-
"handle_project_lifecycle_command",
2620+
"loopx.cli_commands.project_lifecycle.handle_project_lifecycle_command",
26212621
lambda *_args, **_kwargs: 7,
26222622
)
26232623

2624-
exit_code = cli_module.main(
2624+
exit_code = import_module(entrypoint).main(
26252625
[
26262626
"--registry",
26272627
str(registry_path),

‎tests/control_plane/test_refresh_external_delivery.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
from loopx.control_plane.quota.settlement import SettlementIdentity
1010
from loopx.state_refresh import refresh_state_run
1111
from tests.control_plane.test_quota_settlement_cli import (
12-
AGENT_ID, GOAL_ID, TODO_ID, TURN_ID, _write_fixture,
12+
AGENT_ID, GOAL_ID, TODO_ID, TURN_ID, _write_fixture, _initialize_fixture_authority,
1313
)
1414

1515

1616
@pytest.fixture
1717
def session(tmp_path, monkeypatch, capsys):
1818
project, runtime, registry = _write_fixture(tmp_path)
19+
_initialize_fixture_authority(project, runtime)
1920
monkeypatch.setenv("LOOPX_RUNTIME_ROOT", str(tmp_path / "shared"))
2021
monkeypatch.chdir(project)
2122
prefix = ["--registry", str(registry), "--runtime-root", str(runtime)]

‎tests/test_cli_entrypoint.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ def test_command_reference_does_not_require_default_runtime_route(
286286
(["global-risks", "--help"], "loopx.cli_commands.summary_all_registration", "loopx.cli_commands.summary_all"),
287287
(["turn", "--help"], "loopx.cli_commands.turn_registration", "loopx.cli_commands.turn"),
288288
(["turn", "run-once", "--help"], "loopx.cli_commands.turn_registration", "loopx.cli_commands.turn"),
289+
*[( [command, "--help"], "loopx.cli_commands.project_lifecycle", "loopx.cli_commands.project_lifecycle")
290+
for command in ("refresh-state", "checkpoint-context", "read-only-map", "reward", "operator-gate")],
289291
],
290292
)
291293
def test_common_command_help_loads_only_its_owner(
@@ -413,6 +415,12 @@ def test_quota_facade_lazy_exports_preserve_owner_identity() -> None:
413415

414416
def test_selected_parser_matches_full_help_and_diagnostics() -> None:
415417
argv_cases = [
418+
*[[command, flag] for command in
419+
("refresh-state", "checkpoint-context", "read-only-map", "reward", "operator-gate")
420+
for flag in ("--help", "--unknown-option")],
421+
["refresh-state", "--goal-id", "goal", "--delivery-outcome", "invalid"],
422+
["checkpoint-context", "--goal-id", "goal", "--agent-id", "agent", "--turn-instance-id", "turn",
423+
"--todo-id", "todo_work", "--replan-obligation-id", "replan"],
416424
["commands", "--help"],
417425
["commands", "--format", "json"],
418426
["commands", "--format", "markdown"],
@@ -487,6 +495,48 @@ def test_selected_manager_inbox_refusals_match_without_loading_full_cli(tmp_path
487495
assert json.loads(selected.stdout)["ok"] is False
488496

489497

498+
@pytest.mark.parametrize("arguments", [
499+
["refresh-state", "--dry-run"],
500+
["checkpoint-context", "--agent-id", "perf-agent", "--turn-instance-id", "missing", "--todo-id", "todo_missing"],
501+
["read-only-map"],
502+
["reward", "--decision", "continue_route", "--reward", "positive", "--reason-summary", "Checked evidence", "--dry-run"],
503+
["operator-gate", "--decision", "approve", "--reason-summary", "Checked evidence", "--dry-run"],
504+
])
505+
def test_selected_lifecycle_refusals_match_without_full_cli(tmp_path, arguments):
506+
registry, runtime = write_command_fixture(tmp_path)
507+
before = registry.read_bytes()
508+
argv = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json",
509+
*arguments, "--goal-id", "missing-goal"]
510+
selected = run_cli_main("loopx.entrypoint", argv, forbidden_modules=("loopx.cli",))
511+
full = run_cli_main("loopx.cli", argv)
512+
assert selected.returncode == full.returncode == 1
513+
assert selected.stdout == full.stdout
514+
assert selected.stderr == full.stderr
515+
assert json.loads(selected.stdout)["ok"] is False
516+
assert registry.read_bytes() == before
517+
518+
519+
@pytest.mark.parametrize("provider", ["file", "sqlite"])
520+
def test_selected_refresh_preview_keeps_canonical_state_without_full_cli(tmp_path, monkeypatch, provider):
521+
from tests.control_plane.canonical_authority_fixture import initialize_canonical_source_fixture, isolate_sqlite_runtime
522+
from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted
523+
524+
isolate_sqlite_runtime(tmp_path, monkeypatch)
525+
registry, runtime = write_command_fixture(tmp_path)
526+
state = registry.parent.parent / ".codex/goals/perf-goal/ACTIVE_GOAL_STATE.md"
527+
initialize_canonical_source_fixture(runtime, "perf-goal", state_path=state, provider=provider)
528+
before = state.read_bytes(), read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="perf-goal")
529+
argv = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json",
530+
"refresh-state", "--goal-id", "perf-goal", "--agent-id", "perf-agent",
531+
"--next-action", "Continue bounded fixture work.", "--no-global-sync", "--dry-run"]
532+
for module in ("loopx.entrypoint", "loopx.cli"):
533+
result = run_cli_main(module, argv, forbidden_modules=("loopx.cli",) if module == "loopx.entrypoint" else ())
534+
assert result.returncode == 0, result.stderr
535+
payload = json.loads(result.stdout)
536+
assert payload["ok"] and payload["dry_run"]
537+
assert (state.read_bytes(), read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="perf-goal")) == before
538+
539+
490540
def test_selected_manager_result_matches_full_replay_without_loading_full_cli(tmp_path):
491541
from test_collaboration_goal_instance import _create_source_registry, _manager_request
492542
from loopx.control_plane.collaboration.inbox import _root, acknowledge

0 commit comments

Comments
 (0)