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
50 changes: 8 additions & 42 deletions examples/control_plane/todo-user-gate-scope-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,59 +179,25 @@ def read_todo(registry: Path, todo_id: str) -> dict:
return view["todo"]


def assert_legacy_scope_diagnostics() -> None:
# Historical source validation remains readable before import. It cannot
# serve as a normal writer or alter an already canonical authority.
with tempfile.TemporaryDirectory(prefix="loopx-legacy-gate-diagnostics-") as tmp:
def assert_unpromoted_source_is_not_contract_authority() -> None:
# Detailed metadata diagnostics use canonical rows; the File/SQLite cases
# in test_canonical_status_todos cover them without a display file.
with tempfile.TemporaryDirectory(prefix="loopx-unpromoted-gate-diagnostics-") as tmp:
root = Path(tmp)
repo, state, registry = write_fixture(root, import_authority=False)
insert_before_agent_section(
state,
"- [ ] Approve an intentionally unscoped gate.\n"
" <!-- loopx:todo todo_id=todo_unscoped_gate status=open task_class=user_gate -->\n"
"- [ ] Track an unbound non-blocking user action.\n"
" <!-- loopx:todo todo_id=todo_unbound_action status=open task_class=user_action -->\n"
"- [ ] Resolve a mismatched gate binding.\n"
f" <!-- loopx:todo todo_id=todo_mismatched_gate_binding status=open "
f"task_class=user_gate blocks_agent={SIDE_AGENT} bound_agent={PRIMARY_AGENT} -->\n"
"- [ ] Track an intentionally untyped user todo.\n"
" <!-- loopx:todo todo_id=todo_untyped_user status=open -->\n"
"- [ ] Approve a gate with an unregistered blocked agent.\n"
" <!-- loopx:todo todo_id=todo_bad_agent status=open task_class=user_gate blocks_agent=codex-unknown -->\n"
"- [ ] Approve a gate with conflicting scope.\n"
f" <!-- loopx:todo todo_id=todo_both_scopes status=open task_class=user_gate blocks_agent={SIDE_AGENT} global_gate=true -->",
)
state.write_text(
state.read_text(encoding="utf-8")
+ "\n- [ ] Repair a removed continuation value.\n"
+ " <!-- loopx:todo todo_id=todo_removed_continuation status=open task_class=advancement_task action_kind=review continuation_policy=review_handoff -->\n"
+ "- [ ] Repair removed agent gate routing.\n"
+ f" <!-- loopx:todo todo_id=todo_removed_agent_gate status=open task_class=advancement_task blocks_agent={SIDE_AGENT} -->\n"
+ "- [ ] Repair an unknown executor exclusion.\n"
+ " <!-- loopx:todo todo_id=todo_unknown_exclusion status=open task_class=advancement_task excluded_agents=codex-unknown -->\n"
+ "- [ ] Repair malformed executor exclusion metadata.\n"
+ " <!-- loopx:todo todo_id=todo_malformed_exclusion status=open task_class=advancement_task excluded_agents=%%% -->\n",
encoding="utf-8",
" <!-- loopx:todo todo_id=todo_unscoped_gate status=open task_class=user_gate -->",
)
original = state.read_bytes()
check = check_contract(
registry_path=registry, runtime_root_override=str(root / "runtime"),
scan_roots=[repo], limit=1,
)
assert check["ok"] is False, check
for todo_id, diagnostic in (
("todo_unscoped_gate", "requires bound_agent"),
("todo_unbound_action", "requires bound_agent"),
("todo_mismatched_gate_binding", "must bind to blocks_agent"),
("todo_untyped_user", "requires task_class"),
("todo_bad_agent", "not registered"),
("todo_both_scopes", "cannot set both"),
("todo_removed_continuation", "removed continuation_policy"),
("todo_removed_agent_gate", "removed blocks_agent routing"),
("todo_unknown_exclusion", "unregistered agents"),
("todo_malformed_exclusion", "malformed excluded_agents"),
):
assert any(todo_id in error and diagnostic in error for error in check["errors"]), check
assert any(row["code"] == "canonical_authority_required"
and row.get("goal_id") == GOAL_ID for row in check["error_diagnostics"]), check
assert state.read_bytes() == original


Expand Down Expand Up @@ -673,7 +639,7 @@ def main() -> int:
assert state.read_bytes() != original
state.write_bytes(original)

assert_legacy_scope_diagnostics()
assert_unpromoted_source_is_not_contract_authority()
assert_imported_scope_repairs()

print("todo-user-gate-scope-smoke ok")
Expand Down
7 changes: 6 additions & 1 deletion examples/explore-configure-goal-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@


REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402

GOAL_ID = "explore-configure-fixture"


Expand Down Expand Up @@ -86,7 +89,9 @@ def write_fixture(root: Path) -> tuple[Path, Path]:
+ "\n",
encoding="utf-8",
)
return registry, root / "runtime"
runtime_root = root / "runtime"
import_fixture_authority(registry, GOAL_ID)
return registry, runtime_root


