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: 11 additions & 0 deletions docs/architecture/rfcs/goal-outcome-refinement-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ cannot acknowledge a later revision. Old configured verification becomes stale
against a changed objective. A successor lifetime cannot inherit its
predecessor's objective implicitly.

`goal-acceptance inspect` and the Goal settings acceptance read include current
Agent registration from the resolved source registry. The existing TS objective
projector joins those observations with the retained revision cohort and adoption
records. A later registered peer appears pending before it adopts. Inspection
does not persist registration or adoption, change the objective digest/provider
revision, or authorize execution. Provider-only reads without current registration
facts retain their historical projection.
Registered `@` aliases use the same pending/adopted lifecycle. Once an objective
record contains such an alias, a code rollback must retain a compatible ID reader;
do not delete revision or adoption history to make an older reader accept it.

Revisions are exceptional direction changes, not per-Turn maintenance. Ordinary
progress, Todo/route changes, diagnostic feedback and wording polish do not
justify a new shared objective. Agents reuse a pending proposal for the same
Expand Down
8 changes: 8 additions & 0 deletions docs/architecture/rfcs/goal-outcome-refinement-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ lease acquisition 会拒绝其新执行。在途 Turn 的恢复、续租和释
丢失响应恢复原回执,旧采用操作重放不能确认新版本。旧的配置检查结果在目标
变化后转为 stale;后继 Goal 实例不能自动继承旧目标。

`goal-acceptance inspect` 和 Goal 设置中的 acceptance 读取,会携带解析后的源
registry 当前登记成员。既有 TS objective 投影 owner 将这些观察与保留的修订成员、
采用记录合并。后来登记的 Agent 在采用前显示为待采用。读取不保存成员或采用状态,
不修改目标摘要或 provider revision,也不授予执行权限。没有当前登记事实的
provider-only 读取保留原有历史投影。
已登记的 `@` 别名使用相同的待采用/已采用流程。目标记录包含此类别名后,回退代码
必须保留兼容的 ID reader;不能删除修订或采用历史来迁就旧 reader。

修订只用于必要的方向变化,不是每轮维护。普通进展、Todo/路线调整、诊断反馈和
措辞润色不应生成共享目标新版本。相同目标的待审提案复用,不反复打扰 owner 或轮询。
owner 判断语义必要性;运行时拒绝相同或仅空白不同的目标,不伪装成语义等价检测器。
Expand Down
4 changes: 2 additions & 2 deletions loopx/control_plane/goals/acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,10 @@ def inspect_goal_acceptance(
validation_workspace_path: Path | None = None,
) -> dict[str, Any]:
"""Read one canonical basis; command declarations stay inside the host."""
registry_path, _, route = _routing(registry_path, goal_id, runtime_root, agent_id)
registry_path, goal, route = _routing(registry_path, goal_id, runtime_root, agent_id)
basis = _result(
_INSPECT_METHOD,
{**route,
{**route, "registered_agents": registered_agent_ids_for_goal(goal),
**({"agent_id": agent_id} if agent_id is not None else {}),
**({"todo_id": todo_id} if todo_id is not None else {}),
**({"include_work_details": False} if not include_work_details else {})},
Expand Down
6 changes: 4 additions & 2 deletions loopx/control_plane/goals/acceptance_authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ export async function inspectGoalAcceptance(
goalRef?: WireExactGoalRef,
includeWorkDetails = true,
agentId?: string,
registeredAgents?: unknown,
): Promise<JsonObject> {
const head = await store.loadAuthority();
if (head.status !== "loaded") return source(store, {...head});
Expand Down Expand Up @@ -587,7 +588,7 @@ export async function inspectGoalAcceptance(
completion_requirements: acceptanceCompletionRequirements(head.head, goalId, todoId)}),
...(agentId === undefined || currentGoalObjective(head.head) === null ? {} : {objective_work_digest: goalObjectiveWorkDigest(head.head, agentId),
goal_objective_guard: goalObjectiveWorkGuard(head.head, agentId)}),
goal_acceptance_contract: projectGoalAcceptance(head.head, goalId)});
goal_acceptance_contract: projectGoalAcceptance(head.head, goalId, registeredAgents)});
}

