From 0cf12017baffff4bc5cb6f8560db10704047182e Mon Sep 17 00:00:00 2001 From: Duang777 Date: Thu, 8 Oct 2026 23:14:16 +0800 Subject: [PATCH] fix(acceptance): bind authority to goal instances Signed-off-by: Duang777 --- .../control_plane/effect_runtime_handlers.ts | 1 + .../goals/acceptance_authority.ts | 296 ++++++++++++++++-- .../goals/acceptance_contract.ts | 118 ++++++- .../goals/acceptance_lifecycle.ts | 87 +++++ .../effect_runtime_handlers.test.ts | 10 + .../goal_acceptance_authority.test.ts | 153 ++++++++- 6 files changed, 629 insertions(+), 36 deletions(-) create mode 100644 loopx/control_plane/goals/acceptance_lifecycle.ts diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index f04aff9971..ece730d3d7 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -368,6 +368,7 @@ export function createEffectRuntimeHandlers( ["goal.acceptance.inspect", lazyHandler(() => import("./goals/acceptance_authority.ts"), ({inspectLocalGoalAcceptance}) => inspectLocalGoalAcceptance)], ["goal.acceptance.configure", lazyHandler(() => import("./goals/acceptance_authority.ts"), ({commitLocalGoalAcceptance}) => commitLocalGoalAcceptance)], ["goal.acceptance.verify.commit", lazyHandler(() => import("./goals/acceptance_authority.ts"), ({commitLocalGoalAcceptanceVerification}) => commitLocalGoalAcceptanceVerification)], + ["goal.acceptance.lifecycle.transition", lazyHandler(() => import("./goals/acceptance_authority.ts"), ({commitLocalGoalAcceptanceLifecycleTransition}) => commitLocalGoalAcceptanceLifecycleTransition)], ["agent.delivery_workspace.evaluate", lazyHandler(() => import("./agents/delivery_workspace.ts"), ({evaluateDeliveryWorkspace}) => evaluateDeliveryWorkspace)], [ "quota.delivery_workspace_causality.evaluate", diff --git a/loopx/control_plane/goals/acceptance_authority.ts b/loopx/control_plane/goals/acceptance_authority.ts index 09d6af9b3c..e0d217a50e 100644 --- a/loopx/control_plane/goals/acceptance_authority.ts +++ b/loopx/control_plane/goals/acceptance_authority.ts @@ -9,14 +9,19 @@ import {AuthorityStoreProtocolError, canonicalAuthorityObject, canonicalAuthorit import {CoordinationCommandReceipt} from "../coordination/command_receipt.ts"; import {withCanonicalWriter} from "../coordination/local_authority_write.ts"; import {openLocalAuthorityStore, localAuthorityOpenFailure} from "../coordination/local_authority_provider.ts"; -import {GOAL_ACCEPTANCE_SCHEMA, acceptanceKeys, acceptanceRequire, acceptanceTask, acceptanceText, acceptanceTodos, - acceptanceCompletionRequirements, goalAcceptanceTodoDigest, goalAcceptanceWorkDigest, normalizeAcceptanceResults, - normalizeGoalAcceptanceDocument, projectGoalAcceptance, readGoalAcceptance, +import {GOAL_ACCEPTANCE_OWNED_SCHEMA, GOAL_ACCEPTANCE_SCHEMA, acceptanceKeys, acceptanceRequire, acceptanceTask, + acceptanceText, acceptanceTodos, acceptanceCompletionRequirements, bindGoalAcceptanceStateOwner, + goalAcceptanceTodoDigest, goalAcceptanceWorkDigest, normalizeAcceptanceResults, + normalizeGoalAcceptanceDocument, projectGoalAcceptance, readGoalAcceptance, readGoalAcceptanceAuthority, type AcceptanceState, type AcceptanceVerification} from "./acceptance_contract.ts"; import { BARE_SHA256_PATTERN } from "../content_digest.ts"; +import {GOAL_ACCEPTANCE_LIFECYCLE_SCHEMA, parseGoalAcceptanceLifecycleTransition, parseWireExactGoalRef, + readGoalAcceptanceLifecycle, sameExactGoalRef, type GoalAcceptanceLifecycle, + type GoalAcceptanceLifecycleTransition, type WireExactGoalRef} from "./acceptance_lifecycle.ts"; const RESULT_SCHEMA = "loopx_goal_acceptance_result_v0"; const RECEIPT_SCHEMA = "loopx_goal_acceptance_operation_v0"; +const LIFECYCLE_RECEIPT_SCHEMA = "loopx_goal_acceptance_lifecycle_operation_v0"; const REQUEST_FIELDS = ["goal_id", "operation_id", "actor_agent_id", "expected_provider_revision"]; const LOCAL_FIELDS = ["runtime_root", "dry_run"]; @@ -26,7 +31,7 @@ function mutationRequest(value: unknown, verification: boolean): JsonObject { // its wire call need not impersonate an owner. Explicit Agent actors fail. if (verification && !Object.hasOwn(request, "actor_agent_id")) request.actor_agent_id = null; acceptanceKeys(request, [...REQUEST_FIELDS, ...(verification ? ["contract_digest", "revision", "results"] : ["document"])], - [...LOCAL_FIELDS, ...(verification ? ["todo_id"] : ["disable"])]); + [...LOCAL_FIELDS, "goal_ref", ...(verification ? ["todo_id"] : ["disable"])]); acceptanceRequire(request.actor_agent_id === null, "goal acceptance mutation requires the trusted owner/host; agent actors cannot configure or attest acceptance"); for (const field of ["goal_id", "operation_id", "expected_provider_revision"]) { requireAuthorityStoreId(request[field], field); @@ -34,6 +39,7 @@ function mutationRequest(value: unknown, verification: boolean): JsonObject { acceptanceText(request.operation_id, "acceptance operation id", 256); acceptanceRequire(request.dry_run === undefined || typeof request.dry_run === "boolean", "dry_run must be boolean"); if (!verification) acceptanceRequire(request.disable === undefined || typeof request.disable === "boolean", "disable must be boolean"); + if (request.goal_ref !== undefined) parseWireExactGoalRef(request.goal_ref, "acceptance request goal_ref"); return request; } function source(store: AuthorityStore, result: JsonObject): JsonObject { @@ -53,21 +59,78 @@ function receiptFor(request: JsonObject, kind: "configure" | "verify") { decode: original => ({fields: canonicalAuthorityObject(original.result, "acceptance operation result"), changed: true})}); return {identity, receipt}; } +function requestGoalRef(request: JsonObject): WireExactGoalRef | null { + return request.goal_ref === undefined + ? null + : parseWireExactGoalRef(request.goal_ref, "acceptance request goal_ref"); +} +function lifecycleFailure(reason_code: string, reason: string): JsonObject { + return {status: "failed", changed: false, reason_code, reason}; +} +function validateCurrentLifecycle(head: JsonObject, request: JsonObject): JsonObject | null { + const lifecycle = readGoalAcceptanceLifecycle(head, String(request.goal_id)); + const goalRef = requestGoalRef(request); + if (lifecycle === null) { + return null; + } + if (goalRef === null) return lifecycleFailure("goal_acceptance_goal_instance_missing", + "Exact Goal acceptance authority requires goal_ref."); + if (!sameExactGoalRef(lifecycle.goal_ref, goalRef)) return lifecycleFailure( + "goal_acceptance_goal_instance_mismatch", + "Goal acceptance authority belongs to another Goal instance.", + ); + if (lifecycle.state === "retiring") return lifecycleFailure("goal_acceptance_goal_retiring", + "The Goal instance is retiring; acceptance mutation is closed."); + return null; +} function current(loaded: AuthorityStoreLoadResult, request: JsonObject): AuthorityStoreHead | JsonObject { if (loaded.status !== "loaded") return {...loaded}; if (loaded.provider_revision !== request.expected_provider_revision) return { status: "conflict", changed: false, reason_code: "goal_acceptance_provider_revision_mismatch", conflict_kind: "provider_revision_mismatch", current_provider_revision: loaded.provider_revision}; acceptanceTodos(loaded.head, String(request.goal_id)); + const lifecycleFailure = validateCurrentLifecycle(loaded.head, request); + if (lifecycleFailure) return lifecycleFailure; return loaded; } function loaded(value: AuthorityStoreHead | JsonObject): value is AuthorityStoreHead { return "status" in value && value.status === "loaded"; } +async function replayWithCurrentProjection( + store: AuthorityStore, + result: JsonObject, + goalId: string, +): Promise { + const projectsContract = Object.hasOwn(result, "goal_acceptance_contract"); + const projectsLifecycle = Object.hasOwn(result, "goal_acceptance_lifecycle"); + if (!projectsContract && !projectsLifecycle) { + return source(store, {...result, projection_delivery: "not_required"}); + } + const current = await store.loadAuthority(); + const contract = current.status === "loaded" + ? projectGoalAcceptance(current.head, goalId) : {enabled: false, authority_status: current.status}; + const lifecycle = current.status === "loaded" + ? readGoalAcceptanceLifecycle(current.head, goalId) : null; + return source(store, {...result, + ...(projectsContract ? {goal_acceptance_contract: contract} : {}), + ...(projectsLifecycle ? {goal_acceptance_lifecycle: lifecycle} : {}), + projection_delivery: "not_required"}); +} async function commit(store: AuthorityStore, request: JsonObject, head: AuthorityStoreHead, state: AcceptanceState | null, command: ReturnType, kind: "configure" | "verify"): Promise { - const next = {...head.head}; - if (state !== null) next.goal_acceptance = state; + const next: JsonObject = {...head.head}; + const goalRef = requestGoalRef(request); + const lifecycle = readGoalAcceptanceLifecycle(head.head, String(request.goal_id)); + const committedState = goalRef !== null && lifecycle === null && state !== null + ? bindGoalAcceptanceStateOwner(state, goalRef) : state; + if (goalRef !== null && lifecycle === null) { + next.goal_acceptance_lifecycle = { + schema_version: GOAL_ACCEPTANCE_LIFECYCLE_SCHEMA, + state: "active", + goal_ref: goalRef, + }; + } + if (committedState !== null) next.goal_acceptance = committedState; const result = {goal_id: request.goal_id, operation_id: request.operation_id, goal_acceptance_contract: projectGoalAcceptance(next, String(request.goal_id))}; if (request.dry_run === true) return source(store, {status: "planned", dry_run: true, changed: false, @@ -76,12 +139,12 @@ async function commit(store: AuthorityStore, request: JsonObject, head: Authorit operation_id: String(request.operation_id), expected_provider_revision: String(request.expected_provider_revision), next_projection: next, events: [{schema_version: RECEIPT_SCHEMA, kind: `goal_acceptance_${kind}`, goal_id: request.goal_id, - operation_id: request.operation_id, revision: state?.revision ?? null, digest: state?.digest ?? null, - enabled: state?.enabled ?? false}], + operation_id: request.operation_id, revision: committedState?.revision ?? null, + digest: committedState?.digest ?? null, enabled: committedState?.enabled ?? false}], receipts: [{...command.identity, result}], }); // No Todo/Markdown display document changes in this transaction. - return source(store, {...committed, projection_delivery: "not_required"}); + return replayWithCurrentProjection(store, committed, String(request.goal_id)); } export async function configureGoalAcceptance(store: AuthorityStore, value: JsonObject): Promise { @@ -91,9 +154,11 @@ export async function configureGoalAcceptance(store: AuthorityStore, value: Json const document = disable ? null : normalizeGoalAcceptanceDocument(request.document); const command = receiptFor(request, "configure"); const replay = await command.receipt.read(store); - if (replay) return source(store, {...replay, projection_delivery: "not_required"}); + if (replay) return replayWithCurrentProjection(store, replay, String(request.goal_id)); const observation = await command.receipt.observe(store); - if (observation.kind === "receipt") return source(store, {...observation.result, projection_delivery: "not_required"}); + if (observation.kind === "receipt") { + return replayWithCurrentProjection(store, observation.result, String(request.goal_id)); + } const head = current(observation.authority, request); if (!loaded(head)) return source(store, head); const previous = readGoalAcceptance(head.head, String(request.goal_id)); @@ -117,8 +182,12 @@ export async function configureGoalAcceptance(store: AuthorityStore, value: Json "acceptance binding must reference existing Agent advancement work"); return {...binding, todo_semantic_digest: goalAcceptanceTodoDigest(todo), revision, confirmed_by: "owner" as const}; }); - state = {schema_version: GOAL_ACCEPTANCE_SCHEMA, enabled: true, revision, - digest: canonicalAuthoritySha256(document), document, bindings, verification: previous?.verification ?? null}; + const fields = {enabled: true, revision, digest: canonicalAuthoritySha256(document), + document, bindings, verification: previous?.verification ?? null}; + const goalRef = requestGoalRef(request); + state = goalRef === null + ? {schema_version: GOAL_ACCEPTANCE_SCHEMA, ...fields} + : {schema_version: GOAL_ACCEPTANCE_OWNED_SCHEMA, owner_goal_ref: goalRef, ...fields}; } else if (previous) state = {...previous, enabled: false}; return commit(store, request, head, state, command, "configure"); } @@ -134,9 +203,11 @@ export async function commitGoalAcceptanceVerification(store: AuthorityStore, va const results = normalizeAcceptanceResults(request.results); const command = receiptFor(request, "verify"); const replay = await command.receipt.read(store); - if (replay) return source(store, {...replay, projection_delivery: "not_required"}); + if (replay) return replayWithCurrentProjection(store, replay, String(request.goal_id)); const observation = await command.receipt.observe(store); - if (observation.kind === "receipt") return source(store, {...observation.result, projection_delivery: "not_required"}); + if (observation.kind === "receipt") { + return replayWithCurrentProjection(store, 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); @@ -157,12 +228,190 @@ export async function commitGoalAcceptanceVerification(store: AuthorityStore, va return commit(store, request, head, {...state, verification}, command, "verify"); } +function lifecycleRequest(value: unknown): { + request: JsonObject; + transition: GoalAcceptanceLifecycleTransition; +} { + const request = canonicalAuthorityObject(value, "goal acceptance lifecycle request"); + acceptanceKeys(request, ["goal_id", "operation_id", "actor_agent_id", "transition"], ["runtime_root"]); + acceptanceRequire(request.actor_agent_id === null, + "goal acceptance lifecycle transition requires the trusted source owner"); + const goalId = requireAuthorityStoreId(request.goal_id, "goal_id"); + acceptanceText(request.operation_id, "goal acceptance lifecycle operation id", 256); + const transition = parseGoalAcceptanceLifecycleTransition(request.transition); + const refs = transition.kind === "activate_successor" + ? [transition.retired_goal_ref, transition.goal_ref] : [transition.goal_ref]; + acceptanceRequire(refs.every(goalRef => goalRef.goal_id === goalId), + "goal acceptance lifecycle transition changed the Goal alias"); + return {request, transition}; +} + +function lifecycleReceiptFor(request: JsonObject, transition: GoalAcceptanceLifecycleTransition) { + const identity = { + schema_version: LIFECYCLE_RECEIPT_SCHEMA, + operation_id: String(request.operation_id), + goal_id: String(request.goal_id), + request_sha256: canonicalAuthoritySha256({transition}), + }; + const receipt = new CoordinationCommandReceipt({ + result_schema: RESULT_SCHEMA, + identity, + failure, + decode: original => { + const fields = canonicalAuthorityObject(original.result, "goal acceptance lifecycle result"); + acceptanceRequire(typeof fields.changed === "boolean", + "goal acceptance lifecycle receipt omitted its change decision"); + return {fields, changed: fields.changed}; + }, + }); + return {identity, receipt}; +} + +type LifecyclePlan = + | Readonly<{ + kind: "commit"; + lifecycle: GoalAcceptanceLifecycle; + acceptance: AcceptanceState | null; + }> + | Readonly<{kind: "no_change"; lifecycle: GoalAcceptanceLifecycle}> + | Readonly<{kind: "failure"; reason_code: string; reason: string}>; + +function lifecycleMismatch(reason: string): LifecyclePlan { + return {kind: "failure", reason_code: "goal_acceptance_goal_instance_mismatch", reason}; +} + +function planLifecycleTransition( + head: JsonObject, + goalId: string, + transition: GoalAcceptanceLifecycleTransition, +): LifecyclePlan { + const current = readGoalAcceptanceLifecycle(head, goalId); + if (transition.kind === "bind_existing") { + if (current === null) { + const acceptance = readGoalAcceptance(head, goalId); + return { + kind: "commit", + lifecycle: { + schema_version: GOAL_ACCEPTANCE_LIFECYCLE_SCHEMA, + state: "active", + goal_ref: transition.goal_ref, + }, + acceptance: acceptance === null + ? null + : bindGoalAcceptanceStateOwner(acceptance, transition.goal_ref), + }; + } + if (!sameExactGoalRef(current.goal_ref, transition.goal_ref)) { + return lifecycleMismatch("Canonical acceptance is already bound to another Goal instance."); + } + return current.state === "active" + ? {kind: "no_change", lifecycle: current} + : {kind: "failure", reason_code: "goal_acceptance_goal_retiring", + reason: "A retiring Goal instance cannot be rebound as active."}; + } + if (current === null) { + return {kind: "failure", reason_code: "goal_acceptance_lifecycle_unbound", + reason: "Bind the existing Goal instance before changing its acceptance lifecycle."}; + } + readGoalAcceptanceAuthority(head, goalId); + if (transition.kind === "retire") { + if (!sameExactGoalRef(current.goal_ref, transition.goal_ref)) { + return lifecycleMismatch("Only the active Goal instance can begin retirement."); + } + return current.state === "retiring" + ? {kind: "no_change", lifecycle: current} + : {kind: "commit", lifecycle: {...current, state: "retiring"}, + acceptance: readGoalAcceptance(head, goalId)}; + } + if (current.state !== "retiring" + || !sameExactGoalRef(current.goal_ref, transition.retired_goal_ref)) { + return lifecycleMismatch("Goal acceptance successor activation requires its exact retiring predecessor."); + } + return { + kind: "commit", + lifecycle: { + schema_version: GOAL_ACCEPTANCE_LIFECYCLE_SCHEMA, + state: "active", + goal_ref: transition.goal_ref, + }, + acceptance: readGoalAcceptance(head, goalId), + }; +} + +export async function transitionGoalAcceptanceLifecycle( + store: AuthorityStore, + value: JsonObject, +): Promise { + const {request, transition} = lifecycleRequest(value); + const goalId = String(request.goal_id); + const command = lifecycleReceiptFor(request, transition); + const replay = await command.receipt.read(store); + if (replay) return replayWithCurrentProjection(store, replay, goalId); + const observation = await command.receipt.observe(store); + if (observation.kind === "receipt") { + return replayWithCurrentProjection(store, observation.result, goalId); + } + if (observation.authority.status !== "loaded") return source(store, {...observation.authority}); + acceptanceTodos(observation.authority.head, goalId); + const plan = planLifecycleTransition(observation.authority.head, goalId, transition); + if (plan.kind === "failure") return source(store, failure(plan.reason_code, plan.reason)); + const result = { + goal_id: goalId, + operation_id: request.operation_id, + changed: plan.kind === "commit", + goal_acceptance_lifecycle: plan.lifecycle, + goal_acceptance_contract: projectGoalAcceptance( + plan.kind === "commit" + ? { + ...observation.authority.head, + goal_acceptance_lifecycle: plan.lifecycle, + ...(plan.acceptance === null ? {} : {goal_acceptance: plan.acceptance}), + } + : observation.authority.head, + goalId, + ), + }; + const next: JsonObject = {...observation.authority.head, goal_acceptance_lifecycle: plan.lifecycle}; + if (plan.kind === "commit" && plan.acceptance !== null) next.goal_acceptance = plan.acceptance; + const committed = await command.receipt.commit(store, { + operation_id: String(request.operation_id), + expected_provider_revision: observation.authority.provider_revision, + next_projection: next, + events: plan.kind === "commit" ? [{ + schema_version: LIFECYCLE_RECEIPT_SCHEMA, + kind: `goal_acceptance_lifecycle_${transition.kind}`, + goal_id: goalId, + operation_id: request.operation_id, + goal_ref: plan.lifecycle.goal_ref, + state: plan.lifecycle.state, + }] : [], + receipts: [{...command.identity, result}], + }); + return replayWithCurrentProjection(store, committed, goalId); +} + /** Private readback. Only goal_acceptance_contract is safe to project publicly. */ -export async function inspectGoalAcceptance(store: AuthorityStore, goalId: string, todoId?: string): Promise { +export async function inspectGoalAcceptance( + store: AuthorityStore, + goalId: string, + todoId?: string, + goalRef?: WireExactGoalRef, +): Promise { const head = await store.loadAuthority(); if (head.status !== "loaded") return source(store, {...head}); + const lifecycle = readGoalAcceptanceLifecycle(head.head, goalId); + if (lifecycle !== null) { + acceptanceRequire(goalRef !== undefined, "exact Goal acceptance inspection requires goal_ref"); + acceptanceRequire(sameExactGoalRef(lifecycle.goal_ref, goalRef), + "goal acceptance inspection belongs to another Goal instance"); + } else { + acceptanceRequire(goalRef === undefined || goalRef.goal_id === goalId, + "goal acceptance inspection Goal identity mismatch"); + } const todos = acceptanceTodos(head.head, goalId); - const state = readGoalAcceptance(head.head, goalId); + const authority = readGoalAcceptanceAuthority(head.head, goalId); + const state = authority.kind === "legacy" ? authority.state + : authority.owner_matches && authority.lifecycle.state === "active" ? authority.state : null; const tasks = [...todos.entries()].filter(([key]) => todoId === undefined || key === todoId).map(([key, todo]) => ({ todo_id: key, todo_semantic_digest: goalAcceptanceTodoDigest(todo), ...(state?.enabled ? acceptanceTask(key, todo, state) : {state: "unbound", criterion_ids: [], applicable: false}), @@ -175,7 +424,7 @@ export async function inspectGoalAcceptance(store: AuthorityStore, goalId: strin goal_acceptance_contract: projectGoalAcceptance(head.head, goalId)}); } -async function local(value: unknown, kind: "inspect" | "configure" | "verify"): Promise { +async function local(value: unknown, kind: "inspect" | "configure" | "verify" | "lifecycle"): Promise { let store: AuthorityStore | undefined; try { const request = canonicalAuthorityObject(value, "local acceptance request"); @@ -185,8 +434,12 @@ async function local(value: unknown, kind: "inspect" | "configure" | "verify"): const run = async () => { store = await openLocalAuthorityStore(root, goalId); if (kind === "inspect") return inspectGoalAcceptance(store, goalId, - request.todo_id == null ? undefined : requireAuthorityStoreId(request.todo_id, "todo_id")); - return kind === "configure" ? configureGoalAcceptance(store, request) : commitGoalAcceptanceVerification(store, request); + request.todo_id == null ? undefined : requireAuthorityStoreId(request.todo_id, "todo_id"), + request.goal_ref === undefined ? undefined : parseWireExactGoalRef(request.goal_ref, "acceptance request goal_ref")); + if (kind === "configure") return configureGoalAcceptance(store, request); + return kind === "verify" + ? commitGoalAcceptanceVerification(store, request) + : transitionGoalAcceptanceLifecycle(store, request); }; return kind === "inspect" ? await run() : await withCanonicalWriter(root, goalId, request.dry_run === true, run); } catch (error) { @@ -211,3 +464,6 @@ export async function commitLocalGoalAcceptance(value: unknown): Promise { return local(value, "verify"); } +export async function commitLocalGoalAcceptanceLifecycleTransition(value: unknown): Promise { + return local(value, "lifecycle"); +} diff --git a/loopx/control_plane/goals/acceptance_contract.ts b/loopx/control_plane/goals/acceptance_contract.ts index e6420f21e5..0ece3e8d36 100644 --- a/loopx/control_plane/goals/acceptance_contract.ts +++ b/loopx/control_plane/goals/acceptance_contract.ts @@ -7,8 +7,11 @@ import {indexCoordinationProjectionTodos, validateCoordinationTodoReadModel} fro import {COMPLETION_VALIDATION_BINDING_RECEIPT_SCHEMA, completionValidationRevisionHistory} from "../todos/completion_validation_revision.ts"; import { BARE_SHA256_PATTERN } from "../content_digest.ts"; +import {parseWireExactGoalRef, readGoalAcceptanceLifecycle, sameExactGoalRef, type GoalAcceptanceLifecycle, + type WireExactGoalRef} from "./acceptance_lifecycle.ts"; export const GOAL_ACCEPTANCE_SCHEMA = "loopx_goal_acceptance_v0"; +export const GOAL_ACCEPTANCE_OWNED_SCHEMA = "loopx_goal_acceptance_v1"; export interface AcceptanceCriterion extends JsonObject { id: string; description: string; @@ -46,8 +49,7 @@ export interface AcceptanceVerification extends JsonObject { todo_id: string | null; results: AcceptanceResult[]; } -export interface AcceptanceState extends JsonObject { - schema_version: typeof GOAL_ACCEPTANCE_SCHEMA; +interface AcceptanceStateFields extends JsonObject { enabled: boolean; revision: number; digest: string; @@ -55,6 +57,22 @@ export interface AcceptanceState extends JsonObject { bindings: AcceptanceBinding[]; verification: AcceptanceVerification | null; } +export interface LegacyAcceptanceState extends AcceptanceStateFields { + schema_version: typeof GOAL_ACCEPTANCE_SCHEMA; +} +export interface OwnedAcceptanceState extends AcceptanceStateFields { + schema_version: typeof GOAL_ACCEPTANCE_OWNED_SCHEMA; + owner_goal_ref: WireExactGoalRef; +} +export type AcceptanceState = LegacyAcceptanceState | OwnedAcceptanceState; +export type GoalAcceptanceAuthority = + | Readonly<{kind: "legacy"; state: LegacyAcceptanceState | null}> + | Readonly<{ + kind: "exact"; + lifecycle: GoalAcceptanceLifecycle; + state: OwnedAcceptanceState | null; + owner_matches: boolean; + }>; export type AcceptanceBindingState = "ready" | "unbound" | "stale"; export interface AcceptanceTask extends JsonObject { todo_id: string; @@ -299,13 +317,24 @@ export function goalAcceptanceWorkDigest(head: JsonObject, goalId: string, scope } /** Absent is the sole legacy/off shortcut; malformed present state fails closed. */ -export function readGoalAcceptance(head: JsonObject, goalId: string): AcceptanceState | null { - if (!Object.hasOwn(head, "goal_acceptance")) return null; +export function readGoalAcceptanceAuthority(head: JsonObject, goalId: string): GoalAcceptanceAuthority { + const lifecycle = readGoalAcceptanceLifecycle(head, goalId); + if (!Object.hasOwn(head, "goal_acceptance")) { + return lifecycle === null + ? {kind: "legacy", state: null} + : {kind: "exact", lifecycle, state: null, owner_matches: false}; + } acceptanceRequire(head.goal_id === goalId, "acceptance Goal identity mismatch"); const state = canonicalAuthorityObject(head.goal_acceptance, "goal_acceptance"); - acceptanceKeys(state, ["schema_version", "enabled", "revision", "digest", "document", "bindings", "verification"]); - acceptanceRequire(state.schema_version === GOAL_ACCEPTANCE_SCHEMA && typeof state.enabled === "boolean" && + const owned = state.schema_version === GOAL_ACCEPTANCE_OWNED_SCHEMA; + acceptanceKeys(state, ["schema_version", "enabled", "revision", "digest", "document", "bindings", "verification", + ...(owned ? ["owner_goal_ref"] : [])]); + acceptanceRequire((state.schema_version === GOAL_ACCEPTANCE_SCHEMA || owned) && typeof state.enabled === "boolean" && Number.isSafeInteger(state.revision) && Number(state.revision) > 0, "invalid acceptance state version"); + acceptanceRequire((lifecycle === null) === !owned, + "goal acceptance state and lifecycle versions must be migrated atomically"); + const ownerGoalRef = owned ? parseWireExactGoalRef(state.owner_goal_ref, "goal acceptance owner_goal_ref") : null; + if (ownerGoalRef) acceptanceRequire(ownerGoalRef.goal_id === goalId, "acceptance owner Goal identity mismatch"); const document = normalizeGoalAcceptanceDocument(state.document); acceptanceRequire(state.digest === canonicalAuthoritySha256(document), "acceptance contract digest mismatch"); const bindings = list(state.bindings, "canonical bindings", 4096).map(value => { @@ -342,7 +371,53 @@ export function readGoalAcceptance(head: JsonObject, goalId: string): Acceptance verification = {...receipt, operation_id: operationId, todo_id: todoId, results: normalizeAcceptanceResults(receipt.results, expectedIds)} as AcceptanceVerification; } - return {...state, document, bindings, verification} as AcceptanceState; + if (lifecycle === null) { + const legacyState: LegacyAcceptanceState = { + schema_version: GOAL_ACCEPTANCE_SCHEMA, + enabled: Boolean(state.enabled), + revision: Number(state.revision), + digest: String(state.digest), + document, + bindings, + verification, + }; + return {kind: "legacy", state: legacyState}; + } + acceptanceRequire(ownerGoalRef !== null, "exact goal acceptance requires an owner Goal reference"); + const exactState: OwnedAcceptanceState = { + schema_version: GOAL_ACCEPTANCE_OWNED_SCHEMA, + owner_goal_ref: ownerGoalRef, + enabled: Boolean(state.enabled), + revision: Number(state.revision), + digest: String(state.digest), + document, + bindings, + verification, + }; + return {kind: "exact", lifecycle, state: exactState, + owner_matches: sameExactGoalRef(lifecycle.goal_ref, exactState.owner_goal_ref)}; +} + +export function readGoalAcceptance(head: JsonObject, goalId: string): AcceptanceState | null { + const authority = readGoalAcceptanceAuthority(head, goalId); + if (authority.kind === "legacy") return authority.state; + return authority.owner_matches ? authority.state : null; +} + +export function bindGoalAcceptanceStateOwner( + state: AcceptanceState, + ownerGoalRef: WireExactGoalRef, +): OwnedAcceptanceState { + if (state.schema_version === GOAL_ACCEPTANCE_OWNED_SCHEMA) { + acceptanceRequire(sameExactGoalRef(state.owner_goal_ref, ownerGoalRef), + "goal acceptance state belongs to another Goal instance"); + return state; + } + return { + ...state, + schema_version: GOAL_ACCEPTANCE_OWNED_SCHEMA, + owner_goal_ref: ownerGoalRef, + }; } export function acceptanceTask(todoId: string, todo: JsonObject | undefined, state: AcceptanceState): AcceptanceTask { @@ -382,8 +457,15 @@ export function normalizeAcceptanceResults(value: unknown, expectedIds?: readonl } export function projectGoalAcceptance(head: JsonObject, goalId: string): JsonObject { - const state = readGoalAcceptance(head, goalId); - if (!state?.enabled) return {enabled: false}; + const authority = readGoalAcceptanceAuthority(head, goalId); + const exactProjection = authority.kind === "exact" + ? {lifecycle_state: authority.lifecycle.state, goal_ref: authority.lifecycle.goal_ref} + : {}; + const state = authority.kind === "legacy" ? authority.state + : authority.owner_matches ? authority.state : null; + if (!state?.enabled || authority.kind === "exact" && authority.lifecycle.state !== "active") { + return {enabled: false, ...exactProjection}; + } const todos = acceptanceTodos(head, goalId); const taskIds = new Set(state.document.scope?.kind === "selected_work" ? state.document.scope.todo_ids : [...todos.values()].filter(advancement).map(todo => String(todo.todo_id))); @@ -400,7 +482,8 @@ export function projectGoalAcceptance(head: JsonObject, goalId: string): JsonObj else status = "accepted"; } if (held.length) status = "held"; - return {...(state.document.scope === undefined ? {} : {scope: state.document.scope}), enabled: true, revision: state.revision, digest: state.digest, objective: state.document.objective, + return {...exactProjection, ...(state.document.scope === undefined ? {} : {scope: state.document.scope}), + enabled: true, revision: state.revision, digest: state.digest, objective: state.document.objective, non_goals: state.document.non_goals, criteria: state.document.criteria.map(({id, description}) => ({id, description})), tasks, held_todo_ids: held.map(task => task.todo_id), status, verification: receipt ? {operation_id: receipt.operation_id, contract_revision: receipt.contract_revision, @@ -427,14 +510,19 @@ export function projectGoalAcceptanceWorkGuards( goalId: string, todoIds: readonly string[], ): Record { - const state = readGoalAcceptance(head, goalId); + const authority = readGoalAcceptanceAuthority(head, goalId); + const state = authority.kind === "legacy" ? authority.state + : authority.owner_matches ? authority.state : null; if (!state?.enabled) return {}; const todos = acceptanceTodos(head, goalId); + const retiring = authority.kind === "exact" && authority.lifecycle.state === "retiring"; return Object.fromEntries(todoIds.flatMap(todoId => { const todo = todos.get(todoId); if (!acceptanceRequired(state, todoId, todo)) return []; const task = acceptanceTask(todoId, todo, state); - return [[todoId, {allowed: task.state === "ready", ...task, + return [[todoId, {allowed: !retiring && task.state === "ready", ...task, + ...(retiring ? {reason_code: "goal_acceptance_goal_retiring", + reason: "The Goal instance is retiring; acceptance cannot authorize more work."} : {}), revision: state.revision, digest: state.digest}]]; })); } @@ -442,7 +530,9 @@ export function projectGoalAcceptanceWorkGuards( /** Trusted execution adapter only. Run these commands at the inspected provider * revision and commit fresh results with completion in that same provider CAS. */ export function acceptanceCompletionRequirements(head: JsonObject, goalId: string, todoId: string): AcceptanceCompletionRequirements | null { - const state = readGoalAcceptance(head, goalId); + const authority = readGoalAcceptanceAuthority(head, goalId); + const state = authority.kind === "legacy" ? authority.state + : authority.owner_matches ? authority.state : null; if (!state?.enabled) return null; const todo = acceptanceTodos(head, goalId).get(todoId); acceptanceRequire(todo, "acceptance completion Todo is missing"); @@ -451,6 +541,8 @@ export function acceptanceCompletionRequirements(head: JsonObject, goalId: strin // may rewrite. Its remaining job is to decide which *unbound* work must be // held, so it stays as the fallback for Todos the owner never bound. if (!acceptanceRequired(state, todoId, todo)) return null; + acceptanceRequire(authority.kind !== "exact" || authority.lifecycle.state === "active", + "goal_acceptance_goal_retiring"); const task = acceptanceTask(todoId, todo, state); acceptanceRequire(task.state === "ready", task.reason_code); return {contract_revision: state.revision, contract_digest: state.digest, todo_id: todoId, diff --git a/loopx/control_plane/goals/acceptance_lifecycle.ts b/loopx/control_plane/goals/acceptance_lifecycle.ts new file mode 100644 index 0000000000..886dce3d29 --- /dev/null +++ b/loopx/control_plane/goals/acceptance_lifecycle.ts @@ -0,0 +1,87 @@ +import type {JsonObject} from "../effect_program.ts"; +import {AuthorityStoreProtocolError, canonicalAuthorityObject} from "../coordination/authority_store_codec.ts"; +import {parseExactGoalRef} from "./goal_instance_identity.ts"; + +export const GOAL_ACCEPTANCE_LIFECYCLE_SCHEMA = "loopx_goal_acceptance_lifecycle_v0"; + +export interface WireExactGoalRef extends JsonObject { + goal_id: string; + goal_instance_id: string; +} + +export interface GoalAcceptanceLifecycle extends JsonObject { + schema_version: typeof GOAL_ACCEPTANCE_LIFECYCLE_SCHEMA; + state: "active" | "retiring"; + goal_ref: WireExactGoalRef; +} + +export type GoalAcceptanceLifecycleTransition = + | Readonly<{kind: "bind_existing"; goal_ref: WireExactGoalRef}> + | Readonly<{kind: "retire"; goal_ref: WireExactGoalRef}> + | Readonly<{ + kind: "activate_successor"; + retired_goal_ref: WireExactGoalRef; + goal_ref: WireExactGoalRef; + }>; + +function requireLifecycle(condition: unknown, message: string): asserts condition { + if (!condition) throw new AuthorityStoreProtocolError(message); +} + +export function parseWireExactGoalRef(value: unknown, label: string): WireExactGoalRef { + const parsed = parseExactGoalRef(value); + requireLifecycle(parsed.kind === "parsed", `${label} must be an exact Goal reference`); + return { + goal_id: parsed.value.goalId.value, + goal_instance_id: parsed.value.goalInstanceId.value, + }; +} + +export function sameExactGoalRef(left: WireExactGoalRef, right: WireExactGoalRef): boolean { + return left.goal_id === right.goal_id && left.goal_instance_id === right.goal_instance_id; +} + +export function readGoalAcceptanceLifecycle(head: JsonObject, goalId: string): GoalAcceptanceLifecycle | null { + if (!Object.hasOwn(head, "goal_acceptance_lifecycle")) return null; + const raw = canonicalAuthorityObject(head.goal_acceptance_lifecycle, "goal acceptance lifecycle"); + requireLifecycle( + Object.keys(raw).length === 3 + && Object.hasOwn(raw, "schema_version") + && Object.hasOwn(raw, "state") + && Object.hasOwn(raw, "goal_ref"), + "goal acceptance lifecycle fields are missing or unsupported", + ); + requireLifecycle(raw.schema_version === GOAL_ACCEPTANCE_LIFECYCLE_SCHEMA, + "unsupported goal acceptance lifecycle version"); + requireLifecycle(raw.state === "active" || raw.state === "retiring", + "invalid goal acceptance lifecycle state"); + const goalRef = parseWireExactGoalRef(raw.goal_ref, "goal acceptance lifecycle goal_ref"); + requireLifecycle(goalRef.goal_id === goalId && head.goal_id === goalId, + "goal acceptance lifecycle Goal identity mismatch"); + return { + schema_version: GOAL_ACCEPTANCE_LIFECYCLE_SCHEMA, + state: raw.state, + goal_ref: goalRef, + }; +} + +export function parseGoalAcceptanceLifecycleTransition(value: unknown): GoalAcceptanceLifecycleTransition { + const raw = canonicalAuthorityObject(value, "goal acceptance lifecycle transition"); + if (raw.kind === "bind_existing" || raw.kind === "retire") { + requireLifecycle(Object.keys(raw).length === 2 && Object.hasOwn(raw, "goal_ref"), + "goal acceptance lifecycle transition fields are missing or unsupported"); + return {kind: raw.kind, goal_ref: parseWireExactGoalRef(raw.goal_ref, "transition goal_ref")}; + } + requireLifecycle(raw.kind === "activate_successor", + "unsupported goal acceptance lifecycle transition"); + requireLifecycle(Object.keys(raw).length === 3 + && Object.hasOwn(raw, "retired_goal_ref") && Object.hasOwn(raw, "goal_ref"), + "goal acceptance lifecycle transition fields are missing or unsupported"); + const retiredGoalRef = parseWireExactGoalRef(raw.retired_goal_ref, "transition retired_goal_ref"); + const goalRef = parseWireExactGoalRef(raw.goal_ref, "transition goal_ref"); + requireLifecycle(retiredGoalRef.goal_id === goalRef.goal_id, + "goal acceptance successor must preserve the Goal alias"); + requireLifecycle(!sameExactGoalRef(retiredGoalRef, goalRef), + "goal acceptance successor must use a new Goal instance"); + return {kind: "activate_successor", retired_goal_ref: retiredGoalRef, goal_ref: goalRef}; +} diff --git a/tests/control_plane_ts/effect_runtime_handlers.test.ts b/tests/control_plane_ts/effect_runtime_handlers.test.ts index 44fab13fed..3c14d35d68 100644 --- a/tests/control_plane_ts/effect_runtime_handlers.test.ts +++ b/tests/control_plane_ts/effect_runtime_handlers.test.ts @@ -155,6 +155,16 @@ test("runtime exposes the source-session lifetime decisions", async () => { ); }); +test("runtime exposes the canonical Goal acceptance lifecycle transition", async () => { + const result = await dispatchEffectRuntimeMethod( + handlers, + "goal.acceptance.lifecycle.transition", + {}, + ) as Record; + assert.equal(result.status, "failed"); + assert.match(String(result.reason), /runtime_root/); +}); + test("runtime boundary registers the quota monitor-poll transaction", async () => { await assert.rejects( dispatchEffectRuntimeMethod(handlers, "quota.monitor_poll.commit", {}), diff --git a/tests/control_plane_ts/goal_acceptance_authority.test.ts b/tests/control_plane_ts/goal_acceptance_authority.test.ts index 459a28596d..04828750e0 100644 --- a/tests/control_plane_ts/goal_acceptance_authority.test.ts +++ b/tests/control_plane_ts/goal_acceptance_authority.test.ts @@ -20,12 +20,15 @@ import {shadowManagementStatePath} from "../../loopx/control_plane/coordination/ import {authorityProjectionFixture, todoFixtureRecord} from "./authority_projection_fixture.ts"; import {acceptanceCompletionRequirements, acceptanceWorkGuard, goalAcceptanceTodoDigest, goalAcceptanceWorkDigest, normalizeGoalAcceptanceDocument, projectGoalAcceptance, readGoalAcceptance, validateAcceptanceCompletion} from "../../loopx/control_plane/goals/acceptance_contract.ts"; -import {commitGoalAcceptanceVerification, commitLocalGoalAcceptance, commitLocalGoalAcceptanceVerification, - configureGoalAcceptance, inspectGoalAcceptance, inspectLocalGoalAcceptance} from "../../loopx/control_plane/goals/acceptance_authority.ts"; +import {commitGoalAcceptanceVerification, commitLocalGoalAcceptance, commitLocalGoalAcceptanceLifecycleTransition, + commitLocalGoalAcceptanceVerification, configureGoalAcceptance, inspectGoalAcceptance, inspectLocalGoalAcceptance, + transitionGoalAcceptanceLifecycle} from "../../loopx/control_plane/goals/acceptance_authority.ts"; import {decodeCompletionValidationRevision, planCompletionValidationRevision} from "../../loopx/control_plane/todos/completion_validation_revision.ts"; const goal = "goal-acceptance-test"; +const goalA = {goal_id: goal, goal_instance_id: "ginst_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}; +const goalB = {goal_id: goal, goal_instance_id: "ginst_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}; test("documented owner configuration satisfies the canonical acceptance contract", async () => { const reference = await readFile(new URL("../../docs/reference/goal-acceptance-observations.md", import.meta.url), "utf8"); const example = reference.match(/```json\n([\s\S]*?)\n```/); @@ -177,6 +180,18 @@ async function verifyRequest(store: AuthorityStore, extra: JsonObject = {}): Pro revision: basis.revision, contract_digest: basis.contract_digest, results: ["outcome", "prerequisite"].map(criterion_id => ({criterion_id, passed: true, exit_code: 0})), ...extra}; } +async function lifecycleTransition( + store: AuthorityStore, + transition: JsonObject, + operationId = randomUUID(), +): Promise { + return transitionGoalAcceptanceLifecycle(store, { + goal_id: goal, + actor_agent_id: null, + operation_id: operationId, + transition, + }); +} async function update(store: AuthorityStore, todoId: string, patch: JsonObject) { const current = await head(store); const previous = (current.head.todos as JsonObject[]).find(item => item.todo_id === todoId); @@ -216,6 +231,126 @@ async function fixture(t: TestContext, provider: typeof providers[number]): Prom for (const provider of providers) { const options = {skip: (provider === "postgresql" && !process.env.LOOPX_TEST_POSTGRES_URL) || (provider === "sqlite" && !sqliteQualified)}; + test(`${provider}: first exact acceptance mutation binds the current instance atomically`, options, async t => { + const store = await fixture(t, provider); await seed(store); + const request = await configureRequest(store, {goal_ref: goalA}); + const before = await head(store); + assert.equal((await configureGoalAcceptance(store, {...request, dry_run: true})).status, "planned"); + assert.deepEqual(await head(store), before); + assert.equal((await configureGoalAcceptance(store, request)).status, "applied"); + const current = await head(store); + assert.deepEqual(current.head.goal_acceptance_lifecycle, { + schema_version: "loopx_goal_acceptance_lifecycle_v0", + state: "active", + goal_ref: goalA, + }); + assert.deepEqual((current.head.goal_acceptance as JsonObject).owner_goal_ref, goalA); + assert.equal((await inspectGoalAcceptance(store, goal, undefined, goalA)).status, "loaded"); + }); + test(`${provider}: exact lifecycle migration fences retirement and same-alias replacement`, options, async t => { + const store = await fixture(t, provider); await seed(store); + const legacyConfigure = await configureRequest(store); + assert.equal((await configureGoalAcceptance(store, legacyConfigure)).status, "applied"); + const legacyVerification = await verifyRequest(store); + assert.equal((await commitGoalAcceptanceVerification(store, legacyVerification)).status, "applied"); + + const bindOperation = randomUUID(); + assert.equal((await lifecycleTransition(store, {kind: "bind_existing", goal_ref: goalA}, bindOperation)).status, "applied"); + const bound = await head(store); + assert.deepEqual(bound.head.goal_acceptance_lifecycle, { + schema_version: "loopx_goal_acceptance_lifecycle_v0", + state: "active", + goal_ref: goalA, + }); + assert.deepEqual((bound.head.goal_acceptance as JsonObject).owner_goal_ref, goalA); + assert.equal((bound.head.goal_acceptance as JsonObject).schema_version, "loopx_goal_acceptance_v1"); + assert.equal(acceptanceWorkGuard(bound.head, goal, "todo_first")?.allowed, true); + assert.equal(projectGoalAcceptance(bound.head, goal).status, "accepted", + "migration preserves A's verification as A history"); + await assert.rejects(inspectGoalAcceptance(store, goal), /requires goal_ref/); + assert.equal((await inspectGoalAcceptance(store, goal, undefined, goalA)).status, "loaded"); + assert.equal((await configureGoalAcceptance(store, await configureRequest(store))).reason_code, + "goal_acceptance_goal_instance_missing"); + const noChangeOperation = randomUUID(); + assert.equal((await lifecycleTransition(store, + {kind: "bind_existing", goal_ref: goalA}, noChangeOperation)).status, "no_change"); + assert.equal((await store.readReceipt(noChangeOperation)).status, "found"); + assert.equal((await lifecycleTransition(store, { + kind: "activate_successor", + retired_goal_ref: goalA, + goal_ref: goalB, + })).reason_code, "goal_acceptance_goal_instance_mismatch"); + + const staleWriter = await configureRequest(store, {goal_ref: goalA}); + assert.equal((await lifecycleTransition(store, {kind: "retire", goal_ref: goalA})).status, "applied"); + const retiring = await head(store); + assert.equal((retiring.head.goal_acceptance_lifecycle as JsonObject).state, "retiring"); + assert.equal(acceptanceWorkGuard(retiring.head, goal, "todo_first")?.allowed, false); + assert.equal(acceptanceWorkGuard(retiring.head, goal, "todo_first")?.reason_code, + "goal_acceptance_goal_retiring"); + assert.throws(() => acceptanceCompletionRequirements(retiring.head, goal, "todo_first"), + /goal_acceptance_goal_retiring/); + assert.equal((await configureGoalAcceptance(store, staleWriter)).status, "conflict"); + + const retiringConfigure = await configureRequest(store, {goal_ref: goalA}); + assert.equal((await configureGoalAcceptance(store, retiringConfigure)).reason_code, + "goal_acceptance_goal_retiring"); + const retiringVerification = {...legacyVerification, operation_id: randomUUID(), goal_ref: goalA, + expected_provider_revision: retiringConfigure.expected_provider_revision}; + assert.equal((await commitGoalAcceptanceVerification(store, retiringVerification)).reason_code, + "goal_acceptance_goal_retiring"); + const replayWhileRetiring = await configureGoalAcceptance(store, legacyConfigure); + assert.equal(replayWhileRetiring.status, "replayed"); + assert.deepEqual(replayWhileRetiring.goal_acceptance_contract, { + enabled: false, + lifecycle_state: "retiring", + goal_ref: goalA, + }); + + assert.equal((await lifecycleTransition(store, { + kind: "activate_successor", + retired_goal_ref: goalA, + goal_ref: goalB, + })).status, "applied"); + const replaced = await head(store); + assert.deepEqual(projectGoalAcceptance(replaced.head, goal), { + enabled: false, + lifecycle_state: "active", + goal_ref: goalB, + }); + assert.equal(acceptanceWorkGuard(replaced.head, goal, "todo_first"), null); + const historicalReplay = await configureGoalAcceptance(store, legacyConfigure); + assert.equal(historicalReplay.status, "replayed"); + assert.deepEqual(historicalReplay.goal_acceptance_contract, { + enabled: false, + lifecycle_state: "active", + goal_ref: goalB, + }); + const lifecycleReplay = await lifecycleTransition(store, + {kind: "bind_existing", goal_ref: goalA}, bindOperation); + assert.equal(lifecycleReplay.status, "replayed"); + assert.deepEqual(lifecycleReplay.goal_acceptance_lifecycle, { + schema_version: "loopx_goal_acceptance_lifecycle_v0", + state: "active", + goal_ref: goalB, + }); + assert.equal((await lifecycleTransition(store, { + kind: "activate_successor", + retired_goal_ref: goalA, + goal_ref: goalB, + })).reason_code, "goal_acceptance_goal_instance_mismatch", + "only the original durable receipt may replay an already active successor"); + + const staleInstance = await configureRequest(store, {goal_ref: goalA}); + assert.equal((await configureGoalAcceptance(store, staleInstance)).reason_code, + "goal_acceptance_goal_instance_mismatch"); + const replacement = await configureRequest(store, {goal_ref: goalB}); + assert.equal((await configureGoalAcceptance(store, replacement)).status, "applied"); + const replacementHead = await head(store); + assert.deepEqual((replacementHead.head.goal_acceptance as JsonObject).owner_goal_ref, goalB); + assert.equal((replacementHead.head.goal_acceptance as JsonObject).revision, 1); + assert.equal(acceptanceWorkGuard(replacementHead.head, goal, "todo_first")?.allowed, true); + }); test(`${provider}: selected work isolates admission and verification without weakening its own guard`, options, async t => { const store = await fixture(t, provider); await seed(store); const doc = {...document(), scope: {kind: "selected_work", todo_ids: ["todo_first"]}, @@ -643,12 +778,24 @@ for (const provider of ["file", "sqlite"] as const) { assert.equal((await commitLocalGoalAcceptance({...await configureRequest(store), runtime_root: root})).status, "applied"); const verified = await commitLocalGoalAcceptanceVerification({...await verifyRequest(store), runtime_root: root}); assert.equal((verified.goal_acceptance_contract as JsonObject).status, "accepted"); + assert.equal((await commitLocalGoalAcceptanceLifecycleTransition({ + runtime_root: root, + goal_id: goal, + actor_agent_id: null, + operation_id: randomUUID(), + transition: {kind: "bind_existing", goal_ref: goalA}, + })).status, "applied"); + assert.equal((await inspectLocalGoalAcceptance({ + runtime_root: root, + goal_id: goal, + goal_ref: goalA, + })).status, "loaded"); assert.equal((await loadLegacyCoordinationWriterFence(root, goal)).status, "missing", "acceptance never promotes a provider"); // An unreadable maintenance state is not permission to bypass the writer. await writeFile(shadowManagementStatePath(root, goal), "invalid maintenance state"); const blocked = await commitLocalGoalAcceptance({...await configureRequest(store), runtime_root: root}); assert.equal(blocked.status, "failed"); - assert.equal((await inspectLocalGoalAcceptance({runtime_root: root, goal_id: goal})).status, "loaded"); + assert.equal((await inspectLocalGoalAcceptance({runtime_root: root, goal_id: goal, goal_ref: goalA})).status, "loaded"); }); }