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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ for (const copy of Object.values(deliveryReviewCopy)) {
scope: { kind: "selected_work", todo_ids: ["todo_confirmed"] } }), snapshot.goal_id);
assert.ok(deliveryReviewMarkdown(selected, copy).includes(copy.contract.selectedWork));
assert.ok(deliveryReviewMarkdown(parseDeliveryReview(withContract(contract), snapshot.goal_id), copy).includes(copy.contract.allWork));
const goalOnly = parseDeliveryReview(withContract({ ...contract,
scope: { kind: "goal" }, tasks: [], held_todo_ids: [] }), snapshot.goal_id);
assert.ok(deliveryReviewMarkdown(goalOnly, copy).includes(copy.contract.goalOnly));
const empty = parseDeliveryReview(withContract({ ...contract, criteria: [], tasks: [] }), snapshot.goal_id);
assert.ok(deliveryReviewMarkdown(empty, copy).includes(copy.contract.noTasks));
assert.ok(deliveryReviewMarkdown(empty, copy).includes(copy.contract.noCriteria));
Expand Down
8 changes: 4 additions & 4 deletions apps/presentation/dashboard/src/data/delivery-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function reviewNodeColumn(node: ReviewNode) {
}

type ConfiguredContract = GoalAcceptanceChecks;
type ContractExportLabels = Record<"title" | "boundary" | "scope" | "selectedWork" | "allWork" | "source" | "revision" | "digest" | "objective" | "criteria"
type ContractExportLabels = Record<"title" | "boundary" | "scope" | "goalOnly" | "selectedWork" | "allWork" | "source" | "revision" | "digest" | "objective" | "criteria"
| "nonGoals" | "tasks" | "verification" | "unknown" | "noTasks" | "noCriteria" | "notApplicable"
| "heldTasks" | "receipt" | "receiptNote" | "operation" | "verificationScope" | "allCriteria" | "passed" | "failed" | "exitCode"
| "oracleChecker" | "oracleSourceDigest" | "oracleCheckedAt" | "oracleInputVersions" | "oracleOutputVersions", string> & {
Expand Down Expand Up @@ -236,7 +236,7 @@ export function deliveryReviewMarkdown(snapshot: DeliveryReviewSnapshot, labels:
if (hasConfiguredGoalChecks(contract)) {
const copy = labels.contract;
rows.push("", `## ${copy.title}`, "", copy.boundary,
`${copy.scope}: ${contract.scope?.kind === "selected_work" ? copy.selectedWork : copy.allWork}`,
`${copy.scope}: ${contract.scope?.kind === "goal" ? copy.goalOnly : contract.scope?.kind === "selected_work" ? copy.selectedWork : copy.allWork}`,
`${copy.source}: ${line(snapshot.goal_id)}`, `${copy.revision}: ${contract.revision}`, `${copy.digest}: ${line(contract.digest)}`,
"", `### ${copy.objective}`, line(contract.objective || copy.unknown), "", `### ${copy.criteria}`);
if (contract.non_goals.length) rows.push(`${copy.nonGoals}: ${line(contract.non_goals.join("; "))}`);
Expand All @@ -246,8 +246,8 @@ export function deliveryReviewMarkdown(snapshot: DeliveryReviewSnapshot, labels:
if (criterion.oracle) rows.push(` ${copy.oracleChecker}: ${line(criterion.oracle.source_id)} · ${copy.oracleState[criterion.oracle.state]}`,
` ${copy.oracleSourceDigest}: ${line(criterion.oracle.source_digest)}`);
}
rows.push("", `### ${copy.tasks}`);
if (!contract.tasks.length) rows.push(copy.noTasks);
if (contract.tasks.length || contract.scope?.kind !== "goal") rows.push("", `### ${copy.tasks}`);
if (!contract.tasks.length && contract.scope?.kind !== "goal") rows.push(copy.noTasks);
for (const task of contract.tasks) {
rows.push(`- ${line(task.todo_id)}: ${copy.taskState[task.state]}`,
` ${copy.criteria}: ${line(task.criterion_ids.join(", ") || copy.unknown)}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function acceptanceRequest(goalId: string, operation?: Record<strin
// Read model only. The existing typed acceptance owner validates the complete
// document and its command/file bindings again at apply time.
const preparedDocument = z.object({objective: z.string().min(1),
scope: z.discriminatedUnion("kind", [z.object({kind: z.literal("all_advancement")}),
scope: z.discriminatedUnion("kind", [z.object({kind: z.literal("goal")}),z.object({kind: z.literal("all_advancement")}),
z.object({kind: z.literal("selected_work"), todo_ids: z.array(z.string()).min(1)})]),
criteria: z.array(z.object({id: z.string(), description: z.string(),
validation_argv: z.array(z.string()).min(1),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const goalAcceptanceChecksSchema = z.object({
outcome_id: z.string().optional(),
revision: z.number().int().positive(), digest: z.string().min(1), objective: z.string(),
scope: z.discriminatedUnion("kind", [
z.object({ kind: z.literal("goal") }).strict(),
z.object({ kind: z.literal("all_advancement") }).strict(),
z.object({ kind: z.literal("selected_work"), todo_ids: z.array(z.string().min(1)).min(1) }).strict(),
]).optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ReviewExportLabels } from "../../data/delivery-review.js";
export const goalAcceptanceContractCopy = {
en: {
title: "Delivery check", boundary: "Artifact checks do not establish independent review, adoption or Goal completion.",
scope: "Contract coverage", allWork: "All advancement work (including future tasks)", selectedWork: "Only explicitly selected tasks",
goalOnly: "Goal completion only", scope: "Contract coverage", allWork: "All advancement work (including future tasks)", selectedWork: "Only explicitly selected tasks",
source: "Goal source", revision: "Contract revision", digest: "Contract digest", objective: "Objective", criteria: "Acceptance criteria", nonGoals: "Outside scope",
tasks: "Task associations", verification: "Artifact verification", unknown: "Unknown", noTasks: "No task associations reported. Coverage is unknown.",
noCriteria: "No acceptance criteria reported.", retained: "Retained snapshot; refresh to read current acceptance facts.",
Expand All @@ -17,7 +17,7 @@ export const goalAcceptanceContractCopy = {
},
"zh-CN": {
title: "交付检查", boundary: "产物检查不等于独立复核、实际采用或 Goal 完成。",
scope: "合同覆盖范围", allWork: "全部推进任务(包括后续新增任务)", selectedWork: "仅明确选定的任务",
goalOnly: "仅 Goal 整体验收", scope: "合同覆盖范围", allWork: "全部推进任务(包括后续新增任务)", selectedWork: "仅明确选定的任务",
source: "Goal 来源", revision: "合同版本", digest: "合同摘要", objective: "目标", criteria: "验收条件", nonGoals: "范围之外",
tasks: "任务关联", verification: "产物验证", unknown: "未知", noTasks: "未提供任务关联,覆盖范围未知。",
noCriteria: "未提供验收条件。", retained: "当前保留旧快照,请刷新读取最新验收事实。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ export function GoalAcceptanceContractSection({goalId, contract: retained, copy,
<button type="button" disabled={busy || !writeCurrent || !checksConfigured} onClick={() => void operate("clear-checks")}>{zh ? "移除检查配置" : "Remove check configuration"}</button>
</div>
<dl className="delivery-acceptance-source"><div><dt>{copy.source}</dt><dd><code>{goalId}</code></dd></div>
{checksConfigured ? <><div><dt>{copy.scope}</dt><dd>{contract.scope?.kind === "selected_work" ? copy.selectedWork : copy.allWork}</dd></div>
{checksConfigured ? <><div><dt>{copy.scope}</dt><dd>{contract.scope?.kind === "goal" ? copy.goalOnly : contract.scope?.kind === "selected_work" ? copy.selectedWork : copy.allWork}</dd></div>
<div><dt>{copy.revision}</dt><dd>{contract.revision}</dd></div><div><dt>{copy.digest}</dt><dd><code>{contract.digest}</code></dd></div></> : null}
</dl>
{checksConfigured ? <><p>{contract.non_goals.join(" · ")}</p><h4>{copy.tasks}</h4>
{checksConfigured ? <><p>{contract.non_goals.join(" · ")}</p>{contract.tasks.length ? <h4>{copy.tasks}</h4> : null}
<ul>{contract.tasks.map(task => <li key={task.todo_id}><code>{task.todo_id}</code> · {copy.taskState[task.state]}
<p>{copy.criteria}: {task.criterion_ids.join(", ") || copy.unknown}</p>
{task.applicable === false ? <p>{copy.notApplicable}</p> : null}
Expand Down Expand Up @@ -171,7 +171,7 @@ export function GoalAcceptanceContractSection({goalId, contract: retained, copy,
onClick={() => void operate(plan ? "adopt" : "verify")}>{busy ? (zh ? "正在检查…" : "Checking…") : plan ? (zh ? "采用并检查" : "Use plan and check") : (zh ? "检查交付" : "Check delivery")}</button> : null}
</header> : null}
{plan ? <div className="delivery-check-plan">
<p>{plan.document.scope.kind === "all_advancement" ? copy.allWork
<p>{plan.document.scope.kind === "goal" ? copy.goalOnly : plan.document.scope.kind === "all_advancement" ? copy.allWork
: (zh ? `仅覆盖方案选定的 ${plan.document.scope.todo_ids.length} 项任务` : `Covers only ${plan.document.scope.todo_ids.length} selected tasks`)}</p>
<ul>{plan.document.criteria.map(item => <li key={item.id}>{item.description}</li>)}</ul>
{plan.document.criteria.some(item => item.oracle?.output_refs.length) ? <p>{zh ? "交付物" : "Delivery"}: {Array.from(new Set(plan.document.criteria.flatMap(item => item.oracle?.output_refs ?? []))).join(" · ")}</p> : null}
Expand Down
27 changes: 23 additions & 4 deletions docs/reference/goal-acceptance-observations.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,15 @@ Count the complete task: with an Agent-prepared plan already delivered, the owne
has one required decision. If the original request still lacks acceptance intent
or authority, that missing decision is an additional round; preparing a proposal
alone does not establish a one-round cold-start or live-model experience.
No-plan/default-off behavior remains unchanged, and no Lark action is introduced.
The default outcome feedback loop continues while executable verification awaits
an owner-authorized check.

中文:普通路径由 Agent 根据目标与既有上下文准备方案,负责人在交付处核对要求、覆盖范围和
执行影响,一次“采用并检查”;已有配置直接“检查交付”。JSON 导入仅保留为高级迁移入口。
`prepare --execute` 只保存可审阅方案,不修改 Goal 规则、不执行检查。丢失回执先对账,
不能自动重跑;历史通过不能认证变化后的版本。交互次数从原始意图算起,前置缺失的意图或
授权不能从计数中省略。此入口沿用可信本机所有者边界,不证明远程身份、独立复核或实际采用。
授权不能从计数中省略。成果反馈默认运行,可执行检查等待所有者授权。此入口沿用可信本机
所有者边界,不证明远程身份、独立复核或实际采用。
显式启用的 Goal LoopX 会话在原生工具声明 `prepare_check` 时,可直接准备相同方案;
Goal、Agent 与运行目录由宿主固定,暂停、过期 Turn 或撤销配置后不能继续准备。
旧原生线程的工具声明不会自动升级,也不会为新增工具替换未完成线程;未声明时保留 CLI
Expand Down Expand Up @@ -356,11 +358,28 @@ and binding are separate: selected work with no confirmed binding remains held.
Bindings outside the selected set are rejected. Agents cannot change coverage;
owner configuration still uses preview, provider CAS and exact operation replay.

`goal` checks the Goal as a whole without gating local Todo execution or
completion. It requires empty `bindings`; it does not mean every new task needs
an acceptance association. Use it for an independently checked cross-task
commitment. With a retained outcome, `{"outcome_id":"...","scope":{"kind":"goal"},"criteria":[...]}`
uses the original requirement descriptions and needs no placeholder Todo.
The outcome ID binds the receipt to that outcome. A standalone Goal check
reports on the Goal as a whole. An unmet configured check cannot be satisfied
by an Agent's self-review or local Todo completion. The outcome remains available to ordinary replanning when its
linked work ends; permission, Stop, real blockers and the host's finite budget
still govern execution. A scope declaration itself never runs a checker.

`goal` 只验收整个 Goal,不给局部 Todo 加执行或完成门禁,`bindings` 必须为空。
它适用于跨任务的独立验收承诺,可直接引用既有成果要求,无需占位 Todo。
`outcome_id` 将检查回执绑定到对应成果。独立的 Goal 检查报告整个 Goal 的验证结果。
局部任务完成或 Agent 自评不能替代已配置检查;未满足的成果在工作结束后仍进入
原有重规划流程。权限、Stop、真实阻塞及宿主有限预算继续生效。

Legacy persisted documents without `scope` retain their Goal-wide behavior and
original digest. Reading or upgrading does not narrow them. A fresh configuration
without scope is rejected with an actionable error; original committed retries
still recover their receipts. An older runtime that does not understand selected
coverage rejects it rather than silently ignoring it; update readers before an
still recover their receipts. An older runtime that does not understand the selected
coverage kind rejects it rather than silently ignoring it; update readers before an
owner-approved scope change.

Unselected work retains its ordinary validation, claims, leases, permissions and
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/goals/acceptance_authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ export async function configureGoalAcceptance(store: AuthorityStore, value: Json
// Historical receipts above remain replayable; new configurations must make
// the blast radius explicit, including updates to legacy contracts.
if (document.scope === undefined) return source(store, failure("goal_acceptance_scope_required",
"Select selected_work with todo_ids or explicitly choose all_advancement before configuring acceptance."));
"Select goal for Goal-only checks, selected_work with todo_ids, or all_advancement before configuring acceptance."));
const todos = acceptanceTodos(head.head, String(request.goal_id));
if (document.scope?.kind === "selected_work") for (const todoId of document.scope.todo_ids) {
const todo = todos.get(todoId);
Expand Down
21 changes: 15 additions & 6 deletions loopx/control_plane/goals/acceptance_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface AcceptanceCriterion extends JsonObject {
}
export type AcceptanceScope =
| {kind: "all_advancement"}
| {kind: "goal"}
| {kind: "selected_work"; todo_ids: string[]};
export interface AcceptanceDocument extends JsonObject {
outcome_id?: string;
Expand Down Expand Up @@ -169,6 +170,10 @@ function validationFiles(value: unknown): {path: string; sha256: string}[] {
* and its canonical bytes; an explicit subset may still contain unbound work. */
function normalizeAcceptanceScope(value: unknown): AcceptanceScope {
const scope = canonicalAuthorityObject(value, "acceptance scope");
if (scope.kind === "goal") {
acceptanceKeys(scope, ["kind"]);
return {kind: "goal"};
}
if (scope.kind === "all_advancement") {
acceptanceKeys(scope, ["kind"]);
return {kind: "all_advancement"};
Expand All @@ -179,7 +184,7 @@ function normalizeAcceptanceScope(value: unknown): AcceptanceScope {
list(scope.todo_ids, "acceptance scope Todos", 4096, 1).map(id), "acceptance scope Todos")};
}
function acceptanceIncludes(scope: AcceptanceScope | undefined, todoId: string): boolean {
return scope?.kind !== "selected_work" || scope.todo_ids.includes(todoId);
return scope?.kind !== "goal" && (scope?.kind !== "selected_work" || scope.todo_ids.includes(todoId));
}

export function normalizeGoalAcceptanceDocument(value: unknown): AcceptanceDocument {
Expand Down Expand Up @@ -225,11 +230,13 @@ export function normalizeGoalAcceptanceDocument(value: unknown): AcceptanceDocum
export function resolveGoalAcceptanceDocument(head: JsonObject, goalId: string, value: unknown): AcceptanceDocument {
const raw = canonicalAuthorityObject(value, "acceptance document");
if (raw.outcome_id === undefined) return normalizeGoalAcceptanceDocument(raw);
acceptanceKeys(raw, ["outcome_id", "criteria"]);
acceptanceKeys(raw, ["outcome_id", "criteria"], ["scope"]);
const goalOnly = raw.scope !== undefined && normalizeAcceptanceScope(raw.scope).kind === "goal";
acceptanceRequire(raw.scope === undefined || goalOnly, "compact outcome checks support only explicit Goal scope");
const basis = goalOutcomeCheckBasis(head, goalId, raw.outcome_id);
const requirements = basis.requirements as {id: string; description: string}[];
const todoIds = basis.todo_ids as string[];
acceptanceRequire(todoIds.length > 0, "link outcome work before preparing executable checks");
acceptanceRequire(goalOnly || todoIds.length > 0, "link outcome work before preparing executable checks");
const criteria = list(raw.criteria, "acceptance criteria", 64, 1).map(value => {
const check = canonicalAuthorityObject(value, "requirement check");
acceptanceKeys(check, ["id", "validation_argv"], ["validation_timeout_seconds", "validation_files", "oracle"]);
Expand All @@ -238,8 +245,8 @@ export function resolveGoalAcceptanceDocument(head: JsonObject, goalId: string,
return {...check, id: requirement.id, description: requirement.description};
});
return normalizeGoalAcceptanceDocument({outcome_id: basis.outcome_id, objective: basis.objective, non_goals: [],
scope: {kind: "selected_work", todo_ids: todoIds}, criteria,
bindings: todoIds.map(todo_id => ({todo_id, criterion_ids: criteria.map(c => c.id)}))});
scope: goalOnly ? {kind: "goal"} : {kind: "selected_work", todo_ids: todoIds}, criteria,
bindings: goalOnly ? [] : todoIds.map(todo_id => ({todo_id, criterion_ids: criteria.map(c => c.id)}))});
}

function acceptanceDigestMatches(todo: JsonObject, boundDigest: string): boolean {
Expand Down Expand Up @@ -343,6 +350,7 @@ function acceptanceWorkDigest(todos: ReadonlyMap<string, JsonObject>, scope: Acc
head: JsonObject): string {
const objective = currentGoalObjective(head)?.revisions.at(-1)?.digest;
const digest = (work: unknown) => canonicalAuthoritySha256(objective === undefined ? work : {work, objective});
if (scope?.kind === "goal") return digest([]);
if (scope?.kind === "selected_work") return digest(scope.todo_ids.map(todo_id => ({
todo_id, digest: todos.has(todo_id) ? goalAcceptanceTodoDigest(todos.get(todo_id)!) : null,
})));
Expand Down Expand Up @@ -563,7 +571,8 @@ export class GoalAcceptanceRead {
}
this.validateTodoReadModel();
const todos = this.read.todoIndex.todos;
const taskIds = new Set(state.document.scope?.kind === "selected_work" ? state.document.scope.todo_ids
const taskIds = new Set(state.document.scope?.kind === "goal" ? []
: state.document.scope?.kind === "selected_work" ? state.document.scope.todo_ids
: [...todos.values()].filter(advancement).map(todo => String(todo.todo_id)));
for (const binding of state.bindings) taskIds.add(binding.todo_id);
const tasks = [...taskIds].sort(authorityUnicodeCompare).map(key => this.task(key, todos.get(key), state));
Expand Down
Loading
Loading