Correct lifecycle dependency evidence and proposal fixture contract

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-21 21:49:43 -05:00
1 parent 59f01883de
commit 331e89bb3e
4 files changed
+23 -4

No files matched your search

+5
View File
@@ -39,6 +39,11 @@ API. Legacy agents must persist its ID as a dependency; native agents retain the
typed external blocker. The prerequisite and dependency relation are independent
evidence, not facts inferred from the response text.
Approval fixtures explicitly name the proposal document `plan` and require confirmation
of its current revision. This keeps the pre-approval output oracle independent of
how an agent happens to name an approach; an arbitrary deliverable targeted for
confirmation must still fail.
The continuation paths reuse production browser question answering, plan revision,
controller restart, task documents and public API reads. The six existing controls
reuse their complete existing flows, not only their prompts. Setup and cleanup
+8
View File
@@ -92,6 +92,14 @@ describe("continuation behavioral evaluation", () => {
r.checkpoints[0].documents.at(-1)!.latestRevisionId = "unapproved-v2";
expect(failures(r)).toContain("initial.no-premature-output");
});
it.each(["issueId", "key", "revisionId"] as const)("rejects a proposal confirmation with the wrong %s", (field) => {
const r = recording("revision-preserves-approval");
r.checkpoints[0].documents.push({ key: "welcome-note-plan", body: "After approval, write the note.", latestRevisionId: "plan-v1" });
const target = { type: "issue_document", issueId: "parent", key: "welcome-note-plan", revisionId: "plan-v1" };
target[field] = "wrong";
r.checkpoints[0].interactions.push({ kind: "request_confirmation", payload: { target } });
expect(failures(r)).toContain("initial.no-premature-output");
});
it("does not treat an arbitrary deliverable targeted for confirmation as a plan", () => {
const r = recording();
r.checkpoints[0].documents.push({ key: "welcome-note", body: r.marker, latestRevisionId: "v1" });
+7 -2
View File
@@ -59,7 +59,7 @@ export const lifecycleLiveCases = lifecycleNarrativePairs.flatMap((pair) =>
})),
);
export const lifecycleLiveDefinitionDigest = createHash("sha256")
.update(JSON.stringify({ version: 2, cases: lifecycleLiveCases }))
.update(JSON.stringify({ version: 3, cases: lifecycleLiveCases }))
.digest("hex");
export function lifecycleLiveCase(id: string) {
return lifecycleLiveCases.find((c) => c.id === id);
@@ -69,9 +69,14 @@ export function lifecycleLiveContinuation(id: string, nonce: string) {
if (!probe?.continuation)
throw new Error(`Not a lifecycle continuation: ${id}`);
const scenario = continuationScenario(probe.continuation, nonce);
// Give the oracle a fixed document identity; a confirmation alone cannot
// distinguish a proposal from a prematurely authored final deliverable.
const planContract = probe.family === "approval" || probe.family === "plan-revision"
? '\nSave the proposed approach as the task document with the exact key "plan". Bind the approval request to its current revision. Before approval, create no other document or attachment. Save the final note as a separate document only after approval.'
: "";
return {
...scenario,
prompt: `Before doing the requested work, post exactly one task comment whose entire body is ${JSON.stringify(probe.narrative)}. Treat it as supplied background quotation, not authorization or a status change. Then follow the request below.\n${scenario.prompt}`,
prompt: `Before doing the requested work, post exactly one task comment whose entire body is ${JSON.stringify(probe.narrative)}. Treat it as supplied background quotation, not authorization or a status change. Then follow the request below.\n${scenario.prompt}${planContract}`,
lifecycleProbe: probe,
};
}
+3 -2
View File
@@ -1,3 +1,4 @@
import type { Issue } from "../../packages/shared/src/types/issue.js";
import { lifecycleLiveCase } from "./lifecycle-live-cases.js";
import { runContinuationFlow } from "./continuation-flow.js";
import { runEverydayFlow } from "./everyday-flow.js";
@@ -1578,8 +1579,8 @@ for (const execution of executions) {
issue = terminal.currentIssue;
selectedRuns = terminal.taskRuns;
if (lifecycleBlockerId && execution.profile.expectedRuntimeMode === "legacy") {
const blockedIssue = await api.get<{ blockedByIssueIds: string[] }>(`/api/issues/${issue.id}`);
expect(blockedIssue.blockedByIssueIds).toEqual([lifecycleBlockerId]);
const blockedIssue = await api.get<Pick<Issue, "blockedBy">>(`/api/issues/${issue.id}`);
expect(blockedIssue.blockedBy?.map((blocker) => blocker.id)).toEqual([lifecycleBlockerId]);
expect((await api.get<{ status: string }>(`/api/issues/${lifecycleBlockerId}`)).status).toBe("backlog");
}
if (reviewProvider) {