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
11 changes: 10 additions & 1 deletion loopx/control_plane/work_items/interaction_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,16 @@ function interactionRequiredReads(request: JsonObject): JsonObject {
const prefix = requireNonEmptyString(request.command_prefix, "command_prefix");
const goalArg = shellQuote(goalId);
const add = (command: string, source: string, reason: string) => {
if (!reads.some(read => read.command === command)) reads.push({command, source, reason, ordering: "before_work"});
const existing = reads.find(read => read.command === command);
if (!existing) {
reads.push({command, source, reason, ordering: "before_work"});
return;
}
// One command is one read, but it may carry multiple independently
// applicable obligations. Merge guidance without duplicating replay.
const current = typeof existing.reason === "string" ? existing.reason : "";
if (!current) existing.reason = reason;
else if (!current.includes(reason)) existing.reason = `${current} ${reason}`;
};
const stateFile = readArgument(request.goal_state_file, "goal_state_file");
if (stateFile) add(`cat -- ${shellQuote(stateFile)}`, "goal_state",
Expand Down
68 changes: 68 additions & 0 deletions tests/control_plane/test_goal_objective_revision.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Real CLI and conversation effects; scripted callers are not model evidence."""
import json

import pytest

from test_goal_acceptance_cli import acceptance_goal as acceptance_goal
from test_goal_acceptance_prepared_action import conversation_check as conversation_check
from loopx.chat_action_store import ChatActionStore
Expand All @@ -16,6 +18,72 @@ def prepare_source(project):
path.write_text(json.dumps(registry))


def apply_objective(project, cli, objective):
document = {"objective": objective, "reason": "Keep the current direction explicit", "source_ref": "message:owner"}
path = project / "objective.json"
path.write_text(json.dumps(document))
code, prepared = cli("prepare-objective", "--agent-id", "agent-a", "--document", str(path),
"--operation-id", "prepare-guidance", "--execute")
assert code == 0 and prepared["proposal"]["status"] == "preview_ready", prepared
service = ChatActionService(
store=ChatActionStore(project.parent / "runtime" / "chat" / "actions"),
registry_path=project.parent / "registry.json",
)
assert service.apply(prepared["proposal"]["proposal_id"])["proposal"]["status"] == "applied"


@pytest.mark.parametrize("acceptance_kind", ["configured-checks", "cumulative-outcome"])
def test_objective_and_acceptance_guidance_share_real_inline_goal_read(acceptance_goal, acceptance_kind):
project, acceptance_file, cli, run = acceptance_goal
prepare_source(project)
apply_objective(project, cli, "Maintain the export with complete acceptance")
current = cli("inspect")[1]
if acceptance_kind == "configured-checks":
code, configured = cli("configure", "--document", str(acceptance_file), "--operation-id", "guidance-checks",
"--expected-provider-revision", current["provider_revision"], "--execute")
assert code == 0 and configured["status"] == "applied", configured
else:
outcome = {"outcome_id": "tour", "owner_agent_id": "agent-a",
"objective": "Maintain the export with complete acceptance",
"requirements": [{"id": "export", "description": "Retain the exported artifact"}],
"todo_ids": ["todo_export"],
"source": {"ref": "message:outcome", "summary": "The Goal requires the export artifact"}}
outcome_path = project / "outcome.json"
outcome_path.write_text(json.dumps(outcome))
current = cli("inspect")[1]
code, refined = cli("refine", "--agent-id", "agent-a", "--document", str(outcome_path),
"--operation-id", "guidance-outcome", "--expected-provider-revision",
current["provider_revision"], "--execute")
assert code == 0 and refined["status"] == "applied", refined

before = cli("inspect", "--agent-id", "agent-a")[1]
objective = before["goal_acceptance_contract"]["goal_objective"]
adoption_path = project / "adoption.json"
adoption_path.write_text(json.dumps({"revision": objective["revision"], "digest": objective["digest"],
"work_digest": before["objective_work_digest"],
"review_summary": "Reviewed the export and current acceptance before adopting the revision."}))
code, adopted = cli("adopt-objective", "--agent-id", "agent-a", "--document", str(adoption_path),
"--operation-id", "guidance-adoption", "--expected-provider-revision",
before["provider_revision"], "--execute")
assert code == 0, adopted

admission = run("quota", "should-run", "--codex-app", "--goal-id", "goal-acceptance", "--agent-id", "agent-a")[1]
context = admission["interaction_contract"]["agent_channel"]["work_context"]
source = next(row for row in context["sources"] if row["source"] == "goal_acceptance")
reason = source["reason"]
assert "revision/digest" in reason and "own objective_work_digest" in reason
required = [
clause for clause in (
("cumulative requirements", "switching work", "Agent review", "Scoped acceptance never replaces Goal intent")
if acceptance_kind == "cumulative-outcome"
else ("non-goals", "criteria", "scope/revision", "Scoped acceptance does not replace Goal intent")
)
]
assert all(clause in reason for clause in required), reason
reads = admission["interaction_contract"]["agent_channel"]["required_reads"]
assert not any(read["source"] == "goal_acceptance" for read in reads), "one inline read should fulfill the command"


def test_native_proposal_owner_apply_and_separate_peer_adoption(acceptance_goal, monkeypatch):
project, _, cli, run = acceptance_goal
prepare_source(project)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def observe(method, params, **kwargs):
assert all(not directory.exists() for directory in snapshot_directories)
assert reads == [read, {
"command": "cat -- " + shlex.quote(state_file), "source": "goal_state", "ordering": "before_work",
"reason": "Read complete Goal intent, acceptance and stops. Exact Todo reads own task state/claims. Source failure or changed requirements require a fresh guard; summaries cannot replace this read.",
"reason": "Read complete Goal intent, acceptance and stops. Exact Todo reads own task state/claims. Source failure or changed requirements require a fresh guard; summaries cannot replace this read. Only when explicit user direction invalidates the objective, use goal-acceptance prepare-objective; reuse pending proposals. Progress, diagnostics or age need no revision; documents/peers grant no authority.",
}, {
"command": "loopx --format json goal-acceptance inspect --goal-id " + shlex.quote(goal_id),
"source": "goal_acceptance", "ordering": "before_work",
Expand Down
67 changes: 67 additions & 0 deletions tests/control_plane_ts/interaction_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,73 @@ test("shared reads retain all Goal sources before exact current work and existin
assert.deepEqual(projectInteractionRequiredReads(facts).required_reads, reads);
});

test("one Goal acceptance read retains every applicable objective and acceptance obligation", () => {
const objectiveClauses = [
"revision/digest",
"own objective_work_digest",
"adoption",
"Preserve root acceptance, permissions and stops",
];
const outcomeClauses = [
"cumulative requirements",
"switching work",
"Agent review",
"Scoped acceptance never replaces Goal intent",
"fresh guard",
];
const checkClauses = [
"non-goals",
"criteria",
"scope/revision",
"Scoped acceptance does not replace Goal intent",
"fresh guard",
];
const cases = [
{goal_objective_present: false, goal_outcomes_present: true, checks: false, clauses: outcomeClauses},
{goal_objective_present: false, goal_outcomes_present: false, checks: true, clauses: checkClauses},
{goal_objective_present: true, goal_outcomes_present: true, checks: false,
clauses: [...objectiveClauses, ...outcomeClauses]},
{goal_objective_present: true, goal_outcomes_present: false, checks: true,
clauses: [...objectiveClauses, ...checkClauses]},
];
for (const current of cases) {
const facts: JsonObject = {...readFacts(), goal_state_file: null, selected_todo: null,
goal_objective_present: current.goal_objective_present,
goal_outcomes_present: current.goal_outcomes_present,
goal_acceptance_checks_present: current.checks};
const projected = projectInteractionRequiredReads(facts);
const reads = projected.required_reads as JsonObject[];
const acceptanceReads = reads.filter(read => read.source === "goal_acceptance");
assert.equal(acceptanceReads.length, 1, JSON.stringify(current));
const reason = String(acceptanceReads[0]!.reason);
for (const clause of current.clauses) assert.ok(reason.includes(clause), `${clause}: ${reason}`);

const replay = projectInteractionRequiredReads({...facts, required_reads: reads});
assert.deepEqual(replay.required_reads, reads, "repeated projection must not duplicate guidance");

const inline = projectInteractionWorkContext({
required_reads: acceptanceReads,
source_results: [{command: acceptanceReads[0]!.command, content: {
goal_acceptance_contract: {goal_objective: {revision: 1}, criteria: [{id: "check"}]},
}}],
});
assert.deepEqual(inline.required_reads, []);
const source = ((inline.work_context as JsonObject).sources as JsonObject[])[0]!;
assert.equal(source.reason, reason);
}

const facts: JsonObject = {...readFacts(), goal_state_file: null, selected_todo: null,
goal_objective_present: true, goal_outcomes_present: false, goal_acceptance_checks_present: true};
const inspect = `${String(facts.command_prefix)} --format json goal-acceptance inspect --goal-id goal-requirements`;
const existing = {command: inspect, source: "goal_acceptance", reason: "Retain this existing acceptance obligation."};
const merged = (projectInteractionRequiredReads({...facts, required_reads: [existing]}).required_reads as JsonObject[]);
assert.equal(merged.length, 1);
assert.equal(merged[0]!.command, inspect);
assert.ok(String(merged[0]!.reason).startsWith(existing.reason));
assert.ok(String(merged[0]!.reason).includes(checkClauses[0]));
assert.ok(String(merged[0]!.reason).includes(objectiveClauses[0]));
});

test("shared requirement reads use final admission and add no work on non-delivery lanes", () => {
for (const patch of [{should_run: false}, {delivery_allowed: false}, {selection_required: true},
{settlement_only: true, has_replan: true}, {goal_id: null}, {effective_action: "governed_capability_intent"}]) {
Expand Down
Loading