diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index e8fd42a4e6..44ed1ddf37 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -3559,3 +3559,52 @@ 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. + +### 2026-11-13 — Startup metadata isolation and installed context recovery + +Default goal-start help and preview previously imported issue-fix execution, +candidate preflight and repository analysis merely to render command hints. +The existing command templates now have a capability-owned metadata module; +bootstrap and catalog consumers use it, while the workflow module preserves +its existing Python import API and an explicit issue-fix route retains its complete preflight +contract. Independently installed base/head startup output is byte-identical; +default help/preview load 186→171 LoopX modules and the explicit route 186→179. +Module counts alone establish a narrower loading boundary, not a speed claim. + +Thirty alternating installed base/head pairs per workload on macOS arm64, +Python 3.13.5 and ordinary OS/bytecode caches observe first-entry median/p95 +of 314.73/362.55→181.25/303.01 ms for help and +542.36/670.44→311.08/572.66 ms for preview. Two subsequent calls per process +have warm medians 0.66→0.54 ms and 11.83→8.94 ms. Median peak RSS is +43.22→32.46 MB and 46.05→36.18 MB. All complete outputs match; +preview remains 52,727 bytes. Retain the head preview maximum 1,092.15 ms. +First entry includes imports; this is neither OS-cold nor full V10 qualification. +Source regression passes 348 checks with one native Windows/PowerShell skip; +the final installed wheel passes 20 checks. Earlier failed attempts remain. + +Installed CLI qualification exercises the existing context-project/read/hook +owners with complete source or explicit recovery: off keeps original values, +partial delivery and lost ACK cannot establish a baseline, unchanged/revised +views preserve complete constraints, standalone PostCompact invalidates the +baseline, and a changed captured file is refused. Warm transport may choose +full when complete delta framing would cost more. The test runs against the +original installed core as well; it qualifies the existing installed protocol +and supplies a regression boundary rather than introducing another context +owner. Native events are fixtures, not evidence of model retention or adoption. + +Instrumented SQLite/File Turn witnesses retain Python, actual Node-loader and +RPC observations. The selected Turn entry still loads 525 Python modules, +including 111 capability modules; its Node runtime retains eager shared typed +support. Complete work contracts and transport costs remain in the inventory. +This diagnostic does not qualify endpoint/network completeness, OS-cold cost, +model tokens or the full minimal composition. Keep earlier failed checks and +the separate startup/context boundary; full V1–V5/V10, R1/M3, independent +Standard/workspace packages, installed-host adoption and complete original-goal +regression remain open under their existing owners and acceptance. + +The installation gate exposed a project-skill supporting-example link that left +the installed bundle. The link now resolves to the byte-verified public source +at an immutable revision; the procedure, consent and check-scope clauses remain +unchanged. It is an on-demand supporting reference, not a runtime fetch or new +authority. Preserve the earlier failed installation gate and disk-limited base +replays separately from the repaired-head validation. 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 13989153c9..83dc5bb043 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -2775,3 +2775,41 @@ V10 结论,也不批准 D2 或 minimal preset。 和完整成本仍未完成。回滚路由切片时保留 task authority 并协调原始尝试。 既有 exact-head 评审、premerge、双语要求、原始失败、预算及独立研究/发布授权 继续有效。 + +### 2026-11-13 — 启动元数据隔离与安装上下文恢复 + +默认目标启动的帮助和预览此前只是为了渲染命令提示,就导入 issue-fix 执行、 +candidate preflight 和仓库分析。既有命令模板现在放在 capability 自有的元数据 +模块;bootstrap 与 catalog 消费它,workflow 模块保留原 Python 导入接口,显式 +issue-fix 路由仍带完整 preflight 契约。独立安装 base/head 的启动输出逐字节一致; +默认帮助/预览的 LoopX 模块加载为 186→171,显式路由为 186→179。 +模块数量本身只证明加载边界缩小,不是速度收益结论。 + +macOS arm64、Python 3.13.5、普通 OS/bytecode 缓存下,每项 workload 运行 +30 对交替的独立安装 base/head,首次入口的中位数/p95 为帮助 +314.73/362.55→181.25/303.01 ms,预览 542.36/670.44→311.08/572.66 ms。 +每进程随后两次调用的 warm 中位数为 0.66→0.54 ms 和 11.83→8.94 ms; +峰值 RSS 中位数为 43.22→32.46 MB 和 46.05→36.18 MB。全部完整输出一致, +预览仍为 52,727 字节。保留 head 预览最大值 1,092.15 ms。首次入口包含导入, +本记录不属于 OS-cold 或完整 V10 资格。源码回归通过 348 项,1 项因缺少原生 +Windows/PowerShell 环境跳过;最终安装 wheel 通过 20 项。早期失败尝试仍保留。 + +安装 CLI 验证通过既有 context-project/read/hook owner 交付完整源或明确恢复: +关闭功能时保留原始值;部分交付和丢 ACK 不能建立基线;未变化/修订视图保留完整 +约束;独立 PostCompact 使基线失效;捕获文件被修改时拒绝读取。完整 delta framing +更贵时,warm 传输可选择 full。同一测试也在原独立 core 安装上运行,资格化既有 +安装协议并补充回归边界,不新增上下文 owner。原生事件为夹具,不证明模型保留 +上下文或宿主采用。 + +带追踪的 SQLite/File Turn witness 保留 Python、真实 Node loader 和 RPC 观察。 +选定 Turn 入口仍加载 525 个 Python 模块,其中 111 个 capability 模块;Node +runtime 仍保留 eager 的共享 typed 支持。完整 work contract 与传输成本继续保留 +在清单中。本诊断不资格化 endpoint/网络完整性、OS-cold 成本、模型 token 或完整 +最小组合。保留早期检查失败及独立的启动/上下文边界;完整 V1–V5/V10、R1/M3、 +独立 Standard/workspace 包、安装宿主采用及覆盖原始目标的完整回归仍由既有 +owner 和验收约束保持开放。 + +安装门暴露了 project skill 的支持示例链接离开安装包的问题。链接现在指向经字节 +核验的公开源文件,并固定不可变 revision;原流程、同意边界和检查范围条款保持 +原义。它是按需支持资料,不是 runtime fetch 或新 authority。早期安装门失败和 +受磁盘限制的 base 重放,与修复 head 的验证分别保留。 diff --git a/loopx/bootstrap_command_pack.py b/loopx/bootstrap_command_pack.py index 3cf4437cab..06c78b5498 100644 --- a/loopx/bootstrap_command_pack.py +++ b/loopx/bootstrap_command_pack.py @@ -7,10 +7,7 @@ from .agent_registry import registered_agent_ids_for_goal from .control_plane.projects.identity import default_goal_id from .paths import registered_goal_state_file -from .capabilities.issue_fix.candidate_preflight import ( - candidate_preflight_input_contract, -) -from .capabilities.issue_fix.workflow_plan import ( +from .capabilities.issue_fix.command_templates import ( ISSUE_FIX_GOAL_CANDIDATE_DISCOVERY_COMMAND_TEMPLATE, build_issue_fix_goal_command_templates, ) @@ -760,6 +757,8 @@ def _selected_goal_capability_route( "unsupported --capability-route; expected one of: " + ", ".join(START_GOAL_CAPABILITY_ROUTES) ) + from .capabilities.issue_fix.candidate_preflight import candidate_preflight_input_contract + return { "schema_version": GOAL_CAPABILITY_ROUTE_SCHEMA_VERSION, "capability_id": "issue-fix", diff --git a/loopx/capabilities/issue_fix/catalog_entry.py b/loopx/capabilities/issue_fix/catalog_entry.py index 046dcc00c7..1b8b9ca0f2 100644 --- a/loopx/capabilities/issue_fix/catalog_entry.py +++ b/loopx/capabilities/issue_fix/catalog_entry.py @@ -2,7 +2,7 @@ from typing import Any -from .workflow_plan import build_issue_fix_pr_lifecycle_command +from .command_templates import build_issue_fix_pr_lifecycle_command ISSUE_FIX_CATALOG_ENTRY: dict[str, Any] = { diff --git a/loopx/capabilities/issue_fix/command_templates.py b/loopx/capabilities/issue_fix/command_templates.py new file mode 100644 index 0000000000..00224d7b8a --- /dev/null +++ b/loopx/capabilities/issue_fix/command_templates.py @@ -0,0 +1,94 @@ +"""Issue-fix command metadata without execution-provider imports.""" +from __future__ import annotations + +import shlex + +ISSUE_FIX_GOAL_CANDIDATE_DISCOVERY_COMMAND_TEMPLATE = ( + "gh issue list --repo " + '"$(gh repo view --json nameWithOwner --jq .nameWithOwner)" ' + "--state open --limit 20 --json number,title,url,labels" +) + + +def _command_arg(value: str) -> str: + if value.startswith("<") and value.endswith(">"): + return value + return shlex.quote(value) + + +def build_issue_fix_pr_lifecycle_command( + *, + cli_bin: str, + goal_id: str, + agent_id: str, + project: str | None = None, +) -> str: + """Build the canonical executable PR lifecycle reconciliation command.""" + + parts = [ + shlex.quote(cli_bin), + "issue-fix", + "pr-lifecycle", + "--url", + "", + ] + if project: + parts.extend(["--project", _command_arg(project)]) + parts.extend( + [ + "--goal-id", + _command_arg(goal_id), + "--claimed-by", + _command_arg(agent_id), + "--execute-transition", + "--format", + "json", + ] + ) + return " ".join(parts) + + +def build_issue_fix_goal_command_templates( + *, cli_bin: str, goal_id: str, agent_id: str = "" +) -> dict[str, str]: + """Return the capability-owned commands projected into goal-start packets.""" + + cli = shlex.quote(cli_bin) + goal = ( + goal_id + if goal_id.startswith("<") and goal_id.endswith(">") + else shlex.quote(goal_id) + ) + return { + "issue_fix_workflow_plan_template": ( + f"{cli} issue-fix workflow-plan " + "--url " + "--repo-path " + "--repository-context-json " + "--fetch-candidate-evidence " + "--validation-label '' " + f"--goal-id {goal} " + "--format json" + ), + "issue_fix_feasibility_template": ( + f"{cli} issue-fix feasibility " + "--url " + "--reproduction-status " + "--scope-class " + "--repository-context-json " + f"--goal-id {goal} " + "--format json" + ), + "issue_fix_pr_lifecycle_template": build_issue_fix_pr_lifecycle_command( + cli_bin=cli_bin, + goal_id=goal_id, + agent_id=agent_id, + ), + "issue_fix_reviewer_request_template": ( + f"{cli} issue-fix reviewer-request " + "--url " + "--repo-path " + "--base-ref " + "--execute --format json" + ), + } diff --git a/loopx/capabilities/issue_fix/workflow_plan.py b/loopx/capabilities/issue_fix/workflow_plan.py index f5625566aa..c5529f457a 100644 --- a/loopx/capabilities/issue_fix/workflow_plan.py +++ b/loopx/capabilities/issue_fix/workflow_plan.py @@ -1,9 +1,13 @@ from __future__ import annotations -import shlex from collections.abc import Mapping, Sequence from typing import Any +from .command_templates import ( + ISSUE_FIX_GOAL_CANDIDATE_DISCOVERY_COMMAND_TEMPLATE as ISSUE_FIX_GOAL_CANDIDATE_DISCOVERY_COMMAND_TEMPLATE, + build_issue_fix_goal_command_templates as build_issue_fix_goal_command_templates, + build_issue_fix_pr_lifecycle_command as build_issue_fix_pr_lifecycle_command, +) from .acceptance_loop import build_issue_fix_caller_repo_branch_packet from .candidate_preflight import build_issue_fix_candidate_preflight_packet from .intake_surface import build_content_ops_issue_fix_metadata_preview_packet @@ -14,93 +18,6 @@ ) ISSUE_FIX_WORKFLOW_PLAN_PACKET_SCHEMA_VERSION = "issue_fix_workflow_plan_packet_v0" -ISSUE_FIX_GOAL_CANDIDATE_DISCOVERY_COMMAND_TEMPLATE = ( - "gh issue list --repo " - '"$(gh repo view --json nameWithOwner --jq .nameWithOwner)" ' - "--state open --limit 20 --json number,title,url,labels" -) -def _command_arg(value: str) -> str: - if value.startswith("<") and value.endswith(">"): - return value - return shlex.quote(value) - - -def build_issue_fix_pr_lifecycle_command( - *, - cli_bin: str, - goal_id: str, - agent_id: str, - project: str | None = None, -) -> str: - """Build the canonical executable PR lifecycle reconciliation command.""" - - parts = [ - shlex.quote(cli_bin), - "issue-fix", - "pr-lifecycle", - "--url", - "", - ] - if project: - parts.extend(["--project", _command_arg(project)]) - parts.extend( - [ - "--goal-id", - _command_arg(goal_id), - "--claimed-by", - _command_arg(agent_id), - "--execute-transition", - "--format", - "json", - ] - ) - return " ".join(parts) - - -def build_issue_fix_goal_command_templates( - *, cli_bin: str, goal_id: str, agent_id: str = "" -) -> dict[str, str]: - """Return the capability-owned commands projected into goal-start packets.""" - - cli = shlex.quote(cli_bin) - goal = ( - goal_id - if goal_id.startswith("<") and goal_id.endswith(">") - else shlex.quote(goal_id) - ) - return { - "issue_fix_workflow_plan_template": ( - f"{cli} issue-fix workflow-plan " - "--url " - "--repo-path " - "--repository-context-json " - "--fetch-candidate-evidence " - "--validation-label '' " - f"--goal-id {goal} " - "--format json" - ), - "issue_fix_feasibility_template": ( - f"{cli} issue-fix feasibility " - "--url " - "--reproduction-status " - "--scope-class " - "--repository-context-json " - f"--goal-id {goal} " - "--format json" - ), - "issue_fix_pr_lifecycle_template": build_issue_fix_pr_lifecycle_command( - cli_bin=cli_bin, - goal_id=goal_id, - agent_id=agent_id, - ), - "issue_fix_reviewer_request_template": ( - f"{cli} issue-fix reviewer-request " - "--url " - "--repo-path " - "--base-ref " - "--execute --format json" - ), - } def _todo_preview( diff --git a/packages/loopx-core/README.md b/packages/loopx-core/README.md index 3afd0ea850..ab24bf78b2 100644 --- a/packages/loopx-core/README.md +++ b/packages/loopx-core/README.md @@ -43,6 +43,16 @@ The installed-only qualification in exercises setup, independent result validation, process loss, replay and refusal. No model service, credentials or paid task execution is supplied by this wheel. +Goal-start help and preview use issue-fix command metadata without importing +its execution providers. An explicit `--capability-route issue-fix` retains +the existing candidate-preflight contract. The installed context qualification +in [`test_installed_core_context.py`](../../tests/control_plane/test_installed_core_context.py) +executes the existing `quota context-project`, `context-read` and `context-hook` +commands: incomplete delivery cannot establish a baseline, revisions preserve +complete constraints, compaction requires recovery, and changed capture bytes +are refused. Its native events are protocol fixtures; they do not establish +model retention or installed-host adoption. + The candidate owns the same `loopx` import namespace and console command as the ordinary distribution. Do not co-install or overlay them. Return to the ordinary installation by switching back to its environment; preserve task state and diff --git a/skills/loopx-project/references/outcome-refinement.md b/skills/loopx-project/references/outcome-refinement.md index 2c0153b739..c589cdb11b 100644 --- a/skills/loopx-project/references/outcome-refinement.md +++ b/skills/loopx-project/references/outcome-refinement.md @@ -33,7 +33,7 @@ consequential meaning or authority is genuinely missing, not merely because the user gave a correction. For obsolete work covered by executable checks, follow the -[owner check-scope reconciliation example](../../../docs/reference/goal-acceptance-observations.md#owner-check-scope-reconciliation). +[owner check-scope reconciliation example](https://github.com/loopx-project/loopx/blob/ce69003c73378fdb067a6b86e7b60feb50cb1b77/docs/reference/goal-acceptance-observations.md#owner-check-scope-reconciliation). It preserves accepted work and current/final checks in an explicit `selected_work` replacement before lease-fenced supersession. Removing a binding alone does not remove work from `all_advancement`. This owner configuration path diff --git a/tests/control_plane/test_installed_core_context.py b/tests/control_plane/test_installed_core_context.py new file mode 100644 index 0000000000..ccef5f705c --- /dev/null +++ b/tests/control_plane/test_installed_core_context.py @@ -0,0 +1,136 @@ +"""Installed CLI context continuity; native events are protocol fixtures, not a model.""" +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path +import shlex +import subprocess + +from test_core_distribution import AGENT, GOAL, _command, _environment +from test_core_distribution import core_python as core_python + + +def test_installed_context_recovers_partial_delivery_revisions_and_compaction(core_python, tmp_path): + directory = tmp_path / "native" + directory.mkdir(mode=0o700) + registry, runtime = tmp_path / "absent-registry.json", tmp_path / "absent-runtime" + source = tmp_path / "original.json" + event = {"session_id": "core-context-session", "cwd": str(tmp_path)} + environment = _environment(core_python, {"CODEX_THREAD_ID": event["session_id"]}) + prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] + + def command(arguments, *, stdin=None, expected=0): + result = subprocess.run(_command(core_python, [*prefix, *arguments]), + input=None if stdin is None else json.dumps(stdin), env=environment, + cwd=tmp_path, capture_output=True, text=True, timeout=45) + assert result.returncode == expected, (result.stdout, result.stderr) + return json.loads(result.stdout) + + def hook(name, **fields): + return command(["quota", "context-hook", "--context-delivery", "on", "--codex-app", + "--codex-context-dir", str(directory)], stdin={**event, "hook_event_name": name, **fields}) + + def state(): + return json.loads((directory / "session.json").read_bytes()) + + def acknowledge(pages): + for page in pages: + assert hook("PostToolUse", tool_use_id=f"page-{page['offset']}", + tool_response=json.dumps(page)) == {} + + row = {"schema_version": "todo_item_v0", "todo_id": "todo_context", "text": "Verify the report.", + "note": "Preserve the complete source and its constraints. " * 150 + "Stop before publication.", + "content_revision": "revision:one", "claimed_by": AGENT} + original = {"ok": True, "goal_id": GOAL, "agent_identity": {"agent_id": AGENT}, + "heartbeat_receipt": {"turn_instance_id": "context-fixture"}, + "stop": "Stop before publication.", "opaque_counter": 9007199254740993, + "agent_todo_summary": {"schema_version": "todo_summary_v0", + "first_open_items": [row], "backlog_items": [copy.deepcopy(row)], + "claimed_open_items": [copy.deepcopy(row)]}} + + def project(payload, setting="on"): + raw = json.dumps(payload, ensure_ascii=False).encode() + source.write_bytes(raw) + arguments = ["quota", "context-project", "--capture-file", str(source), + "--capture-sha256", hashlib.sha256(raw).hexdigest(), "--goal-id", GOAL, + "--agent-id", AGENT, "--turn-instance-id", "context-fixture", + "--context-delivery", setting, "--codex-app", "--codex-context-dir", str(directory)] + if setting == "on": + arguments += ["--max-page-output-bytes", "4000"] + return command(arguments) + + def read_required(wire, payload, *, budget=4000): + arguments = shlex.split(wire.get("recovery", wire)["command"]) + pages, contents = [], b"" + while True: + page = command(arguments[arguments.index("quota"):]) + assert page["ok"] and page["offset"] == len(contents) + if budget is not None: + assert len((json.dumps(page, ensure_ascii=False, separators=(",", ":")) + "\n").encode()) <= budget + contents += page["content"].encode() + pages.append(page) + if page["eof"]: + break + arguments = shlex.split(page["next_command"]) + required = json.loads(contents) + assert required["stop"] == payload["stop"] + assert required["opaque_counter"] == 9007199254740993 + for lane, expected in payload["agent_todo_summary"].items(): + if lane == "schema_version": + continue + values = [] + for item in required["agent_todo_summary"][lane]: + if "context_ref" in item: + item_source = required + for part in item["context_ref"].split("/")[1:]: + item_source = item_source[int(part)] if isinstance(item_source, list) else item_source[part] + values.append(item_source) + else: + values.append(item) + assert values == expected + return pages + + assert hook("SessionStart", source="startup") == {} + # Feature-off preserves even unknown semantics, without acknowledging them. + off_pages = read_required(project(original, "off"), original, budget=None) + assert json.loads("".join(page["content"] for page in off_pages)) == original + assert "candidate" not in state() + first = project(original) + assert first["mode"] == "full" + pages = read_required(first, original) + assert len(pages) > 1 + acknowledge(pages[:1]) + assert "candidate" not in state() + # Lost ACK: publishing another view cannot make an incomplete baseline valid. + unacknowledged = project(original) + assert unacknowledged["mode"] == "full" + acknowledge(read_required(unacknowledged, original)) + assert "candidate" in state() + warm = project(original) + assert warm["mode"] in {"delta", "full"} # Complete framing may make full cheaper. + acknowledge(read_required(warm, original)) + + revised = copy.deepcopy(original) + revised["stop"] = "Stop all publication and await revised approval." + for lane in ("first_open_items", "backlog_items", "claimed_open_items"): + revised["agent_todo_summary"][lane][0].update(content_revision="revision:two", + note="Contrary evidence changed the work. Preserve the revised constraint.") + changed = project(revised) + acknowledge(read_required(changed, revised)) + before = state() + assert hook("PostCompact") == {} + assert "candidate" not in state() and state()["recovery"] == before["recovery"] + compacted = project(revised) + assert compacted["mode"] == "full" + acknowledge(read_required(compacted, revised)) + + # The advertised reader rejects changed source bytes rather than trusting a digest label. + arguments = shlex.split(compacted["recovery"]["command"]) + capture = state()["recovery"]["path"] + pinned = Path(capture) + pinned.write_bytes(pinned.read_bytes() + b" ") + refused = command(arguments[arguments.index("quota"):], expected=1) + assert refused["ok"] is False and "content" not in refused + assert not registry.exists() and not runtime.exists() diff --git a/tests/test_cli_entrypoint.py b/tests/test_cli_entrypoint.py index bdaef754c0..8f6cd1092e 100644 --- a/tests/test_cli_entrypoint.py +++ b/tests/test_cli_entrypoint.py @@ -15,6 +15,29 @@ REPO_ROOT = Path(__file__).resolve().parents[1] +@pytest.mark.parametrize("preview", [False, True]) +def test_goal_start_does_not_load_unselected_issue_fix_execution(tmp_path, preview): + arguments = ["start-goal", "--help"] + if preview: + arguments = ["--runtime-root", str(tmp_path / "runtime"), "--format", "json", + "start-goal", "--guided", "--project", str(tmp_path / "project"), + "--goal-id", "fixture", "--host-surface", "shell", + "--goal-text", "Produce a verified report; preserve 总计."] + result = run_cli_main("loopx.entrypoint", arguments, + forbidden_modules=("loopx.cli", "loopx.capabilities.issue_fix.workflow_plan", + "loopx.capabilities.issue_fix.candidate_preflight", + "loopx.capabilities.issue_fix.acceptance_loop", + "loopx.capabilities.issue_fix.metadata_preview", + "loopx.capabilities.issue_fix.repository_context"), + environment={"LOOPX_USAGE_PING": "0"}) + assert result.returncode == 0, result.stderr + if preview: + packet = json.loads(result.stdout) + assert packet["goal_text"] == "Produce a verified report; preserve 总计." + assert packet["guided_transaction"]["writes_now"] is False + assert not list(tmp_path.iterdir()) + + @pytest.mark.parametrize("module", ["loopx.entrypoint", "loopx.cli"]) @pytest.mark.parametrize("command", ["auto-research", "multi-agent"]) def test_retired_demo_commands_reject_execution_without_writing_state( diff --git a/tests/test_core_distribution.py b/tests/test_core_distribution.py index 05df34660d..65f047f428 100644 --- a/tests/test_core_distribution.py +++ b/tests/test_core_distribution.py @@ -179,6 +179,30 @@ def command(*arguments, expected=0): assert pack["goal_start_contract"]["explicit_invocation_confirms_project_local_state_writes"] is True +@pytest.mark.parametrize("route", ["help", "preview", "catalog"]) +def test_installed_core_start_metadata_defers_issue_fix_execution(core_python, tmp_path, route): + arguments = ["start-goal", "--help"] if route == "help" else [ + "--runtime-root", str(tmp_path / "runtime"), "--format", "json", + "start-goal", "--guided", "--project", str(tmp_path / "project"), + "--goal-id", GOAL, "--host-surface", "shell", "--goal-text", "Verify the complete report."] + script = '''import json,sys +if sys.argv[2]=='catalog': + from loopx.capabilities.issue_fix.catalog_entry import ISSUE_FIX_CATALOG_ENTRY + assert ISSUE_FIX_CATALOG_ENTRY['default_enabled'] is False +else: + from loopx.entrypoint import main + try:code=main(json.loads(sys.argv[1])) + except SystemExit as error:code=error.code + assert code==0 +for name in ('workflow_plan','candidate_preflight','acceptance_loop','metadata_preview','repository_context'): + assert 'loopx.capabilities.issue_fix.'+name not in sys.modules,name +''' + result = subprocess.run([core_python, "-I", "-c", script, json.dumps(arguments), route], + cwd=tmp_path, env=_environment(core_python), capture_output=True, text=True, timeout=45) + assert result.returncode == 0, (result.stdout, result.stderr) + assert not list(tmp_path.iterdir()) + + 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"]: