diff --git a/packages/paperclip-runner/devtools/browser/src/App.tsx b/packages/paperclip-runner/devtools/browser/src/App.tsx
index 6044c2120d9..0ace32e779c 100644
--- a/packages/paperclip-runner/devtools/browser/src/App.tsx
+++ b/packages/paperclip-runner/devtools/browser/src/App.tsx
@@ -84,6 +84,27 @@ function terminalTone(snapshot: SessionSnapshot) {
return "neutral" as const;
}
+function humanizeProtocolLabel(value: string): string {
+ const words = value.replaceAll("_", " ");
+ return words.length === 0 ? words : `${words[0]?.toUpperCase()}${words.slice(1)}`;
+}
+
+function liveStatusTone(status: LiveStatus, snapshot: SessionSnapshot | null) {
+ if (status === "error") {
+ return "danger" as const;
+ }
+ if (status !== "terminal") {
+ return "neutral" 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,
label = "Session snapshot",
@@ -99,9 +120,11 @@ function SnapshotSummary({
{snapshot.fixtureName}
- {snapshot.integrity === "gap_detected"
- ? "Gap detected"
- : (snapshot.terminal?.runTerminalState ?? snapshot.runPhase)}
+ {humanizeProtocolLabel(
+ snapshot.integrity === "gap_detected"
+ ? "Gap detected"
+ : (snapshot.terminal?.runTerminalState ?? snapshot.runPhase),
+ )}
@@ -126,7 +149,9 @@ function SnapshotSummary({
{snapshot.proposedResult ? (
-
Reported {snapshot.proposedResult.reportedWorkDisposition}
+
+ Reported {humanizeProtocolLabel(snapshot.proposedResult.reportedWorkDisposition)}
+
{snapshot.proposedResult.summary}
) : null}
@@ -366,10 +391,10 @@ function LiveRunner() {
Runner status
- {status}
+ {humanizeProtocolLabel(status)}
diff --git a/packages/paperclip-runner/devtools/browser/static-replay.spec.ts b/packages/paperclip-runner/devtools/browser/static-replay.spec.ts
index 20ebe5d133a..3f25268f721 100644
--- a/packages/paperclip-runner/devtools/browser/static-replay.spec.ts
+++ b/packages/paperclip-runner/devtools/browser/static-replay.spec.ts
@@ -8,8 +8,9 @@ test("validates and renders the shared Phase 1 fixture", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Static replay" }).click();
await expect(page.getByRole("heading", { name: "Live local runner" })).toBeVisible();
- await expect(page.getByTestId("terminal-badge")).toHaveText("succeeded");
+ await expect(page.getByTestId("terminal-badge")).toHaveText("Succeeded");
await expect(page.getByTestId("timeline").getByRole("listitem")).toHaveCount(9);
+ await expect(page.getByTestId("timeline")).not.toContainText("workspace_preparing");
await expect(page.getByTestId("result-summary")).toContainText(
"The scripted run completed successfully.",
);
@@ -34,8 +35,8 @@ test("shows duplicate and unsupported-version replay states", async ({ page }) =
test("streams a live run and proves replay parity", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Start local run" }).click();
- await expect(page.getByTestId("live-status")).toHaveText("terminal");
- await expect(page.getByTestId("terminal-badge")).toHaveText("succeeded");
+ await expect(page.getByTestId("live-status")).toHaveText("Terminal");
+ await expect(page.getByTestId("terminal-badge")).toHaveText("Succeeded");
await expect(page.getByTestId("process-facts")).toContainText("Harness process exit");
await expect(page.getByTestId("process-facts")).toContainText("done");
await expect(page.getByTestId("parity-result")).toContainText("Match");
@@ -50,15 +51,26 @@ test("resolves live permission and input requests", async ({ page }) => {
await page.getByLabel("Scenario").selectOption("permission-input");
await page.getByRole("button", { name: "Start local run" }).click();
await expect(page.getByTestId("runtime-request")).toContainText("permission request");
+ await expect(page.getByTestId("terminal-badge")).toHaveText("Executing");
+ await expect(
+ page.getByTestId("timeline").getByRole("listitem").filter({
+ hasText: "runtime_request.created",
+ }).first(),
+ ).toContainText("permission: Allow the fake driver to write its local fixture?");
await page.screenshot({
path: resolve(packageRoot, "knowledge/evidence/phase-02-live-permission.png"),
fullPage: true,
});
await page.getByRole("button", { name: "Allow" }).click();
await expect(page.getByTestId("runtime-request")).toContainText("input request");
+ await expect(
+ page.getByTestId("timeline").getByRole("listitem").filter({
+ hasText: "runtime_request.resolved",
+ }).first(),
+ ).toContainText("Resolved permission: Allow the fake driver to write its local fixture?");
await page.getByLabel("Response").fill("phase-02-browser-trace");
await page.getByRole("button", { name: "Send input" }).click();
- await expect(page.getByTestId("live-status")).toHaveText("terminal");
+ await expect(page.getByTestId("live-status")).toHaveText("Terminal");
await expect(page.getByTestId("parity-result")).toContainText("Match");
});
@@ -69,11 +81,34 @@ test("interrupts a live turn without duplicating terminal state", async ({ page
const interrupt = page.getByRole("button", { name: "Interrupt turn" });
await expect(interrupt).toBeEnabled();
await interrupt.click();
- await expect(page.getByTestId("live-status")).toHaveText("terminal");
- await expect(page.getByTestId("terminal-badge")).toHaveText("cancelled");
+ await expect(page.getByTestId("live-status")).toHaveText("Terminal");
+ await expect(page.getByTestId("terminal-badge")).toHaveText("Cancelled");
+ await expect(page.getByTestId("live-status")).toHaveAttribute("data-tone", "neutral");
+ await expect(page.getByTestId("terminal-badge")).toHaveAttribute("data-tone", "neutral");
await expect(page.getByTestId("timeline").getByText("run.terminal")).toHaveCount(1);
await page.screenshot({
path: resolve(packageRoot, "knowledge/evidence/phase-02-live-interrupted.png"),
fullPage: true,
});
});
+
+test("shows failed outcomes as danger and names the duplicate-terminal guard", async ({ page }) => {
+ await page.goto("/");
+ await page.getByLabel("Scenario").selectOption("error");
+ await page.getByRole("button", { name: "Start local run" }).click();
+ await expect(page.getByTestId("live-status")).toHaveText("Terminal");
+ await expect(page.getByTestId("live-status")).toHaveAttribute("data-tone", "danger");
+ await expect(page.getByTestId("terminal-badge")).toHaveText("Failed");
+ await expect(page.getByTestId("terminal-badge")).toHaveAttribute("data-tone", "danger");
+
+ await page.getByLabel("Scenario").selectOption("duplicate-terminal");
+ await page.getByRole("button", { name: "Start local run" }).click();
+ await expect(page.getByTestId("live-status")).toHaveText("Terminal");
+ await expect(
+ page.getByTestId("timeline").getByRole("listitem").filter({
+ hasText: "harness.diagnostic",
+ }),
+ ).toContainText(
+ "Duplicate terminal event ignored; the first terminal event remains authoritative.",
+ );
+});
diff --git a/packages/paperclip-runner/knowledge/evidence/phase-01-static-replay.png b/packages/paperclip-runner/knowledge/evidence/phase-01-static-replay.png
index 9d6bdbfac75..b135d0db859 100644
Binary files a/packages/paperclip-runner/knowledge/evidence/phase-01-static-replay.png and b/packages/paperclip-runner/knowledge/evidence/phase-01-static-replay.png differ
diff --git a/packages/paperclip-runner/knowledge/evidence/phase-02-live-complete.png b/packages/paperclip-runner/knowledge/evidence/phase-02-live-complete.png
index d274127043e..94a7f9e0d8b 100644
Binary files a/packages/paperclip-runner/knowledge/evidence/phase-02-live-complete.png and b/packages/paperclip-runner/knowledge/evidence/phase-02-live-complete.png differ
diff --git a/packages/paperclip-runner/knowledge/evidence/phase-02-live-interrupted.png b/packages/paperclip-runner/knowledge/evidence/phase-02-live-interrupted.png
index 87a9b1bd929..56cb5e27e5f 100644
Binary files a/packages/paperclip-runner/knowledge/evidence/phase-02-live-interrupted.png and b/packages/paperclip-runner/knowledge/evidence/phase-02-live-interrupted.png differ
diff --git a/packages/paperclip-runner/knowledge/evidence/phase-02-live-permission.png b/packages/paperclip-runner/knowledge/evidence/phase-02-live-permission.png
index 785ee611a8f..9272fb65e5f 100644
Binary files a/packages/paperclip-runner/knowledge/evidence/phase-02-live-permission.png and b/packages/paperclip-runner/knowledge/evidence/phase-02-live-permission.png differ
diff --git a/packages/paperclip-runner/protocol/fixtures/phase-01/golden/happy-path.snapshot.json b/packages/paperclip-runner/protocol/fixtures/phase-01/golden/happy-path.snapshot.json
index f19c4146659..05498282265 100644
--- a/packages/paperclip-runner/protocol/fixtures/phase-01/golden/happy-path.snapshot.json
+++ b/packages/paperclip-runner/protocol/fixtures/phase-01/golden/happy-path.snapshot.json
@@ -75,7 +75,7 @@
"sourceSeq": 1,
"eventType": "runtime.phase.changed",
"emittedAt": "2026-08-07T12:00:00.030Z",
- "summary": "Phase changed to workspace_preparing"
+ "summary": "Phase changed to workspace preparing"
},
{
"position": 2,
diff --git a/packages/paperclip-runner/protocol/fixtures/phase-01/golden/interrupted-run.snapshot.json b/packages/paperclip-runner/protocol/fixtures/phase-01/golden/interrupted-run.snapshot.json
index c2f9f55ecf2..5d2f2b5836f 100644
--- a/packages/paperclip-runner/protocol/fixtures/phase-01/golden/interrupted-run.snapshot.json
+++ b/packages/paperclip-runner/protocol/fixtures/phase-01/golden/interrupted-run.snapshot.json
@@ -34,6 +34,7 @@
{
"requestId": "request_interrupted_permission",
"requestKind": "runtime",
+ "type": "permission",
"status": "pending",
"prompt": "Allow the fake command?"
}
@@ -92,7 +93,7 @@
"sourceSeq": 3,
"eventType": "runtime_request.created",
"emittedAt": "2026-08-07T12:20:00.040Z",
- "summary": "runtime_request created"
+ "summary": "permission: Allow the fake command?"
},
{
"position": 4,
diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/phase2.rs b/packages/paperclip-runner/runner/crates/runner-core/src/phase2.rs
index 5f65be59ca9..89ab5295de7 100644
--- a/packages/paperclip-runner/runner/crates/runner-core/src/phase2.rs
+++ b/packages/paperclip-runner/runner/crates/runner-core/src/phase2.rs
@@ -775,7 +775,10 @@ impl RunnerState {
} else {
self.emit_event(
"harness.diagnostic",
- json!({ "code": "duplicate_semantic_result_ignored" }),
+ json!({
+ "code": "duplicate_semantic_result_ignored",
+ "message": "Duplicate semantic result ignored; the first result remains authoritative."
+ }),
None,
None,
)?;
@@ -791,7 +794,10 @@ impl RunnerState {
} else {
self.emit_event(
"harness.diagnostic",
- json!({ "code": "duplicate_terminal_ignored" }),
+ json!({
+ "code": "duplicate_terminal_ignored",
+ "message": "Duplicate terminal event ignored; the first terminal event remains authoritative."
+ }),
None,
None,
)?;
@@ -1020,6 +1026,12 @@ pub fn run_fake_harness(
None,
json!({ "driverSessionId": "driver_phase2_fake" }),
)?;
+ output.send_event(
+ "runtime.phase.changed",
+ None,
+ None,
+ json!({ "phase": "executing" }),
+ )?;
let turn = receive_harness_command(&command_receiver, "turn.start", None)?;
let turn_id = turn
.payload
diff --git a/packages/paperclip-runner/src/mock-core/phase2-local-runner.test.ts b/packages/paperclip-runner/src/mock-core/phase2-local-runner.test.ts
index a5b8b154405..b4a82d06c1b 100644
--- a/packages/paperclip-runner/src/mock-core/phase2-local-runner.test.ts
+++ b/packages/paperclip-runner/src/mock-core/phase2-local-runner.test.ts
@@ -20,6 +20,14 @@ describe.sequential("Phase 2 local runner and fake harness", () => {
expect(eventTypes).toContain("harness.ready");
expect(eventTypes).toContain("item.completed");
+ const sessionStartedIndex = eventTypes.indexOf("session.started");
+ const executingIndex = trace.events.findIndex(
+ (event) =>
+ event.eventType === "runtime.phase.changed" &&
+ (event.payload as Record).phase === "executing",
+ );
+ expect(executingIndex).toBeGreaterThan(sessionStartedIndex);
+ expect(executingIndex).toBeLessThan(eventTypes.indexOf("turn.started"));
expect(trace.events.some((event) => event.itemId === "item_phase2_file")).toBe(true);
expect(trace.result?.reportedWorkDisposition).toBe("done");
expect(trace.harnessProcessExit).toEqual({ exitCode: 0, success: true, signal: null });
@@ -99,6 +107,13 @@ describe.sequential("Phase 2 local runner and fake harness", () => {
"duplicate_terminal_ignored",
),
).toBe(true);
+ expect(
+ replayPhase2Events(trace, trace.events).timeline.find(
+ (event) => event.eventType === "harness.diagnostic",
+ )?.summary,
+ ).toBe(
+ "Duplicate terminal event ignored; the first terminal event remains authoritative.",
+ );
});
it("keeps an error process exit distinct from its semantic result", async () => {
diff --git a/packages/paperclip-runner/src/reducer/session-reducer.test.ts b/packages/paperclip-runner/src/reducer/session-reducer.test.ts
index 8d3c0c7624f..efbe337fa6c 100644
--- a/packages/paperclip-runner/src/reducer/session-reducer.test.ts
+++ b/packages/paperclip-runner/src/reducer/session-reducer.test.ts
@@ -2,8 +2,13 @@ import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
-import { parsePrpFixtureText, type PrpFixture } from "../protocol/phase1-contract.js";
import {
+ parsePrpFixtureText,
+ type PrpEvent,
+ type PrpFixture,
+} from "../protocol/phase1-contract.js";
+import {
+ createSessionSnapshot,
reducePrpFixture,
reduceSessionEvents,
type SessionSnapshot,
@@ -70,4 +75,34 @@ describe("deterministic PRP session reducer", () => {
},
]);
});
+
+ it("summarizes runtime request creation and resolution with type and prompt", async () => {
+ const fixture = await loadFixture("interrupted-run");
+ const fixtureRequest = fixture.events.find(
+ (event) => event.eventType === "runtime_request.created",
+ );
+ if (fixtureRequest === undefined) {
+ throw new Error("interrupted-run fixture must create a runtime request");
+ }
+ const created: PrpEvent = {
+ ...fixtureRequest,
+ sourceEventId: "event_request_summary_01",
+ sourceSeq: 1,
+ };
+ const resolved: PrpEvent = {
+ ...fixtureRequest,
+ sourceEventId: "event_request_summary_02",
+ sourceSeq: 2,
+ eventType: "runtime_request.resolved",
+ payload: { requestId: "request_interrupted_permission" },
+ };
+
+ const snapshot = reduceSessionEvents(createSessionSnapshot(fixture), [created, resolved]);
+
+ expect(snapshot.requests[0]).toMatchObject({ type: "permission" });
+ expect(snapshot.timeline.map((entry) => entry.summary)).toEqual([
+ "permission: Allow the fake command?",
+ "Resolved permission: Allow the fake command?",
+ ]);
+ });
});
diff --git a/packages/paperclip-runner/src/reducer/session-reducer.ts b/packages/paperclip-runner/src/reducer/session-reducer.ts
index f8de4c1076d..7fbe5e100b3 100644
--- a/packages/paperclip-runner/src/reducer/session-reducer.ts
+++ b/packages/paperclip-runner/src/reducer/session-reducer.ts
@@ -27,6 +27,7 @@ export interface SessionItemSnapshot {
export interface SessionRequestSnapshot {
requestId: string;
requestKind: string;
+ type: string;
status: string;
prompt: string;
}
@@ -80,10 +81,23 @@ function sourceKey(event: PrpEvent): string {
return `${event.sourceKind}:${event.sourceInstanceId}`;
}
-function timelineSummary(event: PrpEvent): string {
+function humanizeProtocolValue(value: string): string {
+ return value.replaceAll("_", " ");
+}
+
+function sentenceCase(value: string): string {
+ return value.length === 0 ? value : `${value[0]?.toUpperCase()}${value.slice(1)}`;
+}
+
+function requestSummary(type: string, prompt: string, action?: string): string {
+ const detail = prompt.length > 0 ? `${type}: ${prompt}` : `${type} request`;
+ return action === undefined ? detail : `${action} ${detail}`;
+}
+
+function timelineSummary(snapshot: SessionSnapshot, event: PrpEvent): string {
const payload = record(event.payload);
if (event.eventType === "runtime.phase.changed") {
- return `Phase changed to ${stringValue(payload.phase, "unknown")}`;
+ return `Phase changed to ${humanizeProtocolValue(stringValue(payload.phase, "unknown"))}`;
}
if (event.eventType === "item.started") {
return `Started ${stringValue(payload.kind, "item")}`;
@@ -103,6 +117,40 @@ function timelineSummary(event: PrpEvent): string {
if (event.eventType === "run.terminal") {
return `Run ${stringValue(payload.runTerminalState, "terminal")}`;
}
+ if (
+ event.eventType === "runner.diagnostic" ||
+ event.eventType === "harness.diagnostic"
+ ) {
+ const message = stringValue(payload.message);
+ if (message.length > 0) {
+ return message;
+ }
+ const code = stringValue(payload.code);
+ if (code.length > 0) {
+ return sentenceCase(humanizeProtocolValue(code));
+ }
+ }
+ if (event.eventType === "runtime_request.created") {
+ const request = record(payload.request ?? payload);
+ return requestSummary(
+ stringValue(request.type, "runtime"),
+ stringValue(request.prompt),
+ );
+ }
+ if (
+ event.eventType === "runtime_request.resolved" ||
+ event.eventType === "runtime_request.expired" ||
+ event.eventType === "runtime_request.cancelled"
+ ) {
+ const requestId = stringValue(payload.requestId);
+ const request = snapshot.requests.find((candidate) => candidate.requestId === requestId);
+ const action = sentenceCase(
+ humanizeProtocolValue(event.eventType.split(".").at(-1) ?? "updated"),
+ );
+ return request === undefined
+ ? `${action} runtime request ${requestId}`.trim()
+ : requestSummary(request.type, request.prompt, action);
+ }
return event.eventType.replaceAll(".", " ");
}
@@ -191,6 +239,7 @@ function applyProjection(snapshot: SessionSnapshot, event: PrpEvent): void {
snapshot.requests.push({
requestId,
requestKind: stringValue(request.requestKind, "runtime"),
+ type: stringValue(request.type, "runtime"),
status: stringValue(request.status, "pending"),
prompt: stringValue(request.prompt),
});
@@ -307,7 +356,7 @@ export function applyPrpEvent(
eventType: event.eventType,
emittedAt: event.emittedAt,
...(event.itemId === undefined ? {} : { itemId: event.itemId }),
- summary: timelineSummary(event),
+ summary: timelineSummary(snapshot, event),
});
applyProjection(snapshot, event);
snapshot.integrity =