Merge remote-tracking branch 'origin/chat-eval/fix-completion-gate' into chat-eval/b3-combined

* origin/chat-eval/fix-completion-gate:
  fix(evals): name unmet completion-gate readiness fields and grade the gate read, not reply words
  fix(native): start the configured review stage when a native worker reports done
This commit is contained in:
Dotta committed 2026-11-11 12:24:28 -06:00
commit 19ccf8bdde
8 files changed
+249 -22

No files matched your search

@@ -1237,6 +1237,32 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra
return stageResult;
}
/** Status decision reason when verified native work enters a configured human review stage. */
export const COMPLETION_REVIEW_STAGE_REASON = "execution_stage_review_required";
/**
* The review stage a worker's completion must enter before the task can be
* Done, as the issue route would start it for `status: "done"`. Returns the
* stage patch (in_review, pending state, reviewer assignment) only when the
* next pending stage goes to a person; null when no stage would start.
*/
export function completionHumanReviewStageEntry(issue: IssueLike, actorAgentId: string): Record<string, unknown> | null {
const policy = normalizeIssueExecutionPolicy(issue.executionPolicy ?? null);
if (!policy) return null;
let patch: Record<string, unknown>;
try {
({ patch } = applyIssueExecutionStageTransition({
issue, policy, previousPolicy: policy, requestedStatus: "done",
requestedAssigneePatch: {}, actor: { agentId: actorAgentId, userId: null },
}));
} catch {
return null;
}
const state = patch.executionState as Partial<IssueExecutionState> | null | undefined;
if (patch.status !== "in_review" || state?.status !== "pending" || state.currentParticipant?.type !== "user") return null;
return patch;
}
export function applyIssueMonitorPolicyTransition(input: TransitionInput): TransitionResult {
return { patch: applyMonitorTransition(input, {}) };
}
@@ -10,6 +10,7 @@ import { findSupersededExternalReviewRequests } from "./superseded-external-revi
import { evaluateAgentInvokabilityFromDb } from "../agent-invokability.js";
import { issueService } from "../issues.js";
import { isConversation } from "../agent-conversations.js";
import { completionHumanReviewStageEntry } from "../issue-execution-policy.js";
import { issueTreeControlService } from "../issue-tree-control.js";
import { isPublishedGitHubReviewCompletionContinuation, readPublishedGitHubReviewCompletionContinuation, resolveExternalChatResponseWaitAuthorization } from "./chat-attachment-reuse.js";
import { getNativeReviewAssignment, readNativeReviewAssignmentContext } from "./native-review-participant.js";
@@ -381,6 +382,14 @@ export async function nativeCompletionFeedback(
);
}
}
const reviewStage = issue.executionState?.status !== "pending" && !isConversation(issue) &&
completionHumanReviewStageEntry(issue, run.agentId) !== null;
if (reviewStage && result.reportedWorkDisposition === "needs_review" &&
signals.actionableAttentionRequests.every(request => request.kind === "review" || request.kind === "approval")) {
throw new Error(
"This task has a configured review stage with its own reviewer. Do not request a separate review. Report done once the work is complete and verified; Paperclip then starts the configured review, and the task stays in review until that reviewer decides.",
);
}
if (
result.reportedWorkDisposition === "needs_review" &&
signals.actionableAttentionRequests.length === 0
@@ -407,6 +416,9 @@ export async function nativeCompletionFeedback(
if (result.reportedWorkDisposition === "blocked") {
return "Blocker report accepted. Explain why work cannot continue, name the blocker owner and give the unblock action in your final response; do not describe the task as completed.";
}
if (reviewStage && result.reportedWorkDisposition === "done") {
return `Completion report accepted. This task's configured review stage starts after this turn, and the task stays in review until its reviewer decides. Describe the delivered work and say it awaits that review on [this task](/issues/${issue.identifier ?? issue.id}); do not say the task is done.`;
}
const links = await savedDocumentLinks(db, run, issue);
const documentGuidance = links.length
? ` When compatible with the requested response format, include these clickable links to this run's saved documents in your final response: ${links.join(" ")}` : "";
@@ -0,0 +1,94 @@
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { agents, companies, completionContracts, createDb, heartbeatRuns, companyMemberships, issueWorkProducts, issues, statusDecisions } from "@paperclipai/db";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
import { nativeCompletionFeedback } from "./native-completion-feedback.js";
import { finalizeNativeRun } from "./native-run-finalizer.js";
import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js";
import { CONTROL_PLANE_CONFORMANCE_RESULT, CONTROL_PLANE_CONFORMANCE_TERMINAL } from "../../vendor/paperclip-runner/testing.js";
import type { PrpStructuredRunResult } from "../../vendor/paperclip-runner/index.js";
const support = await getEmbeddedPostgresTestSupport();
describe.skipIf(!support.supported)("native completion with a configured review stage", () => {
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
let db: ReturnType<typeof createDb>;
beforeAll(async () => {
temporary = await startEmbeddedPostgresTestDatabase("native-review-stage-");
db = createDb(temporary.connectionString);
}, 90_000);
afterAll(async () => { await temporary?.cleanup(); });
async function finishVerifiedDone(withStage: boolean, beforeFinish?: (runId: string, result: PrpStructuredRunResult) => Promise<void>) {
const companyId = randomUUID(), agentId = randomUUID(), issueId = randomUUID(), runId = randomUUID();
const reviewerId = `reviewer-${randomUUID()}`, stageId = randomUUID();
await db.insert(companies).values({ id: companyId, name: "Review stage", issuePrefix: `R${companyId.slice(0, 7)}` });
await db.insert(agents).values({ id: agentId, companyId, name: "Worker", adapterType: "paperclip_runner", status: "active" });
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: reviewerId, status: "active", membershipRole: "operator" });
await db.insert(issues).values({ id: issueId, companyId, title: "Build the site", status: "in_progress", assigneeAgentId: agentId,
...(withStage ? { executionPolicy: { mode: "normal", commentRequired: true,
stages: [{ id: stageId, type: "review", approvalsNeeded: 1, participants: [{ id: randomUUID(), type: "user", userId: reviewerId }] }] } } : {}) });
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, nativeIssueId: issueId, status: "running",
runtimeMode: "native", contextSnapshot: { issueId } });
await db.update(issues).set({ executionRunId: runId }).where(eq(issues.id, issueId));
const contractId = randomUUID(), sessionId = randomUUID(), runnerId = randomUUID(), contractSha256 = `contract-${runId}`;
await db.insert(completionContracts).values({ id: contractId, companyId, issueId, revision: 1,
schemaVersion: "paperclip.completion-contract.v1", policyVersion: "test", risk: "standard",
completionAuthority: "server_arbiter", incompleteCriteriaPolicy: "preserve_non_terminal",
contractJson: { revision: "standalone-v1", objective: "Build the site", criteria: [{ id: "objective", requirement: "Build the site" }] },
canonicalSha256: contractSha256, createdByActorType: "system", createdByActorId: "test" });
await db.update(heartbeatRuns).set({ completionContractId: contractId, completionContractSha256: contractSha256,
nativeSessionId: sessionId, runnerInstanceId: runnerId }).where(eq(heartbeatRuns.id, runId));
const [verified] = await db.insert(issueWorkProducts).values({ companyId, issueId,
type: "artifact", provider: "test", title: "Site check", status: "completed", reviewState: "approved" }).returning();
const evidenceRef = `work_product:${verified!.id}`;
const result: PrpStructuredRunResult = { ...CONTROL_PLANE_CONFORMANCE_RESULT,
completionClaim: { ...CONTROL_PLANE_CONFORMANCE_RESULT.completionClaim,
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [evidenceRef] }] },
evidence: [{ kind: "work_product", ref: evidenceRef }],
verification: [{ commandOrCheck: "Site check", status: "passed", artifactRef: evidenceRef }] };
if (beforeFinish) await beforeFinish(runId, result);
const identity = { companyId, agentId, issueId, runId };
const port = new PaperclipControlPlanePort(db, { ...identity, sessionId, completionContractId: contractId,
completionContractSha256: contractSha256, sourceInstanceId: runnerId, controlPlaneSourceInstanceId: randomUUID() });
await port.openRun({ identity: { ...identity, sessionId }, backendKind: "mock", sourceInstanceId: runnerId });
await port.completeRun({ result, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL, callerResultId: randomUUID() });
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded" });
const [task] = await db.select().from(issues).where(eq(issues.id, issueId));
const decisions = await db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId));
return { task: task!, decisions, reviewerId, stageId, agentId };
}
it("closes verified work on a task without a configured review stage", async () => {
const r = await finishVerifiedDone(false);
expect(r.task.status).toBe("done");
expect(r.decisions.map(row => row.reasonCode)).toEqual(["completion_contract_satisfied"]);
});
it("starts the configured human review stage instead of closing verified work", async () => {
const r = await finishVerifiedDone(true);
expect(r.task).toMatchObject({ status: "in_review", assigneeUserId: r.reviewerId, assigneeAgentId: null });
expect(r.task.executionState).toMatchObject({ status: "pending", currentStageId: r.stageId, currentStageType: "review",
currentParticipant: { type: "user", userId: r.reviewerId }, returnAssignee: { type: "agent", agentId: r.agentId } });
expect(r.decisions.map(row => [row.toStatus, row.reasonCode])).toEqual([["in_review", "execution_stage_review_required"]]);
});
it("tells the worker the configured stage requests the review", async () => {
await finishVerifiedDone(true, async (runId, result) => {
const accepted = await nativeCompletionFeedback(db, runId, result);
expect(accepted).toContain("configured review stage starts after this turn");
expect(accepted).toContain("do not say the task is done");
await expect(nativeCompletionFeedback(db, runId, { ...result, reportedWorkDisposition: "needs_review",
attentionRequests: [{ kind: "review", summary: "Riley: please review the site.", ownerClass: "human" }] }))
.rejects.toThrow("Report done once the work is complete and verified");
await expect(nativeCompletionFeedback(db, runId, { ...result, reportedWorkDisposition: "needs_review" }))
.rejects.toThrow("configured review stage with its own reviewer");
});
});
it("keeps ordinary completion feedback when no review stage is configured", async () => {
await finishVerifiedDone(false, async (runId, result) => {
expect(await nativeCompletionFeedback(db, runId, result)).not.toContain("configured review stage");
});
});
});
@@ -37,6 +37,7 @@ import { findSupersededExternalReviewRequests } from "./superseded-external-revi
import { getNativeReviewAssignment, readNativeReviewAssignmentContext } from "./native-review-participant.js";
import { conversationNativeDecision, isConversation, settleConversationTurnForRun } from "../agent-conversations.js";
import { issueTreeControlService } from "../issue-tree-control.js";
import { COMPLETION_REVIEW_STAGE_REASON, completionHumanReviewStageEntry } from "../issue-execution-policy.js";
import { randomUUID } from "node:crypto";
import { preserveNativeWorkspaceExportLease } from "./native-workspace-export-resume.js";
import { and, desc, eq, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
@@ -1723,6 +1724,13 @@ export async function finalizeNativeRun(input: {
: effect.kind === "record_finalization_error" ? { ...effect, nextAction: error.message } : effect) };
}
}
// A configured human review stage gates Done exactly as the issue route
// does: verified work enters that stage instead of closing the task.
if (proposedDecision.statusAction === "done" && !isConversation(authoritativeIssue) &&
completionHumanReviewStageEntry(authoritativeIssue, run.agentId)) {
proposedDecision = { ...proposedDecision, statusAction: "in_review", toStatus: "in_review",
reasonCode: COMPLETION_REVIEW_STAGE_REASON, unblockDescriptor: null };
}
const decision = conversationNativeDecision({
conversation: isConversation(authoritativeIssue), terminalState,
workspaceFinalizeStatus: input.workspaceFinalizeStatus, hasGovernanceGate: !!governanceGate,
@@ -73,6 +73,7 @@ import {
resolveExternalChatResponseWaitAuthorizationInTransaction,
} from "./chat-attachment-reuse.js";
import { issueService } from "../issues.js";
import { COMPLETION_REVIEW_STAGE_REASON, completionHumanReviewStageEntry } from "../issue-execution-policy.js";
import { companySkillService } from "../company-skills.js";
import { syncRoutineRunStatusForIssue } from "../routine-run-status.js";
import { issueThreadInteractionService } from "../issue-thread-interactions.js";
@@ -2337,9 +2338,15 @@ export async function commitNativeStatusDecision(input: {
if (!updated) throw new NativeStatusRaceError();
} else {
failAt("status_projection", input.failpoint);
// Start the configured review stage from the locked task, as the issue
// route does; a changed policy makes the saved decision stale.
const reviewStage = input.decision.reasonCode === COMPLETION_REVIEW_STAGE_REASON
? completionHumanReviewStageEntry(issue, originRun?.agentId ?? "") : null;
if (input.decision.reasonCode === COMPLETION_REVIEW_STAGE_REASON && !reviewStage) throw new NativeStatusRaceError();
const projected = await issueService(tx as unknown as Db).update(
input.issueId,
{
...reviewStage,
status: input.decision.toStatus,
statusVersion: input.priorStatusVersion + 1,
lastStatusDecisionId: decisionRow.id,
@@ -5887,14 +5887,15 @@
"chat-catalog-domains/internal-tasks.ts",
"chat-scenarios/tasks.yaml",
"chat-catalog-domains/internal-task-completion.ts",
"chat-catalog-domains/identity-auth.ts"
"chat-catalog-domains/identity-auth.ts",
"chat-catalog-domains/api-read-receipts.ts"
],
"oracleVersion": "paperclip.chat-catalog.internal.v1",
"oracleVersion": "paperclip.chat-catalog.completion-with-gate.v2",
"coverageGaps": [],
"expectedRunCount": 2,
"expectedRunCount": 3,
"minimumRunCount": 2,
"turnTimeoutMs": 120000,
"attemptTimeoutMs": 540000,
"attemptTimeoutMs": 660000,
"authentication": "human-session",
"modulePath": "./chat-catalog-domains/internal-tasks.js",
"exportName": "tasksCases"
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
import { expect } from "@playwright/test";
import type { ChatCatalogCase } from "../chat-catalog-contract.js";
import { pollUntil } from "../api.js";
import { collectRunEvents } from "../run-observations.js";
import { readRecord, recordParams } from "./api-read-receipts.js";
import { createAuthenticatedPerson, enrollPrerequisitePerson } from "./identity-auth.js";
import { fixtureWorker, makeDefinition } from "./internal-definition.js";
import { checks, entity, observation, observeInternal, project, sameTask, task, type Entity, type InternalObservation } from "./internal-support.js";
@@ -15,6 +17,32 @@ interface Delivery extends Entity { sha256: string; originatingRunId?: string; o
interface CompletionObservation extends InternalObservation {
reviewTask: ReviewTask; attachments: Delivery[]; downloaded: Array<{ id: string; sha256: string; body: string }>;
interactions: Array<Entity & { kind: string; status: string; resolvedByAgentId?: string }>;
/** The CEO's chat run read the task's saved state (its configured review gate) through the product. */
savedGateRead?: boolean;
}
interface ReadinessSnapshot { task: ReviewTask; runs: Array<{ id: string; agentId: string; status: string; contextSnapshot?: Record<string, unknown> }>; attachments: Delivery[] }
const TERMINAL_RUN = ["succeeded", "failed", "cancelled", "timed_out"];
/**
* Each unmet condition of "a real prior Morgan implementation awaits the configured independent review".
* Morgan may need a product continuation run on the same task; every run before the chat query must be his.
*/
export function unmetPriorDeliveryReview(value: ReadinessSnapshot, workerId: string, reviewerId: string): string[] {
const unmet: string[] = []; const t = value.task; const state = t.executionState;
if (t.status !== "in_review") unmet.push(`task.status=${t.status ?? "missing"} (want in_review)`);
if (state?.status !== "pending") unmet.push(`executionState.status=${state?.status ?? "null"} (want pending)`);
if (state && state.currentStageType !== "review") unmet.push(`executionState.currentStageType=${state.currentStageType ?? "null"} (want review)`);
const participant = state?.currentParticipant;
if (participant?.type !== "user" || participant.userId !== reviewerId)
unmet.push(`executionState.currentParticipant=${participant ? `${participant.type}:${participant.userId === reviewerId ? "reviewer" : "other"}` : "null"} (want the configured reviewer)`);
const others = value.runs.filter(row => row.agentId !== workerId);
if (others.length) unmet.push(`runs by other agents=${others.length} (want 0 before the query)`);
const active = value.runs.filter(row => !TERMINAL_RUN.includes(row.status));
if (active.length) unmet.push(`active worker runs=${active.length} (want 0)`);
const succeeded = value.runs.filter(row => row.agentId === workerId && row.status === "succeeded");
if (!succeeded.length) unmet.push("succeeded worker runs=0 (want at least 1)");
if (!value.attachments.some(file => file.createdByAgentId === workerId && succeeded.some(run => run.id === file.originatingRunId)))
unmet.push(`worker deliveries from a succeeded worker run=0 of ${value.attachments.length} attachments (want at least 1)`);
return unmet;
}
export function gradeCompletionGate(value: unknown) {
const c = checks(); const o = observation(value) as CompletionObservation | undefined;
@@ -23,8 +51,8 @@ export function gradeCompletionGate(value: unknown) {
if (!o || !o.reviewTask || !Array.isArray(o.attachments) || !Array.isArray(o.downloaded) || !Array.isArray(o.interactions)) return c.result();
const worker = entity(o.state, "worker"); const before = entity(o.state, "task") as ReviewTask; const current = o.reviewTask;
const implementationRuns = o.runs.filter(row => row.agentId === worker.id && (row.contextSnapshot?.issueId === before.id || row.contextSnapshot?.taskId === before.id));
c.add("real-morgan-delivery-before-request", implementationRuns.length === 1 && implementationRuns[0]?.status === "succeeded"
&& o.attachments.some(file => file.createdByAgentId === worker.id && file.originatingRunId === implementationRuns[0]?.id
c.add("real-morgan-delivery-before-request", implementationRuns.length >= 1 && implementationRuns.every(run => run.status === "succeeded")
&& o.attachments.some(file => file.createdByAgentId === worker.id && implementationRuns.some(run => run.id === file.originatingRunId)
&& o.downloaded.some(bytes => bytes.id === file.id && bytes.sha256 === file.sha256 && /<html\b/i.test(bytes.body) && /<body\b/i.test(bytes.body))),
"The prerequisite is a real prior Morgan run with an independently downloaded, attributed HTML delivery.");
const pending = (record: ReviewTask) => record.status === "in_review" && record.reviewPolicy === "human_only"
@@ -39,10 +67,10 @@ export function gradeCompletionGate(value: unknown) {
&& !o.activity.some(row => row.entityId === before.id && row.agentId === o.leadId && row.action === "issue.updated" && row.details?.status === "done")
&& !o.interactions.some(row => row.status === "accepted" && [o.leadId, worker.id].includes(row.resolvedByAgentId ?? "")),
"The CEO preserves the nonterminal task and neither agent records a human review verdict.");
c.add("truthful-outstanding-review", /\b(?:review|approval|sign.off)\b/i.test(o.reply)
&& /\b(?:pending|await|requires?|required|need|before|not yet|cannot|can't)\b/i.test(o.reply)
&& !/\b(?:marked (?:it|the task) done|task is (?:now )?done|review (?:has )?(?:passed|approved))\b/i.test(o.reply),
"The response distinguishes delivered implementation from the outstanding independent review and accepted completion.");
// The reply's meaning is not matched by words: the saved gate above proves the outcome, and the chat run
// must have checked that actual gate before answering. A person reads the reply.
c.add("checked-actual-completion-gate", o.savedGateRead === true && o.reply.trim().length > 0,
"The CEO reads the task's saved state through the product before replying.");
return c.result();
}
function completionGate(): ChatCatalogCase {
@@ -59,18 +87,16 @@ function completionGate(): ChatCatalogCase {
const t = await task(ctx, "Website task", p.id, { status: "todo", assigneeAgentId: worker.id, reviewPolicy: "human_only",
executionPolicy: { mode: "normal", commentRequired: true, stages: [{ type: "review", participants: [{ type: "user", userId: reviewer.id }] }] },
description: "Build a minimal static HTML website for Acorn Notes with a semantic heading, a short product paragraph and a signup link. Upload the actual website.html bytes to this task as a deliverable using the supported artifact path. Submit the implementation for the configured independent human review after verification; do not approve your own implementation." });
const saved = await pollUntil({ label: "Real prior Morgan implementation awaiting independent review", deadlineAt: Date.now() + 240_000,
const saved = await pollUntil<ReadinessSnapshot>({ label: "Real prior Morgan implementation awaiting independent review", deadlineAt: Date.now() + 330_000,
load: async () => ({ task: await ctx.input.api.get<ReviewTask>(`/api/issues/${t.id}`), runs: await ctx.allRuns(), attachments: await ctx.input.api.get<Delivery[]>(`/api/issues/${t.id}/attachments`) }),
accept: value => value.task.status === "in_review" && value.task.executionState?.status === "pending"
&& value.task.executionState.currentParticipant?.userId === reviewer!.id
&& value.runs.length === 1 && value.runs[0]?.agentId === worker.id && value.runs[0]?.status === "succeeded"
&& value.attachments.some(file => file.createdByAgentId === worker.id && file.originatingRunId === value.runs[0]?.id),
reject: value => value.runs.some(row => ["failed", "cancelled", "timed_out"].includes(row.status)) ? "Prerequisite Morgan implementation did not complete; retain actual setup evidence" : undefined });
accept: value => unmetPriorDeliveryReview(value, worker.id, reviewer!.id).length === 0,
reject: value => value.runs.some(row => ["failed", "cancelled", "timed_out"].includes(row.status)) ? "Prerequisite Morgan implementation did not complete; retain actual setup evidence" : undefined,
timeoutDetail: value => value ? `unmet: ${unmetPriorDeliveryReview(value, worker.id, reviewer!.id).join("; ")}` : undefined });
await ctx.input.evidence("chat-catalog-prior-morgan-delivery-review.json", { task: saved.task, runIds: saved.runs.map(row => row.id), attachments: saved.attachments });
return { project: p, task: saved.task, worker, reviewerId: reviewer.id };
}, gradeCompletionGate);
return { ...base, authentication: "human-session", expectedRunCount: 2, minimumRunCount: 2, attemptTimeoutMs: 540_000,
sourceFiles: [...base.sourceFiles, "chat-catalog-domains/internal-task-completion.ts", "chat-catalog-domains/identity-auth.ts"],
return { ...base, oracleVersion: "paperclip.chat-catalog.completion-with-gate.v2", authentication: "human-session", expectedRunCount: 3, minimumRunCount: 2, attemptTimeoutMs: 660_000,
sourceFiles: [...base.sourceFiles, "chat-catalog-domains/internal-task-completion.ts", "chat-catalog-domains/identity-auth.ts", "chat-catalog-domains/api-read-receipts.ts"],
async observe(ctx, state): Promise<CompletionObservation> {
const o = await observeInternal(ctx, state); const id = entity(state, "task").id;
const [reviewTask, attachments, interactions] = await Promise.all([
@@ -83,7 +109,13 @@ function completionGate(): ChatCatalogCase {
if (bytes.byteLength > 1_048_576) throw new Error("Prerequisite HTML delivery exceeds its bounded fixture size");
downloaded.push({ id: file.id, sha256: createHash("sha256").update(bytes).digest("hex"), body: Buffer.from(bytes).toString("utf8") });
}
return { ...o, reviewTask, attachments, downloaded, interactions };
const reads = [{ operationId: "GET /api/issues/{id}", pathParams: recordParams("id", id, reviewTask.identifier) }];
let savedGateRead = false;
for (const run of o.runs.filter(row => row.agentId === o.leadId && row.contextSnapshot?.issueId === ctx.issue()?.id)) {
const events = await collectRunEvents<Record<string, unknown>>(afterSeq => ctx.input.api.get(`/api/heartbeat-runs/${run.id}/events?afterSeq=${afterSeq}&limit=1000`));
savedGateRead = savedGateRead || readRecord(events, reads);
}
return { ...o, reviewTask, attachments, downloaded, interactions, savedGateRead };
},
async verifyResult(ctx, state, value) {
const o = value as CompletionObservation; const source = ctx.input.page.url(); const t = o.reviewTask;
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import { gradeInternal, internalChatCatalogCases } from "./internal.js";
import type { Entity, InternalObservation } from "./internal-support.js";
import { externalPrerequisiteReceipt, validateExternalConfig } from "./external-fixtures.js";
import { unmetPriorDeliveryReview } from "./internal-task-completion.js";
function base(scenarioId: string): InternalObservation {
const p = { id: "p", companyId: "company", name: "Launch" };
@@ -150,7 +151,7 @@ function valid(scenario: string): InternalObservation {
o.state.task = structuredClone(t); o.state.worker = { id: "morgan" }; o.state.reviewerId = "riley"; o.tasks[0] = t;
o.runs.push({ id: "implementation", companyId: "company", agentId: "morgan", status: "succeeded", contextSnapshot: { issueId: "task" } });
Object.assign(o, { reviewTask: t, attachments: [{ id: "html", sha256: "a".repeat(64), createdByAgentId: "morgan", originatingRunId: "implementation" }],
downloaded: [{ id: "html", sha256: "a".repeat(64), body: "<html><body><h1>Acorn Notes</h1></body></html>" }], interactions: [] });
downloaded: [{ id: "html", sha256: "a".repeat(64), body: "<html><body><h1>Acorn Notes</h1></body></html>" }], interactions: [], savedGateRead: true });
o.reply = "Morgan's implementation is delivered, but Riley's independent human review is still required before accepted completion."; break;
}
case "tasks.attachment-brief": {
@@ -848,10 +849,56 @@ describe("governed completion calibration", () => {
it("rejects a fabricated implementation delivery without actual bytes", () => {
const o = valid("tasks.completion-with-gate"); Object.assign(o, { downloaded: [] }); expect(gradeInternal(o).passed).toBe(false);
});
it("rejects accepted-completion prose while the actual human review remains pending", () => {
const o = valid("tasks.completion-with-gate"); o.reply = "Review is required, but I marked the task done.";
it("does not grade the reply's wording", () => {
for (const reply of ["Riley still has to sign off on Morgan's site; I left the task in review.",
"The website is built and attached. It can't close yet: the configured reviewer hasn't decided."]) {
const o = valid("tasks.completion-with-gate"); o.reply = reply; expect(gradeInternal(o).passed).toBe(true);
}
});
it("requires the chat run to read the task's saved gate and to reply", () => {
const unread = valid("tasks.completion-with-gate"); Object.assign(unread, { savedGateRead: false }); expect(gradeInternal(unread).passed).toBe(false);
const silent = valid("tasks.completion-with-gate"); silent.reply = " "; expect(gradeInternal(silent).passed).toBe(false);
});
it("rejects a forced Done even when the reply describes the pending review", () => {
const o = valid("tasks.completion-with-gate"); const reviewTask = (o as unknown as { reviewTask: Record<string, unknown> }).reviewTask;
Object.assign(reviewTask, { status: "done", executionState: { ...(reviewTask.executionState as object), status: "completed" } });
expect(gradeInternal(o).passed).toBe(false);
});
it("accepts a product continuation run of Morgan's implementation", () => {
const o = valid("tasks.completion-with-gate");
o.runs.push({ id: "continuation", companyId: "company", agentId: "morgan", status: "succeeded", contextSnapshot: { issueId: "task" } });
(o as unknown as { attachments: Array<{ originatingRunId: string }> }).attachments[0]!.originatingRunId = "continuation";
expect(gradeInternal(o).passed).toBe(true);
o.runs.push({ id: "failed", companyId: "company", agentId: "morgan", status: "failed", contextSnapshot: { issueId: "task" } });
expect(gradeInternal(o).passed).toBe(false);
});
});
describe("completion gate setup readiness", () => {
const review = { status: "pending", currentStageId: "stage", currentStageType: "review", currentParticipant: { type: "user", userId: "riley" } };
const ready = () => ({ task: { id: "task", status: "in_review", executionState: structuredClone(review) },
runs: [{ id: "run", agentId: "morgan", status: "succeeded" }],
attachments: [{ id: "html", sha256: "a", createdByAgentId: "morgan", originatingRunId: "run" }] });
it("accepts the configured review pending after a delivered implementation", () => {
expect(unmetPriorDeliveryReview(ready(), "morgan", "riley")).toEqual([]);
const continued = ready(); continued.runs.push({ id: "next", agentId: "morgan", status: "succeeded" });
expect(unmetPriorDeliveryReview(continued, "morgan", "riley")).toEqual([]);
});
it("names each unmet field of a review requested outside the configured stage", () => {
const generic = ready(); Object.assign(generic.task, { executionState: null });
expect(unmetPriorDeliveryReview(generic, "morgan", "riley")).toEqual([
"executionState.status=null (want pending)", "executionState.currentParticipant=null (want the configured reviewer)"]);
});
it("names a bypassed review, an active run and a missing delivery", () => {
const bypassed = ready(); Object.assign(bypassed.task, { status: "done", executionState: null });
bypassed.runs.push({ id: "live", agentId: "morgan", status: "running" }); bypassed.attachments = [];
expect(unmetPriorDeliveryReview(bypassed, "morgan", "riley")).toEqual([
"task.status=done (want in_review)", "executionState.status=null (want pending)",
"executionState.currentParticipant=null (want the configured reviewer)", "active worker runs=1 (want 0)",
"worker deliveries from a succeeded worker run=0 of 0 attachments (want at least 1)"]);
const other = ready(); other.task.executionState.currentParticipant.userId = "someone";
expect(unmetPriorDeliveryReview(other, "morgan", "riley")).toEqual(["executionState.currentParticipant=user:other (want the configured reviewer)"]);
});
});
describe("private delegation audience calibration", () => {