From cf10d316375ba729cbb8983c708046397be4ea45 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sat, 24 Oct 2026 08:10:34 +0800 Subject: [PATCH 1/2] fix: align autonomous replan examples with durable vision rules Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../control_plane/goals/vision_checkpoint.ts | 10 ++- .../work_items/replan_semantics.ts | 6 +- tests/control_plane/test_vision_budget_cli.py | 69 ++++++++++++++++++- .../control_plane_ts/replan_semantics.test.ts | 4 +- .../vision_checkpoint.test.ts | 18 +++++ 5 files changed, 98 insertions(+), 9 deletions(-) diff --git a/loopx/control_plane/goals/vision_checkpoint.ts b/loopx/control_plane/goals/vision_checkpoint.ts index d4a11500d1..9ce339097d 100644 --- a/loopx/control_plane/goals/vision_checkpoint.ts +++ b/loopx/control_plane/goals/vision_checkpoint.ts @@ -75,7 +75,7 @@ export const VISION_MATERIAL_CLOSEOUT_HINT = "Checkpoint satisfied alone does not qualify the outcome."; /** Authoring hints share the validator's limits; they grant no transition authority. */ -export function visionAuthoringContract(): JsonObject { +export function visionAuthoringContract(autonomousReplan = false): JsonObject { return { schema_version: GOAL_VISION_REPLAN_SCHEMA_VERSION, fields: { @@ -92,7 +92,7 @@ export function visionAuthoringContract(): JsonObject { schema_version: GOAL_VISION_REPLAN_SCHEMA_VERSION, state: "vision_patch_proposed", vision_patch: {acceptance_summary: "Verified result; scoped work remains."}, path_delta: { - outcome: "continue", prior_assumption: "Viable route.", + outcome: autonomousReplan ? "replan" : "continue", prior_assumption: "Viable route.", observed_reality: "Evidence supports the route.", retained: ["Route"], evidence_refs: ["evidence:verified-result"], }, @@ -115,7 +115,11 @@ export function visionAuthoringContract(): JsonObject { ([field, [maxItems, maxChars]]) => [field, {item_type: "string", max_items: maxItems, max_item_chars: maxChars}], )), }, - rule: "Material closeout: acceptance_summary + evidence-linked path_delta. Changed mainline: outcome=replan. vision_closed closes a stage, not Goal; no_followup: no scoped work.", + rule: "Material closeout: acceptance_summary + evidence-linked path_delta. " + + (autonomousReplan + ? `Autonomous review: changing ${GOAL_VISION_DURABLE_FIELDS.join(", ")} requires outcome=replan, even when the Goal objective stays unchanged. Retaining these fields permits continue/no_change. ` + : "Changed mainline: outcome=replan. ") + + "vision_closed closes a stage, not Goal; no_followup: no scoped work.", }; } // Bounded typed fallback declarations survive prepare unchanged so the diff --git a/loopx/control_plane/work_items/replan_semantics.ts b/loopx/control_plane/work_items/replan_semantics.ts index 13037aaf74..15baff94f6 100644 --- a/loopx/control_plane/work_items/replan_semantics.ts +++ b/loopx/control_plane/work_items/replan_semantics.ts @@ -198,7 +198,7 @@ function lifecycleObligation(request: JsonObject): JsonObject | null { decision: "vision_outcome_checkpoint_required", settlement_only: true, required_outcome: "durable_writeback_receipt", semantic_delta: null, writeback_contract: {preferred_input: "evidence_linked_vision_path", - cli_semantic_args: VISION_CLI_ARGS, vision_authoring: visionAuthoringContract(), + cli_semantic_args: VISION_CLI_ARGS, vision_authoring: visionAuthoringContract(true), required_fields: ["vision_patch.acceptance_summary", "path_delta.outcome", "path_delta.evidence_refs"], path_outcomes: [...FRESH_PATH_DISPOSITIONS], rule: reason}, }}; @@ -321,7 +321,7 @@ function writebackProjection(required: SemanticOutcome[], externalReview: boolea preferred_input: "typed_progress_observation", alternative_input: "evidence_linked_vision_path", alternative_cli_semantic_args: VISION_CLI_ARGS, - vision_authoring: visionAuthoringContract(), + vision_authoring: visionAuthoringContract(true), path_outcomes: [...FRESH_PATH_DISPOSITIONS], identity_outcomes_require_new_evidence: true, rule: "Consecutive evaluated transitions did not serve the goal contract. Discharge with a typed observation whose new surface, hypothesis or probe family cites at least one evidence id absent from every claim in the obligation window, a new concrete blocker, or coverage-backed terminal state; or author an evidence-linked vision path (continue, no_change or replan) from observed evidence when the plan should stand. Renamed identifiers over the window's evidence ids, or a claim already made in the window, do not discharge.", @@ -333,7 +333,7 @@ function writebackProjection(required: SemanticOutcome[], externalReview: boolea cli_semantic_args: VISION_CLI_ARGS, writeback_contract: { preferred_input: "evidence_linked_vision_path", - vision_authoring: visionAuthoringContract(), + vision_authoring: visionAuthoringContract(true), required_fields: ["vision_patch.acceptance_summary", "path_delta.outcome", "path_delta.evidence_refs"], path_outcomes: [...FRESH_PATH_DISPOSITIONS], rule: "First reuse observed evidence valid for current source/acceptance; probe missing, stale or insufficient evidence. Write a JSON file with acceptance summary and evidence-linked path outcome, then run bound refresh and spend. An unchanged reason alone is insufficient. Explicit validation gates and other required_any_of contracts apply.", diff --git a/tests/control_plane/test_vision_budget_cli.py b/tests/control_plane/test_vision_budget_cli.py index e765dab9a1..5f37d5e214 100644 --- a/tests/control_plane/test_vision_budget_cli.py +++ b/tests/control_plane/test_vision_budget_cli.py @@ -8,6 +8,68 @@ import pytest +from loopx.control_plane.testing.canary_harness import import_fixture_authority + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_projected_autonomous_example_updates_evidence_without_changing_goal(tmp_path, monkeypatch, provider): + from canonical_authority_fixture import isolate_sqlite_runtime + + if provider == "sqlite": + isolate_sqlite_runtime(tmp_path, monkeypatch) + source = Path(__file__).resolve().parents[2] / "examples/project/goal-vision-refresh-state-budget-smoke.py" + spec = importlib.util.spec_from_file_location("replan_authoring_fixture", source) + fixture = importlib.util.module_from_spec(spec) + spec.loader.exec_module(fixture) + registry, runtime, project = fixture.write_fixture(tmp_path) + state = project / ".codex/goals" / fixture.GOAL_ID / "ACTIVE_GOAL_STATE.md" + original_goal = "## Goal\n\nPreserve the original objective and verify each scoped result.\n\n" + state.write_text(state.read_text().replace("## Next Action", original_goal + "## Next Action")) + import_fixture_authority(registry, fixture.GOAL_ID, provider=provider) + document = tmp_path / "vision.json" + fixture.write_json(document, {"vision_patch": {"acceptance_summary": "Original evidence pending."}}) + fixture.run_cli(registry, runtime, vision_path=document, check=True, + dry_run=False, autonomous_replan_recorded=False) + + guard = fixture.run_quota(registry, runtime) + assert guard["effective_action"] == "autonomous_replan_required" + assert guard["requires_user_action"] is False + contract = guard["replan_action_packet"]["writeback_contract"]["vision_authoring"] + packet = copy.deepcopy(contract["minimal_example"]) + fixture.write_json(document, packet) + result = fixture.run_cli(registry, runtime, vision_path=document, + check=False, dry_run=False, autonomous_replan_recorded=True) + accepted = fixture.payload(result) + assert result.returncode == 0, accepted.get("error") or accepted + assert accepted["ok"] and accepted["appended"] + assert accepted["agent_vision"]["path_delta"]["outcome"] == "replan" + assert original_goal in state.read_text() + + # Updating evidence is still a durable change under the existing rule. + # A caller replacing the projected disposition must fail before writing. + index = runtime / "goals" / fixture.GOAL_ID / "runs/index.jsonl" + before = index.read_bytes() + packet["vision_patch"]["acceptance_summary"] = "Another evidence update." + packet["path_delta"]["outcome"] = "continue" + fixture.write_json(document, packet) + rejected = fixture.run_cli(registry, runtime, vision_path=document, + check=False, dry_run=False, autonomous_replan_recorded=True) + assert rejected.returncode == 1 + assert "changes durable fields acceptance_summary" in fixture.payload(rejected)["error"] + assert index.read_bytes() == before + + # A review that retains the durable fields may continue without changing + # the Goal, asking its owner, or inventing a direction change. + packet["vision_patch"] = accepted["agent_vision"]["vision_patch"] + for disposition in ("continue", "no_change"): + packet["path_delta"]["outcome"] = disposition + fixture.write_json(document, packet) + retained = fixture.payload(fixture.run_cli(registry, runtime, vision_path=document, + check=True, dry_run=False, autonomous_replan_recorded=True)) + assert retained["agent_vision"]["vision_patch"] == packet["vision_patch"] + assert retained["agent_vision"]["path_delta"]["outcome"] == disposition + assert original_goal in state.read_text() + @pytest.mark.parametrize("character", ["x", "界"]) def test_full_budget_roundtrips_without_erasing_replan_or_partial_writes(tmp_path, character): @@ -16,6 +78,7 @@ def test_full_budget_roundtrips_without_erasing_replan_or_partial_writes(tmp_pat fixture = importlib.util.module_from_spec(spec) spec.loader.exec_module(fixture) registry, runtime, project = fixture.write_fixture(tmp_path / "current") + import_fixture_authority(registry, fixture.GOAL_ID, provider="file") packet = { "state": "vision_closed", "vision_patch": { @@ -34,6 +97,7 @@ def test_full_budget_roundtrips_without_erasing_replan_or_partial_writes(tmp_pat baseline["path_delta"]["prior_assumption"] = character * 220 baseline["path_delta"]["observed_reality"] = character * 220 baseline_registry, baseline_runtime, _ = fixture.write_fixture(tmp_path / "control") + import_fixture_authority(baseline_registry, fixture.GOAL_ID, provider="file") fixture.write_json(path, baseline) fixture.run_cli(baseline_registry, baseline_runtime, vision_path=path, check=True, dry_run=False, autonomous_replan_recorded=False) @@ -78,6 +142,7 @@ def test_misplaced_delta_rejects_before_write_and_corrected_packet_roundtrips(tm fixture = importlib.util.module_from_spec(spec) spec.loader.exec_module(fixture) registry, runtime, project = fixture.write_fixture(tmp_path) + import_fixture_authority(registry, fixture.GOAL_ID, provider="file") state = project / ".codex/goals" / fixture.GOAL_ID / "ACTIVE_GOAL_STATE.md" before = state.read_bytes() delta = {"schema_version": "goal_path_delta_v0", "outcome": "replan", @@ -112,6 +177,7 @@ def test_authoring_example_material_closeout_has_no_missing_path_replan(tmp_path fixture = importlib.util.module_from_spec(spec) spec.loader.exec_module(fixture) registry, runtime, _ = fixture.write_fixture(tmp_path) + import_fixture_authority(registry, fixture.GOAL_ID, provider="file") contract = replan_writeback_requirements({ "satisfying_semantic_outcomes": ["fresh_vision_path_outcome"], })["writeback_contract"]["vision_authoring"] @@ -139,7 +205,7 @@ def test_authoring_example_material_closeout_has_no_missing_path_replan(tmp_path assert checks["path_outcome_valid"] is (omitted == "evidence_refs") else: persisted = result["agent_vision"] - assert persisted["path_delta"]["outcome"] == "continue" + assert persisted["path_delta"]["outcome"] == "replan" assert persisted["path_delta"]["evidence_refs"] @@ -149,6 +215,7 @@ def test_historical_stride_probe_whitespace_roundtrips_with_strategy_off(tmp_pat fixture = importlib.util.module_from_spec(spec) spec.loader.exec_module(fixture) registry, runtime, _ = fixture.write_fixture(tmp_path) + import_fixture_authority(registry, fixture.GOAL_ID, provider="file") expected = {"scope": "One complete experiment.", "prediction": "Distinguish the remaining explanations.", "adoption_criterion": "Retain a validated gain.", "cost_bound": "One bounded comparison."} packet = {"vision_patch": {"vision_summary": "Preserve the historical proposal."}, diff --git a/tests/control_plane_ts/replan_semantics.test.ts b/tests/control_plane_ts/replan_semantics.test.ts index cfc41987a1..0657cff1d4 100644 --- a/tests/control_plane_ts/replan_semantics.test.ts +++ b/tests/control_plane_ts/replan_semantics.test.ts @@ -126,7 +126,7 @@ test("verified original writeback projects only its outstanding debit even after test("obligation source governs both authoring projection and semantic discharge", () => { const projection = projectReplanSemantics({operation: "requirements", obligation}); assert.match(String(projection.cli_semantic_args), /--agent-vision-json/); - assert.deepEqual((projection.writeback_contract as Record).vision_authoring, visionAuthoringContract()); + assert.deepEqual((projection.writeback_contract as Record).vision_authoring, visionAuthoringContract(true)); assert.equal(projectReplanSemantics({operation: "qualify", obligation, observation_delta: {delta_kinds: ["new_surface"]}}).accepted, false); const delta = projectReplanSemantics({operation: "qualify", obligation, agent_vision: vision}); @@ -225,7 +225,7 @@ test("external progress review: renamed identifiers discharge only behind new ev const contract = projection.writeback_contract as Record; assert.equal(contract.identity_outcomes_require_new_evidence, true); assert.match(String(contract.alternative_cli_semantic_args), /--agent-vision-json/); - assert.deepEqual(contract.vision_authoring, visionAuthoringContract()); + assert.deepEqual(contract.vision_authoring, visionAuthoringContract(true)); const qualify = (observation_delta?: Record, agent_vision?: Record) => projectReplanSemantics({operation: "qualify", obligation: review, observation_delta, agent_vision}); diff --git a/tests/control_plane_ts/vision_checkpoint.test.ts b/tests/control_plane_ts/vision_checkpoint.test.ts index 6821af34bb..75be1443dc 100644 --- a/tests/control_plane_ts/vision_checkpoint.test.ts +++ b/tests/control_plane_ts/vision_checkpoint.test.ts @@ -45,6 +45,24 @@ function prepareRequest(overrides: Record = {}) { }; } +test("authoring examples distinguish ordinary continuation from autonomous durable changes", () => { + const existing = {vision_patch: {acceptance_summary: "Original evidence pending."}}; + for (const autonomous of [false, true]) { + const packet = visionAuthoringContract(autonomous).minimal_example as Record; + const result = buildVisionCheckpoint(prepareRequest({ + agent_vision_packet: packet, existing_agent_vision: existing, + require_path_delta_for_durable_change: autonomous, + })).agent_vision as Record; + assert.equal((result.path_delta as Record).outcome, autonomous ? "replan" : "continue"); + assert.deepEqual(result.vision_patch, packet.vision_patch); + } + const ordinary = visionAuthoringContract().minimal_example; + assert.throws(() => buildVisionCheckpoint(prepareRequest({ + agent_vision_packet: ordinary, existing_agent_vision: existing, + require_path_delta_for_durable_change: true, + })), /changes durable fields acceptance_summary/); +}); + test("prepare owns packet normalization, budgets, and path delta", () => { const result = buildVisionCheckpoint(prepareRequest({ agent_vision_packet: { From 627c02291946d5f42d982658be90feed9ef16e99 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:37:00 +0800 Subject: [PATCH 2/2] fix: preserve current Vision in autonomous JSON authoring guidance Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../control_plane/goals/vision_checkpoint.ts | 6 +++ tests/control_plane/test_vision_budget_cli.py | 41 +++++++++++++++++-- .../vision_checkpoint.test.ts | 35 ++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/loopx/control_plane/goals/vision_checkpoint.ts b/loopx/control_plane/goals/vision_checkpoint.ts index 9ce339097d..340e838e24 100644 --- a/loopx/control_plane/goals/vision_checkpoint.ts +++ b/loopx/control_plane/goals/vision_checkpoint.ts @@ -101,6 +101,12 @@ export function visionAuthoringContract(autonomousReplan = false): JsonObject { "For todo_delta, use retain:todo_ for existing work; supported verbs are activate, create, reopen, resume and retain. " + "Replace todo_ with a verified Todo ID after normal authoring/readback. Entries record plans, not Todo mutations, claims or execution authority. " + "For ordinary CLI writeback, pass the packet with --agent-vision-json . " + + (autonomousReplan + ? "JSON replaces the complete packet; the sparse minimal_example is not an update to an existing Vision. " + + "Start from the current Agent's complete packet and full vision_patch, then replace only deliberately reviewed fields and the fresh path_delta. " + + "Preserve every other field and the current state; path_delta.retained text does not merge omitted fields. " + + "Reuse the known current packet, or read it from canonical status/history if unavailable; never reconstruct missing intent from summaries. " + : "") + "checkpoint-context is only for recovery after the original committed Turn writeback; " + "follow its returned same-Turn recovery action, not a fresh-turn preflight.", total_text_limit: GOAL_VISION_TOTAL_LIMIT, diff --git a/tests/control_plane/test_vision_budget_cli.py b/tests/control_plane/test_vision_budget_cli.py index 5f37d5e214..05d70ff5aa 100644 --- a/tests/control_plane/test_vision_budget_cli.py +++ b/tests/control_plane/test_vision_budget_cli.py @@ -12,7 +12,10 @@ @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_projected_autonomous_example_updates_evidence_without_changing_goal(tmp_path, monkeypatch, provider): +@pytest.mark.parametrize("complete_prior", [False, True]) +def test_projected_autonomous_example_updates_evidence_without_changing_goal( + tmp_path, monkeypatch, provider, complete_prior, +): from canonical_authority_fixture import isolate_sqlite_runtime if provider == "sqlite": @@ -27,7 +30,17 @@ def test_projected_autonomous_example_updates_evidence_without_changing_goal(tmp state.write_text(state.read_text().replace("## Next Action", original_goal + "## Next Action")) import_fixture_authority(registry, fixture.GOAL_ID, provider=provider) document = tmp_path / "vision.json" - fixture.write_json(document, {"vision_patch": {"acceptance_summary": "Original evidence pending."}}) + prior_patch = {"acceptance_summary": "Original evidence pending."} + if complete_prior: + prior_patch.update({ + "vision_summary": "Verify the whole original delivery with evidence.", + "role_scope": "Own this bounded verification; do not change peer work.", + "advancement_policy": "repeat_until_closed", + "replan_trigger_summary": "Keep the remaining evidence frontier visible.", + "dreaming_policy": "Propose only evidence-backed alternatives.", + "last_patch_summary": "Retain the existing role and continuation intent.", + }) + fixture.write_json(document, {"vision_patch": prior_patch}) fixture.run_cli(registry, runtime, vision_path=document, check=True, dry_run=False, autonomous_replan_recorded=False) @@ -36,6 +49,18 @@ def test_projected_autonomous_example_updates_evidence_without_changing_goal(tmp assert guard["requires_user_action"] is False contract = guard["replan_action_packet"]["writeback_contract"]["vision_authoring"] packet = copy.deepcopy(contract["minimal_example"]) + assert "JSON replaces the complete packet" in contract["authoring_hint"] + assert "full vision_patch" in contract["authoring_hint"] + if complete_prior: + # Follow the returned authoring procedure using the complete current + # Agent packet from a separate read, not fields reconstructed by a model. + current = fixture.run_status(registry, runtime) + goal = next(g for g in current["run_history"]["goals"] if g["id"] == fixture.GOAL_ID) + current_packet = copy.deepcopy(goal["latest_runs"][0]["agent_vision"]) + assert current_packet["agent_id"] == fixture.AGENT_ID + current_packet["vision_patch"].update(packet["vision_patch"]) + current_packet["path_delta"] = packet["path_delta"] + packet = current_packet fixture.write_json(document, packet) result = fixture.run_cli(registry, runtime, vision_path=document, check=False, dry_run=False, autonomous_replan_recorded=True) @@ -45,9 +70,19 @@ def test_projected_autonomous_example_updates_evidence_without_changing_goal(tmp assert accepted["agent_vision"]["path_delta"]["outcome"] == "replan" assert original_goal in state.read_text() + # A separate CLI read must preserve every existing field, not merely echo + # an accepted summary while discarding direction, role or repeat policy. + expected_patch = prior_patch | packet["vision_patch"] + cold = fixture.run_status(registry, runtime) + goal = next(g for g in cold["run_history"]["goals"] if g["id"] == fixture.GOAL_ID) + assert goal["latest_runs"][0]["agent_vision"]["vision_patch"] == expected_patch + index = runtime / "goals" / fixture.GOAL_ID / "runs/index.jsonl" + assert json.loads(index.read_text().splitlines()[-1])["agent_vision"]["vision_patch"] == expected_patch + if complete_prior: + assert goal["latest_runs"][0]["agent_vision"]["state"] == current_packet["state"] + # Updating evidence is still a durable change under the existing rule. # A caller replacing the projected disposition must fail before writing. - index = runtime / "goals" / fixture.GOAL_ID / "runs/index.jsonl" before = index.read_bytes() packet["vision_patch"]["acceptance_summary"] = "Another evidence update." packet["path_delta"]["outcome"] = "continue" diff --git a/tests/control_plane_ts/vision_checkpoint.test.ts b/tests/control_plane_ts/vision_checkpoint.test.ts index 75be1443dc..fd03e1e99d 100644 --- a/tests/control_plane_ts/vision_checkpoint.test.ts +++ b/tests/control_plane_ts/vision_checkpoint.test.ts @@ -63,6 +63,41 @@ test("authoring examples distinguish ordinary continuation from autonomous durab })), /changes durable fields acceptance_summary/); }); +test("autonomous JSON authoring retains the complete current packet before changing evidence", () => { + const contract = visionAuthoringContract(true); + assert.match(String(contract.authoring_hint), /JSON replaces the complete packet/); + assert.match(String(contract.authoring_hint), /current Agent's complete packet and full vision_patch/); + assert.match(String(contract.authoring_hint), /path_delta.retained text does not merge omitted fields/); + assert.doesNotMatch(String(visionAuthoringContract().authoring_hint), /JSON replaces/); + const existing = { + state: "active", todo_delta: ["retain:todo_current001"], + vision_patch: { + vision_summary: "Verify the whole delivery.", role_scope: "Own only the current role.", + advancement_policy: "repeat_until_closed", acceptance_summary: "Original evidence pending.", + dreaming_policy: "Propose bounded alternatives.", + }, + }; + const example = contract.minimal_example as Record; + // The documented procedure copies known intent and overlays deliberate edits; + // replacement semantics and the existing durable-change validator stay intact. + const packet = {...existing, + vision_patch: {...existing.vision_patch, ...(example.vision_patch as Record)}, + path_delta: example.path_delta}; + const result = buildVisionCheckpoint(prepareRequest({agent_vision_packet: packet, + existing_agent_vision: existing, require_path_delta_for_durable_change: true, + })).agent_vision as Record; + assert.deepEqual(result.vision_patch, {...existing.vision_patch, + acceptance_summary: "Verified result; scoped work remains."}); + assert.equal(result.state, existing.state); + assert.deepEqual(result.todo_delta, existing.todo_delta); + // Sparse JSON remains a replacement: fixing guidance must not silently change + // callers that intentionally replace a packet or weaken the validator. + const sparse = buildVisionCheckpoint(prepareRequest({agent_vision_packet: example, + existing_agent_vision: existing, require_path_delta_for_durable_change: true, + })).agent_vision as Record; + assert.deepEqual(sparse.vision_patch, example.vision_patch); +}); + test("prepare owns packet normalization, budgets, and path delta", () => { const result = buildVisionCheckpoint(prepareRequest({ agent_vision_packet: {