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
21 changes: 21 additions & 0 deletions docs/reference/goal-acceptance-observations.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,27 @@ runnable research and original budget/stop conditions retain precedence.
See the [Agent workflow](../../skills/loopx-project/references/outcome-refinement.md)
for inline context, one-call writeback and upgrade/adoption limits.

When one `refresh-state` explicitly supplies both a new complete outcome review
and an Explore result for work linked to that outcome, the review covers that
same batch's exact additive Todo link. A result for separate same-Agent work
does not change the reviewed outcome's basis or work links. The typed owner
validates the complete result-link intent and ownership in either case, then
hashes a relevant prospective link against the original CAS
snapshot; it does not write the link or rebind an old review. The basis becomes
current only after the Todo owner commits the expected link. Failed delivery
remains stale and exact replay retains its original digest. Other work changes
and additional evidence links still invalidate the review. Separately submitted
and legacy completed reviews keep their original basis; no check result or
independent-verification flag changes.

中文:同一次 `refresh-state` 显式提交完整的新自查,以及该 outcome 关联工作的
Explore 结果时,自查依据包含该批预期追加的精确 Todo 结果关联。同 Agent 的其他
工作结果不改变本 outcome 的依据或关联;typed owner 在两种情况下均先验证完整
意图和归属,再基于原 CAS 快照计算相关工作的摘要,不代替
Todo owner 写关联,也不替旧自查更新依据。只有预期关联真实提交后依据才是 current;
投递失败保持过期,原样重放保留原摘要。其他工作变化、额外证据关联仍使依据失效。
分开提交或旧版本已完成的自查保持原义,独立检查与验证标记不变。

