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
62 changes: 59 additions & 3 deletions loopx/cli_commands/goal_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import argparse
import json
import shlex
from pathlib import Path
from typing import Any

Expand All @@ -24,10 +25,34 @@ def register_goal_acceptance_command(subparsers: Any, add_format: Any) -> None:
parser = subparsers.add_parser(
"goal-acceptance",
help="Read Goal intent, prepare revisions, adopt current intent, refine outcomes or run configured checks.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''Objective CLI workflow (no App required):
1. inspect --agent-id AGENT
2. prepare-objective --agent-id AGENT --document objective.json --operation-id ID --execute
objective.json: {"objective":"Current direction", "reason":"Evidence of changed user direction", "source_ref":"message:owner"}
Saves/reuses a proposal; does not change the Goal or block existing work.
3. inspect-proposal --proposal-id PROPOSAL
4. apply-proposal --proposal-id PROPOSAL --execute
Local owner operation: use existing explicit authorization, or surface once
for owner review and continue independently authorized work. Do not ask
again for an already authorized unchanged proposal. Omit --execute to preview.
Retry the same proposal ID after a lost response; no new operation ID.
5. inspect --agent-id AGENT
Review your own work, then adopt-objective --agent-id AGENT --document adoption.json
--operation-id ID --expected-provider-revision REV --execute
adoption.json: {"revision":1, "digest":"<goal_objective.digest>", "work_digest":"<objective_work_digest>", "review_summary":"How my current work fits"}
Use exact values from inspect; each peer adopts only for itself.
Own work review/adoption needs no human reply or unanimous peer vote.
All steps require --goal-id GOAL. Proposal preparation alone requires neither
owner input nor adoption; only an applied objective revision needs adoption.
Authority, budgets, stops and existing gated actions remain binding.''',
)
add_format(parser)
parser.add_argument("action", type=lambda value: "clear-checks" if value == "disable" else value, choices=("inspect", "prepare", "configure", "verify", "clear-checks", "refine", "prepare-objective", "revise-objective", "adopt-objective"))
parser.add_argument("action", type=lambda value: "clear-checks" if value == "disable" else value, choices=("inspect", "prepare", "configure", "verify", "clear-checks", "refine", "prepare-objective", "inspect-proposal", "apply-proposal", "revise-objective", "adopt-objective"))
parser.add_argument("--goal-id", required=True)
parser.add_argument(
"--proposal-id", help="Existing objective proposal ID for inspect-proposal/apply-proposal; reuses the original reviewed request and recovery identity.",
)
parser.add_argument(
"--agent-id",
help="Registered caller; Agents may prepare objective revisions, adopt only for themselves, refine outcomes or inspect/verify checks. Shared objective revision requires the owner.",
Expand Down Expand Up @@ -60,6 +85,17 @@ def render_goal_acceptance(payload: dict[str, Any]) -> str:
return "\n".join(lines)
if payload.get("provider_revision"):
lines.append(f"- provider revision: {payload['provider_revision']}")
proposal = payload.get("proposal")
if proposal and proposal["action_kind"] == "goal.update":
parameters = proposal["normalized_parameters"]
lines.extend([f"- proposal: {proposal['proposal_id']} ({proposal['status']})",
f"- proposed objective: {parameters['objective']}",
f"- reason: {parameters['reason']}", f"- source: {parameters['source_ref']}"])
lines.extend(f"- CLI: {command}" for command in payload.get("next_cli_actions", []))
lines.append("- A pending proposal does not change intent or block independently authorized work. Owner apply needs existing explicit authorization; do not repeat an answered request.")
if payload.get("current"):
lines.append(render_goal_acceptance(payload["current"]))
return "\n".join(lines)
contract = payload.get("goal_acceptance_contract") or {}
objective = contract.get("goal_objective")
if objective:
Expand Down Expand Up @@ -119,7 +155,17 @@ def handle_goal_acceptance_command(
"runtime_root": runtime_root_arg,
"agent_id": args.agent_id,
}
if args.action in {"revise-objective", "adopt-objective"}:
if args.proposal_id and args.action not in {"inspect-proposal", "apply-proposal"}:
raise ValueError("--proposal-id requires inspect-proposal or apply-proposal")
if args.action in {"inspect-proposal", "apply-proposal"}:
from ..control_plane.goals.goal_objective_actions import objective_revision_proposal
if not args.proposal_id or args.document or args.operation_id or args.expected_provider_revision:
raise ValueError("inspect-proposal/apply-proposal requires only --proposal-id, not a new document, operation or revision")
if args.action == "inspect-proposal" and args.execute:
raise ValueError("inspect-proposal is read-only")
payload = objective_revision_proposal(**common, proposal_id=args.proposal_id,
execute=args.action == "apply-proposal" and args.execute)
elif args.action in {"revise-objective", "adopt-objective"}:
if args.document is None or not args.operation_id or not args.expected_provider_revision:
raise ValueError("objective updates require --document, --operation-id and --expected-provider-revision from inspect")
document = json.loads(args.document.read_text(encoding="utf-8"))
Expand Down Expand Up @@ -184,9 +230,19 @@ def handle_goal_acceptance_command(
payload = public_goal_acceptance(inspect_goal_acceptance(**common))
else:
payload = verify_goal_acceptance(**common, execute=args.execute)
if (payload.get("proposal") or {}).get("action_kind") == "goal.update":
prefix = ["loopx", "--registry", str(registry_path), "--format", "json"]
if runtime_root_arg:
prefix.extend(["--runtime-root", runtime_root_arg])
proposal_args = ["--goal-id", args.goal_id, "--proposal-id", payload["proposal"]["proposal_id"]]
payload["next_cli_actions"] = [shlex.join(prefix + ["goal-acceptance", "inspect-proposal"] + proposal_args)]
if payload["proposal"]["status"] in {"preview_ready", "deferred", "applying"}:
payload["next_cli_actions"].append(shlex.join(prefix + ["goal-acceptance", "apply-proposal"] + proposal_args + ["--execute"]))
payload["next_action"] = "Surface this proposal once and continue independently authorized work under the current Goal. Apply only under existing explicit local-owner authorization; do not ask again for an already authorized unchanged proposal. After apply, each Agent reviews and adopts for itself."
code = (
1
if payload.get("checks_passed") is False
if payload.get("ok") is False
or payload.get("checks_passed") is False
or payload.get("acceptance_ready") is False
else 0
)
Expand Down
45 changes: 40 additions & 5 deletions loopx/control_plane/goals/goal_objective_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,57 @@

import json
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any
from uuid import uuid4

from ...chat_action_store import ChatActionStore, ActionConflictError, normalize_idempotency_key
from ...file_lock import exclusive_file_lock
from .acceptance import inspect_goal_acceptance, update_goal_objective, public_goal_acceptance, goal_acceptance_route, GoalAcceptanceRoute

if TYPE_CHECKING:
from ...chat_actions import ChatActionService

def prepare_objective_revision(*, registry_path: Path, runtime_root: str | None,
goal_id: str, agent_id: str | None, document: dict[str, Any], operation_id: str,
execute: bool) -> dict[str, Any]:

def _objective_service(registry_path: Path, runtime_root: str | None) -> ChatActionService:
from ...chat_actions import ChatActionService
from ...paths import effective_runtime_root

root = effective_runtime_root(registry_path, runtime_root)
service = ChatActionService(store=ChatActionStore(root / "chat" / "actions"), registry_path=registry_path)
return ChatActionService(store=ChatActionStore(root / "chat" / "actions"), registry_path=registry_path)


def objective_revision_proposal(*, registry_path: Path, runtime_root: str | None,
goal_id: str, agent_id: str | None, proposal_id: str, execute: bool = False) -> dict[str, Any]:
"""CLI transport for the same reviewed request, owner apply and recovery as Chat."""
service = _objective_service(registry_path, runtime_root)
current = inspect_goal_acceptance(**service._objective_route(goal_id), agent_id=agent_id)
proposal = service.load(proposal_id)
if proposal is None:
raise ValueError("Objective proposal was not found")
parameters = proposal["normalized_parameters"]
if (proposal["action_kind"] != "goal.update"
or proposal["context"].get("goal_id") != goal_id
or parameters.get("goal_id") != goal_id
or not {"objective", "reason", "source_ref"}.issubset(parameters)):
raise ValueError("Proposal is not a current-objective revision for this Goal")
if not execute:
return {"ok": True, "executed": False, "proposal": proposal,
"current": public_goal_acceptance(current)}
if agent_id is not None:
raise ValueError("Shared objective revision requires the local Goal owner; an Agent may prepare or inspect the proposal, not apply it")
result = service.apply(proposal_id)
# Replays may return only a historical receipt. Always read today's basis;
# never reinstall that historical objective or imply another peer adopted it.
current = inspect_goal_acceptance(**service._objective_route(goal_id))
applied = result["proposal"]["status"] == "applied"
return {**result, "ok": applied, "executed": applied,
"current": public_goal_acceptance(current)}


def prepare_objective_revision(*, registry_path: Path, runtime_root: str | None,
goal_id: str, agent_id: str | None, document: dict[str, Any], operation_id: str,
execute: bool) -> dict[str, Any]:
service = _objective_service(registry_path, runtime_root)
if not isinstance(document, dict) or set(document) != {"objective", "reason", "source_ref"}:
raise ValueError("objective proposal needs objective, reason and an opaque source_ref")
parameters = service._normalize("goal.update", {"goal_id": goal_id, **document, "prepared_by": agent_id})
Expand Down
3 changes: 2 additions & 1 deletion loopx/control_plane/work_items/replan_semantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ const REPLAN_PLANNING_GUIDANCE = [
"in the existing Vision/path_delta, including a justified no-change decision. Only a material " +
"user direction change that makes the objective misleading warrants goal-acceptance prepare-objective " +
"(native: prepare_goal_objective); " +
"reuse a pending proposal for owner apply, then each peer reconciles its own work and adopts. " +
"reuse a pending proposal; surfacing never blocks authorized work or forces a question. " +
"Use goal-acceptance --help for CLI preparation, owner apply and each peer's own adoption. " +
"Progress, Todo/route changes, diagnostics, polish or age alone need no revision. Documents/peers " +
"grant no authority. Reuse inline facts; inspect missing context only, without polling or " +
"a per-Turn revision/check receipt. Retain root commitments; a proposal or adoption grants no permissions.",
Expand Down
22 changes: 17 additions & 5 deletions skills/loopx-project/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,29 @@ or peer instructions. Attribution never expands authority. Record in-scope
refinement under existing authorization, link concrete work, read back, and
revisit unmet requirements before switching focus or claiming completion.

If direct user direction makes the recorded Goal objective obsolete, use
If recorded direct user direction makes the Goal objective obsolete, use
`goal-acceptance prepare-objective` with the proposed objective, reason and
opaque source reference; surface the existing Goal update proposal for owner
apply. Do not silently edit Markdown or treat a peer/document as authorization.
opaque source reference. Autonomous replanning may discover this mismatch from
existing user context; it need not request new intent merely because the Agent
initiated the review. Surface the existing proposal once as reviewable output,
not a mandatory question or precondition for continuing authorized work.
Preparing or reusing a pending proposal changes neither the current objective
nor execution authority. Continue under the current valid Goal; pause only an
action that actually depends on missing intent or authority. Use existing
explicit authorization for the unchanged proposal without asking again. These
rules also apply in unattended harness and benchmark runs; never create an
owner gate merely to surface a proposal. Do not silently edit Markdown or treat
a peer/document as authorization.
Ordinary progress, Todo/route changes, diagnostic feedback, wording polish or
age alone need no revision. Reuse an existing pending proposal without polling.
The current canonical objective then takes precedence over the old source text.
Before new execution, each Agent reviews its own Todo/Vision route and records
Only after owner apply, before new execution, each Agent reviews its own Todo/Vision route and records
`goal-acceptance adopt-objective` with its exact revision/digest and inspected
`objective_work_digest`. One peer cannot acknowledge for another; no unanimous
vote or extra write is needed after that revision is adopted. See the
vote, human response or extra write is needed after that revision is adopted.
Use CLI `inspect-proposal` / `apply-proposal --proposal-id` and the document
formats in `goal-acceptance --help`; App access or source-code inspection is
not required. Owner apply retains its authority boundary. See the
[operating-objective procedure](references/outcome-refinement.md#current-operating-objective).

## Current State, Continued Work And Settlement
Expand Down
63 changes: 57 additions & 6 deletions skills/loopx-project/references/outcome-refinement.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,68 @@ is not campaign completion. Existing legitimate stops/waits still apply.

## Current operating objective

When explicit user direction makes the recorded Goal objective obsolete, prepare
When recorded explicit user direction makes the Goal objective obsolete, prepare
an objective proposal with `goal-acceptance prepare-objective`, the proposed
objective, reason and original user source reference. Reuse a matching pending
proposal and its existing owner-apply route; do not silently edit the active
state or infer authorization from peer messages. Progress, artifact revisions,
proposal. Autonomous replanning may discover a mismatch against existing user
direction; it does not itself require a new human decision. Surface the proposal
once with its reason, effect and CLI route, then continue independently
authorized work under the current objective. A pending proposal is reviewable
output, not a gate, an adoption requirement or a reason to poll/ask repeatedly.
This applies equally to interactive, unattended harness and benchmark work.
Only an action that actually depends on missing consequential intent or authority
waits; do not turn that scoped wait into a Goal-wide stop. Already explicit
authorization for the unchanged proposal needs no repeated confirmation.
Do not silently edit active state or infer authorization from peer messages.
Progress, artifact revisions,
diagnostics and Todo routing changes alone do not require an objective revision.

After owner apply, each Agent adopts the current objective through
The complete path is available through CLI, without App access or source reads.
`goal-acceptance --help` describes both JSON documents. For example:

```json
{"objective":"Maintain reliable exports","reason":"Recorded user direction now includes ongoing recovery","source_ref":"message:owner-direction"}
```

```bash
loopx --format json goal-acceptance prepare-objective --goal-id GOAL \
--agent-id AGENT --document objective.json --operation-id prepare-direction --execute
loopx --format json goal-acceptance inspect-proposal --goal-id GOAL \
--agent-id AGENT --proposal-id PROPOSAL
```

The returned `next_cli_actions` preserve the caller's registry/runtime route and
proposal ID. Apply previews by default. Only when acting under the local owner's
existing explicit authorization, run the owner operation (without `--agent-id`):

```bash
loopx --format json goal-acceptance apply-proposal --goal-id GOAL \
--proposal-id PROPOSAL --execute
loopx --format json goal-acceptance inspect --goal-id GOAL --agent-id AGENT
```

Omitting `--agent-id` selects the existing local-owner boundary; it does not
create authority. A registered Agent cannot apply as itself or acknowledge for
another peer. Retry the same proposal ID after a lost response. The existing
native operation/checkpoint recovers once and returns the current canonical
basis; do not prepare a replacement or replay an old objective over a newer one.

Only after owner apply, each Agent adopts the current objective through
`goal-acceptance adopt-objective` with its exact revision, digest and inspected
`objective_work_digest`. Read the command's current schema before authoring the
proposal or adoption. Adoption covers only that Agent's Todo/Vision route;
`objective_work_digest` from its own `inspect` result:

```json
{"revision":1,"digest":"<goal_objective.digest>","work_digest":"<objective_work_digest>","review_summary":"Reviewed my current Todo/Vision against this objective"}
```

```bash
loopx --format json goal-acceptance adopt-objective --goal-id GOAL \
--agent-id AGENT --document adoption.json --operation-id adopt-direction \
--expected-provider-revision REVISION --execute
```

Use actual inspected values. Adoption is the Agent's own work review, not a
human approval step. It covers only that Agent's Todo/Vision route;
another peer cannot acknowledge for it, and no unanimous approval is required.
The current canonical objective takes precedence over superseded source text.

Expand Down
Loading
Loading