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
16 changes: 13 additions & 3 deletions loopx/control_plane/goals/vision_checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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.",
Comment thread
loopx-agent marked this conversation as resolved.
observed_reality: "Evidence supports the route.", retained: ["Route"],
evidence_refs: ["evidence:verified-result"],
},
Expand All @@ -101,6 +101,12 @@ export function visionAuthoringContract(): JsonObject {
"For todo_delta, use retain:todo_<id> for existing work; supported verbs are activate, create, reopen, resume and retain. " +
"Replace todo_<id> 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 <file>. " +
(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,
Expand All @@ -115,7 +121,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
Expand Down
6 changes: 3 additions & 3 deletions loopx/control_plane/work_items/replan_semantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}};
Expand Down Expand Up @@ -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.",
Expand All @@ -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.",
Expand Down
104 changes: 103 additions & 1 deletion tests/control_plane/test_vision_budget_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,103 @@

import pytest

from loopx.control_plane.testing.canary_harness import import_fixture_authority


@pytest.mark.parametrize("provider", ["file", "sqlite"])
@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":
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"
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)

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"])
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)
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()

# 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.
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):
Expand All @@ -16,6 +113,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": {
Expand All @@ -34,6 +132,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)
Expand Down Expand Up @@ -78,6 +177,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",
Expand Down Expand Up @@ -112,6 +212,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"]
Expand Down Expand Up @@ -139,7 +240,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"]


Expand All @@ -149,6 +250,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."},
Expand Down
4 changes: 2 additions & 2 deletions tests/control_plane_ts/replan_semantics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>).vision_authoring, visionAuthoringContract());
assert.deepEqual((projection.writeback_contract as Record<string, unknown>).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});
Expand Down Expand Up @@ -225,7 +225,7 @@ test("external progress review: renamed identifiers discharge only behind new ev
const contract = projection.writeback_contract as Record<string, unknown>;
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<string, unknown>, agent_vision?: Record<string, unknown>) =>
projectReplanSemantics({operation: "qualify", obligation: review, observation_delta, agent_vision});
Expand Down
53 changes: 53 additions & 0 deletions tests/control_plane_ts/vision_checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,59 @@ function prepareRequest(overrides: Record<string, unknown> = {}) {
};
}

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<string, unknown>;
const result = buildVisionCheckpoint(prepareRequest({
agent_vision_packet: packet, existing_agent_vision: existing,
require_path_delta_for_durable_change: autonomous,
})).agent_vision as Record<string, unknown>;
assert.equal((result.path_delta as Record<string, unknown>).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("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<string, unknown>;
// 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<string, unknown>)},
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<string, unknown>;
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<string, unknown>;
assert.deepEqual(sparse.vision_patch, example.vision_patch);
});

test("prepare owns packet normalization, budgets, and path delta", () => {
const result = buildVisionCheckpoint(prepareRequest({
agent_vision_packet: {
Expand Down
Loading