diff --git a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts
index 9ef4ef10ab..74bc1c4ccb 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts
+++ b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts
@@ -159,6 +159,7 @@ const fieldCopy: Record = {
executor_model: { label: "Model", description: "Optional model for the selected executor. Leave blank to keep the executor's own default." },
executor_reasoning_effort: { label: "Reasoning effort", description: "Optional reasoning effort for the selected executor. Leave blank to keep the executor's own default." },
count_unit: { label: "Review after", description: "Work Turns require accepted settlement; polls and retries do not count.", options: { completed_todos: "Completed Todos", effective_turns: "Settled work Turns" } },
+ planning_horizon_goal: { label: "Show Goal in planning horizon", description: "Default off. Read the full original Goal and acceptance before choosing work. Does not change replan, stride or execution authority." },
stride_strategy: { label: "Stride strategy", description: "Structured work scope, route and validation proposals are required for enabled fresh Vision reviews. Does not grant execution authority.", options: { off: "Off", evidence_conditioned_v1: "Evidence-conditioned · v1" } },
count: { label: "Number between reviews", description: "Settled work Turns: 1–6; completed Todos: 1–5. Goal overrides take precedence over device defaults." },
allowed_domains: { label: "Allowed responsibility domains", description: "Enter one bounded, public-safe domain per line." },
@@ -194,6 +195,7 @@ const fieldCopy: Record = {
executor_model: { label: "模型", description: "所选执行器使用的模型,可留空;留空表示沿用执行器自身的默认模型。" },
executor_reasoning_effort: { label: "推理档位", description: "所选执行器使用的推理档位,可留空;留空表示沿用执行器自身的默认档位。" },
count_unit: { label: "复核计数依据", description: "有效工作 Turn 须完成结算;轮询和重复重试不计数。", options: { completed_todos: "已完成 Todo", effective_turns: "已结算工作 Turn" } },
+ planning_horizon_goal: { label: "在规划视野中显示 Goal", description: "默认关闭。选择工作前读取完整原目标与验收要求,不改变复盘、步幅策略或执行权限。" },
stride_strategy: { label: "步幅策略", description: "启用后,新的 Vision 路线复盘须提交工作大小、路线及验证范围的结构化提案,不授予执行权限。", options: { off: "关闭", evidence_conditioned_v1: "依据证据调整 · v1" } },
count: { label: "两次复核间的数量", description: "已结算工作 Turn 为 1–6,已完成 Todo 为 1–5;Goal 显式设置优先于设备默认值。" },
allowed_domains: { label: "允许的职责域", description: "每行填写一个有边界、可公开的职责域。" },
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx
index 24192a293c..73eda04803 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx
@@ -379,8 +379,8 @@ function CapabilityCatalog({ callbacks, catalog, goalId, loading, notification,
: null}
{selected.effective_value_policy === "goal_stride_over_live_review_cadence" ?
{locale === "zh-CN"
- ? `复盘周期沿用${selected.machine_current ? "设备" : "产品"}默认;步幅策略由本 Goal 设置。`
- : `Review interval follows ${selected.machine_current ? "device" : "product"} defaults; this Goal sets the stride strategy.`}
+ ? `复盘周期沿用${selected.machine_current ? "设备" : "产品"}默认;规划选项由本 Goal 设置。`
+ : `Review interval follows ${selected.machine_current ? "device" : "product"} defaults; this Goal sets the planning options.`}
: null}
tuple[str, str, int]:
def _replan_receipt(self) -> dict[str, int | str]:
key, _, value = self._replan_configuration()
return {key: value, **({"stride_strategy": self.stride_strategy}
- if self.stride_strategy is not None else {})}
+ if self.stride_strategy is not None else {}),
+ **({"planning_horizon_goal": self.planning_horizon_goal}
+ if self.planning_horizon_goal is not None else {})}
@staticmethod
def name() -> str:
@@ -422,6 +431,9 @@ async def _prepare_phase(
key, option, expected = self._replan_configuration()
stride_args = (["--execution-stride-strategy", self.stride_strategy]
if self.stride_strategy is not None else [])
+ if self.planning_horizon_goal is not None:
+ stride_args.append("--execution-planning-horizon-goal" if self.planning_horizon_goal
+ else "--no-execution-planning-horizon-goal")
pending = await environment.exec(
command=f"test -e {_LOOPX_RUNTIME}/benchmark-pending-turn.json"
)
@@ -502,6 +514,9 @@ async def _prepare_phase(
raise RuntimeError(
f"replan cadence readback mismatch for {key}: expected {expected}, got {configured!r}"
)
+ actual_horizon = configured_state.get("execution_profile", {}).get("planning_horizon_goal", False)
+ if actual_horizon != (self.planning_horizon_goal or False):
+ raise RuntimeError("planning_horizon_goal readback mismatch")
strategy = configured_state.get("execution_profile", {}).get("stride_strategy", "off")
if strategy != (self.stride_strategy or "off"):
raise RuntimeError(
diff --git a/benchmark/runtime/sforge.py b/benchmark/runtime/sforge.py
index 0f6b139996..f410678ae1 100644
--- a/benchmark/runtime/sforge.py
+++ b/benchmark/runtime/sforge.py
@@ -95,6 +95,7 @@ def __init__(self, config, *, profile: str, cwd: str,
replan_after_turns: int | None = None,
replan_after_todos: int | None = None,
stride_strategy: str | None = None,
+ planning_horizon_goal: bool | None = None,
checkpoint_request_path: str | None = None,
checkpoint_request: bytes | None = None,
warm_start=None):
@@ -143,6 +144,12 @@ def __init__(self, config, *, profile: str, cwd: str,
if stride_strategy is not None:
from loopx.control_plane.work_items.stride_policy_codec import normalize_stride_strategy
stride_strategy = normalize_stride_strategy(stride_strategy)
+ if planning_horizon_goal is not None:
+ if not isinstance(planning_horizon_goal, bool):
+ raise ValueError("planning_horizon_goal must be a boolean")
+ if not profile.startswith("heartbeat-"):
+ raise ValueError("planning_horizon_goal requires a heartbeat profile")
+ self.planning_horizon_goal = planning_horizon_goal
self.stride_strategy = stride_strategy
self.checkpoint_request_path = None
self.checkpoint_request = checkpoint_request
@@ -265,6 +272,7 @@ def _install_worker(self, backend, handle, log_dir, logger):
replan_after_turns=self.replan_after_turns,
replan_after_todos=self.replan_after_todos,
stride_strategy=self.stride_strategy,
+ planning_horizon_goal=self.planning_horizon_goal,
checkpoint_request_path=self.checkpoint_request_path,
checkpoint_request=self.checkpoint_request,
task_objective=self.task_objective,
diff --git a/benchmark/tests/test_edgebench_report.py b/benchmark/tests/test_edgebench_report.py
index 97e1c965c2..31e39e5a7f 100644
--- a/benchmark/tests/test_edgebench_report.py
+++ b/benchmark/tests/test_edgebench_report.py
@@ -625,3 +625,32 @@ def build(repository):
return data
_publish_and_recheck(tmp_path, build)
+
+
+@pytest.mark.parametrize("enabled", [None, False, True])
+def test_report_preserves_horizon_opt_in_and_historical_absence(trial, enabled):
+ runs, selection, output, _ = trial
+ for name in ("runtime-receipt", "worker-profile"):
+ path = runs / "run" / "case" / (name + ".json")
+ value = json.loads(path.read_text())
+ if enabled is not None:
+ value["planning_horizon_goal"] = enabled
+ write(path, value)
+ export(runs, selection, output, observed_at="2026-01-02T00:00:00+00:00")
+ config = json.loads((output / "settings/run.json").read_text())
+ for field in ("runner", "worker_profile"):
+ assert config[field].get("planning_horizon_goal") is enabled
+ assert ("planning_horizon_goal" in config[field]) is (enabled is not None)
+
+
+@pytest.mark.parametrize("receipt,profile", [(True, False), (True, None), ("true", "true"), (True, 1)])
+def test_report_rejects_unbound_horizon_setting(trial, receipt, profile):
+ runs, selection, output, _ = trial
+ for name, enabled in (("runtime-receipt", receipt), ("worker-profile", profile)):
+ path = runs / "run" / "case" / (name + ".json")
+ value = json.loads(path.read_text())
+ if enabled is not None:
+ value["planning_horizon_goal"] = enabled
+ write(path, value)
+ with pytest.raises(ValueError, match="planning_horizon_goal"):
+ export(runs, selection, output, observed_at="2026-01-02T00:00:00+00:00")
diff --git a/benchmark/tests/test_sforge_runtime.py b/benchmark/tests/test_sforge_runtime.py
index cf91fbbe05..b5f05f6193 100644
--- a/benchmark/tests/test_sforge_runtime.py
+++ b/benchmark/tests/test_sforge_runtime.py
@@ -462,7 +462,8 @@ def test_planned_sforge_entry_rejects_unsupported_profiles(profile, monkeypatch)
@pytest.mark.parametrize("entry", ["seeded-todo", "loopx-planned"])
-def test_sforge_command_preserves_native_timing_and_planning_boundary(tmp_path, monkeypatch, entry):
+@pytest.mark.parametrize("horizon", [None, False, True])
+def test_sforge_command_preserves_native_timing_and_planning_boundary(tmp_path, monkeypatch, entry, horizon):
pytest.importorskip("sforge")
pytest.importorskip("harbor")
from sforge.harness.config import SForgeConfig
@@ -472,9 +473,11 @@ async def installed(self, environment):
pass
monkeypatch.setattr(BenchmarkCodex, "install", installed)
worker = SForgeWorker(SForgeConfig(agent_model="fixture", agent_effort="xhigh"),
- profile="heartbeat-explore", cwd="/task", task_entry=entry)
+ profile="heartbeat-explore", cwd="/task", task_entry=entry, planning_horizon_goal=horizon)
worker.install_stop_hook(None, None, tmp_path, None)
assert worker.runtime.execution.task_entry == entry
+ assert worker.runtime.planning_horizon_goal is horizon
+ assert worker.runtime.execution.turn_envelope is False
worker.prepared = True # Bootstrap is separately covered through real CLI fixtures.
first = worker.format_run_cmd("/task.md")
resumed = worker.format_run_cmd("/task.md", resume=True)
@@ -482,7 +485,10 @@ async def installed(self, environment):
assert ("benchmark.runtime.sforge_entry" in first) is (entry == "loopx-planned")
assert "test -f /opt/loopx-benchmark/control/phase-deadline ||" in first
assert first.index("export LOOPX_PHASE_DEADLINE_EPOCH") < first.index("timeout --signal")
- assert json.loads((tmp_path / "worker-profile.json").read_text())["task_entry"] == entry
+ profile = json.loads((tmp_path / "worker-profile.json").read_text())
+ assert profile["task_entry"] == entry
+ assert profile.get("planning_horizon_goal") is horizon
+ assert ("planning_horizon_goal" in profile) is (horizon is not None)
@pytest.mark.parametrize('profile', ['heartbeat-resume', 'heartbeat-explore'])
@pytest.mark.parametrize('enabled', [False, True])
@pytest.mark.parametrize('cadence', [None, 2, 6])
@@ -656,6 +662,8 @@ def test_invalid_checkpoint_declaration_rejects_before_trial_registration(tmp_pa
@pytest.mark.parametrize("cadence_args,field,count", [
([], "replan_after_effective_turns", 6),
+ (["--planning-horizon-goal"], "replan_after_effective_turns", 6),
+ (["--no-planning-horizon-goal"], "replan_after_effective_turns", 6),
(["--replan-after-turns", "2"], "replan_after_effective_turns", 2),
(["--replan-after-todos", "3"], "replan_after_completed_todos", 3),
])
@@ -693,6 +701,9 @@ def stop_before_solver(**kwargs):
assert kwargs["timeout"] == kwargs["config"].agent_timeout == expected
assert kwargs["eval_interval"] == interval
agent = kwargs["agent"]
+ horizon = (True if "--planning-horizon-goal" in cadence_args else
+ False if "--no-planning-horizon-goal" in cadence_args else None)
+ assert agent.planning_horizon_goal is horizon
assert "total trial budget" not in agent.workspace_instructions
assert agent.task_objective == "fixture task"
assert agent.task_objective in agent.feedback_prompt
@@ -713,6 +724,10 @@ def stop_before_solver(**kwargs):
assert receipt["timeout_seconds"] == expected
assert receipt["eval_interval"] == interval
assert receipt[field] == count
+ horizon = (True if "--planning-horizon-goal" in cadence_args else
+ False if "--no-planning-horizon-goal" in cadence_args else None)
+ assert receipt.get("planning_horizon_goal") is horizon
+ assert ("planning_horizon_goal" in receipt) is (horizon is not None)
other = ("replan_after_completed_todos" if field == "replan_after_effective_turns"
else "replan_after_effective_turns")
assert other not in receipt
diff --git a/benchmark/tests/test_shared_codex_runtime.py b/benchmark/tests/test_shared_codex_runtime.py
index ed41f587cd..792eefb418 100644
--- a/benchmark/tests/test_shared_codex_runtime.py
+++ b/benchmark/tests/test_shared_codex_runtime.py
@@ -935,3 +935,34 @@ def test_toolface_qualification_captures_first_request_and_rejects_empty(tmp_pat
toolface.qualify_toolface(
set(), codex_bin=fake, model="fixture-model", timeout_sec=30,
extra_env={"FAKE_DIE": "1"})
+
+
+@pytest.mark.parametrize("requested,actual", [(True, False), (False, True), (None, True)])
+def test_horizon_setting_must_match_restored_goal_before_work(tmp_path, monkeypatch, requested, actual):
+ pytest.importorskip("harbor")
+ from benchmark.runtime.harbor import BenchmarkCodex
+ agent = BenchmarkCodex(logs_dir=tmp_path, model_name="fixture", planning_horizon_goal=requested)
+ async def prepared(*args, **kwargs):
+ return True
+ async def no_pending(**kwargs):
+ return SimpleNamespace(return_code=1)
+ async def wrong_setting(*args, **kwargs):
+ return {"after": {"execution_profile": {
+ "replan_after_effective_turns": 6, "planning_horizon_goal": actual,
+ }}}
+ monkeypatch.setattr(agent, "_registry_exists", prepared)
+ monkeypatch.setattr(agent, "_write_task_document", prepared)
+ monkeypatch.setattr(agent, "_loopx", wrong_setting)
+ with pytest.raises(RuntimeError, match="planning_horizon_goal readback mismatch"):
+ asyncio.run(agent._prepare_phase(SimpleNamespace(exec=no_pending), "Synthetic task", cwd=str(tmp_path)))
+
+
+@pytest.mark.parametrize("settings", [
+ {"planning_horizon_goal": "true"}, {"planning_horizon_goal": 1},
+ {"execution_mode": "plain", "planning_horizon_goal": False},
+])
+def test_horizon_setting_rejects_non_boolean_and_non_loopx_mode(tmp_path, settings):
+ pytest.importorskip("harbor")
+ from benchmark.runtime.harbor import BenchmarkCodex
+ with pytest.raises(ValueError, match="planning_horizon_goal"):
+ BenchmarkCodex(logs_dir=tmp_path, model_name="fixture", **settings)
diff --git a/benchmark/tests/test_task_entry.py b/benchmark/tests/test_task_entry.py
index 8a72e5887d..921c47a9f9 100644
--- a/benchmark/tests/test_task_entry.py
+++ b/benchmark/tests/test_task_entry.py
@@ -698,6 +698,8 @@ def test_sforge_planning_failed_host_has_no_execution_handoff(planning_env):
@pytest.mark.parametrize("settings,field,value", [
({}, "replan_after_effective_turns", 6),
+ ({"planning_horizon_goal": True}, "replan_after_effective_turns", 6),
+ ({"planning_horizon_goal": False}, "replan_after_effective_turns", 6),
({"replan_after_turns": 2}, "replan_after_effective_turns", 2),
({"replan_after_todos": 3}, "replan_after_completed_todos", 3),
({"stride_strategy": "off"}, "replan_after_effective_turns", 6),
@@ -745,5 +747,8 @@ async def no_pending(**kwargs):
assert other not in profile
strategy = settings.get("stride_strategy")
assert profile.get("stride_strategy", "off") == (strategy or "off")
- assert agent._replan_receipt() == {field: value, **({"stride_strategy": strategy}
+ horizon = settings.get("planning_horizon_goal")
+ assert profile.get("planning_horizon_goal", False) == (horizon or False)
+ assert agent._replan_receipt() == {field: value,
+ **({"planning_horizon_goal": horizon} if horizon is not None else {}), **({"stride_strategy": strategy}
if strategy is not None else {})}
diff --git a/docs/reference/protocols/quota-planning-horizon-v0.md b/docs/reference/protocols/quota-planning-horizon-v0.md
index 651e40874d..07432d40b8 100644
--- a/docs/reference/protocols/quota-planning-horizon-v0.md
+++ b/docs/reference/protocols/quota-planning-horizon-v0.md
@@ -3,9 +3,11 @@
`quota_planning_horizon_v0` is the bounded, read-only context that lets an
agent reason beyond one locally selected Todo without inlining LoopX's full
Todo store or task graph. It is present on the default `quota should-run` path
-only when the current selected work has typed lineage, waiting/blocking,
-scheduled-observation, or goal-acceptance context. Flat runnable alternatives
-remain solely in `action_portfolio`, avoiding a duplicate hot-path view.
+when a planning choice has strategic context. The optional `planning_horizon_goal`
+setting is **off by default**. When enabled it carries the current `core_goal`
+before work selection, including for a single local Todo or flat backlog.
+When disabled, the existing strategic-context condition and projection are
+unchanged. Flat runnable alternatives remain owned by `action_portfolio`.
The problem it addresses is narrower than scheduling. A selected regression
gate can be locally runnable while the strategic path also contains a facts
@@ -35,6 +37,48 @@ not silently replace the selected Todo.
authorities. Planning-horizon items are context until the agent performs the
existing explicit selection/re-entry flow.
+The Goal lens reuses the replan source reader and TypeScript `projectCoreGoal`
+projection: active-state Objective first, registered objective as fallback,
+explicit missing state, and the existing separately scoped acceptance contract.
+It does not persist another Goal, promote an Agent vision or Todo into the
+original objective, enable stride, or change action eligibility. When
+available, `core_goal.read_action` reads the same complete Goal state, with the
+source path shell-quoted. A corrected source is read on the next planning
+projection; no copied objective needs synchronization.
+
+This extends planning context only after explicit opt-in. Existing Todo selection,
+claim/lease, admission, replan precedence and settlement authority remain
+unchanged. Goal text is scoped decision input; public export remains a separate
+boundary. The complete original objective is not shortened to the Todo display
+budget. Transport-size warnings remain visible rather than discarding its tail.
+
+## Enable, Read Back And Disable
+
+The existing **Goal review and stride** capability editor in Settings →
+Capabilities exposes **Show Goal in planning horizon** at machine and Goal scope.
+Use its preview, apply and readback flow. Its optional boolean is stored in the
+existing `execution_profile.planning_horizon_goal`; no new capability or Goal
+store is introduced. The v2 machine review configuration accepts the same
+optional boolean; omission preserves its previous shape and disabled default.
+As with stride, a Goal with an explicit review cadence uses its own configuration;
+a sparse Goal switch inherits the live interval. An explicit false overrides a
+machine opt-in. Clearing a Goal override restores the applicable inherited value.
+
+```sh
+loopx configure-goal --goal-id example --execution-planning-horizon-goal --execute
+loopx configure-goal --goal-id example --format json
+loopx quota should-run --goal-id example --agent-id researcher --format json
+loopx configure-goal --goal-id example --no-execution-planning-horizon-goal --execute
+```
+
+The quota readback contains `planning_horizon.core_goal` only when this projection
+is enabled and a preselection planning projection is applicable. It is not added
+to receipt-bound replays or ineligible work merely to repeat the Goal. Disabling
+skips the extra Goal-document read. Replan continues to read its existing Goal
+context independently. TurnEnvelope, stride, execution permission and quota are
+not enabled by this setting. Context/signature changes are observable input
+changes, not evidence of improved model decisions.
+
## Shape
```json
@@ -44,6 +88,12 @@ not silently replace the selected Todo.
"goal_id": "loopx-meta",
"agent_id": "codex-main-control",
"selected_todo_id": "todo_regression_gate",
+ "core_goal": {
+ "objective": "Deliver the complete accepted outcome.",
+ "objective_source": "active_state",
+ "objective_missing": false,
+ "acceptance_contract": null
+ },
"selection_contract": {
"selected_todo_authority": "$.selected_todo",
"action_choice_authority": "$.action_portfolio",
@@ -179,6 +229,11 @@ A conforming implementation must prove:
- portfolio, horizon, and agent-Todo detail consume the same typed inventory;
- task graph consumes the same canonical Todo selector, plus terminal evidence
rows, without acquiring agent-scoped selection or claim authority;
+- omitted/false settings retain the prior horizon and skip extra Goal reads;
+- when enabled, the same current Goal reaches preselection horizon and replan, with original
+ trailing constraints and source readback intact;
+- missing, corrected and fallback sources stay explicit; scoped acceptance and
+ latest Agent vision do not replace the original Goal;
- the horizon never changes `selected_todo` or action-portfolio eligibility;
- runnable unclaimed work preserves `claim_required_before_work=true`;
- typed successor/resume/route relations survive the default compact path;
diff --git a/examples/personal-workspace-browser/replan-cadence.mjs b/examples/personal-workspace-browser/replan-cadence.mjs
index dd21e56826..2ca9a00192 100644
--- a/examples/personal-workspace-browser/replan-cadence.mjs
+++ b/examples/personal-workspace-browser/replan-cadence.mjs
@@ -127,7 +127,7 @@ export const replanCadenceScenario = {
|| converted.effective_configuration.configuration.count_unit !== "effective_turns") {
throw new Error("JSON-to-form save substituted the product-default interval");
}
- await page.getByText("复盘周期沿用设备默认;步幅策略由本 Goal 设置。", { exact: true }).waitFor();
+ await page.getByText("复盘周期沿用设备默认;规划选项由本 Goal 设置。", { exact: true }).waitFor();
await page.screenshot({ path: resolve(outputDir, "replan-cadence-json-form.png"), animations: "disabled" });
if (JSON.stringify(converted.current) !== JSON.stringify({ stride_strategy: "evidence_conditioned_v1" })) {
throw new Error("JSON-to-form strategy-only save pinned a cadence");
@@ -148,6 +148,33 @@ export const replanCadenceScenario = {
await page.getByRole("combobox", { name: "目标 Goal", exact: true }).selectOption("multi-agent-projection");
await page.getByRole("navigation", { name: "Goal 能力目录" }).getByRole("button", { name: /Goal 复盘与步幅/ }).click();
}
+ // The same real configuration owner supports the independent horizon
+ // switch without pinning cadence or changing stride. Unchecked remains off.
+ const horizonGoal = page.getByLabel(/^在规划视野中显示 Goal/);
+ assert.equal(await horizonGoal.isChecked(), false);
+ await horizonGoal.check();
+ await page.getByRole("button", { name: "预览变更", exact: true }).click();
+ await applyGoalPreview();
+ await reopenGoalCadence();
+ assert.equal(await horizonGoal.isChecked(), true);
+ let horizonReadback = await readGoalCadence();
+ assert.equal(horizonReadback.current.planning_horizon_goal, true);
+ assert.equal(horizonReadback.current.stride_strategy, "evidence_conditioned_v1");
+ assert.equal(horizonReadback.current.count, undefined);
+ await page.screenshot({ path: resolve(outputDir, "planning-horizon-goal-enabled.png"), animations: "disabled" });
+ await horizonGoal.uncheck();
+ await page.getByRole("button", { name: "预览变更", exact: true }).click();
+ await applyGoalPreview();
+ await reopenGoalCadence();
+ assert.equal(await horizonGoal.isChecked(), false);
+ horizonReadback = await readGoalCadence();
+ assert.equal(horizonReadback.current.planning_horizon_goal, false);
+ // Restore the sparse pre-existing scenario before its cadence transitions.
+ await page.getByRole("button", { name: "编辑 JSON", exact: true }).click();
+ await page.locator("#goal-configuration-json").fill(JSON.stringify({stride_strategy: "evidence_conditioned_v1"}));
+ await page.getByRole("button", { name: "返回表单", exact: true }).click();
+ await page.getByRole("button", { name: "预览变更", exact: true }).click();
+ await applyGoalPreview();
await changeMachineCount(4);
if ((await readGoalCadence()).effective_configuration.configuration.count !== 4) {
throw new Error("Strategy-only Goal did not follow a later machine policy");
@@ -187,6 +214,7 @@ export const replanCadenceScenario = {
await delayedReadback.started;
assert.equal(await count.isDisabled(), true, "A committed write must keep the editor closed until its readback settles");
assert.equal(await strategy.isDisabled(), true);
+ assert.equal(await horizonGoal.isDisabled(), true);
assert.equal(await page.getByRole("button", { name: "编辑 JSON", exact: true }).isDisabled(), true);
assert.equal(await page.getByRole("button", { name: "预览变更", exact: true }).isDisabled(), true);
const goalCatalog = page.getByRole("navigation", { name: "Goal 能力目录" });
@@ -223,6 +251,8 @@ export const replanCadenceScenario = {
await unit.focus();
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
+ assert.equal(await horizonGoal.evaluate((element) => element === document.activeElement), true, "Horizon switch is keyboard reachable");
+ await page.keyboard.press("Tab");
if (!(await strategy.evaluate((element) => element === document.activeElement))) throw new Error("Strategy is not keyboard reachable");
await page.screenshot({ path: resolve(outputDir, "replan-cadence-goal-mobile.png"), animations: "disabled" });
if (await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1)) throw new Error("Cadence editor overflowed");
diff --git a/loopx/capabilities/configuration_ui.py b/loopx/capabilities/configuration_ui.py
index 40b211c309..47813f3359 100644
--- a/loopx/capabilities/configuration_ui.py
+++ b/loopx/capabilities/configuration_ui.py
@@ -115,6 +115,8 @@ def capability_configuration_editor(
_field("count", "Review interval", "number", minimum=1,
maximum=MAX_EFFECTIVE_TURN_REPLAN_THRESHOLD, required=True,
description="Effective Turns: 1–6; completed Todos: 1–5. Effective Turns require accepted work settlement. Retries and observation-only polls do not count."),
+ _field("planning_horizon_goal", "Show Goal in planning horizon", "boolean",
+ description="Default off. Include the full original Goal and acceptance before choosing work; replan Goal reading, stride and execution authority are unchanged."),
_field("stride_strategy", "Stride strategy", "select",
options=list(stride_strategy_options()),
description="Off retains ordinary review. Evidence-conditioned v1 independently reviews work-slice size, route changes and validation scope; fresh Vision path reviews require a structured proposal, without automatic execution."),
@@ -618,7 +620,8 @@ def _attach_effective_configuration(entry: dict[str, Any]) -> None:
if (
entry["capability_id"] == "todo_replan_cadence"
and goal_override is not None
- and set(goal_override) == {"stride_strategy"}
+ and bool(goal_override)
+ and set(goal_override) <= {"stride_strategy", "planning_horizon_goal"}
):
from .todo_replan_cadence.machine_defaults import (
cadence_configuration_value,
@@ -633,8 +636,7 @@ def _attach_effective_configuration(entry: dict[str, Any]) -> None:
else default_replan_cadence_configuration()
)
goal_override = {
- "count_unit": inherited_cadence["count_unit"],
- "count": inherited_cadence["count"],
+ **inherited_cadence,
**goal_override,
}
entry["effective_value_policy"] = "goal_stride_over_live_review_cadence"
diff --git a/loopx/capabilities/todo_replan_cadence/goal_configuration.py b/loopx/capabilities/todo_replan_cadence/goal_configuration.py
index 4267f92751..2290e46489 100644
--- a/loopx/capabilities/todo_replan_cadence/goal_configuration.py
+++ b/loopx/capabilities/todo_replan_cadence/goal_configuration.py
@@ -12,7 +12,7 @@ def configuration_summary(goal: Mapping[str, Any]) -> dict[str, Any] | None:
profile = goal.get("execution_profile")
if not isinstance(profile, Mapping):
return None
- strategy = {"stride_strategy": profile["stride_strategy"]} if "stride_strategy" in profile else {}
+ strategy = {key: profile[key] for key in ("stride_strategy", "planning_horizon_goal") if key in profile}
if "replan_after_effective_turns" in profile:
return {
"count_unit": "effective_turns",
diff --git a/loopx/capabilities/todo_replan_cadence/machine_defaults.py b/loopx/capabilities/todo_replan_cadence/machine_defaults.py
index e0b171e481..53b126b4f2 100644
--- a/loopx/capabilities/todo_replan_cadence/machine_defaults.py
+++ b/loopx/capabilities/todo_replan_cadence/machine_defaults.py
@@ -36,7 +36,7 @@ def normalize_replan_cadence_configuration(raw: Mapping[str, Any]) -> dict[str,
"count_unit": ReplanCadenceUnit.COMPLETED_TODOS.value,
"count": normalize_completed_todo_replan_threshold(raw["completed_todos"]),
}
- if set(raw) not in ({"count_unit", "count"}, {"count_unit", "count", "stride_strategy"}):
+ if not {"count_unit", "count"} <= set(raw) or set(raw) - {"count_unit", "count", "stride_strategy", "planning_horizon_goal"}:
raise ValueError("review cadence requires count_unit and count")
try:
unit = ReplanCadenceUnit(raw["count_unit"])
@@ -53,6 +53,10 @@ def normalize_replan_cadence_configuration(raw: Mapping[str, Any]) -> dict[str,
if "stride_strategy" in raw:
from ...control_plane.work_items.stride_policy_codec import normalize_stride_strategy
result["stride_strategy"] = normalize_stride_strategy(raw["stride_strategy"])
+ if "planning_horizon_goal" in raw:
+ if not isinstance(raw["planning_horizon_goal"], bool):
+ raise ValueError("planning_horizon_goal must be a boolean")
+ result["planning_horizon_goal"] = raw["planning_horizon_goal"]
return result
@@ -74,6 +78,8 @@ def normalize_todo_replan_cadence_machine_defaults(
expected = {"schema_version", "count_unit", "count"}
if version == TODO_REPLAN_CADENCE_MACHINE_DEFAULTS_V2:
expected.add("stride_strategy")
+ if "planning_horizon_goal" in raw:
+ expected.add("planning_horizon_goal")
if set(raw) != expected:
raise ValueError("review configuration fields do not match its schema version")
return {
@@ -177,6 +183,8 @@ def apply_todo_replan_cadence_machine_default(
profile[field] = cadence["count"]
if "stride_strategy" not in profile and cadence.get("stride_strategy", "off") != "off":
profile["stride_strategy"] = cadence["stride_strategy"]
+ if "planning_horizon_goal" not in profile and "planning_horizon_goal" in cadence:
+ profile["planning_horizon_goal"] = cadence["planning_horizon_goal"]
projected["execution_profile"] = profile
return projected
diff --git a/loopx/chat_goal_configuration_api.py b/loopx/chat_goal_configuration_api.py
index 8388c08dbe..d9a333c6bf 100644
--- a/loopx/chat_goal_configuration_api.py
+++ b/loopx/chat_goal_configuration_api.py
@@ -195,7 +195,7 @@ def _goal_capability_options(
if capability_id == "periodic_report":
return {"clear_periodic_report_configuration": True}
if capability_id == "todo_replan_cadence":
- return {"clear_execution_replan_after_todos": True, "clear_execution_replan_after_turns": True, "clear_execution_stride_strategy": True}
+ return {"clear_execution_replan_after_todos": True, "clear_execution_replan_after_turns": True, "clear_execution_stride_strategy": True, "clear_execution_planning_horizon_goal": True}
if capability_id == "pull_request_review":
return {"clear_pull_request_review_configuration": True}
if capability_id == "change_quality_qualification":
@@ -208,7 +208,7 @@ def _goal_capability_options(
config = dict(configuration)
allowed: dict[str, set[str]] = {
"goal_capability_organization": {"mode", "discovery_budget_minutes", "max_trials"},
- "todo_replan_cadence": {"completed_todos", "count_unit", "count", "stride_strategy"},
+ "todo_replan_cadence": {"completed_todos", "count_unit", "count", "stride_strategy", "planning_horizon_goal"},
"multi_subagent": {
"enabled",
"max_children",
@@ -244,19 +244,26 @@ def _goal_capability_options(
if capability_id == "todo_replan_cadence":
from .capabilities.todo_replan_cadence.machine_defaults import normalize_replan_cadence_configuration
from .control_plane.work_items.stride_policy_codec import normalize_stride_strategy
- if set(config) == {"stride_strategy"}:
+ horizon = {"clear_execution_planning_horizon_goal": True}
+ if "planning_horizon_goal" in config:
+ if not isinstance(config["planning_horizon_goal"], bool):
+ raise ValueError("planning_horizon_goal must be a boolean")
+ horizon = {"execution_planning_horizon_goal": config["planning_horizon_goal"]}
+ if config and set(config) <= {"stride_strategy", "planning_horizon_goal"}:
# The capability editor submits the complete sparse value. Unlike
# the CLI's single-field strategy patch, omission restores live
# interval inheritance even when this Goal was previously pinned.
return {
- "execution_stride_strategy": normalize_stride_strategy(config["stride_strategy"]),
+ **horizon,
+ **({"execution_stride_strategy": normalize_stride_strategy(config["stride_strategy"])}
+ if "stride_strategy" in config else {}),
"clear_execution_replan_after_turns": True,
"clear_execution_replan_after_todos": True,
}
cadence = normalize_replan_cadence_configuration(config)
option = ("execution_replan_after_turns" if cadence["count_unit"] == "effective_turns"
else "execution_replan_after_todos")
- return {option: cadence["count"], **({"execution_stride_strategy": cadence["stride_strategy"]}
+ return {option: cadence["count"], **horizon, **({"execution_stride_strategy": cadence["stride_strategy"]}
if "stride_strategy" in cadence else {})}
if capability_id == "periodic_report":
return {"periodic_report_configuration": config}
diff --git a/loopx/cli_commands/registry_admin.py b/loopx/cli_commands/registry_admin.py
index d6ea55411a..01d93dc8ff 100644
--- a/loopx/cli_commands/registry_admin.py
+++ b/loopx/cli_commands/registry_admin.py
@@ -452,6 +452,8 @@ def handle_registry_admin_command(
execution_turn_granularity=args.execution_turn_granularity,
execution_replan_after_todos=args.execution_replan_after_todos,
execution_replan_after_turns=args.execution_replan_after_turns,
+ execution_planning_horizon_goal=args.execution_planning_horizon_goal,
+ clear_execution_planning_horizon_goal=args.clear_execution_planning_horizon_goal,
execution_stride_strategy=args.execution_stride_strategy,
clear_execution_stride_strategy=args.clear_execution_stride_strategy,
clear_execution_replan_after_turns=args.clear_execution_replan_after_turns,
diff --git a/loopx/cli_commands/registry_admin_configure.py b/loopx/cli_commands/registry_admin_configure.py
index 06b7692340..48300f2bcb 100644
--- a/loopx/cli_commands/registry_admin_configure.py
+++ b/loopx/cli_commands/registry_admin_configure.py
@@ -70,6 +70,15 @@ def register_configure_goal_command(subparsers: argparse._SubParsersAction) -> N
"--clear-execution-replan-after-turns", action="store_true",
help="Remove the Goal effective-Turn override and restore device/default inheritance.",
)
+ from argparse import BooleanOptionalAction
+ configure_goal_parser.add_argument(
+ "--execution-planning-horizon-goal", action=BooleanOptionalAction, default=None,
+ help="Include the full same-source Goal in preselection planning horizons. Default off; no selection or execution authority change.",
+ )
+ configure_goal_parser.add_argument(
+ "--clear-execution-planning-horizon-goal", action="store_true",
+ help="Remove the Goal horizon override and restore review-configuration inheritance.",
+ )
from ..control_plane.work_items.stride_policy_codec import stride_strategy_options
configure_goal_parser.add_argument(
"--execution-stride-strategy", choices=stride_strategy_options(),
diff --git a/loopx/configure_goal.py b/loopx/configure_goal.py
index c7db8e3676..24901ced0b 100644
--- a/loopx/configure_goal.py
+++ b/loopx/configure_goal.py
@@ -440,6 +440,8 @@ def configure_goal(
execution_replan_after_turns: int | None = None,
execution_stride_strategy: str | None = None,
clear_execution_stride_strategy: bool = False,
+ execution_planning_horizon_goal: bool | None = None,
+ clear_execution_planning_horizon_goal: bool = False,
clear_execution_replan_after_turns: bool = False,
clear_execution_replan_after_todos: bool = False,
self_repair_enabled: bool | None = None,
@@ -833,6 +835,8 @@ def configure_goal(
replan_after_completed_todos=execution_replan_after_todos,
replan_after_effective_turns=execution_replan_after_turns,
stride_strategy=execution_stride_strategy, clear_stride_strategy=clear_execution_stride_strategy,
+ planning_horizon_goal=execution_planning_horizon_goal,
+ clear_planning_horizon_goal=clear_execution_planning_horizon_goal,
clear_replan_after_effective_turns=clear_execution_replan_after_turns,
clear_replan_after_completed_todos=clear_execution_replan_after_todos,
)
diff --git a/loopx/control_plane/quota/should_run_packet.py b/loopx/control_plane/quota/should_run_packet.py
index f81bda1c3b..d354005a94 100644
--- a/loopx/control_plane/quota/should_run_packet.py
+++ b/loopx/control_plane/quota/should_run_packet.py
@@ -810,6 +810,8 @@ def _planning_projections(
capability_gate=prepared.capability_gate,
blocked_priority_fallback=prepared.blocked_priority_fallback,
goal_frontier_projection=prepared.goal_frontier_projection,
+ source_status=prepared.status_payload,
+ planning_horizon_goal=(prepared.project_asset.get("execution_profile") or {}).get("planning_horizon_goal", False),
replan_selection_context=(
{
"has_turn_identity": bool(turn_instance_id),
diff --git a/loopx/control_plane/work_items/action_portfolio.py b/loopx/control_plane/work_items/action_portfolio.py
index f17e05cb47..5ad65201a6 100644
--- a/loopx/control_plane/work_items/action_portfolio.py
+++ b/loopx/control_plane/work_items/action_portfolio.py
@@ -54,9 +54,13 @@ def build_quota_planning_packet(
blocked_priority_fallback: Mapping[str, Any] | None,
goal_frontier_projection: Mapping[str, Any] | None,
replan_selection_context: Mapping[str, Any] | None = None,
+ source_status: Mapping[str, Any] | None = None,
+ planning_horizon_goal: bool = False,
) -> dict[str, Any]:
"""Project every requested planning lens through one TypeScript request."""
+ if not isinstance(planning_horizon_goal, bool):
+ raise ValueError("planning_horizon_goal must be a boolean")
if not projection_enabled and not include_detail and replan_selection_context is None:
return {}
inventory_request = build_quota_planning_inventory_request(
@@ -70,6 +74,12 @@ def build_quota_planning_packet(
)
if inventory_request is None:
return {}
+ from .replan_context_codec import goal_context_facts
+
+ goal_facts = (
+ goal_context_facts(source_status, goal_id, (agent_todo_summary or {}).get("goal_acceptance_contract"))
+ if planning_horizon_goal and source_status and source_status.get("registry") else None
+ )
try:
projected = effect_runtime_result(
"work_item.action_portfolio.project",
@@ -81,6 +91,7 @@ def build_quota_planning_packet(
"acceptance_gaps": _frontier_acceptance_gaps(
goal_frontier_projection
),
+ **({"goal_facts": goal_facts} if goal_facts is not None else {}),
**({"replan_selection_context": dict(replan_selection_context)}
if replan_selection_context is not None else {}),
},
diff --git a/loopx/control_plane/work_items/action_portfolio.ts b/loopx/control_plane/work_items/action_portfolio.ts
index 436787f60a..12f18cc34a 100644
--- a/loopx/control_plane/work_items/action_portfolio.ts
+++ b/loopx/control_plane/work_items/action_portfolio.ts
@@ -324,6 +324,7 @@ function projectQuotaPlanningPacket(request: JsonObject): JsonObject {
schema_version: PLANNING_HORIZON_REQUEST_SCHEMA_VERSION,
planning_inventory: inventory,
acceptance_gaps: request.acceptance_gaps,
+ ...(request.goal_facts === undefined ? {} : {goal_facts: request.goal_facts}),
});
if (horizon !== null) projected.planning_horizon = horizon;
}
diff --git a/loopx/control_plane/work_items/planning_horizon.ts b/loopx/control_plane/work_items/planning_horizon.ts
index 8f612afcd7..33891a9ef8 100644
--- a/loopx/control_plane/work_items/planning_horizon.ts
+++ b/loopx/control_plane/work_items/planning_horizon.ts
@@ -13,6 +13,7 @@ import {
import { relationKey, todoRef, type PlanningInventoryRelation } from "./planning_relations.ts";
import type { JsonObject } from "../effect_program.ts";
+import { projectCoreGoal } from "./replan_context.ts";
export const PLANNING_HORIZON_SCHEMA_VERSION = "quota_planning_horizon_v0";
export const PLANNING_HORIZON_REQUEST_SCHEMA_VERSION =
@@ -242,7 +243,8 @@ export function projectQuotaPlanningHorizon(value: unknown): JsonObject | null {
item.todo_id !== selected.todo_id &&
strategicContextStates.has(item.planning_state)
);
- if (!addsStrategicContext) {
+ const core = request.goal_facts === undefined ? null : projectCoreGoal(request.goal_facts);
+ if (!addsStrategicContext && core === null) {
return null;
}
return {
@@ -251,6 +253,7 @@ export function projectQuotaPlanningHorizon(value: unknown): JsonObject | null {
goal_id: goalId,
agent_id: agentId,
selected_todo_id: selected.todo_id,
+ ...(core === null ? {} : {core_goal: core}),
selection_contract: {
selected_todo_authority: "$.selected_todo",
action_choice_authority: "$.action_portfolio",
diff --git a/loopx/control_plane/work_items/replan_context.ts b/loopx/control_plane/work_items/replan_context.ts
index 3d6147fcf2..ed934c9e67 100644
--- a/loopx/control_plane/work_items/replan_context.ts
+++ b/loopx/control_plane/work_items/replan_context.ts
@@ -91,7 +91,7 @@ function evidenceFactsKey(row: JsonObject): string {
return digest(facts);
}
-function coreGoal(value: unknown): JsonObject {
+export function projectCoreGoal(value: unknown): JsonObject {
const facts = value == null ? {} : requireJsonObject(value, "goal_facts");
const active = facts.active_state_objective;
const registered = facts.registry_objective;
@@ -102,7 +102,10 @@ function coreGoal(value: unknown): JsonObject {
const acceptance = facts.acceptance_contract == null ? null
: requireJsonObject(facts.acceptance_contract, "goal_acceptance_contract");
return {objective, objective_source: active ? "active_state" : registered ? "registry" : null,
- objective_missing: objective === null, acceptance_contract: acceptance};
+ objective_missing: objective === null, acceptance_contract: acceptance,
+ ...(facts.goal_read_action == null ? {} : {
+ read_action: requireNonEmptyString(facts.goal_read_action, "goal_facts.goal_read_action"),
+ })};
}
/** Qualify recorded bindings using the settlement owner, without settling work.
@@ -388,7 +391,7 @@ export function projectReplanContext(value: unknown): JsonObject {
const selectedCoverage = new Set(chosen.flatMap(({row}) => row.progress_observation
? [String((row.progress_observation as JsonObject).fingerprint)] : []));
const ledger = [...coverage].filter(([key]) => selectedCoverage.has(key)).map(([, value]) => value);
- const core = coreGoal(request.goal_facts);
+ const core = projectCoreGoal(request.goal_facts);
const strideStrategy = normalizeStrideStrategy(request.stride_strategy);
const exploreBasis = strideStrategy !== "off" ? projectStrideExploreBasis(request.explore_context, goal, agent) : null;
const followthrough = agent && strideStrategy !== "off"
diff --git a/loopx/control_plane/work_items/replan_context_codec.py b/loopx/control_plane/work_items/replan_context_codec.py
index 5ab872f2d9..317c88ca04 100644
--- a/loopx/control_plane/work_items/replan_context_codec.py
+++ b/loopx/control_plane/work_items/replan_context_codec.py
@@ -111,7 +111,7 @@ def replan_evidence_rows(runs: Iterable[Mapping[str, Any]], *, goal_id: str, age
return rows
-def _goal_facts(status: Mapping[str, Any] | None, goal_id: str, acceptance: Mapping[str, Any] | None) -> dict[str, Any]:
+def goal_context_facts(status: Mapping[str, Any] | None, goal_id: str, acceptance: Mapping[str, Any] | None) -> dict[str, Any]:
"""Read existing Goal sources; source precedence belongs to TypeScript."""
def goal_text(value: Any) -> str | None:
if value is not None and not isinstance(value, str):
@@ -130,6 +130,8 @@ def goal_text(value: Any) -> str | None:
facts["goal_ref"] = {"goal_id": goal_id, "goal_instance_id": goal["goal_instance_id"]}
facts["registry_objective"] = goal_text(goal.get("objective"))
path = goal_state_path(goal)
+ if path:
+ facts["goal_read_action"] = "cat -- " + shlex.quote(str(path.resolve()))
if path and path.exists():
state = path.read_text(encoding="utf-8")
metadata, _ = split_state_frontmatter(state)
@@ -177,7 +179,7 @@ def project_replan_context(
**({"explore_context": explore_context} if explore_context is not None else {}),
"evidence_ref": evidence_ref,
"read_prefix": shlex.join(prefix),
- "goal_facts": _goal_facts(source_status, goal_id, goal_acceptance_contract) if evidence_ref is None else {},
+ "goal_facts": goal_context_facts(source_status, goal_id, goal_acceptance_contract) if evidence_ref is None else {},
"from_full_index": bool(source_status and source_status.get("registry") and source_status.get("runtime_root")),
}, method="work_item.replan_context")
except EffectRuntimeRejected as exc:
diff --git a/loopx/execution_profile.py b/loopx/execution_profile.py
index e2beb5e700..bed86f2439 100644
--- a/loopx/execution_profile.py
+++ b/loopx/execution_profile.py
@@ -154,6 +154,11 @@ def compact_execution_profile(value: Any) -> dict[str, Any]:
if not isinstance(value, dict):
return profile
+ if "planning_horizon_goal" in value:
+ if not isinstance(value["planning_horizon_goal"], bool):
+ raise ValueError("planning_horizon_goal must be a boolean")
+ profile["planning_horizon_goal"] = value["planning_horizon_goal"]
+
if "stride_strategy" in value:
from .control_plane.work_items.stride_policy_codec import normalize_stride_strategy
profile["stride_strategy"] = normalize_stride_strategy(value["stride_strategy"])
@@ -249,6 +254,8 @@ def apply_goal_execution_profile_change(
clear_replan_after_effective_turns: bool = False,
stride_strategy: str | None = None,
clear_stride_strategy: bool = False,
+ planning_horizon_goal: bool | None = None,
+ clear_planning_horizon_goal: bool = False,
) -> None:
if (
clear_replan_after_completed_todos
@@ -264,10 +271,14 @@ def apply_goal_execution_profile_change(
raise ValueError("choose one review cadence override or clear operation")
if stride_strategy is not None and clear_stride_strategy:
raise ValueError("choose a stride strategy override or clear operation")
+ if planning_horizon_goal is not None and clear_planning_horizon_goal:
+ raise ValueError("choose a planning horizon Goal override or clear operation")
raw = goal.get("execution_profile")
profile = dict(raw) if isinstance(raw, dict) else {}
if (
- stride_strategy is None
+ planning_horizon_goal is None
+ and not clear_planning_horizon_goal
+ and stride_strategy is None
and not clear_stride_strategy
and turn_granularity is None
and replan_after_completed_todos is None
@@ -276,6 +287,10 @@ def apply_goal_execution_profile_change(
and not (clear_replan_after_completed_todos and "replan_after_completed_todos" in profile)
):
return
+ if clear_planning_horizon_goal:
+ profile.pop("planning_horizon_goal", None)
+ if planning_horizon_goal is not None:
+ profile["planning_horizon_goal"] = planning_horizon_goal
if clear_stride_strategy:
profile.pop("stride_strategy", None)
if stride_strategy is not None:
diff --git a/tests/capabilities/test_machine_configuration_goal_defaults.py b/tests/capabilities/test_machine_configuration_goal_defaults.py
index 408f882e80..d9e886f2b7 100644
--- a/tests/capabilities/test_machine_configuration_goal_defaults.py
+++ b/tests/capabilities/test_machine_configuration_goal_defaults.py
@@ -400,6 +400,7 @@ def test_goal_stride_only_uses_live_review_interval(tmp_path: Path) -> None:
"stride_strategy": "evidence_conditioned_v1",
})
assert options == {
+ "clear_execution_planning_horizon_goal": True,
"execution_stride_strategy": "evidence_conditioned_v1",
"clear_execution_replan_after_turns": True,
"clear_execution_replan_after_todos": True,
@@ -482,3 +483,41 @@ def test_stride_configuration_rejects_unsupported_or_ambiguous_versions(schema_v
"schema_version": schema_version, "count_unit": "effective_turns",
"count": 6, "stride_strategy": strategy,
})
+
+
+def test_horizon_goal_configuration_opt_in_inheritance_and_clear(tmp_path):
+ from loopx.chat_goal_configuration_api import _goal_capability_options
+ from loopx.capabilities.goal_inspection import inspect_goal_capabilities
+
+ _repo, registry_path, runtime_root = _fixture(tmp_path)
+ owner = build_builtin_machine_configuration_registry()
+ configuration = _machine_configuration()
+ configuration["namespaces"]["todo_replan_cadence"] = {
+ "schema_version": "todo_replan_cadence_machine_defaults_v2",
+ "count_unit": "effective_turns", "count": 4, "stride_strategy": "off",
+ "planning_horizon_goal": True,
+ }
+ assert "planning_horizon_goal" not in _history_goal(registry_path, runtime_root)["execution_profile"]
+ preview = configure_machine_configuration(runtime_root=runtime_root, registry=owner, configuration=configuration)
+ configure_machine_configuration(runtime_root=runtime_root, registry=owner,
+ configuration=configuration, execute=True, expected_plan_revision=preview["plan_revision"])
+ assert _history_goal(registry_path, runtime_root)["execution_profile"]["planning_horizon_goal"] is True
+ for enabled in (False, True):
+ options = _goal_capability_options("todo_replan_cadence", {"planning_horizon_goal": enabled})
+ configure_goal(registry_path=registry_path, goal_id=GOAL_ID, execute=True, **options)
+ profile = _history_goal(registry_path, runtime_root)["execution_profile"]
+ assert profile["planning_horizon_goal"] is enabled
+ assert profile["replan_after_effective_turns"] == 4
+ inspected = inspect_goal_capabilities(registry_path=registry_path, runtime_root=runtime_root,
+ goal_id=GOAL_ID)["configuration"]
+ entry = next(x for x in inspected["capability_catalog"]["capabilities"] if x["capability_id"] == "todo_replan_cadence")
+ assert entry["current"] == {"planning_horizon_goal": enabled}
+ assert entry["effective_configuration"]["configuration"]["planning_horizon_goal"] is enabled
+ configure_goal(registry_path=registry_path, goal_id=GOAL_ID, execute=True,
+ clear_execution_planning_horizon_goal=True)
+ assert _history_goal(registry_path, runtime_root)["execution_profile"]["planning_horizon_goal"] is True
+ before = registry_path.read_bytes()
+ with pytest.raises(ValueError, match="planning_horizon_goal"):
+ configure_goal(registry_path=registry_path, goal_id=GOAL_ID, execute=True,
+ execution_planning_horizon_goal="true")
+ assert registry_path.read_bytes() == before
diff --git a/tests/control_plane/test_quota_planning_horizon.py b/tests/control_plane/test_quota_planning_horizon.py
index d4020c78e6..97afd36a14 100644
--- a/tests/control_plane/test_quota_planning_horizon.py
+++ b/tests/control_plane/test_quota_planning_horizon.py
@@ -1,6 +1,11 @@
from __future__ import annotations
from collections.abc import Mapping
+import json
+from pathlib import Path
+import shlex
+import subprocess
+import pytest
from typing import Any
from pytest import MonkeyPatch
@@ -93,7 +98,7 @@ def test_default_quota_and_turn_envelope_expose_one_bounded_planning_horizon() -
assert envelope["compaction"]["envelope_json_bytes"] <= 8_192
-def test_single_selected_todo_does_not_add_a_redundant_horizon() -> None:
+def test_snapshot_without_goal_sources_does_not_add_a_redundant_horizon() -> None:
selected = quota_todo_item(
todo_id="todo_only_work",
index=1,
@@ -121,7 +126,7 @@ def test_single_selected_todo_does_not_add_a_redundant_horizon() -> None:
assert "planning_horizon" not in packet
-def test_flat_runnable_backlog_stays_in_action_portfolio_without_horizon() -> None:
+def test_snapshot_flat_backlog_stays_in_action_portfolio_without_horizon() -> None:
items = [
quota_todo_item(
todo_id=f"todo_flat_{index:03d}",
@@ -153,6 +158,93 @@ def test_flat_runnable_backlog_stays_in_action_portfolio_without_horizon() -> No
assert "planning_horizon" not in packet
+def test_preselection_horizon_and_replan_share_current_goal_sources(tmp_path: Path) -> None:
+ from loopx.control_plane.work_items.replan_context_codec import project_replan_context
+
+ project = tmp_path / "project with spaces"
+ project.mkdir()
+ state = project / "ACTIVE_GOAL_STATE.md"
+ objective = "Deliver the complete useful outcome. " + "Retain original constraints. " * 20 + "Final priority: accuracy."
+ state.write_text("# Goal\n\n## Objective\n> " + objective + "\n\n## Agent Todo\n- Read the local report.\n")
+ runtime = tmp_path / "runtime"
+ registry = tmp_path / "registry.json"
+ registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{
+ "id": GOAL_ID, "repo": str(project), "state_file": state.name,
+ "objective": "Registered fallback", "status": "active-read-only", "domain": "fixture",
+ }]}))
+ status = planning_horizon_strategic_context_status()
+ status.update(registry=str(registry), runtime_root=str(runtime))
+ status["attention_queue"]["items"][0]["project_asset"]["execution_profile"] = {"planning_horizon_goal": True}
+ def read_packet():
+ return build_quota_should_run(status, goal_id=GOAL_ID, agent_id=AGENT_ID,
+ turn_instance_id="turn-goal-horizon")
+
+ original = read_packet()
+ horizon = original["planning_horizon"]["core_goal"]
+ assert horizon["objective"] == objective
+ assert horizon["objective_source"] == "active_state"
+ assert horizon["objective_missing"] is False
+ assert horizon == project_replan_context(
+ goal_id=GOAL_ID, agent_id=AGENT_ID, runs=(), source_status=status,
+ )["core_goal"]
+ readback = subprocess.run(shlex.split(horizon["read_action"]), capture_output=True, text=True, check=True)
+ assert readback.stdout == state.read_text()
+ assert original["planning_horizon"]["selection_contract"]["horizon_changes_selection"] is False
+ envelope = build_turn_envelope(original)
+ assert envelope["action"]["planning_horizon"]["core_goal"] == horizon
+
+ state.write_text("# Goal\n\n## Objective\n> Current owner-corrected objective.\n")
+ corrected = read_packet()
+ assert corrected["planning_horizon"]["core_goal"]["objective"] == "Current owner-corrected objective."
+ assert corrected["selected_todo"] == original["selected_todo"]
+ assert corrected.get("action_portfolio") == original.get("action_portfolio")
+ assert corrected["should_run"] == original["should_run"]
+ assert corrected["interaction_contract"] == original["interaction_contract"]
+ assert quota_action_signature_document(corrected) != quota_action_signature_document(original)
+ state.write_text("# Goal\n")
+ fallback = read_packet()
+ assert fallback["planning_horizon"]["core_goal"]["objective"] == "Registered fallback"
+ assert fallback["planning_horizon"]["core_goal"]["objective_source"] == "registry"
+ registry_data = json.loads(registry.read_text())
+ registry_data["goals"][0].pop("objective")
+ registry.write_text(json.dumps(registry_data))
+ missing = read_packet()
+ assert missing["planning_horizon"]["core_goal"]["objective_missing"] is True
+ assert missing["planning_horizon"]["core_goal"]["objective"] is None
+
+
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
+def test_real_cli_exposes_goal_before_selection_without_granting_delivery(tmp_path, monkeypatch, provider):
+ from test_replan_successor_guard_reentry import _fixture, AGENT, GOAL
+
+ call, runtime, index = _fixture(tmp_path, monkeypatch, provider, future_monitor=False)
+ index.write_text("")
+ call("configure-goal", "--goal-id", GOAL, "--execution-planning-horizon-goal", "--execute")
+ for number in range(2):
+ call("todo", "add", "--goal-id", GOAL, "--role", "agent", "--claimed-by", AGENT,
+ "--text", f"Validate independent source {number}.", "--task-class", "advancement_task",
+ "--action-kind", "validate")
+ args = ("quota", "should-run", "--codex-app", "--goal-id", GOAL, "--agent-id", AGENT)
+ before = call(*args, "--turn-instance-id", "turn-goal-selection")
+ core = before["planning_horizon"]["core_goal"]
+ assert core["objective"] == "Deliver the independently accepted source outcome."
+ assert core["objective_source"] == "active_state"
+ assert before["interaction_contract"]["agent_channel"]["delivery_allowed"] is False
+ assert before["interaction_contract"]["cli_channel"]["selection_required"] is True
+ assert "settlement_identity" not in before["heartbeat_receipt"]
+ state = tmp_path / "project/ACTIVE_GOAL_STATE.md"
+ assert subprocess.check_output(shlex.split(core["read_action"]), text=True) == state.read_text()
+ assert build_turn_envelope(before)["action"]["planning_horizon"]["core_goal"] == core
+ state.write_text(state.read_text().replace(
+ "Deliver the independently accepted source outcome.", "Deliver the corrected whole outcome."))
+ corrected = call(*args, "--turn-instance-id", "turn-goal-corrected")
+ assert corrected["planning_horizon"]["core_goal"]["objective"] == "Deliver the corrected whole outcome."
+ assert corrected["action_portfolio"] == before["action_portfolio"]
+ assert corrected["selected_todo"] == before["selected_todo"]
+ assert corrected["interaction_contract"]["agent_channel"]["delivery_allowed"] is False
+ assert not index.read_text()
+
+
def test_agent_todo_detail_expands_claim_semantics_from_the_shared_inventory() -> None:
selected = quota_todo_item(
todo_id="todo_selected_detail",
@@ -312,3 +404,19 @@ def test_task_graph_reads_deferred_nodes_from_the_shared_planning_source() -> No
for todo_id in node["refs"].get("todo_ids", [])
}
assert deferred["todo_id"] in todo_ids
+
+
+@pytest.mark.parametrize("enabled", [False, None])
+def test_disabled_goal_projection_does_not_read_goal_sources(monkeypatch, enabled):
+ from loopx.control_plane.work_items import replan_context_codec
+ def forbidden(*args, **kwargs):
+ raise AssertionError("disabled horizon must not load Goal documents")
+ monkeypatch.setattr(replan_context_codec, "goal_context_facts", forbidden)
+ status = planning_horizon_strategic_context_status()
+ status["registry"] = "/unreadable/registry.json"
+ if enabled is not None:
+ status["attention_queue"]["items"][0]["project_asset"]["execution_profile"] = {"planning_horizon_goal": enabled}
+ packet = build_quota_should_run(status, goal_id=GOAL_ID, agent_id=AGENT_ID,
+ turn_instance_id="horizon-default-off")
+ assert "core_goal" not in packet["planning_horizon"]
+ assert packet["planning_horizon"]["selection_contract"]["horizon_changes_selection"] is False
diff --git a/tests/control_plane/test_replan_context_evidence.py b/tests/control_plane/test_replan_context_evidence.py
index 62dcfd4d7a..bcd03ff1b0 100644
--- a/tests/control_plane/test_replan_context_evidence.py
+++ b/tests/control_plane/test_replan_context_evidence.py
@@ -92,12 +92,12 @@ def test_goal_and_evidence_paths_survive_scoped_replan(
def test_scoped_goal_keeps_original_text_without_public_filtering(tmp_path: Path) -> None:
- from loopx.control_plane.work_items.replan_context_codec import _goal_facts
+ from loopx.control_plane.work_items.replan_context_codec import goal_context_facts
registry = tmp_path / "registry.json"
registry.write_text(json.dumps({"goals": [{"id": "evidence-goal",
"objective": "Inspect access_key=synthetic-secret before continuing."}]}))
- facts = _goal_facts({"registry": str(registry)}, "evidence-goal", None)
+ facts = goal_context_facts({"registry": str(registry)}, "evidence-goal", None)
assert facts["registry_objective"] == "Inspect access_key=synthetic-secret before continuing."
diff --git a/tests/control_plane_ts/planning_horizon.test.ts b/tests/control_plane_ts/planning_horizon.test.ts
index d744288462..812daa2b93 100644
--- a/tests/control_plane_ts/planning_horizon.test.ts
+++ b/tests/control_plane_ts/planning_horizon.test.ts
@@ -58,9 +58,31 @@ function request(overrides: Record = {}) {
source_context_todo_count: legacy.source_context_todo_count,
}),
acceptance_gaps: legacy.acceptance_gaps,
+ ...(overrides.goal_facts === undefined ? {} : {goal_facts: overrides.goal_facts}),
};
}
+test("one local task still carries the original Goal and separately scoped acceptance", () => {
+ const objective = "Deliver the whole accepted outcome. ".repeat(20) + "Preserve the final constraint.";
+ const result = projectQuotaPlanningHorizon(request({goal_facts: {
+ active_state_objective: objective,
+ registry_objective: "Older registered objective",
+ goal_read_action: "cat -- 'source with spaces/ACTIVE_GOAL_STATE.md'",
+ acceptance_contract: {objective: "Validate this slice only", scope: {kind: "selected_work"}},
+ }}));
+ assert.deepEqual(result?.core_goal, {
+ objective, objective_source: "active_state", objective_missing: false,
+ read_action: "cat -- 'source with spaces/ACTIVE_GOAL_STATE.md'",
+ acceptance_contract: {objective: "Validate this slice only", scope: {kind: "selected_work"}},
+ });
+ assert.equal(result?.selected_todo_id, "todo_regression_gate");
+ assert.equal((result?.selection_contract as Record).horizon_changes_selection, false);
+ assert.throws(() => projectQuotaPlanningHorizon(request({goal_facts: {active_state_objective: 42}})));
+ assert.deepEqual(projectQuotaPlanningHorizon(request({goal_facts: {}}))?.core_goal, {
+ objective: null, objective_source: null, objective_missing: true, acceptance_contract: null,
+ });
+});
+
test("planning horizon preserves the typed strategic chain around local selected work", () => {
const facts = todo("todo_facts_source", 1, "P0", {
status: "deferred",
diff --git a/tests/test_chat_goal_configuration_api.py b/tests/test_chat_goal_configuration_api.py
index 6172238005..5d3504a11a 100644
--- a/tests/test_chat_goal_configuration_api.py
+++ b/tests/test_chat_goal_configuration_api.py
@@ -324,6 +324,7 @@ def test_machine_inheritable_goal_capabilities_can_clear_their_overrides() -> No
"clear_execution_replan_after_todos": True,
"clear_execution_replan_after_turns": True,
"clear_execution_stride_strategy": True,
+ "clear_execution_planning_horizon_goal": True,
}
assert _goal_capability_options("change_quality_qualification", None) == {
"clear_change_quality_configuration": True