mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-14 17:40:37 +02:00
Keep project edits organizational, validate immutable native task roots before materialization, align authorization fixtures, and correct live E2E evidence collection. Co-Authored-By: Paperclip <noreply@paperclip.ing>
88 lines
6.8 KiB
TypeScript
88 lines
6.8 KiB
TypeScript
export interface TaskWorkspaceCheckpoint {
|
|
turn: number;
|
|
issue: { id: string; projectId: string | null; executionWorkspaceId: string | null; status: string; scheduledRetry?: unknown };
|
|
workspace: { id: string; projectId: string | null; cwd: string };
|
|
confinedTaskRoot: boolean;
|
|
note: string;
|
|
repositories: Array<{ id: string; state: string; relativePath: string; pinnedCommit: string | null; requestedByIssueId: string | null; requestKeys: string[] }>;
|
|
run: { id: string; status: string; runtimeMode?: string; nativePhase?: string; nativeBinding?: string; nativeCwd?: string; transport?: string; authenticated?: boolean; nativeCompleted?: boolean };
|
|
repository?: { head: string; baseIsAncestor: boolean; dirty: boolean; content: string };
|
|
prepareCalls?: number;
|
|
}
|
|
export interface TaskWorkspaceObservation {
|
|
nonce: string;
|
|
native: boolean;
|
|
daytona: boolean;
|
|
initial: { workspaceId: string; cwd: string; repositories: number } | null;
|
|
checkpoints: TaskWorkspaceCheckpoint[];
|
|
projectCount: number;
|
|
runCount: number;
|
|
restarted: boolean;
|
|
pendingInteractions: number;
|
|
activeOperations: number;
|
|
activeRecovery: boolean;
|
|
artifact: { content: string; contentVerified: boolean; mimeType: string } | null;
|
|
remoteLeaseRunIds: string[];
|
|
}
|
|
|
|
/** Every check uses independent persisted/API/filesystem evidence, never model assertions. */
|
|
export function gradeTaskWorkspaces(value: TaskWorkspaceObservation) {
|
|
const checks: Array<{ id: string; passed: boolean; detail: string }> = [];
|
|
const check = (id: string, passed: boolean, detail: string) => checks.push({ id, passed, detail });
|
|
const [first, second, third] = value.checkpoints;
|
|
const complete = value.checkpoints.length === 3 && value.checkpoints.every((p, i) => p.turn === i + 1);
|
|
check("three-attributed-turns", complete && value.runCount === 3 && new Set(value.checkpoints.map(p => p.run.id)).size === 3
|
|
&& value.checkpoints.every(p => p.run.status === "succeeded" && p.issue.status === "done" && p.issue.scheduledRetry == null),
|
|
"Exactly three successful requested turns with no hidden retries or unfinished tasks.");
|
|
check("projectless-task-owned-root", complete && value.projectCount === 0 && value.checkpoints.every(p =>
|
|
p.issue.projectId === null && p.workspace.projectId === null && p.confinedTaskRoot && p.issue.executionWorkspaceId === p.workspace.id),
|
|
"Task and execution workspace remain projectless, with a confined company/task directory rather than agent home.");
|
|
// The provider may enqueue its receipt before the first browser observation.
|
|
// Deferred acquisition is checked at the settled first-turn checkpoint below.
|
|
check("immutable-root-and-binding", complete && value.initial !== null &&
|
|
value.checkpoints.every(p => p.workspace.id === value.initial!.workspaceId && p.workspace.cwd === value.initial!.cwd),
|
|
"The independently observed initial root and binding survive all three admissions.");
|
|
check("task-file-continuity", complete && value.checkpoints.every(p => p.note === `task-files-turn-${p.turn}-${value.nonce}\n`),
|
|
"Public execution-workspace file reads match the exact expected bytes after every turn.");
|
|
const a = first?.repositories[0], b = second?.repositories[0], c = third?.repositories[0];
|
|
check("deferred-idempotent-acquisition", complete && value.checkpoints.every(p => p.repositories.length === 1 &&
|
|
p.repositories[0]?.requestedByIssueId === p.issue.id && p.repositories[0]?.requestKeys.length === 1 && p.repositories[0]?.requestKeys[0] === `repository-${value.nonce}`)
|
|
&& a?.state === "pending" && a.pinnedCommit === null && a.id === b?.id && a.id === c?.id
|
|
&& /^\.paperclip-repositories\/task-repo-[a-f0-9]{24}$/.test(a.relativePath)
|
|
&& a.relativePath === b?.relativePath && a.relativePath === c?.relativePath && b?.state === "ready" && c?.state === "ready"
|
|
&& /^[a-f0-9]{40,64}$/.test(b?.pinnedCommit ?? "") && b?.pinnedCommit === c?.pinnedCommit,
|
|
"Duplicate preparation converges to one pending receipt, realized only on the next admission with a stable contained path and pinned source.");
|
|
check("nested-git-and-dirty-files", complete && second?.repository !== undefined && third?.repository !== undefined
|
|
&& second.repository.baseIsAncestor && third.repository.baseIsAncestor && second.repository.head !== b?.pinnedCommit
|
|
&& second.repository.head === third.repository.head && second.repository.dirty && third.repository.dirty
|
|
&& second.repository.content === `repository-dirty-${value.nonce}\n` && third.repository.content === second.repository.content,
|
|
"A new local commit and subsequent uncommitted bytes in a nested repository survive the next admission.");
|
|
check("controller-restart", value.restarted && complete, "The isolated controller restarted between the first and second requested turns.");
|
|
check("downloaded-artifact", value.artifact?.contentVerified === true && value.artifact.mimeType === "text/plain"
|
|
&& value.artifact.content === `repository-dirty-${value.nonce}\n`,
|
|
"The final run's registered downloadable artifact has verified metadata, digest and exact prior repository bytes.");
|
|
check("settled-control-plane", value.pendingInteractions === 0 && value.activeOperations === 0 && !value.activeRecovery,
|
|
"No unanswered decision, workspace operation or scheduled native recovery remains.");
|
|
check("runtime-and-transport", complete && value.checkpoints.every(p => value.native
|
|
? p.run.runtimeMode === "native" && p.run.nativePhase === "committed" && p.run.nativeCompleted === true && p.run.nativeBinding === p.workspace.id
|
|
&& typeof p.run.nativeCwd === "string" && p.run.nativeCwd.length > 0
|
|
&& p.run.transport === (value.daytona ? "provider_ingress" : "local_loopback") && (!value.daytona || p.run.authenticated === true)
|
|
: p.run.runtimeMode === "legacy"), "Each observed run uses the selected actual runtime and native transport/admitted binding.");
|
|
check("semantic-repository-tool", !value.native || first?.prepareCalls === 2,
|
|
"Native preparation uses two successful real prepare_repository tool calls, not a model-authored clone command.");
|
|
check("daytona-run-leases", !value.daytona || (complete && value.checkpoints.every(p => value.remoteLeaseRunIds.includes(p.run.id))),
|
|
"Every remote turn has a public Daytona environment lease attributed to that run.");
|
|
return checks;
|
|
}
|
|
|
|
/** Count durable runner receipts, including the provider's normalized tool name. */
|
|
export function isRepositoryPreparationReceipt(event: {
|
|
eventType?: string;
|
|
payload?: { prpEvent?: { sourceKind?: string; runId?: string; payload?: { name?: string; status?: string } } };
|
|
}, runId: string) {
|
|
const receipt = event.payload?.prpEvent;
|
|
return event.eventType === "tool.execution.completed" && receipt?.sourceKind === "runner"
|
|
&& receipt.runId === runId && receipt.payload?.status === "completed"
|
|
&& ["prepare_repository", "paperclip.prepare_repository", "paperclip_prepare_repository"].includes(receipt.payload.name ?? "");
|
|
}
|