def goal(registry: Path) -> dict:
Expand Down
3 changes: 3 additions & 0 deletions examples/explore-worker-plan-gate-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from loopx.control_plane.testing.canary_harness import import_fixture_authority # noqa: E402

from loopx.capabilities.explore.todo_branch_plan import ( # noqa: E402
MAX_BRANCH_WIDTH,
Expand Down Expand Up @@ -387,6 +388,8 @@ def check_cli_gate_states() -> None:
+ "\n",
encoding="utf-8",
)
for entry in goal_entries:
import_fixture_authority(registry, entry["id"])

def run_cli(*extra_args: str, fmt: str = "json", check: bool = True):
result = subprocess.run(
Expand Down
7 changes: 7 additions & 0 deletions loopx/capabilities/explore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ identify the current retained candidate, establish causality, satisfy acceptance
or authorize execution. Actual adoption and outcome benefit require subsequent
work and evidence; automatic branch rankings remain unchanged.

Manually captured measured findings also appear in the existing result pages
and, when linked to eligible work, its exact-source comparison cards. They need
no `writeback-result` origin tag. Read the page before optionally binding a
`selected_candidate`; normal source-revision, Todo-link and followthrough checks
still apply. Unmeasured manual notes retain their existing summary/audit path.
Reading or selecting a measured result does not certify adoption or execution.

## At a Glance

Explore Harness is a supported, default-off optional capability for
Expand Down
5 changes: 5 additions & 0 deletions loopx/capabilities/explore/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
重复尝试。best 不代表当前保留版本、因果收益、验收通过或执行授权;是否实际采用
并产生收益,仍需后续工作和证据确认。自动分支排序保持原有行为。

手动登记的量化 finding 同样进入既有结果分页;关联可执行工作时,也进入其精确来源
比较卡片,无需补写 `writeback-result` 来源标签。先读取结果页,再按需绑定
`selected_candidate`,仍核对来源修订、Todo 关联和后续实际工作。未带测量的手动笔记
保持既有摘要/审计路径。读取或选择某条成绩不认证实际采用或执行。

## 概览

LoopX Explore 是面向长程探索目标(软件研究、安全攻击面测绘、领域调研)的受支持、默认关闭的可选能力。它把“到处看看”变成有界、可观测、有门控的流程,包含三根支柱:
Expand Down
10 changes: 7 additions & 3 deletions loopx/control_plane/capabilities/explore_turn_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,20 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject {
const audit = requireJsonObject(branch.typed_evidence_audit, "Explore evidence audit");
return requireStringArray(audit.requested_node_refs ?? [], "Explore node refs");
}) : []);
const attachedResults = findings.filter(row => Array.isArray(row.tags) && row.tags.includes("writeback-result"));
// Manual quantitative capture uses the same finding owner. Keep its sources
// available to the existing exact-result decision path, without requiring a
// writeback-origin tag or admitting unrelated unmeasured notes.
const decisionResults = findings.filter(row => row.measurements !== undefined
|| Array.isArray(row.tags) && row.tags.includes("writeback-result"));
// Keep the same detail budget, but do not let unrelated newer results evict
// the applicability of evidence linked to the next selected work item.
// Explicitly bound work can differ from this independent planner. Prioritize
// its canonical links without changing candidates, scores or admission.
const plannerResults = prioritizeLinkedResults(attachedResults, selectedRefs);
const plannerResults = prioritizeLinkedResults(decisionResults, selectedRefs);
// Shared links must not inherit another task's planner priority. Use source
// recency within the focus and retain planner ordering only outside it.
const writebackResults = focusedRefs.size
? prioritizeLinkedResults(attachedResults, focusedRefs, plannerResults) : plannerResults;
? prioritizeLinkedResults(decisionResults, focusedRefs, plannerResults) : plannerResults;
const command = (...args: string[]) => [...route, "explore", ...args, "--goal-id", goal];
const resultLimit = requireInteger(params.result_limit ?? 3, "result_limit");
const resultOffset = requireInteger(params.result_offset ?? 0, "result_offset");
Expand Down
23 changes: 14 additions & 9 deletions tests/control_plane/test_stride_comparison_followthrough_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ def review(value, turn_id=turn, *, expected=0):


