Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/architecture/rfcs/monorepo-distribution-split-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -4505,3 +4505,35 @@ view shows its independently checked result in the same conversation.
actual-model improvements, full D2/D5 closure, R1/M3 or C1 default cutover.
Preserve prior failures and the original ten requirements; full original-goal
regression still precedes completion and EdgeBench preparation.

### 2026-11-15 — Load PR wait observation after permission recheck

- **Gap:** composing an executing Turn imported PR reconciliation and its
metadata/content providers before resolving observation permission, even
when the hook was unavailable. A provider import failure escaped the optional
observation failure boundary.
- **Delta:** the existing producer imports the reconciler inside its provider
`try`, after the invocation-time capability check succeeds. Registration and
invocation retain their separate permission checks and revocation behavior.
No wrapper, cached decision, new activation flag or policy owner is added.
Python remains the stable GitHub provider/transport adapter; typed selection,
permissions, authority and observation cadence retain their existing owners.
- **Evidence:** fresh-process blocked-provider cases cover denied, revoked and
unavailable permission, plus import failure with unknown reads/effects.
Eight matched hook scenarios use a frozen clock to compare deterministic
returns, permission checks and provider calls;
source File/SQLite checks retain provider success, failure, response loss,
cadence and full-source diagnostics. Separate installed Core wheels execute
authorized File/SQLite observation and canonical Todo readback, alongside
execution, replay, process-loss and invalid-artifact checks. The original
consumer suite retains four matching setup failures on both arms; a separate
current-carrier protocol fixture does not replace those records.
- **Boundary:** unavailable observation avoids loading its provider closure;
granted observation keeps the existing behavior. This is a bounded default
closure increment. The first granted invocation now includes deferred imports
in the existing `duration_ms` observation window; real timing equivalence or
performance gains are not qualified. It does not establish full D2/D5,
actual-model/context adoption, complete-cost
improvement, original-ten acceptance or C1 cutover. Preserve prior sources,
failures and seals; full original-goal regression still precedes completion
and EdgeBench preparation.
22 changes: 22 additions & 0 deletions docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3488,3 +3488,25 @@ workspace 批准和脚本模型工作。桌面视图在采用检查后显示原
激活、capture 发布、恢复或结算边界。它不证明总延迟、完整成本或真实模型改善,
也不完成 D2/D5 闭包、R1/M3 或 C1 默认切换。保留历史失败和原十项要求;完成前
仍需完整原目标回归,随后才准备 EdgeBench。

### 2026-11-15 — PR 等待观察在权限复查后加载

- **缺口:**执行性 Turn 的组合在解析观察权限前导入 PR reconciler 及其 metadata/
content provider,即使 hook 不可用也会加载。provider 导入失败还会逃出可选
观察原有的故障边界。
- **变化:**既有 producer 在 invocation-time capability 检查成功后,于原 provider
`try` 内导入 reconciler。注册和调用仍分别检查权限,保留撤销行为;没有新增
wrapper、缓存决策、激活 flag 或 policy owner。Python 仍承担稳定的 GitHub
provider/transport adapter;typed selection、权限、authority 和观察 cadence
继续由既有 owner 持有。
- **证据:**独立进程的 blocked-provider 反例覆盖未授权、权限撤销、权限源不可用,
以及导入失败时读取/效果为 unknown。八类配对 hook 场景冻结时钟,比较确定性
返回、权限检查与 provider 调用;源码 File/SQLite 检查保留 provider 成功、失败、响应丢失、cadence
和完整源诊断。独立安装的两份 Core wheel 执行获权的 File/SQLite 观察和 canonical
Todo 回读,并覆盖执行、回放、进程丢失与非法 artifact。原 consumer 套件在两侧
保留四个一致的 setup 失败;独立的当前 carrier 协议 fixture 不替代这些记录。
- **边界:**不可用观察不加载其 provider 闭包,获权观察保留既有行为。本次是有界
默认闭包增量;首次获权调用现在把 deferred import 纳入既有 `duration_ms` 观察
窗口,不证明真实计时等价或性能收益。它不完成 D2/D5、真实模型/上下文采用、完整成本改善、原十项验收
或 C1 切换。保留历史源码、失败和 seal;完成前仍需完整原目标回归,随后才准备
EdgeBench。
4 changes: 3 additions & 1 deletion loopx/capabilities/issue_fix/pr_wait_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
TURN_START_HOOK_RESULT_SCHEMA_VERSION,
TurnStartHookRegistration,
)
from .pr_wait_reconcile import reconcile_pr_wait_dependencies


