mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 22:13:59 +01:00
## Thinking Path > - Paperclip manages agent work and human decisions. > - Onboarding can approve a plan, a hire, and an execution step in separate turns. > - Tests covered single approvals more closely than that full history. > - The repaired gate must retain valid plan approval through later decisions. > - Every consumer must recognize the saved confirmation kind and selected scope. > - This change tests that contract and registers a complete interview-to-execution journey. ## Linked Issues or Issue Description Refs #19043, #17914, #18283. The earlier tests admitted one accepted proposal at a time. Decomposition fixtures used standard confirmations. The direct-task E2E path forbids hiring. Those cases did not cover an accepted checkbox plan followed by hiring and a separate untargeted execution approval. ## What Changed - Add eight PostgreSQL contract cases for standard and checkbox plan approvals. Native tools create the cards. An authenticated human accepts them through HTTP. A later native run reads the approved revision and actual selected scope. - Exercise native task creation, generic REST creation, child REST creation, and accepted-plan decomposition. Check the assigned child and one creation audit event. Check durable replay on the three entry points that support it. A pending execution confirmation still prevents creation. - Register the explicit-only `onboarding-approval-history` suite for local native Codex and ACPX Claude. The browser chooses the interview, selects only drafting in the saved plan, approves one writer hire, and approves a separate Start drafting confirmation. The model must hire the writer and use that writer's own child execution to save the welcome document. - Grade each human decision, the unchanged plan revision and selection, approval timing, one assigned child and worker execution, the exact writer-authored saved revision, no pending requests, and persistence after reload. Calibrate the oracle against missing, forged, premature, duplicate, and changed outcomes. - Preserve synthetic option choices, document/run bindings, and saved revision authorship in the existing public first-task evidence projection. Keep private provider/account data excluded. Document the workflow, evidence, billing, and cleanup bounds. - Align the approval-history fixture with its completion oracle: the onboarding parent must be Done. Test both native profiles against graded completion evidence and reject a completed child whose parent remains In review. Count parent heartbeats separately from the writer: accept one to seven parent records while retaining the twelve-company-run cap and the independent three-approval/one-writer oracle. - Retain the existing 52-cell first-task suite. The new suite has one attempt, seven expected work turns, a twelve-run maximum, a fifteen-minute deadline, and the existing $5 company task budget. It is excluded from `--all`. - Update two stale cleanup assertions to include the existing thirty-second deletion timeout. ## Verification - Red control: temporarily restore the pre-fix admission module and run the new checkbox/native history case. It fails with the expected `First-task proposal requires human acceptance` error after plan, hiring, and execution acceptance. Restore the repaired module after the probe. - The final post-rebase history matrix and existing onboarding safety suite passed together: 74 tests, no skips. - Final focused Runner E2E support passed: 413 Vitest tests and all 370 Node support checks. This covers catalog discovery, journey controls, wrong-outcome calibration, public evidence, and existing first-task behavior. - Review controls: four authorship cases fail before the revision check. A delayed browser-response control fails before the approval-time fix. Both pass after repair. The final oracle requires the exact current document revision, writer ID, native child run ID, matching body, and readback after reload. - Credential-free discovery lists both new native cells. No provider call was made. `OPENAI_API_KEY` and `ANTHROPIC_API_KEY` are absent, so the real provider journey remains unqualified. - The earlier broad credential-free E2E support run reported 6,728 passing cases, three skipped cases, and nine failures in three unrelated files. All nine reproduce with the changed E2E source restored to the unchanged baseline: three expired overhead-fixture cases, one stale Pi profile-version assertion, and five workflow security assertions. This change does not alter those files, paid workflow policy, or provider pins. - Previous full workspace typecheck/build and [Linux CI](https://github.com/paperclipai/paperclip/actions/runs/41921754638) passed on `4f431f5aae99d072cc75027e90d7b847b4c342ca`, including the new eight-case PostgreSQL file. Ponytail subsequently found a fixture/oracle terminal-status mismatch. Both native-cell controls fail on that fixture (`done` observed versus `in_review` expected), while the parent-in-review negative control passes. A second comparison uses the actual `matchesRunCount` predicate: graded evidence with six parent runs and one writer run fails the old exact-seven-parent requirement. Both fixes pass these controls, all 413 focused support tests, all 370 Node checks, and the owning E2E project typecheck. [Fresh CI](https://github.com/paperclipai/paperclip/actions/runs/41936699686) passed on `91c980b110252da451b92eda597c168517de1276`: 48 successful jobs and one expected skip. Greptile reviewed this head at 5/5 with no unresolved findings. Ponytail's terminal-status finding is fixed and its thread is resolved; its current-head re-review remains queued. Live provider cells remain unqualified. ## Risks The production API and approval gate are unchanged. The new paid journey can expose model or product failures at an intermediate approval boundary. It retains those failures and uses the existing diagnostics and cleanup. Registration and deterministic tests do not establish a live provider pass. The unrelated baseline support failures remain explicit. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, and code execution. The exact serving model ID and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass (focused tests; unrelated baseline failures are recorded above) - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
490 lines
19 KiB
TypeScript
490 lines
19 KiB
TypeScript
import { isBlockedUnstartedWake, isTerminalUnstartedWake } from "./non-execution-wake.js";
|
||
import { firstTaskRejection, firstTaskRejectionReplyRecorded, isFirstTaskRejectionCancellation } from "./first-task-rejection.js";
|
||
import { answerableRuntimeRunIds } from "./runtime-question-readiness.js";
|
||
import { sanitizeJson } from "./redaction.js";
|
||
import { createHash } from "node:crypto";
|
||
import { firstTaskScenario } from "./first-task-cases.js";
|
||
import { completionDelivery } from "./completion-updates.js";
|
||
import { APPROVAL_HISTORY_CASE, approvalHistoryWriter, gradeApprovalHistory } from "./first-task-approval-history.js";
|
||
export type Row = { id: string; [key: string]: any };
|
||
export interface FirstTaskCheckpoint {
|
||
id: string;
|
||
at: string;
|
||
phase:
|
||
| "opening"
|
||
| "response"
|
||
| "clarified"
|
||
| "revised"
|
||
| "plan-accepted"
|
||
| "hiring-proposed"
|
||
| "hiring-accepted"
|
||
| "execution-proposed"
|
||
| "accepted"
|
||
| "rejected"
|
||
| "finished";
|
||
issueId: string;
|
||
tasks: Row[];
|
||
agents: Row[];
|
||
comments: Row[];
|
||
interactions: Row[];
|
||
documents: Row[];
|
||
attachments?: Row[];
|
||
runs: Row[];
|
||
}
|
||
export interface FirstTaskEvidence {
|
||
caseId: string;
|
||
nonce: string;
|
||
onboardingIssueId: string;
|
||
agentId: string;
|
||
initialTaskIds: string[];
|
||
instructions: Array<{
|
||
path: string;
|
||
content: string;
|
||
sha256: string;
|
||
contentSha256?: string;
|
||
redacted?: boolean;
|
||
}>;
|
||
source?: { sha: string; ref: string; dirty: boolean };
|
||
runtimeSettings?: Record<string, unknown>;
|
||
configuredModel: string | null;
|
||
observedModels: string[];
|
||
checkpoints: FirstTaskCheckpoint[];
|
||
checks: FirstTaskCheck[];
|
||
}
|
||
export interface FirstTaskCheck {
|
||
id: string;
|
||
passed: boolean;
|
||
/** Unexercised checks remain non-passing, but are not behavioral failures. */
|
||
notReached?: string;
|
||
evidence: string[];
|
||
detail: string;
|
||
}
|
||
export const digestText = (text: string) =>
|
||
createHash("sha256").update(text).digest("hex");
|
||
export function snapshotInstruction(
|
||
path: string,
|
||
content: string,
|
||
secrets: readonly string[] = [],
|
||
) {
|
||
const safe = sanitizeJson({ content }, secrets) as { content: string };
|
||
return {
|
||
path,
|
||
content: safe.content,
|
||
sha256: digestText(content),
|
||
contentSha256: digestText(safe.content),
|
||
redacted: safe.content !== content,
|
||
};
|
||
}
|
||
/** Only user-visible affirmative proposals count; incidental task nouns do not. */
|
||
function hasTaskProposal(text: string, confirmationCard = false) {
|
||
const affirmative = text.split(/(?<=[.!?])\s+/).filter((sentence) =>
|
||
!/\b(?:no\s+(?:(?:new|child)\s+)?(?:subtask|task)|(?:not|never|won['’]t|don['’]t|without)\b[^.!?]{0,80}\b(?:subtask|task))\b/i.test(sentence),
|
||
).join("\n");
|
||
return /\b(?:approve|accept|propose|proposed|proposing|proposal|suggest|suggested|suggesting|recommend|recommended|recommending|create|creating|set up)\b[\s\S]{0,160}\b(?:subtask|task)\b/i.test(affirmative) ||
|
||
/\b(?:subtask|task)\b[^.!?\n]{0,30}\bproposal\b/i.test(affirmative) ||
|
||
(confirmationCard && (
|
||
/\b(?:I|we)(?:['’]ll| will)\s+(?:save|attach|write|make|open|create)\b[^.!?]{0,160}\b(?:(?:new|child)\s+task|subtask)\b/i.test(affirmative) ||
|
||
/\b(?:subtask|task)\s+(?:I|we)(?:['’]ll| will)\s+(?:create|open|set up)\b/i.test(affirmative)
|
||
));
|
||
}
|
||
|
||
export const activeRuns = (runs: Row[]) =>
|
||
runs.filter((run) => ["queued", "running"].includes(run.status));
|
||
export function questionCount(interaction: Row) {
|
||
return (
|
||
interaction.payload?.questionSet?.questions ??
|
||
interaction.payload?.questions ??
|
||
[]
|
||
).length;
|
||
}
|
||
/** Inspect the presentation the user saw, including superseded cards at earlier checkpoints. */
|
||
function gradeQuestionChoices(
|
||
checkpoints: FirstTaskCheckpoint[],
|
||
): FirstTaskCheck {
|
||
const seen = new Set<string>();
|
||
const failures: string[] = [];
|
||
const refs = new Set<string>();
|
||
for (const checkpoint of checkpoints) {
|
||
for (const interaction of checkpoint.interactions) {
|
||
if (interaction.kind !== "ask_user_questions") continue;
|
||
const questions =
|
||
interaction.payload?.questionSet?.questions ??
|
||
interaction.payload?.questions ??
|
||
[];
|
||
for (const question of questions) {
|
||
const key = JSON.stringify([interaction.id, question]);
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
// A canonical text question has no visible choice controls. Its legacy
|
||
// compatibility entry may contain a single freeText option; ignore that.
|
||
if (question?.answerMode === "text") continue;
|
||
const labels = new Set<string>(
|
||
(question?.options ?? [])
|
||
.map((option: any) =>
|
||
String(option.label ?? "")
|
||
.trim()
|
||
.toLowerCase(),
|
||
)
|
||
.filter(Boolean),
|
||
);
|
||
if (labels.size >= 2) continue;
|
||
refs.add(checkpoint.id);
|
||
failures.push(
|
||
`${interaction.id} / ${question?.id ?? "unknown question"}: "${question?.prompt ?? ""}" has ${labels.size} distinct choice option(s); expected at least 2, or a text question`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
id: "question-choice-options",
|
||
passed: failures.length === 0,
|
||
detail:
|
||
failures.length > 0
|
||
? failures.join("; ")
|
||
: "Every presented choice question offers at least two distinct options; open-ended text questions are allowed",
|
||
evidence: failures.length > 0 ? [...refs] : checkpoints.map((c) => c.id),
|
||
};
|
||
}
|
||
|
||
/** A terminal parent with no child is an assessable wrong outcome, not a transport timeout. */
|
||
export function firstTaskCompletionSettled(
|
||
tasks: Row[],
|
||
initialTaskIds: string[],
|
||
issueId: string,
|
||
): boolean {
|
||
const children = tasks.filter((task) => !initialTaskIds.includes(task.id));
|
||
return children.length > 0
|
||
? children.every((task) => task.status === "done")
|
||
: tasks.some((task) => task.id === issueId && task.status === "done");
|
||
}
|
||
|
||
function isPlanDocument(document: Row): boolean {
|
||
return (
|
||
document.key === "plan" ||
|
||
(/(?:^|[-_])plan(?:$|[-_])/i.test(String(document.key)) &&
|
||
(/\bplan\b/i.test(String(document.title ?? "")) ||
|
||
/^#+\s+plan\b/im.test(String(document.body ?? ""))))
|
||
);
|
||
}
|
||
/** Plan/proposal documents are planning evidence, never durable completion output. */
|
||
function isPlanningDocument(document: Row): boolean {
|
||
return (
|
||
isPlanDocument(document) ||
|
||
(/(?:^|[-_])proposal(?:$|[-_])/i.test(String(document.key)) &&
|
||
/(?:^|\n)(?:#+\s*)?(?:proposed (?:(?:child|single|first) )?task|(?:(?:first|single)[- ])?task proposal|proposal)\b/i.test(
|
||
`${document.title ?? ""}\n${document.body ?? ""}`,
|
||
))
|
||
);
|
||
}
|
||
function isVerifiedAttachment(a: Row): boolean {
|
||
return a.contentVerified === true && typeof a.body === "string" && a.contentSha256 === digestText(a.body);
|
||
}
|
||
function attachmentDocument(a: Row): Row {
|
||
return { ...a, key: String(a.originalFilename ?? a.filename ?? "").replace(/\.(?:md|txt)$/i, ""), title: a.title ?? a.originalFilename ?? a.filename };
|
||
}
|
||
function isPlanningAttachment(a: Row): boolean {
|
||
return isVerifiedAttachment(a) && isPlanningDocument(attachmentDocument(a));
|
||
}
|
||
function verifiedFirstTaskOutputs(checkpoint: FirstTaskCheckpoint): Row[] {
|
||
return [...checkpoint.documents, ...(checkpoint.attachments ?? []).filter(isVerifiedAttachment).map(attachmentDocument)];
|
||
}
|
||
/** Provider identities, not generic sessionReused flags, prove continuity. */
|
||
export function gradeNativeSessionContinuity(runs: Row[], issueId: string): FirstTaskCheck {
|
||
const parent = [...new Map(runs.filter((run) => run.nativeIssueId === issueId).map((run) => [run.id, run])).values()];
|
||
const identities = parent.map((run) => ({
|
||
run: run.id,
|
||
session: run.nativeSessionId,
|
||
provider: run.runnerProfileJson?.sessionCheckpoint?.providerSessionId,
|
||
workspace: run.runnerProfileJson?.nativeExecutionInput?.binding?.executionWorkspaceId,
|
||
}));
|
||
const passed = parent.length >= 2 && ["session", "provider", "workspace"].every((key) =>
|
||
identities.every((identity) => typeof identity[key as "session"] === "string" && identity[key as "session"].length > 0) &&
|
||
new Set(identities.map((identity) => identity[key as "session"])).size === 1,
|
||
);
|
||
return { id: "native-session-continuity", passed, evidence: ["finished"],
|
||
detail: `Same-task follow-ups must retain native/provider/workspace identities: ${JSON.stringify(identities)}` };
|
||
}
|
||
|
||
export function gradeFirstTask(e: FirstTaskEvidence): FirstTaskCheck[] {
|
||
const scenario = firstTaskScenario(e.caseId, e.nonce);
|
||
const checks: FirstTaskCheck[] = [];
|
||
const add = (
|
||
id: string,
|
||
passed: boolean,
|
||
detail: string,
|
||
refs: string[],
|
||
notReached?: string,
|
||
) =>
|
||
checks.push({
|
||
id,
|
||
passed: notReached ? false : passed,
|
||
detail,
|
||
evidence: refs,
|
||
...(notReached ? { notReached } : {}),
|
||
});
|
||
const first = e.checkpoints.find((c) => c.phase === "response");
|
||
const last = e.checkpoints.at(-1);
|
||
const approval = e.checkpoints.find((c) => c.phase === "accepted");
|
||
const rejection =
|
||
firstTaskRejection(e)
|
||
? e.checkpoints.find((c) => c.phase === "rejected")
|
||
: undefined;
|
||
const extras = (c: FirstTaskCheckpoint) =>
|
||
c.tasks.filter((t) => !e.initialTaskIds.includes(t.id));
|
||
const seeded = new Set(e.checkpoints[0]?.comments.map((c) => c.id));
|
||
const replies = (c: FirstTaskCheckpoint) =>
|
||
c.comments.filter((r) => r.authorAgentId && !seeded.has(r.id));
|
||
add(
|
||
"recorded-response",
|
||
Boolean(
|
||
first &&
|
||
replies(first).length +
|
||
first.interactions.filter(
|
||
(i) =>
|
||
!e.checkpoints[0]?.interactions.some((old) => old.id === i.id),
|
||
).length >
|
||
0,
|
||
),
|
||
"An agent response or structured interaction was recorded",
|
||
first ? [first.id] : [],
|
||
);
|
||
add(
|
||
"instruction-snapshot",
|
||
e.instructions.some((i) => i.path.endsWith("first-task/SKILL.md")) &&
|
||
e.instructions.some((i) => i.path === "AGENTS.md") &&
|
||
e.instructions.every(
|
||
(i) =>
|
||
digestText(i.content) === (i.contentSha256 ?? i.sha256) &&
|
||
(i.redacted || i.sha256 === digestText(i.content)),
|
||
),
|
||
"Actual persona and skill were retained with verified hashes",
|
||
e.instructions.map((i) => i.path),
|
||
);
|
||
checks.push(gradeQuestionChoices(e.checkpoints));
|
||
if (!first || !last) return checks;
|
||
const text = replies(first)
|
||
.map((r) => r.body ?? "")
|
||
.join("\n");
|
||
const questions = first.interactions.filter(
|
||
(i) =>
|
||
i.kind === "ask_user_questions" &&
|
||
!e.checkpoints[0]?.interactions.some((old) => old.id === i.id),
|
||
);
|
||
add(
|
||
"no-reintroduction",
|
||
!/welcome to paperclip|your first agent teammate/i.test(text),
|
||
"Do not repeat the seeded welcome",
|
||
[first.id],
|
||
);
|
||
add(
|
||
"opening-not-repeated",
|
||
!questions.some((i) =>
|
||
(i.payload?.questionSet?.questions ?? i.payload?.questions ?? []).some(
|
||
(q: any) => q.id === "first-task-opening",
|
||
),
|
||
),
|
||
"Do not post another opening choice",
|
||
[first.id],
|
||
);
|
||
if (scenario.opening === "interview")
|
||
add(
|
||
"interview-questions",
|
||
questions.length === 1 &&
|
||
questionCount(questions[0]) >= 3 &&
|
||
questionCount(questions[0]) <= 4,
|
||
"One structured interview with 3–4 questions",
|
||
[first.id],
|
||
);
|
||
if (scenario.opening === "ambiguous")
|
||
add(
|
||
"focused-clarification",
|
||
questions.length > 0 || (text.match(/\?/g)?.length ?? 0) >= 2,
|
||
"Ask focused questions before proposing ambiguous work",
|
||
[first.id],
|
||
);
|
||
if (["task", "message"].includes(scenario.opening))
|
||
add(
|
||
"subtask-proposal",
|
||
[text, ...first.documents.filter(isPlanningDocument)
|
||
.map((d) => `${d.title ?? ""}\n${d.body ?? ""}`)]
|
||
.some((visible) => hasTaskProposal(visible)) ||
|
||
first.interactions.some((i) =>
|
||
["request_confirmation", "request_checkbox_confirmation"].includes(i.kind) &&
|
||
hasTaskProposal([i.title, i.summary, i.payload?.prompt, i.payload?.detailsMarkdown]
|
||
.filter(Boolean).join("\n"), true),
|
||
),
|
||
"Propose a task for the concrete request",
|
||
[first.id],
|
||
);
|
||
if (scenario.opening === "plan" || ["interview-plan-accept", APPROVAL_HISTORY_CASE].includes(e.caseId)) {
|
||
const proposal =
|
||
e.checkpoints.find((c) => c.phase === "clarified") ?? first;
|
||
add(
|
||
"durable-plan",
|
||
proposal.documents.some(
|
||
(d) => isPlanDocument(d) && String(d.body).trim(),
|
||
),
|
||
"Save the requested plan before acceptance",
|
||
[proposal.id],
|
||
);
|
||
}
|
||
const before = e.checkpoints.filter(
|
||
(c) =>
|
||
c.phase !== "opening" &&
|
||
(!approval || Date.parse(c.at) < Date.parse(approval.at)),
|
||
);
|
||
if (e.caseId === APPROVAL_HISTORY_CASE) {
|
||
checks.push(...gradeApprovalHistory(e, scenario.marker));
|
||
} else if (scenario.opening !== "ordinary") {
|
||
add(
|
||
"no-premature-work",
|
||
before.every(
|
||
(c) =>
|
||
extras(c).length === 0 &&
|
||
c.agents.length === e.checkpoints[0].agents.length &&
|
||
c.documents.every(isPlanningDocument) &&
|
||
(c.attachments ?? []).every(isPlanningAttachment) &&
|
||
(c.tasks.find((t) => t.id === c.issueId)?.status !== "done" ||
|
||
Boolean(rejection && Date.parse(c.at) >= Date.parse(rejection.at))),
|
||
),
|
||
"Before acceptance: no hires, execution tasks, finished output, or claimed completion; closing an unexecuted rejected task is allowed",
|
||
before.map((c) => c.id),
|
||
);
|
||
} else {
|
||
add(
|
||
"ordinary-task-control",
|
||
questions.length === 0 &&
|
||
extras(last).length === 0 &&
|
||
verifiedFirstTaskOutputs(last).some(
|
||
(d) =>
|
||
!isPlanningDocument(d) && String(d.body).includes(scenario.marker),
|
||
),
|
||
"Ordinary work produces output without onboarding questions or delegation",
|
||
[last.id],
|
||
);
|
||
}
|
||
if (e.caseId === "accept-while-running") {
|
||
const accepted = approval?.interactions.find(i => i.status === "accepted" &&
|
||
["request_confirmation", "request_checkbox_confirmation"].includes(i.kind));
|
||
const source = last.runs.find(r => r.id === accepted?.sourceRunId);
|
||
const acceptedAt = Date.parse(accepted?.resolvedAt ?? "");
|
||
const startedAt = Date.parse(source?.startedAt ?? "");
|
||
const finishedAt = Date.parse(source?.finishedAt ?? "");
|
||
const overlapped = Number.isFinite(acceptedAt) && Number.isFinite(startedAt) &&
|
||
Number.isFinite(finishedAt) && startedAt <= acceptedAt && acceptedAt < finishedAt;
|
||
add("accepted-while-running", overlapped,
|
||
"The persisted approval resolution must fall inside its source run's actual execution interval",
|
||
approval ? [approval.id, last.id] : [last.id],
|
||
overlapped ? undefined : "The recording did not prove acceptance during the source run; the concurrency regression was not exercised.");
|
||
}
|
||
if (!scenario.firstResponseOnly && e.caseId !== "reject-no-execution") {
|
||
const notReached = approval
|
||
? undefined
|
||
: "The recording did not reach the acceptance checkpoint; this part of the journey was not evaluated.";
|
||
const acceptedComment = approval?.comments.some(
|
||
(c) => !c.authorAgentId && c.body === scenario.acceptance,
|
||
);
|
||
const acceptedCard = approval?.interactions.some(
|
||
(i) =>
|
||
["request_confirmation", "request_checkbox_confirmation"].includes(
|
||
i.kind,
|
||
) &&
|
||
i.status === "accepted" &&
|
||
i.result?.outcome === "accepted",
|
||
);
|
||
add(
|
||
"acceptance-recorded",
|
||
Boolean(approval && (acceptedComment || acceptedCard)),
|
||
"Explicit acceptance is persisted as a user comment or approved confirmation card",
|
||
approval ? [approval.id] : [],
|
||
notReached,
|
||
);
|
||
if (e.caseId === "interview-plan-accept") {
|
||
add(
|
||
"accepted-plan-retained",
|
||
last.documents.some((d) => isPlanDocument(d) && String(d.body).trim()),
|
||
"The accepted plan remains available as a durable document",
|
||
[last.id],
|
||
notReached,
|
||
);
|
||
} else {
|
||
const children = extras(last);
|
||
const expectedAssignee = e.caseId === APPROVAL_HISTORY_CASE
|
||
? last.agents.find(agent => agent.name === approvalHistoryWriter(scenario.marker))?.id : e.agentId;
|
||
add(
|
||
"one-scoped-subtask",
|
||
children.length === 1 &&
|
||
children[0].parentId === e.onboardingIssueId &&
|
||
Boolean(expectedAssignee) && children[0].assigneeAgentId === expectedAssignee,
|
||
"Exactly one approved subtask belongs to this onboarding issue and its intended agent",
|
||
[last.id],
|
||
notReached,
|
||
);
|
||
add(
|
||
"creation-after-acceptance",
|
||
Boolean(approval) &&
|
||
children.every(
|
||
(t) =>
|
||
typeof t.createdAt === "string" &&
|
||
Date.parse(t.createdAt) >= Date.parse(approval!.at),
|
||
),
|
||
"Task creation must follow acceptance, including between checkpoints",
|
||
[last.id],
|
||
notReached,
|
||
);
|
||
add(
|
||
"durable-completion",
|
||
children.length === 1 &&
|
||
children[0].status === "done" &&
|
||
verifiedFirstTaskOutputs(last).some(
|
||
(d) =>
|
||
d.issueId === children[0].id &&
|
||
!isPlanningDocument(d) &&
|
||
String(d.body).includes(scenario.marker) &&
|
||
(e.caseId !== "revise-accept" ||
|
||
(!String(d.body).includes(scenario.originalMarker) &&
|
||
/sunday/i.test(d.body))),
|
||
),
|
||
"Approved output is saved on the completed child, with revised scope when applicable",
|
||
[last.id],
|
||
notReached,
|
||
);
|
||
}
|
||
}
|
||
if (e.caseId === "reject-no-execution")
|
||
add(
|
||
"rejection-respected",
|
||
firstTaskRejectionReplyRecorded(e, last.comments, last.runs) &&
|
||
extras(last).length === 0 &&
|
||
last.documents.every(isPlanningDocument) &&
|
||
(last.attachments ?? []).every(isPlanningAttachment) &&
|
||
activeRuns(last.runs).length === 0,
|
||
"Persisted refusal receives a response without execution tasks, finished output, or remaining runs",
|
||
[last.id],
|
||
rejection
|
||
? undefined
|
||
: "The recording did not reach the rejection checkpoint; the rejection response was not evaluated.",
|
||
);
|
||
add(
|
||
"provider-runs-succeeded",
|
||
last.runs.length > 0 && last.runs.every((r) => r.status === "succeeded" || isBlockedUnstartedWake(r) ||
|
||
isTerminalUnstartedWake(r, last.tasks) ||
|
||
(isFirstTaskRejectionCancellation(r, e, last.tasks) && firstTaskRejectionReplyRecorded(e, last.comments, [r])) ||
|
||
(scenario.firstResponseOnly && r.status === "running" && answerableRuntimeRunIds(last.interactions).has(r.id))),
|
||
"Runs succeeded, were suppressed before execution, stopped with a recorded refusal response, or paused on an answerable first-response native question",
|
||
[last.id],
|
||
);
|
||
if (e.runtimeSettings?.adapterType === "paperclip_runner" &&
|
||
["task-reply-accept", "task-card-accept"].includes(e.caseId) && last.phase === "finished") {
|
||
checks.push({ ...gradeNativeSessionContinuity(e.checkpoints.flatMap((checkpoint) => checkpoint.runs), e.onboardingIssueId), evidence: [last.id] });
|
||
}
|
||
if (e.runtimeSettings?.completionDeliveryProbe && last) {
|
||
const worker = last.tasks.find(t => t.parentId === e.onboardingIssueId) ?? {};
|
||
checks.push(...completionDelivery({ sourceId: e.onboardingIssueId, worker, documents: last.documents,
|
||
comments: last.comments, runs: last.runs, marker: scenario.marker,
|
||
renderedLinks: e.runtimeSettings.completionRenderedLinks as Array<{ commentId: string; href: string }> | undefined,
|
||
}).checks.map(check => ({ ...check, evidence: [last.id] })));
|
||
}
|
||
return checks;
|
||
}
|