async function local(value: unknown, kind: "inspect" | "configure" | "verify" | "lifecycle" | "refine" | "objective"): Promise<JsonObject> {
Expand All @@ -606,7 +607,8 @@ async function local(value: unknown, kind: "inspect" | "configure" | "verify" |
request.todo_id == null ? undefined : requireAuthorityStoreId(request.todo_id, "todo_id"),
request.goal_ref === undefined ? undefined : parseWireExactGoalRef(request.goal_ref, "acceptance request goal_ref"),
request.include_work_details !== false,
request.agent_id === undefined ? undefined : requireAuthorityStoreId(request.agent_id, "agent_id"));
request.agent_id === undefined ? undefined : requireAuthorityStoreId(request.agent_id, "agent_id"),
request.registered_agents);
}
if (kind === "configure") return configureGoalAcceptance(store, request);
if (kind === "refine") return refineGoalAcceptanceOutcome(store, request);
Expand Down
8 changes: 4 additions & 4 deletions loopx/control_plane/goals/acceptance_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,10 @@ export class GoalAcceptanceRead {
contract_digest: receipt.contract_digest, todo_id: receipt.todo_id, results: receipt.results} : null};
}

contract(): JsonObject {
contract(registeredAgents?: unknown): JsonObject {
const checks = structuredClone(this.checks());
const outcomes = projectGoalOutcomes(this.head, this.goalId, checks);
const objective = projectGoalObjective(this.head);
const objective = projectGoalObjective(this.head, registeredAgents);
return {...checks, ...(outcomes === null ? {} : {outcome_requirements: outcomes}),
...(objective === null ? {} : {goal_objective: objective})};
}
Expand Down Expand Up @@ -619,8 +619,8 @@ export class GoalAcceptanceRead {

/** Outcomes extend the existing acceptance read, even before checks are configured.
* No declaration preserves the original projection byte-for-byte. */
export function projectGoalAcceptance(head: JsonObject, goalId: string): JsonObject {
return new GoalAcceptanceRead(head, goalId).contract();
export function projectGoalAcceptance(head: JsonObject, goalId: string, registeredAgents?: unknown): JsonObject {
return new GoalAcceptanceRead(head, goalId).contract(registeredAgents);
}

/** Explicit selection and confirmed bindings survive mutable work fields.
Expand Down
10 changes: 7 additions & 3 deletions loopx/control_plane/goals/goal_objective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {canonicalAuthorityObject, canonicalAuthoritySha256, AuthorityStoreProtoc
import {indexCoordinationProjectionTodos} from "../coordination/coordination_projection.ts";
import {BARE_SHA256_PATTERN} from "../content_digest.ts";
import {readGoalAcceptanceLifecycle, sameExactGoalRef, parseWireExactGoalRef, type WireExactGoalRef} from "./acceptance_lifecycle.ts";
import {normalizeRegisteredTodoAgents} from "../coordination/todo_agents.ts";

export const GOAL_OBJECTIVE_SCHEMA = "goal_objective_revision_v0";
export const GOAL_OBJECTIVE_REVIEW_REQUIRED = "goal_objective_review_required";
Expand All @@ -28,7 +29,8 @@ function keys(row: JsonObject, names: string[]): void {
function agents(value: unknown): string[] {
requireValue(Array.isArray(value) && value.length <= 128, "registered Agents must be a bounded list");
const result = value.map(v => text(v, "Agent", 128));
requireValue(result.every(v => /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(v)) && new Set(result).size === result.length, "Agent identities must be unique safe identifiers");
// Retain historical ID forms while accepting the registry's @ aliases.
requireValue(result.every(v => /^[A-Za-z0-9][A-Za-z0-9_.:@-]*$/.test(v)) && new Set(result).size === result.length, "Agent identities must be unique safe identifiers");
return result;
}
export function readGoalObjective(head: JsonObject): GoalObjectiveState | null {
Expand Down Expand Up @@ -83,11 +85,13 @@ export function goalObjectiveWorkGuard(head: JsonObject, agent: string): JsonObj
revision: state.revisions.length, digest: state.revisions.at(-1)!.digest,
read_action: `loopx --format json goal-acceptance inspect --goal-id ${head.goal_id} --agent-id ${agent}`};
}
export function projectGoalObjective(head: JsonObject): JsonObject | null {
export function projectGoalObjective(head: JsonObject, registeredAgents?: unknown): JsonObject | null {
requireValue(registeredAgents === undefined || Array.isArray(registeredAgents), "registered_agents must be an array");
const currentPeers = registeredAgents === undefined ? [] : normalizeRegisteredTodoAgents(registeredAgents);
const state = currentGoalObjective(head);
if (!state) return null;
const current = state.revisions.at(-1)!;
const peers = [...new Set([...state.registered_agents, ...Object.keys(state.adoptions)])];
const peers = [...new Set([...state.registered_agents, ...Object.keys(state.adoptions), ...currentPeers])];
return {...current, original_objective: state.original_objective,
peer_adoption: peers.map(agent => ({agent_id: agent, revision: state.adoptions[agent]?.revision ?? null, current: !objectiveAdoptionRequired(state, agent)})),
acceptance_gaps: peers.filter(agent => objectiveAdoptionRequired(state, agent)).map(agent => ({
Expand Down
26 changes: 26 additions & 0 deletions tests/control_plane/test_goal_acceptance_source_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,32 @@ def global_cli(*arguments, override=None):
server.server_close()


def test_objective_inspection_projects_later_source_peer_through_global_app(source_app):
project, _, source, global_cli, request, source_registry, _ = source_app
document = project / "objective.json"
document.write_text(json.dumps({"objective": "Maintain the original export and recovery",
"reason": "The export is now maintained", "source_ref": "message:owner"}))
code, revised = source("revise-objective", "--document", str(document),
"--operation-id", "source-objective", "--expected-provider-revision",
source("inspect")[1]["provider_revision"], "--execute")
assert code == 0, revised
payload = json.loads(source_registry.read_text())
payload["goals"][0]["coordination"]["registered_agents"].append("later-source-peer")
source_registry.write_text(json.dumps(payload))
global_before = (project.parent / "global.json").read_bytes()

code, inspected = global_cli("inspect")
assert code == 0, inspected
status, app = request("GET", f"/api/chat/goal-acceptance?goal_id={GOAL}")
assert status == 200, app
assert app["provider_revision"] == inspected["provider_revision"] == revised["provider_revision"]
objective = app["goal_acceptance_contract"]["goal_objective"]
assert objective == inspected["goal_acceptance_contract"]["goal_objective"]
assert {"agent_id": "later-source-peer", "revision": None, "current": False} in objective["peer_adoption"]
assert source("inspect", "--agent-id", "later-source-peer")[1]["goal_objective_guard"]["allowed"] is False
assert (project.parent / "global.json").read_bytes() == global_before


def test_source_read_configure_verify_and_explicit_cli_override(source_app):
project, document, source, global_cli, request, _, shared = source_app
before = global_cli("inspect", override=shared)[1]
Expand Down
33 changes: 33 additions & 0 deletions tests/control_plane/test_goal_objective_revision.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,39 @@ def apply_objective(project, cli, objective):
assert service.apply(prepared["proposal"]["proposal_id"])["proposal"]["status"] == "applied"


@pytest.mark.parametrize("peer_id", ["later-peer", "later@host"])
def test_later_registered_peer_is_visible_and_requires_its_own_adoption(acceptance_goal, peer_id):
project, _, cli, _ = acceptance_goal
prepare_source(project)
apply_objective(project, cli, "Maintain the accepted export")
before = cli("inspect")[1]
registry_path = project.parent / "registry.json"
registry = json.loads(registry_path.read_text())
registry["goals"][0]["coordination"]["registered_agents"].append(peer_id)
registry_path.write_text(json.dumps(registry))

code, inspected = cli("inspect", "--agent-id", peer_id)
assert code == 0, inspected
assert inspected["provider_revision"] == before["provider_revision"], "inspection cannot persist membership or adoption"
assert inspected["goal_objective_guard"]["allowed"] is False
objective = inspected["goal_acceptance_contract"]["goal_objective"]
assert objective["digest"] == before["goal_acceptance_contract"]["goal_objective"]["digest"]
peer = next((row for row in objective["peer_adoption"] if row["agent_id"] == peer_id), None)
assert peer == {"agent_id": peer_id, "revision": None, "current": False}
assert any(row["agent_id"] == peer_id for row in objective["acceptance_gaps"])
assert cli("inspect")[1]["goal_acceptance_contract"]["goal_objective"]["peer_adoption"] == objective["peer_adoption"]
adoption = project / "later-adoption.json"
adoption.write_text(json.dumps({"revision": objective["revision"], "digest": objective["digest"],
"work_digest": inspected["objective_work_digest"], "review_summary": "Reviewed my current work against the shared objective."}))
code, adopted = cli("adopt-objective", "--agent-id", peer_id, "--document", str(adoption),
"--operation-id", "later-peer-adoption", "--expected-provider-revision", inspected["provider_revision"], "--execute")
assert code == 0, adopted
current = cli("inspect", "--agent-id", peer_id)[1]
assert current["goal_objective_guard"] is None
assert next(row for row in current["goal_acceptance_contract"]["goal_objective"]["peer_adoption"]
if row["agent_id"] == peer_id)["current"] is True


@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
Expand Down
40 changes: 39 additions & 1 deletion tests/control_plane_ts/goal_objective.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {FileAuthorityStore} from "../../loopx/control_plane/coordination/file_au
import {SqliteAuthorityStore} from "../../loopx/control_plane/coordination/sqlite_authority_store.ts";
import {authorityProjectionFixture} from "./authority_projection_fixture.ts";
import {updateGoalObjective, inspectGoalAcceptance} from "../../loopx/control_plane/goals/acceptance_authority.ts";
import {goalObjectiveWorkGuard, goalObjectiveWorkDigest, readGoalObjective, reviseGoalObjective} from "../../loopx/control_plane/goals/goal_objective.ts";
import {goalObjectiveWorkGuard, goalObjectiveWorkDigest, readGoalObjective, reviseGoalObjective, projectGoalObjective, adoptGoalObjective} from "../../loopx/control_plane/goals/goal_objective.ts";
import {projectCoreGoal} from "../../loopx/control_plane/work_items/replan_context.ts";
import {projectGoalAcceptance, goalAcceptanceWorkDigest} from "../../loopx/control_plane/goals/acceptance_contract.ts";
import type {JsonObject} from "../../loopx/control_plane/effect_program.ts";
Expand All @@ -19,6 +19,44 @@ const document = {objective: "Deliver a reliable maintained service", reason: "T
const request = {goal_id: goal, actor_agent_id: null, operation: "revise-objective", operation_id: "revision-1", document,
source_objective: {registry_objective: "Bootstrap the project"}, registered_agents: ["agent-a", "agent-b"]};

test("current membership is read-only evidence and preserves historical peers and adoption", () => {
const head = fixture();
head.goal_objective = reviseGoalObjective(head, null, "Bootstrap the project", request.registered_agents, document);
const revision = readGoalObjective(head)!.revisions.at(-1)!;
head.goal_objective = adoptGoalObjective(head, "agent-a", {revision: revision.revision, digest: revision.digest,
work_digest: goalObjectiveWorkDigest(head, "agent-a"), review_summary: "Reviewed the existing work."});
const before = structuredClone(head), retained = projectGoalObjective(head);
const current = projectGoalObjective(head, ["later-peer"])!;
assert.deepEqual(current.peer_adoption, [
{agent_id: "agent-a", revision: 1, current: true},
{agent_id: "agent-b", revision: null, current: false},
{agent_id: "later-peer", revision: null, current: false},
]);
assert.deepEqual(projectGoalObjective(head, []), retained);
assert.deepEqual(projectGoalObjective(head), retained, "callers without live membership keep the retained projection");
assert.equal(goalObjectiveWorkGuard(head, "later-peer")?.allowed, false);
const roster = Array.from({length: 130}, (_, index) => `peer-${index}`);
assert.equal((projectGoalObjective(head, roster)!.peer_adoption as unknown[]).length, 132);
assert.deepEqual(head, before, "reads cannot create revisions, membership or adoption");
});

test("current registration does not normalize or erase historical objective IDs", () => {
const head = fixture();
head.goal_objective = reviseGoalObjective(head, null, "Bootstrap the project", ["1Legacy", "Legacy.Owner"], document);
const projected = projectGoalObjective(head, ["agent-a"])!;
assert.deepEqual((projected.peer_adoption as JsonObject[]).map(peer => peer.agent_id),
["1Legacy", "Legacy.Owner", "agent-a"]);
assert.deepEqual(readGoalObjective(head)!.registered_agents, ["1Legacy", "Legacy.Owner"]);
});

for (const roster of [null, "agent-a", [1], ["invalid/peer"], ["agent-a", "agent-a"]]) {
test(`objective membership rejects malformed source facts: ${JSON.stringify(roster)}`, () => {
const head = fixture();
head.goal_objective = reviseGoalObjective(head, null, "Bootstrap the project", request.registered_agents, document);
assert.throws(() => projectGoalObjective(head, roster), /registered_agents|public-safe agent/);
});
}

for (const provider of ["file", "sqlite"] as const) test(`${provider}: shared revisions, competing writers, peer adoption and restart`, async t => {
const root = await mkdtemp(join(tmpdir(), "goal-objective-")); t.after(() => rm(root, {recursive: true, force: true}));
const open = () => provider === "file" ? new FileAuthorityStore(root, goal) : new SqliteAuthorityStore(root, goal);
Expand Down
Loading