Skip to content

Commit 82ec42e

Browse files
committed
Expose typed Goal Acceptance completion failures
1 parent e77de55 commit 82ec42e

5 files changed

Lines changed: 71 additions & 1 deletion

File tree

‎docs/reference/goal-acceptance-observations.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ and retains the existing claim, lease/fence, permission and continuation gates.
136136
A prior verification receipt or a confirmed association cannot complete a task.
137137
Use `loopx todo claim --help` and `loopx todo complete --help` for the existing
138138
task arguments; this contract adds no bypass flags.
139+
When a fresh completion criterion fails, `todo complete` keeps the Todo open
140+
and returns `goal_acceptance_validation_failure_v0` with the criterion ID,
141+
privacy-safe validation status, exit code when available, and a bounded next
142+
action. A dirty or mismatched delivery worktree is diagnosed as a workspace
143+
failure, not as a stale owner association. Commands, output, local paths and
144+
arbitrary runner summaries are not projected. Retry under the same Turn and
145+
current lease after repairing the indicated execution context.
139146

140147
Terminal observations, including `no_followup`, do not change the work digest:
141148
finishing a task must not stale the binding that just admitted its completion.
@@ -298,6 +305,10 @@ status 同时在独立的 `run_history.goals[].artifact_lifecycle` 和 Markdown
298305
所有者通过重新配置确认当前关联;完成任务必须执行当前绑定的产物检查,并继续满足原有
299306
claim、lease/fence、权限和后续工作要求。既有验证回执或已确认的关联不能代替本次任务完成验证。
300307
任务参数沿用 `loopx todo claim --help`、`loopx todo complete --help`,没有绕过门禁的新参数。
308+
本次完成验收失败时,`todo complete` 保持 Todo 未完成,返回
309+
`goal_acceptance_validation_failure_v0`:验收项 ID、脱敏的验证状态、可得的退出码和有界
310+
下一步动作。工作区不干净或不匹配会明确归类为工作区失败,而非所有者关联过期;命令、输出、
311+
本地路径和执行器任意摘要不会投影。修复执行环境后沿原 Turn 和当前 lease 重试。
301312

302313
终态观察不会让刚完成的任务关联过期。对既有 v0 绑定,若差异仅来自可校验的完成验证命令
303314
修订历史追加、后继任务链接追加,或此前不存在的 `resume_when` 调度条件新增,读出会比对

‎loopx/control_plane/coordination/todo_terminal_lifecycle.ts‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,43 @@ function acceptanceCompletionEvidence(head: JsonObject, input: ResolvedCoordinat
676676
return {source_binding: binding, ...evidence, validation_receipts: validationReceipts};
677677
}
678678