中文:Goal 概览显示 Agent 登记的支撑成果:原始目的、累计要求、关联工作、来源,
并区分 Agent 自查与独立检查。所有者可原地补充要求并读取原生结果;过期视图先
刷新再重基,未确认写入保留原操作重试。不启动模型或执行检查命令。默认维护和
Expand Down
5 changes: 4 additions & 1 deletion loopx/cli_commands/project_lifecycle_refresh_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,10 @@ def handle_refresh_state_command(
from ..control_plane.goals.acceptance import refine_goal_outcome, public_goal_acceptance
refinement_result = refine_goal_outcome(
registry_path=registry_path, runtime_root=args.runtime_root, goal_id=args.goal_id,
agent_id=args.agent_id, execute=not args.dry_run, **outcome_update)
agent_id=args.agent_id, execute=not args.dry_run, **outcome_update,
**({"reviewed_work_update": {"todo_id": args.todo_id,
"append_explore_result_node_refs": [explore_result["node_id"]]}}
if explore_result is not None and "review" in outcome_update["refinement"] else {}))
outcome_update_result = public_goal_acceptance(refinement_result, include_contract=False)
if args.dry_run:
# Preview the same ordered composition as execution: the typed
Expand Down
5 changes: 4 additions & 1 deletion loopx/control_plane/goals/acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def refine_goal_outcome(
*, registry_path: Path, goal_id: str, refinement: dict[str, Any],
expected_provider_revision: str, operation_id: str, agent_id: str | None = None,
runtime_root: str | None = None, execute: bool = False,
reviewed_work_update: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Record scoped planning intent, never configure validators or attest success."""
registry_path, goal, route = _routing(registry_path, goal_id, runtime_root, agent_id)
Expand All @@ -165,7 +166,9 @@ def refine_goal_outcome(
raise ValueError("Outcome refinement requires an active Goal")
return _result("goal.acceptance.refine", {**route, "actor_agent_id": agent_id,
"expected_provider_revision": expected_provider_revision, "operation_id": operation_id,
"refinement": refinement, "dry_run": not execute}, timeout=CANONICAL_AUTHORITY_WRITE_TIMEOUT_SECONDS)
"refinement": refinement, "dry_run": not execute,
**({"reviewed_work_update": reviewed_work_update} if reviewed_work_update is not None else {}),
}, timeout=CANONICAL_AUTHORITY_WRITE_TIMEOUT_SECONDS)


def update_goal_objective(
Expand Down
17 changes: 13 additions & 4 deletions loopx/control_plane/goals/acceptance_authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,19 +197,28 @@ async function commit(store: AuthorityStore, request: JsonObject, head: Authorit
/** Append planning requirements without configuring checks or changing Goal/Todo intent. */
export async function refineGoalAcceptanceOutcome(store: AuthorityStore, value: JsonObject): Promise<JsonObject> {
const request = canonicalAuthorityObject(value, "outcome refinement request");
acceptanceKeys(request, [...REQUEST_FIELDS, "refinement"], [...LOCAL_FIELDS, "goal_ref"]);
acceptanceKeys(request, [...REQUEST_FIELDS, "refinement"], [...LOCAL_FIELDS, "goal_ref", "reviewed_work_update"]);
for (const key of ["goal_id", "operation_id", "expected_provider_revision"]) requireAuthorityStoreId(request[key], key);
acceptanceRequire(request.actor_agent_id === null || typeof request.actor_agent_id === "string" && !!request.actor_agent_id, "outcome actor is required");
acceptanceRequire(request.dry_run === undefined || typeof request.dry_run === "boolean", "dry_run must be boolean");
const command = receiptFor(request, "refine");
const readback = async (result: JsonObject): Promise<JsonObject> => {
// Older refreshes stored the same review before considering their later
// attachment. Exact legacy recovery is read-only; never upgrade its basis.
if (result.reason_code !== "coordination_operation_identity_mismatch" || request.reviewed_work_update === undefined) return result;
const {reviewed_work_update: _update, ...legacyRequest} = request;
const legacy = await receiptFor(legacyRequest, "refine").receipt.read(store);
return legacy?.status === "replayed" ? legacy : result;
};
const replay = await command.receipt.read(store);
if (replay) return replayWithCurrentProjection(store, replay, String(request.goal_id));
if (replay) return replayWithCurrentProjection(store, await readback(replay), String(request.goal_id));
const observation = await command.receipt.observe(store);
if (observation.kind === "receipt") return replayWithCurrentProjection(store, observation.result, String(request.goal_id));
if (observation.kind === "receipt") return replayWithCurrentProjection(store, await readback(observation.result), String(request.goal_id));
const head = current(observation.authority, request);
if (!loaded(head)) return source(store, head);
const goalId = String(request.goal_id), goalRef = requestGoalRef(request);
const state = refineGoalOutcome(head.head, goalId, goalRef, request.actor_agent_id as string | null, request.refinement);
const state = refineGoalOutcome(head.head, goalId, goalRef, request.actor_agent_id as string | null, request.refinement,
request.reviewed_work_update);
const next: JsonObject = {...head.head, goal_outcomes: state};
// Bind a first exact-lifetime declaration to the same lifecycle owner as checks.
if (goalRef !== null && readGoalAcceptanceLifecycle(head.head, goalId) === null) {
Expand Down
24 changes: 22 additions & 2 deletions loopx/control_plane/goals/outcome_requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {BARE_SHA256_PATTERN} from "../content_digest.ts";
import {goalAcceptanceTodoDigest} from "./acceptance_work_basis.ts";
import {projectRelations, todoRef} from "../work_items/planning_relations.ts";
import {normalizeTodoResumeWhen, TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION} from "../todos/resume_condition.ts";
import {appendTodoExploreReferences} from "../todos/work_requirements.ts";

export const GOAL_OUTCOMES_SCHEMA = "loopx_goal_outcomes_v0";
export const OUTCOME_CONTEXT_NAVIGATION_SCHEMA = "outcome_context_navigation_v1";
Expand Down Expand Up @@ -126,7 +127,7 @@ function outcomeBytes(state: OutcomeState): {declaration: number; review: number
}

export function refineGoalOutcome(head: JsonObject, goalId: string, goalRef: WireExactGoalRef | null,
actor: string | null, value: unknown): OutcomeState {
actor: string | null, value: unknown, reviewedWorkUpdate?: unknown): OutcomeState {
let previous = readState(head, goalId);
const lifecycle = readGoalAcceptanceLifecycle(head, goalId);
requireValue(!lifecycle || lifecycle.state === "active" && goalRef && sameExactGoalRef(lifecycle.goal_ref, goalRef), "outcome Goal lifetime is unavailable or changed");
Expand All @@ -144,6 +145,8 @@ export function refineGoalOutcome(head: JsonObject, goalId: string, goalRef: Wir
}
const patch = canonicalAuthorityObject(value, "outcome refinement");
keys(patch, ["outcome_id"], ["source", "owner_agent_id", "objective", "requirements", "todo_ids", "review"]);
requireValue(reviewedWorkUpdate === undefined || patch.review !== undefined,
"a reviewed work update requires a newly authored complete review");
const outcomeId = id(patch.outcome_id, "outcome id");
const old = previous?.outcomes.find(o => o.id === outcomeId);
const owner = id(patch.owner_agent_id === undefined ? old?.owner_agent_id : patch.owner_agent_id, "outcome Agent");
Expand Down Expand Up @@ -181,7 +184,24 @@ export function refineGoalOutcome(head: JsonObject, goalId: string, goalRef: Wir
if (patch.review !== undefined) {
const items = reviewItems(patch.review);
requireValue(items.length === outcome.requirements.length && items.every(row => reqs.has(row.id)), "review must address every retained requirement exactly once");
outcome.review = {basis_digest: reviewBasis(outcome, todos), items};
let reviewedTodos = todos;
if (reviewedWorkUpdate !== undefined) {
// The host transports only the explicit same-batch additive effect. Hash
// its exact prospective work state, never bless a later arbitrary head.
// Until the Todo owner commits that effect this review remains stale.
const update = canonicalAuthorityObject(reviewedWorkUpdate, "reviewed work update");
keys(update, ["todo_id", "append_explore_result_node_refs"]);
const todoId = id(update.todo_id, "reviewed Todo id"), todo = todos.get(todoId);
requireValue(actor !== null && todo && todo.role === "agent" && todo.claimed_by === actor,
"reviewed work must be the caller's Todo");
requireValue(Array.isArray(update.append_explore_result_node_refs) && update.append_explore_result_node_refs.length === 1,
"reviewed work must name exactly one additive result link");
const prospectiveTodo = {...todo, ...appendTodoExploreReferences(todo, update.append_explore_result_node_refs)};
// A batch may review another outcome while delivering this Todo's result.
// Validate the complete intent first; only linked work enters this basis.
if (outcome.todo_ids.includes(todoId)) reviewedTodos = new Map(todos).set(todoId, prospectiveTodo);
}
outcome.review = {basis_digest: reviewBasis(outcome, reviewedTodos), items};
}
// Refinement changes one outcome, not the positions of unrelated requirements
// in canonical snapshots and their derived full/delta context.
Expand Down
12 changes: 2 additions & 10 deletions loopx/control_plane/todos/field_update.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Pure field intent planning. Admission, leases, validation and commit stay
* with the calling lifecycle transaction; this result grants no write right. */
import { normalizeTodoWorkRequirements } from "./work_requirements.ts";
import { appendTodoExploreReferences, normalizeTodoWorkRequirements } from "./work_requirements.ts";
import {planTodoPriority} from "./priority.ts";
import type { JsonObject } from "../effect_program.ts";
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
Expand Down Expand Up @@ -215,15 +215,7 @@ export function planTodoFieldUpdate(value: unknown): TodoFieldUpdatePlan {
if (present(intent.explore_result_node_refs)) {
throw new EffectRuntimeRequestError("cannot replace and append Explore references together");
}
const additions = normalizeTodoWorkRequirements({
explore_result_node_refs: intent.append_explore_result_node_refs,
}).explore_result_node_refs as string[];
const existing = normalizeTodoWorkRequirements({
explore_result_node_refs: block.explore_result_node_refs ?? [],
}).explore_result_node_refs as string[];
Object.assign(updates, normalizeTodoWorkRequirements({
explore_result_node_refs: [...existing, ...additions],
}));
Object.assign(updates, appendTodoExploreReferences(block, intent.append_explore_result_node_refs));
}
// Public update carries the effective scope and raw observation once. The
// field plan composes validation and generation without another RPC.
Expand Down
10 changes: 10 additions & 0 deletions loopx/control_plane/todos/work_requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,13 @@ export function normalizeTodoWorkRequirements(intent: JsonObject): JsonObject {
}
return result;
}

/** One additive effect, shared by the committing Todo owner and prospective
* review bases. Calculating the fields never grants authority or writes them. */
export function appendTodoExploreReferences(todo: JsonObject, additions: unknown): JsonObject {
const refs = normalizeTodoWorkRequirements({explore_result_node_refs: requireStringArray(additions, "explore_result_node_refs")})
.explore_result_node_refs as string[];
const existing = normalizeTodoWorkRequirements({explore_result_node_refs: todo.explore_result_node_refs ?? []})
.explore_result_node_refs as string[];
return normalizeTodoWorkRequirements({explore_result_node_refs: [...existing, ...refs]});
}
11 changes: 11 additions & 0 deletions skills/loopx-project/references/outcome-refinement.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ For changed requirements or reviews, add this envelope to the **existing**
its ordinary routing, Turn, checkpoint and delivery arguments. This does not
replace them.

If that same writeback includes an explicit Explore result for work linked to
the reviewed outcome, a newly authored complete review binds the exact additive
result link expected from this batch. A valid result for separate same-Agent
work leaves this outcome's basis and work links unchanged; both still use the
existing ownership and intent validation.
It remains stale until the Todo owner commits the link; failed delivery replays
the original digest. Other work/evidence changes still require review. Do not
repeat an unchanged review solely to acknowledge a successfully delivered
same-batch link. Separately submitted and legacy completed reviews are not
automatically rebound, and independent checks retain their authority.

An enabled due Stride review binds the delivered context, which includes outcome
facts. When this milestone changes those facts, apply the refinement first with
the standalone command below, then re-enter the guard with the same Turn identity
Expand Down
1 change: 1 addition & 0 deletions skills/loopx-self-repair/references/repair-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson.

| Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair |
| --- | --- | --- | --- | --- |
| `outcome_review_same_batch_link_invalidation` | A successful review-plus-result refresh immediately reports its Agent review basis stale; an unchanged standalone review is then repeated. | Exact refinement and result-delivery receipts, original CAS revision, retained conclusions and canonical Todo links before/after; distinguish the authored effect from other new evidence. | Refinement hashes pre-link work, then post-writeback delivery adds a work-basis input. | Validate the complete intent and same-Goal ownership before determining relevance in the typed owner. Bind only the newly authored complete review whose outcome includes that work to the exact normalized same-batch additive link using the shared append rule and original CAS snapshot. Keep it stale until actual commit; a valid separate-work result leaves the reviewed outcome unchanged. Preserve independent verification, all retained requirements, separate/legacy reviews, initial CAS refusal, failed delivery, exact receipt replay and concurrent work/evidence invalidation through real File/SQLite CLI. Never exclude all evidence links, refresh old review digests against an arbitrary later head, reorder effects before primary admission or claim model/score benefit from this repair. |
| `answer_question_replaced_by_delivery_status` | A user asks why a result occurred or whether it is expected, but receives measurements, tests or PR progress even though the reply was delivered completely. | The actual question and relevant corrections, current receiver brief, shared answer guidance, and the complete message on its original return route. | The author answered implementation progress instead of the causal question; successful transport or correct counters were mistaken for a useful answer. | Retain the correction in the existing Task and outcome, then repair the existing shared generation guidance: explain meaning, supported mechanism, expectations and the specific unknown before supporting data. Keep hypotheses separate from confirmed causes and preserve the qualified investigation owner. Verify guidance at native callers and read back the actual explanatory answer. Do not add keyword-based answer scoring, a second model rewrite, another display or a duplicate investigation; a prompt test or delivery receipt does not prove model compliance or user adoption. |
| `queue_action_type_coercion` | A queued input action such as `["edit"]` is accepted but applies cancellation, or a stored operation accepts a non-string action. | Original JSON types, the typed planner and receipt decoder, real HTTP responses, canonical Turn/transcript/events/Session snapshots and native IO before/after refusal. | Membership checks stringify the action while effect selection and replay compare its original literal value. | Validate the original literal action at the existing owner before branching and when decoding pending or committed receipts. Cover arrays, nested arrays, objects, nulls, numbers, booleans and unknown strings; require rejection without state or native effects, then verify legal edit/cancel, original-operation replay and queue races. Preserve malformed historical evidence rather than coercing or rewriting receipts. |
| `collaboration_return_format_projection_gap` | A complete answer that fits the result budget is rejected, or a published versioned return becomes unreadable or cannot replay through the real CLI. | Selected renderer output and Unicode count; persisted reply, compact publication/Inbox observations, original-route delivery and legacy replay at the same boundary. | Validation counted a different renderer before choosing the immutable format, or a compact codec omitted the persisted format marker and silently treated it as legacy. | Apply the unchanged budget to the actual selected format, carry its existing marker through every compact observation, and retain old bytes on replay. Validate BMP/non-BMP exact-limit and one-character-over cases, unsupported versions, real CLI publication/retry, original-audience return and File/SQLite packaged readback. Do not raise the budget, truncate the answer, rewrite historical receipts or add a second model. |
Expand Down
Loading
Loading