PR_WAIT_OBSERVATION_HOOK_ID = "issue_fix.pr_wait_observation"
Expand Down Expand Up @@ -91,6 +90,9 @@ def produce() -> dict[str, Any]:
"external_read_count": 0, "state_written": False,
"summary_lines": ["Observation permission source is unavailable; provider not contacted."]}}
try:
# Optional provider loading follows the invocation-time permission recheck.
from .pr_wait_reconcile import reconcile_pr_wait_dependencies

readback = reconcile_pr_wait_dependencies(registry_path=registry_path,
runtime_root_arg=str(runtime_root), goal_id=goal_id, agent_id=agent_id,
fetch_timeout_seconds=fetch_timeout_seconds)
Expand Down
12 changes: 4 additions & 8 deletions tests/capabilities/test_pr_wait_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,21 +249,19 @@ def read(inspector):

@pytest.mark.parametrize("provider", ["file", "sqlite"])
def test_lost_observation_response_refreshes_cached_readiness(tmp_path, monkeypatch, provider):
from loopx.capabilities.issue_fix import pr_wait_hook

registry, runtime, _, todo_id, argv = _native_wait_turn(tmp_path, monkeypatch, provider)
# Capture the old pending projection before permission is configured.
_invoke_guard(argv, "capture-pending", "--write-projection-cache")
_declare_observation(registry)
monkeypatch.setattr(pr_lifecycle, "fetch_github_pr_lifecycle_payload", lambda *a, **kw: {
"url": "https://github.com/owner/repo/pull/42", "state": "MERGED", "mergedAt": "2026-10-01T01:12:00Z"})
original = pr_wait_hook.reconcile_pr_wait_dependencies
original = pr_wait_reconcile.reconcile_pr_wait_dependencies

def response_lost(**kwargs):
original(**kwargs)
raise OSError("response lost after local commit")