@pytest.mark.parametrize("provider", ["file", "sqlite"])
def test_wide_result_page_replays_every_linked_outcome_at_writeback(tmp_path, monkeypatch, provider):
@pytest.mark.parametrize("tagged", [False, True], ids=["manual-measurement", "writeback-result"])
def test_wide_result_page_replays_every_linked_outcome_at_writeback(tmp_path, monkeypatch, provider, tagged):
isolate_sqlite_runtime(tmp_path, monkeypatch)
monkeypatch.setenv("LOOPX_USAGE_PING", "0")
monkeypatch.setenv("PYTHONPATH", str(fixture.REPO_ROOT))
Expand All @@ -175,15 +176,16 @@ def cli(*args, expected=0):
actor.probe(request, "retained")
for index in range(4):
measurements = project / f"measurement-{index}.json"
measurements.write_text(json.dumps([{"metric": "quality", "value": index,
measurements.write_text(json.dumps([{"metric": "quality", "value": 3 - index,
"unit": "points", "higher_is_better": True,
"comparison_scope": "validation:owned-v1", "input_revision": "owned-input-v1",
"evaluation_ref": "evaluator:owned-v1", "candidate_ref": f"candidate:scope-{index}",
"evidence_ref": f"measured:scope-{index}"}]))
cli("explore", "finding", *identity, "--finding-id", f"wide-result-{index}",
"--node", "comparison-retained", "--title", f"Observed input scope {index}",
"--summary", f"Only the recorded input scope {index}; other inputs remain unknown.",
"--status", "confirmed", "--tag", "writeback-result", "--evidence-ref", f"measured:scope-{index}",
"--status", "confirmed", *(("--tag", "writeback-result") if tagged else ()),
"--evidence-ref", f"measured:scope-{index}",
"--measurements-json", str(measurements))
turn = "wide-page-review"
cli("quota", "should-run", *identity, "--codex-app", "--turn-instance-id", turn)
Expand All @@ -196,17 +198,20 @@ def cli(*args, expected=0):
# Choose a quantified outcome omitted by the default three-row view.
visible = {row["finding_id"]: row for row in page["graph"]["writeback_results"]}
default_refs = {row["finding_id"] for row in control["explore_basis"]["comparison_candidates"][0]["outcomes"]}
selected = {"todo_id": fixture.TODO_ID, **next(row for row in reversed(card["outcomes"])
if row["finding_id"] not in default_refs and visible[row["finding_id"]].get("measurements"))}
measurement = visible[selected["finding_id"]]["measurements"][0]
assert measurement["evidence_ref"] == visible[selected["finding_id"]]["evidence_refs"][0]
[feedback] = page["graph"]["measurement_feedback"]
assert feedback["measured_count"] == 4
assert feedback["best"]["candidate_ref"] == "candidate:scope-3"
assert feedback["best"]["candidate_ref"] == "candidate:scope-0"
assert feedback["best"]["value"] == 3 and feedback["latest"]["measurement"]["value"] == 0
selected = {"todo_id": fixture.TODO_ID, **next(row for row in card["outcomes"]
if row["finding_id"] == feedback["best"]["finding_id"])}
assert selected["finding_id"] not in default_refs, "an older best must remain selectable beyond the first page"
measurement = visible[selected["finding_id"]]["measurements"][0]
assert measurement["evidence_ref"] == visible[selected["finding_id"]]["evidence_refs"][0]
decision = deepcopy(control["minimal_example"])
decision["explore_review"] = {"revision": control["explore_basis"]["revision"],
"disposition": "used", "considered_refs": [selected["finding_id"]],
"selected_candidate": selected, "reason": "This exact measured scope informs the next experiment.",
"selected_candidate": selected,
"reason": "Revisit the same-scope best quality of 3 before repeating the latest candidate's score of 0.",
"result_pages": [{k: page["graph"]["result_page"][k] for k in ("revision", "offset", "limit", "todo_id")}]}
packet = actor.vision(request, selected["finding_id"], outcome="replan")
packet.update(stride_decision=decision, todo_delta=["retain:" + fixture.TODO_ID])
Expand Down
7 changes: 4 additions & 3 deletions tests/control_plane_ts/explore_turn_context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ for (const includeDigests of [false, true]) {
});
}

for (const limit of [3, 4, 20]) {
test(`every scoped outcome on an explicitly requested page remains selectable: ${limit}`, () => {
for (const tagged of [false, true]) for (const limit of [3, 4, 20]) {
test(`every scoped measured outcome remains selectable: tagged=${tagged}, limit=${limit}`, () => {
const findings = Array.from({length: 20}, (_, index) => ({finding_id: `scoped-${index}`,
node_id: "question", finding: `Measured input family ${index}`, status: "confirmed",
summary: `Applicable to input family ${index} only.`, tags: ["writeback-result"],
summary: `Applicable to input family ${index} only.`, tags: tagged ? ["writeback-result"] : [],
evidence_refs: [`result:scoped-${index}`],
measurements: [{metric: "quality", value: index, unit: "points", higher_is_better: true,
comparison_scope: "validation:owned-v1", input_revision: "owned-input-v1",
Expand All @@ -99,6 +99,7 @@ for (const limit of [3, 4, 20]) {
typed_evidence_audit: {requested_node_refs: ["question"]}}]}};
const source = projectExploreTurnContext(request);
const graph = source.graph as JsonObject, harness = source.harness as JsonObject;
assert.ok(harness.comparison_candidates?.length, "measured findings must reach the existing decision cards without an origin tag");
const card = harness.comparison_candidates[0];
// Quantitative feedback still uses all sources, independently of page size.
assert.equal(graph.measurement_feedback[0].measured_count, 20);
Expand Down
Loading