From 23a46a88219cd1c9bb7153edec80bf24b2f036ac Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:36:08 +0800 Subject: [PATCH] fix(quota): expose writeback inputs in settlement guidance Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/effect_program.py | 2 + loopx/control_plane/quota/settlement_plan.ts | 23 +++++- tests/control_plane/test_quota_settlement.py | 16 +++++ .../test_settlement_command_guidance_cli.py | 71 +++++++++++++++++++ .../quota_settlement_plan.test.ts | 5 ++ 5 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/control_plane/test_settlement_command_guidance_cli.py diff --git a/loopx/control_plane/quota/effect_program.py b/loopx/control_plane/quota/effect_program.py index 6eaa5137e3..b32e4c385f 100644 --- a/loopx/control_plane/quota/effect_program.py +++ b/loopx/control_plane/quota/effect_program.py @@ -27,6 +27,7 @@ settlement_result_payload, ) from ..goals.goal_ref_validation import exact_goal_ref +from ..work_items.delivery_batch_scale import DELIVERY_BATCH_SCALE_INPUT_CHOICES __all__ = [ "SETTLEMENT_IDENTITY_SCHEMA_VERSION", @@ -198,6 +199,7 @@ def build_turn_scoped_cli_settlement_plan( ) payload = effect_runtime_result("settlement.turn_scoped_cli_plan", { "identity": identity.as_dict(), "delivery_boundary": delivery_boundary, + "delivery_batch_scale_choices": list(DELIVERY_BATCH_SCALE_INPUT_CHOICES), **({"goal_ref": normalized_goal_ref} if normalized_goal_ref is not None else {}), "command_templates": { "todo_completion": ordinary_completion, "durable_writeback": writeback, diff --git a/loopx/control_plane/quota/settlement_plan.ts b/loopx/control_plane/quota/settlement_plan.ts index 446658f862..f30e51ea7a 100644 --- a/loopx/control_plane/quota/settlement_plan.ts +++ b/loopx/control_plane/quota/settlement_plan.ts @@ -6,7 +6,10 @@ import { } from "../effect_program.ts"; import {VISION_MATERIAL_CLOSEOUT_HINT, visionAuthoringContract} from "../goals/vision_checkpoint.ts"; import {parseExactGoalRef} from "../goals/goal_instance_identity.ts"; -import {requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts"; +import {requireJsonObject, requireNonEmptyString, requireStringArray} from "../runtime_decode.ts"; +import {DELIVERY_OUTCOMES} from "../work_items/delivery_outcome.ts"; +import {DELIVERY_BOUNDARIES} from "../turn_driver/delivery_continuity.ts"; +import {PROGRESS_RESULT_CLASSES} from "../work_items/replan_history.ts"; export function turnScopedCliSettlementPlan(params: JsonObject): SettlementPlan { const identity = settlementIdentity( @@ -15,6 +18,22 @@ export function turnScopedCliSettlementPlan(params: JsonObject): SettlementPlan const commands = requireJsonObject(params.command_templates, "command_templates"); const writeback = requireNonEmptyString(commands.durable_writeback, "durable_writeback"); const spend = requireNonEmptyString(commands.quota_spend, "quota_spend"); + // Scale remains CLI-owned. Project its actual accepted inputs rather than + // copying another enum or asking callers to load the entire command help. + const scales = requireStringArray(params.delivery_batch_scale_choices, "delivery_batch_scale_choices"); + requireNonEmptyString(scales.map(scale => requireNonEmptyString(scale, "delivery batch scale")).join("|"), + "delivery batch scale choices"); + const inputHint = " Fill placeholders from the actual result: classification is a descriptive label; " + + "scale=" + scales.join("|") + "; outcome=" + DELIVERY_OUTCOMES.join("|") + ". " + + "Set --delivery-boundary " + DELIVERY_BOUNDARIES.join("|") + " for the applicable case above. " + + "Typed evidence: --progress-result-class " + PROGRESS_RESULT_CLASSES.join("|") + + ". For blocked outcome_gap, --progress-work-item-id must match this plan's Todo/obligation id " + + "and --progress-blocker-id names the blocker. Repeat --progress-evidence-id for evidence. " + + "IDs are 1-128 characters, start with an ASCII letter/digit, then letters/digits or ._:/- " + + "(e.g. evidence:validation-1); an ID does not upload a file. " + + "Use --agent-vision-json for the material vision packet; " + + "--vision-unchanged-reason only reuses an already evidence-linked vision. " + + "These are authoring choices, not defaults, proof of progress or new requirements."; const parsedGoalRef = params.goal_ref === undefined ? null : parseExactGoalRef(params.goal_ref); @@ -58,7 +77,7 @@ export function turnScopedCliSettlementPlan(params: JsonObject): SettlementPlan "For an admitted open continuous Monitor whose observation is unavailable, " + "use this original identity for typed blocked outcome_gap; it settles without " + "poll or spend and preserves observation clocks/hash/generation. " + - "An already committed Monitor poll needs neither refresh nor spend.", + "An already committed Monitor poll needs neither refresh nor spend." + inputHint, idempotency_key_ref: "$.identity.effect_id", expected_receipt: "durable_writeback_receipt", command_template: writeback, // In-flight continuation uses its projected flags without a material vision packet. diff --git a/tests/control_plane/test_quota_settlement.py b/tests/control_plane/test_quota_settlement.py index 3b8f56aac3..628c47dc6b 100644 --- a/tests/control_plane/test_quota_settlement.py +++ b/tests/control_plane/test_quota_settlement.py @@ -55,6 +55,22 @@ TURN_ID = "turn-settlement-1" +def test_writeback_guidance_uses_current_cli_scale_vocabulary(): + from loopx.control_plane.work_items.delivery_batch_scale import ( + DELIVERY_BATCH_SCALE_INPUT_CHOICES, DELIVERY_BATCH_SCALE_ALIASES, + ) + + plan = quota_effect_program.build_turn_scoped_cli_settlement_plan( + goal_id=GOAL_ID, agent_id=AGENT_ID, todo_id=TODO_ID, + turn_instance_id=TURN_ID, scoped_cli_args="", lifecycle_actor_args="", + ) + hint = plan.as_dict()["ordered_steps"][1]["precondition"] + for scale in DELIVERY_BATCH_SCALE_INPUT_CHOICES: + assert scale in hint + for alias in DELIVERY_BATCH_SCALE_ALIASES: + assert alias not in hint # Historical aliases remain read-only. + + @pytest.mark.parametrize("replan", [False, True]) def test_command_plan_supplies_actor_from_identity(replan): plan = quota_effect_program.build_turn_scoped_cli_settlement_plan( diff --git a/tests/control_plane/test_settlement_command_guidance_cli.py b/tests/control_plane/test_settlement_command_guidance_cli.py new file mode 100644 index 0000000000..5911b74934 --- /dev/null +++ b/tests/control_plane/test_settlement_command_guidance_cli.py @@ -0,0 +1,71 @@ +"""Ordinary callers can fill the returned writeback without global CLI help.""" +from __future__ import annotations + +import shlex +from pathlib import Path + +import pytest + +from loopx.control_plane.quota.turn_envelope import build_turn_envelope +from test_quota_settlement_cli import ( + AGENT_ID, GOAL_ID, TODO_ID, _initialize_fixture_authority, + _run_cli, _run_generated_cli, _write_fixture, +) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("profile", ["codex_app", "generic_cli"]) +def test_returned_writeback_can_record_progress_and_settle_once( + tmp_path: Path, provider: str, profile: str, +) -> None: + project, runtime, registry = _write_fixture(tmp_path) + _initialize_fixture_authority(project, runtime, provider=provider) + host = ["--codex-app"] if profile == "codex_app" else ["--runtime-profile", profile] + rc, guard = _run_cli(registry, runtime, "quota", "should-run", *host, + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", TODO_ID, "--turn-instance-id", "command-guidance") + assert rc == 0, guard + plan = guard["interaction_contract"]["cli_channel"]["settlement_plan"] + assert build_turn_envelope(guard)["writeback"]["settlement_plan"] == plan + step = next(s for s in plan["ordered_steps"] if s["kind"] == "durable_writeback") + guidance = step["precondition"] + # These are the inputs the caller needs for this actual bounded result. + for value in ("test_only", "outcome_progress", "in_flight_continuation", + "--progress-result-class", "advanced", "--progress-evidence-id"): + assert value in guidance + assert "classification" in guidance + command = step["command_template"] + for placeholder, value in { + "": "focused_validation", + "": "test_only", + "": "outcome_progress", + "": "in_flight_continuation", + }.items(): + command = command.replace(placeholder, value) + if "--delivery-boundary" not in shlex.split(command): + command += " --delivery-boundary in_flight_continuation" + command += " " + shlex.join([ + "--delivery-workspace-path", str(project), + "--progress-result-class", "advanced", + "--progress-evidence-id", "evidence:focused-validation", + "--no-global-sync", "--suppress-external-sinks", + ]) + # Discoverability must not weaken evidence or exactly-once settlement gates. + rc, refused = _run_generated_cli( + command.replace("evidence:focused-validation", ".local/not-an-evidence-id"), + registry_path=registry, + ) + assert rc != 0, refused + spend = next(s["command_template"] for s in plan["ordered_steps"] if s["kind"] == "quota_spend") + rc, premature = _run_generated_cli(spend, registry_path=registry) + assert rc != 0, premature + rc, writeback = _run_generated_cli(command, registry_path=registry) + assert rc == 0, writeback + assert writeback["progress_observation"]["result_class"] == "advanced" + rc, spent = _run_generated_cli(spend, registry_path=registry) + assert rc == 0, spent + rc, replay = _run_generated_cli(spend, registry_path=registry) + assert rc == 0 and replay["idempotent_replay"] is True, replay + rc, todo = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID, + "--todo-id", TODO_ID) + assert rc == 0 and todo["todo"]["status"] == "open", todo diff --git a/tests/control_plane_ts/quota_settlement_plan.test.ts b/tests/control_plane_ts/quota_settlement_plan.test.ts index 650db4e057..abbcd2a3aa 100644 --- a/tests/control_plane_ts/quota_settlement_plan.test.ts +++ b/tests/control_plane_ts/quota_settlement_plan.test.ts @@ -7,6 +7,7 @@ import {turnScopedCliSettlementPlan} from "../../loopx/control_plane/quota/settl const input = { identity: {goal_id: "goal", agent_id: "agent", todo_id: "todo", turn_instance_id: "turn"}, + delivery_batch_scale_choices: ["test_only", "single_surface", "multi_surface", "implementation"], command_templates: { todo_completion: "ordinary completion", durable_writeback: "writeback", quota_spend: "spend", terminal_closeout: "terminal completion", @@ -80,6 +81,10 @@ test("autonomous replan never manufactures a Todo completion step", () => { }); test("incomplete command facts or ambiguous identity fail closed", () => { + for (const choices of [undefined, [], [""], [1]]) { + assert.throws(() => turnScopedCliSettlementPlan({...input, delivery_batch_scale_choices: choices}), + /must be an array of strings|must be a non-empty string/); + } for (const key of ["todo_completion", "durable_writeback", "quota_spend", "terminal_closeout"]) { assert.throws(() => turnScopedCliSettlementPlan({...input, command_templates: {...input.command_templates, [key]: ""},