feat(runner): add phase 1 static replay contract

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-07 19:13:00 +00:00
1 parent a86edda579
commit 1b51f95c6c
56 files changed
+5034 -5

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
/dist-browser/
/playwright-report/
/test-results/
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>Paperclip Runner · PRP Static Replay</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,20 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: ".",
testMatch: "static-replay.spec.ts",
fullyParallel: false,
workers: 1,
reporter: "line",
use: {
baseURL: "http://127.0.0.1:4179",
viewport: { width: 1440, height: 1000 },
},
webServer: {
command:
"pnpm exec vite preview --config vite.config.ts --host 127.0.0.1 --port 4179",
url: "http://127.0.0.1:4179",
reuseExistingServer: false,
timeout: 30_000,
},
});
@@ -0,0 +1,229 @@
import { useMemo, useState } from "react";
import { replayPhase1FixtureText } from "../../../src/tracer/phase1-replay";
import type { SessionSnapshot } from "../../../src/reducer/session-reducer";
import { Badge } from "./components/ui/badge";
import { Button } from "./components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "./components/ui/card";
import { Textarea } from "./components/ui/textarea";
const fixtureFiles = import.meta.glob(
"../../../protocol/fixtures/phase-01/*.json",
{ eager: true, query: "?raw", import: "default" },
) as Record<string, string>;
const fixtureOrder = [
"happy-path",
"failed-run",
"interrupted-run",
"duplicate-event",
"source-gap",
"unknown-optional-fields",
"unsupported-required-version",
];
interface BrowserFixture {
key: string;
label: string;
source: string;
}
const browserFixtures = Object.entries(fixtureFiles)
.map(([path, source]): BrowserFixture => {
const key = path.split("/").at(-1)?.replace(/\.json$/, "") ?? path;
const parsed = JSON.parse(source) as { name?: string };
return { key, label: parsed.name ?? key, source };
})
.sort((left, right) => fixtureOrder.indexOf(left.key) - fixtureOrder.indexOf(right.key));
function terminalTone(snapshot: SessionSnapshot) {
if (snapshot.integrity === "gap_detected") {
return "warning" as const;
}
if (snapshot.terminal?.runTerminalState === "succeeded") {
return "success" as const;
}
if (snapshot.terminal?.runTerminalState === "failed") {
return "danger" as const;
}
return "neutral" as const;
}
function SnapshotSummary({ snapshot }: { snapshot: SessionSnapshot }) {
return (
<>
<div className="snapshot-heading">
<div>
<p className="eyebrow">Final session snapshot</p>
<h2>{snapshot.fixtureName}</h2>
</div>
<Badge tone={terminalTone(snapshot)} data-testid="terminal-badge">
{snapshot.integrity === "gap_detected"
? "Gap detected"
: (snapshot.terminal?.runTerminalState ?? snapshot.runPhase)}
</Badge>
</div>
<dl className="snapshot-grid">
<div>
<dt>Run</dt>
<dd>{snapshot.identity.runId}</dd>
</div>
<div>
<dt>Session</dt>
<dd>{snapshot.identity.normalizedSessionId}</dd>
</div>
<div>
<dt>Turn</dt>
<dd>{snapshot.terminal?.turnTerminalState ?? snapshot.turnState}</dd>
</div>
<div>
<dt>Events</dt>
<dd>{snapshot.timeline.length}</dd>
</div>
</dl>
{snapshot.proposedResult ? (
<div className="result-summary" data-testid="result-summary">
<span>Reported {snapshot.proposedResult.reportedWorkDisposition}</span>
<p>{snapshot.proposedResult.summary}</p>
</div>
) : null}
{snapshot.gaps.length > 0 ? (
<div className="diagnostic" role="status">
Missing source sequence {snapshot.gaps.flatMap((gap) => gap.missing).join(", ")}.
Replay remains inspectable but is not complete.
</div>
) : null}
<div className="timeline-heading">
<h3>Ordered timeline</h3>
<span>{snapshot.duplicateEventIds.length} duplicate events ignored</span>
</div>
<ol className="timeline" data-testid="timeline">
{snapshot.timeline.map((event) => (
<li key={event.sourceEventId}>
<span className="sequence" aria-hidden="true">
{event.sourceSeq}
</span>
<div>
<div className="timeline-title">
<code>{event.eventType}</code>
<time dateTime={event.emittedAt}>{event.emittedAt.slice(11, 23)}</time>
</div>
<p>{event.summary}</p>
</div>
</li>
))}
</ol>
<details className="snapshot-json">
<summary>Inspect snapshot JSON</summary>
<pre>{JSON.stringify(snapshot, null, 2)}</pre>
</details>
</>
);
}
export function App() {
const defaultFixture = browserFixtures[0];
if (defaultFixture === undefined) {
throw new Error("No Phase 1 fixtures were bundled");
}
const [selectedFixture, setSelectedFixture] = useState(defaultFixture.key);
const [source, setSource] = useState(defaultFixture.source);
const [replay, setReplay] = useState(() => replayPhase1FixtureText(defaultFixture.source));
const selected = useMemo(
() => browserFixtures.find((fixture) => fixture.key === selectedFixture),
[selectedFixture],
);
function chooseFixture(key: string) {
const fixture = browserFixtures.find((candidate) => candidate.key === key);
if (fixture === undefined) {
return;
}
setSelectedFixture(key);
setSource(fixture.source);
setReplay(replayPhase1FixtureText(fixture.source));
}
return (
<main className="app-shell">
<header className="page-header">
<div>
<p className="eyebrow">Paperclip Runner Protocol · Phase 1</p>
<h1>Static session replay</h1>
<p>
Validate an editable PRP fixture and reduce its ordered events with the same
contract used by the CLI.
</p>
</div>
<Badge tone="neutral">Standalone · no Paperclip core</Badge>
</header>
<div className="workspace-grid">
<Card aria-labelledby="fixture-editor-title">
<CardHeader>
<CardTitle id="fixture-editor-title">Fixture input</CardTitle>
<CardDescription>
Choose a conformance case or edit the JSON, then validate and replay it.
</CardDescription>
</CardHeader>
<CardContent className="editor-content">
<label htmlFor="fixture-select">Fixture</label>
<select
id="fixture-select"
value={selectedFixture}
onChange={(event) => chooseFixture(event.target.value)}
>
{browserFixtures.map((fixture) => (
<option key={fixture.key} value={fixture.key}>
{fixture.label}
</option>
))}
</select>
<label htmlFor="fixture-source">PRP JSON</label>
<Textarea
id="fixture-source"
spellCheck={false}
value={source}
onChange={(event) => setSource(event.target.value)}
/>
<div className="editor-actions">
<span>{selected?.label ?? "Edited fixture"}</span>
<Button type="button" onClick={() => setReplay(replayPhase1FixtureText(source))}>
Validate &amp; replay
</Button>
</div>
</CardContent>
</Card>
<section className="replay-panel" aria-live="polite">
{replay.ok ? (
<SnapshotSummary snapshot={replay.snapshot} />
) : (
<div className="validation-errors" role="alert">
<Badge tone="danger">Validation failed</Badge>
<h2>Fixture cannot be replayed</h2>
<ul>
{replay.issues.map((issue, index) => (
<li key={`${issue.path}-${index}`}>
<code>{issue.path}</code> — {issue.message}
</li>
))}
</ul>
</div>
)}
</section>
</div>
</main>
);
}
@@ -0,0 +1,19 @@
import * as React from "react";
/** Source-adapted shadcn Badge with semantic state supplied through data-tone. */
export function Badge({
className = "",
tone = "neutral",
...props
}: React.ComponentProps<"span"> & {
tone?: "neutral" | "success" | "warning" | "danger";
}) {
return (
<span
data-slot="badge"
data-tone={tone}
className={`ui-badge ${className}`.trim()}
{...props}
/>
);
}
@@ -0,0 +1,15 @@
import * as React from "react";
/** Source-adapted shadcn Button for the standalone, non-Tailwind devtool. */
export function Button({
className = "",
...props
}: React.ComponentProps<"button">) {
return (
<button
data-slot="button"
className={`ui-button ${className}`.trim()}
{...props}
/>
);
}
@@ -0,0 +1,26 @@
import * as React from "react";
function slotClass(slot: string, className: string | undefined) {
return `${slot} ${className ?? ""}`.trim();
}
/** Source-adapted shadcn Card family for the standalone devtool. */
export function Card({ className, ...props }: React.ComponentProps<"section">) {
return <section data-slot="card" className={slotClass("ui-card", className)} {...props} />;
}
export function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-header" className={slotClass("ui-card-header", className)} {...props} />;
}
export function CardTitle({ className, ...props }: React.ComponentProps<"h2">) {
return <h2 data-slot="card-title" className={slotClass("ui-card-title", className)} {...props} />;
}
export function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
return <p data-slot="card-description" className={slotClass("ui-card-description", className)} {...props} />;
}
export function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-content" className={slotClass("ui-card-content", className)} {...props} />;
}
@@ -0,0 +1,15 @@
import * as React from "react";
/** Source-adapted shadcn Textarea for editable fixture JSON. */
export function Textarea({
className = "",
...props
}: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={`ui-textarea ${className}`.trim()}
{...props}
/>
);
}
@@ -0,0 +1,16 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./styles.css";
const root = document.getElementById("root");
if (root === null) {
throw new Error("Browser replay root element is missing");
}
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
);
@@ -0,0 +1,430 @@
:root {
color-scheme: light;
--background: #f7f7f5;
--foreground: #191917;
--card: #ffffff;
--card-foreground: #191917;
--muted: #efefeb;
--muted-foreground: #66665f;
--border: #deded7;
--primary: #242420;
--primary-foreground: #ffffff;
--success: #17633a;
--success-surface: #e9f5ed;
--warning: #8a5a10;
--warning-surface: #fff4dc;
--danger: #a02f2f;
--danger-surface: #fceaea;
--ring: #66665f;
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-md: 1rem;
--font-size-lg: 1.25rem;
--font-size-xl: 1.75rem;
--line-tight: 1.2;
--line-normal: 1.5;
--weight-medium: 500;
--weight-semibold: 600;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--radius-sm: 0.375rem;
--radius-md: 0.625rem;
--shadow-sm: 0 0.0625rem 0.125rem rgb(0 0 0 / 0.05);
--editor-height: 32rem;
--page-width: 90rem;
--control-height: 2.5rem;
--timeline-column: 2rem;
--motion-fast: 120ms;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 20rem;
background: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
font-size: var(--font-size-sm);
line-height: var(--line-normal);
}
button,
select,
textarea {
font: inherit;
}
h1,
h2,
h3,
p {
margin: 0;
}
.app-shell {
width: min(100%, var(--page-width));
margin-inline: auto;
padding: var(--space-8);
}
.page-header,
.snapshot-heading,
.timeline-heading,
.editor-actions,
.timeline-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
}
.page-header {
align-items: flex-start;
margin-bottom: var(--space-8);
}
.page-header > div {
display: grid;
gap: var(--space-2);
}
.page-header h1 {
font-size: var(--font-size-xl);
line-height: var(--line-tight);
letter-spacing: -0.02em;
}
.page-header p:not(.eyebrow),
.ui-card-description,
.timeline-heading span,
.editor-actions span,
dt {
color: var(--muted-foreground);
}
.eyebrow {
color: var(--muted-foreground);
font-size: var(--font-size-xs);
font-weight: var(--weight-semibold);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.workspace-grid {
display: grid;
grid-template-columns: minmax(22rem, 0.8fr) minmax(28rem, 1.2fr);
gap: var(--space-8);
align-items: start;
}
.ui-card {
display: grid;
gap: var(--space-5);
padding: var(--space-6);
background: var(--card);
color: var(--card-foreground);
border: 0.0625rem solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.ui-card-header,
.ui-card-content,
.editor-content {
display: grid;
gap: var(--space-3);
}
.ui-card-title,
.snapshot-heading h2 {
font-size: var(--font-size-lg);
line-height: var(--line-tight);
font-weight: var(--weight-semibold);
}
.ui-card-description {
font-size: var(--font-size-sm);
}
.ui-button {
min-height: var(--control-height);
padding: var(--space-2) var(--space-4);
border: 0;
border-radius: var(--radius-sm);
background: var(--primary);
color: var(--primary-foreground);
font-weight: var(--weight-medium);
cursor: pointer;
transition: opacity var(--motion-fast) ease;
}
.ui-button:hover {
opacity: 0.88;
}
.ui-button:focus-visible,
select:focus-visible,
.ui-textarea:focus-visible {
outline: 0.125rem solid var(--ring);
outline-offset: 0.125rem;
}
.ui-badge {
display: inline-flex;
align-items: center;
width: fit-content;
min-height: 1.625rem;
padding-inline: var(--space-2);
border-radius: 999rem;
background: var(--muted);
color: var(--muted-foreground);
font-size: var(--font-size-xs);
font-weight: var(--weight-semibold);
text-transform: capitalize;
white-space: nowrap;
}
.ui-badge[data-tone="success"] {
background: var(--success-surface);
color: var(--success);
}
.ui-badge[data-tone="warning"] {
background: var(--warning-surface);
color: var(--warning);
}
.ui-badge[data-tone="danger"] {
background: var(--danger-surface);
color: var(--danger);
}
label,
dt {
font-size: var(--font-size-xs);
font-weight: var(--weight-semibold);
}
select,
.ui-textarea {
width: 100%;
border: 0.0625rem solid var(--border);
border-radius: var(--radius-sm);
background: var(--card);
color: var(--card-foreground);
}
select {
min-height: var(--control-height);
padding: var(--space-2) var(--space-3);
}
.ui-textarea {
min-height: var(--editor-height);
resize: vertical;
padding: var(--space-3);
font-family: var(--font-mono);
font-size: var(--font-size-xs);
line-height: var(--line-normal);
tab-size: 2;
}
.replay-panel {
display: grid;
gap: var(--space-6);
}
.snapshot-heading {
align-items: flex-start;
}
.snapshot-heading > div {
display: grid;
gap: var(--space-2);
}
.snapshot-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--space-4);
margin: 0;
padding-block: var(--space-5);
border-block: 0.0625rem solid var(--border);
}
.snapshot-grid div {
min-width: 0;
}
dd {
margin: var(--space-1) 0 0;
overflow: hidden;
font-family: var(--font-mono);
font-size: var(--font-size-xs);
text-overflow: ellipsis;
white-space: nowrap;
}
.result-summary {
display: grid;
gap: var(--space-1);
}
.result-summary span {
color: var(--muted-foreground);
font-size: var(--font-size-xs);
font-weight: var(--weight-semibold);
text-transform: uppercase;
}
.result-summary p {
font-size: var(--font-size-md);
}
.diagnostic,
.validation-errors {
padding: var(--space-4);
border-radius: var(--radius-sm);
background: var(--warning-surface);
color: var(--warning);
}
.validation-errors {
display: grid;
gap: var(--space-4);
background: var(--danger-surface);
color: var(--danger);
}
.validation-errors ul {
margin: 0;
padding-left: var(--space-5);
}
.timeline-heading {
margin-top: var(--space-2);
}
.timeline-heading h3 {
font-size: var(--font-size-md);
}
.timeline-heading span {
font-size: var(--font-size-xs);
}
.timeline {
display: grid;
gap: 0;
margin: 0;
padding: 0;
list-style: none;
}
.timeline li {
display: grid;
grid-template-columns: var(--timeline-column) minmax(0, 1fr);
gap: var(--space-3);
padding-block: var(--space-3);
}
.timeline li + li {
border-top: 0.0625rem solid var(--border);
}
.sequence {
display: inline-grid;
place-items: center;
align-self: start;
width: var(--timeline-column);
height: var(--timeline-column);
border-radius: 999rem;
background: var(--muted);
color: var(--muted-foreground);
font-family: var(--font-mono);
font-size: var(--font-size-xs);
}
.timeline-title code,
.snapshot-json pre,
.validation-errors code {
font-family: var(--font-mono);
}
.timeline-title code {
font-size: var(--font-size-xs);
font-weight: var(--weight-semibold);
}
.timeline-title time,
.timeline li p {
color: var(--muted-foreground);
font-size: var(--font-size-xs);
}
.timeline li p {
margin-top: var(--space-1);
}
.snapshot-json {
border-top: 0.0625rem solid var(--border);
padding-top: var(--space-4);
}
.snapshot-json summary {
cursor: pointer;
font-weight: var(--weight-medium);
}
.snapshot-json pre {
max-height: var(--editor-height);
overflow: auto;
padding: var(--space-4);
border-radius: var(--radius-sm);
background: var(--muted);
font-size: var(--font-size-xs);
}
@media (max-width: 60rem) {
.workspace-grid {
grid-template-columns: 1fr;
}
.ui-textarea {
min-height: 20rem;
}
}
@media (max-width: 36rem) {
.app-shell {
padding: var(--space-5);
}
.page-header,
.snapshot-heading,
.timeline-heading {
align-items: flex-start;
flex-direction: column;
}
.snapshot-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (prefers-reduced-motion: reduce) {
:root {
--motion-fast: 0ms;
}
}
@@ -0,0 +1,30 @@
import { expect, test } from "@playwright/test";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
test("validates and renders the shared happy-path fixture", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: "Static session replay" })).toBeVisible();
await expect(page.getByTestId("terminal-badge")).toHaveText("succeeded");
await expect(page.getByTestId("timeline").getByRole("listitem")).toHaveCount(9);
await expect(page.getByTestId("result-summary")).toContainText(
"The scripted run completed successfully.",
);
await page.screenshot({
path: resolve(packageRoot, "knowledge/evidence/phase-01-static-replay.png"),
fullPage: true,
});
});
test("shows duplicate and unsupported-version fixture states", async ({ page }) => {
await page.goto("/");
await page.getByLabel("Fixture", { exact: true }).selectOption("duplicate-event");
await expect(page.getByText("1 duplicate events ignored")).toBeVisible();
await page
.getByLabel("Fixture", { exact: true })
.selectOption("unsupported-required-version");
await expect(page.getByRole("heading", { name: "Fixture cannot be replayed" })).toBeVisible();
await expect(page.getByText(/protocolVersion 2 is unsupported/)).toBeVisible();
});
+26 -5
View File
@@ -16,26 +16,47 @@
"README.md"
],
"scripts": {
"build": "pnpm run build:typescript && pnpm run build:rust",
"build:typescript": "tsc -p tsconfig.json",
"build": "pnpm run build:typescript && pnpm run build:rust && pnpm run build:browser",
"build:typescript": "pnpm run generate:protocol-types && tsc -p tsconfig.json",
"build:rust": "cargo build --manifest-path runner/Cargo.toml --locked --workspace",
"typecheck": "pnpm run typecheck:typescript && pnpm run typecheck:rust",
"typecheck:typescript": "tsc -p tsconfig.json --noEmit",
"build:browser": "vite build --config vite.config.ts",
"typecheck": "pnpm run typecheck:typescript && pnpm run typecheck:rust && pnpm run typecheck:browser",
"typecheck:typescript": "pnpm run check:protocol-types && tsc -p tsconfig.json --noEmit",
"typecheck:rust": "cargo check --manifest-path runner/Cargo.toml --locked --workspace",
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",
"test": "pnpm run test:typescript && pnpm run test:rust && node --test scripts/check-forbidden-imports.test.mjs",
"test:typescript": "vitest run",
"test:rust": "cargo test --manifest-path runner/Cargo.toml --locked --workspace",
"test:browser": "pnpm run build:browser && playwright test --config devtools/browser/playwright.config.ts",
"generate:protocol-types": "node scripts/generate-protocol-schema-module.mjs",
"check:protocol-types": "node scripts/generate-protocol-schema-module.mjs --check",
"generate:phase1-goldens": "pnpm run build:typescript && node scripts/generate-phase1-goldens.mjs",
"check:phase1-goldens": "node scripts/generate-phase1-goldens.mjs --check",
"check:browser-tokens": "node scripts/check-browser-tokens.mjs",
"check:forbidden-imports": "node scripts/check-forbidden-imports.mjs",
"check:phase0-parity": "vitest run src/tracer/phase0-runner.test.ts && cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core runs_the_mock_core_path_with_stable_output && node -e \"process.stdout.write('Rust and TypeScript Phase 0 tracer output matches the shared golden fixture.\\n')\"",
"check:phase1-parity": "vitest run src/reducer/session-reducer.test.ts && cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core phase1_fixture_parity && node -e \"process.stdout.write('Rust and TypeScript Phase 1 reducer summaries match the shared golden fixtures.\\n')\"",
"docs:validate": "node scripts/validate-doc-links.mjs && node scripts/validate-okf.mjs",
"trace:phase0": "pnpm run trace:phase0:rust",
"trace:phase0:rust": "pnpm run build:rust && cargo run --quiet --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin phase0-tracer",
"trace:phase0:typescript": "pnpm run build:typescript && node dist/tracer/phase0.js",
"verify": "pnpm run build && pnpm run test && pnpm run check:forbidden-imports && pnpm run docs:validate && pnpm run check:phase0-parity && pnpm run trace:phase0"
"replay:phase1": "pnpm run build:typescript && node dist/cli/phase1-replay.js",
"browser:dev": "vite --config vite.config.ts",
"browser:preview": "vite preview --config vite.config.ts",
"verify": "pnpm run build && pnpm run typecheck && pnpm run check:phase1-goldens && pnpm run test && pnpm run check:browser-tokens && pnpm run test:browser && pnpm run check:forbidden-imports && pnpm run docs:validate && pnpm run check:phase0-parity && pnpm run check:phase1-parity && pnpm run trace:phase0 && pnpm run replay:phase1"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"@types/node": "^22.20.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"ajv": "^8.20.0",
"json-schema-to-ts": "^3.1.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"typescript": "^5.7.3",
"vite": "^6.1.0",
"vitest": "^4.1.10"
}
}
@@ -0,0 +1,30 @@
{
"schema": "paperclip.prp.fixture.v1", "fixtureVersion": 1, "protocolVersion": 1,
"name": "Duplicate event", "description": "At-least-once delivery repeats one source event without repeating its effect.",
"identity": {
"schema": "paperclip.prp.identity.v1", "companyId": "company_phase1", "issueId": "issue_phase1_duplicate",
"runId": "run_phase1_duplicate", "environmentLeaseId": "lease_phase1_duplicate", "runnerInstanceId": "runner_phase1",
"normalizedSessionId": "session_phase1_duplicate", "driverSessionId": "driver_phase1_duplicate"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "fake", "version": "1.0.0" },
"steer": false, "interrupt": true, "resume": true, "runtimeRequests": false, "structuredResult": true, "typedEvents": true
},
"commands": [
{ "schema": "paperclip.prp.command.v1", "commandId": "command_duplicate_prepare", "controllerSeq": 1, "type": "run.prepare", "issuedAt": "2026-08-07T12:30:00.000Z", "payload": {} }
],
"events": [
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_01", "sourceSeq": 1, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_duplicate", "normalizedSessionId": "session_phase1_duplicate", "eventType": "session.started", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:30:00.010Z", "payload": {} },
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_02", "sourceSeq": 2, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_duplicate", "normalizedSessionId": "session_phase1_duplicate", "turnId": "turn_phase1_duplicate", "eventType": "turn.started", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-07T12:30:00.020Z", "payload": {} },
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_03", "sourceSeq": 3, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_duplicate", "normalizedSessionId": "session_phase1_duplicate", "turnId": "turn_phase1_duplicate", "itemId": "item_phase1_duplicate", "eventType": "item.completed", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-07T12:30:00.030Z", "payload": { "kind": "assistant_message", "text": "Exactly one projection." } },
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_03", "sourceSeq": 3, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_duplicate", "normalizedSessionId": "session_phase1_duplicate", "turnId": "turn_phase1_duplicate", "itemId": "item_phase1_duplicate", "eventType": "item.completed", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-07T12:30:00.030Z", "payload": { "kind": "assistant_message", "text": "Exactly one projection." } },
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_04", "sourceSeq": 4, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_duplicate", "normalizedSessionId": "session_phase1_duplicate", "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:30:00.040Z",
"payload": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Duplicate delivery was idempotent.", "completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] }
},
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_05", "sourceSeq": 5, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_duplicate", "normalizedSessionId": "session_phase1_duplicate", "turnId": "turn_phase1_duplicate", "eventType": "turn.completed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:30:00.050Z", "payload": {} },
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_06", "sourceSeq": 6, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_duplicate", "normalizedSessionId": "session_phase1_duplicate", "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:30:00.060Z", "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "done" } }
],
"requests": [],
"result": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Duplicate delivery was idempotent.", "completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] }
}
@@ -0,0 +1,106 @@
{
"schema": "paperclip.prp.fixture.v1",
"fixtureVersion": 1,
"protocolVersion": 1,
"name": "Failed run",
"description": "The driver reports an item and turn failure before the run terminalizes.",
"identity": {
"schema": "paperclip.prp.identity.v1",
"companyId": "company_phase1",
"issueId": "issue_phase1_failed",
"runId": "run_phase1_failed",
"environmentLeaseId": "lease_phase1_failed",
"runnerInstanceId": "runner_phase1",
"normalizedSessionId": "session_phase1_failed",
"driverSessionId": "driver_phase1_failed"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1",
"sessionReusePolicy": "new_per_run",
"driver": { "kind": "fake", "version": "1.0.0" },
"steer": true,
"interrupt": true,
"resume": true,
"runtimeRequests": true,
"structuredResult": true,
"typedEvents": true
},
"commands": [
{
"schema": "paperclip.prp.command.v1",
"commandId": "command_failed_prepare",
"controllerSeq": 1,
"type": "run.prepare",
"issuedAt": "2026-08-07T12:10:00.000Z",
"payload": {}
},
{
"schema": "paperclip.prp.command.v1",
"commandId": "command_failed_turn",
"controllerSeq": 2,
"type": "turn.start",
"issuedAt": "2026-08-07T12:10:00.010Z",
"payload": { "text": "Run failing scenario" }
}
],
"events": [
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_01", "sourceSeq": 1,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_failed",
"normalizedSessionId": "session_phase1_failed", "eventType": "session.started", "schemaVersion": 1,
"priority": 0, "emittedAt": "2026-08-07T12:10:00.020Z", "payload": {}
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_02", "sourceSeq": 2,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_failed",
"normalizedSessionId": "session_phase1_failed", "turnId": "turn_phase1_failed",
"eventType": "turn.started", "schemaVersion": 1, "priority": 1,
"emittedAt": "2026-08-07T12:10:00.030Z", "payload": {}
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_03", "sourceSeq": 3,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_failed",
"normalizedSessionId": "session_phase1_failed", "turnId": "turn_phase1_failed", "itemId": "item_failed_command",
"eventType": "item.failed", "schemaVersion": 1, "priority": 0,
"emittedAt": "2026-08-07T12:10:00.040Z", "payload": { "kind": "command", "message": "Command exited with code 1" }
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_04", "sourceSeq": 4,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_failed",
"normalizedSessionId": "session_phase1_failed", "turnId": "turn_phase1_failed",
"eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0,
"emittedAt": "2026-08-07T12:10:00.050Z",
"payload": {
"schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded",
"summary": "The scripted command failed and needs a retry.",
"completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Retry the failed command", "blocksCompletion": true }] },
"evidence": [], "verification": [{ "commandOrCheck": "fake command", "status": "failed" }],
"attentionRequests": [], "artifacts": [],
"continuation": { "kind": "retry", "summary": "Retry after correcting the command", "idempotencyKey": "retry_phase1_failed" }
}
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_05", "sourceSeq": 5,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_failed",
"normalizedSessionId": "session_phase1_failed", "turnId": "turn_phase1_failed",
"eventType": "turn.failed", "schemaVersion": 1, "priority": 0,
"emittedAt": "2026-08-07T12:10:00.060Z", "payload": { "message": "Fake driver failure" }
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_06", "sourceSeq": 6,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_failed",
"normalizedSessionId": "session_phase1_failed", "eventType": "run.terminal", "schemaVersion": 1,
"priority": 0, "emittedAt": "2026-08-07T12:10:00.070Z",
"payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "failed", "runTerminalState": "failed", "reportedWorkDisposition": "yielded" }
}
],
"requests": [],
"result": {
"schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded",
"summary": "The scripted command failed and needs a retry.",
"completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Retry the failed command", "blocksCompletion": true }] },
"evidence": [], "verification": [{ "commandOrCheck": "fake command", "status": "failed" }],
"attentionRequests": [], "artifacts": [],
"continuation": { "kind": "retry", "summary": "Retry after correcting the command", "idempotencyKey": "retry_phase1_failed" }
}
}
@@ -0,0 +1,130 @@
{
"schema": "paperclip.prp.session-snapshot.v1",
"fixtureName": "Duplicate event",
"identity": {
"schema": "paperclip.prp.identity.v1",
"companyId": "company_phase1",
"issueId": "issue_phase1_duplicate",
"runId": "run_phase1_duplicate",
"environmentLeaseId": "lease_phase1_duplicate",
"runnerInstanceId": "runner_phase1",
"normalizedSessionId": "session_phase1_duplicate",
"driverSessionId": "driver_phase1_duplicate"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1",
"sessionReusePolicy": "new_per_run",
"driver": {
"kind": "fake",
"version": "1.0.0"
},
"steer": false,
"interrupt": true,
"resume": true,
"runtimeRequests": false,
"structuredResult": true,
"typedEvents": true
},
"runPhase": "terminal",
"sessionState": "running",
"turnState": "completed",
"activeTurnId": null,
"items": [
{
"itemId": "item_phase1_duplicate",
"kind": "assistant_message",
"status": "completed",
"text": "Exactly one projection."
}
],
"requests": [],
"proposedResult": {
"schema": "paperclip.run_result.v1",
"reportedWorkDisposition": "done",
"summary": "Duplicate delivery was idempotent.",
"completionClaim": {
"contractRevision": "phase1-contract-v1",
"objectiveSatisfied": true,
"criteria": [],
"remainingWork": []
},
"evidence": [],
"verification": [],
"attentionRequests": [],
"artifacts": []
},
"terminal": {
"schema": "paperclip.prp.terminal.v1",
"turnTerminalState": "completed",
"runTerminalState": "succeeded",
"reportedWorkDisposition": "done"
},
"timeline": [
{
"position": 1,
"sourceEventId": "event_duplicate_01",
"sourceSeq": 1,
"eventType": "session.started",
"emittedAt": "2026-08-07T12:30:00.010Z",
"summary": "session started"
},
{
"position": 2,
"sourceEventId": "event_duplicate_02",
"sourceSeq": 2,
"eventType": "turn.started",
"emittedAt": "2026-08-07T12:30:00.020Z",
"summary": "turn started"
},
{
"position": 3,
"sourceEventId": "event_duplicate_03",
"sourceSeq": 3,
"eventType": "item.completed",
"emittedAt": "2026-08-07T12:30:00.030Z",
"itemId": "item_phase1_duplicate",
"summary": "Completed assistant_message"
},
{
"position": 4,
"sourceEventId": "event_duplicate_04",
"sourceSeq": 4,
"eventType": "run.result.proposed",
"emittedAt": "2026-08-07T12:30:00.040Z",
"summary": "Duplicate delivery was idempotent."
},
{
"position": 5,
"sourceEventId": "event_duplicate_05",
"sourceSeq": 5,
"eventType": "turn.completed",
"emittedAt": "2026-08-07T12:30:00.050Z",
"summary": "turn completed"
},
{
"position": 6,
"sourceEventId": "event_duplicate_06",
"sourceSeq": 6,
"eventType": "run.terminal",
"emittedAt": "2026-08-07T12:30:00.060Z",
"summary": "Run succeeded"
}
],
"sourceCursors": {
"runner:runner_phase1": 6
},
"processedEventIds": [
"event_duplicate_01",
"event_duplicate_02",
"event_duplicate_03",
"event_duplicate_04",
"event_duplicate_05",
"event_duplicate_06"
],
"duplicateEventIds": [
"event_duplicate_03"
],
"outOfOrderEventIds": [],
"gaps": [],
"integrity": "complete"
}
@@ -0,0 +1,11 @@
{
"runId": "run_phase1_duplicate",
"integrity": "complete",
"timelineCount": 6,
"duplicateEventIds": [
"event_duplicate_03"
],
"gaps": [],
"turnTerminalState": "completed",
"runTerminalState": "succeeded"
}
@@ -0,0 +1,143 @@
{
"schema": "paperclip.prp.session-snapshot.v1",
"fixtureName": "Failed run",
"identity": {
"schema": "paperclip.prp.identity.v1",
"companyId": "company_phase1",
"issueId": "issue_phase1_failed",
"runId": "run_phase1_failed",
"environmentLeaseId": "lease_phase1_failed",
"runnerInstanceId": "runner_phase1",
"normalizedSessionId": "session_phase1_failed",
"driverSessionId": "driver_phase1_failed"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1",
"sessionReusePolicy": "new_per_run",
"driver": {
"kind": "fake",
"version": "1.0.0"
},
"steer": true,
"interrupt": true,
"resume": true,
"runtimeRequests": true,
"structuredResult": true,
"typedEvents": true
},
"runPhase": "terminal",
"sessionState": "running",
"turnState": "failed",
"activeTurnId": null,
"items": [
{
"itemId": "item_failed_command",
"kind": "command",
"status": "failed",
"text": ""
}
],
"requests": [],
"proposedResult": {
"schema": "paperclip.run_result.v1",
"reportedWorkDisposition": "yielded",
"summary": "The scripted command failed and needs a retry.",
"completionClaim": {
"contractRevision": "phase1-contract-v1",
"objectiveSatisfied": false,
"criteria": [],
"remainingWork": [
{
"description": "Retry the failed command",
"blocksCompletion": true
}
]
},
"evidence": [],
"verification": [
{
"commandOrCheck": "fake command",
"status": "failed"
}
],
"attentionRequests": [],
"artifacts": [],
"continuation": {
"kind": "retry",
"summary": "Retry after correcting the command",
"idempotencyKey": "retry_phase1_failed"
}
},
"terminal": {
"schema": "paperclip.prp.terminal.v1",
"turnTerminalState": "failed",
"runTerminalState": "failed",
"reportedWorkDisposition": "yielded"
},
"timeline": [
{
"position": 1,
"sourceEventId": "event_failed_01",
"sourceSeq": 1,
"eventType": "session.started",
"emittedAt": "2026-08-07T12:10:00.020Z",
"summary": "session started"
},
{
"position": 2,
"sourceEventId": "event_failed_02",
"sourceSeq": 2,
"eventType": "turn.started",
"emittedAt": "2026-08-07T12:10:00.030Z",
"summary": "turn started"
},
{
"position": 3,
"sourceEventId": "event_failed_03",
"sourceSeq": 3,
"eventType": "item.failed",
"emittedAt": "2026-08-07T12:10:00.040Z",
"itemId": "item_failed_command",
"summary": "Command exited with code 1"
},
{
"position": 4,
"sourceEventId": "event_failed_04",
"sourceSeq": 4,
"eventType": "run.result.proposed",
"emittedAt": "2026-08-07T12:10:00.050Z",
"summary": "The scripted command failed and needs a retry."
},
{
"position": 5,
"sourceEventId": "event_failed_05",
"sourceSeq": 5,
"eventType": "turn.failed",
"emittedAt": "2026-08-07T12:10:00.060Z",
"summary": "turn failed"
},
{
"position": 6,
"sourceEventId": "event_failed_06",
"sourceSeq": 6,
"eventType": "run.terminal",
"emittedAt": "2026-08-07T12:10:00.070Z",
"summary": "Run failed"
}
],
"sourceCursors": {
"runner:runner_phase1": 6
},
"processedEventIds": [
"event_failed_01",
"event_failed_02",
"event_failed_03",
"event_failed_04",
"event_failed_05",
"event_failed_06"
],
"duplicateEventIds": [],
"outOfOrderEventIds": [],
"gaps": [],
"integrity": "complete"
}
@@ -0,0 +1,9 @@
{
"runId": "run_phase1_failed",
"integrity": "complete",
"timelineCount": 6,
"duplicateEventIds": [],
"gaps": [],
"turnTerminalState": "failed",
"runTerminalState": "failed"
}
@@ -0,0 +1,166 @@
{
"schema": "paperclip.prp.session-snapshot.v1",
"fixtureName": "Happy path",
"identity": {
"schema": "paperclip.prp.identity.v1",
"companyId": "company_phase1",
"issueId": "issue_phase1_happy",
"runId": "run_phase1_happy",
"environmentLeaseId": "lease_phase1_happy",
"runnerInstanceId": "runner_phase1",
"normalizedSessionId": "session_phase1_happy",
"driverSessionId": "driver_phase1_happy",
"providerSessionId": "provider_phase1_happy"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1",
"sessionReusePolicy": "new_per_run",
"driver": {
"kind": "fake",
"version": "1.0.0"
},
"steer": true,
"interrupt": true,
"resume": true,
"runtimeRequests": true,
"structuredResult": true,
"typedEvents": true,
"unsupported": []
},
"runPhase": "terminal",
"sessionState": "running",
"turnState": "completed",
"activeTurnId": null,
"items": [
{
"itemId": "item_phase1_message",
"kind": "assistant_message",
"status": "completed",
"text": "Validated fixture. Replay complete."
}
],
"requests": [],
"proposedResult": {
"schema": "paperclip.run_result.v1",
"reportedWorkDisposition": "done",
"summary": "The scripted run completed successfully.",
"completionClaim": {
"contractRevision": "phase1-contract-v1",
"objectiveSatisfied": true,
"criteria": [],
"remainingWork": []
},
"evidence": [],
"verification": [
{
"commandOrCheck": "fixture replay",
"status": "passed"
}
],
"attentionRequests": [],
"artifacts": []
},
"terminal": {
"schema": "paperclip.prp.terminal.v1",
"turnTerminalState": "completed",
"runTerminalState": "succeeded",
"reportedWorkDisposition": "done",
"workAssessmentId": "assessment_phase1_happy",
"statusDecisionId": "decision_phase1_happy"
},
"timeline": [
{
"position": 1,
"sourceEventId": "event_happy_01",
"sourceSeq": 1,
"eventType": "runtime.phase.changed",
"emittedAt": "2026-08-07T12:00:00.030Z",
"summary": "Phase changed to workspace_preparing"
},
{
"position": 2,
"sourceEventId": "event_happy_02",
"sourceSeq": 2,
"eventType": "session.started",
"emittedAt": "2026-08-07T12:00:00.040Z",
"summary": "session started"
},
{
"position": 3,
"sourceEventId": "event_happy_03",
"sourceSeq": 3,
"eventType": "turn.started",
"emittedAt": "2026-08-07T12:00:00.050Z",
"summary": "turn started"
},
{
"position": 4,
"sourceEventId": "event_happy_04",
"sourceSeq": 4,
"eventType": "item.started",
"emittedAt": "2026-08-07T12:00:00.060Z",
"itemId": "item_phase1_message",
"summary": "Started assistant_message"
},
{
"position": 5,
"sourceEventId": "event_happy_05",
"sourceSeq": 5,
"eventType": "item.delta",
"emittedAt": "2026-08-07T12:00:00.070Z",
"itemId": "item_phase1_message",
"summary": "Validated fixture. "
},
{
"position": 6,
"sourceEventId": "event_happy_06",
"sourceSeq": 6,
"eventType": "item.completed",
"emittedAt": "2026-08-07T12:00:00.080Z",
"itemId": "item_phase1_message",
"summary": "Completed assistant_message"
},
{
"position": 7,
"sourceEventId": "event_happy_07",
"sourceSeq": 7,
"eventType": "run.result.proposed",
"emittedAt": "2026-08-07T12:00:00.090Z",
"summary": "The scripted run completed successfully."
},
{
"position": 8,
"sourceEventId": "event_happy_08",
"sourceSeq": 8,
"eventType": "turn.completed",
"emittedAt": "2026-08-07T12:00:00.100Z",
"summary": "turn completed"
},
{
"position": 9,
"sourceEventId": "event_happy_09",
"sourceSeq": 9,
"eventType": "run.terminal",
"emittedAt": "2026-08-07T12:00:00.110Z",
"summary": "Run succeeded"
}
],
"sourceCursors": {
"runner:runner_phase1": 9
},
"processedEventIds": [
"event_happy_01",
"event_happy_02",
"event_happy_03",
"event_happy_04",
"event_happy_05",
"event_happy_06",
"event_happy_07",
"event_happy_08",
"event_happy_09"
],
"duplicateEventIds": [],
"outOfOrderEventIds": [],
"gaps": [],
"integrity": "complete"
}
@@ -0,0 +1,9 @@
{
"runId": "run_phase1_happy",
"integrity": "complete",
"timelineCount": 9,
"duplicateEventIds": [],
"gaps": [],
"turnTerminalState": "completed",
"runTerminalState": "succeeded"
}
@@ -0,0 +1,137 @@
{
"schema": "paperclip.prp.session-snapshot.v1",
"fixtureName": "Interrupted run",
"identity": {
"schema": "paperclip.prp.identity.v1",
"companyId": "company_phase1",
"issueId": "issue_phase1_interrupted",
"runId": "run_phase1_interrupted",
"environmentLeaseId": "lease_phase1_interrupted",
"runnerInstanceId": "runner_phase1",
"normalizedSessionId": "session_phase1_interrupted",
"driverSessionId": "driver_phase1_interrupted"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1",
"sessionReusePolicy": "reuse_per_issue",
"driver": {
"kind": "fake",
"version": "1.0.0"
},
"steer": true,
"interrupt": true,
"resume": true,
"runtimeRequests": true,
"structuredResult": true,
"typedEvents": true
},
"runPhase": "terminal",
"sessionState": "running",
"turnState": "interrupted",
"activeTurnId": null,
"items": [],
"requests": [
{
"requestId": "request_interrupted_permission",
"requestKind": "runtime",
"status": "pending",
"prompt": "Allow the fake command?"
}
],
"proposedResult": {
"schema": "paperclip.run_result.v1",
"reportedWorkDisposition": "yielded",
"summary": "The turn was interrupted.",
"completionClaim": {
"contractRevision": "phase1-contract-v1",
"objectiveSatisfied": false,
"criteria": [],
"remainingWork": [
{
"description": "Resume the interrupted turn",
"blocksCompletion": true
}
]
},
"evidence": [],
"verification": [],
"attentionRequests": [],
"artifacts": [],
"continuation": {
"kind": "same_agent",
"summary": "Resume in the same normalized session",
"idempotencyKey": "resume_phase1_interrupted"
}
},
"terminal": {
"schema": "paperclip.prp.terminal.v1",
"turnTerminalState": "interrupted",
"runTerminalState": "cancelled",
"reportedWorkDisposition": "yielded"
},
"timeline": [
{
"position": 1,
"sourceEventId": "event_interrupted_01",
"sourceSeq": 1,
"eventType": "session.started",
"emittedAt": "2026-08-07T12:20:00.020Z",
"summary": "session started"
},
{
"position": 2,
"sourceEventId": "event_interrupted_02",
"sourceSeq": 2,
"eventType": "turn.started",
"emittedAt": "2026-08-07T12:20:00.030Z",
"summary": "turn started"
},
{
"position": 3,
"sourceEventId": "event_interrupted_03",
"sourceSeq": 3,
"eventType": "runtime_request.created",
"emittedAt": "2026-08-07T12:20:00.040Z",
"summary": "runtime_request created"
},
{
"position": 4,
"sourceEventId": "event_interrupted_04",
"sourceSeq": 4,
"eventType": "turn.interrupted",
"emittedAt": "2026-08-07T12:20:00.060Z",
"summary": "turn interrupted"
},
{
"position": 5,
"sourceEventId": "event_interrupted_05",
"sourceSeq": 5,
"eventType": "run.result.proposed",
"emittedAt": "2026-08-07T12:20:00.070Z",
"summary": "The turn was interrupted."
},
{
"position": 6,
"sourceEventId": "event_interrupted_06",
"sourceSeq": 6,
"eventType": "run.terminal",
"emittedAt": "2026-08-07T12:20:00.080Z",
"summary": "Run cancelled"
}
],
"sourceCursors": {
"runner:runner_phase1": 6
},
"processedEventIds": [
"event_interrupted_01",
"event_interrupted_02",
"event_interrupted_03",
"event_interrupted_04",
"event_interrupted_05",
"event_interrupted_06"
],
"duplicateEventIds": [],
"outOfOrderEventIds": [],
"gaps": [],
"integrity": "complete"
}
@@ -0,0 +1,9 @@
{
"runId": "run_phase1_interrupted",
"integrity": "complete",
"timelineCount": 6,
"duplicateEventIds": [],
"gaps": [],
"turnTerminalState": "interrupted",
"runTerminalState": "cancelled"
}
@@ -0,0 +1,125 @@
{
"schema": "paperclip.prp.session-snapshot.v1",
"fixtureName": "Source sequence gap",
"identity": {
"schema": "paperclip.prp.identity.v1",
"companyId": "company_phase1",
"issueId": "issue_phase1_gap",
"runId": "run_phase1_gap",
"environmentLeaseId": "lease_phase1_gap",
"runnerInstanceId": "runner_phase1",
"normalizedSessionId": "session_phase1_gap",
"driverSessionId": "driver_phase1_gap"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1",
"sessionReusePolicy": "new_per_run",
"driver": {
"kind": "fake",
"version": "1.0.0"
},
"steer": false,
"interrupt": false,
"resume": true,
"runtimeRequests": false,
"structuredResult": true,
"typedEvents": true
},
"runPhase": "terminal",
"sessionState": "running",
"turnState": "completed",
"activeTurnId": null,
"items": [],
"requests": [],
"proposedResult": {
"schema": "paperclip.run_result.v1",
"reportedWorkDisposition": "needs_review",
"summary": "The source gap requires reconciliation.",
"completionClaim": {
"contractRevision": "phase1-contract-v1",
"objectiveSatisfied": false,
"criteria": [],
"remainingWork": [
{
"description": "Replay missing source event",
"blocksCompletion": true
}
]
},
"evidence": [],
"verification": [],
"attentionRequests": [],
"artifacts": []
},
"terminal": {
"schema": "paperclip.prp.terminal.v1",
"turnTerminalState": "completed",
"runTerminalState": "succeeded",
"reportedWorkDisposition": "needs_review"
},
"timeline": [
{
"position": 1,
"sourceEventId": "event_gap_01",
"sourceSeq": 1,
"eventType": "session.started",
"emittedAt": "2026-08-07T12:40:00.010Z",
"summary": "session started"
},
{
"position": 2,
"sourceEventId": "event_gap_02",
"sourceSeq": 2,
"eventType": "turn.started",
"emittedAt": "2026-08-07T12:40:00.020Z",
"summary": "turn started"
},
{
"position": 3,
"sourceEventId": "event_gap_04",
"sourceSeq": 4,
"eventType": "run.result.proposed",
"emittedAt": "2026-08-07T12:40:00.040Z",
"summary": "The source gap requires reconciliation."
},
{
"position": 4,
"sourceEventId": "event_gap_05",
"sourceSeq": 5,
"eventType": "turn.completed",
"emittedAt": "2026-08-07T12:40:00.050Z",
"summary": "turn completed"
},
{
"position": 5,
"sourceEventId": "event_gap_06",
"sourceSeq": 6,
"eventType": "run.terminal",
"emittedAt": "2026-08-07T12:40:00.060Z",
"summary": "Run succeeded"
}
],
"sourceCursors": {
"runner:runner_phase1": 6
},
"processedEventIds": [
"event_gap_01",
"event_gap_02",
"event_gap_04",
"event_gap_05",
"event_gap_06"
],
"duplicateEventIds": [],
"outOfOrderEventIds": [],
"gaps": [
{
"sourceKey": "runner:runner_phase1",
"expected": 3,
"received": 4,
"missing": [
3
]
}
],
"integrity": "gap_detected"
}
@@ -0,0 +1,18 @@
{
"runId": "run_phase1_gap",
"integrity": "gap_detected",
"timelineCount": 5,
"duplicateEventIds": [],
"gaps": [
{
"sourceKey": "runner:runner_phase1",
"expected": 3,
"received": 4,
"missing": [
3
]
}
],
"turnTerminalState": "completed",
"runTerminalState": "succeeded"
}
@@ -0,0 +1,111 @@
{
"schema": "paperclip.prp.session-snapshot.v1",
"fixtureName": "Unknown optional fields",
"identity": {
"schema": "paperclip.prp.identity.v1",
"companyId": "company_phase1",
"issueId": "issue_phase1_future",
"runId": "run_phase1_future",
"environmentLeaseId": "lease_phase1_future",
"runnerInstanceId": "runner_phase1",
"normalizedSessionId": "session_phase1_future",
"driverSessionId": "driver_phase1_future",
"futureIdentityLabel": "preserved"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1",
"sessionReusePolicy": "new_per_run",
"driver": {
"kind": "fake",
"version": "1.1.0",
"futureDriverField": true
},
"steer": true,
"interrupt": true,
"resume": true,
"runtimeRequests": true,
"structuredResult": true,
"typedEvents": true,
"futureCapability": {
"mode": "optional"
}
},
"runPhase": "terminal",
"sessionState": "running",
"turnState": "completed",
"activeTurnId": null,
"items": [],
"requests": [],
"proposedResult": {
"schema": "paperclip.run_result.v1",
"reportedWorkDisposition": "done",
"summary": "Unknown optional fields did not change the v1 projection.",
"completionClaim": {
"contractRevision": "phase1-contract-v1",
"objectiveSatisfied": true,
"criteria": [],
"remainingWork": []
},
"evidence": [],
"verification": [],
"attentionRequests": [],
"artifacts": [],
"futureResultAnnotation": {
"safe": true
}
},
"terminal": {
"schema": "paperclip.prp.terminal.v1",
"turnTerminalState": "completed",
"runTerminalState": "succeeded",
"reportedWorkDisposition": "done",
"futureTerminalReason": "optional"
},
"timeline": [
{
"position": 1,
"sourceEventId": "event_future_01",
"sourceSeq": 1,
"eventType": "session.started",
"emittedAt": "2026-08-07T12:50:00.010Z",
"summary": "session started"
},
{
"position": 2,
"sourceEventId": "event_future_02",
"sourceSeq": 2,
"eventType": "run.result.proposed",
"emittedAt": "2026-08-07T12:50:00.020Z",
"summary": "Unknown optional fields did not change the v1 projection."
},
{
"position": 3,
"sourceEventId": "event_future_03",
"sourceSeq": 3,
"eventType": "turn.completed",
"emittedAt": "2026-08-07T12:50:00.030Z",
"summary": "turn completed"
},
{
"position": 4,
"sourceEventId": "event_future_04",
"sourceSeq": 4,
"eventType": "run.terminal",
"emittedAt": "2026-08-07T12:50:00.040Z",
"summary": "Run succeeded"
}
],
"sourceCursors": {
"runner:runner_phase1": 4
},
"processedEventIds": [
"event_future_01",
"event_future_02",
"event_future_03",
"event_future_04"
],
"duplicateEventIds": [],
"outOfOrderEventIds": [],
"gaps": [],
"integrity": "complete"
}
@@ -0,0 +1,9 @@
{
"runId": "run_phase1_future",
"integrity": "complete",
"timelineCount": 4,
"duplicateEventIds": [],
"gaps": [],
"turnTerminalState": "completed",
"runTerminalState": "succeeded"
}
@@ -0,0 +1,230 @@
{
"schema": "paperclip.prp.fixture.v1",
"fixtureVersion": 1,
"protocolVersion": 1,
"name": "Happy path",
"description": "A typed assistant message completes and the run proposes a successful result.",
"identity": {
"schema": "paperclip.prp.identity.v1",
"companyId": "company_phase1",
"issueId": "issue_phase1_happy",
"runId": "run_phase1_happy",
"environmentLeaseId": "lease_phase1_happy",
"runnerInstanceId": "runner_phase1",
"normalizedSessionId": "session_phase1_happy",
"driverSessionId": "driver_phase1_happy",
"providerSessionId": "provider_phase1_happy"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1",
"sessionReusePolicy": "new_per_run",
"driver": { "kind": "fake", "version": "1.0.0" },
"steer": true,
"interrupt": true,
"resume": true,
"runtimeRequests": true,
"structuredResult": true,
"typedEvents": true,
"unsupported": []
},
"commands": [
{
"schema": "paperclip.prp.command.v1",
"commandId": "command_happy_prepare",
"controllerSeq": 1,
"type": "run.prepare",
"issuedAt": "2026-08-07T12:00:00.000Z",
"payload": { "workspace": "fixture" }
},
{
"schema": "paperclip.prp.command.v1",
"commandId": "command_happy_session",
"controllerSeq": 2,
"type": "session.open",
"issuedAt": "2026-08-07T12:00:00.010Z",
"payload": { "reuse": "new_per_run" }
},
{
"schema": "paperclip.prp.command.v1",
"commandId": "command_happy_turn",
"controllerSeq": 3,
"type": "turn.start",
"issuedAt": "2026-08-07T12:00:00.020Z",
"payload": { "text": "Inspect the fixture" }
}
],
"events": [
{
"schema": "paperclip.prp.event.v1",
"sourceEventId": "event_happy_01",
"sourceSeq": 1,
"sourceInstanceId": "runner_phase1",
"sourceKind": "runner",
"runId": "run_phase1_happy",
"normalizedSessionId": "session_phase1_happy",
"eventType": "runtime.phase.changed",
"schemaVersion": 1,
"priority": 1,
"emittedAt": "2026-08-07T12:00:00.030Z",
"payload": { "phase": "workspace_preparing" }
},
{
"schema": "paperclip.prp.event.v1",
"sourceEventId": "event_happy_02",
"sourceSeq": 2,
"sourceInstanceId": "runner_phase1",
"sourceKind": "runner",
"runId": "run_phase1_happy",
"normalizedSessionId": "session_phase1_happy",
"eventType": "session.started",
"schemaVersion": 1,
"priority": 0,
"emittedAt": "2026-08-07T12:00:00.040Z",
"payload": { "driverSessionId": "driver_phase1_happy" }
},
{
"schema": "paperclip.prp.event.v1",
"sourceEventId": "event_happy_03",
"sourceSeq": 3,
"sourceInstanceId": "runner_phase1",
"sourceKind": "runner",
"runId": "run_phase1_happy",
"normalizedSessionId": "session_phase1_happy",
"turnId": "turn_phase1_happy",
"eventType": "turn.started",
"schemaVersion": 1,
"priority": 1,
"emittedAt": "2026-08-07T12:00:00.050Z",
"payload": {}
},
{
"schema": "paperclip.prp.event.v1",
"sourceEventId": "event_happy_04",
"sourceSeq": 4,
"sourceInstanceId": "runner_phase1",
"sourceKind": "runner",
"runId": "run_phase1_happy",
"normalizedSessionId": "session_phase1_happy",
"turnId": "turn_phase1_happy",
"itemId": "item_phase1_message",
"eventType": "item.started",
"schemaVersion": 1,
"priority": 1,
"emittedAt": "2026-08-07T12:00:00.060Z",
"payload": { "kind": "assistant_message", "text": "" }
},
{
"schema": "paperclip.prp.event.v1",
"sourceEventId": "event_happy_05",
"sourceSeq": 5,
"sourceInstanceId": "runner_phase1",
"sourceKind": "runner",
"runId": "run_phase1_happy",
"normalizedSessionId": "session_phase1_happy",
"turnId": "turn_phase1_happy",
"itemId": "item_phase1_message",
"eventType": "item.delta",
"schemaVersion": 1,
"priority": 2,
"emittedAt": "2026-08-07T12:00:00.070Z",
"payload": { "text": "Validated fixture. " }
},
{
"schema": "paperclip.prp.event.v1",
"sourceEventId": "event_happy_06",
"sourceSeq": 6,
"sourceInstanceId": "runner_phase1",
"sourceKind": "runner",
"runId": "run_phase1_happy",
"normalizedSessionId": "session_phase1_happy",
"turnId": "turn_phase1_happy",
"itemId": "item_phase1_message",
"eventType": "item.completed",
"schemaVersion": 1,
"priority": 1,
"emittedAt": "2026-08-07T12:00:00.080Z",
"payload": { "kind": "assistant_message", "text": "Validated fixture. Replay complete." }
},
{
"schema": "paperclip.prp.event.v1",
"sourceEventId": "event_happy_07",
"sourceSeq": 7,
"sourceInstanceId": "runner_phase1",
"sourceKind": "runner",
"runId": "run_phase1_happy",
"normalizedSessionId": "session_phase1_happy",
"turnId": "turn_phase1_happy",
"eventType": "run.result.proposed",
"schemaVersion": 1,
"priority": 0,
"emittedAt": "2026-08-07T12:00:00.090Z",
"payload": {
"schema": "paperclip.run_result.v1",
"reportedWorkDisposition": "done",
"summary": "The scripted run completed successfully.",
"completionClaim": {
"contractRevision": "phase1-contract-v1",
"objectiveSatisfied": true,
"criteria": [],
"remainingWork": []
},
"evidence": [],
"verification": [{ "commandOrCheck": "fixture replay", "status": "passed" }],
"attentionRequests": [],
"artifacts": []
}
},
{
"schema": "paperclip.prp.event.v1",
"sourceEventId": "event_happy_08",
"sourceSeq": 8,
"sourceInstanceId": "runner_phase1",
"sourceKind": "runner",
"runId": "run_phase1_happy",
"normalizedSessionId": "session_phase1_happy",
"turnId": "turn_phase1_happy",
"eventType": "turn.completed",
"schemaVersion": 1,
"priority": 0,
"emittedAt": "2026-08-07T12:00:00.100Z",
"payload": {}
},
{
"schema": "paperclip.prp.event.v1",
"sourceEventId": "event_happy_09",
"sourceSeq": 9,
"sourceInstanceId": "runner_phase1",
"sourceKind": "runner",
"runId": "run_phase1_happy",
"normalizedSessionId": "session_phase1_happy",
"eventType": "run.terminal",
"schemaVersion": 1,
"priority": 0,
"emittedAt": "2026-08-07T12:00:00.110Z",
"payload": {
"schema": "paperclip.prp.terminal.v1",
"turnTerminalState": "completed",
"runTerminalState": "succeeded",
"reportedWorkDisposition": "done",
"workAssessmentId": "assessment_phase1_happy",
"statusDecisionId": "decision_phase1_happy"
}
}
],
"requests": [],
"result": {
"schema": "paperclip.run_result.v1",
"reportedWorkDisposition": "done",
"summary": "The scripted run completed successfully.",
"completionClaim": {
"contractRevision": "phase1-contract-v1",
"objectiveSatisfied": true,
"criteria": [],
"remainingWork": []
},
"evidence": [],
"verification": [{ "commandOrCheck": "fixture replay", "status": "passed" }],
"attentionRequests": [],
"artifacts": []
}
}
@@ -0,0 +1,90 @@
{
"schema": "paperclip.prp.fixture.v1",
"fixtureVersion": 1,
"protocolVersion": 1,
"name": "Interrupted run",
"description": "A pending runtime permission request is followed by an explicit turn interruption.",
"identity": {
"schema": "paperclip.prp.identity.v1", "companyId": "company_phase1", "issueId": "issue_phase1_interrupted",
"runId": "run_phase1_interrupted", "environmentLeaseId": "lease_phase1_interrupted",
"runnerInstanceId": "runner_phase1", "normalizedSessionId": "session_phase1_interrupted",
"driverSessionId": "driver_phase1_interrupted"
},
"capabilities": {
"schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "reuse_per_issue",
"driver": { "kind": "fake", "version": "1.0.0" }, "steer": true, "interrupt": true,
"resume": true, "runtimeRequests": true, "structuredResult": true, "typedEvents": true
},
"commands": [
{
"schema": "paperclip.prp.command.v1", "commandId": "command_interrupted_prepare", "controllerSeq": 1,
"type": "run.prepare", "issuedAt": "2026-08-07T12:20:00.000Z", "payload": {}
},
{
"schema": "paperclip.prp.command.v1", "commandId": "command_interrupted_turn", "controllerSeq": 2,
"type": "turn.start", "issuedAt": "2026-08-07T12:20:00.010Z", "payload": { "text": "Wait for interruption" }
},
{
"schema": "paperclip.prp.command.v1", "commandId": "command_interrupted_interrupt", "controllerSeq": 3,
"type": "turn.interrupt", "issuedAt": "2026-08-07T12:20:00.050Z", "payload": { "reason": "operator" }
}
],
"events": [
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_01", "sourceSeq": 1,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_interrupted",
"normalizedSessionId": "session_phase1_interrupted", "eventType": "session.started", "schemaVersion": 1,
"priority": 0, "emittedAt": "2026-08-07T12:20:00.020Z", "payload": {}
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_02", "sourceSeq": 2,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_interrupted",
"normalizedSessionId": "session_phase1_interrupted", "turnId": "turn_phase1_interrupted",
"eventType": "turn.started", "schemaVersion": 1, "priority": 1,
"emittedAt": "2026-08-07T12:20:00.030Z", "payload": {}
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_03", "sourceSeq": 3,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_interrupted",
"normalizedSessionId": "session_phase1_interrupted", "turnId": "turn_phase1_interrupted",
"eventType": "runtime_request.created", "schemaVersion": 1, "priority": 0,
"emittedAt": "2026-08-07T12:20:00.040Z",
"payload": { "request": { "schema": "paperclip.runtime_request.v1", "requestKind": "runtime", "requestId": "request_interrupted_permission", "type": "permission", "status": "pending", "prompt": "Allow the fake command?" } }
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_04", "sourceSeq": 4,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_interrupted",
"normalizedSessionId": "session_phase1_interrupted", "turnId": "turn_phase1_interrupted",
"eventType": "turn.interrupted", "schemaVersion": 1, "priority": 0,
"emittedAt": "2026-08-07T12:20:00.060Z", "payload": { "reason": "operator" }
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_05", "sourceSeq": 5,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_interrupted",
"normalizedSessionId": "session_phase1_interrupted", "eventType": "run.result.proposed", "schemaVersion": 1,
"priority": 0, "emittedAt": "2026-08-07T12:20:00.070Z",
"payload": {
"schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded", "summary": "The turn was interrupted.",
"completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Resume the interrupted turn", "blocksCompletion": true }] },
"evidence": [], "verification": [], "attentionRequests": [], "artifacts": [],
"continuation": { "kind": "same_agent", "summary": "Resume in the same normalized session", "idempotencyKey": "resume_phase1_interrupted" }
}
},
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_06", "sourceSeq": 6,
"sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_interrupted",
"normalizedSessionId": "session_phase1_interrupted", "eventType": "run.terminal", "schemaVersion": 1,
"priority": 0, "emittedAt": "2026-08-07T12:20:00.080Z",
"payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "interrupted", "runTerminalState": "cancelled", "reportedWorkDisposition": "yielded" }
}
],
"requests": [
{ "schema": "paperclip.runtime_request.v1", "requestKind": "runtime", "requestId": "request_interrupted_permission", "type": "permission", "status": "pending", "prompt": "Allow the fake command?" }
],
"result": {
"schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded", "summary": "The turn was interrupted.",
"completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Resume the interrupted turn", "blocksCompletion": true }] },
"evidence": [], "verification": [], "attentionRequests": [], "artifacts": [],
"continuation": { "kind": "same_agent", "summary": "Resume in the same normalized session", "idempotencyKey": "resume_phase1_interrupted" }
}
}
@@ -0,0 +1,19 @@
{
"schema": "paperclip.prp.fixture.v1", "fixtureVersion": 1, "protocolVersion": 1,
"name": "Source sequence gap", "description": "The runner stream skips source sequence 3 and replay records the missing cursor.",
"identity": { "schema": "paperclip.prp.identity.v1", "companyId": "company_phase1", "issueId": "issue_phase1_gap", "runId": "run_phase1_gap", "environmentLeaseId": "lease_phase1_gap", "runnerInstanceId": "runner_phase1", "normalizedSessionId": "session_phase1_gap", "driverSessionId": "driver_phase1_gap" },
"capabilities": { "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "fake", "version": "1.0.0" }, "steer": false, "interrupt": false, "resume": true, "runtimeRequests": false, "structuredResult": true, "typedEvents": true },
"commands": [{ "schema": "paperclip.prp.command.v1", "commandId": "command_gap_prepare", "controllerSeq": 1, "type": "run.prepare", "issuedAt": "2026-08-07T12:40:00.000Z", "payload": {} }],
"events": [
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_01", "sourceSeq": 1, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_gap", "normalizedSessionId": "session_phase1_gap", "eventType": "session.started", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:40:00.010Z", "payload": {} },
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_02", "sourceSeq": 2, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_gap", "normalizedSessionId": "session_phase1_gap", "turnId": "turn_phase1_gap", "eventType": "turn.started", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-07T12:40:00.020Z", "payload": {} },
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_04", "sourceSeq": 4, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_gap", "normalizedSessionId": "session_phase1_gap", "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:40:00.040Z",
"payload": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "needs_review", "summary": "The source gap requires reconciliation.", "completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Replay missing source event", "blocksCompletion": true }] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] }
},
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_05", "sourceSeq": 5, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_gap", "normalizedSessionId": "session_phase1_gap", "turnId": "turn_phase1_gap", "eventType": "turn.completed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:40:00.050Z", "payload": {} },
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_06", "sourceSeq": 6, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_gap", "normalizedSessionId": "session_phase1_gap", "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:40:00.060Z", "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "needs_review" } }
],
"requests": [],
"result": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "needs_review", "summary": "The source gap requires reconciliation.", "completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Replay missing source event", "blocksCompletion": true }] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] }
}
@@ -0,0 +1,19 @@
{
"schema": "paperclip.prp.fixture.v1", "fixtureVersion": 1, "protocolVersion": 1,
"name": "Unknown optional fields", "description": "A future producer adds optional fields that a PRP v1 consumer preserves and ignores safely.",
"futureFixtureHint": { "producerVersion": "1.1-preview" },
"identity": { "schema": "paperclip.prp.identity.v1", "companyId": "company_phase1", "issueId": "issue_phase1_future", "runId": "run_phase1_future", "environmentLeaseId": "lease_phase1_future", "runnerInstanceId": "runner_phase1", "normalizedSessionId": "session_phase1_future", "driverSessionId": "driver_phase1_future", "futureIdentityLabel": "preserved" },
"capabilities": { "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "fake", "version": "1.1.0", "futureDriverField": true }, "steer": true, "interrupt": true, "resume": true, "runtimeRequests": true, "structuredResult": true, "typedEvents": true, "futureCapability": { "mode": "optional" } },
"commands": [{ "schema": "paperclip.prp.command.v1", "commandId": "command_future_prepare", "controllerSeq": 1, "type": "run.prepare", "issuedAt": "2026-08-07T12:50:00.000Z", "payload": {}, "futureDeadlineMode": "soft" }],
"events": [
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_future_01", "sourceSeq": 1, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_future", "normalizedSessionId": "session_phase1_future", "eventType": "session.started", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:50:00.010Z", "payload": { "futureSessionDetail": "ignored" }, "futureEnvelopeField": 42 },
{
"schema": "paperclip.prp.event.v1", "sourceEventId": "event_future_02", "sourceSeq": 2, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_future", "normalizedSessionId": "session_phase1_future", "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:50:00.020Z",
"payload": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Unknown optional fields did not change the v1 projection.", "completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [], "futureResultAnnotation": { "safe": true } }
},
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_future_03", "sourceSeq": 3, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_future", "normalizedSessionId": "session_phase1_future", "turnId": "turn_phase1_future", "eventType": "turn.completed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:50:00.030Z", "payload": {} },
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_future_04", "sourceSeq": 4, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_future", "normalizedSessionId": "session_phase1_future", "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:50:00.040Z", "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "done", "futureTerminalReason": "optional" } }
],
"requests": [],
"result": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Unknown optional fields did not change the v1 projection.", "completionClaim": { "contractRevision": "phase1-contract-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [], "futureResultAnnotation": { "safe": true } }
}
@@ -0,0 +1,13 @@
{
"schema": "paperclip.prp.fixture.v1", "fixtureVersion": 1, "protocolVersion": 2,
"name": "Unsupported required version", "description": "A fixture that requires PRP v2 must fail closed in a PRP v1 consumer.",
"identity": { "schema": "paperclip.prp.identity.v1", "companyId": "company_phase1", "issueId": "issue_phase1_v2", "runId": "run_phase1_v2", "environmentLeaseId": "lease_phase1_v2", "runnerInstanceId": "runner_phase1", "normalizedSessionId": "session_phase1_v2", "driverSessionId": "driver_phase1_v2" },
"capabilities": { "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "future", "version": "2.0.0" }, "steer": true, "interrupt": true, "resume": true, "runtimeRequests": true, "structuredResult": true, "typedEvents": true },
"commands": [{ "schema": "paperclip.prp.command.v1", "commandId": "command_v2_prepare", "controllerSeq": 1, "type": "run.prepare", "issuedAt": "2026-08-07T13:00:00.000Z", "payload": {} }],
"events": [
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_v2_01", "sourceSeq": 1, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_v2", "normalizedSessionId": "session_phase1_v2", "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T13:00:00.010Z", "payload": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "This result requires PRP v2.", "completionClaim": { "contractRevision": "phase1-contract-v2", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] } },
{ "schema": "paperclip.prp.event.v1", "sourceEventId": "event_v2_02", "sourceSeq": 2, "sourceInstanceId": "runner_phase1", "sourceKind": "runner", "runId": "run_phase1_v2", "normalizedSessionId": "session_phase1_v2", "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T13:00:00.020Z", "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "done" } }
],
"requests": [],
"result": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "This result requires PRP v2.", "completionClaim": { "contractRevision": "phase1-contract-v2", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] }
}
@@ -0,0 +1,44 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/capabilities.schema.json",
"title": "PRP negotiated capabilities",
"type": "object",
"required": [
"schema",
"sessionReusePolicy",
"driver",
"steer",
"interrupt",
"resume",
"runtimeRequests",
"structuredResult",
"typedEvents"
],
"properties": {
"schema": { "const": "paperclip.prp.capabilities.v1" },
"sessionReusePolicy": {
"enum": ["new_per_run", "reuse_per_issue", "reuse_per_workspace"]
},
"driver": {
"type": "object",
"required": ["kind", "version"],
"properties": {
"kind": { "type": "string", "minLength": 1, "maxLength": 80 },
"version": { "type": "string", "minLength": 1, "maxLength": 80 }
},
"additionalProperties": true
},
"steer": { "type": "boolean" },
"interrupt": { "type": "boolean" },
"resume": { "type": "boolean" },
"runtimeRequests": { "type": "boolean" },
"structuredResult": { "type": "boolean" },
"typedEvents": { "type": "boolean" },
"unsupported": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true
}
},
"additionalProperties": true
}
@@ -0,0 +1,43 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/command.schema.json",
"title": "PRP runner command",
"type": "object",
"required": ["schema", "commandId", "controllerSeq", "type", "issuedAt", "payload"],
"properties": {
"schema": { "const": "paperclip.prp.command.v1" },
"commandId": { "type": "string", "minLength": 1, "maxLength": 160 },
"controllerSeq": { "type": "integer", "minimum": 1 },
"type": {
"enum": [
"run.prepare",
"session.open",
"turn.start",
"turn.steer",
"turn.interrupt",
"turn.stop",
"request.resolve",
"interaction.receipt",
"session.snapshot",
"session.close",
"run.cancel",
"runner.drain",
"runner.shutdown"
]
},
"issuedAt": { "type": "string", "format": "date-time" },
"deadlineAt": { "type": "string", "format": "date-time" },
"precondition": {
"type": "object",
"properties": {
"runnerState": { "type": "array", "items": { "type": "string" } },
"runState": { "type": "array", "items": { "type": "string" } },
"sessionState": { "type": "array", "items": { "type": "string" } },
"activeTurnId": { "type": ["string", "null"] }
},
"additionalProperties": true
},
"payload": { "type": "object", "additionalProperties": true }
},
"additionalProperties": true
}
@@ -0,0 +1,72 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/event.schema.json",
"title": "PRP native event",
"type": "object",
"required": [
"schema",
"sourceEventId",
"sourceSeq",
"sourceInstanceId",
"sourceKind",
"runId",
"eventType",
"schemaVersion",
"priority",
"emittedAt",
"payload"
],
"properties": {
"schema": { "const": "paperclip.prp.event.v1" },
"sourceEventId": { "type": "string", "minLength": 1, "maxLength": 160 },
"sourceSeq": { "type": "integer", "minimum": 1 },
"sourceInstanceId": { "type": "string", "minLength": 1, "maxLength": 160 },
"sourceKind": { "enum": ["runner", "control_plane"] },
"runId": { "type": "string", "minLength": 1, "maxLength": 160 },
"normalizedSessionId": { "type": "string", "minLength": 1, "maxLength": 160 },
"turnId": { "type": "string", "minLength": 1, "maxLength": 160 },
"itemId": { "type": "string", "minLength": 1, "maxLength": 160 },
"eventType": {
"enum": [
"runner.connected", "runner.reconnected", "runner.disconnected", "runner.draining", "runner.diagnostic",
"runtime.phase.changed", "sandbox.metric", "workspace.ready",
"harness.starting", "harness.ready", "harness.exited", "harness.diagnostic",
"session.starting", "session.started", "session.resuming", "session.resumed", "session.reconciled", "session.closed", "session.failed",
"turn.submitted", "turn.accepted", "turn.started", "turn.completed", "turn.failed", "turn.interrupted", "turn.cancelled",
"item.started", "item.delta", "item.completed", "item.failed",
"mcp_app.discovered", "mcp_app.resource.resolved", "mcp_app.initializing", "mcp_app.ready", "mcp_app.tool_input", "mcp_app.tool_result", "mcp_app.action.requested", "mcp_app.action.resolved", "mcp_app.host_context.changed", "mcp_app.failed", "mcp_app.teardown",
"runtime_request.created", "runtime_request.resolved", "runtime_request.expired", "runtime_request.cancelled",
"interaction.request.proposed", "interaction.request.materialized", "interaction.request.rejected", "interaction.response.progressed", "interaction.response.resolved", "interaction.response.delivered",
"run.result.proposed", "run.result.accepted", "run.result.rejected",
"attention.request.proposed", "attention.request.routed", "attention.request.resolved", "attention.request.expired", "attention.request.superseded",
"work.assessment.recorded", "issue.status.decision.recorded", "issue.status.decision.applied", "issue.status.decision.rejected", "issue.status.decision.superseded",
"run.terminal"
]
},
"schemaVersion": { "const": 1 },
"priority": { "enum": [0, 1, 2] },
"emittedAt": { "type": "string", "format": "date-time" },
"observedAt": { "type": "string", "format": "date-time" },
"payload": { "type": "object", "additionalProperties": true },
"debug": { "type": "object", "additionalProperties": true }
},
"allOf": [
{
"if": { "properties": { "eventType": { "const": "run.terminal" } } },
"then": {
"properties": {
"payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/terminal.schema.json" }
}
}
},
{
"if": { "properties": { "eventType": { "const": "run.result.proposed" } } },
"then": {
"properties": {
"payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/result.schema.json" }
}
}
}
],
"additionalProperties": true
}
@@ -0,0 +1,42 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/fixture.schema.json",
"title": "PRP scripted replay fixture",
"type": "object",
"required": [
"schema",
"fixtureVersion",
"protocolVersion",
"name",
"description",
"identity",
"capabilities",
"commands",
"events",
"result"
],
"properties": {
"schema": { "const": "paperclip.prp.fixture.v1" },
"fixtureVersion": { "const": 1 },
"protocolVersion": { "const": 1 },
"name": { "type": "string", "minLength": 1, "maxLength": 120 },
"description": { "type": "string", "minLength": 1, "maxLength": 1000 },
"identity": { "$ref": "https://paperclip.dev/schemas/prp/v1/identity.schema.json" },
"capabilities": { "$ref": "https://paperclip.dev/schemas/prp/v1/capabilities.schema.json" },
"commands": {
"type": "array",
"items": { "$ref": "https://paperclip.dev/schemas/prp/v1/command.schema.json" }
},
"events": {
"type": "array",
"minItems": 1,
"items": { "$ref": "https://paperclip.dev/schemas/prp/v1/event.schema.json" }
},
"requests": {
"type": "array",
"items": { "$ref": "https://paperclip.dev/schemas/prp/v1/request.schema.json" }
},
"result": { "$ref": "https://paperclip.dev/schemas/prp/v1/result.schema.json" }
},
"additionalProperties": true
}
@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/identity.schema.json",
"title": "PRP identity graph",
"type": "object",
"required": [
"schema",
"companyId",
"issueId",
"runId",
"environmentLeaseId",
"runnerInstanceId",
"normalizedSessionId",
"driverSessionId"
],
"properties": {
"schema": { "const": "paperclip.prp.identity.v1" },
"companyId": { "$ref": "#/$defs/stableId" },
"issueId": { "$ref": "#/$defs/stableId" },
"runId": { "$ref": "#/$defs/stableId" },
"environmentLeaseId": { "$ref": "#/$defs/stableId" },
"runnerInstanceId": { "$ref": "#/$defs/stableId" },
"normalizedSessionId": { "$ref": "#/$defs/stableId" },
"driverSessionId": { "$ref": "#/$defs/stableId" },
"providerSessionId": { "$ref": "#/$defs/stableId" }
},
"additionalProperties": true,
"$defs": {
"stableId": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
}
}
}
@@ -0,0 +1,53 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/request.schema.json",
"title": "PRP request",
"oneOf": [
{
"type": "object",
"required": ["schema", "requestKind", "requestId", "type", "status", "prompt"],
"properties": {
"schema": { "const": "paperclip.runtime_request.v1" },
"requestKind": { "const": "runtime" },
"requestId": { "type": "string", "minLength": 1, "maxLength": 160 },
"type": { "enum": ["permission", "input"] },
"status": { "enum": ["pending", "resolved", "expired", "cancelled"] },
"prompt": { "type": "string", "minLength": 1, "maxLength": 4000 },
"choices": {
"type": "array",
"items": {
"type": "object",
"required": ["key", "label"],
"properties": {
"key": { "type": "string", "minLength": 1 },
"label": { "type": "string", "minLength": 1 }
},
"additionalProperties": true
}
}
},
"additionalProperties": true
},
{
"type": "object",
"required": ["schema", "requestKind", "requestId", "kind", "blocking", "payload"],
"properties": {
"schema": { "const": "paperclip.interaction_request.v1" },
"requestKind": { "const": "issue_thread" },
"requestId": { "type": "string", "minLength": 1, "maxLength": 160 },
"kind": {
"enum": [
"request_confirmation",
"request_checkbox_confirmation",
"request_item_verdicts",
"ask_user_questions",
"suggest_tasks"
]
},
"blocking": { "type": "boolean" },
"payload": { "type": "object", "additionalProperties": true }
},
"additionalProperties": true
}
]
}
@@ -0,0 +1,121 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/result.schema.json",
"title": "PRP structured run result",
"type": "object",
"required": [
"schema",
"reportedWorkDisposition",
"summary",
"completionClaim",
"evidence",
"verification",
"attentionRequests",
"artifacts"
],
"properties": {
"schema": { "const": "paperclip.run_result.v1" },
"reportedWorkDisposition": {
"enum": ["done", "blocked", "needs_review", "yielded"]
},
"summary": { "type": "string", "minLength": 1, "maxLength": 12000 },
"completionClaim": {
"type": "object",
"required": ["contractRevision", "objectiveSatisfied", "criteria", "remainingWork"],
"properties": {
"contractRevision": { "type": "string", "minLength": 1 },
"objectiveSatisfied": { "type": "boolean" },
"criteria": {
"type": "array",
"items": {
"type": "object",
"required": ["criterionId", "status", "evidenceRefs"],
"properties": {
"criterionId": { "type": "string", "minLength": 1 },
"status": { "enum": ["satisfied", "not_satisfied", "unknown"] },
"evidenceRefs": { "type": "array", "items": { "type": "string" } },
"explanation": { "type": "string" }
},
"additionalProperties": true
}
},
"remainingWork": {
"type": "array",
"items": {
"type": "object",
"required": ["description", "blocksCompletion"],
"properties": {
"description": { "type": "string", "minLength": 1 },
"blocksCompletion": { "type": "boolean" }
},
"additionalProperties": true
}
}
},
"additionalProperties": true
},
"evidence": { "type": "array", "items": { "type": "object" } },
"verification": {
"type": "array",
"items": {
"type": "object",
"required": ["commandOrCheck", "status"],
"properties": {
"commandOrCheck": { "type": "string", "minLength": 1 },
"status": { "enum": ["passed", "failed", "not_run"] },
"detail": { "type": "string" },
"artifactRef": { "type": "string" }
},
"additionalProperties": true
}
},
"blocker": {
"type": "object",
"required": ["reasonCode", "owner", "unblockAction", "scope"],
"properties": {
"reasonCode": { "type": "string", "minLength": 1 },
"owner": { "type": "object", "additionalProperties": true },
"unblockAction": { "type": "string", "minLength": 1 },
"scope": { "enum": ["current_track", "task_wide"] }
},
"additionalProperties": true
},
"attentionRequests": { "type": "array", "items": { "type": "object" } },
"artifacts": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "ref"],
"properties": {
"kind": { "type": "string", "minLength": 1 },
"ref": { "type": "string", "minLength": 1 },
"title": { "type": "string" }
},
"additionalProperties": true
}
},
"continuation": {
"type": "object",
"required": ["kind", "summary", "idempotencyKey"],
"properties": {
"kind": {
"enum": ["same_agent", "retry", "delegated_issue", "response_wake", "monitor"]
},
"summary": { "type": "string", "minLength": 1 },
"idempotencyKey": { "type": "string", "minLength": 1 }
},
"additionalProperties": true
}
},
"allOf": [
{
"if": { "properties": { "reportedWorkDisposition": { "const": "blocked" } } },
"then": { "required": ["blocker"] }
},
{
"if": { "properties": { "reportedWorkDisposition": { "const": "yielded" } } },
"then": { "required": ["continuation"] }
}
],
"additionalProperties": true
}
@@ -0,0 +1,25 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/terminal.schema.json",
"title": "PRP run terminal state",
"type": "object",
"required": [
"schema",
"turnTerminalState",
"runTerminalState",
"reportedWorkDisposition"
],
"properties": {
"schema": { "const": "paperclip.prp.terminal.v1" },
"turnTerminalState": {
"enum": ["completed", "failed", "interrupted", "cancelled"]
},
"runTerminalState": { "enum": ["succeeded", "failed", "cancelled"] },
"reportedWorkDisposition": {
"enum": ["done", "blocked", "needs_review", "yielded"]
},
"workAssessmentId": { "type": "string", "minLength": 1 },
"statusDecisionId": { "type": "string", "minLength": 1 }
},
"additionalProperties": true
}
@@ -1,5 +1,7 @@
#![forbid(unsafe_code)]
pub mod phase1;
use std::error::Error;
use std::fmt::{self, Display, Formatter};
@@ -0,0 +1,308 @@
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const PRP_PROTOCOL_VERSION: u64 = 1;
const PRP_FIXTURE_VERSION: u64 = 1;
const PRP_FIXTURE_SCHEMA: &str = "paperclip.prp.fixture.v1";
const PRP_EVENT_SCHEMA: &str = "paperclip.prp.event.v1";
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Phase1Fixture {
schema: String,
fixture_version: u64,
protocol_version: u64,
identity: Phase1Identity,
events: Vec<Phase1Event>,
result: Value,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Phase1Identity {
run_id: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Phase1Event {
schema: String,
source_event_id: String,
source_seq: u64,
source_instance_id: String,
source_kind: String,
run_id: String,
schema_version: u64,
event_type: String,
payload: Value,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Phase1SequenceGap {
source_key: String,
expected: u64,
received: u64,
missing: Vec<u64>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Phase1ParitySummary {
run_id: String,
integrity: String,
timeline_count: usize,
duplicate_event_ids: Vec<String>,
gaps: Vec<Phase1SequenceGap>,
turn_terminal_state: Option<String>,
run_terminal_state: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Phase1Error(String);
impl Phase1Error {
fn invalid(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl Display for Phase1Error {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl Error for Phase1Error {}
pub fn reduce_phase1_fixture(input: &str) -> Result<Phase1ParitySummary, Phase1Error> {
let fixture: Phase1Fixture = serde_json::from_str(input)
.map_err(|error| Phase1Error::invalid(format!("fixture must be valid JSON: {error}")))?;
if fixture.protocol_version != PRP_PROTOCOL_VERSION {
return Err(Phase1Error::invalid(format!(
"unsupported required protocolVersion {}; expected {PRP_PROTOCOL_VERSION}",
fixture.protocol_version
)));
}
if fixture.fixture_version != PRP_FIXTURE_VERSION {
return Err(Phase1Error::invalid(format!(
"unsupported required fixtureVersion {}; expected {PRP_FIXTURE_VERSION}",
fixture.fixture_version
)));
}
if fixture.schema != PRP_FIXTURE_SCHEMA {
return Err(Phase1Error::invalid(format!(
"unsupported required fixture schema {}; expected {PRP_FIXTURE_SCHEMA}",
fixture.schema
)));
}
let mut delivery_counts = HashMap::<&str, usize>::new();
for event in &fixture.events {
*delivery_counts
.entry(event.source_event_id.as_str())
.or_default() += 1;
}
let mut duplicate_event_ids = delivery_counts
.into_iter()
.filter_map(|(event_id, count)| (count > 1).then(|| event_id.to_owned()))
.collect::<Vec<_>>();
duplicate_event_ids.sort();
let mut seen = HashMap::<&str, &Phase1Event>::new();
let mut cursors = HashMap::<String, u64>::new();
let mut gaps = Vec::<Phase1SequenceGap>::new();
let mut timeline_count = 0_usize;
let mut turn_terminal_state = None;
let mut run_terminal_state = None;
let mut proposed_result_count = 0_usize;
let mut terminal_count = 0_usize;
for event in &fixture.events {
if event.schema != PRP_EVENT_SCHEMA {
return Err(Phase1Error::invalid(format!(
"unsupported required event schema {}; expected {PRP_EVENT_SCHEMA}",
event.schema
)));
}
if event.schema_version != 1 {
return Err(Phase1Error::invalid(format!(
"unsupported required event schemaVersion {}; expected 1",
event.schema_version
)));
}
if event.run_id != fixture.identity.run_id {
return Err(Phase1Error::invalid(
"event runId must match identity.runId",
));
}
if let Some(previous) = seen.get(event.source_event_id.as_str()) {
if *previous != event {
return Err(Phase1Error::invalid(
"duplicate sourceEventId deliveries must be byte-equivalent",
));
}
continue;
}
seen.insert(event.source_event_id.as_str(), event);
let key = format!("{}:{}", event.source_kind, event.source_instance_id);
let cursor = *cursors.get(&key).unwrap_or(&0);
let expected = cursor + 1;
if event.source_seq <= cursor {
continue;
}
if event.source_seq > expected {
gaps.push(Phase1SequenceGap {
source_key: key.clone(),
expected,
received: event.source_seq,
missing: (expected..event.source_seq).collect(),
});
}
cursors.insert(key, event.source_seq);
timeline_count += 1;
match event.event_type.as_str() {
"run.result.proposed" => {
proposed_result_count += 1;
if event.payload != fixture.result {
return Err(Phase1Error::invalid(
"fixture result must match the run.result.proposed event payload",
));
}
}
"run.terminal" => {
terminal_count += 1;
turn_terminal_state = event
.payload
.get("turnTerminalState")
.and_then(Value::as_str)
.map(str::to_owned);
run_terminal_state = event
.payload
.get("runTerminalState")
.and_then(Value::as_str)
.map(str::to_owned);
}
_ => {}
}
}
if proposed_result_count != 1 {
return Err(Phase1Error::invalid(
"scripted fixtures must contain exactly one unique run.result.proposed event",
));
}
if terminal_count != 1 {
return Err(Phase1Error::invalid(
"scripted fixtures must contain exactly one unique run.terminal event",
));
}
Ok(Phase1ParitySummary {
run_id: fixture.identity.run_id,
integrity: if gaps.is_empty() {
"complete".to_owned()
} else {
"gap_detected".to_owned()
},
timeline_count,
duplicate_event_ids,
gaps,
turn_terminal_state,
run_terminal_state,
})
}
#[cfg(test)]
mod tests {
use super::{reduce_phase1_fixture, Phase1ParitySummary};
use serde_json::Value;
fn assert_fixture_parity(fixture: &str, expected: &str) {
let actual = reduce_phase1_fixture(fixture).expect("fixture should reduce");
let expected: Phase1ParitySummary =
serde_json::from_str(expected).expect("golden summary should parse");
assert_eq!(actual, expected);
}
macro_rules! parity_case {
($name:ident, $fixture:literal, $golden:literal) => {
#[test]
fn $name() {
assert_fixture_parity(
include_str!(concat!(
"../../../../protocol/fixtures/phase-01/",
$fixture,
".json"
)),
include_str!(concat!(
"../../../../protocol/fixtures/phase-01/golden/",
$golden,
".summary.json"
)),
);
}
};
}
parity_case!(phase1_fixture_parity_happy_path, "happy-path", "happy-path");
parity_case!(phase1_fixture_parity_failed_run, "failed-run", "failed-run");
parity_case!(
phase1_fixture_parity_interrupted_run,
"interrupted-run",
"interrupted-run"
);
parity_case!(
phase1_fixture_parity_duplicate_event,
"duplicate-event",
"duplicate-event"
);
parity_case!(phase1_fixture_parity_source_gap, "source-gap", "source-gap");
parity_case!(
phase1_fixture_parity_unknown_optional_fields,
"unknown-optional-fields",
"unknown-optional-fields"
);
#[test]
fn phase1_fixture_parity_rejects_unsupported_required_version() {
let error = reduce_phase1_fixture(include_str!(
"../../../../protocol/fixtures/phase-01/unsupported-required-version.json"
))
.expect_err("PRP v2 fixture must fail closed");
assert!(error
.to_string()
.contains("unsupported required protocolVersion 2"));
}
#[test]
fn phase1_fixture_parity_rejects_unsupported_fixture_version() {
let fixture = include_str!("../../../../protocol/fixtures/phase-01/happy-path.json")
.replace("\"fixtureVersion\": 1", "\"fixtureVersion\": 2");
let error =
reduce_phase1_fixture(&fixture).expect_err("fixture version 2 must fail closed");
assert!(error
.to_string()
.contains("unsupported required fixtureVersion 2"));
}
#[test]
fn phase1_fixture_parity_rejects_a_mismatched_declared_result() {
let mut fixture: Value = serde_json::from_str(include_str!(
"../../../../protocol/fixtures/phase-01/happy-path.json"
))
.expect("fixture should parse");
fixture["result"]["summary"] = Value::String("Contradictory result".to_owned());
let error = reduce_phase1_fixture(&fixture.to_string())
.expect_err("mismatched result must fail closed");
assert!(error
.to_string()
.contains("fixture result must match the run.result.proposed event payload"));
}
}
@@ -0,0 +1,67 @@
import { readFile, readdir } from "node:fs/promises";
import { dirname, extname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const sourceRoot = resolve(packageRoot, "devtools/browser/src");
async function sourceFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = await Promise.all(
entries.map((entry) => {
const path = resolve(directory, entry.name);
return entry.isDirectory() ? sourceFiles(path) : [path];
}),
);
return nested.flat();
}
const componentRules = [
["hex color", /#[0-9a-f]{3,8}\b/i],
["raw pixel value", /\b\d+(?:\.\d+)?px\b/],
["inline style", /\bstyle\s*=\s*\{/],
["raw font size", /\bfont(?:-size|Size)\s*[:=]/],
["arbitrary Tailwind value", /\b[a-z-]+-\[[^\]]+\]/],
];
const violations = [];
for (const path of await sourceFiles(sourceRoot)) {
if (![".ts", ".tsx"].includes(extname(path))) {
continue;
}
const source = await readFile(path, "utf8");
for (const [label, pattern] of componentRules) {
if (pattern.test(source)) {
violations.push(`${path.slice(packageRoot.length + 1)}: ${label}`);
}
}
}
const styles = await readFile(resolve(sourceRoot, "styles.css"), "utf8");
for (const token of [
"--background",
"--foreground",
"--card",
"--primary",
"--muted",
"--border",
"--ring",
"--font-sans",
"--font-mono",
"--space-1",
"--radius-sm",
"--motion-fast",
]) {
if (!styles.includes(`${token}:`)) {
violations.push(`devtools/browser/src/styles.css: missing ${token}`);
}
}
if (violations.length > 0) {
process.stderr.write(`Standalone browser token check failed:\n- ${violations.join("\n- ")}\n`);
process.exitCode = 1;
} else {
process.stdout.write(
"Standalone browser token check passed; component visual values stay in styles.css.\n",
);
}
@@ -0,0 +1,59 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
parsePrpFixtureText,
phase1ParitySummary,
reducePrpFixture,
} from "../dist/index.js";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const fixtureDirectory = resolve(packageRoot, "protocol/fixtures/phase-01");
const goldenDirectory = resolve(fixtureDirectory, "golden");
const fixtureNames = [
"happy-path",
"failed-run",
"interrupted-run",
"duplicate-event",
"source-gap",
"unknown-optional-fields",
];
const check = process.argv.includes("--check");
const stale = [];
await mkdir(goldenDirectory, { recursive: true });
for (const fixtureName of fixtureNames) {
const source = await readFile(resolve(fixtureDirectory, `${fixtureName}.json`), "utf8");
const validation = parsePrpFixtureText(source);
if (!validation.ok) {
throw new Error(
`${fixtureName}.json failed validation: ${validation.issues.map((issue) => issue.message).join("; ")}`,
);
}
const snapshot = reducePrpFixture(validation.fixture);
const outputs = [
["snapshot", snapshot],
["summary", phase1ParitySummary(snapshot)],
];
for (const [kind, value] of outputs) {
const path = resolve(goldenDirectory, `${fixtureName}.${kind}.json`);
const generated = `${JSON.stringify(value, null, 2)}\n`;
if (check) {
if ((await readFile(path, "utf8").catch(() => "")) !== generated) {
stale.push(`${fixtureName}.${kind}.json`);
}
} else {
await writeFile(path, generated);
}
}
}
if (stale.length > 0) {
process.stderr.write(`Phase 1 golden files are stale:\n- ${stale.join("\n- ")}\n`);
process.exitCode = 1;
} else if (check) {
process.stdout.write("Phase 1 golden snapshots and parity summaries are current.\n");
} else {
process.stdout.write("Generated Phase 1 golden snapshots and parity summaries.\n");
}
@@ -0,0 +1,43 @@
import { readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const schemaDirectory = resolve(packageRoot, "protocol/schemas");
const outputPath = resolve(packageRoot, "src/protocol/generated/schema-bundle.ts");
const schemaNames = [
"identity",
"capabilities",
"command",
"terminal",
"request",
"result",
"event",
"fixture",
];
const schemas = await Promise.all(
schemaNames.map(async (name) => ({
name,
value: JSON.parse(await readFile(resolve(schemaDirectory, `${name}.schema.json`), "utf8")),
})),
);
const declarations = schemas
.map(({ name, value }) => `export const ${name}Schema = ${JSON.stringify(value, null, 2)} as const;`)
.join("\n\n");
const bundle = schemaNames.map((name) => ` ${name}: ${name}Schema,`).join("\n");
const generated = `// Generated by scripts/generate-protocol-schema-module.mjs. Do not edit.\n\n${declarations}\n\nexport const prpSchemaBundle = {\n${bundle}\n} as const;\n`;
if (process.argv.includes("--check")) {
const current = await readFile(outputPath, "utf8").catch(() => "");
if (current !== generated) {
process.stderr.write("Generated PRP schema module is stale. Run pnpm generate:protocol-types.\n");
process.exitCode = 1;
} else {
process.stdout.write("Generated PRP schema module matches the JSON Schema sources.\n");
}
} else {
await writeFile(outputPath, generated);
process.stdout.write("Generated src/protocol/generated/schema-bundle.ts.\n");
}
@@ -0,0 +1,17 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { formatPhase1Replay, replayPhase1FixtureText } from "../tracer/phase1-replay.js";
const defaultFixtureUrl = new URL(
"../../protocol/fixtures/phase-01/happy-path.json",
import.meta.url,
);
const input = process.argv[2];
const fixtureUrl = input === undefined ? defaultFixtureUrl : pathToFileURL(resolve(input));
const result = replayPhase1FixtureText(await readFile(fixtureUrl, "utf8"));
process.stdout.write(`${formatPhase1Replay(result)}\n`);
if (!result.ok) {
process.exitCode = 1;
}
+4
View File
@@ -4,4 +4,8 @@ export * from "./contracts/native-session-backend.js";
export * from "./contracts/types.js";
export * from "./mock-core/mock-control-plane-adapter.js";
export * from "./protocol/phase0-fixture.js";
export * from "./protocol/phase1-contract.js";
export * from "./protocol/phase1-loader.js";
export * from "./reducer/session-reducer.js";
export * from "./tracer/phase0-runner.js";
export * from "./tracer/phase1-replay.js";
@@ -0,0 +1,924 @@
// Generated by scripts/generate-protocol-schema-module.mjs. Do not edit.
export const identitySchema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/identity.schema.json",
"title": "PRP identity graph",
"type": "object",
"required": [
"schema",
"companyId",
"issueId",
"runId",
"environmentLeaseId",
"runnerInstanceId",
"normalizedSessionId",
"driverSessionId"
],
"properties": {
"schema": {
"const": "paperclip.prp.identity.v1"
},
"companyId": {
"$ref": "#/$defs/stableId"
},
"issueId": {
"$ref": "#/$defs/stableId"
},
"runId": {
"$ref": "#/$defs/stableId"
},
"environmentLeaseId": {
"$ref": "#/$defs/stableId"
},
"runnerInstanceId": {
"$ref": "#/$defs/stableId"
},
"normalizedSessionId": {
"$ref": "#/$defs/stableId"
},
"driverSessionId": {
"$ref": "#/$defs/stableId"
},
"providerSessionId": {
"$ref": "#/$defs/stableId"
}
},
"additionalProperties": true,
"$defs": {
"stableId": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
}
}
} as const;
export const capabilitiesSchema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/capabilities.schema.json",
"title": "PRP negotiated capabilities",
"type": "object",
"required": [
"schema",
"sessionReusePolicy",
"driver",
"steer",
"interrupt",
"resume",
"runtimeRequests",
"structuredResult",
"typedEvents"
],
"properties": {
"schema": {
"const": "paperclip.prp.capabilities.v1"
},
"sessionReusePolicy": {
"enum": [
"new_per_run",
"reuse_per_issue",
"reuse_per_workspace"
]
},
"driver": {
"type": "object",
"required": [
"kind",
"version"
],
"properties": {
"kind": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"version": {
"type": "string",
"minLength": 1,
"maxLength": 80
}
},
"additionalProperties": true
},
"steer": {
"type": "boolean"
},
"interrupt": {
"type": "boolean"
},
"resume": {
"type": "boolean"
},
"runtimeRequests": {
"type": "boolean"
},
"structuredResult": {
"type": "boolean"
},
"typedEvents": {
"type": "boolean"
},
"unsupported": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
}
},
"additionalProperties": true
} as const;
export const commandSchema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/command.schema.json",
"title": "PRP runner command",
"type": "object",
"required": [
"schema",
"commandId",
"controllerSeq",
"type",
"issuedAt",
"payload"
],
"properties": {
"schema": {
"const": "paperclip.prp.command.v1"
},
"commandId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"controllerSeq": {
"type": "integer",
"minimum": 1
},
"type": {
"enum": [
"run.prepare",
"session.open",
"turn.start",
"turn.steer",
"turn.interrupt",
"turn.stop",
"request.resolve",
"interaction.receipt",
"session.snapshot",
"session.close",
"run.cancel",
"runner.drain",
"runner.shutdown"
]
},
"issuedAt": {
"type": "string",
"format": "date-time"
},
"deadlineAt": {
"type": "string",
"format": "date-time"
},
"precondition": {
"type": "object",
"properties": {
"runnerState": {
"type": "array",
"items": {
"type": "string"
}
},
"runState": {
"type": "array",
"items": {
"type": "string"
}
},
"sessionState": {
"type": "array",
"items": {
"type": "string"
}
},
"activeTurnId": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": true
},
"payload": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": true
} as const;
export const terminalSchema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/terminal.schema.json",
"title": "PRP run terminal state",
"type": "object",
"required": [
"schema",
"turnTerminalState",
"runTerminalState",
"reportedWorkDisposition"
],
"properties": {
"schema": {
"const": "paperclip.prp.terminal.v1"
},
"turnTerminalState": {
"enum": [
"completed",
"failed",
"interrupted",
"cancelled"
]
},
"runTerminalState": {
"enum": [
"succeeded",
"failed",
"cancelled"
]
},
"reportedWorkDisposition": {
"enum": [
"done",
"blocked",
"needs_review",
"yielded"
]
},
"workAssessmentId": {
"type": "string",
"minLength": 1
},
"statusDecisionId": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": true
} as const;
export const requestSchema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/request.schema.json",
"title": "PRP request",
"oneOf": [
{
"type": "object",
"required": [
"schema",
"requestKind",
"requestId",
"type",
"status",
"prompt"
],
"properties": {
"schema": {
"const": "paperclip.runtime_request.v1"
},
"requestKind": {
"const": "runtime"
},
"requestId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"type": {
"enum": [
"permission",
"input"
]
},
"status": {
"enum": [
"pending",
"resolved",
"expired",
"cancelled"
]
},
"prompt": {
"type": "string",
"minLength": 1,
"maxLength": 4000
},
"choices": {
"type": "array",
"items": {
"type": "object",
"required": [
"key",
"label"
],
"properties": {
"key": {
"type": "string",
"minLength": 1
},
"label": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": true
}
}
},
"additionalProperties": true
},
{
"type": "object",
"required": [
"schema",
"requestKind",
"requestId",
"kind",
"blocking",
"payload"
],
"properties": {
"schema": {
"const": "paperclip.interaction_request.v1"
},
"requestKind": {
"const": "issue_thread"
},
"requestId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"kind": {
"enum": [
"request_confirmation",
"request_checkbox_confirmation",
"request_item_verdicts",
"ask_user_questions",
"suggest_tasks"
]
},
"blocking": {
"type": "boolean"
},
"payload": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": true
}
]
} as const;
export const resultSchema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/result.schema.json",
"title": "PRP structured run result",
"type": "object",
"required": [
"schema",
"reportedWorkDisposition",
"summary",
"completionClaim",
"evidence",
"verification",
"attentionRequests",
"artifacts"
],
"properties": {
"schema": {
"const": "paperclip.run_result.v1"
},
"reportedWorkDisposition": {
"enum": [
"done",
"blocked",
"needs_review",
"yielded"
]
},
"summary": {
"type": "string",
"minLength": 1,
"maxLength": 12000
},
"completionClaim": {
"type": "object",
"required": [
"contractRevision",
"objectiveSatisfied",
"criteria",
"remainingWork"
],
"properties": {
"contractRevision": {
"type": "string",
"minLength": 1
},
"objectiveSatisfied": {
"type": "boolean"
},
"criteria": {
"type": "array",
"items": {
"type": "object",
"required": [
"criterionId",
"status",
"evidenceRefs"
],
"properties": {
"criterionId": {
"type": "string",
"minLength": 1
},
"status": {
"enum": [
"satisfied",
"not_satisfied",
"unknown"
]
},
"evidenceRefs": {
"type": "array",
"items": {
"type": "string"
}
},
"explanation": {
"type": "string"
}
},
"additionalProperties": true
}
},
"remainingWork": {
"type": "array",
"items": {
"type": "object",
"required": [
"description",
"blocksCompletion"
],
"properties": {
"description": {
"type": "string",
"minLength": 1
},
"blocksCompletion": {
"type": "boolean"
}
},
"additionalProperties": true
}
}
},
"additionalProperties": true
},
"evidence": {
"type": "array",
"items": {
"type": "object"
}
},
"verification": {
"type": "array",
"items": {
"type": "object",
"required": [
"commandOrCheck",
"status"
],
"properties": {
"commandOrCheck": {
"type": "string",
"minLength": 1
},
"status": {
"enum": [
"passed",
"failed",
"not_run"
]
},
"detail": {
"type": "string"
},
"artifactRef": {
"type": "string"
}
},
"additionalProperties": true
}
},
"blocker": {
"type": "object",
"required": [
"reasonCode",
"owner",
"unblockAction",
"scope"
],
"properties": {
"reasonCode": {
"type": "string",
"minLength": 1
},
"owner": {
"type": "object",
"additionalProperties": true
},
"unblockAction": {
"type": "string",
"minLength": 1
},
"scope": {
"enum": [
"current_track",
"task_wide"
]
}
},
"additionalProperties": true
},
"attentionRequests": {
"type": "array",
"items": {
"type": "object"
}
},
"artifacts": {
"type": "array",
"items": {
"type": "object",
"required": [
"kind",
"ref"
],
"properties": {
"kind": {
"type": "string",
"minLength": 1
},
"ref": {
"type": "string",
"minLength": 1
},
"title": {
"type": "string"
}
},
"additionalProperties": true
}
},
"continuation": {
"type": "object",
"required": [
"kind",
"summary",
"idempotencyKey"
],
"properties": {
"kind": {
"enum": [
"same_agent",
"retry",
"delegated_issue",
"response_wake",
"monitor"
]
},
"summary": {
"type": "string",
"minLength": 1
},
"idempotencyKey": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": true
}
},
"allOf": [
{
"if": {
"properties": {
"reportedWorkDisposition": {
"const": "blocked"
}
}
},
"then": {
"required": [
"blocker"
]
}
},
{
"if": {
"properties": {
"reportedWorkDisposition": {
"const": "yielded"
}
}
},
"then": {
"required": [
"continuation"
]
}
}
],
"additionalProperties": true
} as const;
export const eventSchema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/event.schema.json",
"title": "PRP native event",
"type": "object",
"required": [
"schema",
"sourceEventId",
"sourceSeq",
"sourceInstanceId",
"sourceKind",
"runId",
"eventType",
"schemaVersion",
"priority",
"emittedAt",
"payload"
],
"properties": {
"schema": {
"const": "paperclip.prp.event.v1"
},
"sourceEventId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"sourceSeq": {
"type": "integer",
"minimum": 1
},
"sourceInstanceId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"sourceKind": {
"enum": [
"runner",
"control_plane"
]
},
"runId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"normalizedSessionId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"turnId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"itemId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"eventType": {
"enum": [
"runner.connected",
"runner.reconnected",
"runner.disconnected",
"runner.draining",
"runner.diagnostic",
"runtime.phase.changed",
"sandbox.metric",
"workspace.ready",
"harness.starting",
"harness.ready",
"harness.exited",
"harness.diagnostic",
"session.starting",
"session.started",
"session.resuming",
"session.resumed",
"session.reconciled",
"session.closed",
"session.failed",
"turn.submitted",
"turn.accepted",
"turn.started",
"turn.completed",
"turn.failed",
"turn.interrupted",
"turn.cancelled",
"item.started",
"item.delta",
"item.completed",
"item.failed",
"mcp_app.discovered",
"mcp_app.resource.resolved",
"mcp_app.initializing",
"mcp_app.ready",
"mcp_app.tool_input",
"mcp_app.tool_result",
"mcp_app.action.requested",
"mcp_app.action.resolved",
"mcp_app.host_context.changed",
"mcp_app.failed",
"mcp_app.teardown",
"runtime_request.created",
"runtime_request.resolved",
"runtime_request.expired",
"runtime_request.cancelled",
"interaction.request.proposed",
"interaction.request.materialized",
"interaction.request.rejected",
"interaction.response.progressed",
"interaction.response.resolved",
"interaction.response.delivered",
"run.result.proposed",
"run.result.accepted",
"run.result.rejected",
"attention.request.proposed",
"attention.request.routed",
"attention.request.resolved",
"attention.request.expired",
"attention.request.superseded",
"work.assessment.recorded",
"issue.status.decision.recorded",
"issue.status.decision.applied",
"issue.status.decision.rejected",
"issue.status.decision.superseded",
"run.terminal"
]
},
"schemaVersion": {
"const": 1
},
"priority": {
"enum": [
0,
1,
2
]
},
"emittedAt": {
"type": "string",
"format": "date-time"
},
"observedAt": {
"type": "string",
"format": "date-time"
},
"payload": {
"type": "object",
"additionalProperties": true
},
"debug": {
"type": "object",
"additionalProperties": true
}
},
"allOf": [
{
"if": {
"properties": {
"eventType": {
"const": "run.terminal"
}
}
},
"then": {
"properties": {
"payload": {
"$ref": "https://paperclip.dev/schemas/prp/v1/terminal.schema.json"
}
}
}
},
{
"if": {
"properties": {
"eventType": {
"const": "run.result.proposed"
}
}
},
"then": {
"properties": {
"payload": {
"$ref": "https://paperclip.dev/schemas/prp/v1/result.schema.json"
}
}
}
}
],
"additionalProperties": true
} as const;
export const fixtureSchema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/fixture.schema.json",
"title": "PRP scripted replay fixture",
"type": "object",
"required": [
"schema",
"fixtureVersion",
"protocolVersion",
"name",
"description",
"identity",
"capabilities",
"commands",
"events",
"result"
],
"properties": {
"schema": {
"const": "paperclip.prp.fixture.v1"
},
"fixtureVersion": {
"const": 1
},
"protocolVersion": {
"const": 1
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 120
},
"description": {
"type": "string",
"minLength": 1,
"maxLength": 1000
},
"identity": {
"$ref": "https://paperclip.dev/schemas/prp/v1/identity.schema.json"
},
"capabilities": {
"$ref": "https://paperclip.dev/schemas/prp/v1/capabilities.schema.json"
},
"commands": {
"type": "array",
"items": {
"$ref": "https://paperclip.dev/schemas/prp/v1/command.schema.json"
}
},
"events": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "https://paperclip.dev/schemas/prp/v1/event.schema.json"
}
},
"requests": {
"type": "array",
"items": {
"$ref": "https://paperclip.dev/schemas/prp/v1/request.schema.json"
}
},
"result": {
"$ref": "https://paperclip.dev/schemas/prp/v1/result.schema.json"
}
},
"additionalProperties": true
} as const;
export const prpSchemaBundle = {
identity: identitySchema,
capabilities: capabilitiesSchema,
command: commandSchema,
terminal: terminalSchema,
request: requestSchema,
result: resultSchema,
event: eventSchema,
fixture: fixtureSchema,
} as const;
@@ -0,0 +1,142 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import {
negotiateProtocolVersion,
parsePrpFixtureText,
PRP_PROTOCOL_VERSION,
} from "./phase1-contract.js";
const fixtureDirectory = new URL("../../protocol/fixtures/phase-01/", import.meta.url);
const validFixtures = [
"happy-path.json",
"failed-run.json",
"interrupted-run.json",
"duplicate-event.json",
"source-gap.json",
"unknown-optional-fields.json",
];
async function readFixture(name = "happy-path.json"): Promise<Record<string, unknown>> {
return JSON.parse(
await readFile(new URL(name, fixtureDirectory), "utf8"),
) as Record<string, unknown>;
}
describe("PRP v1 JSON Schema contract", () => {
for (const fixtureName of validFixtures) {
it(`validates ${fixtureName}`, async () => {
const result = parsePrpFixtureText(
await readFile(new URL(fixtureName, fixtureDirectory), "utf8"),
);
expect(result.ok).toBe(true);
});
}
it("preserves unknown optional fields for forward compatibility", async () => {
const result = parsePrpFixtureText(
await readFile(new URL("unknown-optional-fields.json", fixtureDirectory), "utf8"),
);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.fixture.futureFixtureHint).toEqual({
producerVersion: "1.1-preview",
});
expect(result.fixture.events[0]?.futureEnvelopeField).toBe(42);
}
});
it("fails closed on an unsupported required protocol version", async () => {
const result = parsePrpFixtureText(
await readFile(new URL("unsupported-required-version.json", fixtureDirectory), "utf8"),
);
expect(result).toMatchObject({
ok: false,
issues: [
{
code: "unsupported_required_version",
path: "/protocolVersion",
},
],
});
});
it("fails closed on unsupported nested required schema versions", async () => {
const fixture = await readFixture();
const events = fixture.events as Array<Record<string, unknown>>;
events[0]!.schemaVersion = 2;
expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({
ok: false,
issues: [
{
code: "unsupported_required_version",
path: "/events/0/schemaVersion",
},
],
});
});
it("requires the declared result to match the replayed result event", async () => {
const fixture = await readFixture();
const result = fixture.result as Record<string, unknown>;
result.summary = "A contradictory expected result.";
expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({
ok: false,
issues: [
{
code: "binding_mismatch",
path: "/result",
},
],
});
});
it("rejects a duplicate event id carrying different content", async () => {
const fixture = await readFixture("duplicate-event.json");
const events = fixture.events as Array<Record<string, unknown>>;
const payload = events[3]!.payload as Record<string, unknown>;
payload.text = "A mutated duplicate.";
expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({
ok: false,
issues: [
{
code: "binding_mismatch",
path: "/events/3/sourceEventId",
},
],
});
});
it("requires exactly one unique terminal event", async () => {
const fixture = await readFixture();
const events = fixture.events as Array<Record<string, unknown>>;
fixture.events = events.filter((event) => event.eventType !== "run.terminal");
expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({
ok: false,
issues: [
{
code: "binding_mismatch",
path: "/events",
},
],
});
});
it("reports invalid JSON without throwing", () => {
expect(parsePrpFixtureText("{")).toMatchObject({
ok: false,
issues: [{ code: "invalid_json", path: "/" }],
});
});
it("selects only an overlapping supported protocol version", () => {
expect(
negotiateProtocolVersion(
{ min: 1, max: PRP_PROTOCOL_VERSION },
{ min: 1, max: 2 },
),
).toBe(1);
expect(negotiateProtocolVersion({ min: 2, max: 3 }, { min: 1, max: 1 })).toBeNull();
});
});
@@ -0,0 +1,271 @@
import {
Ajv2020,
type ErrorObject,
type ValidateFunction,
} from "ajv/dist/2020.js";
import type { FromSchema } from "json-schema-to-ts";
import {
capabilitiesSchema,
commandSchema,
eventSchema,
fixtureSchema,
identitySchema,
prpSchemaBundle,
requestSchema,
resultSchema,
terminalSchema,
} from "./generated/schema-bundle.js";
export const PRP_PROTOCOL_NAME = "paperclip.runner";
export const PRP_PROTOCOL_VERSION = 1;
export const PRP_FIXTURE_SCHEMA = "paperclip.prp.fixture.v1";
type EventReferences = [typeof terminalSchema, typeof resultSchema];
export type PrpIdentity = FromSchema<typeof identitySchema>;
export type PrpCapabilities = FromSchema<typeof capabilitiesSchema>;
export type PrpCommand = FromSchema<typeof commandSchema>;
export type PrpTerminalState = FromSchema<typeof terminalSchema>;
export type PrpRequest = FromSchema<typeof requestSchema>;
export type PrpStructuredRunResult = FromSchema<typeof resultSchema>;
export type PrpEvent = FromSchema<
typeof eventSchema,
{ references: EventReferences }
>;
/** Runtime-validated composition of the JSON-Schema-derived contract types. */
export interface PrpFixture {
schema: typeof PRP_FIXTURE_SCHEMA;
fixtureVersion: 1;
protocolVersion: typeof PRP_PROTOCOL_VERSION;
name: string;
description: string;
identity: PrpIdentity;
capabilities: PrpCapabilities;
commands: PrpCommand[];
events: PrpEvent[];
requests?: PrpRequest[];
result: PrpStructuredRunResult;
[key: string]: unknown;
}
export type ProtocolValidationIssueCode =
| "invalid_json"
| "schema_validation"
| "unsupported_required_version"
| "binding_mismatch";
export interface ProtocolValidationIssue {
code: ProtocolValidationIssueCode;
path: string;
message: string;
}
export type ProtocolValidationResult =
| { ok: true; fixture: PrpFixture; issues: [] }
| { ok: false; fixture: null; issues: ProtocolValidationIssue[] };
export interface ProtocolVersionRange {
min: number;
max: number;
}
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
ajv.addFormat("date-time", {
type: "string",
validate(value: string) {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value);
},
});
for (const schema of Object.values(prpSchemaBundle)) {
ajv.addSchema(schema);
}
const registeredFixtureValidator = ajv.getSchema(fixtureSchema.$id);
if (registeredFixtureValidator === undefined) {
throw new Error("PRP fixture validator was not registered");
}
const fixtureValidator = registeredFixtureValidator as ValidateFunction<PrpFixture>;
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function canonicalJson(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map(canonicalJson).join(",")}]`;
}
const object = asRecord(value);
if (object !== null) {
return `{${Object.keys(object)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "undefined";
}
function versionIssues(value: unknown): ProtocolValidationIssue[] {
const fixture = asRecord(value);
if (fixture === null) {
return [];
}
const issues: ProtocolValidationIssue[] = [];
for (const [field, supported] of [
["fixtureVersion", 1],
["protocolVersion", PRP_PROTOCOL_VERSION],
] as const) {
const actual = fixture[field];
if (typeof actual === "number" && actual !== supported) {
issues.push({
code: "unsupported_required_version",
path: `/${field}`,
message: `${field} ${actual} is unsupported; this implementation requires ${supported}`,
});
}
}
if (Array.isArray(fixture.events)) {
fixture.events.forEach((entry, index) => {
const event = asRecord(entry);
const actual = event?.schemaVersion;
if (typeof actual === "number" && actual !== 1) {
issues.push({
code: "unsupported_required_version",
path: `/events/${index}/schemaVersion`,
message: `event schemaVersion ${actual} is unsupported; this implementation requires 1`,
});
}
});
}
return issues;
}
function ajvIssue(error: ErrorObject): ProtocolValidationIssue {
return {
code: "schema_validation",
path: error.instancePath || "/",
message: error.message ?? "does not match the PRP schema",
};
}
function bindingIssues(fixture: PrpFixture): ProtocolValidationIssue[] {
const issues: ProtocolValidationIssue[] = [];
const uniqueEvents = new Map<string, PrpEvent>();
fixture.events.forEach((event, index) => {
if (event.runId !== fixture.identity.runId) {
issues.push({
code: "binding_mismatch",
path: `/events/${index}/runId`,
message: "event runId must match identity.runId",
});
}
if (
event.normalizedSessionId !== undefined &&
event.normalizedSessionId !== fixture.identity.normalizedSessionId
) {
issues.push({
code: "binding_mismatch",
path: `/events/${index}/normalizedSessionId`,
message: "event normalizedSessionId must match identity.normalizedSessionId",
});
}
const existing = uniqueEvents.get(event.sourceEventId);
if (existing === undefined) {
uniqueEvents.set(event.sourceEventId, event);
} else if (canonicalJson(existing) !== canonicalJson(event)) {
issues.push({
code: "binding_mismatch",
path: `/events/${index}/sourceEventId`,
message: "duplicate sourceEventId deliveries must be byte-equivalent",
});
}
});
fixture.commands.forEach((command, index) => {
if (command.controllerSeq !== index + 1) {
issues.push({
code: "binding_mismatch",
path: `/commands/${index}/controllerSeq`,
message: `controllerSeq must be ${index + 1} in a scripted fixture`,
});
}
});
const distinctEvents = [...uniqueEvents.values()];
const proposedResults = distinctEvents.filter(
(event) => event.eventType === "run.result.proposed",
);
if (proposedResults.length !== 1) {
issues.push({
code: "binding_mismatch",
path: "/events",
message: "scripted fixtures must contain exactly one unique run.result.proposed event",
});
} else if (canonicalJson(proposedResults[0]?.payload) !== canonicalJson(fixture.result)) {
issues.push({
code: "binding_mismatch",
path: "/result",
message: "fixture result must match the run.result.proposed event payload",
});
}
const terminalEvents = distinctEvents.filter(
(event) => event.eventType === "run.terminal",
);
if (terminalEvents.length !== 1) {
issues.push({
code: "binding_mismatch",
path: "/events",
message: "scripted fixtures must contain exactly one unique run.terminal event",
});
}
return issues;
}
export function validatePrpFixture(value: unknown): ProtocolValidationResult {
const unsupported = versionIssues(value);
if (unsupported.length > 0) {
return { ok: false, fixture: null, issues: unsupported };
}
if (!fixtureValidator(value)) {
return {
ok: false,
fixture: null,
issues: (fixtureValidator.errors ?? []).map(ajvIssue),
};
}
const bindings = bindingIssues(value);
return bindings.length === 0
? { ok: true, fixture: value, issues: [] }
: { ok: false, fixture: null, issues: bindings };
}
export function parsePrpFixtureText(text: string): ProtocolValidationResult {
try {
return validatePrpFixture(JSON.parse(text) as unknown);
} catch (error) {
return {
ok: false,
fixture: null,
issues: [
{
code: "invalid_json",
path: "/",
message: error instanceof Error ? error.message : "fixture is not valid JSON",
},
],
};
}
}
export function negotiateProtocolVersion(
runner: ProtocolVersionRange,
controller: ProtocolVersionRange,
): number | null {
const selected = Math.min(runner.max, controller.max);
return selected >= Math.max(runner.min, controller.min) ? selected : null;
}
@@ -0,0 +1,32 @@
import { readFile } from "node:fs/promises";
import {
parsePrpFixtureText,
type ProtocolValidationIssue,
type PrpFixture,
} from "./phase1-contract.js";
export const phase1HappyFixtureUrl = new URL(
"../../protocol/fixtures/phase-01/happy-path.json",
import.meta.url,
);
export class PrpFixtureValidationError extends Error {
readonly issues: ProtocolValidationIssue[];
constructor(issues: ProtocolValidationIssue[]) {
super(issues.map((issue) => `${issue.path}: ${issue.message}`).join("; "));
this.name = "PrpFixtureValidationError";
this.issues = issues;
}
}
export async function loadPrpFixture(
url: URL = phase1HappyFixtureUrl,
): Promise<PrpFixture> {
const result = parsePrpFixtureText(await readFile(url, "utf8"));
if (!result.ok) {
throw new PrpFixtureValidationError(result.issues);
}
return result.fixture;
}
@@ -0,0 +1,73 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { parsePrpFixtureText, type PrpFixture } from "../protocol/phase1-contract.js";
import {
reducePrpFixture,
reduceSessionEvents,
type SessionSnapshot,
} from "./session-reducer.js";
const fixtureDirectory = new URL("../../protocol/fixtures/phase-01/", import.meta.url);
const fixtureNames = [
"happy-path",
"failed-run",
"interrupted-run",
"duplicate-event",
"source-gap",
"unknown-optional-fields",
];
async function loadFixture(name: string): Promise<PrpFixture> {
const result = parsePrpFixtureText(
await readFile(new URL(`${name}.json`, fixtureDirectory), "utf8"),
);
if (!result.ok) {
throw new Error(result.issues.map((issue) => issue.message).join("; "));
}
return result.fixture;
}
async function loadGolden(name: string): Promise<SessionSnapshot> {
return JSON.parse(
await readFile(new URL(`golden/${name}.snapshot.json`, fixtureDirectory), "utf8"),
) as SessionSnapshot;
}
describe("deterministic PRP session reducer", () => {
for (const fixtureName of fixtureNames) {
it(`matches the ${fixtureName} golden snapshot`, async () => {
expect(reducePrpFixture(await loadFixture(fixtureName))).toEqual(
await loadGolden(fixtureName),
);
});
}
it("is deterministic and idempotent when the same batch is replayed", async () => {
const fixture = await loadFixture("happy-path");
const first = reducePrpFixture(fixture);
expect(reducePrpFixture(fixture)).toEqual(first);
expect(reduceSessionEvents(first, fixture.events)).toEqual(first);
});
it("deduplicates at-least-once delivery before projection effects", async () => {
const snapshot = reducePrpFixture(await loadFixture("duplicate-event"));
expect(snapshot.duplicateEventIds).toEqual(["event_duplicate_03"]);
expect(snapshot.items).toHaveLength(1);
expect(snapshot.timeline).toHaveLength(6);
});
it("records a source cursor gap without inventing the missing event", async () => {
const snapshot = reducePrpFixture(await loadFixture("source-gap"));
expect(snapshot.integrity).toBe("gap_detected");
expect(snapshot.gaps).toEqual([
{
sourceKey: "runner:runner_phase1",
expected: 3,
received: 4,
missing: [3],
},
]);
});
});
@@ -0,0 +1,354 @@
import type {
PrpCapabilities,
PrpEvent,
PrpFixture,
PrpIdentity,
PrpStructuredRunResult,
PrpTerminalState,
} from "../protocol/phase1-contract.js";
export interface SessionTimelineEntry {
position: number;
sourceEventId: string;
sourceSeq: number;
eventType: PrpEvent["eventType"];
emittedAt: string;
itemId?: string;
summary: string;
}
export interface SessionItemSnapshot {
itemId: string;
kind: string;
status: "running" | "completed" | "failed";
text: string;
}
export interface SessionRequestSnapshot {
requestId: string;
requestKind: string;
status: string;
prompt: string;
}
export interface SequenceGap {
sourceKey: string;
expected: number;
received: number;
missing: number[];
}
export interface SessionSnapshot {
schema: "paperclip.prp.session-snapshot.v1";
fixtureName: string;
identity: PrpIdentity;
capabilities: PrpCapabilities;
runPhase: string;
sessionState: "not_started" | "running" | "closed" | "failed";
turnState:
| "not_started"
| "running"
| "completed"
| "failed"
| "interrupted"
| "cancelled";
activeTurnId: string | null;
items: SessionItemSnapshot[];
requests: SessionRequestSnapshot[];
proposedResult: PrpStructuredRunResult | null;
terminal: PrpTerminalState | null;
timeline: SessionTimelineEntry[];
sourceCursors: Record<string, number>;
processedEventIds: string[];
duplicateEventIds: string[];
outOfOrderEventIds: string[];
gaps: SequenceGap[];
integrity: "complete" | "gap_detected";
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
function stringValue(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function sourceKey(event: PrpEvent): string {
return `${event.sourceKind}:${event.sourceInstanceId}`;
}
function timelineSummary(event: PrpEvent): string {
const payload = record(event.payload);
if (event.eventType === "runtime.phase.changed") {
return `Phase changed to ${stringValue(payload.phase, "unknown")}`;
}
if (event.eventType === "item.started") {
return `Started ${stringValue(payload.kind, "item")}`;
}
if (event.eventType === "item.delta") {
return stringValue(payload.text, "Item updated");
}
if (event.eventType === "item.completed") {
return `Completed ${stringValue(payload.kind, "item")}`;
}
if (event.eventType === "item.failed") {
return stringValue(payload.message, "Item failed");
}
if (event.eventType === "run.result.proposed") {
return stringValue(payload.summary, "Structured result proposed");
}
if (event.eventType === "run.terminal") {
return `Run ${stringValue(payload.runTerminalState, "terminal")}`;
}
return event.eventType.replaceAll(".", " ");
}
function upsertItem(
items: SessionItemSnapshot[],
event: PrpEvent,
status: SessionItemSnapshot["status"],
): void {
if (event.itemId === undefined) {
return;
}
const payload = record(event.payload);
const existing = items.find((item) => item.itemId === event.itemId);
const text = stringValue(payload.text);
if (existing === undefined) {
items.push({
itemId: event.itemId,
kind: stringValue(payload.kind, "diagnostic"),
status,
text,
});
return;
}
existing.kind = stringValue(payload.kind, existing.kind);
existing.status = status;
if (event.eventType === "item.delta") {
existing.text += text;
} else if (text.length > 0) {
existing.text = text;
}
}
function applyProjection(snapshot: SessionSnapshot, event: PrpEvent): void {
const payload = record(event.payload);
switch (event.eventType) {
case "runtime.phase.changed":
snapshot.runPhase = stringValue(payload.phase, snapshot.runPhase);
break;
case "session.started":
case "session.resumed":
snapshot.sessionState = "running";
break;
case "session.closed":
snapshot.sessionState = "closed";
break;
case "session.failed":
snapshot.sessionState = "failed";
break;
case "turn.started":
snapshot.turnState = "running";
snapshot.activeTurnId = event.turnId ?? null;
break;
case "turn.completed":
snapshot.turnState = "completed";
snapshot.activeTurnId = null;
break;
case "turn.failed":
snapshot.turnState = "failed";
snapshot.activeTurnId = null;
break;
case "turn.interrupted":
snapshot.turnState = "interrupted";
snapshot.activeTurnId = null;
break;
case "turn.cancelled":
snapshot.turnState = "cancelled";
snapshot.activeTurnId = null;
break;
case "item.started":
case "item.delta":
upsertItem(snapshot.items, event, "running");
break;
case "item.completed":
upsertItem(snapshot.items, event, "completed");
break;
case "item.failed":
upsertItem(snapshot.items, event, "failed");
break;
case "runtime_request.created": {
const request = record(payload.request ?? payload);
const requestId = stringValue(request.requestId);
if (
requestId.length > 0 &&
!snapshot.requests.some((candidate) => candidate.requestId === requestId)
) {
snapshot.requests.push({
requestId,
requestKind: stringValue(request.requestKind, "runtime"),
status: stringValue(request.status, "pending"),
prompt: stringValue(request.prompt),
});
}
break;
}
case "runtime_request.resolved":
case "runtime_request.expired":
case "runtime_request.cancelled": {
const requestId = stringValue(payload.requestId);
const request = snapshot.requests.find((candidate) => candidate.requestId === requestId);
if (request !== undefined) {
request.status = event.eventType.split(".").at(-1) ?? request.status;
}
break;
}
case "run.result.proposed":
snapshot.proposedResult = structuredClone(
event.payload as PrpStructuredRunResult,
);
break;
case "run.terminal":
snapshot.terminal = structuredClone(event.payload as PrpTerminalState);
snapshot.runPhase = "terminal";
break;
}
}
function addGap(snapshot: SessionSnapshot, event: PrpEvent, expected: number): void {
const key = sourceKey(event);
if (
snapshot.gaps.some(
(gap) => gap.sourceKey === key && gap.expected === expected && gap.received === event.sourceSeq,
)
) {
return;
}
snapshot.gaps.push({
sourceKey: key,
expected,
received: event.sourceSeq,
missing: Array.from(
{ length: event.sourceSeq - expected },
(_, index) => expected + index,
),
});
}
export function createSessionSnapshot(fixture: PrpFixture): SessionSnapshot {
return {
schema: "paperclip.prp.session-snapshot.v1",
fixtureName: fixture.name,
identity: structuredClone(fixture.identity),
capabilities: structuredClone(fixture.capabilities),
runPhase: "queued",
sessionState: "not_started",
turnState: "not_started",
activeTurnId: null,
items: [],
requests: [],
proposedResult: null,
terminal: null,
timeline: [],
sourceCursors: {},
processedEventIds: [],
duplicateEventIds: [],
outOfOrderEventIds: [],
gaps: [],
integrity: "complete",
};
}
export function applyPrpEvent(
current: SessionSnapshot,
event: PrpEvent,
): SessionSnapshot {
if (current.processedEventIds.includes(event.sourceEventId)) {
return current;
}
const snapshot = structuredClone(current);
const key = sourceKey(event);
const cursor = snapshot.sourceCursors[key] ?? 0;
const expected = cursor + 1;
if (event.sourceSeq <= cursor) {
if (!snapshot.outOfOrderEventIds.includes(event.sourceEventId)) {
snapshot.outOfOrderEventIds.push(event.sourceEventId);
}
snapshot.integrity = "gap_detected";
return snapshot;
}
if (event.sourceSeq > expected) {
addGap(snapshot, event, expected);
}
snapshot.sourceCursors[key] = event.sourceSeq;
snapshot.processedEventIds.push(event.sourceEventId);
snapshot.timeline.push({
position: snapshot.timeline.length + 1,
sourceEventId: event.sourceEventId,
sourceSeq: event.sourceSeq,
eventType: event.eventType,
emittedAt: event.emittedAt,
...(event.itemId === undefined ? {} : { itemId: event.itemId }),
summary: timelineSummary(event),
});
applyProjection(snapshot, event);
snapshot.integrity =
snapshot.gaps.length > 0 || snapshot.outOfOrderEventIds.length > 0
? "gap_detected"
: "complete";
return snapshot;
}
export function reduceSessionEvents(
current: SessionSnapshot,
events: readonly PrpEvent[],
): SessionSnapshot {
const counts = new Map<string, number>();
for (const event of events) {
counts.set(event.sourceEventId, (counts.get(event.sourceEventId) ?? 0) + 1);
}
let snapshot = structuredClone(current);
for (const [eventId, count] of counts) {
if (count > 1 && !snapshot.duplicateEventIds.includes(eventId)) {
snapshot.duplicateEventIds.push(eventId);
}
}
snapshot.duplicateEventIds.sort();
for (const event of events) {
snapshot = applyPrpEvent(snapshot, event);
}
return snapshot;
}
export function reducePrpFixture(fixture: PrpFixture): SessionSnapshot {
return reduceSessionEvents(createSessionSnapshot(fixture), fixture.events);
}
export interface Phase1ParitySummary {
runId: string;
integrity: SessionSnapshot["integrity"];
timelineCount: number;
duplicateEventIds: string[];
gaps: SequenceGap[];
turnTerminalState: string | null;
runTerminalState: string | null;
}
export function phase1ParitySummary(snapshot: SessionSnapshot): Phase1ParitySummary {
return {
runId: snapshot.identity.runId,
integrity: snapshot.integrity,
timelineCount: snapshot.timeline.length,
duplicateEventIds: [...snapshot.duplicateEventIds],
gaps: structuredClone(snapshot.gaps),
turnTerminalState: snapshot.terminal?.turnTerminalState ?? null,
runTerminalState: snapshot.terminal?.runTerminalState ?? null,
};
}
@@ -0,0 +1,23 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { formatPhase1Replay, replayPhase1FixtureText } from "./phase1-replay.js";
describe("Phase 1 replay tracer", () => {
it("formats the validated reducer snapshot for CLI consumers", async () => {
const source = await readFile(
new URL("../../protocol/fixtures/phase-01/happy-path.json", import.meta.url),
"utf8",
);
const result = replayPhase1FixtureText(source);
expect(result.ok).toBe(true);
expect(JSON.parse(formatPhase1Replay(result))).toMatchObject({
ok: true,
snapshot: {
integrity: "complete",
terminal: { runTerminalState: "succeeded" },
},
});
});
});
@@ -0,0 +1,21 @@
import {
parsePrpFixtureText,
type ProtocolValidationIssue,
} from "../protocol/phase1-contract.js";
import { reducePrpFixture, type SessionSnapshot } from "../reducer/session-reducer.js";
export type Phase1ReplayResult =
| { ok: true; snapshot: SessionSnapshot; issues: [] }
| { ok: false; snapshot: null; issues: ProtocolValidationIssue[] };
export function replayPhase1FixtureText(text: string): Phase1ReplayResult {
const validation = parsePrpFixtureText(text);
if (!validation.ok) {
return { ok: false, snapshot: null, issues: validation.issues };
}
return { ok: true, snapshot: reducePrpFixture(validation.fixture), issues: [] };
}
export function formatPhase1Replay(result: Phase1ReplayResult): string {
return JSON.stringify(result, null, 2);
}
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"rootDir": ".",
"types": ["vite/client"]
},
"include": ["devtools/browser/src/**/*.ts", "devtools/browser/src/**/*.tsx"]
}
+20
View File
@@ -0,0 +1,20 @@
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
const packageRoot = fileURLToPath(new URL(".", import.meta.url));
export default defineConfig({
root: resolve(packageRoot, "devtools/browser"),
plugins: [react()],
build: {
outDir: resolve(packageRoot, "dist-browser"),
emptyOutDir: true,
// The standalone devtool targets current evergreen browsers. Keeping the
// emitted syntax modern also avoids asking Vite to downlevel Ajv's runtime
// validator, which recent esbuild releases intentionally do not support.
target: "esnext",
},
});