679+
/** Keep the runner's typed failure at the public boundary without exposing its
680+
* command, output, workspace path, or caller-controlled summary. */
681+
function acceptanceCriterionFailure(receipts: unknown): JsonObject | null {
682+
if (!Array.isArray(receipts)) return null;
683+
for (const value of receipts) {
684+
if (value === null || typeof value !== "object" || Array.isArray(value)) continue;
685+
const row = value as Record<string, unknown>;
686+
const receipt = row.receipt;
687+
if (receipt === null || typeof receipt !== "object" || Array.isArray(receipt)) continue;
688+
const result = receipt as Record<string, unknown>;
689+
if (result.passed !== false || typeof row.criterion_id !== "string") continue;
690+
const status = typeof result.status === "string" ? result.status : "command_failed";
691+
const nextActions: Record<string, string> = {
692+
workspace_dirty: "Preserve unrelated Git-visible files in ignored private storage or outside the worktree, then retry completion from that clean worktree with the same Turn identity.",
693+
workspace_unverified: "Re-enter a verifiable independent delivery worktree and retry with the same Turn identity.",
694+
workspace_receipt_unavailable: "Recover the recorded delivery workspace receipt before retrying completion.",
695+
workspace_receipt_invalid: "Inspect the recorded delivery workspace receipt before retrying completion.",
696+
workspace_receipt_mismatch: "Re-enter the recorded delivery worktree before retrying completion.",
697+
workspace_repository_mismatch: "Re-enter the recorded delivery repository and revision before retrying completion.",
698+
workspace_unavailable: "Restore the declared validation workspace before retrying completion.",
699+
validation_basis_changed: "Inspect the changed verifier files and ask the contract owner to review the acceptance basis.",
700+
timeout: "Inspect the validator runtime and retry the same criterion without changing the acceptance binding.",
701+
command_not_run: "Restore the configured validation executable and retry completion.",
702+
command_malformed: "Ask the contract owner to repair the configured validation command.",
703+
command_failed: "Inspect the configured criterion's evidence and validator, then retry completion without rebinding acceptance.",
704+
};
705+
const validationStatus = Object.hasOwn(nextActions, status) ? status : "unknown_failure";
706+
const exitCode = typeof result.exit_code === "number" && Number.isInteger(result.exit_code)
707+
? result.exit_code : null;
708+
return {schema_version: "goal_acceptance_validation_failure_v0",
709+
criterion_id: row.criterion_id, validation_status: validationStatus,
710+
exit_code: exitCode,
711+
next_action: nextActions[validationStatus] ?? "Inspect the privacy-safe runner receipt and configured criterion before retrying completion."};
712+
}
713+
return null;
714+
}
715+
679716
async function commitTerminalResult(
680717
store: AuthorityStore,
681718
input: ResolvedCoordinationTodoTerminalLifecycleInput,
@@ -1129,8 +1166,13 @@ export async function executeCoordinationTodoTerminalLifecycle(
11291166
try {
11301167
acceptanceEvidence = acceptanceCompletionEvidence(completionHead, input, acceptanceRequirements, acceptanceBinding);
11311168
} catch (error) {
1169+
const criterionFailure = error instanceof Error && error.message === "acceptance completion criteria failed"
1170+
? acceptanceCriterionFailure(input.goal_acceptance_validation_receipts) : null;
11321171
return terminalFailure("goal_acceptance_validation_rejected",
1133-
error instanceof Error ? error.message : "Acceptance completion validation failed.", {}, "decision_rejection");
1172+
criterionFailure === null
1173+
? error instanceof Error ? error.message : "Acceptance completion validation failed."
1174+
: `Goal acceptance criterion ${String(criterionFailure.criterion_id)} failed (${String(criterionFailure.validation_status)}). ${String(criterionFailure.next_action)}`,
1175+
criterionFailure === null ? {} : {goal_acceptance_validation_failure: criterionFailure}, "decision_rejection");
11341176
}
11351177
} else if (input.goal_acceptance_source_binding != null || input.goal_acceptance_validation_receipts != null) {
11361178
return terminalFailure("goal_acceptance_validation_unexpected",

‎skills/loopx-self-repair/references/repair-patterns.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ teaches a reusable control-plane lesson.
55

66
| Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair |
77
| --- | --- | --- | --- | --- |
8+
| `acceptance_validation_failure_flattened` | A bound Todo is `ready` and its ordinary validator passes, yet completion reports only `goal_acceptance_validation_rejected`; the agent searches contract bindings before discovering a workspace or runner failure. | Exact Todo acceptance state, completion's typed criterion failure, recorded delivery workspace, current worktree cleanliness, and the privacy-safe runner receipt. | The completion boundary collapsed a failed criterion receipt into a generic contract error, hiding the workspace or command status. | Project the failed criterion ID, allowlisted validation status, safe exit code and bounded next action without command output or local paths. Repair the execution context and retry under the same Turn/lease; do not rebind owner criteria or infer a Goal-wide hold. |
89
| `review_compatibility_assumption_gap` | A correct fix retains parallel protocol paths, and the review treats historical receipt recovery as proof that every old request decoder is needed. | Published review, structured compatibility rationale, real caller/deployment inventory, stored request versus receipt shape, and smaller-design readback. | Re-review verifies the last bug but does not separate compatibility obligations or test consolidation; free-text claims pass as evidence. | Replace the capability's existing compatibility rationale with a bounded structured assessment. Distinguish transient requests, independent client rollout and persisted replay formats; compare one typed current contract; preserve real legacy consumers. Cover needless retention and unsafe removal with positive twins. Keep optional simplifications advisory and do not make field completeness certify evidence truth. |
910
| `archive_capture_classification_gap` | Whole-Goal capture rejects a reachable archived Agent record although its active read was valid. | Recorded role/class, existing legacy read classification, transitive dependency closure, bootstrap and writer-outbox readback. | Archive storage preserved the role but omitted the resolved class; capture treated missing class as missing authority. | Keep recorded identity separate from compatibility classification. Only a recorded Agent role can adopt the existing read class; use it consistently for closure and materialization. Preserve the class on new archive moves, keep user authority fail-closed, and validate full source capture in disposable real providers without changing the active Goal. |
1011
| `shadow_proof_transport_amplification` | A large Goal cannot capture its first mutation or finish drain although the same tiny Goal succeeds; RPC rejects an oversized response. | Same source population, base/head serialized response sizes, actual sequence/drain callers, qualified lineage and cursor readback. | A consumer needing progress or partition markers received the full head and every historical projection across the language boundary. | Keep full history verification in the typed owner and return a purpose-specific compact proof. Preserve receipts, sequence and lineage checks; do not raise transport limits, truncate source records or weaken qualification to make the test pass. Cover the old oversized response and real CLI capture, drain and reviewed cutover on a disposable snapshot. |

‎tests/control_plane/test_goal_acceptance_cli.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ def test_bound_todo_completes_only_after_its_criteria_actually_run(acceptance_go
270270
)
271271
code, refused = run(*complete)
272272
assert code == 1 and refused["reason_code"] == "goal_acceptance_validation_rejected", refused
273+
assert refused["goal_acceptance_validation_failure"]["criterion_id"] == "export"
274+
assert refused["goal_acceptance_validation_failure"]["validation_status"] == "command_failed"
275+
assert "configured criterion" in refused["reason"]
273276
assert "validation_argv" not in json.dumps(refused)
274277

275278
(project / "artifact.txt").write_text("accepted")

‎tests/control_plane_ts/goal_acceptance_runtime.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,19 @@ for (const provider of ["file", ...(process.env.LOOPX_TEST_POSTGRES_URL ? ["post
169169
assert.equal((await executeCoordinationTodoTerminalLifecycle(store, {...attempt,
170170
goal_acceptance_validation_receipts: receipts})).reason_code, "goal_acceptance_validation_rejected");
171171
}
172+
const dirty = await executeCoordinationTodoTerminalLifecycle(store, {...attempt,
173+
goal_acceptance_validation_receipts: [{criterion_id: "criterion-a", receipt: {
174+
...runnerReceipt("criterion-a", false), exit_code: null, status: "workspace_dirty",
175+
summary: "private path /private/sensitive/worktree must never be projected",
176+
}}]});
177+
assert.equal(dirty.reason_code, "goal_acceptance_validation_rejected");
178+
assert.deepEqual(dirty.goal_acceptance_validation_failure, {
179+
schema_version: "goal_acceptance_validation_failure_v0", criterion_id: "criterion-a",
180+
validation_status: "workspace_dirty", exit_code: null,
181+
next_action: "Preserve unrelated Git-visible files in ignored private storage or outside the worktree, then retry completion from that clean worktree with the same Turn identity.",
182+
});
183+
assert.match(String(dirty.reason), /workspace_dirty.*clean worktree/);
184+
assert.doesNotMatch(JSON.stringify(dirty), /private\/sensitive|validation_argv/);
172185
const good = {...attempt, goal_acceptance_validation_receipts: [{criterion_id: "criterion-a", receipt: runnerReceipt()}]};
173186
for (const field of ["provider_revision", "contract_digest", "todo_semantic_digest", "operation_id"]) {
174187
assert.equal((await executeCoordinationTodoTerminalLifecycle(store, {...good,

0 commit comments

Comments
 (0)