mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 02:32:07 +01:00
feat(runner): add phase 1 static replay contract
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a86edda579
commit
1b51f95c6c
56 files changed
+5034
-5
No files matched your search
@@ -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 & 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();
|
||||
});
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
+130
@@ -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"
|
||||
}
|
||||
+11
@@ -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"
|
||||
}
|
||||
+137
@@ -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"
|
||||
}
|
||||
+9
@@ -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"
|
||||
}
|
||||
+111
@@ -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"
|
||||
}
|
||||
+9
@@ -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 } }
|
||||
}
|
||||
+13
@@ -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,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"]
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user