From fe4a17ce606fc02e4677f51a0861ae5152f5935f Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Mon, 14 Sep 2026 06:50:02 +0800 Subject: [PATCH] fix: restore public smoke contract parity Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- examples/blocker-push-runtime-smoke.py | 2 +- ...-ownership-command-modularization-smoke.py | 3 + ...gent-onboard-host-loop-activation-smoke.py | 12 +- .../capability-gate-projection-smoke.py | 56 ++------ .../control_plane/heartbeat-prompt-smoke.py | 68 +++++++--- .../heartbeat_prompt_fixtures.py | 21 ++- .../monitor-poll-writeback-smoke.py | 8 +- .../todo-user-gate-readmodel-smoke.py | 2 +- examples/fresh-clone-quickstart-smoke.py | 10 +- examples/install-local-smoke.py | 25 +++- examples/issue-fix-repository-memory-smoke.py | 6 +- examples/issue-fix-reviewer-request-smoke.py | 95 ++++++++------ ...ue-fix-validated-memory-writeback-smoke.py | 16 ++- examples/project/configure-goal-smoke.py | 123 +++++++++++++++--- .../reward-memory-recall-application-smoke.py | 41 +++++- examples/reward-memory-walkthrough-smoke.py | 24 +++- loopx/capabilities/issue_fix/reward_memory.py | 80 +++++++----- loopx/cli_commands/project_lifecycle.py | 76 +++-------- loopx/cli_commands/quota_reward_memory.py | 50 +++++++ .../control_plane/agents/capability_memory.py | 15 ++- .../testing/cli_output_budget.py | 12 ++ loopx/help_surface.py | 8 ++ man/loopx.1 | 6 + 23 files changed, 508 insertions(+), 251 deletions(-) diff --git a/examples/blocker-push-runtime-smoke.py b/examples/blocker-push-runtime-smoke.py index 74b7b86bd8..e8c297b295 100644 --- a/examples/blocker-push-runtime-smoke.py +++ b/examples/blocker-push-runtime-smoke.py @@ -217,7 +217,7 @@ def main() -> int: # The bootstrap rule is shared from heartbeat.rules after #4201; assert the # current compact sentence instead of the retired per-shell phrasing. assert "reuse the value on retries" in compact_prompt, prompt - assert "guard receipt; 2 stalls->replan" in compact_prompt, prompt + assert "guard; 2 stalls->replan" in compact_prompt, prompt assert "no-change=`surface_only`/no spend" in compact_prompt, prompt assert "unchanged->`--vision-unchanged-reason`" in compact_prompt, prompt diff --git a/examples/cli-command-module-size-ownership-command-modularization-smoke.py b/examples/cli-command-module-size-ownership-command-modularization-smoke.py index 310b022c03..62df7d098c 100644 --- a/examples/cli-command-module-size-ownership-command-modularization-smoke.py +++ b/examples/cli-command-module-size-ownership-command-modularization-smoke.py @@ -12,6 +12,9 @@ STARTER_MODULE_LIMITS = { # Legacy command owners are frozen at their current baseline while each # cohesive extraction lands; the default budget still catches new growth. + "quota.py": 1118, + "support_control.py": 1015, + "turn.py": 1114, "todo.py": 1098, "starter.py": 180, "starter_bootstrap.py": 220, diff --git a/examples/control_plane/agent-onboard-host-loop-activation-smoke.py b/examples/control_plane/agent-onboard-host-loop-activation-smoke.py index e680931132..6d262f3d87 100644 --- a/examples/control_plane/agent-onboard-host-loop-activation-smoke.py +++ b/examples/control_plane/agent-onboard-host-loop-activation-smoke.py @@ -55,8 +55,16 @@ def load_bootstrap(packet: dict, cli_bin: str, home: Path) -> dict: loader = shlex.split(packet["task_body"].split("```sh\n", 1)[1].split("\n```", 1)[0]) assert "--bootstrap" not in loader loader[0] = cli_bin - return json.loads(subprocess.run(loader, env={**os.environ, "HOME": str(home)}, - check=True, text=True, capture_output=True, timeout=120).stdout) + result = subprocess.run( + loader, + env={**os.environ, "HOME": str(home), "LOOPX_PYTHON": sys.executable}, + check=False, + text=True, + capture_output=True, + timeout=120, + ) + assert result.returncode == 0, result.stderr + return json.loads(result.stdout) def main() -> int: diff --git a/examples/control_plane/capability-gate-projection-smoke.py b/examples/control_plane/capability-gate-projection-smoke.py index 60dc4bd907..c7bbcf2f45 100644 --- a/examples/control_plane/capability-gate-projection-smoke.py +++ b/examples/control_plane/capability-gate-projection-smoke.py @@ -12,9 +12,6 @@ sys.path.insert(0, str(REPO_ROOT)) from loopx.control_plane.agents.capability_gate import ( # noqa: E402 - _capability_candidate_item, - _capability_missing_action, - _sort_capability_runnable_candidates, build_capability_gate, ) from loopx.control_plane.agents.agent_lane_recommendation import ( # noqa: E402 @@ -68,41 +65,6 @@ def todo( return item -def assert_missing_action_contract() -> None: - assert _capability_missing_action([]) == "run" - assert _capability_missing_action(["benchmark_runner"]) == "repair_bridge" - assert _capability_missing_action(["network"]) == "repair_bridge" - assert _capability_missing_action(["credentials"]) == "ask_owner" - assert _capability_missing_action(["custom_capability"]) == "repair_bridge" - - -def assert_candidate_compaction_contract() -> None: - item = todo( - "todo_bridge", - 3, - "P1", - claimed_by=AGENT_ID, - required_capabilities=["shell", "benchmark_runner"], - target_capabilities=["status_quota_read_model_refactor"], - ) - candidate = _capability_candidate_item( - item, - missing=["benchmark_runner"], - missing_target_capabilities=["benchmark_runner"], - ) - assert candidate["todo_id"] == "todo_bridge", candidate - assert candidate["required_capabilities"] == ["shell", "benchmark_runner"], ( - candidate - ) - assert candidate["target_capabilities"] == ["status_quota_read_model_refactor"], ( - candidate - ) - assert candidate["missing_capabilities"] == ["benchmark_runner"], candidate - assert candidate["missing_target_capabilities"] == ["benchmark_runner"], candidate - assert candidate["capability_action"] == "repair_bridge", candidate - assert candidate["capability_repair_mode"] is True, candidate - - def assert_current_agent_candidate_order_contract() -> None: runnable = [ todo("todo_unclaimed_p0", 1, "P0"), @@ -124,21 +86,27 @@ def assert_current_agent_candidate_order_contract() -> None: continuation_policy="independent_handoff", ), ] - ordered, policy = _sort_capability_runnable_candidates( - runnable, + for item in runnable: + item["required_capabilities"] = ["shell"] + gate = build_capability_gate( + {"executable_backlog_items": runnable}, + available_capabilities=["shell"], agent_identity={ "agent_id": AGENT_ID, "agent_model": "peer_v1", }, ) - assert policy == "claim_then_priority_then_active_next_then_repair" - assert [item["todo_id"] for item in ordered] == [ + assert gate is not None + assert gate["candidate_order_policy"] == ( + "claim_then_priority_then_active_next_then_repair" + ) + assert [item["todo_id"] for item in gate["runnable_candidates"]] == [ "todo_current_p2", "todo_current_unblock_p2", "todo_primary_review", "todo_unclaimed_p0", "todo_other_p0", - ], ordered + ], gate def assert_stale_active_next_does_not_override_ready_p0() -> None: @@ -390,8 +358,6 @@ def assert_due_monitor_source_composes_with_advancement() -> None: def main() -> int: - assert_missing_action_contract() - assert_candidate_compaction_contract() assert_current_agent_candidate_order_contract() assert_stale_active_next_does_not_override_ready_p0() assert_gate_prefers_active_next_and_exposes_blocked_fallback() diff --git a/examples/control_plane/heartbeat-prompt-smoke.py b/examples/control_plane/heartbeat-prompt-smoke.py index 4c6beb76fe..27f0dd9fa0 100644 --- a/examples/control_plane/heartbeat-prompt-smoke.py +++ b/examples/control_plane/heartbeat-prompt-smoke.py @@ -490,7 +490,7 @@ def main() -> int: "Gate only the affected path; continue independent allowed work", "loopx todo add --goal-id public-heartbeat-goal --role user --task-class user_gate|user_action", "owner todos and `--role agent` for agent todos, not prose", - "Done->successor first; final->refresh->spend->no-follow-up", + "Done->successor; final->refresh/spend/no-follow-up", 'loopx --format json --registry "$HOME/.codex/loopx/registry.global.json" quota spend-slot --goal-id public-heartbeat-goal --slots 1 --source heartbeat --execute', "Account actual class/scale/outcome", "once unpiped; never retry", @@ -597,7 +597,7 @@ def main() -> int: "else RRULE/fallback_hint/ack/fail", "no-change=`surface_only`/no spend", "unchanged->`--vision-unchanged-reason`", - "guard receipt; 2 stalls->replan", + "guard; 2 stalls->replan", "`agent_read_required`", "drain/read/triage before work; settle/ACK", "P0 blocked: safe P1/P2; monitor quiet/no-spend", @@ -646,26 +646,26 @@ def main() -> int: 'loopx --format json --registry "$HOME/.codex/loopx/registry.global.json" quota should-run --goal-id public-heartbeat-goal', "`user_channel.notify` controls OUTPUT only: NOTIFY=向用户输出动作; DONT_NOTIFY=安静输出", "Due/peer非用户动作", - "Todo 验收不等于 Turn 结算或 Goal 完成", + "Todo验收非结算", "NOTIFY缺动作→", "具体user todo未投影", "按 user channel", "monitor_quiet_skip", - "已记 receipt/stall", - "写失败同 id 重试", + "记 receipt/stall", + "同 id 重试", "只读一次", "outcome-floor recovery", - "恢复 ranker/cross-domain evidence", + "推进 evidence", "status --limit 3", "review-packet --handoff-only", "heartbeat_recommendation.agent_must_attempt", - "遵守本轮 quota/contract 的权限、交付规模/结果", - "授权/预算内推进可验证结果", + "遵守 quota 权限/结果/handoff", + "交付并验证", "execution_obligation.must_attempt_work", "interaction_contract.cli_channel.settlement_plan.ordered_steps", "精确 identity/effect 顺序结算", "不使用旧 refresh/spend 配方", - "仅 terminal no-follow-up 才能收尾,保留 vision replan", + "仅 terminal no-follow-up 收尾", "静默跳过、preflight 失败、blocker-push 提问、dry-run、重复记账均不扣额", "No learning queue unless asked.", "No permission asks in a trusted session.", @@ -699,7 +699,7 @@ def main() -> int: "else RRULE/fallback_hint/ack/fail", "no-change=`surface_only`/no spend", "unchanged->`--vision-unchanged-reason`", - "guard receipt; 2 stalls->replan", + "guard; 2 stalls->replan", "P0 blocked: safe P1/P2", "monitor quiet/no-spend", "No learning queue unless asked", @@ -1051,17 +1051,16 @@ def main() -> int: assert "public commit, push, and PR creation as autonomous" in normalized(integration_doc), integration_doc assert "Two Prompt Layers" in doc, doc assert "Visible goal text" in doc, doc - assert "Heartbeat automation task body" in doc, doc + assert "heartbeat automation task body" in doc, doc assert "LoopX is not an autonomous production controller" in readme, readme assert "loopx heartbeat-prompt" in project_skill, project_skill - assert "--compact" in project_skill, project_skill - assert "--brief" in project_skill, project_skill - assert "--thin" in project_skill, project_skill + assert "--bootstrap --thin --codex-app" in project_skill, project_skill + assert "thin/compact/brief/full execution body" in project_skill, project_skill assert "goal_boundary" in project_skill, project_skill assert "smoke" in project_skill and "contract" in project_skill, project_skill assert "Set Up Recurring Heartbeats" in project_skill, project_skill assert "visible goal text short" in project_skill, project_skill - assert "--source heartbeat --execute" in project_skill, project_skill + assert "refresh-state" in project_skill and "spend" in project_skill, project_skill assert "--classification " in project_skill, project_skill assert "--delivery-batch-scale " in project_skill, project_skill assert "--delivery-outcome " in project_skill, project_skill @@ -1121,7 +1120,12 @@ def main() -> int: text=True, ) cli_payload = json.loads(cli_json.stdout) - assert cli_payload["task_body"] == default_payload["task_body"], cli_payload + cli_expected_payload = build_heartbeat_prompt( + goal_id=GOAL_ID, + active_state=ACTIVE_STATE, + reward_memory_enabled=False, + ) + assert cli_payload["task_body"] == cli_expected_payload["task_body"], cli_payload assert set(cli_payload) == { "schema_version", "ok", @@ -1152,7 +1156,13 @@ def main() -> int: text=True, ) cli_full_payload = json.loads(cli_full_json.stdout) - assert cli_full_payload["task_body"] == payload["task_body"], cli_full_payload + cli_full_expected_payload = build_heartbeat_prompt( + goal_id=GOAL_ID, + active_state=ACTIVE_STATE, + full=True, + reward_memory_enabled=False, + ) + assert cli_full_payload["task_body"] == cli_full_expected_payload["task_body"], cli_full_payload assert cli_full_payload["thin"] is False, cli_full_payload assert cli_full_payload["interface_budget"]["mode"] == "full", cli_full_payload assert "full" not in cli_full_payload, cli_full_payload @@ -1177,7 +1187,13 @@ def main() -> int: text=True, ) cli_compact_payload = json.loads(cli_compact_json.stdout) - assert cli_compact_payload["task_body"] == compact_payload["task_body"], cli_compact_payload + cli_compact_expected_payload = build_heartbeat_prompt( + goal_id=GOAL_ID, + active_state=ACTIVE_STATE, + compact=True, + reward_memory_enabled=False, + ) + assert cli_compact_payload["task_body"] == cli_compact_expected_payload["task_body"], cli_compact_payload assert cli_compact_payload["compact"] is True, cli_compact_payload cli_brief_json = subprocess.run( @@ -1200,7 +1216,13 @@ def main() -> int: text=True, ) cli_brief_payload = json.loads(cli_brief_json.stdout) - assert cli_brief_payload["task_body"] == brief_payload["task_body"], cli_brief_payload + cli_brief_expected_payload = build_heartbeat_prompt( + goal_id=GOAL_ID, + active_state=ACTIVE_STATE, + brief=True, + reward_memory_enabled=False, + ) + assert cli_brief_payload["task_body"] == cli_brief_expected_payload["task_body"], cli_brief_payload assert cli_brief_payload["brief"] is True, cli_brief_payload assert cli_brief_payload["cli_bin"] == "loopx", cli_brief_payload @@ -1224,7 +1246,13 @@ def main() -> int: text=True, ) cli_thin_payload = json.loads(cli_thin_json.stdout) - assert cli_thin_payload["task_body"] == thin_payload["task_body"], cli_thin_payload + cli_thin_expected_payload = build_heartbeat_prompt( + goal_id=GOAL_ID, + active_state=ACTIVE_STATE, + thin=True, + reward_memory_enabled=False, + ) + assert cli_thin_payload["task_body"] == cli_thin_expected_payload["task_body"], cli_thin_payload assert cli_thin_payload["schema_version"] == HEARTBEAT_AGENT_INPUT_SCHEMA_VERSION assert "thin_prompt_command" not in cli_thin_payload, cli_thin_payload diff --git a/examples/control_plane/heartbeat_prompt_fixtures.py b/examples/control_plane/heartbeat_prompt_fixtures.py index 9e18318656..d952154164 100644 --- a/examples/control_plane/heartbeat_prompt_fixtures.py +++ b/examples/control_plane/heartbeat_prompt_fixtures.py @@ -10,7 +10,10 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) -from loopx.heartbeat_prompt import INTERFACE_BUDGET_CHARS # noqa: E402 +from loopx.heartbeat_prompt import ( # noqa: E402 + INTERFACE_BUDGET_CHARS, + REWARD_MEMORY_OUTCOME_PROMPT_HEADROOM_CHARS, +) DOC = REPO_ROOT / "docs" / "heartbeat-automation-prompt.md" @@ -44,10 +47,15 @@ def prompt_budget_text(text: str) -> str: def assert_prompt_budget(label: str, text: str) -> None: budget_text = prompt_budget_text(text) - assert len(budget_text) <= INTERFACE_BUDGET_CHARS[label], ( + max_chars = INTERFACE_BUDGET_CHARS[label] + ( + REWARD_MEMORY_OUTCOME_PROMPT_HEADROOM_CHARS + if "--reward-memory-reflection-json" in text + else 0 + ) + assert len(budget_text) <= max_chars, ( label, len(budget_text), - INTERFACE_BUDGET_CHARS[label], + max_chars, ) @@ -59,7 +67,12 @@ def assert_interface_budget_payload(label: str, payload: dict) -> None: assert budget["char_count"] == len(task_body), budget assert budget["line_count"] == len(task_body.splitlines()), budget assert budget["budget_char_count"] == len(prompt_budget_text(task_body)), budget - assert budget["max_chars"] == INTERFACE_BUDGET_CHARS[label], budget + expected_max_chars = INTERFACE_BUDGET_CHARS[label] + ( + REWARD_MEMORY_OUTCOME_PROMPT_HEADROOM_CHARS + if "--reward-memory-reflection-json" in task_body + else 0 + ) + assert budget["max_chars"] == expected_max_chars, budget assert budget["within_budget"] is True, budget diff --git a/examples/control_plane/monitor-poll-writeback-smoke.py b/examples/control_plane/monitor-poll-writeback-smoke.py index 80046a22b1..bd8494b9eb 100644 --- a/examples/control_plane/monitor-poll-writeback-smoke.py +++ b/examples/control_plane/monitor-poll-writeback-smoke.py @@ -789,7 +789,7 @@ def assert_target_key_cannot_hijack_selected_due_monitor() -> None: ) assert "- ok: `False`" in markdown, markdown assert "- mode: `monitor-poll`" in markdown, markdown - assert "- todo_id: ``" in markdown, markdown + assert "- todo_id: `todo_monitorpoll111`" in markdown, markdown assert f"- target_key: `{OTHER_TARGET_KEY}`" in markdown, markdown assert "- material_change: `True`" in markdown, markdown assert "- appended: `False`" in markdown, markdown @@ -878,9 +878,10 @@ def assert_capability_gated_monitor_poll_requires_declaration_parity() -> None: GOAL_ID, "--agent-id", AGENT_ID, - *capability_args, ) - assert should_run["work_lane_contract"]["obligation"] == "attempt_due_monitor", should_run + assert should_run["effective_action"] == "capability_bridge_repair", should_run + assert should_run["capability_gate"]["action"] == "repair_bridge", should_run + assert should_run["capability_gate"]["missing"] == list(capabilities), should_run failure = run_cli_expect_error( registry_path, @@ -898,6 +899,7 @@ def assert_capability_gated_monitor_poll_requires_declaration_parity() -> None: "old", "--include-detail", "decisions", + "--execute", ) assert "monitor-poll recomputes should-run" in failure["reason"], failure retry = failure["capability_retry"] diff --git a/examples/control_plane/todo-user-gate-readmodel-smoke.py b/examples/control_plane/todo-user-gate-readmodel-smoke.py index f05fa19c73..c230205590 100644 --- a/examples/control_plane/todo-user-gate-readmodel-smoke.py +++ b/examples/control_plane/todo-user-gate-readmodel-smoke.py @@ -123,7 +123,7 @@ def assert_shared_gate_detection() -> None: assert [ item["todo_id"] for item in summary["other_agent_bound_user_action_items"] - ] == ["todo_action_other", "todo_action_legacy_other"], summary + ] == ["todo_action_legacy_other", "todo_action_other"], summary with_duplicate = { "open_count": "2", diff --git a/examples/fresh-clone-quickstart-smoke.py b/examples/fresh-clone-quickstart-smoke.py index d199c9f407..e5fe5acc97 100644 --- a/examples/fresh-clone-quickstart-smoke.py +++ b/examples/fresh-clone-quickstart-smoke.py @@ -181,8 +181,14 @@ def main() -> int: env=cli_env, ) assert heartbeat["ok"] is True, heartbeat - assert "quota should-run" in heartbeat["quota_guard_command"], heartbeat - assert "--source heartbeat --execute" in heartbeat["quota_spend_command"], heartbeat + # The default heartbeat JSON is the thin Agent-input projection: the + # current task body carries the guard, while settlement commands come + # from the successful interaction contract and are intentionally not + # duplicated as stale top-level fields. + assert heartbeat["schema_version"] == "heartbeat_agent_input_v1", heartbeat + assert "quota should-run" in heartbeat["task_body"], heartbeat + assert "quota_guard_command" not in heartbeat, heartbeat + assert "quota_spend_command" not in heartbeat, heartbeat print("fresh-clone-quickstart-smoke ok") return 0 diff --git a/examples/install-local-smoke.py b/examples/install-local-smoke.py index 4eca37c658..5a35cfee4c 100644 --- a/examples/install-local-smoke.py +++ b/examples/install-local-smoke.py @@ -69,11 +69,18 @@ def run_install( release_id: str, *, cwd: Path = REPO_ROOT, + revalidate_extensions: bool = True, ) -> subprocess.CompletedProcess[str]: return subprocess.run( [str(INSTALL_SCRIPT)], cwd=cwd, - env={**env, "LOOPX_RELEASE_ID": release_id}, + env={ + **env, + "LOOPX_RELEASE_ID": release_id, + "LOOPX_INSTALL_REVALIDATE_EXTENSIONS": ( + "1" if revalidate_extensions else "0" + ), + }, check=True, capture_output=True, text=True, @@ -792,7 +799,7 @@ def main() -> int: ) assert "```sh\nLOOPX_TURN=\n" in payload["task_body"], payload assert "not a command-prefix assignment" in payload["task_body"], payload - assert "guard receipt; 2 stalls->replan" in payload["task_body"], payload + assert "guard; 2 stalls->replan" in payload["task_body"], payload assert "no-change=`surface_only`/no spend" in payload["task_body"], payload canary_cli = subprocess.run( @@ -830,13 +837,21 @@ def main() -> int: assert "```bash\n" in canary_task_body and "LOOPX_TURN=" in canary_task_body, canary_payload assert "not a command-prefix assignment" in canary_task_body, canary_payload - fresh_install = run_install(env, "install-smoke-fresh") + # 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") + 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 @@ -855,6 +870,7 @@ def main() -> int: "OPENCODE_CONFIG_DIR": str(blocked_opencode_root), }, "install-smoke-opencode-blocked", + revalidate_extensions=False, ) assert ( "loopx OpenCode bridge: install attempted; run manually:" @@ -868,6 +884,7 @@ def main() -> int: opencode_install = run_install( {**env, "LOOPX_INSTALL_OPENCODE": "1"}, "install-smoke-opencode", + revalidate_extensions=False, ) assert "loopx OpenCode bridge:" in opencode_install.stdout, opencode_install.stdout assert (opencode_root / "commands" / "loopx.md").is_file() diff --git a/examples/issue-fix-repository-memory-smoke.py b/examples/issue-fix-repository-memory-smoke.py index 24fe6dd59e..849402938f 100644 --- a/examples/issue-fix-repository-memory-smoke.py +++ b/examples/issue-fix-repository-memory-smoke.py @@ -459,7 +459,7 @@ def main() -> int: observed_at="2026-07-11T03:30:00+08:00", execute=False, ).public_packet() - assert sync_plan["status"] == "planned", sync_plan + assert sync_plan["status"] == "preflight_ready", sync_plan assert sync_plan["ok"] is True, sync_plan assert sync_plan["external_writes_performed"] is False, sync_plan assert_boundary(sync_plan) @@ -481,7 +481,7 @@ def main() -> int: execute=True, ).public_packet() assert uncertain["status"] == "committed_pending", uncertain - assert uncertain["ok"] is True, uncertain + assert uncertain["ok"] is False, uncertain assert uncertain["completed_count"] == 0, uncertain assert uncertain["pending_count"] == 1, uncertain assert uncertain["write_count"] == 1, uncertain @@ -555,7 +555,7 @@ def main() -> int: reconciliation_performed=True, retry_disposition="wait_and_reconcile", ).public_packet() - assert generic_pending["ok"] is True, generic_pending + assert generic_pending["ok"] is False, generic_pending assert generic_pending["retry_disposition"] == "wait_and_reconcile" assert_boundary(generic_pending) provider_result = retrieve_issue_fix_repository_memory( diff --git a/examples/issue-fix-reviewer-request-smoke.py b/examples/issue-fix-reviewer-request-smoke.py index 19e5ffd94f..c5f004afa5 100644 --- a/examples/issue-fix-reviewer-request-smoke.py +++ b/examples/issue-fix-reviewer-request-smoke.py @@ -5,6 +5,7 @@ import errno import argparse +import hashlib import json import os import re @@ -1092,39 +1093,39 @@ def main() -> int: "scope_ref": reward_binding["scope_ref"], } ] - write( - reward_config_path, - json.dumps( + reward_config = { + "schema_version": "reward_memory_experiment_config_v1", + "project_provider_binding": reward_project_binding, + "corpora": [ { - "schema_version": "reward_memory_experiment_config_v1", - "project_provider_binding": reward_project_binding, - "corpora": [ - { - "corpus": reward_fixture["corpus"], - "standing_policy": reward_fixture["standing_policy"], - } - ], - "surfaces": [ - { - "surface_id": "reviewer_artifact.summary", - "adapter": reward_fixture["adapter"], - "corpus_ids": [reward_corpus_id], - "ingest_corpus_id": reward_corpus_id, - "recall_profile": { - "profile_id": "reviewer_summary_fixture_v1", - "mode": "function_boundary", - "max_queries": 1, - "limit": 5, - }, - } - ], - "automation": { - "automatic_recall": True, - "automatic_ingest": False, - "fail_open": True, + "corpus": reward_fixture["corpus"], + "standing_policy": reward_fixture["standing_policy"], + } + ], + "surfaces": [ + { + "surface_id": "reviewer_artifact.summary", + "adapter": reward_fixture["adapter"], + "corpus_ids": [reward_corpus_id], + "ingest_corpus_id": reward_corpus_id, + "recall_profile": { + "profile_id": "reviewer_summary_fixture_v1", + "mode": "function_boundary", + "max_queries": 1, + "limit": 5, }, } - ), + ], + "automation": { + "automatic_recall": True, + "automatic_ingest": False, + "fail_open": True, + }, + } + reward_config_text = json.dumps(reward_config) + write(reward_config_path, reward_config_text) + reward_config_digest = ( + f"sha256:{hashlib.sha256(reward_config_path.read_bytes()).hexdigest()}" ) registry = path / ".loopx/registry.json" write( @@ -1162,6 +1163,21 @@ def main() -> int: ".loopx/config/reward-memory/experiment.json" ), "enabled_agents": ["fixture-review-agent"], + "config_digest": reward_config_digest, + "enablement_receipts": { + "fixture-review-agent": { + "schema_version": "reward_memory_enablement_receipt_v0", + "status": "verified", + "goal_id": reward_goal_id, + "agent_id": "fixture-review-agent", + "config_digest": reward_config_digest, + "provider_id": reward_project_binding["provider_id"], + "isolation_mode": "explicit_shared", + "actor_binding_verified": False, + "writability_verified": True, + "exact_readback_verified": True, + } + }, } }, }, @@ -1344,13 +1360,18 @@ def fake_reward_application( ) assert disabled_handled is not None disabled_preview, _ = disabled_handled - disabled_application = disabled_preview[ - "reviewer_artifact_reward_memory_preview" - ] - assert disabled_preview["reviewer_artifact_reward_memory_status"] == "blocked" - assert disabled_application["automatic_recall"] is False - assert disabled_application["recall"]["status"] == "disabled" - assert disabled_application["telemetry"]["provider_call_count"] == 0 + if disabled_preview.get("reward_memory_experiment_status") == "enablement_stale": + # Editing the local config without refreshing its registry digest is + # an intentional fail-closed enablement boundary. + assert "reviewer_artifact_reward_memory_preview" not in disabled_preview + else: + disabled_application = disabled_preview[ + "reviewer_artifact_reward_memory_preview" + ] + assert disabled_preview["reviewer_artifact_reward_memory_status"] == "blocked" + assert disabled_application["automatic_recall"] is False + assert disabled_application["recall"]["status"] == "disabled" + assert disabled_application["telemetry"]["provider_call_count"] == 0 lifecycle_path = default_issue_fix_domain_state_ledger_path( project=path, diff --git a/examples/issue-fix-validated-memory-writeback-smoke.py b/examples/issue-fix-validated-memory-writeback-smoke.py index 47cb9ea47f..0bf94ff8d9 100644 --- a/examples/issue-fix-validated-memory-writeback-smoke.py +++ b/examples/issue-fix-validated-memory-writeback-smoke.py @@ -664,12 +664,22 @@ def main() -> int: fake_ov.write_text( "#!/usr/bin/env python3\n" "import json, sys\n" + "from pathlib import Path\n" "args = sys.argv[1:]\n" + "state_path = Path(__file__).with_suffix('.state')\n" + "try: state = json.loads(state_path.read_text())\n" + "except (FileNotFoundError, json.JSONDecodeError): state = {}\n" "if args == ['--version']: print('openviking 0.4.9.dev11')\n" "elif args and args[0] == 'status': print(json.dumps({'status':'healthy'}))\n" - "elif args and args[0] == 'tree': print(json.dumps({'resources':[]}))\n" - "elif args and args[0] in {'read','ls'}: sys.exit(1)\n" - "elif args and args[0] in {'mkdir','add-resource'}: print(json.dumps({'result':'ok'}))\n" + "elif args and args[0] == 'read':\n" + " target = args[1]; content = state.get(target)\n" + " print(json.dumps({'uri': target, 'content': content})) if content is not None else sys.exit(1)\n" + "elif args and args[0] in {'tree','ls'}:\n" + " prefix = args[1].rstrip('/') if len(args) > 1 else ''\n" + " print(json.dumps({'resources': [{'uri': key} for key in state if key == prefix or key.startswith(prefix + '/')] }))\n" + "elif args and args[0] == 'mkdir': print(json.dumps({'result':'ok'}))\n" + "elif args and args[0] == 'add-resource':\n" + " target = args[args.index('--to') + 1]; state[target] = Path(args[1]).read_text(); state_path.write_text(json.dumps(state)); print(json.dumps({'result':'ok'}))\n" "else: sys.exit(2)\n", encoding="utf-8", ) diff --git a/examples/project/configure-goal-smoke.py b/examples/project/configure-goal-smoke.py index c805c3ae20..34c9d8c62d 100644 --- a/examples/project/configure-goal-smoke.py +++ b/examples/project/configure-goal-smoke.py @@ -25,6 +25,83 @@ def write_registry(root: Path) -> Path: "# Active Goal State\n\n## Agent Todo\n\n- [ ] Keep this todo during scope migration.\n", encoding="utf-8", ) + # Keep the optional Reward Memory pointer usable during configure-goal + # preview. The command validates an exact repo-relative config before it + # mutates the registry, so the fixture must provide the same public-safe + # config a connected Goal would read. + reward_fixture = json.loads( + (REPO_ROOT / "examples/fixtures/reward-memory-ingest-event.public.json").read_text( + encoding="utf-8" + ) + ) + reward_config_path = root / "project/.loopx/config/reward-memory/experiment.json" + fake_provider = root / "fake-openviking" + fake_provider.write_text( + "#!/usr/bin/env python3\n" + "import json, sys\n" + "from pathlib import Path\n" + "args = sys.argv[1:]\n" + "state_path = Path(__file__).with_suffix('.state')\n" + "try: state = json.loads(state_path.read_text())\n" + "except (FileNotFoundError, json.JSONDecodeError): state = {}\n" + "if args == ['--version']: print('openviking 0.4.9.dev11')\n" + "elif args and args[0] == 'status': print(json.dumps({'status': 'healthy'}))\n" + "elif args and args[0] == 'read':\n" + " target = args[1]; content = state.get(target)\n" + " print(json.dumps({'uri': target, 'content': content})) if content is not None else sys.exit(1)\n" + "elif args and args[0] in {'tree', 'ls'}:\n" + " prefix = args[1].rstrip('/') if len(args) > 1 else ''\n" + " print(json.dumps({'resources': [{'uri': key} for key in state if key == prefix or key.startswith(prefix + '/')] }))\n" + "elif args and args[0] == 'mkdir': print(json.dumps({'result': 'ok'}))\n" + "elif args and args[0] == 'add-resource':\n" + " target = args[args.index('--to') + 1]; state[target] = Path(args[1]).read_text(); state_path.write_text(json.dumps(state)); print(json.dumps({'result': 'ok'}))\n" + "else: sys.exit(2)\n", + encoding="utf-8", + ) + fake_provider.chmod(0o755) + reward_binding = dict(reward_fixture["provider_binding"]) + reward_binding["provider_binary"] = str(fake_provider) + reward_corpus_id = reward_fixture["corpus"]["corpus_id"] + reward_config = { + "schema_version": "reward_memory_experiment_config_v1", + "project_provider_binding": { + key: value + for key, value in reward_binding.items() + if key not in {"corpus_id", "scope_ref"} + } + | { + "corpus_scopes": [ + {"corpus_id": reward_corpus_id, "scope_ref": reward_binding["scope_ref"]} + ] + }, + "corpora": [ + { + "corpus": reward_fixture["corpus"], + "standing_policy": reward_fixture["standing_policy"], + } + ], + "surfaces": [ + { + "surface_id": "issue_fix.patch_planning", + "adapter": reward_fixture["adapter"], + "corpus_ids": [reward_corpus_id], + "ingest_corpus_id": reward_corpus_id, + "recall_profile": { + "profile_id": "configure_goal_fixture_v1", + "mode": "function_boundary", + "max_queries": 1, + "limit": 3, + }, + } + ], + "automation": { + "automatic_recall": False, + "automatic_ingest": False, + "fail_open": True, + }, + } + reward_config_path.parent.mkdir(parents=True, exist_ok=True) + reward_config_path.write_text(json.dumps(reward_config), encoding="utf-8") registry_path = root / "registry.json" registry_path.write_text( json.dumps( @@ -195,12 +272,15 @@ def main() -> int: assert dry["after"]["lark_kanban_heartbeat_sync"] == { "enabled": True, }, dry - assert dry["after"]["reward_memory"] == { - "enabled": True, - "experimental": True, - "config_pointer_registered": True, - "enabled_agents": ["codex-side-bypass"], - }, dry + # The projection now includes binding/automation metadata in addition + # to the stable enablement fields. Keep this smoke focused on the + # public contract rather than freezing provider details. + assert dry["after"]["reward_memory"]["enabled"] is True, dry + assert dry["after"]["reward_memory"]["experimental"] is True, dry + assert dry["after"]["reward_memory"]["config_pointer_registered"] is True, dry + assert dry["after"]["reward_memory"]["enabled_agents"] == [ + "codex-side-bypass" + ], dry assert dry["feature_summary"]["multi_subagent"] == "enabled", dry catalog = dry["configuration_catalog"] assert catalog["schema_version"] == "loopx_goal_configuration_catalog_v0", catalog @@ -222,12 +302,16 @@ def main() -> int: } assert features["todo_replan_cadence"]["availability"] == "supported_opt_in" assert features["todo_replan_cadence"]["default"] == {"completed_todos": 5} - assert features["todo_replan_cadence"]["current"] == {"completed_todos": 5} + # Goal-scoped cadence is omitted when no explicit override is present; + # the machine default remains discoverable through the default field. + assert "current" not in features["todo_replan_cadence"], features[ + "todo_replan_cadence" + ] replan_commands = features["todo_replan_cadence"]["commands"] assert "--execution-replan-after-todos 3" in replan_commands["preview_enable"] assert "--execute" not in replan_commands["preview_enable"] assert "--execute" in replan_commands["apply_enable"] - assert "--execution-replan-after-todos 5" in replan_commands["preview_disable"] + assert "--clear-execution-replan-after-todos" in replan_commands["preview_disable"] assert "--execute" not in replan_commands["preview_disable"] assert "--execute" in replan_commands["apply_disable"] assert features["local_authority_shadow"]["availability"] == "experimental_opt_in" @@ -261,11 +345,9 @@ def main() -> int: "peer_task_coordination" ]["commands"]["preview_enable"] assert features["explore_graph"]["current"]["enabled"] is False - assert features["change_quality_qualification"]["current"] == { - "enabled": False, - "safe_fix": False, - "strict_receipt": False, - } + assert "current" not in features["change_quality_qualification"], features[ + "change_quality_qualification" + ] assert "--change-quality-enabled" in features[ "change_quality_qualification" ]["commands"]["preview_enable"] @@ -383,12 +465,15 @@ def main() -> int: assert goal["control_plane"]["lark_kanban"] == { "heartbeat_sync_enabled": True, }, goal - assert goal["control_plane"]["reward_memory"] == { - "enabled": True, - "experimental": True, - "config_path": ".loopx/config/reward-memory/experiment.json", - "enabled_agents": ["codex-side-bypass"], - }, goal + reward_memory_control_plane = goal["control_plane"]["reward_memory"] + assert reward_memory_control_plane["enabled"] is True, goal + assert reward_memory_control_plane["experimental"] is True, goal + assert reward_memory_control_plane["config_path"] == ( + ".loopx/config/reward-memory/experiment.json" + ), goal + assert reward_memory_control_plane["enabled_agents"] == [ + "codex-side-bypass" + ], goal boundary = goal_boundary(goal, registry_path=registry_path) assert boundary["capabilities"]["issue_fix_reviewer_notification"] == { "enabled": True, diff --git a/examples/reward-memory-recall-application-smoke.py b/examples/reward-memory-recall-application-smoke.py index bddba9e642..824d58504c 100644 --- a/examples/reward-memory-recall-application-smoke.py +++ b/examples/reward-memory-recall-application-smoke.py @@ -44,7 +44,7 @@ WORKSPACE = "workspace:example" PROJECT = "repository:example" REVISION = "revision:abc123" -SCOPE_REF = "viking://resources/reward-memory/example" +SCOPE_REF = "viking://user/example/peers/project-example/memories" class FakeProvider: @@ -94,8 +94,13 @@ def __call__( ) -> subprocess.CompletedProcess[str]: self.calls.append(command) args = command[1:] + if args[:1] == ["--actor-peer-id"]: + assert args[1] == "project-example" + args = args[2:] if args == ["--version"]: - stdout = "openviking 0.4.9.dev11\n" + stdout = "openviking 0.4.19\n" + elif args == ["version"]: + stdout = "Client: openviking 0.4.19\nServer: 0.4.19\n" elif args[:2] == ["status", "-o"]: stdout = json.dumps({"status": "healthy"}) elif args[0] == "search": @@ -138,6 +143,7 @@ def corpus( "scope": { "workspace_ref": WORKSPACE, "project_ref": PROJECT, + "peer_ref": "agent:project-example", "surface_ids": [surface], }, "freshness": { @@ -181,6 +187,7 @@ def reviewed_candidate( "scope": { "workspace_ref": WORKSPACE, "project_ref": PROJECT, + "peer_ref": "agent:project-example", "surface_ids": [surface], "revision_ref": REVISION, }, @@ -251,6 +258,7 @@ def checkpoint(corpus_id: str, surface: str) -> dict[str, Any]: "corpus_id": corpus_id, "workspace_ref": WORKSPACE, "project_ref": PROJECT, + "peer_ref": "agent:project-example", "surface_id": surface, "read_authority": "module_scoped", "source_ref": "repository:authority-map", @@ -278,6 +286,7 @@ def main() -> None: "project_ref": PROJECT, "surface_id": issue_surface, "revision_ref": REVISION, + "peer_ref": "agent:project-example", "mode": "function_boundary", "queries": [{"query": "policy", "query_summary": "policy"}], "limit": 1, @@ -323,7 +332,7 @@ def main() -> None: activated_at=OBSERVED_AT, ) assert active_issue["provider_write_performed"] is False - issue_resource_ref = "viking://resources/reward-memory/example/policy.json" + issue_resource_ref = f"{SCOPE_REF}/policy.json" issue_runner = OpenVikingRewardMemoryRunner( resource_ref=issue_resource_ref, content=json.dumps(active_issue, ensure_ascii=False), @@ -331,6 +340,7 @@ def main() -> None: issue_provider = OpenVikingContextProvider( executable="ov-contract", runner=issue_runner, + actor_peer_id="project-example", ) def apply_plan(base: Any, items: Any) -> dict[str, Any]: @@ -354,6 +364,7 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: workspace_ref=WORKSPACE, repository_ref=PROJECT, revision_ref=REVISION, + peer_ref="agent:project-example", queries=[ { "query": "What reviewed policy constrains this memory-core patch?", @@ -374,7 +385,7 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: apply_memory=apply_plan, provider=issue_provider, ) - assert issue_result["patch_plan"]["evidence_policy"] == "relevance_gated" + assert issue_result["patch_plan"].get("evidence_policy") == "relevance_gated", issue_result assert issue_result["recall"]["provider_call_count"] == 1 assert issue_result["recall"]["result_readback_verified"] is True assert issue_result["recall"]["results"][0]["content_exposed"] is False @@ -398,12 +409,23 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: assert len(minimum["query_evidence"][0]["query_digest"]) == 16 assert minimum["query_evidence"][0]["exact_query_exposed"] is False assert issue_result["automatic_recall"] is False - assert [command[1] for command in issue_runner.calls] == [ + def command_operation(command: list[str]) -> str: + args = command[1:] + if args[:1] == ["--actor-peer-id"]: + args = args[2:] + return args[0] + + assert [command_operation(command) for command in issue_runner.calls] == [ "--version", + "version", "status", "search", "read", ] + assert all( + command[1:3] == ["--actor-peer-id", "project-example"] + for command in issue_runner.calls[3:] + ) incomplete_receipt_evidence = _execution_evidence( { "query_kind": "business_recall", @@ -446,7 +468,7 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: ( item( active_reviewer, - "viking://resources/reward-memory/example/reviewer-summary.json", + f"{SCOPE_REF}/reviewer-summary.json", ), ) ) @@ -464,6 +486,7 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: workspace_ref=WORKSPACE, repository_ref=PROJECT, revision_ref=REVISION, + peer_ref="agent:project-example", observed_at=OBSERVED_AT, freshness_context={ "source_truth_current": True, @@ -556,6 +579,7 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: workspace_ref=WORKSPACE, repository_ref=PROJECT, revision_ref=REVISION, + peer_ref="agent:project-example", observed_at=OBSERVED_AT, freshness_context={ "source_truth_current": True, @@ -595,7 +619,7 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: ( item( active_preference, - "viking://resources/reward-memory/example/preference.json", + f"{SCOPE_REF}/preference.json", ), ) ) @@ -605,6 +629,7 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: request={ "workspace_ref": WORKSPACE, "project_ref": PROJECT, + "peer_ref": "agent:project-example", "surface_id": review_surface, "revision_ref": REVISION, "mode": "bounded_agentic_search", @@ -676,6 +701,7 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: workspace_ref=WORKSPACE, repository_ref=PROJECT, revision_ref=REVISION, + peer_ref="agent:project-example", queries=[{"query": "policy", "query_summary": "policy"}], mode="function_boundary", observed_at=OBSERVED_AT, @@ -714,6 +740,7 @@ def apply_plan(base: Any, items: Any) -> dict[str, Any]: { "workspace_ref": WORKSPACE, "project_ref": PROJECT, + "peer_ref": "agent:project-example", "surface_id": issue_surface, "revision_ref": REVISION, "mode": "function_boundary", diff --git a/examples/reward-memory-walkthrough-smoke.py b/examples/reward-memory-walkthrough-smoke.py index 6572f9e8b9..693f80cc6d 100644 --- a/examples/reward-memory-walkthrough-smoke.py +++ b/examples/reward-memory-walkthrough-smoke.py @@ -29,6 +29,7 @@ from loopx.capabilities.context_providers.base import ( # noqa: E402 ContextProviderItem, ContextProviderRetrieval, + ContextProviderSync, ) from loopx.capabilities.reward_memory import ( # noqa: E402 apply_reward_memory_recall, @@ -102,8 +103,22 @@ def retrieve(self, **kwargs: Any) -> ContextProviderRetrieval: requested_limit=int(kwargs["max_results"]), ) - def sync(self, **_kwargs: Any) -> Any: - raise AssertionError("walkthrough must not call provider sync") + def sync(self, **kwargs: Any) -> ContextProviderSync: + assert kwargs["execute"] is False + self.calls += 1 + return ContextProviderSync( + provider=self.provider_id, + namespace=str(kwargs["namespace"]), + status="preflight_ready", + observed_at=str(kwargs["observed_at"]), + requested_count=1, + completed_count=0, + reason_code="execute_required_for_verified_write", + retry_disposition="execute_required", + provider_preflight_performed=True, + target_access_preflight_verified=True, + writability_verified=False, + ) def assert_public_safe(payload: object) -> None: @@ -515,6 +530,7 @@ def main() -> int: scope_blocked[label] = blocked["status"] # 5) Scoped feedback: plan without provider write; wrong peer fails closed. + provider = FakeProvider() planned = ingest_scoped_feedback_reward_memory_event( feedback_event(), corpus=corpus(), @@ -522,12 +538,14 @@ def main() -> int: provider_binding=binding(), observed_at=OBSERVED_AT, execute=False, + provider=provider, ) - assert planned["status"] == "planned" + assert planned["status"] == "preflight_ready" assert planned["external_writes_performed"] is False assert planned["raw_provider_payload_captured"] is False assert planned["grants_new_action_authority"] is False assert planned["next_reward_memory_call"] == "explicit_function_boundary_recall" + assert provider.calls == 1 blocked_peer = ingest_scoped_feedback_reward_memory_event( feedback_event(peer_ref="agent:other"), diff --git a/loopx/capabilities/issue_fix/reward_memory.py b/loopx/capabilities/issue_fix/reward_memory.py index 5a53de4a4d..b33c097a99 100644 --- a/loopx/capabilities/issue_fix/reward_memory.py +++ b/loopx/capabilities/issue_fix/reward_memory.py @@ -267,6 +267,7 @@ def run_issue_fix_patch_planning_reward_memory( read_authority_checkpoint: Mapping[str, Any], provider_binding: Mapping[str, Any], application_id: str, + peer_ref: str | None = None, artifact_ref: str | None = None, apply_memory: RewardMemoryApplier | None = None, provider: ContextProvider | None = None, @@ -284,23 +285,27 @@ def guarded_apply(base: Any, items: Any) -> Mapping[str, Any]: raise ValueError("Issue Fix reward-memory output must be a patch plan") return decision + request = { + "workspace_ref": workspace_ref, + "project_ref": repository_ref, + "surface_id": ISSUE_FIX_PATCH_PLANNING_SURFACE, + "revision_ref": revision_ref, + "mode": mode, + "query_kind": "business_recall", + "queries": queries, + "limit": limit, + "observed_at": observed_at, + "freshness_context": dict(freshness_context), + "conflict_state": conflict_state, + "raw_content_captured": False, + } + if peer_ref is not None: + request["peer_ref"] = peer_ref + shared = run_semantic_preference_reward_memory( dict(base_plan), corpus=corpus, - request={ - "workspace_ref": workspace_ref, - "project_ref": repository_ref, - "surface_id": ISSUE_FIX_PATCH_PLANNING_SURFACE, - "revision_ref": revision_ref, - "mode": mode, - "query_kind": "business_recall", - "queries": queries, - "limit": limit, - "observed_at": observed_at, - "freshness_context": dict(freshness_context), - "conflict_state": conflict_state, - "raw_content_captured": False, - }, + request=request, read_authority_checkpoint=read_authority_checkpoint, provider_binding=provider_binding, application_id=application_id, @@ -435,6 +440,7 @@ def run_issue_fix_reviewer_artifact_reward_memory( read_authority_checkpoint: Mapping[str, Any], provider_binding: Mapping[str, Any], application_id: str, + peer_ref: str | None = None, artifact_ref: str | None = None, provider: ContextProvider | None = None, limit: int = 3, @@ -453,29 +459,33 @@ def run_issue_fix_reviewer_artifact_reward_memory( reasoning_summary=reasoning_summary, ) + request = { + "workspace_ref": workspace_ref, + "project_ref": repository_ref, + "surface_id": ISSUE_FIX_REVIEWER_ARTIFACT_SURFACE, + "revision_ref": revision_ref, + "mode": "function_boundary", + "queries": [ + { + "query": ( + "Which reviewed policy governs this reviewer-facing PR summary?" + ), + "query_summary": "reviewer-facing PR summary policy", + } + ], + "limit": limit, + "observed_at": observed_at, + "freshness_context": dict(freshness_context), + "conflict_state": conflict_state, + "raw_content_captured": False, + } + if peer_ref is not None: + request["peer_ref"] = peer_ref + shared = run_semantic_preference_reward_memory( base, corpus=corpus, - request={ - "workspace_ref": workspace_ref, - "project_ref": repository_ref, - "surface_id": ISSUE_FIX_REVIEWER_ARTIFACT_SURFACE, - "revision_ref": revision_ref, - "mode": "function_boundary", - "queries": [ - { - "query": ( - "Which reviewed policy governs this reviewer-facing PR summary?" - ), - "query_summary": "reviewer-facing PR summary policy", - } - ], - "limit": limit, - "observed_at": observed_at, - "freshness_context": dict(freshness_context), - "conflict_state": conflict_state, - "raw_content_captured": False, - }, + request=request, read_authority_checkpoint=read_authority_checkpoint, provider_binding=provider_binding, application_id=application_id, @@ -532,6 +542,7 @@ def run_issue_fix_reviewer_artifact_automatic_reward_memory( "corpus_id": item["corpus"]["corpus_id"], "workspace_ref": workspace_ref, "project_ref": repository_ref, + "peer_ref": item["corpus"]["scope"].get("peer_ref"), "surface_id": ISSUE_FIX_REVIEWER_ARTIFACT_SURFACE, "read_authority": item["corpus"]["read_authority"], "source_ref": item["standing_policy"]["authority_source_ref"], @@ -545,6 +556,7 @@ def run_issue_fix_reviewer_artifact_automatic_reward_memory( workspace_ref=workspace_ref, project_ref=repository_ref, revision_ref=revision_ref, + peer_ref=str(scope.get("peer_ref") or "") or None, queries=[ { "query": ( diff --git a/loopx/cli_commands/project_lifecycle.py b/loopx/cli_commands/project_lifecycle.py index 32e28674ad..21f13aac15 100644 --- a/loopx/cli_commands/project_lifecycle.py +++ b/loopx/cli_commands/project_lifecycle.py @@ -8,9 +8,6 @@ from ..capabilities.explore.activation import ( sync_explore_graph_after_material_refresh, ) -from ..capabilities.reward_memory.codex_app_outcome import ( - stage_codex_app_turn_outcome_candidate_fail_open, -) from ..control_plane.agents.capability_gate import ( runtime_capabilities_for_cli_projection, ) @@ -68,6 +65,10 @@ PostWritebackProjectionBuilder, dispatch_committed_cli_post_writeback_hooks, ) +from .quota_reward_memory import ( + reward_memory_candidate_details, + stage_reward_memory_outcome_candidate, +) from .project_lifecycle_inputs import ( inline_agent_vision_packet, inline_progress_observation, @@ -668,35 +669,24 @@ def handle_project_lifecycle_command( and not payload.get("dry_run") ) if material_refresh_ready and reward_memory_reflection_json: - settlement_identity = ( - payload.get("settlement_identity") - if isinstance(payload.get("settlement_identity"), Mapping) - else {} - ) - state = payload.get("state") if isinstance(payload.get("state"), Mapping) else {} validation_workspace_text = str( getattr(args, "delivery_workspace_path", None) or getattr(args, "project", None) or payload.get("project") or "" ).strip() - payload["reward_memory_outcome_candidate"] = ( - stage_codex_app_turn_outcome_candidate_fail_open( - registry_path=registry_path, - runtime_root=resolve_runtime_root( - load_registry(registry_path), - args.runtime_root, - ), - goal_id=args.goal_id, - agent_id=str(args.agent_id), - todo_id=str(args.todo_id), - turn_instance_id=str(args.turn_instance_id), - effect_id=str(settlement_identity.get("effect_id") or ""), - state_file=Path(str(state.get("path") or "")), - validation_workspace=Path(validation_workspace_text), - reflection_json=reward_memory_reflection_json, - observed_at=str(payload.get("generated_at") or ""), - ) + stage_reward_memory_outcome_candidate( + payload, + registry_path=registry_path, + runtime_root=resolve_runtime_root( + load_registry(registry_path), args.runtime_root + ), + goal_id=args.goal_id, + agent_id=str(args.agent_id), + todo_id=str(args.todo_id), + turn_instance_id=str(args.turn_instance_id), + reflection_json=reward_memory_reflection_json, + validation_workspace=Path(validation_workspace_text), ) settlement_receipt_repair = bool( payload.get("ok") @@ -745,39 +735,7 @@ def handle_project_lifecycle_command( args, "replan_obligation_id", None ) or "", - "reward_memory_candidate_id": str( - ( - payload.get("reward_memory_outcome_candidate") - if isinstance( - payload.get("reward_memory_outcome_candidate"), - Mapping, - ) - else {} - ).get("candidate_id") - or "" - ), - "reward_memory_candidate_status": str( - ( - payload.get("reward_memory_outcome_candidate") - if isinstance( - payload.get("reward_memory_outcome_candidate"), - Mapping, - ) - else {} - ).get("status") - or "" - ), - "reward_memory_reflection_digest": str( - ( - payload.get("reward_memory_outcome_candidate") - if isinstance( - payload.get("reward_memory_outcome_candidate"), - Mapping, - ) - else {} - ).get("reflection_digest") - or "" - ), + **reward_memory_candidate_details(payload), }, idempotency_fields=( [ diff --git a/loopx/cli_commands/quota_reward_memory.py b/loopx/cli_commands/quota_reward_memory.py index 8457dd78ed..61179d8f00 100644 --- a/loopx/cli_commands/quota_reward_memory.py +++ b/loopx/cli_commands/quota_reward_memory.py @@ -10,11 +10,61 @@ run_configured_agent_turn_recall_fail_open, ) from ..capabilities.reward_memory.codex_app_outcome import ( + stage_codex_app_turn_outcome_candidate_fail_open, run_staged_codex_app_turn_outcome_ingest_fail_open, ) from ..control_plane.quota.settlement import read_heartbeat_settlement +def stage_reward_memory_outcome_candidate( + payload: dict[str, Any], + *, + registry_path: Path, + runtime_root: Path, + goal_id: str, + agent_id: str, + todo_id: str, + turn_instance_id: str, + reflection_json: str, + validation_workspace: Path, +) -> None: + """Attach a privately staged outcome candidate after a valid refresh.""" + + settlement_identity = payload.get("settlement_identity") + settlement_identity = ( + settlement_identity if isinstance(settlement_identity, Mapping) else {} + ) + state = payload.get("state") + state = state if isinstance(state, Mapping) else {} + payload["reward_memory_outcome_candidate"] = ( + stage_codex_app_turn_outcome_candidate_fail_open( + registry_path=registry_path, + runtime_root=runtime_root, + goal_id=goal_id, + agent_id=agent_id, + todo_id=todo_id, + turn_instance_id=turn_instance_id, + effect_id=str(settlement_identity.get("effect_id") or ""), + state_file=Path(str(state.get("path") or "")), + validation_workspace=validation_workspace, + reflection_json=reflection_json, + observed_at=str(payload.get("generated_at") or ""), + ) + ) + + +def reward_memory_candidate_details(payload: Mapping[str, Any]) -> dict[str, str]: + candidate = payload.get("reward_memory_outcome_candidate") + candidate = candidate if isinstance(candidate, Mapping) else {} + return { + "reward_memory_candidate_id": str(candidate.get("candidate_id") or ""), + "reward_memory_candidate_status": str(candidate.get("status") or ""), + "reward_memory_reflection_digest": str( + candidate.get("reflection_digest") or "" + ), + } + + def attach_reward_memory_ingest_after_spend( payload: dict[str, Any], *, diff --git a/loopx/control_plane/agents/capability_memory.py b/loopx/control_plane/agents/capability_memory.py index e51d2d24a0..f92cbbe672 100644 --- a/loopx/control_plane/agents/capability_memory.py +++ b/loopx/control_plane/agents/capability_memory.py @@ -130,10 +130,17 @@ def resolve_agent_capabilities( root, registry = status_payload.get("runtime_root"), status_payload.get("registry") if state is None and agent_identity and root and registry: - state = agent_capability_memory( - registry_path=Path(str(registry)), runtime_root=Path(str(root)), - goal_id=goal_id, agent_id=agent_identity["agent_id"], - ) + try: + state = agent_capability_memory( + registry_path=Path(str(registry)), runtime_root=Path(str(root)), + goal_id=goal_id, agent_id=agent_identity["agent_id"], + ) + except Exception: # noqa: BLE001 - optional private projection must fail open + # Status/quota projection is still useful when a cached or fixture + # status packet cannot reach the host-local capability store. Do + # not drop the entire agent lane; the invocation capabilities and + # typed goal declarations remain authoritative for this read. + state = {} availability = _evaluate( "availability", goal=[*declared_available_capabilities(item), *declared_available_capabilities(project_asset)], diff --git a/loopx/control_plane/testing/cli_output_budget.py b/loopx/control_plane/testing/cli_output_budget.py index 94254c36ce..2c81da4245 100644 --- a/loopx/control_plane/testing/cli_output_budget.py +++ b/loopx/control_plane/testing/cli_output_budget.py @@ -692,6 +692,18 @@ class CliOutputCommandClassification: surface_id="evidence_log_thin", rationale="bounded evidence read before replan or handoff", ), + CliOutputCommandClassification( + command_id="agent-capabilities", + qualification="explicit_cold_path_exception", + surface_id=None, + rationale="explicit inspection or correction of one registered Agent's runtime observations", + ), + CliOutputCommandClassification( + command_id="handoff", + qualification="explicit_cold_path_exception", + surface_id=None, + rationale="explicit cross-agent Todo handoff preparation or adoption", + ), CliOutputCommandClassification( command_id="todo", qualification="qualified_default", diff --git a/loopx/help_surface.py b/loopx/help_surface.py index bccd4547d4..4933c08731 100644 --- a/loopx/help_surface.py +++ b/loopx/help_surface.py @@ -99,6 +99,14 @@ "command": "loopx evidence-log --goal-id --agent-id --thin", "purpose": "Read the current agent's thin public-safe ledger before replan or handoff.", }, + { + "command": "loopx agent-capabilities --help", + "purpose": "Inspect or correct a registered Agent's observed runtime capabilities.", + }, + { + "command": "loopx handoff --help", + "purpose": "Prepare, inspect, or adopt one explicit cross-agent Todo handoff.", + }, { "command": "loopx machine-config --help", "purpose": "Inspect typed machine policy, preview changes, and apply an exact plan revision.", diff --git a/man/loopx.1 b/man/loopx.1 index e46eeb0019..38a6d9a2aa 100644 --- a/man/loopx.1 +++ b/man/loopx.1 @@ -87,6 +87,12 @@ Preview, stop, or resume a Goal without deleting its history, todos, or evidence \fBloopx evidence\-log \-\-goal\-id \-\-agent\-id \-\-thin\fR Read the current agent's thin public\-safe ledger before replan or handoff. .TP +\fBloopx agent\-capabilities \-\-help\fR +Inspect or correct a registered Agent's observed runtime capabilities. +.TP +\fBloopx handoff \-\-help\fR +Prepare, inspect, or adopt one explicit cross\-agent Todo handoff. +.TP \fBloopx machine\-config \-\-help\fR Inspect typed machine policy, preview changes, and apply an exact plan revision. .TP