monkeypatch.setattr(pr_wait_hook, "reconcile_pr_wait_dependencies", response_lost)
monkeypatch.setattr(pr_wait_reconcile, "reconcile_pr_wait_dependencies", response_lost)
result = _invoke_guard(argv, "lost-response", "--use-projection-cache")
readback = _observation(result)
assert readback["external_read_count"] is None and readback["state_written"] is None
Expand Down Expand Up @@ -291,8 +289,6 @@ def test_unavailable_permission_source_never_contacts_provider(tmp_path, monkeyp

@pytest.mark.parametrize("provider", ["file", "sqlite"])
def test_full_source_selection_is_independent_of_bounded_hook_diagnostics(tmp_path, monkeypatch, provider):
from loopx.capabilities.issue_fix import pr_wait_hook

isolate_sqlite_runtime(tmp_path, monkeypatch)
lines = tuple(line for number in range(1, 501) for line in (
f"- [ ] [P2] Continue dependency {number}.",
Expand All @@ -301,7 +297,7 @@ def test_full_source_selection_is_independent_of_bounded_hook_diagnostics(tmp_pa
_, runtime, registry = _write_live_fixture(tmp_path, provider=provider, extra_agent_todo_lines=lines)
_declare_observation(registry)
calls, full_results = [], []
original = pr_wait_hook.reconcile_pr_wait_dependencies
original = pr_wait_reconcile.reconcile_pr_wait_dependencies

def capture(**kwargs):
result = original(**kwargs)
Expand All @@ -312,7 +308,7 @@ def fetch(reference, **kwargs):
calls.append(reference)
return {"url": f"https://github.com/{reference['repo']}/pull/{reference['number']}", "state": "OPEN"}

monkeypatch.setattr(pr_wait_hook, "reconcile_pr_wait_dependencies", capture)
monkeypatch.setattr(pr_wait_reconcile, "reconcile_pr_wait_dependencies", capture)
monkeypatch.setattr(pr_lifecycle, "fetch_github_pr_lifecycle_payload", fetch)
dispatch = extend_turn_start_dispatch({}, registry_path=registry, runtime_root=runtime,
goal_id="loopx-turn-fixture", agent_id="codex-fixture", available=None)
Expand Down
76 changes: 75 additions & 1 deletion tests/test_cli_entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@ def test_heartbeat_chain_routes_do_not_load_the_chat_layer(tmp_path, monkeypatch
initialize_canonical_source_fixture(runtime, "perf-goal", state_path=state, provider="sqlite")
argv = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", *command]
result = run_cli_main("loopx.entrypoint", argv,
forbidden_modules=("loopx.cli", "loopx.chat_manager", "loopx.chat_agent", "loopx.chat_store"))
forbidden_modules=("loopx.cli", "loopx.chat_manager", "loopx.chat_agent", "loopx.chat_store",
"loopx.capabilities.issue_fix.pr_wait_reconcile"))
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout)["ok"] is True

Expand Down Expand Up @@ -700,6 +701,10 @@ def test_capability_hook_imports_defer_unselected_facade_exports() -> None:
"issue_fix.periodic_report",
"issue_fix.pr_description",
"issue_fix.reward_memory",
"issue_fix.pr_wait_reconcile",
"issue_fix.pr_lifecycle",
"issue_fix.metadata_preview",
"content_ops.surface",
"reward_memory.codex_app_outcome",
"reward_memory.dogfood",
"reward_memory.evaluation",
Expand All @@ -719,6 +724,75 @@ def test_capability_hook_imports_defer_unselected_facade_exports() -> None:
assert completed.returncode == 0, completed.stderr


@pytest.mark.parametrize("permission", ["denied", "revoked", "source-unavailable", "recheck-unavailable", "granted"])
def test_pr_wait_provider_import_obeys_permission_and_failure_owner(permission):
script = f"""
import importlib.abc
import sys
from pathlib import Path

attempts = []
class UnavailableProvider(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname == "loopx.capabilities.issue_fix.pr_wait_reconcile":
attempts.append(fullname)
raise ImportError("optional observation provider unavailable")

sys.meta_path.insert(0, UnavailableProvider())
from loopx.capabilities.issue_fix import pr_wait_hook

permission = {permission!r}
grant = ["external_evidence_poll"]
states = iter({{
"denied": [[]], "revoked": [grant, []],
"source-unavailable": [OSError("permission source unavailable")] * 2,
"recheck-unavailable": [grant, OSError("permission source unavailable")],
"granted": [grant, grant],
}}[permission])
checks = []
def availability(*args):
value = next(states)
checks.append(value)
if isinstance(value, Exception):
raise value
return {{"effective": value}}

pr_wait_hook._availability = availability
hook = pr_wait_hook.turn_start_hook(registry_path=Path("unused"), runtime_root=Path("unused"),
goal_id="fixture", agent_id="fixture", available=None)
assert attempts == [], attempts
if permission == "denied":
assert hook is None and len(checks) == 1
else:
assert hook is not None
result = hook.producer()
assert len(checks) == 2
assert result["external_writes_performed"] is False
if permission == "granted":
assert len(attempts) == 1
assert result["status"] == "failed"
assert result["error_code"] == "dependency_observation_failed"
assert result["external_reads_performed"] is None
assert result["local_private_state_mutated"] is None
assert result["readback"]["external_read_count"] is None
assert result["readback"]["state_written"] is None
assert result["readback"]["error_category"] == "ImportError"
else:
assert attempts == []
assert result["external_reads_performed"] is False
assert result["local_private_state_mutated"] is False
assert result["status"] == ("not_applicable" if permission == "revoked" else "unavailable")
if permission != "revoked":
assert result["error_code"] == "capability_source_unavailable"
assert result["readback"]["external_read_count"] == 0
for name in ("pr_wait_reconcile", "pr_lifecycle", "metadata_preview"):
assert "loopx.capabilities.issue_fix." + name not in sys.modules
assert "loopx.capabilities.content_ops.surface" not in sys.modules
"""
completed = run_isolated_script(script)
assert completed.returncode == 0, completed.stderr


def test_quota_facade_lazy_exports_preserve_owner_identity() -> None:
script = """
from importlib import import_module
Expand Down
Loading