diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index d6976e2071..e8fd42a4e6 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -3509,3 +3509,53 @@ exit-only Agent: registration and honest failed-Turn recovery do not prove natural-language task completion or model benefit. Complete minimal closure, R1/M3, workspace/meta publication, installed-host adoption and full-cost acceptance remain open. + +### 2026-11-13 — Standalone natural-language startup reaches existing owners + +The independently installed core now routes `start-goal`, +`bootstrap-command-pack`, `register-agent` and `bind-agent-thread` to the +ordinary product's existing registrars and handlers. Previously all four +required the omitted full CLI facade. The goal preview preserves exact text, +remains read-only and exposes the same ordered bootstrap, identity, Todo and +host/guard transaction. Fresh registration, collision rejection, global/source +readback and bound-session reuse execute against the default SQLite authority. +No new engine, model service, authority rule or capability activation is added. +The ordinary installation retains its complete-parser diagnostic fallback; +invalid standalone arguments retain the existing explicit unavailable failure. + +Installed qualification passes 16 checks, including the guided identity journey +and existing SQLite/File execution, rejected artifacts, renewed/released lease +refusal and confirmed-result recovery/replay. Eight focused comparisons match +the ordinary parser and preview. The original baseline independently installed +wheel refuses natural-language startup; the repaired wheel executes it. These +are executable behavioral deltas, not help-text-only delivery. + +A separate isolated generic-CLI task was performed by the current host model: +create a reusable inventory generator and JSON/Markdown reports from source +catalog/movement files, with exact decimal valuation and excluded records. Its +independent validator accepts the result. Killing the driver after its typed +result was journaled, then resuming the original key, completes the Todo and +records one settlement without invoking the model again; a further replay has +no state or quota effects. An earlier pre-work attempt expired while awaiting +model input; its unknown-outcome recovery rejection remains recorded. There +were two host transport dispatches in total and one confirmed model result; +this is not a claim of one dispatch across the entire experiment. + +A three-pair local loading census (macOS arm64, Python 3.13.5, Node 26.0.0, +new Python processes with ordinary filesystem/bytecode caches, plus two warm +calls per process) observes 8 core modules for top-level help and 186 for guided +help/preview. The direct ordinary full-facade comparison loads 1,151/1,164 +modules. Preview includes two subprocess events and nine Python socket audit +events, so process/IPC costs must remain in the closure inventory. Different +source/install cache and launch conditions, three samples, Python-only audit +coverage and unavailable model-token counters prevent a performance or complete +V10 claim. This does not approve D2 or a minimal preset. + +The source-regression attempt that exhausted local disk remains failed; its +recovered run is recorded separately. Complete minimal install/load/context +closure, initial/revised/compacted/lost-ACK context qualification, supported host +adoption, full V1–V5/V10, R1/M3, Standard/workspace/meta splitting and complete +costs remain open. Rollback the routing slice while retaining task authority and +reconciling original attempts. Existing exact-head review, premerge and bilingual +requirements, original failures, budgets and separate research/release authority +remain in force. diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md index 0159df1b5a..13989153c9 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -2736,3 +2736,42 @@ dry-run 拒绝路径的中位数为 142.4→135.1 ms、934.4→911.8 ms。零条 首次 Goal 夹具使用退出式 Agent:登记及如实显示失败 Turn 的恢复不证明自然语言任务 完成或模型收益。完整最小闭包、R1/M3、workspace/meta 发布、安装宿主采用及完整成本 验收仍保持开放。 + +### 2026-11-13 — 独立发行版的自然语言启动到达既有 owner + +独立安装的 core 现在将 `start-goal`、`bootstrap-command-pack`、 +`register-agent` 和 `bind-agent-thread` 路由到普通产品既有的 registrar 与 +handler。此前四条命令都依赖被排除的完整 CLI facade。目标预览保留完整原文, +保持只读,并返回同一套有序的 bootstrap、身份、Todo 与 host/guard 事务。 +新身份注册、冲突拒绝、global/source 回读和绑定会话复用在默认 SQLite authority +上真实执行。没有新增 engine、模型服务、权限规则或 capability 激活。 +普通安装仍保留完整 parser 的诊断 fallback;独立 core 的非法参数仍返回既有明确 +unavailable 错误。 + +安装验证通过 16 项检查,覆盖 guided 身份旅程及既有 SQLite/File 执行、产物拒绝、 +续租/释放后的完成拒绝和已确认结果恢复/重放。八项聚焦对照与普通 parser 和预览 +一致。原始独立安装基线 wheel 拒绝自然语言启动,修复 wheel 能执行这条路径。 +这是真实可执行的行为差异,不是仅修改帮助文案。 + +另一个隔离 generic-CLI 任务由当前 host 模型实际完成:依据 catalog/movement +源文件生成可复用库存脚本及 JSON/Markdown 报告,保留精确小数计价和排除记录。 +独立 validator 接受结果。typed result 已入 journal 后杀掉 driver,再用原始 key +恢复,可完成 Todo 并只结算一次,且不再次调用模型;再次重放无状态或额度副作用。 +更早一次任务前尝试在等待模型输入期间过期,其 unknown-outcome 恢复拒绝仍保留。 +整个实验共两次 host transport dispatch、一个已确认模型结果,不能宣称全实验 +只 dispatch 一次。 + +三对本地 loading census 使用 macOS arm64、Python 3.13.5、Node 26.0.0, +新 Python 进程保留普通文件系统/bytecode 缓存,每进程再运行两次 warm 调用。 +core 顶层帮助加载 8 个模块,guided 帮助/预览加载 186 个;直接普通 full-facade +对照为 1,151/1,164 个。预览出现两次 subprocess 和九次 Python socket audit +事件,process/IPC 成本必须保留在闭包清单。源码/安装的缓存与启动条件不同、仅三 +个样本、仅 Python audit 范围及缺失模型 token 计数,使其不能支持性能或完整 +V10 结论,也不批准 D2 或 minimal preset。 + +源码回归因本地磁盘耗尽而中止的尝试仍为失败,恢复后的运行单独记录。 +完整最小 install/load/context 闭包、initial/revised/compacted/lost-ACK 上下文 +资格、受支持 host 采用、完整 V1–V5/V10、R1/M3、Standard/workspace/meta 拆包 +和完整成本仍未完成。回滚路由切片时保留 task authority 并协调原始尝试。 +既有 exact-head 评审、premerge、双语要求、原始失败、预算及独立研究/发布授权 +继续有效。 diff --git a/loopx/cli_runtime.py b/loopx/cli_runtime.py index 5624deb6d2..01d57b6ef4 100644 --- a/loopx/cli_runtime.py +++ b/loopx/cli_runtime.py @@ -75,7 +75,8 @@ _REGISTRY_INSPECTION_COMMANDS = frozenset({"registry", "registry-boundary"}) _SELECTED_COMMANDS = _STATUS_COMMANDS | GLOBAL_READ_COMMANDS | PROJECT_LIFECYCLE_COMMANDS | _REGISTRY_INSPECTION_COMMANDS | { "todo", "quota", "task-lease", "change-window", "delegation", "turn", "doctor", "commands", "version", - "bootstrap", "connect", "configure-goal", + "bootstrap", "connect", "configure-goal", "start-goal", "bootstrap-command-pack", + "register-agent", "bind-agent-thread", "authority-archive", "coordination-shadow", "extension", "slash-commands", "manager-inbox", "agent-context", "agent-directory", "agent-capabilities", @@ -393,7 +394,19 @@ def _handle_registry_inspection_command( def _build_selected_parser(command: str) -> LoopXArgumentParser: parser, subparsers = build_cli_parser(parser_class=_SelectedArgumentParser) - if command in {"bootstrap", "connect"}: + if command == "start-goal": + from .cli_commands.start_goal import register_start_goal_command + + register_start_goal_command(subparsers) + elif command == "bootstrap-command-pack": + from .cli_commands.starter_bootstrap_registration import register_starter_bootstrap_commands + + register_starter_bootstrap_commands(subparsers) + elif command in {"register-agent", "bind-agent-thread"}: + from .cli_commands.registry_admin import register_registry_admin_commands + + register_registry_admin_commands(subparsers) + elif command in {"bootstrap", "connect"}: from .cli_commands.bootstrap_connect import register_bootstrap_connect_command register_bootstrap_connect_command(subparsers) @@ -526,13 +539,25 @@ def _dispatch_common_command( registry_path: Path, allow_missing_registry: bool, ) -> int | None: + if args.command == "start-goal": + from .cli_commands.start_goal import handle_start_goal_command + + return handle_start_goal_command( + args, print_payload, runtime_root_arg=args.runtime_root, + ) + if args.command == "bootstrap-command-pack": + from .cli_commands.starter_bootstrap import handle_loopx_bootstrap_command_pack_command + + return handle_loopx_bootstrap_command_pack_command( + args, print_payload, runtime_root_arg=args.runtime_root, + ) if args.command in {"bootstrap", "connect"}: from .cli_commands.bootstrap_connect import handle_bootstrap_connect_command return handle_bootstrap_connect_command( args, registry_path=registry_path, print_payload=print_payload, ) - if args.command == "configure-goal": + if args.command in {"configure-goal", "register-agent", "bind-agent-thread"}: from .cli_commands.registry_admin import handle_registry_admin_command return handle_registry_admin_command( diff --git a/loopx/help_surface.py b/loopx/help_surface.py index 9bfc84fcd7..7b5b3fa782 100644 --- a/loopx/help_surface.py +++ b/loopx/help_surface.py @@ -488,6 +488,8 @@ def render_concise_help(program: str = "loopx", *, standalone_core: bool = False "LoopX keeps long-running agent work moving with durable state and evidence.", "", "Usage:", f" {program} [global options] [command options]", "", "Start:", + ' loopx start-goal --guided --project . --goal-text ""', + " Start from a goal with your existing model host.", " loopx connect --help Connect a project and its objective.", " loopx configure-goal --help Configure its Agent and execution budget.", " loopx todo --help Create, claim and complete work.", diff --git a/packages/loopx-core/README.md b/packages/loopx-core/README.md index cc2c5a43e1..3afd0ea850 100644 --- a/packages/loopx-core/README.md +++ b/packages/loopx-core/README.md @@ -21,8 +21,20 @@ tasks requires a qualified Node runtime (22.22.3+ and the existing SQLite reliability checks), an explicitly selected host and its own prerequisites. Use the existing `turn run-once --host generic-cli` entry, a separate validation command, `turn inspect-journal` and `--resume-turn-key` for execution and recovery. -Bootstrap with `connect`, configure one Agent with `configure-goal`, and create -and claim work through the existing `todo` commands. An ordinary managed claim +Start with one natural-language goal in your existing model host: + +```sh +loopx start-goal --guided --project . --goal-text "Create and check an inventory report." +``` + +Follow its returned host choice and ordered transaction. The preview preserves +the exact goal text and writes no state. Its project bootstrap, fresh identity +registration, session binding and subsequent Todo/guard commands execute in +this standalone distribution through their existing owners. The host supplies +the model, tools and scheduler; the wheel does not provide them or activate an +optional capability from goal wording. `connect` and `configure-goal` remain +available for explicit setup. Create and claim work through the existing `todo` +commands. An ordinary managed claim prepares the default SQLite lease without asking the task model to manage lease flags. Completion carries the original captured lease generation; recovery refuses a renewed, released or replaced generation rather than adopting it. diff --git a/tests/test_cli_entrypoint.py b/tests/test_cli_entrypoint.py index f2e7ce2bb0..bdaef754c0 100644 --- a/tests/test_cli_entrypoint.py +++ b/tests/test_cli_entrypoint.py @@ -2056,3 +2056,38 @@ def test_selected_connect_preview_keeps_full_cli_result_without_workspace(tmp_pa assert selected.returncode == ordinary.returncode == 0, (selected.stderr, ordinary.stderr) assert json.loads(selected.stdout) == json.loads(ordinary.stdout) assert not registry.exists() + + +@pytest.mark.parametrize('arguments', [ + ['start-goal', '--help'], + ['bootstrap-command-pack', '--help'], + ['register-agent', '--help'], + ['bind-agent-thread', '--help'], + ['start-goal', '--guided', '--goal-text', '完整 goal', '--host-surface', 'unsupported'], + ['register-agent', '--goal-id', 'missing'], + ['bind-agent-thread', '--goal-id', 'missing', '--agent-id', 'test'], +]) +def test_guided_core_routes_match_complete_parser(arguments): + # Invalid argv retains the ordinary installation's canonical full-parser + # diagnostic fallback; valid selected help must never load that facade. + selected = run_cli_main('loopx.entrypoint', arguments, + forbidden_modules=('loopx.cli',) if '--help' in arguments else ()) + full = run_cli_main('loopx.cli', arguments) + assert (selected.returncode, selected.stdout, selected.stderr) == (full.returncode, full.stdout, full.stderr) + + +def test_guided_core_preview_matches_complete_owner_without_writes(tmp_path): + project = tmp_path / 'project' + project.mkdir() + argv = ['--runtime-root', str(tmp_path / 'runtime'), '--format', 'json', + 'start-goal', '--guided', '--project', str(project), + '--host-surface', 'shell', '--goal-text', 'Report "inventory" and 总计.'] + environment = {'CODEX_THREAD_ID': '', 'TRAECLI_THREAD_ID': '', 'LOOPX_USAGE_PING': '0'} + selected = run_cli_main('loopx.entrypoint', argv, forbidden_modules=('loopx.cli',), environment=environment) + full = run_cli_main('loopx.cli', argv, environment=environment) + assert selected.returncode == full.returncode == 0 + assert selected.stdout == full.stdout and selected.stderr == full.stderr + packet = json.loads(selected.stdout) + assert packet['goal_text'] == 'Report "inventory" and 总计.' + assert packet['guided_transaction']['writes_now'] is False + assert not list(project.iterdir()) and not (tmp_path / 'runtime').exists() diff --git a/tests/test_core_distribution.py b/tests/test_core_distribution.py index 1d8f45ba65..05df34660d 100644 --- a/tests/test_core_distribution.py +++ b/tests/test_core_distribution.py @@ -118,7 +118,7 @@ def test_installed_core_identity_help_and_unavailable_workspace(core_python, tmp assert not Path(json.loads(probe.stdout)["package"]).is_relative_to(REPOSITORY) help_text = _run(core_python, ["--help"], tmp_path, environment={"PATH": str(tmp_path)}).stdout assert "loopx connect --help" in help_text and "loopx turn run-once --help" in help_text - assert "preset list" not in help_text and "start-goal" not in help_text + assert "preset list" not in help_text and "loopx start-goal --guided" in help_text reference = json.loads(_run(core_python, ["commands", "--format", "json"], tmp_path).stdout) commands = [entry["command"] for group in reference["groups"] for entry in group["commands"]] assert "loopx configure-goal --help" in commands @@ -132,6 +132,53 @@ def test_installed_core_identity_help_and_unavailable_workspace(core_python, tmp assert not list(tmp_path.iterdir()) +def test_installed_core_guided_start_identity_and_readback(core_python, tmp_path, monkeypatch): + from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime + + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime = tmp_path / "project", tmp_path / "runtime" + project.mkdir() + prefix = ["--runtime-root", str(runtime), "--format", "json"] + objective = 'Produce an inventory report; preserve "quoted" source names and 总计.' + start = [*prefix, "start-goal", "--guided", "--project", str(project), + "--goal-id", GOAL, "--host-surface", "shell", "--goal-text", objective] + + def command(*arguments, expected=0): + return json.loads(_run(core_python, list(arguments), project, expected=expected).stdout) + + preview = command(*start) + assert preview["goal_text"] == objective + assert preview["guided_transaction"]["writes_now"] is False + assert preview["guided_transaction"]["spends_quota_now"] is False + assert preview["recommended_next_step"]["identity_selection_gate"]["default_action"] == "register_fresh_agent" + assert not list(project.iterdir()) and not runtime.exists() + # These are the real bootstrap and identity owners advertised by the + # preview, running from the standalone wheel with the full facade absent. + created = command(*prefix, "bootstrap", "--project", str(project), "--goal-id", GOAL, + "--objective", objective, "--write-scope", "docs/**") + assert created["storage_selection"]["provider"] == "sqlite" + assert created["global_sync"]["ok"] is True + registered = command(*prefix, "register-agent", "--goal-id", GOAL, + "--agent-id", AGENT, "--require-new", "--execute") + assert registered["registration_readback"]["verified"] is True + assert registered["global_sync"]["ok"] is True + collision = command(*prefix, "register-agent", "--goal-id", GOAL, + "--agent-id", AGENT, "--require-new", "--execute", expected=1) + assert collision["ok"] is False and collision["written"] is False + binding = command(*prefix, "bind-agent-thread", "--goal-id", GOAL, + "--agent-id", AGENT, "--thread-id", "core-session", "--host-surface", "shell", "--execute") + assert binding["global_sync"]["ok"] is True + assert binding["registration_readback"]["verified"] is True + resumed = command(*start, "--thread-id", "core-session") + assert resumed["agent_id"] == AGENT + assert resumed["project_connection"]["goal_found"] is True + assert "identity_selection_gate" not in resumed["guided_transaction"] + pack = command(*prefix, "bootstrap-command-pack", "--project", str(project), + "--goal-id", GOAL, "--agent-id", AGENT, "--host-surface", "shell", "--goal-text", objective) + assert pack["goal_start_contract"]["goal_text"] == objective + assert pack["goal_start_contract"]["explicit_invocation_confirms_project_local_state_writes"] is True + + def test_installed_core_catalog_entries_have_executable_parsers(core_python, tmp_path): reference = json.loads(_run(core_python, ["commands", "--format", "json"], tmp_path).stdout) for group in reference["groups"]: