refactor(runner): organize package by responsibility

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-11 12:56:59 +00:00
1 parent ef2d8c6e03
commit 59e57017b1
574 files changed
+16232 -28863

No files matched your search

+2 -2
View File
@@ -41,8 +41,8 @@
"check:tokens": "node scripts/check-forbidden-tokens.mjs",
"check:token-gates": "node scripts/check-token-gates.mjs",
"check:no-git-push": "node scripts/check-no-git-push.mjs",
"check:runner-phase4-spec": "node scripts/check-runner-phase4-spec.mjs",
"check:runner-phase5-spec": "node scripts/check-runner-phase5-spec.mjs",
"check:runner-operator-contract": "node scripts/check-runner-operator-contract.mjs",
"check:runner-status-authority": "node scripts/check-runner-status-authority.mjs",
"test:check-no-git-push": "node --test scripts/check-no-git-push.test.mjs",
"test:install-sh-docker": "./scripts/test-install-sh-docker.sh",
"test:hermes-gateway-smoke": "node --test scripts/smoke/hermes-gateway-smoke.test.mjs",
+3 -3
View File
@@ -1,9 +1,9 @@
/dist-browser/
/dist-phase5/
/dist-sdk/
/playwright-report/
/test-results/
/dist-phase6/
/dist-phase7/
/dist-standalone/
/dist-scenarios/
/dist-issue-thread/
# The repository root ignores scratch `check-*.mjs` files and only re-includes
+80 -100
View File
@@ -1,38 +1,19 @@
# Paperclip Native Runner
This workspace is the standalone development boundary for Paperclip's native
runner protocol, harness drivers, and normalized session backends. The
production runner direction is Rust; `runner/` is its Cargo workspace. The
TypeScript surface remains a control-plane/client reference implementation.
Phase 0 runs both implementations against one language-neutral fixture. Phase 1
adds executable PRP schemas, a cross-language conformance corpus, deterministic
static replay, and a standalone browser reference page. Phase 2 adds the Rust
runner process, process-group supervision, a scripted fake harness over stdio
JSONL, a TypeScript mock core, and validated live browser replay. Phase 3 adds
the Rust outbound WebSocket client, private durable state, replay and restart
recovery, and safe CLI/browser diagnostics. Phase 4 adds a direct, skillless
Codex app-server driver, semantic completion tools, canonical event tracing,
and a real-model example against the mock core. These phases do not import or
start Paperclip's server, UI, CLI, or production database. The Phase 4b lower
layer adds browser-resolved provider requests, goal capability detection,
subagent lineage, control-race semantics, and a package-local demo server that
keeps Codex authentication server-side. The Phase 4b live console adds the
browser tracer over that boundary: a live transcript and composer, same-turn
steering, three distinct interrupt races, inline request cards,
capability-gated goal controls, parent/child lineage, reconnect, refresh,
replay, and a redacted protocol inspector. Phase 5 extracts that accepted
transport, reducer projection, hook, components, styles, and reference console
into versioned public subpaths, then proves the surface with a second minimal
consumer.
Phase 6 currently adds a package-local standalone adapter demo at the public
control-plane/session boundary. It proves PRP replay, reducer projection,
result finalization, a default legacy path, a feature-flagged native demo path,
and a kill switch without contacting or modifying a Paperclip instance.
Consumers import this demo contract through the declared
`@paperclipai/paperclip-runner/standalone` subpath instead of package source
files.
This package is the standalone development boundary for Paperclip's native
runner protocol, process supervision, durable transport, provider drivers, and
normalized session backends. Rust owns the production runner under `runner/`;
TypeScript provides the control-plane reference, browser SDK, scenario tools,
and conformance oracle.
## Phase 0–5 quick start
The package includes one coherent set of capabilities: PRP v1 validation and
replay, a supervised local runner with a scripted fake harness, durable
WebSocket delivery and recovery, a skillless Codex app-server driver, live
session and issue-thread surfaces, a public browser/React SDK, a standalone
adapter demo, and a deterministic mock control plane. None of these surfaces
imports or starts Paperclip's server, UI, CLI, or production database.
## Quick start
From the repository root:
@@ -55,41 +36,41 @@ pnpm --filter @paperclipai/paperclip-runner verify:rootless
The tracer's final line is stable:
```json
{"schemaVersion":"paperclip.runner.phase0.output.v1","runIdentity":{"runId":"run_phase0_0001","sessionId":"session_phase0_0001"},"result":{"status":"succeeded","summary":"Standalone Phase 0 fixture accepted."}}
{"schemaVersion":"paperclip.runner.conformance.output.v1","runIdentity":{"runId":"run_conformance_0001","sessionId":"session_conformance_0001"},"result":{"status":"succeeded","summary":"Standalone Conformance fixture accepted."}}
```
Run only the tracer with:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase0
pnpm --filter @paperclipai/paperclip-runner trace:conformance
```
Replay the Phase 1 happy path, run a Phase 2 local session, or open the browser
Replay the Replay happy path, run a Local session, or open the browser
devtool:
```sh
pnpm --filter @paperclipai/paperclip-runner replay:phase1
pnpm --filter @paperclipai/paperclip-runner trace:phase2 -- --scenario happy-path
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault lost-ack
pnpm --filter @paperclipai/paperclip-runner trace:phase4
pnpm --filter @paperclipai/paperclip-runner demo:phase4b -- --host 127.0.0.1 --port 4174
pnpm --filter @paperclipai/paperclip-runner replay:fixture
pnpm --filter @paperclipai/paperclip-runner trace:local-runner -- --scenario happy-path
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault lost-ack
pnpm --filter @paperclipai/paperclip-runner trace:codex
pnpm --filter @paperclipai/paperclip-runner demo:live-console -- --host 127.0.0.1 --port 4174
# Phase 4b: chat with a live session in the browser.
pnpm --filter @paperclipai/paperclip-runner console:phase4b
# Live console: chat with a live session in the browser.
pnpm --filter @paperclipai/paperclip-runner console:live-console
pnpm --filter @paperclipai/paperclip-runner browser:dev --host 127.0.0.1 --port 4179
# Phase 5: open the public-SDK reference console and mini consumer.
pnpm --filter @paperclipai/paperclip-runner console:phase5
# SDK: open the public-SDK reference console and mini consumer.
pnpm --filter @paperclipai/paperclip-runner console:sdk
# Phase 6: run the standalone legacy/native/kill-switch tracer and page.
pnpm --filter @paperclipai/paperclip-runner trace:phase6
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --feature-flag enabled
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --feature-flag enabled --kill-switch enabled
pnpm --filter @paperclipai/paperclip-runner demo:phase6
# Standalone: run the standalone legacy/native/kill-switch tracer and page.
pnpm --filter @paperclipai/paperclip-runner trace:standalone
pnpm --filter @paperclipai/paperclip-runner trace:standalone -- --feature-flag enabled
pnpm --filter @paperclipai/paperclip-runner trace:standalone -- --feature-flag enabled --kill-switch enabled
pnpm --filter @paperclipai/paperclip-runner demo:standalone
```
Phase 4b provider-backed routes are loopback-only and reject wildcard/LAN
Live console provider-backed routes are loopback-only and reject wildcard/LAN
binds. Browser mutations require same-origin Fetch Metadata, matching Origin,
and JSON content; see the protocol-server tutorial for direct `curl` examples.
@@ -102,74 +83,73 @@ and JSON content; see the protocol-server tutorial for direct `curl` examples.
| `test` | Run Rust/TypeScript fixture, supervisor, fake-driver, live/replay, and boundary tests. |
| `check:forbidden-imports` | Reject TypeScript imports and Cargo path dependencies that cross into Paperclip core. |
| `check:tracked-imports` | Reject tracked imports and `package.json` entry points that only resolve against untracked files, so a clean checkout of any commit builds. |
| `check:phase0-parity` | Require byte-for-byte equivalent Rust and TypeScript tracer output. |
| `check:phase1-goldens` | Require all reducer snapshots and cross-language summaries to match checked goldens. |
| `check:phase1-parity` | Run TypeScript and Rust against the same Phase 1 fixture summaries. |
| `check:numbered-milestones` | Reject numbered construction-milestone names in tracked package paths and source. |
| `check:conformance-parity` | Require byte-for-byte equivalent Rust and TypeScript tracer output. |
| `check:replay-goldens` | Require all reducer snapshots and cross-language summaries to match checked goldens. |
| `check:replay-parity` | Run TypeScript and Rust against the same Replay fixture summaries. |
| `check:browser-tokens` | Reject component-local visual literals and require the standalone token layer. |
| `docs:validate` | Validate local documentation links and the OKF v0.2 bundle. |
| `trace:phase0` | Run the Rust mock-core tracer, print the stable result, and exit. |
| `trace:phase0:typescript` | Run the TypeScript reference tracer directly. |
| `replay:phase1` | Validate and reduce a fixture to a final snapshot. |
| `trace:phase2` | Run one native local session through the Rust runner and fake harness. |
| `record:phase2` | Capture a validated happy-path live trace as a replay fixture. |
| `trace:phase3` | Run the Rust runner against the mock core with a selected recovery fault. |
| `record:phase3` | Regenerate the complete Phase 3 fault matrix and exact per-fault traces. |
| `trace:phase4` | Run the mock core with a real, local skillless Codex app-server session. |
| `record:phase4` | Run the safe Codex task and record its validated, normalized trace. |
| `demo:phase4b` | Start the package-local HTTP/SSE server with server-only Codex authentication. |
| `console:phase4b` | Start the standalone browser devtool with the Phase 4b live console on `127.0.0.1:4180`. |
| `record:phase4b` | Run a safe real Codex task through the demo server and record reconnect/replay evidence. |
| `console:phase5` | Start the public-SDK reference console and mini consumer on `127.0.0.1:4181`. |
| `test:phase5` | Run targeted browser-client, reducer-projection, and React component contract tests. |
| `test:browser:phase5` | Exercise both consumers with the fake driver, keyboard/a11y checks, reconnect/replay, measurements, and screenshots. |
| `record:phase5:codex` | Run both public consumers against a safe real Codex session and capture live screenshots. |
| `check:phase-07-contract` | Verify the generated capability, legacy MCP, and eval traceability contract. |
| `trace:conformance` | Run the Rust mock-core tracer, print the stable result, and exit. |
| `trace:conformance:typescript` | Run the TypeScript reference tracer directly. |
| `replay:fixture` | Validate and reduce a fixture to a final snapshot. |
| `trace:local-runner` | Run one native local session through the Rust runner and fake harness. |
| `record:local-runner` | Capture a validated happy-path live trace as a replay fixture. |
| `trace:durable-recovery` | Run the Rust runner against the mock core with a selected recovery fault. |
| `record:durable-recovery` | Regenerate the complete Durable recovery fault matrix and exact per-fault traces. |
| `trace:codex` | Run the mock core with a real, local skillless Codex app-server session. |
| `record:codex` | Run the safe Codex task and record its validated, normalized trace. |
| `demo:live-console` | Start the package-local HTTP/SSE server with server-only Codex authentication. |
| `console:live-console` | Start the standalone browser devtool with the Live console on `127.0.0.1:4180`. |
| `record:live-console` | Run a safe real Codex task through the demo server and record reconnect/replay evidence. |
| `console:sdk` | Start the public-SDK reference console and mini consumer on `127.0.0.1:4181`. |
| `test:sdk` | Run targeted browser-client, reducer-projection, and React component contract tests. |
| `test:browser:sdk` | Exercise both consumers with the fake driver, keyboard/a11y checks, reconnect/replay, measurements, and screenshots. |
| `record:sdk:codex` | Run both public consumers against a safe real Codex session and capture live screenshots. |
| `check:capability-contract` | Verify the generated capability, legacy MCP, and eval traceability contract. |
| `check:semantic-contracts` | Verify the provider-neutral semantic tool contract is current. |
| `trace:phase7` | Run the real runnerd/Codex semantic loop against the mock control plane. |
| `demo:phase7` | Start the Phase 7 scenario explorer over the mock control plane on `127.0.0.1:4183`. |
| `trace:live-runner` | Run the real runnerd/Codex semantic loop against the mock control plane. |
| `demo:scenarios` | Start the Capability scenario explorer over the mock control plane on `127.0.0.1:4183`. |
| `console:issue-thread` | Start the Paperclip-style issue thread on `127.0.0.1:4184`. |
| `test:phase7` | Run the scenario index, run-artifact, parity, explorer component, and route tests. |
| `test:browser:phase7` | Exercise both the scenario explorer and issue-thread browser contracts. |
| `record:phase7` | Record the twelve-route screenshot acceptance set at both viewports. |
| `test:scenarios` | Run the scenario index, run-artifact, parity, explorer component, and route tests. |
| `test:browser:scenarios` | Exercise both the scenario explorer and issue-thread browser contracts. |
| `record:capability` | Record the twelve-route screenshot acceptance set at both viewports. |
| `browser:dev` | Start the standalone live/replay/recovery browser devtool. |
| `test:browser` | Exercise static replay and live scenarios, then capture temporary screenshots under ignored test output. |
| `verify` | Run the complete deterministic Phase 0 through Phase 5 acceptance sequence. |
| `verify` | Run the complete deterministic Conformance through SDK acceptance sequence. |
| `verify:rootless` | Extract Debian/Ubuntu browser libraries without root, then run `verify`. |
## Navigate
- [Architecture and dependency boundary](docs/architecture.md)
- [Tutorial index](docs/index.md)
- [Phase 0 hand-run tutorial](docs/tutorials/phase-00-standalone-tracer.md)
- [Phase 1 hand-run tutorial](docs/tutorials/phase-01-static-replay.md)
- [Phase 2 hand-run tutorial](docs/tutorials/phase-02-local-runner.md)
- [Phase 2 local protocol reference](docs/phase-02-local-protocol.md)
- [Phase 3 break-it-on-purpose tutorial](docs/tutorials/phase-03-break-recovery.md)
- [Phase 3 durable transport reference](docs/phase-03-durable-transport.md)
- [Phase 4 skillless Codex tutorial](docs/tutorials/phase-04-skillless-codex.md)
- [Phase 4 skillless Codex driver reference](docs/phase-04-skillless-codex-driver.md)
- [Phase 4b protocol/server tutorial](docs/tutorials/phase-04b-protocol-server.md)
- [Phase 4b protocol/server reference](docs/phase-04b-protocol-server.md)
- [Phase 4b live console tutorial](docs/tutorials/phase-04b-live-console.md)
- [Phase 4b live console reference](docs/phase-04b-live-console.md)
- [Phase 5 SDK console tutorial](docs/tutorials/phase-05-sdk-console.md)
- [Phase 5 browser SDK reference](docs/phase-05-sdk.md)
- [Phase 5 component decision record](docs/design/phase-5-component-decisions.md)
- [Phase 7 semantic catalog and authorization](docs/phase-07-semantic-catalog.md)
- [Phase 7 live runnerd/Codex loop](docs/phase-07-live-runnerd-codex.md)
- [Phase 7 issue-thread UI](docs/phase-07-issue-thread-ui.md)
- [Conformance hand-run tutorial](docs/tutorials/conformance-standalone-tracer.md)
- [Replay hand-run tutorial](docs/tutorials/replay.md)
- [Local runner hand-run tutorial](docs/tutorials/local-runner.md)
- [Local protocol reference](docs/local-runner.md)
- [Durable recovery break-it-on-purpose tutorial](docs/tutorials/durable-recovery.md)
- [Durable transport reference](docs/durable-recovery.md)
- [Codex skillless Codex tutorial](docs/tutorials/codex.md)
- [Codex skillless Codex driver reference](docs/codex-driver.md)
- [Live console protocol/server tutorial](docs/tutorials/live-console-protocol-server.md)
- [Live console protocol/server reference](docs/live-console-protocol-server.md)
- [Live console tutorial](docs/tutorials/live-console.md)
- [Live console reference](docs/live-console.md)
- [SDK console tutorial](docs/tutorials/sdk-console.md)
- [SDK browser SDK reference](docs/sdk.md)
- [SDK component decision record](docs/design/sdk-component-decisions.md)
- [Capability semantic catalog and authorization](docs/capability-semantic-catalog.md)
- [Capability live runnerd/Codex loop](docs/capability-live-runnerd-codex.md)
- [Capability issue-thread UI](docs/capability-issue-thread-ui.md)
- [PRP compatibility/versioning policy](docs/protocol-compatibility.md)
- [Cumulative end-to-end tutorial](docs/tutorials/end-to-end.md)
- [Journal guide](docs/journal.md)
- [OKF knowledge bundle](knowledge/)
- [Implementation plan](spec/paperclip-native-runner-implementation-plan.md)
- [Normative spike specification](spec/paperclip-native-runner-spike-spec.md)
- [Normative native-runner contract](spec/native-runner-contract.md)
Phase 4 adds the package-local real-model reference driver, Phase 4b adds the
package-local browser console, and Phase 5 extracts a reusable public SDK plus
Codex adds the package-local real-model reference driver, Live console adds the
package-local browser console, and SDK extracts a reusable public SDK plus
two standalone consumers. Production Paperclip integration remains deferred.
The Phase 5 reference console opens in direct chat mode. Enter a normal prompt,
The SDK reference console opens in direct chat mode. Enter a normal prompt,
then open the protocol inspector to review events and reducer state. Expand a
Terminal row and its nested **Debug details** disclosure to inspect every
canonical event retained for that command. The header marker `🖇️ v0.1.2`
@@ -1,17 +1,10 @@
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, test, type Page } from "@playwright/test";
const evidenceRoot = resolve(
fileURLToPath(new URL("../../knowledge/evidence/phase-04b", import.meta.url)),
);
const DESKTOP = { width: 1440, height: 900 };
const MOBILE = { width: 390, height: 844 };
function shot(page: Page, name: string) {
return page.screenshot({ path: resolve(evidenceRoot, `${name}.png`), fullPage: true });
return page.screenshot({ path: test.info().outputPath(`${name}.png`), fullPage: true });
}
async function openConsole(page: Page) {
@@ -27,7 +20,7 @@ async function runManifest(page: Page, id: string) {
await expect(page.getByTestId("connection-status")).toHaveText("connected", { timeout: 15_000 });
}
test.describe("Phase 4b live console", () => {
test.describe("Live console", () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(DESKTOP);
});
@@ -46,7 +39,7 @@ test.describe("Phase 4b live console", () => {
await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 20_000 });
await expect(page.getByTestId("live-transcript")).toContainText("finished the task");
await expect(page.getByTestId("live-transcript")).toContainText(
"Summarize what the Phase 4b demo server proves.",
"Summarize what the Live console demo server proves.",
);
await expect(page.getByTestId("tool-item")).toHaveAttribute("data-status", "completed");
await shot(page, "desktop-03-turn-completed");
@@ -364,7 +357,7 @@ test.describe("Phase 4b live console", () => {
});
});
test.describe("Phase 4b live console on a small screen", () => {
test.describe("Live console on a small screen", () => {
test("keeps the transcript and composer usable at 390 by 844", async ({ page }) => {
await page.setViewportSize(MOBILE);
await openConsole(page);
@@ -16,11 +16,11 @@ export default defineConfig({
env: {
// Keep streamed chunks slow enough that interrupt races are reachable
// from a test, and fast enough that the suite stays quick.
PAPERCLIP_PHASE4B_CHUNK_DELAY_MS: "60",
PAPERCLIP_LIVE_CONSOLE_CHUNK_DELAY_MS: "60",
// One server serves the whole suite, and every test leaves its session
// behind when its page closes, so the interactive default of 16 is
// exhausted partway through.
PAPERCLIP_PHASE4B_MAX_SESSIONS: "64",
PAPERCLIP_LIVE_CONSOLE_MAX_SESSIONS: "64",
},
url: "http://127.0.0.1:4179",
reuseExistingServer: false,
@@ -1,24 +1,24 @@
import { useMemo, useRef, useState } from "react";
import type { PrpEvent, PrpRequest } from "../../../src/protocol/phase1-contract";
import type { PrpEvent, PrpRequest } from "../../../src/protocol/replay-contract";
import type { SessionSnapshot } from "../../../src/reducer/session-reducer";
import type {
Phase2RunMetadata,
Phase2RunTrace,
Phase2Scenario,
} from "../../../src/contracts/phase2";
LocalRunnerRunMetadata,
LocalRunnerRunTrace,
LocalRunnerScenario,
} from "../../../src/contracts/local-runner";
import {
PHASE3_FAULTS,
type Phase3Fault,
type Phase3RunTrace,
} from "../../../src/contracts/phase3";
DURABLE_RECOVERY_FAULTS,
type DurableRecoveryFault,
type DurableRecoveryRunTrace,
} from "../../../src/contracts/durable-recovery";
import {
applyPhase2LiveEvent,
createPhase2LiveSnapshot,
phase2SnapshotsMatch,
replayPhase2Events,
} from "../../../src/tracer/phase2-live";
import { replayPhase1FixtureText } from "../../../src/tracer/phase1-replay";
applyLocalRunnerLiveEvent,
createLocalRunnerLiveSnapshot,
localRunnerSnapshotsMatch,
replayLocalRunnerEvents,
} from "../../../src/tracer/local-runner-live";
import { replayReplayFixtureText } from "../../../src/tracer/replay";
import { Badge } from "./components/ui/badge";
import { Button } from "./components/ui/button";
import {
@@ -32,7 +32,7 @@ import { Textarea } from "./components/ui/textarea";
import { LiveConsole } from "./live/LiveConsole";
const fixtureFiles = import.meta.glob(
"../../../protocol/fixtures/phase-01/*.json",
"../../../protocol/fixtures/replay/*.json",
{ eager: true, query: "?raw", import: "default" },
) as Record<string, string>;
@@ -46,7 +46,7 @@ const fixtureOrder = [
"unsupported-required-version",
];
const liveScenarios: Array<{ key: Phase2Scenario; label: string }> = [
const liveScenarios: Array<{ key: LocalRunnerScenario; label: string }> = [
{ key: "happy-path", label: "Happy path" },
{ key: "permission-input", label: "Permission and input" },
{ key: "interrupted", label: "Interruption" },
@@ -57,7 +57,7 @@ const liveScenarios: Array<{ key: Phase2Scenario; label: string }> = [
type BrowserMode = "console" | "recovery" | "live" | "replay";
type LiveStatus = "idle" | "starting" | "running" | "terminal" | "error";
const phase3FaultLabels: Record<Phase3Fault, string> = {
const durableRecoveryFaultLabels: Record<DurableRecoveryFault, string> = {
none: "Normal connection",
"socket-drop": "Socket drop",
"lost-ack": "Lost ACK",
@@ -76,25 +76,25 @@ const modeCopy: Record<
{ eyebrow: string; title: string; description: string }
> = {
console: {
eyebrow: "Paperclip Runner Protocol · Phase 4b",
eyebrow: "Paperclip Runner Protocol · Live console",
title: "Live Codex protocol console",
description:
"Chat with a live session, steer it, stop it, answer its requests, and inspect the canonical protocol behind every surface.",
},
recovery: {
eyebrow: "Paperclip Runner Protocol · Phase 3",
eyebrow: "Paperclip Runner Protocol · Durable recovery",
title: "Durable recovery diagnostics",
description:
"Break the outbound transport, recover the same session, and inspect durable state.",
},
live: {
eyebrow: "Paperclip Runner Protocol · Phase 2",
eyebrow: "Paperclip Runner Protocol · Local runner",
title: "Live runner diagnostics",
description:
"Run the local harness, stream validated events, and compare the live and replayed state.",
},
replay: {
eyebrow: "Paperclip Runner Protocol · Phase 1",
eyebrow: "Paperclip Runner Protocol · Replay",
title: "Static protocol replay",
description:
"Validate a protocol fixture, replay its events, and inspect the resulting session state.",
@@ -107,10 +107,10 @@ interface BrowserFixture {
source: string;
}
type Phase2StreamRecord =
type LocalRunnerStreamRecord =
| { kind: "event"; event: unknown }
| { kind: "diagnostic"; message: string }
| { kind: "trace"; trace: Phase2RunTrace }
| { kind: "trace"; trace: LocalRunnerRunTrace }
| { kind: "error"; message: string };
const browserFixtures = Object.entries(fixtureFiles)
@@ -139,7 +139,7 @@ function humanizeProtocolLabel(value: string): string {
return words.length === 0 ? words : `${words[0]?.toUpperCase()}${words.slice(1)}`;
}
function recoveryOutcomeTone(outcome: Phase3RunTrace["diagnostics"]["recovery"]["outcome"]) {
function recoveryOutcomeTone(outcome: DurableRecoveryRunTrace["diagnostics"]["recovery"]["outcome"]) {
if (outcome === "recovered") {
return "success" as const;
}
@@ -150,8 +150,8 @@ function recoveryOutcomeTone(outcome: Phase3RunTrace["diagnostics"]["recovery"][
}
function RecoveryDiagnostics() {
const [fault, setFault] = useState<Phase3Fault>("lost-ack");
const [trace, setTrace] = useState<Phase3RunTrace | null>(null);
const [fault, setFault] = useState<DurableRecoveryFault>("lost-ack");
const [trace, setTrace] = useState<DurableRecoveryRunTrace | null>(null);
const [status, setStatus] = useState<"idle" | "running" | "complete" | "error">("idle");
const [error, setError] = useState<string | null>(null);
@@ -159,12 +159,12 @@ function RecoveryDiagnostics() {
setStatus("running");
setError(null);
try {
const response = await fetch("/api/phase3/recovery", {
const response = await fetch("/api/durableRecovery/recovery", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ fault }),
});
const result = (await response.json()) as Phase3RunTrace & { error?: string };
const result = (await response.json()) as DurableRecoveryRunTrace & { error?: string };
if (!response.ok) throw new Error(result.error ?? "The recovery trace failed.");
setTrace(result);
setStatus("complete");
@@ -189,16 +189,16 @@ function RecoveryDiagnostics() {
id="recovery-fault"
value={fault}
disabled={status === "running"}
onChange={(event) => setFault(event.target.value as Phase3Fault)}
onChange={(event) => setFault(event.target.value as DurableRecoveryFault)}
>
{PHASE3_FAULTS.map((candidate) => (
{DURABLE_RECOVERY_FAULTS.map((candidate) => (
<option key={candidate} value={candidate}>
{phase3FaultLabels[candidate]}
{durableRecoveryFaultLabels[candidate]}
</option>
))}
</select>
<Button type="button" disabled={status === "running"} onClick={() => void runRecovery()}>
{status === "running" ? "Running recovery…" : "Run Phase 3 recovery"}
{status === "running" ? "Running recovery…" : "Run Durable recovery"}
</Button>
<div className="live-status" aria-live="polite">
<span>Recovery status</span>
@@ -223,22 +223,22 @@ function RecoveryDiagnostics() {
<div className="empty-live-state"><p>Run the trace to collect recovery diagnostics.</p></div>
) : (
<>
<dl className="recovery-grid" data-testid="phase3-diagnostics">
<dl className="recovery-grid" data-testid="durable-recovery-diagnostics">
<div>
<dt>Connection</dt>
<dd data-testid="phase3-connection-state">
<dd data-testid="durable-recovery-connection-state">
{humanizeProtocolLabel(trace.diagnostics.connection.state)}
</dd>
</div>
<div>
<dt>Connections</dt>
<dd data-testid="phase3-connections">
<dd data-testid="durable-recovery-connections">
{trace.diagnostics.connection.connectionCount}
</dd>
</div>
<div>
<dt>Reconnects</dt>
<dd data-testid="phase3-reconnects">
<dd data-testid="durable-recovery-reconnects">
{trace.diagnostics.connection.reconnectCount}
</dd>
</div>
@@ -260,7 +260,7 @@ function RecoveryDiagnostics() {
</div>
<div>
<dt>Runner restarts</dt>
<dd data-testid="phase3-runner-restarts">
<dd data-testid="durable-recovery-runner-restarts">
{trace.diagnostics.recovery.runnerRestarts}
</dd>
</div>
@@ -270,7 +270,7 @@ function RecoveryDiagnostics() {
</div>
<div>
<dt>Fresh bootstraps</dt>
<dd data-testid="phase3-fresh-bootstraps">
<dd data-testid="durable-recovery-fresh-bootstraps">
{trace.diagnostics.recovery.freshBootstraps}
</dd>
</div>
@@ -280,7 +280,7 @@ function RecoveryDiagnostics() {
</div>
<div>
<dt>Storage</dt>
<dd className="storage-diagnostic" data-testid="phase3-storage">
<dd className="storage-diagnostic" data-testid="durable-recovery-storage">
<span>
{trace.diagnostics.outbox.bytes} bytes of {trace.diagnostics.outbox.maxBytes}{" "}
max
@@ -299,7 +299,7 @@ function RecoveryDiagnostics() {
<dd className="outcome-diagnostic">
<Badge
tone={recoveryOutcomeTone(trace.diagnostics.recovery.outcome)}
data-testid="phase3-outcome"
data-testid="durable-recovery-outcome"
>
{humanizeProtocolLabel(trace.diagnostics.recovery.outcome)}
</Badge>
@@ -481,15 +481,15 @@ function pendingRuntimeRequest(events: readonly PrpEvent[]): PrpRequest | null {
}
function LiveRunner() {
const [scenario, setScenario] = useState<Phase2Scenario>("happy-path");
const [scenario, setScenario] = useState<LocalRunnerScenario>("happy-path");
const [status, setStatus] = useState<LiveStatus>("idle");
const [runId, setRunId] = useState<string | null>(null);
const [snapshot, setSnapshot] = useState<SessionSnapshot | null>(null);
const [events, setEvents] = useState<PrpEvent[]>([]);
const [error, setError] = useState<string | null>(null);
const [diagnostic, setDiagnostic] = useState<string | null>(null);
const [input, setInput] = useState("phase-02-live-trace");
const [trace, setTrace] = useState<Phase2RunTrace | null>(null);
const [input, setInput] = useState("local-runner-live-trace");
const [trace, setTrace] = useState<LocalRunnerRunTrace | null>(null);
const [parity, setParity] = useState<boolean | null>(null);
const snapshotRef = useRef<SessionSnapshot | null>(null);
const eventsRef = useRef<PrpEvent[]>([]);
@@ -509,26 +509,26 @@ function LiveRunner() {
snapshotRef.current = null;
try {
const startResponse = await fetch("/api/phase2/runs", {
const startResponse = await fetch("/api/localRunner/runs", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ scenario }),
});
const started = (await startResponse.json()) as {
id?: string;
metadata?: Phase2RunMetadata;
metadata?: LocalRunnerRunMetadata;
error?: string;
};
if (!startResponse.ok || started.id === undefined || started.metadata === undefined) {
throw new Error(started.error ?? "The local runner did not start.");
}
setRunId(started.id);
const initial = createPhase2LiveSnapshot(started.metadata);
const initial = createLocalRunnerLiveSnapshot(started.metadata);
setSnapshot(initial);
snapshotRef.current = initial;
setStatus("running");
const streamResponse = await fetch(`/api/phase2/runs/${started.id}/events`);
const streamResponse = await fetch(`/api/localRunner/runs/${started.id}/events`);
if (!streamResponse.ok || streamResponse.body === null) {
throw new Error("The live event stream did not open.");
}
@@ -542,12 +542,12 @@ function LiveRunner() {
buffered = lines.pop() ?? "";
for (const line of lines) {
if (line.trim().length > 0) {
handleRecord(JSON.parse(line) as Phase2StreamRecord, started.metadata);
handleRecord(JSON.parse(line) as LocalRunnerStreamRecord, started.metadata);
}
}
if (part.done) {
if (buffered.trim().length > 0) {
handleRecord(JSON.parse(buffered) as Phase2StreamRecord, started.metadata);
handleRecord(JSON.parse(buffered) as LocalRunnerStreamRecord, started.metadata);
}
break;
}
@@ -558,13 +558,13 @@ function LiveRunner() {
}
}
function handleRecord(record: Phase2StreamRecord, metadata: Phase2RunMetadata) {
function handleRecord(record: LocalRunnerStreamRecord, metadata: LocalRunnerRunMetadata) {
if (record.kind === "event") {
const current = snapshotRef.current;
if (current === null) {
return;
}
const applied = applyPhase2LiveEvent(current, record.event);
const applied = applyLocalRunnerLiveEvent(current, record.event);
if (!applied.ok) {
setStatus("error");
setError(applied.issues.join("; "));
@@ -586,11 +586,11 @@ function LiveRunner() {
return;
}
setTrace(record.trace);
const replay = replayPhase2Events(metadata, record.trace.events);
const replay = replayLocalRunnerEvents(metadata, record.trace.events);
setParity(
snapshotRef.current === null
? false
: phase2SnapshotsMatch(snapshotRef.current, replay),
: localRunnerSnapshotsMatch(snapshotRef.current, replay),
);
setStatus("terminal");
}
@@ -599,7 +599,7 @@ function LiveRunner() {
if (runId === null) {
return;
}
const response = await fetch(`/api/phase2/runs/${runId}/${action}`, {
const response = await fetch(`/api/localRunner/runs/${runId}/${action}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body ?? {}),
@@ -626,7 +626,7 @@ function LiveRunner() {
id="live-scenario"
value={scenario}
disabled={status === "starting" || status === "running"}
onChange={(event) => setScenario(event.target.value as Phase2Scenario)}
onChange={(event) => setScenario(event.target.value as LocalRunnerScenario)}
>
{liveScenarios.map((candidate) => (
<option key={candidate.key} value={candidate.key}>
@@ -737,7 +737,7 @@ function LiveRunner() {
<div className="empty-live-state">
<p className="eyebrow">Validated live snapshot</p>
<h2>Start a local run</h2>
<p>Each incoming event will pass the Phase 1 validator before the reducer applies it.</p>
<p>Each incoming event will pass the Replay validator before the reducer applies it.</p>
</div>
)}
</section>
@@ -748,11 +748,11 @@ function LiveRunner() {
function StaticReplay() {
const defaultFixture = browserFixtures[0];
if (defaultFixture === undefined) {
throw new Error("No Phase 1 fixtures were bundled");
throw new Error("No Replay fixtures were bundled");
}
const [selectedFixture, setSelectedFixture] = useState(defaultFixture.key);
const [source, setSource] = useState(defaultFixture.source);
const [replay, setReplay] = useState(() => replayPhase1FixtureText(defaultFixture.source));
const [replay, setReplay] = useState(() => replayReplayFixtureText(defaultFixture.source));
const selected = useMemo(
() => browserFixtures.find((fixture) => fixture.key === selectedFixture),
[selectedFixture],
@@ -765,7 +765,7 @@ function StaticReplay() {
}
setSelectedFixture(key);
setSource(fixture.source);
setReplay(replayPhase1FixtureText(fixture.source));
setReplay(replayReplayFixtureText(fixture.source));
}
return (
@@ -799,7 +799,7 @@ function StaticReplay() {
/>
<div className="editor-actions">
<span>{selected?.label ?? "Edited fixture"}</span>
<Button type="button" onClick={() => setReplay(replayPhase1FixtureText(source))}>
<Button type="button" onClick={() => setReplay(replayReplayFixtureText(source))}>
Validate &amp; replay
</Button>
</div>
@@ -828,8 +828,8 @@ function StaticReplay() {
}
export function App() {
// Phase 1-3 surfaces keep their landing mode: their screenshots are frozen
// QA evidence and their specs open on the Phase 2 runner.
// Replay-3 surfaces keep their landing mode: their screenshots are frozen
// QA evidence and their specs open on the Local runner runner.
const [mode, setMode] = useState<BrowserMode>("live");
const header = modeCopy[mode];
@@ -1,6 +1,6 @@
import * as React from "react";
import type { PrpEvent } from "../../../../src/protocol/phase1-contract";
import type { PrpEvent } from "../../../../src/protocol/replay-contract";
import type { SessionSnapshot } from "../../../../src/reducer/session-reducer";
import { Badge } from "../components/ui/badge";
import { Tabs } from "../components/ui/tabs";
@@ -74,13 +74,13 @@ export function LiveConsole() {
const compact = useCompactLayout();
React.useEffect(() => {
const stored = window.localStorage.getItem("paperclip-runner.phase4b.inspector");
const stored = window.localStorage.getItem("paperclip-runner.live-console.inspector");
if (stored === "open") setInspectorOpen(true);
}, []);
React.useEffect(() => {
window.localStorage.setItem(
"paperclip-runner.phase4b.inspector",
"paperclip-runner.live-console.inspector",
inspectorOpen ? "open" : "closed",
);
}, [inspectorOpen]);
@@ -1,11 +1,11 @@
import type { PrpEvent } from "../../../../src/protocol/phase1-contract";
import type { PrpEvent } from "../../../../src/protocol/replay-contract";
import type {
GoalOperation,
LiveManifestSummary,
LiveSessionState,
} from "./protocol";
const BASE = "/api/phase4b";
const BASE = "/api/liveConsole";
export class LiveConsoleError extends Error {
readonly status: number;
@@ -1,4 +1,4 @@
import type { PrpEvent } from "../../../../src/protocol/phase1-contract";
import type { PrpEvent } from "../../../../src/protocol/replay-contract";
import type { SessionSnapshot } from "../../../../src/reducer/session-reducer";
/**
@@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
import { Phase4bScriptedDriver } from "../../../../src/mock-core/phase4b-scripted-driver";
import { LiveConsoleScriptedDriver } from "../../../../src/mock-core/live-console-scripted-driver";
import {
parseHarnessRuntimeRequestResolution,
type HarnessSession,
} from "../../../../src/contracts/harness-driver";
import type { PrpEvent } from "../../../../src/protocol/phase1-contract";
import type { PrpEvent } from "../../../../src/protocol/replay-contract";
import {
applyPrpEvent,
createSessionSnapshotFromMetadata,
@@ -35,21 +35,21 @@ const FULL: LiveCapabilities = {
function seed(runId: string, sessionId: string): SessionSnapshot {
return createSessionSnapshotFromMetadata({
fixtureName: "phase4b-demo",
fixtureName: "live-console-demo",
identity: {
schema: "paperclip.prp.identity.v1",
companyId: "package-local-demo",
issueId: "phase4b-demo",
issueId: "live-console-demo",
runId,
environmentLeaseId: "package-local",
runnerInstanceId: "phase4b-demo-server",
runnerInstanceId: "live-console-demo-server",
normalizedSessionId: sessionId,
driverSessionId: `thread-${sessionId}`,
},
capabilities: {
schema: "paperclip.prp.capabilities.v1",
sessionReusePolicy: "reuse_per_issue",
driver: { kind: "phase4b-demo", version: "1" },
driver: { kind: "live-console-demo", version: "1" },
steer: true,
interrupt: true,
resume: true,
@@ -64,7 +64,7 @@ async function drive(
manifestId: string,
interact?: (session: HarnessSession, turnId: string) => Promise<void>,
): Promise<{ events: PrpEvent[]; snapshot: SessionSnapshot }> {
const driver = new Phase4bScriptedDriver({ manifestId, chunkDelayMs: 0 });
const driver = new LiveConsoleScriptedDriver({ manifestId, chunkDelayMs: 0 });
const session = await driver.openSession({
runId: `run-${manifestId}`,
normalizedSessionId: `session-${manifestId}`,
@@ -114,7 +114,7 @@ describe("buildTranscript", () => {
const user = entries[0];
expect(user.kind === "user" && user.text).toBe("Go.");
const tool = entries[2];
expect(tool.kind === "item" && tool.input).toBe("docs/phase-04b-protocol-server.md");
expect(tool.kind === "item" && tool.input).toBe("docs/live-console-protocol-server.md");
expect(tool.kind === "item" && tool.output).toContain("server-sent events");
});
@@ -1,4 +1,4 @@
import type { PrpEvent } from "../../../../src/protocol/phase1-contract";
import type { PrpEvent } from "../../../../src/protocol/replay-contract";
import type {
SessionItemSnapshot,
SessionSnapshot,
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { PrpEvent } from "../../../../src/protocol/phase1-contract";
import type { PrpEvent } from "../../../../src/protocol/replay-contract";
import {
applyPrpEvent,
createSessionSnapshotFromMetadata,
@@ -20,7 +20,7 @@ import {
type TranscriptEntry,
} from "./transcript-model";
const SESSION_STORAGE_KEY = "paperclip-runner.phase4b.session";
const SESSION_STORAGE_KEY = "paperclip-runner.live-console.session";
const RECONNECT_DELAY_MS = 900;
const MAX_RECONNECT_ATTEMPTS = 5;
@@ -338,7 +338,7 @@ export function useLiveConsole(): LiveConsole {
try {
const created = await client.createSession({
manifest: input.manifestId,
objective: manifest?.objective ?? "Run the Phase 4b demo.",
objective: manifest?.objective ?? "Run the Live console demo.",
message: input.message,
});
sessionIdRef.current = created.sessionId;
@@ -15,7 +15,7 @@
--warning-surface: #fff4dc;
--danger: #a02f2f;
--danger-surface: #fceaea;
/* Pending and streaming are not warnings; Phase 4b needs their own pair. */
/* Pending and streaming are not warnings; Live console needs their own pair. */
--accent: #2f5fa8;
--accent-surface: #e9f0fa;
--surface-raised: #fcfcfa;
@@ -118,6 +118,7 @@ p {
}
.mode-switch {
flex-wrap: wrap;
margin-bottom: var(--space-6);
}
@@ -560,7 +561,7 @@ dd {
}
/* ---------------------------------------------------------------------------
Phase 4b live console
Live console
Every value below comes from the token layer above. Adapted shadcn/ui and
AI Elements components carry no visual values of their own.
--------------------------------------------------------------------------- */
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
test("validates and renders the shared Phase 1 fixture", async ({ page }, testInfo) => {
test("validates and renders the shared Replay fixture", async ({ page }, testInfo) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: "Live runner diagnostics" })).toBeVisible();
await page.getByRole("button", { name: "Static replay" }).click();
@@ -12,12 +12,12 @@ test("validates and renders the shared Phase 1 fixture", async ({ page }, testIn
"The scripted run completed successfully.",
);
await page.screenshot({
path: testInfo.outputPath("phase-01-static-replay.png"),
path: testInfo.outputPath("replay-static-replay.png"),
fullPage: true,
});
});
test("shows Phase 3 recovery diagnostics without secrets", async ({ page }, testInfo) => {
test("shows Durable recovery diagnostics without secrets", async ({ page }, testInfo) => {
await page.goto("/");
await page.getByRole("button", { name: "Recovery", exact: true }).click();
await expect(page.getByRole("heading", { name: "Durable recovery diagnostics" })).toBeVisible();
@@ -37,20 +37,20 @@ test("shows Phase 3 recovery diagnostics without secrets", async ({ page }, test
"Revoke",
]);
await fault.selectOption("none");
await page.getByRole("button", { name: "Run Phase 3 recovery" }).click();
await page.getByRole("button", { name: "Run Durable recovery" }).click();
await expect(fault).toBeDisabled();
await expect(page.getByText("complete", { exact: true })).toBeVisible();
await expect(fault).toBeEnabled();
await expect(page.getByTestId("phase3-connection-state")).toHaveText("Stopped");
await expect(page.getByTestId("phase3-connections")).toHaveText("1");
await expect(page.getByTestId("phase3-reconnects")).toHaveText("0");
await expect(page.getByTestId("phase3-runner-restarts")).toHaveText("0");
await expect(page.getByTestId("phase3-fresh-bootstraps")).toHaveText("1");
await expect(page.getByTestId("phase3-diagnostics")).toContainText("At-least-once redeliveries");
await expect(page.getByTestId("phase3-diagnostics")).toContainText("Duplicate commands");
await expect(page.getByTestId("phase3-storage")).toContainText("0 bytes of 65536 max");
await expect(page.getByTestId("phase3-outcome")).toHaveText("Recovered");
await expect(page.getByTestId("phase3-outcome")).toHaveAttribute("data-tone", "success");
await expect(page.getByTestId("durable-recovery-connection-state")).toHaveText("Stopped");
await expect(page.getByTestId("durable-recovery-connections")).toHaveText("1");
await expect(page.getByTestId("durable-recovery-reconnects")).toHaveText("0");
await expect(page.getByTestId("durable-recovery-runner-restarts")).toHaveText("0");
await expect(page.getByTestId("durable-recovery-fresh-bootstraps")).toHaveText("1");
await expect(page.getByTestId("durable-recovery-diagnostics")).toContainText("At-least-once redeliveries");
await expect(page.getByTestId("durable-recovery-diagnostics")).toContainText("Duplicate commands");
await expect(page.getByTestId("durable-recovery-storage")).toContainText("0 bytes of 65536 max");
await expect(page.getByTestId("durable-recovery-outcome")).toHaveText("Recovered");
await expect(page.getByTestId("durable-recovery-outcome")).toHaveAttribute("data-tone", "success");
await expect(page.getByTestId("recovery-history").getByRole("listitem")).toHaveCount(9);
const firstRecoveryEvent = page.getByTestId("recovery-history").getByRole("listitem").first();
await expect(firstRecoveryEvent).toContainText("workspace.ready");
@@ -59,25 +59,25 @@ test("shows Phase 3 recovery diagnostics without secrets", async ({ page }, test
await expect(page.getByText("Yes", { exact: true })).toBeVisible();
await expect(page.locator("body")).not.toContainText("leaseToken");
await expect(page.locator("body")).not.toContainText("bootstrapTicket");
await page.screenshot({ path: testInfo.outputPath("phase-03-recovery-diagnostics.png"), fullPage: true });
await page.screenshot({ path: testInfo.outputPath("durable-recovery-recovery-diagnostics.png"), fullPage: true });
});
test("tones failure outcomes and storage backpressure", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Recovery", exact: true }).click();
const fault = page.getByLabel("Fault", { exact: true });
const run = page.getByRole("button", { name: "Run Phase 3 recovery" });
const run = page.getByRole("button", { name: "Run Durable recovery" });
await fault.selectOption("revoke");
await run.click();
await expect(page.getByTestId("phase3-outcome")).toHaveText("Revoked");
await expect(page.getByTestId("phase3-outcome")).toHaveAttribute("data-tone", "warning");
await expect(page.getByTestId("durable-recovery-outcome")).toHaveText("Revoked");
await expect(page.getByTestId("durable-recovery-outcome")).toHaveAttribute("data-tone", "warning");
await expect(page.getByText("Revoke completed", { exact: true })).toBeVisible();
await fault.selectOption("storage-pressure");
await run.click();
await expect(page.getByTestId("phase3-storage")).toContainText("0 bytes of 16384 max");
await expect(page.getByTestId("phase3-storage").getByText("Backpressure")).toHaveAttribute(
await expect(page.getByTestId("durable-recovery-storage")).toContainText("0 bytes of 16384 max");
await expect(page.getByTestId("durable-recovery-storage").getByText("Backpressure")).toHaveAttribute(
"data-tone",
"warning",
);
@@ -88,7 +88,7 @@ test("keeps recovery diagnostics within the narrow viewport", async ({ page }, t
await page.goto("/");
await page.getByRole("button", { name: "Recovery", exact: true }).click();
await page.getByLabel("Fault", { exact: true }).selectOption("none");
await page.getByRole("button", { name: "Run Phase 3 recovery" }).click();
await page.getByRole("button", { name: "Run Durable recovery" }).click();
await expect(page.getByTestId("recovery-history")).toBeVisible();
expect(
await page.evaluate(
@@ -96,7 +96,7 @@ test("keeps recovery diagnostics within the narrow viewport", async ({ page }, t
),
).toBe(true);
await page.screenshot({
path: testInfo.outputPath("phase-03-recovery-diagnostics-mobile.png"),
path: testInfo.outputPath("durable-recovery-recovery-diagnostics-mobile.png"),
fullPage: true,
});
});
@@ -122,7 +122,7 @@ test("streams a live run and proves replay parity", async ({ page }, testInfo) =
await expect(page.getByTestId("process-facts")).toContainText("done");
await expect(page.getByTestId("parity-result")).toContainText("Match");
await page.screenshot({
path: testInfo.outputPath("phase-02-live-complete.png"),
path: testInfo.outputPath("local-runner-live-complete.png"),
fullPage: true,
});
});
@@ -139,7 +139,7 @@ test("resolves live permission and input requests", async ({ page }, testInfo) =
}).first(),
).toContainText("permission: Allow the fake driver to write its local fixture?");
await page.screenshot({
path: testInfo.outputPath("phase-02-live-permission.png"),
path: testInfo.outputPath("local-runner-live-permission.png"),
fullPage: true,
});
await page.getByRole("button", { name: "Allow" }).click();
@@ -150,7 +150,7 @@ test("resolves live permission and input requests", async ({ page }, testInfo) =
}).first(),
).toContainText("Resolved permission: Allow the fake driver to write its local fixture?");
await page.waitForTimeout(10_500);
await page.getByLabel("Response").fill("phase-02-browser-trace");
await page.getByLabel("Response").fill("local-runner-browser-trace");
await page.getByRole("button", { name: "Send input" }).click();
await expect(page.getByTestId("live-status")).toHaveText("Terminal");
await expect(page.getByTestId("parity-result")).toContainText("Match");
@@ -169,7 +169,7 @@ test("interrupts a live turn without duplicating terminal state", async ({ page
await expect(page.getByTestId("terminal-badge")).toHaveAttribute("data-tone", "neutral");
await expect(page.getByTestId("timeline").getByText("run.terminal")).toHaveCount(1);
await page.screenshot({
path: testInfo.outputPath("phase-02-live-interrupted.png"),
path: testInfo.outputPath("local-runner-live-interrupted.png"),
fullPage: true,
});
});
@@ -2,10 +2,10 @@ import { createRequire } from "node:module";
import { expect, test, type Page } from "@playwright/test";
import { PHASE7_UI_SHOT_SLUGS } from "../../src/issue-thread/fixtures";
import { CAPABILITY_UI_SHOT_SLUGS } from "../../src/issue-thread/fixtures";
import type {
Phase7IssueThreadSnapshot,
Phase7ThreadItem,
CapabilityIssueThreadSnapshot,
CapabilityThreadItem,
} from "../../src/issue-thread/types";
const require = createRequire(import.meta.url);
@@ -50,7 +50,7 @@ async function seriousAxeViolations(page: Page): Promise<AxeViolation[]> {
);
}
test.describe("Phase 7G issue thread", () => {
test.describe("Capability issue thread", () => {
test("loads the bundled sans and mono faces before the thread settles", async ({ page }) => {
await open(page, "thread-baseline");
@@ -437,7 +437,7 @@ test.describe("Phase 7G issue thread", () => {
await expect(page.locator('[data-interaction-id="ix-questions-01"]')).toBeInViewport();
});
for (const slug of PHASE7_UI_SHOT_SLUGS) {
for (const slug of CAPABILITY_UI_SHOT_SLUGS) {
test(`axe reports no serious or critical violation on ${slug} (desktop)`, async ({ page }) => {
await page.setViewportSize(DESKTOP);
await open(page, slug, slug === "debug-panel-open" ? { panel: "authorization" } : {});
@@ -452,17 +452,17 @@ test.describe("Phase 7G issue thread", () => {
}
});
/* ------------------------------------------------------- Phase 7M clean room */
/* ------------------------------------------------------- Capability clean room */
/**
* The clean room is live-only by construction, so these tests stub the package
* session route rather than starting a real Codex process: what is under test
* here is the surface — entry, blank state, evidence-on-demand, identity
* rotation, failure honesty, and the narrow layout — not the tool loop, which
* `smoke:phase7:cleanroom` and the server suite cover against the real thing.
* `smoke:capability:cleanroom` and the server suite cover against the real thing.
*/
const CLEAN_ROOM_API = "**/api/phase7/ui/cleanroom/session*";
const CLEAN_ROOM_API = "**/api/capability/ui/cleanroom/session*";
function cleanRoomView(identifier: string, withTurn: boolean) {
const guard = {
@@ -475,7 +475,7 @@ function cleanRoomView(identifier: string, withTurn: boolean) {
threadAnchorId: null,
};
return {
schema: "paperclip.phase7.issue-thread-view.v1",
schema: "paperclip.capability.issue-thread-view.v1",
sessionId: `session-${identifier}`,
mode: "live",
identity: {
@@ -493,7 +493,7 @@ function cleanRoomView(identifier: string, withTurn: boolean) {
priority: "medium",
assignee: "Mock Agent",
runState: `run-${identifier} · idle`,
scenarioId: "phase7-clean-room",
scenarioId: "capability-clean-room",
fixtureProfile: "clean-room",
},
turns: withTurn
@@ -577,7 +577,7 @@ function cleanRoomPayload(identifier: string, withTurn = false) {
*/
function turnStreamBody(payload: unknown, interim: unknown[] = []): string {
const frames = interim.map((view, index) => ({
schema: "paperclip.phase7.turn-stream.v1",
schema: "paperclip.capability.turn-stream.v1",
type: "frame",
seq: index + 1,
reason: "delta",
@@ -587,7 +587,7 @@ function turnStreamBody(payload: unknown, interim: unknown[] = []): string {
return [
...frames,
{
schema: "paperclip.phase7.turn-stream.v1",
schema: "paperclip.capability.turn-stream.v1",
type: "settled",
seq: frames.length + 1,
payload,
@@ -617,7 +617,7 @@ async function openCleanRoom(page: Page, identifiers: string[] = ["MCK-1000"]) {
await expect(page.locator('[data-thread-state="settled"]')).toBeVisible();
}
test.describe("Phase 7M clean-room chat", () => {
test.describe("Capability clean-room chat", () => {
test("the landing surface offers a clean-room entry that needs no scenario", async ({ page }) => {
await open(page, "thread-baseline");
const entry = page.getByTestId("surface-chat-link");
@@ -660,7 +660,7 @@ test.describe("Phase 7M clean-room chat", () => {
await page.addInitScript(() => window.localStorage.clear());
await page.route(CLEAN_ROOM_API, async (route) => {
const payload = cleanRoomPayload("MCK-1000");
const view = payload.view as Phase7IssueThreadSnapshot;
const view = payload.view as CapabilityIssueThreadSnapshot;
view.evidence.runner = [
{
id: "session-1",
@@ -708,7 +708,7 @@ test.describe("Phase 7M clean-room chat", () => {
test("a sent message renders the live turn it produced", async ({ page }) => {
await openCleanRoom(page);
await page.route("**/api/phase7/ui/message", async (route) => {
await page.route("**/api/capability/ui/message", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/x-ndjson",
@@ -747,7 +747,7 @@ test.describe("Phase 7M clean-room chat", () => {
status: 500,
contentType: "application/json",
body: JSON.stringify({
error: "phase7_issue_thread_unavailable",
error: "capability_issue_thread_unavailable",
message: "paperclip-runnerd exited before the Codex app-server was ready",
}),
});
@@ -807,7 +807,7 @@ test.describe("Phase 7M clean-room chat", () => {
const PENDING_ANNOUNCEMENT = "A request is waiting for your answer.";
function questionsItem(state: "pending" | "answered" | "withdrawn"): Phase7ThreadItem {
function questionsItem(state: "pending" | "answered" | "withdrawn"): CapabilityThreadItem {
return {
kind: "interaction",
id: "item-ix-1",
@@ -846,10 +846,10 @@ function interactionView(
identifier: string,
state: "pending" | "answered",
options: { withdrawn?: boolean; extraTurn?: boolean } = {},
): Phase7IssueThreadSnapshot {
): CapabilityIssueThreadSnapshot {
const view = JSON.parse(
JSON.stringify(cleanRoomView(identifier, true)),
) as Phase7IssueThreadSnapshot;
) as CapabilityIssueThreadSnapshot;
view.turns[0].items.push(questionsItem(options.withdrawn === true ? "withdrawn" : state));
if (options.extraTurn === true) {
view.turns.push({
@@ -914,7 +914,7 @@ test.describe("pending-request live region", () => {
await expect(live).toHaveText(PENDING_ANNOUNCEMENT);
await expect(page.locator('[data-composer-state="waiting"]')).toBeVisible();
await page.route("**/api/phase7/ui/interaction", async (route) => {
await page.route("**/api/capability/ui/interaction", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
@@ -935,7 +935,7 @@ test.describe("pending-request live region", () => {
await expect(live).toHaveText("Your answer was recorded.");
// A later turn must not resurrect the pending guidance either.
await page.route("**/api/phase7/ui/message", async (route) => {
await page.route("**/api/capability/ui/message", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/x-ndjson",
@@ -972,7 +972,7 @@ test.describe("pending-request live region", () => {
const live = page.getByTestId("thread-live-region");
await expect(live).toHaveText(PENDING_ANNOUNCEMENT);
await page.route("**/api/phase7/ui/reconnect", async (route) => {
await page.route("**/api/capability/ui/reconnect", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
@@ -990,7 +990,7 @@ test.describe("pending-request live region", () => {
});
});
/* -------------------------------------------- Phase 7Q streamed live turn */
/* -------------------------------------------- Capability streamed live turn */
/**
* Streaming is proved against the real package server, not a stub: port 4185
@@ -1012,7 +1012,7 @@ async function openStreamingCleanRoom(page: Page): Promise<void> {
await expect(page.locator('[data-thread-state="settled"]')).toBeVisible({ timeout: 60_000 });
}
test.describe("Phase 7Q streamed live turn", () => {
test.describe("Capability streamed live turn", () => {
test("shows sanitized thinking progress before assistant text arrives", async ({ page }) => {
await openStreamingCleanRoom(page);
@@ -1,7 +1,7 @@
import { defineConfig } from "@playwright/test";
/**
* Focused browser suite for the Phase 7G issue thread.
* Focused browser suite for the Capability issue thread.
*
* The suite runs the built app through `vite preview` on loopback. Fake-mode
* routes render from package fixtures, so no provider, runnerd, or Codex
@@ -1,38 +1,38 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import {
PHASE7_DEFAULT_FIXTURE_PROFILE,
phase7IssueThreadFixture,
CAPABILITY_DEFAULT_FIXTURE_PROFILE,
capabilityIssueThreadFixture,
} from "../../../src/issue-thread/fixtures";
import type {
Phase7EvidenceSectionId,
Phase7IssueThreadSnapshot,
CapabilityEvidenceSectionId,
CapabilityIssueThreadSnapshot,
} from "../../../src/issue-thread/types";
import { phase7DenialCount } from "../../../src/issue-thread/types";
import { capabilityDenialCount } from "../../../src/issue-thread/types";
import { Composer } from "./Composer";
import { EvidencePanel } from "./EvidencePanel";
import { IssueHeader } from "./IssueHeader";
import { applyFakeInteractionResponse } from "./fake-store";
import type { Phase7InteractionResponse } from "./InteractionCard";
import type { CapabilityInteractionResponse } from "./InteractionCard";
import {
phase7LiveClient,
capabilityLiveClient,
recallSession,
rememberSession,
type Phase7CleanRoomIdentity,
type CapabilityCleanRoomIdentity,
} from "./live-client";
import { parsePhase7Route, phase7RouteHref, type Phase7Route } from "./route";
import { parseCapabilityRoute, capabilityRouteHref, type CapabilityRoute } from "./route";
import { SurfaceNav } from "./SurfaceNav";
import { TurnGroup } from "./ThreadItems";
const PANEL_OPEN_KEY = "paperclip-runner.phase7.panel.open";
const PANEL_OPEN_KEY = "paperclip-runner.capability.panel.open";
/**
* The clean room keeps its own panel preference. Evidence is collapsed by
* default there on purpose (revision 5: "the default view reads as plain
* chat"), and inheriting an explorer session's opened drawer would quietly
* break that on the very first visit.
*/
const CHAT_PANEL_OPEN_KEY = "paperclip-runner.phase7.chat.panel.open";
const PANEL_WIDTH_KEY = "paperclip-runner.phase7.panel.width";
const CHAT_PANEL_OPEN_KEY = "paperclip-runner.capability.chat.panel.open";
const PANEL_WIDTH_KEY = "paperclip-runner.capability.panel.width";
const PANEL_MIN = 320;
const PANEL_MAX = 640;
/** Play-all cadence for the replay strip (§6); slow enough to read a turn. */
@@ -85,7 +85,7 @@ function describe(cause: unknown): string {
}
/** Resolution copy for a request that settled without this client answering it. */
function settledAnnouncement(snapshot: Phase7IssueThreadSnapshot, interactionId: string): string {
function settledAnnouncement(snapshot: CapabilityIssueThreadSnapshot, interactionId: string): string {
for (const turn of snapshot.turns) {
for (const item of turn.items) {
if (item.kind === "interaction" && item.interactionId === interactionId) {
@@ -96,10 +96,10 @@ function settledAnnouncement(snapshot: Phase7IssueThreadSnapshot, interactionId:
return SETTLED_ANNOUNCEMENT;
}
function useRoute(): Phase7Route {
const [route, setRoute] = useState(() => parsePhase7Route(window.location));
function useRoute(): CapabilityRoute {
const [route, setRoute] = useState(() => parseCapabilityRoute(window.location));
useEffect(() => {
const onChange = () => setRoute(parsePhase7Route(window.location));
const onChange = () => setRoute(parseCapabilityRoute(window.location));
window.addEventListener("hashchange", onChange);
window.addEventListener("popstate", onChange);
return () => {
@@ -127,8 +127,15 @@ export function App() {
const route = useRoute();
const chat = route.surface === "chat";
const layout = useLayout();
const [snapshot, setSnapshot] = useState<Phase7IssueThreadSnapshot | null>(null);
const [identity, setIdentity] = useState<Phase7CleanRoomIdentity | null>(null);
useEffect(() => {
document.title = chat
? "🫧 Mock Paperclip · Issue thread"
: "🧯 Mock Paperclip · Issue thread";
}, [chat]);
const [snapshot, setSnapshot] = useState<CapabilityIssueThreadSnapshot | null>(null);
const [identity, setIdentity] = useState<CapabilityCleanRoomIdentity | null>(null);
/**
* Which surface produced `snapshot`. A hash change commits the new route in
* the same frame that still renders the old snapshot, so without this a
@@ -145,7 +152,7 @@ export function App() {
);
const [panelWidth, setPanelWidth] = useState(() => readStoredNumber(PANEL_WIDTH_KEY, 384));
const [segment, setSegment] = useState<"thread" | "evidence">(route.segment);
const [openSections, setOpenSections] = useState<Phase7EvidenceSectionId[]>(["tools"]);
const [openSections, setOpenSections] = useState<CapabilityEvidenceSectionId[]>(["tools"]);
const [selectedTurnId, setSelectedTurnId] = useState<string | "all">("all");
const [highlightedRecordId, setHighlightedRecordId] = useState<string | null>(route.record);
const [focusInteractionId, setFocusInteractionId] = useState<string | null>(null);
@@ -188,7 +195,7 @@ export function App() {
// cannot start, the surface says so rather than rendering a canned thread.
void (async () => {
try {
const response = await phase7LiveClient.loadCleanRoom(recallSession("cleanroom"));
const response = await capabilityLiveClient.loadCleanRoom(recallSession("cleanroom"));
if (cancelled) return;
rememberSession(response.sessionId, "cleanroom");
setIdentity(response.identity ?? null);
@@ -201,7 +208,7 @@ export function App() {
} else if (route.mode === "live") {
void (async () => {
try {
const response = await phase7LiveClient.load(recallSession());
const response = await capabilityLiveClient.load(recallSession());
if (cancelled) return;
rememberSession(response.sessionId);
setSnapshot(response.view);
@@ -212,7 +219,7 @@ export function App() {
})();
} else {
setIdentity(null);
setSnapshot(phase7IssueThreadFixture(route.shot ?? "thread-baseline", route.fixtureProfile));
setSnapshot(capabilityIssueThreadFixture(route.shot ?? "thread-baseline", route.fixtureProfile));
setSnapshotSurface("issue");
}
return () => {
@@ -230,9 +237,9 @@ export function App() {
setPanelOpen(true);
setSegment("evidence");
setOpenSections((current) =>
current.includes(route.panel as Phase7EvidenceSectionId)
current.includes(route.panel as CapabilityEvidenceSectionId)
? current
: [...current, route.panel as Phase7EvidenceSectionId],
: [...current, route.panel as CapabilityEvidenceSectionId],
);
setHighlightedRecordId(route.record);
}, [route.panel, route.record]);
@@ -330,7 +337,7 @@ export function App() {
);
const openEvidence = useCallback(
(section: Phase7EvidenceSectionId, recordId: string) => {
(section: CapabilityEvidenceSectionId, recordId: string) => {
setPanelOpen(true);
setSegment("evidence");
setSelectedTurnId("all");
@@ -371,11 +378,11 @@ export function App() {
}, []);
const respond = useCallback(
(response: Phase7InteractionResponse) => {
(response: CapabilityInteractionResponse) => {
setSnapshot((current) => {
if (current === null) return current;
if (route.mode === "live") {
void phase7LiveClient
void capabilityLiveClient
.respond(current.sessionId, response.interactionId, response.outcome, response.result)
.then((next) => setSnapshot(next.view))
.catch((cause) => setActionError(describe(cause)));
@@ -418,7 +425,7 @@ export function App() {
setSnapshot((current) =>
current === null ? current : { ...current, composer: { ...current.composer, state: "sending" } },
);
void phase7LiveClient
void capabilityLiveClient
.send(snapshot.sessionId, message, {
signal: controller.signal,
onFrame: (view) => {
@@ -453,7 +460,7 @@ export function App() {
// turn stream keeps rendering and its terminal frame — not this
// response — decides what the stopped turn finally looks like.
const streaming = turnAbortRef.current !== null;
void phase7LiveClient
void capabilityLiveClient
.stop(current.sessionId)
.then((next) => {
if (!streaming && turnAbortRef.current === null) setSnapshot(next.view);
@@ -478,7 +485,7 @@ export function App() {
* server always answers with a new session id and new mock identities, so the
* client's job is only to forget the old ones.
*/
const adoptCleanRoom = useCallback((next: Awaited<ReturnType<typeof phase7LiveClient.newCleanRoom>>) => {
const adoptCleanRoom = useCallback((next: Awaited<ReturnType<typeof capabilityLiveClient.newCleanRoom>>) => {
rememberSession(next.sessionId, "cleanroom");
setIdentity(next.identity ?? null);
setSnapshot(next.view);
@@ -494,7 +501,7 @@ export function App() {
// dropped before the request that retires it.
abandonTurn();
setAnnouncement("Starting a new clean-room chat…");
void phase7LiveClient
void capabilityLiveClient
.newCleanRoom(snapshot?.sessionId ?? null)
.then(adoptCleanRoom)
.catch((cause) => setActionError(describe(cause)));
@@ -504,14 +511,14 @@ export function App() {
setConfirmReset(false);
abandonTurn();
if (chat && snapshot !== null) {
void phase7LiveClient
void capabilityLiveClient
.reset(snapshot.sessionId)
.then(adoptCleanRoom)
.catch((cause) => setActionError(describe(cause)));
return;
}
if (route.mode === "live" && snapshot !== null) {
void phase7LiveClient
void capabilityLiveClient
.reset(snapshot.sessionId)
.then((next) => {
rememberSession(next.sessionId);
@@ -520,13 +527,13 @@ export function App() {
.catch((cause) => setActionError(describe(cause)));
return;
}
setSnapshot(phase7IssueThreadFixture("thread-baseline", route.fixtureProfile));
setSnapshot(capabilityIssueThreadFixture("thread-baseline", route.fixtureProfile));
setAnnouncement("Scenario reset. The mock state is back to its clean seed.");
}, [abandonTurn, adoptCleanRoom, chat, route.fixtureProfile, route.mode, snapshot]);
const retry = useCallback(() => {
if (route.mode === "live" && snapshot !== null) {
void phase7LiveClient
void capabilityLiveClient
.reconnect(snapshot.sessionId)
.then((next) => setSnapshot(next.view))
.catch((cause) => setActionError(describe(cause)));
@@ -570,7 +577,7 @@ export function App() {
const seekReplay = useCallback(
(ordinal: number) => {
setPlaying(false);
window.location.hash = phase7RouteHref(route, { at: ordinal });
window.location.hash = capabilityRouteHref(route, { at: ordinal });
},
[route],
);
@@ -582,7 +589,7 @@ export function App() {
return;
}
const timer = window.setTimeout(() => {
window.location.hash = phase7RouteHref(route, { at: replay.ordinal + 1 });
window.location.hash = capabilityRouteHref(route, { at: replay.ordinal + 1 });
}, REPLAY_STEP_MS);
return () => window.clearTimeout(timer);
}, [playing, replay, route]);
@@ -594,7 +601,7 @@ export function App() {
/* ---------------------------------------------------------------- render */
const denialCount = useMemo(
() => (snapshot === null ? 0 : phase7DenialCount(snapshot.evidence, null)),
() => (snapshot === null ? 0 : capabilityDenialCount(snapshot.evidence, null)),
[snapshot],
);
@@ -678,10 +685,10 @@ export function App() {
if (layout === "segment") setSegment(next ? "evidence" : "thread");
}}
onSelectScenario={(scenario) => {
window.location.hash = phase7RouteHref(route, { fixtureProfile: scenario });
window.location.hash = capabilityRouteHref(route, { fixtureProfile: scenario });
}}
onReplay={() => {
window.location.hash = phase7RouteHref(route, {
window.location.hash = capabilityRouteHref(route, {
shot: "replay-mode",
mode: "replay",
at: 12,
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, type FormEvent } from "react";
import type { Phase7ComposerModel } from "../../../src/issue-thread/types";
import type { CapabilityComposerModel } from "../../../src/issue-thread/types";
/**
* Composer (contract §4). `data-composer-state` is the single test and
@@ -9,7 +9,7 @@ import type { Phase7ComposerModel } from "../../../src/issue-thread/types";
*/
export interface ComposerProps {
model: Phase7ComposerModel;
model: CapabilityComposerModel;
sessionId: string;
onSend: (message: string) => void;
onStop: () => void;
@@ -21,7 +21,7 @@ export interface ComposerProps {
}
function draftKey(sessionId: string): string {
return `paperclip-runner.phase7.draft.${sessionId}`;
return `paperclip-runner.capability.draft.${sessionId}`;
}
function readDraft(sessionId: string): string {
@@ -1,13 +1,13 @@
import { describe, expect, it } from "vitest";
import type { Phase7EvidenceRunnerRecord } from "../../../src/issue-thread/types.js";
import { groupPhase7RunnerEvents } from "./EvidencePanel.js";
import type { CapabilityEvidenceRunnerRecord } from "../../../src/issue-thread/types.js";
import { groupCapabilityRunnerEvents } from "./EvidencePanel.js";
function record(
ordinal: number,
event: string,
turnId = "turn-1",
): Phase7EvidenceRunnerRecord {
): CapabilityEvidenceRunnerRecord {
return {
id: `event-${ordinal}`,
turnId,
@@ -20,7 +20,7 @@ function record(
describe("Runner event grouping", () => {
it("aggregates each delta category per turn at its first position", () => {
const session: Phase7EvidenceRunnerRecord = {
const session: CapabilityEvidenceRunnerRecord = {
id: "event-1",
turnId: "turn-0",
kind: "session",
@@ -28,7 +28,7 @@ describe("Runner event grouping", () => {
detail: "session · started",
details: [{ label: "Action", value: "started" }],
};
const groups = groupPhase7RunnerEvents([
const groups = groupCapabilityRunnerEvents([
session,
record(2, "assistant_delta"),
record(3, "reasoning_delta"),
@@ -45,7 +45,7 @@ describe("Runner event grouping", () => {
});
it("leaves non-delta events individually expandable", () => {
const groups = groupPhase7RunnerEvents([
const groups = groupCapabilityRunnerEvents([
record(1, "turn_started"),
record(2, "tool_result"),
]);
@@ -1,15 +1,15 @@
import { useEffect, useRef, type ReactNode } from "react";
import type {
Phase7EvidenceModel,
Phase7EvidenceRunnerRecord,
Phase7EvidenceSectionId,
Phase7IssueThreadSnapshot,
Phase7ToolDisposition,
CapabilityEvidenceModel,
CapabilityEvidenceRunnerRecord,
CapabilityEvidenceSectionId,
CapabilityIssueThreadSnapshot,
CapabilityToolDisposition,
} from "../../../src/issue-thread/types";
import {
PHASE7_EVIDENCE_SECTIONS,
phase7DispositionLabel,
CAPABILITY_EVIDENCE_SECTIONS,
capabilityDispositionLabel,
} from "../../../src/issue-thread/types";
/**
@@ -18,16 +18,16 @@ import {
* no verdicts of its own.
*/
const DISPOSITION_ORDER: Phase7ToolDisposition[] = [
const DISPOSITION_ORDER: CapabilityToolDisposition[] = [
"always_agent_tool",
"optional_agent_tool",
"control_plane_owned",
];
export interface Phase7RunnerEventGroup {
export interface CapabilityRunnerEventGroup {
id: string;
event: string | null;
records: Phase7EvidenceRunnerRecord[];
records: CapabilityEvidenceRunnerRecord[];
}
/**
@@ -35,11 +35,11 @@ export interface Phase7RunnerEventGroup {
* Aggregate each sanitized delta category within a turn at its first position;
* expanding the group still exposes every individual event in ordinal order.
*/
export function groupPhase7RunnerEvents(
records: readonly Phase7EvidenceRunnerRecord[],
): Phase7RunnerEventGroup[] {
const groups: Phase7RunnerEventGroup[] = [];
const deltaGroups = new Map<string, Phase7RunnerEventGroup>();
export function groupCapabilityRunnerEvents(
records: readonly CapabilityEvidenceRunnerRecord[],
): CapabilityRunnerEventGroup[] {
const groups: CapabilityRunnerEventGroup[] = [];
const deltaGroups = new Map<string, CapabilityRunnerEventGroup>();
for (const record of records) {
const event = record.details.find((entry) => entry.label === "Event")?.value ?? null;
if (event === null || !event.endsWith("_delta")) {
@@ -59,7 +59,7 @@ export function groupPhase7RunnerEvents(
return groups;
}
function RunnerEvent({ record }: { record: Phase7EvidenceRunnerRecord }) {
function RunnerEvent({ record }: { record: CapabilityEvidenceRunnerRecord }) {
return (
<details className="pit-runner-event" data-record-id={record.id}>
<summary className="pit-runner-event-summary">
@@ -86,7 +86,7 @@ function RunnerEvent({ record }: { record: Phase7EvidenceRunnerRecord }) {
);
}
function RunnerEventGroup({ group }: { group: Phase7RunnerEventGroup }) {
function RunnerEventGroup({ group }: { group: CapabilityRunnerEventGroup }) {
if (group.event === null) return <RunnerEvent record={group.records[0]!} />;
return (
<details className="pit-runner-group" data-runner-delta-group={group.event}>
@@ -105,21 +105,21 @@ function RunnerEventGroup({ group }: { group: Phase7RunnerEventGroup }) {
}
export interface EvidencePanelProps {
snapshot: Phase7IssueThreadSnapshot;
snapshot: CapabilityIssueThreadSnapshot;
layout: "side" | "overlay" | "segment";
width: number;
selectedTurnId: string | "all";
openSections: Phase7EvidenceSectionId[];
openSections: CapabilityEvidenceSectionId[];
highlightedRecordId: string | null;
onSelectTurn: (turnId: string | "all") => void;
onToggleSection: (section: Phase7EvidenceSectionId) => void;
onToggleSection: (section: CapabilityEvidenceSectionId) => void;
onClose: () => void;
onJumpToThread: (anchorId: string) => void;
}
function sectionCount(
evidence: Phase7EvidenceModel,
section: Phase7EvidenceSectionId,
evidence: CapabilityEvidenceModel,
section: CapabilityEvidenceSectionId,
turnId: string | "all",
): string {
if (section === "parity") {
@@ -149,7 +149,7 @@ function Section({
onToggle,
children,
}: {
id: Phase7EvidenceSectionId;
id: CapabilityEvidenceSectionId;
title: string;
count: string;
open: boolean;
@@ -214,7 +214,7 @@ export function EvidencePanel(props: EvidencePanelProps) {
return () => document.removeEventListener("keydown", onKeyDown);
}, [layout, onClose]);
const isOpen = (section: Phase7EvidenceSectionId) => openSections.includes(section);
const isOpen = (section: CapabilityEvidenceSectionId) => openSections.includes(section);
return (
<aside
@@ -268,7 +268,7 @@ export function EvidencePanel(props: EvidencePanelProps) {
<p className="pit-tool-group-label" data-group={disposition}>
{disposition === "control_plane_owned"
? "Control plane (not exposed to the agent)"
: phase7DispositionLabel(disposition)}
: capabilityDispositionLabel(disposition)}
</p>
{rows.map((row) => (
<p
@@ -398,7 +398,7 @@ export function EvidencePanel(props: EvidencePanelProps) {
open={isOpen("runner")}
onToggle={() => onToggleSection("runner")}
>
{groupPhase7RunnerEvents(filterByTurn(evidence.runner, selectedTurnId)).map((group) => (
{groupCapabilityRunnerEvents(filterByTurn(evidence.runner, selectedTurnId)).map((group) => (
<RunnerEventGroup key={group.id} group={group} />
))}
</Section>
@@ -487,6 +487,6 @@ export function EvidencePanel(props: EvidencePanelProps) {
);
}
export const PHASE7_EVIDENCE_SECTION_IDS = PHASE7_EVIDENCE_SECTIONS.map(
export const CAPABILITY_EVIDENCE_SECTION_IDS = CAPABILITY_EVIDENCE_SECTIONS.map(
(section) => section.id,
);
@@ -1,8 +1,8 @@
import { useEffect, useId, useRef, useState } from "react";
import type {
Phase7ThreadInteractionCard,
Phase7ThreadInteractionState,
CapabilityThreadInteractionCard,
CapabilityThreadInteractionState,
} from "../../../src/issue-thread/types";
import { Chip } from "./primitives";
@@ -17,13 +17,13 @@ import { Chip } from "./primitives";
* resolution here.
*/
export interface Phase7InteractionResponse {
export interface CapabilityInteractionResponse {
interactionId: string;
outcome: "answered" | "accepted" | "rejected";
result: Record<string, unknown>;
}
const STATE_GLYPH: Record<Phase7ThreadInteractionState, string> = {
const STATE_GLYPH: Record<CapabilityThreadInteractionState, string> = {
pending: "⏳",
submitting: "⏳",
accepted: "✓",
@@ -36,7 +36,7 @@ const STATE_GLYPH: Record<Phase7ThreadInteractionState, string> = {
issue_closed: "⌛",
};
const RESOLVED_STATES = new Set<Phase7ThreadInteractionState>([
const RESOLVED_STATES = new Set<CapabilityThreadInteractionState>([
"accepted",
"answered",
"rejected",
@@ -47,7 +47,7 @@ const RESOLVED_STATES = new Set<Phase7ThreadInteractionState>([
"issue_closed",
]);
function stateTone(state: Phase7ThreadInteractionState) {
function stateTone(state: CapabilityThreadInteractionState) {
if (state === "accepted" || state === "answered") return "success" as const;
if (state === "rejected") return "danger" as const;
if (state === "pending" || state === "submitting") return "accent" as const;
@@ -60,8 +60,8 @@ export function InteractionCard({
onOpenEvidence,
autoFocus,
}: {
card: Phase7ThreadInteractionCard;
onRespond?: (response: Phase7InteractionResponse) => void;
card: CapabilityThreadInteractionCard;
onRespond?: (response: CapabilityInteractionResponse) => void;
onOpenEvidence?: (section: string, recordId: string) => void;
autoFocus?: boolean;
}) {
@@ -80,7 +80,7 @@ export function InteractionCard({
const [error, setError] = useState<string | null>(null);
const firstControl = useRef<HTMLElement | null>(null);
const stateChip = useRef<HTMLSpanElement | null>(null);
const previousState = useRef<Phase7ThreadInteractionState>(card.state);
const previousState = useRef<CapabilityThreadInteractionState>(card.state);
const resolved = RESOLVED_STATES.has(card.state);
const busy = card.state === "submitting";
@@ -96,7 +96,7 @@ export function InteractionCard({
}
}, [card.state]);
function respond(response: Phase7InteractionResponse) {
function respond(response: CapabilityInteractionResponse) {
setError(null);
onRespond?.(response);
}
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from "react";
import type { Phase7IssueThreadSnapshot } from "../../../src/issue-thread/types";
import type { CapabilityIssueThreadSnapshot } from "../../../src/issue-thread/types";
import { Chip, PriorityIcon, StatusBadge } from "./primitives";
/**
@@ -10,7 +10,7 @@ import { Chip, PriorityIcon, StatusBadge } from "./primitives";
*/
export interface IssueHeaderProps {
snapshot: Phase7IssueThreadSnapshot;
snapshot: CapabilityIssueThreadSnapshot;
scenarios: string[];
/** `chat` is the clean room: no preset scenario, no recording to replay. */
surface?: "issue" | "chat";
@@ -1,9 +1,9 @@
import type { Phase7Surface } from "./route";
import type { CapabilitySurface } from "./route";
/**
* Top-level surface switch (Phase 7 plan revision 5).
* Top-level surface switch (Capability plan revision 5).
*
* Phase 7 now has two primary paths, not one path with a mode toggle: the
* Capability now has two primary paths, not one path with a mode toggle: the
* preset scenario explorer and a clean-room chat that starts from nothing. They
* are different jobs, so they get links rather than tabs — the clean-room entry
* has to be reachable from the landing page without first choosing a scenario.
@@ -11,9 +11,9 @@ import type { Phase7Surface } from "./route";
* These are real anchors on purpose. The entry must survive a hard refresh, be
* copyable, and open in a new tab, which a button handler would not give.
*/
export function SurfaceNav({ surface }: { surface: Phase7Surface }) {
export function SurfaceNav({ surface }: { surface: CapabilitySurface }) {
return (
<nav className="pit-surface-nav" aria-label="Phase 7 surfaces" data-testid="surface-nav">
<nav className="pit-surface-nav" aria-label="Capability surfaces" data-testid="surface-nav">
<a
className="pit-surface-link"
href="#/issue/hb-baseline"
@@ -1,11 +1,11 @@
import { useState } from "react";
import type {
Phase7EvidenceSectionId,
Phase7ThreadItem,
Phase7ThreadTurn,
CapabilityEvidenceSectionId,
CapabilityThreadItem,
CapabilityThreadTurn,
} from "../../../src/issue-thread/types";
import { InteractionCard, type Phase7InteractionResponse } from "./InteractionCard";
import { InteractionCard, type CapabilityInteractionResponse } from "./InteractionCard";
import { Chip, StatusBadge, Timestamp, formatBytes } from "./primitives";
/** Progressive disclosure budget from contract §3 (T4). */
@@ -14,8 +14,8 @@ const VISIBLE_STRIPS = 3;
const STATUS_GLYPH = { ok: "✓", denied: "✕", running: "⏳" } as const;
export interface ThreadCallbacks {
onOpenEvidence: (section: Phase7EvidenceSectionId, recordId: string) => void;
onRespond: (response: Phase7InteractionResponse) => void;
onOpenEvidence: (section: CapabilityEvidenceSectionId, recordId: string) => void;
onRespond: (response: CapabilityInteractionResponse) => void;
focusInteractionId: string | null;
}
@@ -23,7 +23,7 @@ function ToolStrip({
item,
callbacks,
}: {
item: Extract<Phase7ThreadItem, { kind: "tool_activity" }>;
item: Extract<CapabilityThreadItem, { kind: "tool_activity" }>;
callbacks: ThreadCallbacks;
}) {
return (
@@ -66,7 +66,7 @@ function ToolStrip({
function ProgressItem({
item,
}: {
item: Extract<Phase7ThreadItem, { kind: "progress_activity" }>;
item: Extract<CapabilityThreadItem, { kind: "progress_activity" }>;
}) {
return (
<details
@@ -99,7 +99,7 @@ function ThreadItemView({
item,
callbacks,
}: {
item: Phase7ThreadItem;
item: CapabilityThreadItem;
callbacks: ThreadCallbacks;
}) {
switch (item.kind) {
@@ -208,7 +208,7 @@ function ThreadItemView({
autoFocus={callbacks.focusInteractionId === item.interactionId}
onRespond={callbacks.onRespond}
onOpenEvidence={(section, recordId) =>
callbacks.onOpenEvidence(section as Phase7EvidenceSectionId, recordId)
callbacks.onOpenEvidence(section as CapabilityEvidenceSectionId, recordId)
}
/>
);
@@ -331,7 +331,7 @@ export function TurnGroup({
turn,
callbacks,
}: {
turn: Phase7ThreadTurn;
turn: CapabilityThreadTurn;
callbacks: ThreadCallbacks;
}) {
const [showAllStrips, setShowAllStrips] = useState(false);
@@ -1,9 +1,9 @@
import type {
Phase7IssueThreadSnapshot,
Phase7ThreadInteractionState,
Phase7ThreadItem,
CapabilityIssueThreadSnapshot,
CapabilityThreadInteractionState,
CapabilityThreadItem,
} from "../../../src/issue-thread/types";
import type { Phase7InteractionResponse } from "./InteractionCard";
import type { CapabilityInteractionResponse } from "./InteractionCard";
/**
* `fake`-mode interaction store.
@@ -17,14 +17,14 @@ import type { Phase7InteractionResponse } from "./InteractionCard";
*/
function resolvedState(
outcome: Phase7InteractionResponse["outcome"],
): { state: Phase7ThreadInteractionState; label: string } {
outcome: CapabilityInteractionResponse["outcome"],
): { state: CapabilityThreadInteractionState; label: string } {
if (outcome === "accepted") return { state: "accepted", label: "Accepted" };
if (outcome === "rejected") return { state: "rejected", label: "Changes requested" };
return { state: "answered", label: "Answered" };
}
function summarise(response: Phase7InteractionResponse): string[] {
function summarise(response: CapabilityInteractionResponse): string[] {
const result = response.result;
if (Array.isArray(result.selected)) return result.selected.map((entry) => String(entry));
if (Array.isArray(result.acceptedTaskIds)) {
@@ -46,9 +46,9 @@ function summarise(response: Phase7InteractionResponse): string[] {
}
function applyToItem(
item: Phase7ThreadItem,
response: Phase7InteractionResponse,
): Phase7ThreadItem {
item: CapabilityThreadItem,
response: CapabilityInteractionResponse,
): CapabilityThreadItem {
if (item.kind !== "interaction" || item.interactionId !== response.interactionId) return item;
const view = resolvedState(response.outcome);
return {
@@ -62,9 +62,9 @@ function applyToItem(
/** Store the typed response, then return the snapshot the card renders from. */
export function applyFakeInteractionResponse(
snapshot: Phase7IssueThreadSnapshot,
response: Phase7InteractionResponse,
): Phase7IssueThreadSnapshot {
snapshot: CapabilityIssueThreadSnapshot,
response: CapabilityInteractionResponse,
): CapabilityIssueThreadSnapshot {
const turns = snapshot.turns.map((turn) => ({
...turn,
items: turn.items.map((item) => applyToItem(item, response)),
@@ -1,6 +1,6 @@
# Phase 7 issue-thread fonts
# Capability issue-thread fonts
The Phase 7 issue-thread bundle includes Latin-subset WOFF2 files so captures
The Capability issue-thread bundle includes Latin-subset WOFF2 files so captures
do not depend on fonts installed on the host.
## Inter
@@ -1,5 +1,5 @@
/*
* Phase 7G issue-thread surface.
* Capability issue-thread surface.
*
* Package-local implementation of the Paperclip visual language (contract §8):
* dark default, OKLCH neutral grays, semantic tokens only. Components carry no
@@ -1,14 +1,14 @@
import type { Phase7IssueThreadSnapshot } from "../../../src/issue-thread/types";
import type { CapabilityIssueThreadSnapshot } from "../../../src/issue-thread/types";
import {
PHASE7_TURN_STREAM_ACCEPT,
Phase7TurnStreamError,
readPhase7TurnStream,
} from "../../../src/phase7/turn-stream";
CAPABILITY_TURN_STREAM_ACCEPT,
CapabilityTurnStreamError,
readCapabilityTurnStream,
} from "../../../src/live/turn-stream";
/**
* Browser client for the package session server.
*
* Every response is a server-projected `Phase7IssueThreadSnapshot`. The client
* Every response is a server-projected `CapabilityIssueThreadSnapshot`. The client
* posts intents and renders what comes back; it never patches the snapshot
* locally, which is what keeps policy and state authority on the server
* (contract §11).
@@ -20,7 +20,7 @@ import {
* the settled one is final.
*/
const BASE = "/api/phase7/ui";
const BASE = "/api/capability/ui";
/**
* Every route is session-scoped and mediated by the per-browser capability
@@ -30,7 +30,7 @@ const BASE = "/api/phase7/ui";
*/
const CREDENTIALS = "same-origin" as const;
export interface Phase7CleanRoomIdentity {
export interface CapabilityCleanRoomIdentity {
token: string;
sequence: number;
companyId: string;
@@ -39,12 +39,12 @@ export interface Phase7CleanRoomIdentity {
identifier: string;
}
export interface Phase7LiveResponse {
export interface CapabilityLiveResponse {
sessionId: string;
view: Phase7IssueThreadSnapshot;
view: CapabilityIssueThreadSnapshot;
surface?: "issue" | "cleanroom";
/** Present on the clean-room surface so the UI can show which tenant is live. */
identity?: Phase7CleanRoomIdentity;
identity?: CapabilityCleanRoomIdentity;
limits?: { maxTurns: number; maxMessageBytes: number };
turns?: number;
}
@@ -55,29 +55,29 @@ export interface Phase7LiveResponse {
* lets a failure to start real Codex read as a failure rather than quietly
* degrading into a fixture.
*/
export class Phase7LiveError extends Error {
export class CapabilityLiveError extends Error {
constructor(
readonly code: string,
message: string,
) {
super(message);
this.name = "Phase7LiveError";
this.name = "CapabilityLiveError";
}
}
async function readError(response: Response, path: string): Promise<Phase7LiveError> {
async function readError(response: Response, path: string): Promise<CapabilityLiveError> {
try {
const body = (await response.json()) as { error?: string; message?: string };
return new Phase7LiveError(
return new CapabilityLiveError(
body.error ?? `http_${response.status}`,
body.message ?? `${path} failed with ${response.status}`,
);
} catch {
return new Phase7LiveError(`http_${response.status}`, `${path} failed with ${response.status}`);
return new CapabilityLiveError(`http_${response.status}`, `${path} failed with ${response.status}`);
}
}
async function post(path: string, body: unknown): Promise<Phase7LiveResponse> {
async function post(path: string, body: unknown): Promise<CapabilityLiveResponse> {
const response = await fetch(`${BASE}${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
@@ -85,14 +85,14 @@ async function post(path: string, body: unknown): Promise<Phase7LiveResponse> {
credentials: CREDENTIALS,
});
if (!response.ok) throw await readError(response, path);
return (await response.json()) as Phase7LiveResponse;
return (await response.json()) as CapabilityLiveResponse;
}
/** Interim projection delivered while the turn is still running. */
export type Phase7TurnFrameHandler = (view: Phase7IssueThreadSnapshot) => void;
export type CapabilityTurnFrameHandler = (view: CapabilityIssueThreadSnapshot) => void;
export interface Phase7SendOptions {
onFrame?: Phase7TurnFrameHandler;
export interface CapabilitySendOptions {
onFrame?: CapabilityTurnFrameHandler;
/** Abort the turn stream — used when the surface is torn down or rotated. */
signal?: AbortSignal;
}
@@ -100,11 +100,11 @@ export interface Phase7SendOptions {
async function postTurnStream(
path: string,
body: unknown,
options: Phase7SendOptions,
): Promise<Phase7LiveResponse> {
options: CapabilitySendOptions,
): Promise<CapabilityLiveResponse> {
const response = await fetch(`${BASE}${path}`, {
method: "POST",
headers: { "content-type": "application/json", accept: PHASE7_TURN_STREAM_ACCEPT },
headers: { "content-type": "application/json", accept: CAPABILITY_TURN_STREAM_ACCEPT },
body: JSON.stringify(body),
credentials: CREDENTIALS,
...(options.signal === undefined ? {} : { signal: options.signal }),
@@ -113,37 +113,37 @@ async function postTurnStream(
// because they are decided before the first frame is written.
if (!response.ok) throw await readError(response, path);
try {
return await readPhase7TurnStream<Phase7IssueThreadSnapshot, Phase7LiveResponse>(
return await readCapabilityTurnStream<CapabilityIssueThreadSnapshot, CapabilityLiveResponse>(
response,
(frame) => options.onFrame?.(frame.view),
);
} catch (cause) {
if (cause instanceof Phase7TurnStreamError) {
throw new Phase7LiveError(cause.code, cause.message);
if (cause instanceof CapabilityTurnStreamError) {
throw new CapabilityLiveError(cause.code, cause.message);
}
throw cause;
}
}
export const phase7LiveClient = {
async load(sessionId: string | null): Promise<Phase7LiveResponse> {
export const capabilityLiveClient = {
async load(sessionId: string | null): Promise<CapabilityLiveResponse> {
const query = sessionId === null ? "" : `?sessionId=${encodeURIComponent(sessionId)}`;
const response = await fetch(`${BASE}/session${query}`, { credentials: CREDENTIALS });
if (!response.ok) throw await readError(response, "/session");
return (await response.json()) as Phase7LiveResponse;
return (await response.json()) as CapabilityLiveResponse;
},
create(scenario: string): Promise<Phase7LiveResponse> {
create(scenario: string): Promise<CapabilityLiveResponse> {
return post("/session", { scenario });
},
/** Reconnect to a clean room, or open one when there is nothing to resume. */
async loadCleanRoom(sessionId: string | null): Promise<Phase7LiveResponse> {
async loadCleanRoom(sessionId: string | null): Promise<CapabilityLiveResponse> {
const query = sessionId === null ? "" : `?sessionId=${encodeURIComponent(sessionId)}`;
const response = await fetch(`${BASE}/cleanroom/session${query}`, { credentials: CREDENTIALS });
if (!response.ok) throw await readError(response, "/cleanroom/session");
return (await response.json()) as Phase7LiveResponse;
return (await response.json()) as CapabilityLiveResponse;
},
/** `New chat`: retire the current room and mint a new mock tenant. */
newCleanRoom(sessionId: string | null): Promise<Phase7LiveResponse> {
newCleanRoom(sessionId: string | null): Promise<CapabilityLiveResponse> {
return post("/cleanroom/session", sessionId === null ? {} : { sessionId });
},
/**
@@ -153,17 +153,17 @@ export const phase7LiveClient = {
send(
sessionId: string,
message: string,
options: Phase7SendOptions = {},
): Promise<Phase7LiveResponse> {
options: CapabilitySendOptions = {},
): Promise<CapabilityLiveResponse> {
return postTurnStream("/message", { sessionId, message }, options);
},
stop(sessionId: string): Promise<Phase7LiveResponse> {
stop(sessionId: string): Promise<CapabilityLiveResponse> {
return post("/interrupt", { sessionId });
},
reset(sessionId: string): Promise<Phase7LiveResponse> {
reset(sessionId: string): Promise<CapabilityLiveResponse> {
return post("/reset", { sessionId });
},
reconnect(sessionId: string): Promise<Phase7LiveResponse> {
reconnect(sessionId: string): Promise<CapabilityLiveResponse> {
return post("/reconnect", { sessionId });
},
respond(
@@ -171,18 +171,18 @@ export const phase7LiveClient = {
interactionId: string,
outcome: string,
result: unknown,
): Promise<Phase7LiveResponse> {
): Promise<CapabilityLiveResponse> {
return post("/interaction", { sessionId, interactionId, outcome, result });
},
};
const SESSION_STORAGE_KEY = "paperclip-runner.phase7.session";
const SESSION_STORAGE_KEY = "paperclip-runner.capability.session";
/**
* The clean room keeps its own key. Sharing one would let a scenario session id
* be handed to the clean-room route (and the reverse) after a refresh, which is
* exactly the cross-surface bleed the isolation criterion forbids.
*/
const CLEAN_ROOM_STORAGE_KEY = "paperclip-runner.phase7.cleanroom.session";
const CLEAN_ROOM_STORAGE_KEY = "paperclip-runner.capability.cleanroom.session";
function storageKey(surface: "issue" | "cleanroom"): string {
return surface === "cleanroom" ? CLEAN_ROOM_STORAGE_KEY : SESSION_STORAGE_KEY;
@@ -14,7 +14,7 @@ if (params.get("capture") === "1" || new URLSearchParams(hashQuery).get("capture
const root = document.getElementById("root");
if (root === null) {
throw new Error("Phase 7 issue-thread root element is missing");
throw new Error("Capability issue-thread root element is missing");
}
createRoot(root).render(
@@ -1,6 +1,6 @@
import type { ReactNode, Ref } from "react";
import type { Phase7TaskStatus } from "../../../src/mock-core/phase7-control-plane-types";
import type { CapabilityTaskStatus } from "../../../src/mock-core/capability-control-plane-types";
/**
* Package-local equivalents of the product StatusBadge/chip anatomy. Every
@@ -8,7 +8,7 @@ import type { Phase7TaskStatus } from "../../../src/mock-core/phase7-control-pla
* without color (contract §9.5).
*/
const STATUS_COPY: Record<Phase7TaskStatus, { label: string; glyph: string }> = {
const STATUS_COPY: Record<CapabilityTaskStatus, { label: string; glyph: string }> = {
backlog: { label: "Backlog", glyph: "○" },
todo: { label: "Todo", glyph: "○" },
in_progress: { label: "In progress", glyph: "◐" },
@@ -25,7 +25,7 @@ const PRIORITY_GLYPH: Record<string, string> = {
low: "▼",
};
export function StatusBadge({ status }: { status: Phase7TaskStatus }) {
export function StatusBadge({ status }: { status: CapabilityTaskStatus }) {
const copy = STATUS_COPY[status];
return (
<span className="pit-status-badge" data-status={status}>
@@ -88,4 +88,4 @@ export function Timestamp({ value }: { value: string }) {
}
/** Re-exported so the thread strip and the deliverable card share one unit. */
export { phase7FormatBytes as formatBytes } from "../../../src/issue-thread/types";
export { capabilityFormatBytes as formatBytes } from "../../../src/issue-thread/types";
@@ -1,12 +1,12 @@
import { describe, expect, it } from "vitest";
import { parsePhase7Route, phase7RouteHref } from "./route";
import { parseCapabilityRoute, capabilityRouteHref } from "./route";
function at(hash: string, search = "") {
return parsePhase7Route({ hash, search });
return parseCapabilityRoute({ hash, search });
}
describe("Phase 7 issue-thread routes", () => {
describe("Capability issue-thread routes", () => {
it("keeps the existing deterministic scenario deep links intact", () => {
const route = at("#/issue/dp-documents?shot=document-revision&panel=state&rec=doc-1&at=3&seg=evidence");
@@ -18,7 +18,7 @@ describe("Phase 7 issue-thread routes", () => {
expect(route.at).toBe(3);
expect(route.segment).toBe("evidence");
expect(route.mode).toBe("fake");
expect(phase7RouteHref(route, {})).toBe(
expect(capabilityRouteHref(route, {})).toBe(
"#/issue/dp-documents?shot=document-revision&panel=state&rec=doc-1&at=3&seg=evidence",
);
});
@@ -29,14 +29,14 @@ describe("Phase 7 issue-thread routes", () => {
});
});
describe("Phase 7 clean-room route", () => {
describe("Capability clean-room route", () => {
it("is reachable without naming a scenario", () => {
const route = at("#/chat");
expect(route.surface).toBe("chat");
expect(route.shot).toBe(null);
expect(route.at).toBe(null);
expect(phase7RouteHref(route, {})).toBe("#/chat");
expect(capabilityRouteHref(route, {})).toBe("#/chat");
});
it("is always live: no URL can downgrade it to a fixture or a recording", () => {
@@ -62,13 +62,13 @@ describe("Phase 7 clean-room route", () => {
expect(route.panel).toBe("calls");
expect(route.record).toBe("call-1");
expect(route.segment).toBe("evidence");
expect(phase7RouteHref(route, {})).toBe("#/chat?panel=calls&rec=call-1&seg=evidence");
expect(phase7RouteHref(route, { panel: null, record: null, segment: "thread" })).toBe("#/chat");
expect(capabilityRouteHref(route, {})).toBe("#/chat?panel=calls&rec=call-1&seg=evidence");
expect(capabilityRouteHref(route, { panel: null, record: null, segment: "thread" })).toBe("#/chat");
});
it("switches surfaces without dragging scenario state across", () => {
const chat = at("#/chat");
expect(phase7RouteHref(chat, { surface: "issue", fixtureProfile: "ar-artifacts" })).toBe(
expect(capabilityRouteHref(chat, { surface: "issue", fixtureProfile: "ar-artifacts" })).toBe(
"#/issue/ar-artifacts?mode=live",
);
});
@@ -1,6 +1,6 @@
/**
* Deterministic route scheme from the Phase 7B UX contract §10.1, plus the
* Phase 7M clean-room surface.
* Deterministic route scheme from the Capability UX contract §10.1, plus the
* Capability clean-room surface.
*
* `#/issue/<fixtureProfile>?shot=<slug>&panel=<section>&rec=<id>&at=<ordinal>&seg=thread|evidence`
* `#/chat?panel=<section>&rec=<id>&seg=thread|evidence`
@@ -14,17 +14,17 @@
* with a fixture or a recording.
*/
import type { Phase7EvidenceSectionId } from "../../../src/issue-thread/types";
import { PHASE7_EVIDENCE_SECTIONS } from "../../../src/issue-thread/types";
import { PHASE7_DEFAULT_FIXTURE_PROFILE } from "../../../src/issue-thread/fixtures";
import type { CapabilityEvidenceSectionId } from "../../../src/issue-thread/types";
import { CAPABILITY_EVIDENCE_SECTIONS } from "../../../src/issue-thread/types";
import { CAPABILITY_DEFAULT_FIXTURE_PROFILE } from "../../../src/issue-thread/fixtures";
export type Phase7Surface = "issue" | "chat";
export type CapabilitySurface = "issue" | "chat";
export interface Phase7Route {
surface: Phase7Surface;
export interface CapabilityRoute {
surface: CapabilitySurface;
fixtureProfile: string;
shot: string | null;
panel: Phase7EvidenceSectionId | null;
panel: CapabilityEvidenceSectionId | null;
record: string | null;
at: number | null;
segment: "thread" | "evidence";
@@ -32,7 +32,7 @@ export interface Phase7Route {
mode: "fake" | "live" | "replay";
}
const SECTION_IDS = new Set<string>(PHASE7_EVIDENCE_SECTIONS.map((section) => section.id));
const SECTION_IDS = new Set<string>(CAPABILITY_EVIDENCE_SECTIONS.map((section) => section.id));
function mergeParams(search: string, hash: string): URLSearchParams {
const params = new URLSearchParams(search);
@@ -45,10 +45,10 @@ function mergeParams(search: string, hash: string): URLSearchParams {
return params;
}
export function parsePhase7Route(url: {
export function parseCapabilityRoute(url: {
search: string;
hash: string;
}): Phase7Route {
}): CapabilityRoute {
const params = mergeParams(url.search, url.hash);
const path = url.hash.replace(/^#/, "").split("?")[0] ?? "";
const chat = /^\/chat\/?$/.test(path);
@@ -67,9 +67,9 @@ export function parsePhase7Route(url: {
: "fake";
return {
surface: chat ? "chat" : "issue",
fixtureProfile: match?.[1] ?? PHASE7_DEFAULT_FIXTURE_PROFILE,
fixtureProfile: match?.[1] ?? CAPABILITY_DEFAULT_FIXTURE_PROFILE,
shot,
panel: panel !== null && SECTION_IDS.has(panel) ? (panel as Phase7EvidenceSectionId) : null,
panel: panel !== null && SECTION_IDS.has(panel) ? (panel as CapabilityEvidenceSectionId) : null,
record: params.get("rec"),
at: chat || at === null || !/^\d+$/.test(at) ? null : Number.parseInt(at, 10),
segment: segment === "evidence" ? "evidence" : "thread",
@@ -77,7 +77,7 @@ export function parsePhase7Route(url: {
};
}
export function phase7RouteHref(route: Phase7Route, overrides: Partial<Phase7Route>): string {
export function capabilityRouteHref(route: CapabilityRoute, overrides: Partial<CapabilityRoute>): string {
const next = { ...route, ...overrides };
const params = new URLSearchParams();
if (next.surface === "chat") {
@@ -2,7 +2,7 @@
* Scripted Codex transport for the browser streaming test (track 7Q).
*
* Everything below the provider is the real thing: the same package server
* middleware, the same `Phase7LiveSession`, the same NDJSON turn stream, and
* middleware, the same `CapabilityLiveSession`, the same NDJSON turn stream, and
* the same built browser bundle. Only the provider is scripted, and only so the
* test can decide when a delta arrives — a real Codex process cannot be asked
* to emit exactly four deltas 220 ms apart, and a browser assertion needs that
@@ -15,18 +15,18 @@
*/
const DELTA_INTERVAL_MS = 220;
export const PHASE7_STREAM_FIXTURE_PRIVATE_REASONING =
export const CAPABILITY_STREAM_FIXTURE_PRIVATE_REASONING =
"PRIVATE reasoning text must never reach the browser.";
/** Four deltas: enough for a browser to observe growth twice over, and short. */
export const PHASE7_STREAM_FIXTURE_DELTAS = [
export const CAPABILITY_STREAM_FIXTURE_DELTAS = [
"Reading the clean-room issue. ",
"It is blank, with one mock agent and one mock task. ",
"Recording a first status against the mock control plane. ",
"Done — every record stayed in the mock port.",
];
export const PHASE7_STREAM_FIXTURE_REPLY = PHASE7_STREAM_FIXTURE_DELTAS.join("");
export const CAPABILITY_STREAM_FIXTURE_REPLY = CAPABILITY_STREAM_FIXTURE_DELTAS.join("");
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -122,10 +122,10 @@ class StreamFixtureTransport {
params: {
threadId: "stream-fixture-thread",
turnId,
delta: PHASE7_STREAM_FIXTURE_PRIVATE_REASONING,
delta: CAPABILITY_STREAM_FIXTURE_PRIVATE_REASONING,
},
});
for (const delta of PHASE7_STREAM_FIXTURE_DELTAS) {
for (const delta of CAPABILITY_STREAM_FIXTURE_DELTAS) {
await sleep(DELTA_INTERVAL_MS);
if (this.#closed || this.#interrupted.has(turnId)) return;
this.#queue.push({
@@ -140,7 +140,7 @@ class StreamFixtureTransport {
params: {
threadId: "stream-fixture-thread",
turnId,
item: { id: `message-${turnId}`, type: "agentMessage", text: PHASE7_STREAM_FIXTURE_REPLY },
item: { id: `message-${turnId}`, type: "agentMessage", text: CAPABILITY_STREAM_FIXTURE_REPLY },
},
});
this.#queue.push({
@@ -150,7 +150,7 @@ class StreamFixtureTransport {
}
}
export function phase7StreamFixtureTransportFactory(options = {}) {
export function capabilityStreamFixtureTransportFactory(options = {}) {
const evidence = {
runnerPid: 7100,
runnerProcessGroupId: 7100,
+38 -40
View File
@@ -11,7 +11,7 @@
| |
+---------------+---------------+
v
byte-identical Phase 0 result
byte-identical Conformance result
Paperclip core (later) --> implements ControlPlanePort / NativeSessionBackend
```
@@ -19,7 +19,7 @@ Paperclip core (later) --> implements ControlPlanePort / NativeSessionBackend
The dependency arrow always points from an implementation toward a contract.
The standalone package does not reach backward into a Paperclip implementation.
## Initial contracts
## Core contracts
- `ControlPlanePort` is the narrow surface through which a runner opens a run,
appends ordered events, and submits a terminal structured result.
@@ -29,9 +29,8 @@ The standalone package does not reach backward into a Paperclip implementation.
a future control-plane consumer. Environment placement is not implied by the
backend type.
The Phase 0 TypeScript contracts are deliberately sketches: they name
responsibility and dependency direction without prematurely implementing the
runtime transport. Phase 1 adds this executable static path:
The TypeScript contracts name responsibility and dependency direction while
the executable replay path provides a deterministic oracle:
```text
protocol/schemas/*.json
@@ -49,7 +48,7 @@ TypeScript schema constants/types -> validator -> deterministic reducer
```
The Rust `runner-core` crate establishes the production language/package
boundary and checks the same fixture summaries. Phase 2 adds the package-local
boundary and checks the same fixture summaries. Local runner adds the package-local
`paperclip-runnerd` and `fake-harness` binaries without changing that dependency
direction.
@@ -59,19 +58,19 @@ direction.
supervision, durable delivery, and the eventual `paperclip-runnerd` binary.
- TypeScript owns the control-plane/browser side and remains a useful reference
client/test oracle.
- JSON Schema and shared fixtures are the language-neutral authority. Phase 0
keeps its narrow tracer fixture; Phase 1 adds the executable PRP v1 schema and
conformance corpus without silently changing the accepted Phase 0 path.
- `check:phase0-parity` prevents either implementation from introducing a
- JSON Schema and shared fixtures are the language-neutral authority. Conformance
keeps its narrow tracer fixture; Replay adds the executable PRP v1 schema and
conformance corpus without silently changing the accepted Conformance path.
- `check:conformance-parity` prevents either implementation from introducing a
language-specific observable result.
- `check:phase1-parity` prevents TypeScript replay and the Rust production
- `check:replay-parity` prevents TypeScript replay and the Rust production
direction from disagreeing on identity, terminal state, duplicates, or gaps.
## Allowed dependencies
- Rust crates declared by the package-local Cargo workspace.
- Node.js standard-library modules.
- Third-party packages declared by this workspace when a later phase needs them.
- Third-party packages declared by this workspace.
- Files within `packages/paperclip-runner/`.
- A future explicit generated-schema package only after architecture review and
an allowlist change in the boundary checker.
@@ -83,7 +82,7 @@ The following imports and package dependencies are rejected:
- `server/`, `ui/`, and `cli/` implementation paths;
- `@paperclipai/db` and production database schema or client modules;
- `@paperclipai/shared`, adapter utilities, and other Paperclip workspace
internals unless a later boundary review explicitly allows a public contract;
internals unless a boundary review explicitly allows a public contract;
- relative or absolute imports that escape `packages/paperclip-runner/`.
This rule applies to type-only imports, exports, dynamic imports, CommonJS
@@ -96,19 +95,19 @@ must fail the checker.
```sh
pnpm --filter @paperclipai/paperclip-runner check:forbidden-imports
pnpm --filter @paperclipai/paperclip-runner test
pnpm --filter @paperclipai/paperclip-runner check:phase1-parity
pnpm --filter @paperclipai/paperclip-runner check:replay-parity
```
The first command scans the package source, scripts, and manifest. The test
command additionally asserts that the negative fixture is rejected. The normal
scan excludes that fixture so a deliberate proof does not make the package fail.
## Phase 0 process boundary
## Conformance process boundary
The mock core is an in-memory adapter, not a Paperclip server. Starting it only
changes local object state. The tracer performs this sequence:
1. load and validate `protocol/fixtures/phase-00-minimal-run.json`;
1. load and validate `protocol/fixtures/conformance-minimal-run.json`;
2. start the mock adapter;
3. open the fixture run through `ControlPlanePort`;
4. append contiguous typed events;
@@ -119,9 +118,9 @@ No socket, database, browser, Paperclip process, or model process is started.
The default command executes this sequence in Rust. The TypeScript reference
executes the same sequence, and the parity check compares their complete stdout.
## Phase 1 static replay boundary
## Static replay boundary
`replayPhase1FixtureText` is the single entry point used by the CLI and browser.
`replayReplayFixtureText` is the single entry point used by the CLI and browser.
It parses JSON, validates JSON Schema plus cross-record bindings, and only then
calls the reducer. The reducer is pure: it clones input state, applies an event
at most once by source event ID, records source gaps/out-of-order deliveries,
@@ -132,7 +131,7 @@ Card, and Textarea are source-compatible adaptations of shadcn primitives; all
visual values live in its local `styles.css` token layer. It imports the same
replay module as the CLI and does not create a browser-only protocol model.
## Phase 2 local process boundary
## Local process boundary
```text
TypeScript mock core
@@ -169,7 +168,7 @@ Every browser event passes `validatePrpEvent` and `applyPrpEvent`. When the run
ends, the browser reduces the complete event list again and compares the replay
snapshot with the live snapshot.
## Phase 3 durable transport boundary
## Durable transport boundary
```text
TypeScript mock core
@@ -180,7 +179,7 @@ paperclip-runnerd (Rust WebSocket client)
| atomic private JSON state
+-- durable outbox and processed-command cache
+-- stable runner/session/turn/item identities
+-- Phase 2 fake-harness process for restart proof
+-- Local runner fake-harness process for restart proof
```
The runner initiates the loopback WebSocket. The bootstrap ticket is present
@@ -196,7 +195,7 @@ lease expiry, storage pressure, drain, and revoke. The browser recovery page
calls that peer through guarded package-local Vite middleware and renders only
safe trace fields.
## Phase 7 capability-model boundary
## Capability-model boundary
```text
Paperclip skill + 7 references Paperclip Evals corpus (106 cases)
@@ -216,19 +215,19 @@ Paperclip skill + 7 references Paperclip Evals corpus (106 cases)
read-only browser scenario explorer
```
Phase 7 is a package-local model of a native Paperclip run. It classifies every
Capability is a package-local model of a native Paperclip run. It classifies every
capability as control-plane-owned, always-agent-tool, or optional-agent-tool,
exposes the always/optional set as a transport-neutral semantic tool catalog,
gates optional tools behind grants, and proves 106 eval-derived cases against an
in-process mock `ControlPlanePort`. The mock adapter is the only coupling point,
so a real adapter can replace it later without touching the catalog,
authorization rules, or conformance suite. Phase 7 contacts no Paperclip
authorization rules, or conformance suite. Capability contacts no Paperclip
service, database, ACPX session, or provider credential; the
[forbidden-imports checker](#forbidden-dependencies) keeps it that way. Real
integration is Phase 8 (ACPX) and requires separate approval; see
[the future binding boundary](phase-07-future-binding-boundary.md).
integration is future upload integration (ACPX) and requires separate approval; see
[the future binding boundary](capability-future-binding-boundary.md).
## Phase 7 live process topology
## Capability live process topology
The live surface adds a real provider turn loop over the same mock core. The
package server owns every credential and every child process; the browser holds
@@ -238,7 +237,7 @@ none.
browser (issue thread + evidence panel; no credential)
| HTTP/SSE over the trusted-proxy boundary
v
package server ── projectPhase7IssueThread ──> Phase7IssueThreadSnapshot
package server ── projectCapabilityIssueThread ──> CapabilityIssueThreadSnapshot
| (one contract, two producers)
| owns session, tool loop, provider auth
v
@@ -248,7 +247,7 @@ none.
codex app-server (real session)
| tool request
v
Phase7SemanticDispatcher ── typed command ──> in-process mock ControlPlanePort
CapabilitySemanticDispatcher ── typed command ──> in-process mock ControlPlanePort
^ |
+──────── typed result / typed denial ─────+
```
@@ -256,28 +255,27 @@ none.
Three actors stay separate at all times: **Real Codex** (the app-server
session), **Real runnerd** (the package-local binary), and **Mock Paperclip**
(the in-process `ControlPlanePort`). The same
`Phase7IssueThreadSnapshot` is produced by deterministic `fake` fixtures for the
`CapabilityIssueThreadSnapshot` is produced by deterministic `fake` fixtures for the
screenshot matrix and by the server-side projection for a live session; the
projection reads only durable records and decides nothing, so UI-side state math
is a defect by construction. The scripted (`fake`) mode drives the conformance
suite and replay offline; the Codex (live) mode requires a locally authenticated
Codex. See [execution modes and identity](phase-07-execution-modes.md) for the
mode and eligibility rules, and [the live runnerd/Codex loop](phase-07-live-runnerd-codex.md)
Codex. See [execution modes and identity](capability-execution-modes.md) for the
mode and eligibility rules, and [the live runnerd/Codex loop](capability-live-runnerd-codex.md)
for the session API. The package server blocks every request to a real
Paperclip API, and the evidence suite proves no such request occurred. The same
topology serves the [clean-room chat](phase-07-clean-room-chat.md): the only
topology serves the [clean-room chat](capability-clean-room-chat.md): the only
difference is a mock tenant seeded with a company, an agent, and one blank issue
instead of a recorded eval case.
## Future integration rule
## Integration rule
The [implementation plan](../spec/paperclip-native-runner-implementation-plan.md)
keeps production integration in a separately reviewed phase. Paperclip core may
implement these contracts later, but this package must remain independently
buildable, testable, and runnable against the mock adapter.
Paperclip core may implement these contracts behind a separately reviewed
adapter, but this package must remain independently buildable, testable, and
runnable against the mock adapter.
The proposed Phase 6 seam is recorded in
[Phase 6 Thin Paperclip Adapter Boundary](design/phase-6-thin-paperclip-adapter.md).
The proposed Standalone seam is recorded in
[Standalone Thin Paperclip Adapter Boundary](design/standalone-thin-paperclip-adapter.md).
It keeps one dependency direction, branches only after Paperclip workspace and
environment realization, composes a package-owned `NativeSessionBackend` with
a server-bound `ControlPlanePort`, and returns to the existing Paperclip
@@ -1,12 +1,12 @@
# Phase 7 Authorization and Exposure
# Capability Authorization and Exposure
Two decisions gate every capability: **exposure** (is the tool even offered to
this actor?) and **invocation** (may this specific call proceed?). The
authorization engine answers both, produces typed denials that carry no
protected state, and redacts secrets before any observable boundary.
Sources: `src/tools/phase7-tool-authorization.ts`,
`src/tools/phase7-semantic-tool-runtime.ts`, `src/tools/phase7-tool-bindings.ts`.
Sources: `src/tools/capability-tool-authorization.ts`,
`src/tools/capability-semantic-tool-runtime.ts`, `src/tools/capability-tool-bindings.ts`.
## Three-way classification
@@ -50,7 +50,7 @@ authorization record stores the `consideredClaims`, `grantedClaims`, and
## Typed denials carry no protected state
A denial is a `Phase7PolicyDenial`: `ok: false` with an `error.code` of
A denial is a `CapabilityPolicyDenial`: `ok: false` with an `error.code` of
`policy_denied`, `operation_absent`, `input_invalid`, or `operation_unsupported`,
the `operationId`, a generic `reason`, and the authorization record. It carries
no fixture state and no protected payload. If the underlying mock operation
@@ -67,7 +67,7 @@ Redaction is two layers:
unredacted secret is `invokeForModel()`, which returns a frozen capsule whose
`toJSON()` and default serialization always yield the redacted form; only an
explicit `readModelResult()` opens the raw value. The capsule uses a distinct
schema (`paperclip.phase7.model-tool-result.v1`) so it cannot be assigned to
schema (`paperclip.capability.model-tool-result.v1`) so it cannot be assigned to
an observable sink by mistake.
2. **Path redaction rules.** `read_secret_value` redacts `$.value` to
`[SECRET_VALUE]` across output, error, and authorization-record channels;
@@ -84,7 +84,7 @@ browser view.
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/tools/phase7-semantic-tools.test.ts
src/tools/capability-semantic-tools.test.ts
```
The "exposure and authorization" and "security policy" describes cover typed
@@ -93,6 +93,6 @@ prohibition, and the test-only, explicitly granted, allowlisted escape hatch.
## Related
- [Capability disposition](phase-07-capability-disposition.md)
- [Semantic tool catalog](phase-07-semantic-tools.md)
- [Scenario explorer](phase-07-scenario-explorer.md)
- [Capability disposition](capability-disposition.md)
- [Semantic tool catalog](capability-semantic-tools.md)
- [Scenario explorer](capability-scenario-explorer.md)
@@ -1,6 +1,6 @@
# Phase 7 clean-room live chat
# Capability clean-room live chat
Phase 7M adds a second primary path beside the preset scenario explorer. A board
Capability adds a second primary path beside the preset scenario explorer. A board
user opens a blank chat, sends a free-form message, and watches real Codex work
a mock Paperclip issue through real runnerd. There is no scenario to pick, no
recorded transcript, and no scripted tool tour.
@@ -20,7 +20,7 @@ for a provider.
## What "clean room" means
Every open mints a new mock tenant — a new company, actor, task, and `MCK-`
identifier — and seeds nothing else. `assertPhase7CleanRoomSeedIsBlank` fails the
identifier — and seeds nothing else. `assertCapabilityCleanRoomSeedIsBlank` fails the
open if any comment, document, interaction, approval, artifact, blocker, wake,
or fault is present, so "the thread starts blank" is enforced at the seam that
creates the session rather than assumed.
@@ -32,7 +32,7 @@ is why a fresh room reads `in_progress` rather than `todo`.
The clean room never falls back:
- `parsePhase7Route` pins `mode` to `live` on `#/chat` and drops `shot` and
- `parseCapabilityRoute` pins `mode` to `live` on `#/chat` and drops `shot` and
`at`. No URL can talk it into rendering a fixture or a recording.
- The package server has no scripted path for this route. It projects the live
session and refuses any view that is not `mode=live` with a `Real Codex`
@@ -43,7 +43,7 @@ The clean room never falls back:
## Exposure profile
The profile is deterministic and inspectable even though the calls are not.
`PHASE7_CLEAN_ROOM_CLAIMS` grants the read, comment, document, interaction,
`CAPABILITY_CLEAN_ROOM_CLAIMS` grants the read, comment, document, interaction,
deliverable, delegation, dependency, wake, and approval-request operations.
Three grants stay withheld on purpose:
@@ -60,20 +60,20 @@ state does not move. The Evidence drawer's Tools section lists all three under
`Control plane (not exposed to the agent)`.
The run itself holds the wider adapter claim set that the mock command boundary
requires (`phase7FixtureRunCapabilities`). That union widens the port, never the
requires (`capabilityFixtureRunCapabilities`). That union widens the port, never the
catalog the model sees: effective claims are the intersection of run claims,
scenario claims, and explicitly delegated claims.
## Session lifecycle
```text
GET /api/phase7/ui/cleanroom/session[?sessionId=…] open or reconnect
POST /api/phase7/ui/cleanroom/session {sessionId?} New chat (retires the caller's room)
POST /api/phase7/ui/message {sessionId, message}
POST /api/phase7/ui/interrupt {sessionId}
POST /api/phase7/ui/reconnect {sessionId}
POST /api/phase7/ui/reset {sessionId}
POST /api/phase7/ui/interaction {sessionId, interactionId, outcome, result}
GET /api/capability/ui/cleanroom/session[?sessionId=…] open or reconnect
POST /api/capability/ui/cleanroom/session {sessionId?} New chat (retires the caller's room)
POST /api/capability/ui/message {sessionId, message}
POST /api/capability/ui/interrupt {sessionId}
POST /api/capability/ui/reconnect {sessionId}
POST /api/capability/ui/reset {sessionId}
POST /api/capability/ui/interaction {sessionId, interactionId, outcome, result}
```
`Reset` and `New chat` both stop active work, clear the previous session's
@@ -92,11 +92,11 @@ scenario session can never be handed to the chat route or the reverse.
| Turns per chat | 24, then a named `turn_limit` refusal that points at `New chat` |
| Message size | 8 KiB |
| Concurrent clean rooms | 4; the oldest yields to a new board user |
| Turn timeout | 120 s (`Phase7LiveSessionService` default) |
| Turn timeout | 120 s (`CapabilityLiveSessionService` default) |
## Real-API block
`Phase7LiveSession` routes every Paperclip operation through the in-process mock
`CapabilityLiveSession` routes every Paperclip operation through the in-process mock
`ControlPlanePort`; no code path reaches a Paperclip URL. The projection turns
that into a record rather than a claim: the Control plane section of the
Evidence drawer carries a `network-guard-<sessionId>` row reading
@@ -109,7 +109,7 @@ provider, runner, or control-plane credential.
## Remote preview gateway
The tailnet preview keeps the package server on loopback and exposes it through
`dist/phase7/tailnet-gateway.js`. Every API call requires the gateway's
`dist/capability/tailnet-gateway.js`. Every API call requires the gateway's
high-entropy HttpOnly capability cookie. Mutations additionally require the
exact configured `Origin` and `application/json`.
@@ -124,14 +124,14 @@ without weakening the explicit cross-site denial.
| Surface | Command |
| --- | --- |
| Seed, exposure profile, and identity rotation | `pnpm --filter @paperclipai/paperclip-runner test:phase7` |
| Clean-room HTTP routes end to end (stub provider) | included in `test:phase7` |
| Seed, exposure profile, and identity rotation | `pnpm --filter @paperclipai/paperclip-runner test:scenarios` |
| Clean-room HTTP routes end to end (stub provider) | included in `test:scenarios` |
| Browser entry, blank state, evidence-on-demand, narrow layout, axe | `pnpm --filter @paperclipai/paperclip-runner test:browser:issue-thread` |
| Real Codex through real runnerd | `pnpm --filter @paperclipai/paperclip-runner smoke:phase7:cleanroom` |
| Live screenshots | `pnpm --filter @paperclipai/paperclip-runner record:phase7:cleanroom` |
| Real Codex through real runnerd | `pnpm --filter @paperclipai/paperclip-runner smoke:capability:cleanroom` |
| Live screenshots | `pnpm --filter @paperclipai/paperclip-runner record:capability:cleanroom` |
See the [clean-room chat tutorial](tutorials/phase-07-clean-room-chat.md) for the
clean-start walkthrough, [execution modes and identity](phase-07-execution-modes.md)
See the [clean-room chat tutorial](tutorials/capability-clean-room-chat.md) for the
clean-start walkthrough, [execution modes and identity](capability-execution-modes.md)
for the fake/live eligibility rules, and the
[issue-thread UI reference](phase-07-issue-thread-ui.md) for the thread,
[issue-thread UI reference](capability-issue-thread-ui.md) for the thread,
composer, and Evidence panel this surface reuses.
@@ -1,6 +1,6 @@
<!-- GENERATED FILE — DO NOT EDIT. Run pnpm generate:phase7-inventory. -->
<!-- GENERATED FILE — DO NOT EDIT. Run pnpm generate:capability-inventory. -->
# Phase 7A Capability Contract
# Capability Capability Contract
This generated contract is a self-contained derivative of the Paperclip skill, its seven references, the Paperclip Evals corpus, and the legacy MCP tool surface. It does not import or contact the Paperclip control plane.
@@ -34,8 +34,8 @@ The skill/reference inventory and eval cases are the only normative behavior sou
## Regeneration
- `pnpm --dir packages/paperclip-runner generate:phase7-inventory` imports the canonical baselines and rewrites every generated file.
- `pnpm --dir packages/paperclip-runner check:phase7-inventory` validates counts, uniqueness, normative dispositions, one-to-one MCP folds, required fields, and generated-file drift without requiring the external eval repository.
- `pnpm --dir packages/paperclip-runner generate:capability-inventory` imports the canonical baselines and rewrites every generated file.
- `pnpm --dir packages/paperclip-runner check:capability-inventory` validates counts, uniqueness, normative dispositions, one-to-one MCP folds, required fields, and generated-file drift without requiring the external eval repository.
## Skill / Reference Rows
@@ -1,9 +1,9 @@
# Phase 7 Capability Disposition
# Capability Capability Disposition
This page explains how Phase 7 classifies every Paperclip capability. The
This page explains how Capability classifies every Paperclip capability. The
classification itself lives in a generated file,
[the Phase 7A capability contract](phase-07-capability-contract.md), which
carries a "DO NOT EDIT" header and is rewritten by `generate:phase7-inventory`.
[the Capability contract](capability-contract.md), which
carries a "DO NOT EDIT" header and is rewritten by `generate:capability-inventory`.
Read this page to understand what the generated table means; read the table for
the authoritative rows.
@@ -35,7 +35,7 @@ Every capability is classified as exactly one of:
deliverable are always-agent-tool capabilities.
- **`optional_agent_tool`** — the agent may have this tool, but only when a
grant unlocks it. Absent the grant, the capability is not exposed and calling
it is denied. See [authorization and exposure](phase-07-authorization-and-exposure.md).
it is denied. See [authorization and exposure](capability-authorization-and-exposure.md).
The disposition is what makes the boundary legible: it states, per capability,
whether an agent can act, must wait for the control plane, or needs a grant
@@ -58,27 +58,27 @@ Generation reads the live in-repo skill/reference sources, the legacy MCP tool
source, and the Paperclip Evals corpus, so it **requires** the external eval
repository (via `PAPERCLIP_EVALS_ROOT` or a known local path) and is not part of
the offline path. Checking and testing read only the checked-in derivatives
under `spec/phase-07/` and need no external repository.
under `spec/capability/` and need no external repository.
```sh
# Rewrite every generated file. Requires the external Paperclip Evals corpus.
pnpm --filter @paperclipai/paperclip-runner generate:phase7-inventory
pnpm --filter @paperclipai/paperclip-runner generate:capability-inventory
# Validate counts, uniqueness, normative dispositions, one-to-one MCP folds,
# required fields, and generated-file drift. Offline; no external eval repo.
pnpm --filter @paperclipai/paperclip-runner check:phase7-inventory
pnpm --filter @paperclipai/paperclip-runner check:capability-inventory
# Prove the validator rejects an independent MCP classification and rejects
# missing, duplicate, or unknown MCP folds. Offline.
pnpm --filter @paperclipai/paperclip-runner test:phase7-inventory
pnpm --filter @paperclipai/paperclip-runner test:capability-inventory
```
`check:phase7-inventory` diffs the checked-in generated files against what the
`check:capability-inventory` diffs the checked-in generated files against what the
live in-repo sources imply and fails on any drift or stale anchor, so the
contract cannot silently fall out of sync.
## Related
- [Semantic tool catalog](phase-07-semantic-tools.md)
- [Authorization and exposure](phase-07-authorization-and-exposure.md)
- [Eval conformance](phase-07-eval-conformance.md)
- [Semantic tool catalog](capability-semantic-tools.md)
- [Authorization and exposure](capability-authorization-and-exposure.md)
- [Eval conformance](capability-eval-conformance.md)
@@ -1,15 +1,15 @@
# Phase 7 Eval-Derived Conformance
# Capability Eval-Derived Conformance
Phase 7E turns the Paperclip Evals corpus into an executable, offline
conformance suite. The suite derives its cases from the checked-in Phase 7
traceability derivative (`spec/phase-07/eval-traceability.yaml`) — it does
Capability turns the Paperclip Evals corpus into an executable, offline
conformance suite. The suite derives its cases from the checked-in Capability
traceability derivative (`spec/capability/eval-traceability.yaml`) — it does
**not** clone or read an external eval repository, and it starts only the
in-process mock control plane. It hard-fails unless the derivative declares
schema version 2, exactly 106 rows, exactly 16 groups, and unique case IDs.
Sources: `src/conformance/phase7-eval-suite.ts` and its test
`src/conformance/phase7-eval-suite.test.ts`; the reporter
`scripts/run-phase7-eval-suite.mjs`.
Sources: `src/conformance/capability-eval-suite.ts` and its test
`src/conformance/capability-eval-suite.test.ts`; the reporter
`scripts/run-capability-eval-suite.mjs`.
## The corpus
@@ -17,7 +17,7 @@ Sources: `src/conformance/phase7-eval-suite.ts` and its test
se 4, su 4, bl 5, dp 3, ix 9, ap 6, ar 4, er 9, rf 22, mh 4, rs 3, wk 8.
- Each case declares an actor role, a task mode, an input scenario, and the
expected outcome, all bound to a row in
[the capability contract](phase-07-capability-contract.md).
[the capability contract](capability-contract.md).
## Assertion classes
@@ -60,31 +60,31 @@ contacted, and the browser explorer holds no credential.
```sh
# Run the 106-case suite in-process (one vitest file drives all cases).
pnpm --filter @paperclipai/paperclip-runner test:phase7-evals
pnpm --filter @paperclipai/paperclip-runner test:capability-evals
# Build the public surface and write the parity report with per-group counts,
# assertion classes, the fake-agent matrix, the bounded Codex sample, and the
# semantic-operation execution counts.
pnpm --filter @paperclipai/paperclip-runner report:phase7-evals
pnpm --filter @paperclipai/paperclip-runner report:capability-evals
```
The reporter writes `knowledge/evidence/phase-07/eval-parity-report.{json,md}`.
The reporter writes `knowledge/evidence/capability/eval-parity-report.{json,md}`.
Run the bounded provider conformance matrix separately. It creates exactly one
real Codex turn for each of the 16 checked-in eval groups while retaining the
in-process mock control plane:
```sh
pnpm --filter @paperclipai/paperclip-runner report:phase7-live-evals
pnpm --filter @paperclipai/paperclip-runner report:capability-live-evals
```
Each failure carries its case ID, assertion class, semantic operation,
authorization decision, and final state diff. The report is generated on demand
and is not committed; delete it before running `docs:validate` (it carries no
OKF frontmatter). See the
[verification commands reference](phase-07-verification-commands.md).
[verification commands reference](capability-verification-commands.md).
## Related
- [Capability disposition](phase-07-capability-disposition.md)
- [Semantic tool catalog](phase-07-semantic-tools.md)
- [Scenario explorer](phase-07-scenario-explorer.md)
- [Capability disposition](capability-disposition.md)
- [Semantic tool catalog](capability-semantic-tools.md)
- [Scenario explorer](capability-scenario-explorer.md)
@@ -1,13 +1,13 @@
# Phase 7 execution modes and identity
# Capability execution modes and identity
Phase 7 runs the same semantic tool catalog, authorization engine, and mock
Capability runs the same semantic tool catalog, authorization engine, and mock
`ControlPlanePort` under two agent modes. Whichever mode is active, the
surface always names three separate actors so a reader never mistakes one for
another.
## Three actors, always separate
| Actor | What it is in Phase 7 | What it is **not** |
| Actor | What it is in Capability | What it is **not** |
| --- | --- | --- |
| **Real Codex** | A real Codex app-server session driving the turn loop. The agent has no Paperclip skill; it sees only the semantic tools the scenario exposes. | Not a scripted stand-in, and not a Paperclip-aware agent. |
| **Real runnerd** | The real package-local `paperclip-runnerd` binary. It owns the Codex child process group and proxies newline-delimited JSON-RPC over stdio. | Not an in-process fake and not the production Paperclip runtime. |
@@ -70,7 +70,7 @@ the runner session, not on the agent:
Live mode is reachable from both primary surfaces:
- the preset scenario explorer, with `mode=live` on a chosen scenario;
- the [clean-room chat](phase-07-clean-room-chat.md) at `#/chat`, which has no
- the [clean-room chat](capability-clean-room-chat.md) at `#/chat`, which has no
other mode. It starts blank on a freshly minted mock tenant, pins `mode=live`
in the route, and reports a failure to start real Codex rather than falling
back to a fixture or a recording.
@@ -83,7 +83,5 @@ Live mode is reachable from both primary surfaces:
| Live Codex run on the final build | Revision 3/4 final acceptance criteria that require a real provider turn |
| Revision 2 preview (build `da0d32d74a`, historical URL) | Historical comparison only — **not** eligible for final acceptance |
See the [live runnerd and Codex loop](phase-07-live-runnerd-codex.md) reference
for the session API, and the
[final evidence manifest](../knowledge/evidence/2026-08-10-phase-07i-final-evidence.md)
for the command-by-command record behind each row above.
See the [live runnerd and Codex loop](capability-live-runnerd-codex.md) reference
for the session API and verification commands behind each row above.
@@ -1,15 +1,15 @@
# Phase 7 Future Binding Boundary (Phase 8 / ACPX)
# Capability Future Binding Boundary (future upload integration / ACPX)
Phase 7 is a package-local model. It does not integrate the runner into
Paperclip, and it must not. This page states exactly what Phase 7 defers to
Phase 8 and what clean seam it preserves so the deferral stays cheap.
Capability is a package-local model. It does not integrate the runner into
Paperclip, and it must not. This page states exactly what Capability defers to
future upload integration and what clean seam it preserves so the deferral stays cheap.
## What Phase 7 does not do
## What Capability does not do
Nothing in Phase 7 contacts a real Paperclip control plane, database, ACPX
Nothing in Capability contacts a real Paperclip control plane, database, ACPX
session, or provider credential. The 106-case conformance suite and the browser
explorer run entirely against the in-process
[mock ControlPlanePort](phase-07-mock-control-plane-port.md) and checked-in
[mock ControlPlanePort](capability-mock-control-plane-port.md) and checked-in
fixtures. The [forbidden-imports boundary](architecture.md) rejects any import
that crosses into `server/`, `ui/`, `cli/`, `@paperclipai/db`, or other
Paperclip workspace internals.
@@ -21,31 +21,31 @@ The package owns two contracts a future consumer implements:
- **`ControlPlanePort`** — the narrow surface through which a runner opens a
run, appends ordered events, records semantic operations, and submits a
terminal result. Phase 7 injects the *mock* adapter behind this port; Phase 8
terminal result. Capability injects the *mock* adapter behind this port; future upload integration
injects a real one. The catalog, authorization engine, and conformance suite
bind to the port, not to any adapter.
- **`NativeSessionBackend`** — the normalized session surface for a future
control-plane consumer.
Because the port is the only coupling point, Phase 8 replaces the adapter
Because the port is the only coupling point, future upload integration replaces the adapter
without touching the tool catalog, the authorization rules, or the eval-derived
conformance suite. The package remains independently buildable, testable, and
runnable against the mock adapter after the real one exists.
## What Phase 8 (ACPX) will bind
## What future upload integration (ACPX) will bind
Phase 8 binds a real Paperclip `ControlPlanePort` implementation behind the same
future upload integration binds a real Paperclip `ControlPlanePort` implementation behind the same
seam so the semantic tools and authorization engine act against a live control
plane instead of the mock. That work is out of scope here and requires separate
CTO approval at the Phase 7 checkpoint (`PAP-16908`). Until then:
CTO approval at the Capability checkpoint (`PAP-16908`). Until then:
- ACPX is Phase 8, not Phase 7.
- No Phase 7 documentation claims Phase 8 capability.
- ACPX is future upload integration, not Capability.
- No Capability documentation claims future upload integration capability.
- Real integration is a separately reviewed phase, consistent with the
[future integration rule](architecture.md) recorded for every prior phase.
## Related
- [Architecture and dependency boundary](architecture.md)
- [Mock ControlPlanePort](phase-07-mock-control-plane-port.md)
- [Capability disposition](phase-07-capability-disposition.md)
- [Mock ControlPlanePort](capability-mock-control-plane-port.md)
- [Capability disposition](capability-disposition.md)
@@ -1,12 +1,12 @@
# Phase 7 Paperclip-style issue-thread UI
# Capability Paperclip-style issue-thread UI
Phase 7G renders the mock Paperclip issue as a native issue thread. A board user
Capability renders the mock Paperclip issue as a native issue thread. A board user
reads the thread, answers typed interactions inline, and inspects the evidence
behind every mock mutation. The implementation follows the binding
[Phase 7B issue-thread UX contract](design/phase-7-issue-thread-ux-contract.md).
[Capability issue-thread UX contract](design/capability-issue-thread-ux-contract.md).
The same shell hosts a second primary path: the
[clean-room live chat](phase-07-clean-room-chat.md) at `#/chat`, which starts a
[clean-room live chat](capability-clean-room-chat.md) at `#/chat`, which starts a
blank thread on a freshly minted mock tenant instead of a preset scenario. It
reuses every surface described below — header, thread, composer, and Evidence
panel — and differs only in what the session is seeded with and in refusing any
@@ -15,24 +15,24 @@ non-live mode.
## Authority boundary
```text
browser -> package server -> Phase7LiveSession -> paperclip-runnerd -> codex
(projection) Phase7SemanticDispatcher -> ControlPlanePort mock
browser -> package server -> CapabilityLiveSession -> paperclip-runnerd -> codex
(projection) CapabilitySemanticDispatcher -> ControlPlanePort mock
```
The browser holds no authority. It renders one shape,
`Phase7IssueThreadSnapshot` (`src/issue-thread/types.ts`), and computes no
`CapabilityIssueThreadSnapshot` (`src/issue-thread/types.ts`), and computes no
claim, policy decision, state diff, or parity verdict. Two producers emit that
shape:
- `phase7IssueThreadFixture(slug)` — deterministic `fake` snapshots used by the
- `capabilityIssueThreadFixture(slug)` — deterministic `fake` snapshots used by the
screenshot matrix and the browser suite.
- `projectPhase7IssueThread({ snapshot })` — the live projection. It runs in the
- `projectCapabilityIssueThread({ snapshot })` — the live projection. It runs in the
package server and rearranges durable records only: the live session's
transcript, evidence entries, semantic authorization records, and the
serialized mock state.
The one browser-initiated mock mutation is an interaction response. It posts to
`POST /api/phase7/ui/interaction`; `Phase7LiveSession.resolveInteraction` stores
`POST /api/capability/ui/interaction`; `CapabilityLiveSession.resolveInteraction` stores
the typed response in the mock control plane **before** resuming the same Codex
thread, so the card only leaves `submitting` on server acknowledgement.
@@ -43,7 +43,7 @@ the reserved `MCK-` prefix, and no real Paperclip URL is ever rendered.
## What the browser is allowed to see
The projection is an internal shape. What ships is the published DTO,
`toPhase7PublicThreadView` (`src/issue-thread/public-view.ts`), which copies the
`toCapabilityPublicThreadView` (`src/issue-thread/public-view.ts`), which copies the
view field by field — so a field added to the projection, or to any record it
passes through, cannot reach a browser until it is listed there. Every response
path uses it: interim stream frames, the settled payload, and reconnect or
@@ -51,8 +51,8 @@ replay replies alike.
Three narrowings stack, so no single omission opens a disclosure path:
1. **At record time.** `redactPhase7EvidenceData`
(`src/phase7/evidence-redaction.ts`) is the only way an evidence entry enters
1. **At record time.** `redactCapabilityEvidenceData`
(`src/live/evidence-redaction.ts`) is the only way an evidence entry enters
a live session. Provider notifications are reduced to a coarse category,
provider diagnostics to the fact that one occurred, tool arguments to the
catalog-declared field names they used, and tool results to the outcome,
@@ -89,10 +89,10 @@ there would make two tabs of one surface revoke each other on every load while
protecting nothing: one browser is one principal, and cross-browser denial rests
on the binding rather than on how often the value changes.
The two surfaces use separate cookie names (`paperclip_phase7_issue`,
`paperclip_phase7_chat`) because they are separate pages of one origin: a single
The two surfaces use separate cookie names (`paperclip_capability_issue`,
`paperclip_capability_chat`) because they are separate pages of one origin: a single
name would make opening the explorer revoke the clean room. A script that drives
these routes has to behave like one browser — `scripts/phase7-cookie-jar.mjs` is
these routes has to behave like one browser — `scripts/capability-cookie-jar.mjs` is
what the smoke scripts use for that.
## Surfaces
@@ -150,18 +150,18 @@ owns it.
pnpm --filter @paperclipai/paperclip-runner console:issue-thread
# Focused browser suite, including the axe gate on all 12 slugs × 2 viewports
pnpm --filter @paperclipai/paperclip-runner test:browser:phase7
pnpm --filter @paperclipai/paperclip-runner test:browser:scenarios
# View-model and live-projection unit tests
pnpm --filter @paperclipai/paperclip-runner exec vitest run src/issue-thread
# Screenshot matrix (12 slugs × 2 viewports) and its byte-stability check
pnpm --filter @paperclipai/paperclip-runner record:phase7:ui
pnpm --filter @paperclipai/paperclip-runner check:phase7:ui
pnpm --filter @paperclipai/paperclip-runner record:capability:ui
pnpm --filter @paperclipai/paperclip-runner check:capability:ui
# Real runnerd + real Codex through the same HTTP routes the browser uses
pnpm --filter @paperclipai/paperclip-runner smoke:phase7:ui
pnpm --filter @paperclipai/paperclip-runner record:phase7:ui:live
pnpm --filter @paperclipai/paperclip-runner smoke:capability:ui
pnpm --filter @paperclipai/paperclip-runner record:capability:ui:live
```
Hosts without the Playwright chromium system libraries can either run
@@ -169,7 +169,7 @@ Hosts without the Playwright chromium system libraries can either run
`PAPERCLIP_RUNNER_CHROMIUM_PATH` to a preinstalled Chromium.
The committed PNGs are pinned to the Chromium build listed in
`knowledge/evidence/phase-07/ui/index.md`, so `check:phase7:ui` needs that same
`knowledge/evidence/capability/ui/index.md`, so `check:capability:ui` needs that same
browser. Point `PAPERCLIP_RUNNER_CHROMIUM_PATH` at the recorded browser before
comparing — and when that path is the agent-browser wrapper, also set
`PAPERCLIP_CHROMIUM_BIN` to the exact binary, because the wrapper otherwise
@@ -203,7 +203,7 @@ Focus management (§9.2) is covered by named regressions in the browser suite:
## Contract deviations
One deviation is recorded against the Phase 7B contract:
One deviation is recorded against the Capability contract:
- **§5 expired-family dimming.** The contract asks for a 60% opacity body on
`stale_target` and the other expired outcomes. A literal opacity drops that
@@ -215,6 +215,6 @@ One deviation is recorded against the Phase 7B contract:
Fake-mode fixtures render from authored data with a fixed clock, so two captures
of a slug from a clean checkout are byte-identical. Live sessions are not
byte-stable — a real model writes their prose — so live evidence lives in
`knowledge/evidence/phase-07/ui-live/` and is excluded from the determinism gate.
`knowledge/evidence/capability/ui-live/` and is excluded from the determinism gate.
Durable comments in live mode carry the mock control plane's own deterministic
clock rather than wall time, because that is the timestamp on the mock record.
@@ -1,6 +1,6 @@
# Phase 7 live runnerd and Codex loop
# Capability live runnerd and Codex loop
Phase 7E binds the provider-neutral semantic catalog to a real package-local
Capability binds the provider-neutral semantic catalog to a real package-local
`paperclip-runnerd` process and a real Codex app-server session. Paperclip data
remains deterministic mock state behind `ControlPlanePort`; no request reaches
the Paperclip API.
@@ -10,8 +10,8 @@ the Paperclip API.
The process chain is:
```text
Phase7LiveSession -> paperclip-runnerd -> codex app-server
-> Phase7SemanticDispatcher -> ControlPlanePort mock
CapabilityLiveSession -> paperclip-runnerd -> codex app-server
-> CapabilitySemanticDispatcher -> ControlPlanePort mock
```
`paperclip-runnerd` owns the Codex child and proxies newline-delimited JSON-RPC
@@ -26,12 +26,12 @@ the separate skillless, network-disabled workspace permission profile.
## Stable session API
Import the package root and construct `Phase7LiveSessionService`:
Import the package root and construct `CapabilityLiveSessionService`:
```ts
import { Phase7LiveSessionService } from "@paperclipai/paperclip-runner";
import { CapabilityLiveSessionService } from "@paperclipai/paperclip-runner";
const service = new Phase7LiveSessionService({ store });
const service = new CapabilityLiveSessionService({ store });
const session = await service.create({ workingDirectory });
const turn = await session.sendMessage("Read the mock task and report progress.");
await session.interrupt(); // only when a turn is active
@@ -53,14 +53,14 @@ The handoff surface for later tracks is:
`reset` also deletes the old snapshot and restores the original clean mock
seed under new run/session authority.
Supply a durable implementation of `Phase7LiveSessionStore` for refresh and
Supply a durable implementation of `CapabilityLiveSessionStore` for refresh and
server-restart survival. The included in-memory store is intended for tests and
single-process consumers.
Every snapshot includes bounded transcript/evidence, serialized mock state,
semantic authorization records, runner/Codex PIDs and exit state, and explicit
network evidence. Tool calls are admitted only when their thread and turn match
the active Codex turn. Their typed `Phase7SemanticToolResult` is serialized into
the active Codex turn. Their typed `CapabilitySemanticToolResult` is serialized into
the app-server response, allowing Codex to use the resulting state revision in
its next response.
@@ -69,13 +69,13 @@ its next response.
Run the deterministic contract suite:
```sh
pnpm --filter @paperclipai/paperclip-runner test:phase7
pnpm --filter @paperclipai/paperclip-runner test:scenarios
```
Run a real runnerd and Codex app-server smoke:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase7 -- --json
pnpm --filter @paperclipai/paperclip-runner trace:live-runner -- --json
```
The smoke requires an authenticated local Codex installation. It checks a real
@@ -1,18 +1,18 @@
# Phase 7 Mock ControlPlanePort
# Capability Mock ControlPlanePort
`Phase7MockControlPlaneAdapter` is a deterministic, serializable, in-memory
`CapabilityMockControlPlaneAdapter` is a deterministic, serializable, in-memory
model of Paperclip's control-plane **semantics** — not its transport. It imports
no server route, service, database, or provider binding, and it holds no
credential.
Sources: `src/mock-core/phase7-mock-control-plane-adapter.ts`,
`src/mock-core/phase7-control-plane-types.ts`. Shared contract:
Sources: `src/mock-core/capability-mock-control-plane-adapter.ts`,
`src/mock-core/capability-control-plane-types.ts`. Shared contract:
`src/conformance/control-plane-port.ts`.
## Entity domains
The adapter models ten renderable entity domains, which are exactly the domains
the [scenario explorer](phase-07-scenario-explorer.md) diffs:
the [scenario explorer](capability-scenario-explorer.md) diffs:
`tasks`, `comments`, `documents`, `interactions`, `approvals`, `artifacts`
(artifacts and work products), `blockers`, `workspace` (workspace services),
@@ -43,7 +43,7 @@ The eighteen semantic command kinds (`report_progress`, `write_document`,
`set_dependencies`, `create_task`, `request_approval`, `decide_approval`,
`comment_on_approval`, `control_workspace_service`, `record_budget_usage`,
`finish_task`, `block_task`, `request_review`, `release_task`, `schedule_wake`)
are the mock side of the [semantic tool catalog](phase-07-semantic-tools.md).
are the mock side of the [semantic tool catalog](capability-semantic-tools.md).
## Determinism
@@ -56,7 +56,7 @@ are the mock side of the [semantic tool catalog](phase-07-semantic-tools.md).
value is deep-frozen. Canonical JSON with sorted keys backs every dedupe and
replay comparison.
- **Serializable.** `serialize()` / `restore()` round-trip under the schema tag
`paperclip.phase7.mock-state.v1`.
`paperclip.capability.mock-state.v1`.
- **Scripted faults.** A fault rule injects a bounded number of
`retryable_error` or `lost_ack` effects; a lost-ack command commits once and
returns its stored result on retry.
@@ -67,7 +67,7 @@ The adapter is a pure state machine: no network, no database, no provider SDK,
no credentials. Secrets appearing in wake payloads are redacted against
`/authorization|credential|api.?key|secret|token/i`; the adapter test seeds
`apiKey` and `Bearer` values and asserts they never surface. See
[authorization and exposure](phase-07-authorization-and-exposure.md).
[authorization and exposure](capability-authorization-and-exposure.md).
## Shared conformance
@@ -81,7 +81,7 @@ and result mutations (failing closed with `native_event_replay_conflict`).
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/conformance/control-plane-port.test.ts \
src/mock-core/phase7-mock-control-plane-adapter.test.ts
src/mock-core/capability-mock-control-plane-adapter.test.ts
```
Nine tests, no npm alias of their own — they also run inside
@@ -89,6 +89,6 @@ Nine tests, no npm alias of their own — they also run inside
## Related
- [Semantic tool catalog](phase-07-semantic-tools.md)
- [Authorization and exposure](phase-07-authorization-and-exposure.md)
- [Future binding boundary](phase-07-future-binding-boundary.md)
- [Semantic tool catalog](capability-semantic-tools.md)
- [Authorization and exposure](capability-authorization-and-exposure.md)
- [Future binding boundary](capability-future-binding-boundary.md)
@@ -1,15 +1,15 @@
# Phase 7 Browser Scenario Explorer
# Capability Browser Scenario Explorer
The scenario explorer is a read-only browser surface over all 106 conformance
cases. It runs each scenario in the page against the
[mock control plane](phase-07-mock-control-plane-port.md) through the
[semantic tool runtime](phase-07-semantic-tools.md) and renders the resulting
[mock control plane](capability-mock-control-plane-port.md) through the
[semantic tool runtime](capability-semantic-tools.md) and renders the resulting
run artifact. It never re-judges parity, never leaves its own origin, and holds
no credential.
Sources: `src/phase7/` (scenario index, runner, parity, state diff) and
`examples/phase7-explorer/`. Interaction contract:
[Phase 7 scenario explorer UX](design/phase-7-scenario-explorer-ux.md).
Sources: `src/scenarios/` (scenario index, runner, parity, state diff) and
`examples/scenario-explorer/`. Interaction contract:
[Capability scenario explorer UX](design/capability-scenario-explorer-ux.md).
## The run artifact
@@ -23,14 +23,14 @@ Running a scenario produces one immutable artifact carrying:
the missing claim and no protected task data.
- **State diff** — an immutable diff over the ten entity domains, with unchanged
domains collapsed.
- **Parity verdict** — the runtime's own verdict, plus Phase 7E's per-case
- **Parity verdict** — the runtime's own verdict, plus Capability's per-case
result carried through in a separately labelled block. The explorer displays
these; it does not recompute them.
## Read-only and frozen SDK
The explorer imports the frozen **0.1.2** public SDK through the package-local
alias `@paperclip-runner-local/phase7`, which is deliberately not the published
alias `@paperclip-runner-local/capability`, which is deliberately not the published
package name. It adds no SDK export and changes no published surface. Fake mode
runs entirely in the page against checked-in fixtures and renders fixture time
only, so two loads of the same route produce identical settled DOM.
@@ -49,10 +49,10 @@ only, so two loads of the same route produce identical settled DOM.
- No network request leaves the explorer's own origin.
- `localStorage` holds no run artifact, grant, or fixture payload.
- Codex mode is a disabled option with a stated reason; wiring it to the Phase 5
- Codex mode is a disabled option with a stated reason; wiring it to the SDK
relay is deferred, and the browser holds no provider credential either way.
- Secrets are redacted before display; redaction chips name the rule and never
the value. See [authorization and exposure](phase-07-authorization-and-exposure.md).
the value. See [authorization and exposure](capability-authorization-and-exposure.md).
## Accessibility
@@ -66,20 +66,19 @@ a time with zero horizontal overflow.
```sh
# Open the explorer on 127.0.0.1:4183.
pnpm --filter @paperclipai/paperclip-runner demo:phase7
pnpm --filter @paperclipai/paperclip-runner demo:scenarios
# Scenario runtime, explorer components, and route determinism (49 tests).
pnpm --filter @paperclipai/paperclip-runner test:phase7
pnpm --filter @paperclipai/paperclip-runner test:scenarios
# Browser IA, determinism, evidence routes, boundary, a11y, responsive (25).
pnpm --filter @paperclipai/paperclip-runner test:browser:phase7
pnpm --filter @paperclipai/paperclip-runner test:browser:scenarios
# Deterministic 24-image acceptance set (12 routes x 2 viewports).
pnpm --filter @paperclipai/paperclip-runner record:phase7
pnpm --filter @paperclipai/paperclip-runner record:capability
```
## Related
- [Eval conformance](phase-07-eval-conformance.md)
- [Verification commands](phase-07-verification-commands.md)
- [Phase 7F explorer verification](../knowledge/evidence/2026-08-09-phase-07f-explorer-verification.md)
- [Eval conformance](capability-eval-conformance.md)
- [Verification commands](capability-verification-commands.md)
@@ -1,6 +1,6 @@
# Phase 7 semantic catalog and authorization
# Capability semantic catalog and authorization
Phase 7D adds a transport-neutral tool boundary over the deterministic mock
Capability adds a transport-neutral tool boundary over the deterministic mock
`ControlPlanePort`. It does not contain Paperclip REST routes, authentication
headers, credentials, ACPX code, or a real control-plane binding.
@@ -11,11 +11,11 @@ root:
```ts
import {
Phase7SemanticDispatcher,
createPhase7ProviderNeutralBinding,
CapabilitySemanticDispatcher,
createCapabilityProviderNeutralBinding,
} from "@paperclipai/paperclip-runner";
const dispatcher = new Phase7SemanticDispatcher(mockPort, {
const dispatcher = new CapabilitySemanticDispatcher(mockPort, {
scenario: {
id: "dependency-manager",
claims: ["dependencies:write"],
@@ -35,8 +35,8 @@ const result = await dispatcher.dispatch({
});
```
`createPhase7ProviderNeutralBinding("fake")` and
`createPhase7ProviderNeutralBinding("live_codex")` return identical contract
`createCapabilityProviderNeutralBinding("fake")` and
`createCapabilityProviderNeutralBinding("live_codex")` return identical contract
arrays. Only the binding label differs. Track 7E can translate those definitions
to Codex tool definitions and pass calls to the dispatcher without changing
operation IDs, schemas, claims, or result shapes.
@@ -75,5 +75,5 @@ pnpm --dir packages/paperclip-runner check:semantic-contracts
```
The generated contract is
`generated/phase-07/semantic-tool-contracts.json`. Phase 7A traceability remains
under `generated/phase-07/` and is checked with `check:phase-07-contract`.
`generated/capability/semantic-tool-contracts.json`. Capability traceability remains
under `generated/capability/` and is checked with `check:capability-contract`.
@@ -1,12 +1,12 @@
# Phase 7 Semantic Tool Catalog
# Capability Semantic Tool Catalog
The semantic tool catalog is the transport-neutral surface an agent may call. It
is a frozen set of JSON-schema descriptors — no HTTP method, path, or provider
detail. Provider-specific shapes are produced only by *bindings*, so every
provider sees the same operation set.
Sources: `src/tools/phase7-semantic-tool-catalog.ts`,
`src/tools/phase7-semantic-tool-types.ts`, `src/tools/phase7-tool-bindings.ts`,
Sources: `src/tools/capability-semantic-tool-catalog.ts`,
`src/tools/capability-semantic-tool-types.ts`, `src/tools/capability-tool-bindings.ts`,
barrel `src/tools/index.ts`.
## Two dispositions, plus a separate control-plane list
@@ -14,8 +14,8 @@ barrel `src/tools/index.ts`.
A tool descriptor's `disposition` is either **`always_agent_tool`** or
**`optional_agent_tool`**. Control-plane-owned operations are a separate frozen
list, not a disposition — they are never exposed as tools. See
[capability disposition](phase-07-capability-disposition.md) and
[authorization and exposure](phase-07-authorization-and-exposure.md).
[capability disposition](capability-disposition.md) and
[authorization and exposure](capability-authorization-and-exposure.md).
The catalog holds **36 tools**: 14 always-agent tools and 22 optional tools
across 10 groups.
@@ -60,20 +60,20 @@ the tool does against the mock, never *how* a transport would carry it.
Bindings, not descriptors, produce provider shapes, both derived from the same
`visibleTools.tools` array:
- `Phase7FakeAgentToolBinding` emits `{operationId, description, inputSchema,
- `CapabilityFakeAgentToolBinding` emits `{operationId, description, inputSchema,
outputSchema}`.
- `Phase7CodexToolBinding` emits `{type: "function", name, description, strict:
- `CapabilityCodexToolBinding` emits `{type: "function", name, description, strict:
true, parameters}`.
Because both bindings derive from one array, the fake-agent and Codex operation
surfaces are byte-identical. The [conformance suite](phase-07-eval-conformance.md)
surfaces are byte-identical. The [conformance suite](capability-eval-conformance.md)
asserts this parity (the 18/18 fake-agent/Codex operation matrix).
## Running the tests
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/tools/phase7-semantic-tools.test.ts
src/tools/capability-semantic-tools.test.ts
```
The "catalog" describe asserts a unique, versioned, provider-neutral catalog and
@@ -81,6 +81,6 @@ that every optional group is present.
## Related
- [Authorization and exposure](phase-07-authorization-and-exposure.md)
- [Mock ControlPlanePort](phase-07-mock-control-plane-port.md)
- [Eval conformance](phase-07-eval-conformance.md)
- [Authorization and exposure](capability-authorization-and-exposure.md)
- [Mock ControlPlanePort](capability-mock-control-plane-port.md)
- [Eval conformance](capability-eval-conformance.md)
@@ -0,0 +1,52 @@
# Capability Verification Commands
Every command here runs from the repository root, is offline and deterministic,
starts no Paperclip service, and holds no credential. All are prefixed
`pnpm --filter @paperclipai/paperclip-runner`.
| Surface | Command | Expected result |
| --- | --- | --- |
| Capability contract completeness and drift | `check:capability-inventory` | `Capability inventory completeness and generated-output checks passed.` |
| Contract validator negatives | `test:capability-inventory` | 4 tests pass |
| Mock control plane + shared port | `exec vitest run src/conformance/control-plane-port.test.ts src/mock-core/capability-mock-control-plane-adapter.test.ts` | 9 tests pass |
| Semantic tools + authorization/redaction | `exec vitest run src/tools/capability-semantic-tools.test.ts` | 9 tests pass |
| 106-case conformance | `test:capability-evals` | 1 file (all 106 cases) passes |
| Fake-agent matrix + bounded Codex + parity report | `report:capability-evals` | `Capability eval conformance passed: 106 cases across 16 groups.` |
| Scenario runtime + explorer + clean room + routes | `test:scenarios` | 159 tests pass |
| Browser IA, accessibility, determinism, boundary | `test:browser:scenarios` | Playwright suites pass (60 in the issue-thread/clean-room suite) |
| Screenshot acceptance set | `record:capability` | 24 deterministic images |
| Doc links + OKF bundle | `docs:validate` | 73 files, 28 OKF concepts, 5 indexes |
## Live commands (not offline)
The clean-room chat has no fake or replay path, so its end-to-end proof needs a
Rust toolchain and a locally authenticated Codex. These are the only Capability
commands that start a provider.
| Surface | Command | Expected result |
| --- | --- | --- |
| Real Codex through real runnerd on a fresh mock tenant | `smoke:capability:cleanroom` | every assertion `true`; two `MCK-` identifiers, one per chat |
| Preset issue thread against real Codex | `smoke:capability:ui` | every assertion `true` |
| Clean-room screenshots | `record:capability:cleanroom` | 7 images; intentionally not byte-stable |
## Notes
- **No Rust toolchain is needed** for any Capability command. The full
`verify` target still builds the Rust workspace, but nothing above does.
- **Determinism.** Fake-mode runs render fixture time only, so repeat runs and
repeat screenshots are byte-identical.
- **The parity report is generated on demand.** `report:capability-evals` writes
`knowledge/evidence/capability/eval-parity-report.{json,md}`, which are not
committed. Because the `.md` carries no OKF frontmatter, run `docs:validate`
**before** generating the report, or delete the report first. This dirty-tree
interaction is tracked for the Capability QA gate (`PAP-16907`).
- **Browser libraries.** On minimal or rootless hosts, extract Playwright's
browser libraries first; the package's `verify:rootless` target shows the
pattern.
## Related
- [Clean-start tutorial](tutorials/capability-scenario-explorer.md)
- [Eval conformance](capability-eval-conformance.md)
- [Scenario explorer](capability-scenario-explorer.md)
- [Clean-room live chat](capability-clean-room-chat.md)
@@ -1,8 +1,8 @@
# Phase 4 Skillless Codex Driver
# Codex Skillless Codex Driver
## Scope
Phase 4 implements a direct Codex app-server v2 driver behind the package's
Codex implements a direct Codex app-server v2 driver behind the package's
existing `HarnessDriver` contract. The driver, mock core, example CLI, tests,
and evidence stay inside `packages/paperclip-runner/`. They do not import or
change Paperclip server, UI, database, or production control-plane behavior.
@@ -78,7 +78,7 @@ workspace overlapping host `CODEX_HOME`.
The returned sandbox facts remain authoritative. Codex 0.132.0 may inject a
provider-managed writable root such as `~/.codex/memories` after a first run,
even with `features.memories=false` and an explicit Codex-home deny. That makes
the Codex-home directory discoverable in a warmed environment, so Phase 4 does
the Codex-home directory discoverable in a warmed environment, so Codex does
not claim whole-directory unreadability. Its authenticated proof instead
requires each readable `auth.json`/`config.toml` file and an unrelated host
secret to remain unreadable and unwritable, while recording any injected root
@@ -142,22 +142,22 @@ in-flight server requests, queued notification count and bytes, diagnostic
lines, and retained provider payloads. Malformed or oversized messages close
the transport and reject pending work.
The existing Phase 1 reducer consumes the live stream. Replay crosses a
The existing Replay reducer consumes the live stream. Replay crosses a
serialized JSONL boundary that validates byte and event counts, line size,
schema, run/session binding, unique source event IDs, continuous per-source
sequence, and exactly one final run terminal before reducing. The Phase 4
sequence, and exactly one final run terminal before reducing. The Codex
tracer requires byte-equivalent live and replay snapshots.
## Runnable example
`trace:phase4` starts a real local `codex app-server` session through the mock
`trace:codex` starts a real local `codex app-server` session through the mock
core. Its safe task creates `hello.txt` with network disabled. The evidence
recorder additionally probes all readable host Codex credential/config files
and an unrelated host secret, requiring reads and writes to be denied while
workspace output and app-server authentication still succeed. It gates output
reads on an accepted `done` result and reports missing files by name rather
than surfacing a raw filesystem `ENOENT`. See the
[Phase 4 tutorial](tutorials/phase-04-skillless-codex.md).
[Codex tutorial](tutorials/codex.md).
This phase changes no browser surface, so no new browser screenshot applies.
The canonical events are proved through the existing reducer/replay path and
@@ -1,10 +1,10 @@
# Phase 7B — Issue-Thread UX Contract
# Capability — Issue-Thread UX Contract
Status: **binding** for track 7G (Paperclip-style Web UI) and reviewable by tracks 7E/7K.
Source of authority: Phase 7 plan (PAP-16897 `plan` document), Revision 3 §"Page information
Source of authority: Capability plan (PAP-16897 `plan` document), Revision 3 §"Page information
architecture", §"Native issue-thread interactions", §"Live execution contract", and Revision 4
§"Final evidence eligibility". Capability vocabulary: Phase 7A generated contract
(`generated/phase-07/capabilities.yaml`, `mcp-tool-map.yaml`, `eval-traceability.yaml`,
§"Final evidence eligibility". Capability vocabulary: Capability generated contract
(`generated/capability/capabilities.yaml`, `mcp-tool-map.yaml`, `eval-traceability.yaml`,
`contract-schema.json` — PAP-16939).
If a corner of this contract is underspecified, 7G comments on PAP-16940 and waits for a
@@ -105,7 +105,7 @@ what runs it, what control plane it talks to, and whether this session is live.
active). Row 2 = the three identity chips, wrapping to a second line if needed — chips never
cause horizontal page scroll.
- A **segmented control** with exactly two segments — `Thread` and `Evidence` — sits under the
header (pattern validated in the Phase 7I mobile review; a drawer was rejected there and
header (pattern validated in the Scenario chat mobile review; a drawer was rejected there and
stays rejected). Thread is default. Badge on `Evidence` shows the current turn's
authorization-denial count when nonzero.
- Composer is fixed to the bottom of the `Thread` segment, above the keyboard inset
@@ -147,7 +147,7 @@ States (mutually exclusive; `data-composer-state` attribute is the test/screensh
| State | Trigger | Visual | Controls |
|-------|---------|--------|----------|
| `ready` | session attached, no active turn, issue non-terminal | normal input, `Send` primary | Send (Cmd/Ctrl+Enter), attach disabled in Phase 7 |
| `ready` | session attached, no active turn, issue non-terminal | normal input, `Send` primary | Send (Cmd/Ctrl+Enter), attach disabled in Capability |
| `sending` | message posted, turn not yet streaming | input cleared, inline spinner on Send | Send disabled |
| `streaming` | active turn | input stays **editable** (steer), primary button becomes `Stop` (destructive-outline), helper text `Codex is working — send to steer, or stop the turn.` | Send = steer (queued as next user input), Stop |
| `waiting` | pending interaction card requires the user | input disabled, helper `Answer the pending request above to continue.` with an anchor link that scrolls to and focuses the pending card | none |
@@ -334,7 +334,7 @@ Follow the Paperclip design language without importing the product `ui/` package
### 10.2 Required matrix (12 slugs × 2 viewports = 24 PNGs)
Viewports: desktop `1440×900`, mobile `390×844`. Output path:
`knowledge/evidence/phase-07/ui/<slug>--<desktop|mobile>.png` (package-local). Every capture
`knowledge/evidence/capability/ui/<slug>--<desktop|mobile>.png` (package-local). Every capture
also asserts `scrollWidth <= clientWidth` on the scrolling element at 390×844.
| Slug | Seeded state | Must be visible |
@@ -353,13 +353,13 @@ also asserts `scrollWidth <= clientWidth` on the scrolling element at 390×844.
| `replay-mode` | replay of recorded fake run `?at=12` | `Replay · fake source` chip, read-only composer, progress strip |
Determinism rule: two captures of the same slug/viewport from a clean checkout must be
pixel-identical (the Phase 7I byte-identical bar). Anything time-, random-, or
pixel-identical (the Scenario chat byte-identical bar). Anything time-, random-, or
locale-dependent renders from fixture data.
### 10.3 Evidence naming
Per-eval-case evidence (7F/7K scope) reuses `browserEvidenceRecipe` verbatim:
`knowledge/evidence/phase-07/cases/<group>/<case-id>--<viewport>.png`. The §10.2 matrix is
`knowledge/evidence/capability/cases/<group>/<case-id>--<viewport>.png`. The §10.2 matrix is
the UI acceptance set; case evidence is additive and follows the same settle/determinism
rules.
@@ -1,16 +1,15 @@
# Phase 7 Interaction Map — Scenario Explorer and Screenshot Acceptance Spec
# Capability Interaction Map — Scenario Explorer and Screenshot Acceptance Spec
Status: **approved UX contract for 7F implementation** (PAP-16899, 2026-08-09).
Owner: UXDesigner. Implementer: 7F (browser scenario explorer), integrating
7C mock adapter output and 7E parity output.
Companion records: [Phase 7A capability contract](../phase-07-capability-contract.md),
`spec/phase-07/eval-traceability.yaml`, the Phase 7 plan (PAP-16897),
[Phase 4b interaction map](phase-4b-interaction-map.md),
[Phase 5 component decisions](phase-5-component-decisions.md).
Mockup: [`mockups/phase-7-scenario-explorer.html`](mockups/phase-7-scenario-explorer.html)
(annotated desktop + mobile renders in `knowledge/evidence/phase-07/`).
Companion records: [Capability contract](../capability-contract.md),
`spec/capability/eval-traceability.yaml`, the Capability plan (PAP-16897),
[Live console interaction map](live-console-interaction-map.md),
[SDK component decisions](sdk-component-decisions.md).
(annotated desktop + mobile renders in `knowledge/evidence/capability/`).
This map is the interaction contract for the Phase 7 browser scenario
This map is the interaction contract for the Capability browser scenario
explorer. The explorer is a **read surface over records the runtime already
produced**. It renders scenario metadata from the generated 7A traceability
manifest, run artifacts from the 7C mock control-plane adapter, and parity
@@ -22,7 +21,7 @@ on screen must arrive as a record emitted by the mock core.
## 0. Naming, vocabulary, and canonical labels
- Product name in the UI: **Scenario Explorer** (header: “Phase 7 scenario
- Product name in the UI: **Scenario Explorer** (header: “Capability scenario
explorer — mock control plane”).
- UI copy uses the canonical term **task** (`DESIGN.md` principle 7). Eval
case IDs, fixture IDs, route paths, JSON payloads, and source anchors are
@@ -43,14 +42,14 @@ on screen must arrive as a record emitted by the mock core.
## 1. Shell and information architecture
Package-local Vite entry `examples/phase7-explorer/` with script
`demo:phase7` (default port 4183; 4182 belongs to the Phase 6 demo). It runs
Package-local Vite entry `examples/scenario-explorer/` with script
`demo:scenarios` (default port 4183; 4182 belongs to the Standalone demo). It runs
from static assets plus checked-in fixtures with **no Paperclip services and
no network dependency** in fake-agent mode. Reuse the frozen `0.1.2` SDK
surface (`@paperclipai/paperclip-runner/react` + `./styles.css`) through its
five approved extension points; do not fork the token layer.
Desktop layout (≥ 64rem), one React tree (Phase 5 finding — never render two
Desktop layout (≥ 64rem), one React tree (SDK finding — never render two
trees):
- **Left rail (`--pcr-rail-width`, 17rem):** scenario picker — search,
@@ -64,7 +63,7 @@ trees):
first-class acceptance evidence, not diagnostics).
Mobile (< 64rem): single column behind a segmented control
(`Scenarios · Run · Inspect`), same pattern as the Phase 4b console. The
(`Scenarios · Run · Inspect`), same pattern as the Live console console. The
segmented control is sticky at the top; the Run segment is the default when
a case is selected via route. Touch targets ≥ `--pcr-touch-target` (2.75rem).
@@ -75,7 +74,7 @@ check:token-gates` applies).
## 2. Scenario picker and filters (left rail)
**Source of truth:** the generated scenario index derived from
`spec/phase-07/eval-traceability.yaml` (all 106 cases, 16 groups) plus the
`spec/capability/eval-traceability.yaml` (all 106 cases, 16 groups) plus the
latest parity results artifact from 7E. The picker consumes these; it never
re-derives group membership or dispositions.
@@ -130,7 +129,7 @@ never a bare void.
when the local relay is reachable; otherwise the option renders disabled
with the reason (“provider relay not running — see tutorial §4”). The
browser never holds a provider credential; Codex traffic goes through the
same server-side relay boundary proven in Phase 5.
same server-side relay boundary proven in SDK.
- **Run / Re-run** button and replay controls (SDK `replay-controls`).
Deterministic fixtures mean re-run in fake mode reproduces the identical
timeline; state diffs and parity always describe the displayed run.
@@ -172,11 +171,11 @@ operations, none invoked” which deep-links the Parity tab’s forbidden list.
Restraint evidence must never depend on an empty screen.
PRP events stay behind a per-item “Protocol events” disclosure (same nested
pattern as Phase 5 `debugEvents`) — the semantic view is primary; raw
pattern as SDK `debugEvents`) — the semantic view is primary; raw
protocol stays inspectable but secondary (progressive disclosure).
Auto-follow, jump-to-latest, streaming affordances, and reduced-motion rules
are inherited verbatim from the Phase 4b map §1 — do not redesign them.
are inherited verbatim from the Live console map §1 — do not redesign them.
## 4. Inspector tabs (right rail)
@@ -268,7 +267,7 @@ The eval acceptance surface:
## 6. Accessibility, responsive, empty/error states
Accessibility (WCAG POUR; inherits all Phase 4b/5 baseline rules):
Accessibility (WCAG POUR; inherits all Live console/5 baseline rules):
- Landmarks: `nav` (picker), `main` (run view), `complementary`
(inspector); one `h1`; group headers are `h2`/`h3` in order.
@@ -281,7 +280,7 @@ Accessibility (WCAG POUR; inherits all Phase 4b/5 baseline rules):
assertively once.
- Reduced motion: with `prefers-reduced-motion`, streaming reveal, banner
transitions, and scroll-flash connect cues render instantly/statically
(Phase 5 rule).
(SDK rule).
- Contrast from the existing token pairs only; parity/authorization
statuses always icon + text.
@@ -289,7 +288,7 @@ Responsive: content-driven single breakpoint at 64rem (§1). At mobile the
inspector tabs become full-width stacked sections inside the Inspect
segment; expected-vs-observed blocks stack; the transcript keeps every
descendant intrinsically shrinkable (`min-w-0` discipline) so no horizontal
scrollbar appears at 390px (Phase 5 finding).
scrollbar appears at 390px (SDK finding).
States that must be designed, not defaulted:
@@ -301,7 +300,7 @@ States that must be designed, not defaulted:
| Run failed (harness error) | Run view | Danger banner with the exact diagnostic + “Re-run”; partial timeline stays visible and labeled partial. Never silently reset. |
| Parity fail | Banner/Parity | Fail is a first-class rendered state (danger banner + failing assertion rows) — the explorer’s job is showing it, not avoiding it. |
| Denied/absent tool | Transcript/Context | Per §5. |
| Missing/invalid fixture or artifact | Shell | Error card naming the artifact path and the regenerate command (`pnpm generate:phase7-inventory` / 7E command). No blank screen. |
| Missing/invalid fixture or artifact | Shell | Error card naming the artifact path and the regenerate command (`pnpm generate:capability-inventory` / 7E command). No blank screen. |
| Codex relay unavailable | Mode control | Disabled option + reason; fake mode remains fully usable. |
## 7. Deterministic screenshot routes
@@ -386,7 +385,7 @@ Determinism contract for screenshot/CI use:
## 9. Screenshot acceptance matrix
Evidence lands in `knowledge/evidence/phase-07/` as
Evidence lands in `knowledge/evidence/capability/` as
`<slug>-<viewport>.png`, at both **1440×900** and **390×844**, captured from
the §7 routes after `data-run-state="settled"`. These twelve shots are the
UX acceptance set for 7F (they also cover the plan’s human-checkpoint
@@ -421,7 +420,7 @@ fails the gate.
## 10. Out of scope / rejected
- No second app shell, no Tailwind/Radix, no dark mode, no markdown
renderer, no command palette, no transcript virtualization — Phase 5
renderer, no command palette, no transcript virtualization — SDK
rejections stand.
- No editing affordances of any kind: fixtures, grants, and policies are
changed in code/YAML, not in the browser. The mode toggle and run/replay
@@ -445,25 +444,25 @@ of the contract.
event references ride on `semantic_result.protocolEventRefs` rather than
standing as their own entry, keeping the semantic view primary.
2. **Parity is computed by the runtime, not the browser — and not by 7E.**
2. **Parity is computed by the runtime, not the browser.**
§4.4 and §10 forbid the UI from judging assertions, and they do: the
package-local scenario runner (`src/phase7/scenario-parity.ts`) produces the
verdict from the traceability expectations and the recorded artifact. Phase
7E's own per-case result is carried through verbatim in a separately
labelled "Phase 7E conformance suite" block and is never merged into, or
used to overrule, the assertion list. When no 7E report is bundled the block
package-local scenario runner (`src/scenarios/scenario-parity.ts`) produces the
verdict from the traceability expectations and the recorded artifact. The
conformance suite's own per-case result is carried through verbatim in a separately
labelled "Capability conformance suite" block and is never merged into, or
used to overrule, the assertion list. When no conformance report is bundled the block
reads **Not run**.
3. **Actor role, task mode, and scenario claims are declared, not inferred.**
§8 assigned `actorRole`/`taskMode` to 7C's index generator; 7C shipped
without them. They are now declared in `src/phase7/scenario-index.ts` as an
§8 assigned `actorRole`/`taskMode` to the scenario index generator; it shipped
without them. They are now declared in `src/scenarios/scenario-index.ts` as an
explicit fixture/case profile table, alongside the catalog claims each
scenario grants. A claim that cannot unlock any operation in the scenario's
task mode or for its actor role is dropped from the profile, so no scenario
renders a grant chip that unlocks nothing.
4. **The mobile section switcher is a `radiogroup`, not a `tablist`.** §1 asked
for the Phase 4b segmented control. The Phase 4b console renders it only in
for the Live console segmented control. The Live console console renders it only in
compact mode; the explorer keeps one CSS-driven tree, so a `tablist` would
put two tablists in the document and model the three landmarks as tabpanels.
The three regions stay `nav`/`main`/`complementary`, and the only `tablist`
@@ -475,7 +474,7 @@ of the contract.
squeezed below their own content and pushed the page into horizontal scroll.
The SDK badge also applies `text-transform: capitalize`, which renders
"Optional Tool" and "Not Run" against the §0 vocabulary. Both are corrected
in `examples/phase7-explorer/src/explorer.css` under `.pcr7-shell`; the
in `examples/scenario-explorer/src/explorer.css` under `.pcr7-shell`; the
frozen `0.1.2` sheet is unchanged. **These are SDK defects worth fixing at
the next SDK revision, not explorer quirks.**
@@ -1,10 +1,10 @@
# Phase 4b Component Decision Record — shadcn/ui and Vercel AI Elements
# Live console Component Decision Record — shadcn/ui and Vercel AI Elements
Status: **approved** (UX gate PAP-16832, 2026-08-08). Sources checked
2026-08-08 against the live shadcn/ui and AI Elements documentation; this
supersedes and extends the
[2026-08-07 compatibility note](../research/2026-08-07-ui-library-compatibility.md)
for the authorized Phase 4b browser surface.
for the authorized Live console browser surface.
## 1. Ground rules (binding on the implementation)
@@ -26,14 +26,14 @@ for the authorized Phase 4b browser surface.
pull a runtime dep (radix primitives, `cmdk`, `streamdown`, `shiki`,
`use-stick-to-bottom`, `nanoid`, `zod`) is either rebuilt on native
platform primitives (`<dialog>`, `<details>`, ARIA patterns) or rejected.
Rationale: Phase 4b is a proof under `packages/paperclip-runner/` that
Phase 5 will freeze into an SDK — every dependency added here becomes an
Rationale: Live console is a proof under `packages/paperclip-runner/` that
SDK will freeze into an SDK — every dependency added here becomes an
SDK liability decision later.
4. **Accessibility parity or better.** Where we rebuild a radix-backed
pattern on native primitives, the keyboard/SR contract in interaction map
§10 is the acceptance bar; "the library would have done it" is not
available as an excuse once we adapt source.
5. **Token gaps are system changes.** Phase 4b needs a few new tokens (see
5. **Token gaps are system changes.** Live console needs a few new tokens (see
§5). Add them to `src/styles.css` `:root` in one commit with this record —
never inline values in component files.
@@ -64,19 +64,19 @@ re-typed to reducer snapshots and restyled to tokens.
| `Reasoning` | `ui/reasoning-item.tsx` | collapsible streaming reasoning with auto-open-while-streaming, duration caption | rebuild on `<details>`/summary with ARIA disclosure; content = plain streamed text (no markdown dep) |
| `Tool` | `ui/tool-item.tsx` | collapsible tool block: header (name + status badge) / input / output sections | status enum mapped from canonical item/turn events, not AI SDK `ToolUIPart` states; payloads render in `<pre>` |
**REJECT (with reasons; revisit in Phase 5 where noted):**
**REJECT (with reasons; revisit in SDK where noted):**
| Primitive | Reason |
|---|---|
| `Response` (streamed markdown) | pulls `streamdown`/markdown pipeline; tracer renders exact text — markdown prettification can hide protocol truth. Phase 5 candidate for the SDK console. |
| `CodeBlock` | needs `shiki` highlighting dep; `<pre>` + mono token suffices for a tracer. Phase 5 candidate. |
| `Response` (streamed markdown) | pulls `streamdown`/markdown pipeline; tracer renders exact text — markdown prettification can hide protocol truth. SDK candidate for the SDK console. |
| `CodeBlock` | needs `shiki` highlighting dep; `<pre>` + mono token suffices for a tracer. SDK candidate. |
| `Actions` (retry/like/copy row) | retry/regenerate/vote have no protocol backing; copy-to-clipboard is implemented locally in the inspector. |
| `Branch` | message branching does not exist in the PRP session model. |
| `Sources` / `InlineCitation` | no citation events in the protocol. |
| `Task` | overlapping with adapted `Tool`; subagent lineage uses the tree (map §6), not a task widget. |
| `ChainOfThought` | duplicate of `Reasoning` at higher visual weight. |
| `Context` (token/cost meter) | usage belongs in the inspector Session tab as plain data; a persistent meter overweights cost in a tracer. Phase 5 candidate. |
| `Artifact`, `WebPreview`, `Image`, `Attachments`, `OpenIn`, `Suggestion`, `Queue` | no protocol counterpart in Phase 4b; adopting them would invent affordances the runner cannot honor (fabricated-controls rule). |
| `Context` (token/cost meter) | usage belongs in the inspector Session tab as plain data; a persistent meter overweights cost in a tracer. SDK candidate. |
| `Artifact`, `WebPreview`, `Image`, `Attachments`, `OpenIn`, `Suggestion`, `Queue` | no protocol counterpart in Live console; adopting them would invent affordances the runner cannot honor (fabricated-controls rule). |
| `Loader`/`Shimmer` | trivially replaced by a token-compliant CSS pulse honoring `prefers-reduced-motion`. |
## 4. shadcn/ui — decisions per primitive
@@ -96,10 +96,10 @@ re-typed to reducer snapshots and restyled to tokens.
| Primitive | Reason |
|---|---|
| `Command` (cmdk palette) | the goal surface has five fixed verbs — a searchable palette adds a dep and search UI for nothing (Hick's law works in our favor with a plain menu). Revisit only if goal/command vocabulary grows in Phase 5+. |
| `Command` (cmdk palette) | the goal surface has five fixed verbs — a searchable palette adds a dep and search UI for nothing (Hick's law works in our favor with a plain menu). Revisit only if goal/command vocabulary grows in SDK+. |
| `ScrollArea` | custom scrollbars are cosmetic; native scrolling is more accessible and free. |
| `Accordion`/`Collapsible` | native `<details>` covers reasoning/tool/observation disclosures. |
| `Sheet`/`Drawer`, `Popover`, `Select`, `Switch`, `Progress`, `Skeleton`, `Toast`/`Sonner` | no Phase 4b surface needs them; toasts specifically are rejected because failures must live in the transcript record, not ephemeral notifications (map §1.5). |
| `Sheet`/`Drawer`, `Popover`, `Select`, `Switch`, `Progress`, `Skeleton`, `Toast`/`Sonner` | no Live console surface needs them; toasts specifically are rejected because failures must live in the transcript record, not ephemeral notifications (map §1.5). |
| `Table` | inspector uses definition lists / simple grids; sortable tables are not needed. |
| `Sidebar` | the existing app-shell layout already provides rails; adopting the shadcn sidebar would restructure the proven shell. |
@@ -128,7 +128,7 @@ Contrast pairs must pass map §10-V1; verify `--accent` on `--accent-surface`
`streamdown`, or `shiki` anywhere under `devtools/browser/`.
- Do not add a second event model, message store, or client-side session
cache that could diverge from the reducer (map §7.2).
- Do not restyle existing Phase 1–3 surfaces beyond shared-token additions;
- Do not restyle existing Replay–3 surfaces beyond shared-token additions;
their screenshots are frozen QA evidence.
## 7. Evidence checklist for this record
@@ -139,7 +139,5 @@ Contrast pairs must pass map §10-V1; verify `--accent` on `--accent-surface`
- [x] Existing package idiom confirmed (`ui/button.tsx` et al. — plain CSS
classes on tokens, React 19, no Tailwind).
- [x] Interaction map cross-references: every ADAPT row maps to a surface in
the [interaction map](phase-4b-interaction-map.md); every surface has a
the [interaction map](live-console-interaction-map.md); every surface has a
component owner.
- [x] Annotated mockups rendered at 1440×900 and 390×844 — see
[`mockups/`](mockups/).
@@ -1,11 +1,9 @@
# Phase 4b Interaction Map — Live Codex Protocol Web UI
# Live console Interaction Map — Live Codex Protocol Web UI
Status: **approved for implementation** (UX gate PAP-16832, 2026-08-08).
Owner: UXDesigner. Implementer: browser tracer owner (PAP-16834).
Companion record: [component decisions](phase-4b-component-decisions.md).
Mockups: [`mockups/`](mockups/) (annotated desktop + mobile renders).
This map is the interaction contract for the Phase 4b live console required by
Companion record: [component decisions](live-console-component-decisions.md).
This map is the interaction contract for the Live console required by
spec §29.1. It binds every surface to canonical Paperclip Runner Protocol (PRP)
events and reducer state only. No surface may derive state from third-party
component message types, and no surface may show a control the upstream
@@ -14,7 +12,7 @@ capability set does not support.
## 0. Shell and information architecture
Extend the existing devtools browser app (`devtools/browser/`) with a new
**Live console** mode next to the existing Phase 1–3 modes. Do not build a
**Live console** mode next to the existing Replay–3 modes. Do not build a
second app shell. Reuse the existing token layer in `src/styles.css` and the
existing `ui-*` primitives.
@@ -245,7 +243,7 @@ inference.
visually distinguished by an eyebrow badge `REPLAY` in the header — never
let replay be mistakable for live (trust).
5. Never offer "start new session" as the recovery path for a drop — resume
the exact session (Phase 4 rule: never replace a lost session).
the exact session (Codex rule: never replace a lost session).
## 8. Protocol inspector
Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

@@ -1,374 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Phase 4b Live Console — annotated mockup (PAP-16832)</title>
<style>
/* Token layer: values mirror devtools/browser/src/styles.css, plus the
Phase 4b token additions proposed in the component decision record §5. */
: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;
--accent: #2f5fa8; --accent-surface: #e9f0fa;
--surface-raised: #fcfcfa;
--ring: #66665f;
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--fs-xs: 0.75rem; --fs-sm: 0.875rem; --fs-md: 1rem; --fs-lg: 1.25rem;
--w-med: 500; --w-semi: 600;
--s-1: 0.25rem; --s-2: 0.5rem; --s-3: 0.75rem; --s-4: 1rem; --s-5: 1.25rem; --s-6: 1.5rem; --s-8: 2rem;
--r-sm: 0.375rem; --r-md: 0.625rem;
--shadow-sm: 0 0.0625rem 0.125rem rgb(0 0 0 / 0.05);
}
* { box-sizing: border-box; }
body { margin:0; background:var(--background); color:var(--foreground);
font:var(--fs-sm)/1.5 var(--font-sans); }
code, .mono { font-family: var(--font-mono); font-size: var(--fs-xs); }
h1,h2,h3,p { margin:0; }
.console { display:grid; grid-template-columns: 17rem minmax(0,1fr) 24rem;
gap:var(--s-4); width:min(100%,90rem); margin-inline:auto; padding:var(--s-4); }
header.top { grid-column:1/-1; display:flex; align-items:center; gap:var(--s-3);
padding:var(--s-2) var(--s-1); }
.eyebrow { font-size:var(--fs-xs); letter-spacing:.08em; text-transform:uppercase;
color:var(--muted-foreground); font-weight:var(--w-semi); }
header.top h1 { font-size:var(--fs-lg); font-weight:var(--w-semi); margin-right:auto; }
.panel { background:var(--card); border:1px solid var(--border); border-radius:var(--r-md);
box-shadow:var(--shadow-sm); }
.panel + .panel { margin-top:var(--s-4); }
.panel-h { display:flex; align-items:center; gap:var(--s-2); padding:var(--s-3) var(--s-4);
border-bottom:1px solid var(--border); font-weight:var(--w-semi); font-size:var(--fs-sm); }
.panel-b { padding:var(--s-3) var(--s-4); }
.badge { display:inline-flex; align-items:center; gap:var(--s-1); border-radius:999px;
padding:0 var(--s-2); font-size:var(--fs-xs); font-weight:var(--w-med);
border:1px solid var(--border); background:var(--muted); color:var(--muted-foreground); height:1.25rem;}
.badge.ok { background:var(--success-surface); color:var(--success); border-color:transparent;}
.badge.warn { background:var(--warning-surface); color:var(--warning); border-color:transparent;}
.badge.err { background:var(--danger-surface); color:var(--danger); border-color:transparent;}
.badge.info { background:var(--accent-surface); color:var(--accent); border-color:transparent;}
.dot { width:.5rem; height:.5rem; border-radius:999px; display:inline-block; }
.dot.ok{background:var(--success);} .dot.run{background:var(--accent);}
.dot.err{background:var(--danger);} .dot.mut{background:var(--muted-foreground);}
.btn { display:inline-flex; align-items:center; justify-content:center; gap:var(--s-1);
height:2.25rem; padding:0 var(--s-4); border-radius:var(--r-sm); font-weight:var(--w-med);
font-size:var(--fs-sm); border:1px solid var(--border); background:var(--card); cursor:default;}
.btn.primary { background:var(--primary); color:var(--primary-foreground); border-color:var(--primary);}
.btn.danger { background:var(--danger-surface); color:var(--danger); border-color:transparent;}
.btn.ghost { border-color:transparent; background:transparent; color:var(--muted-foreground);}
.btn.sm { height:1.75rem; padding:0 var(--s-3); font-size:var(--fs-xs);}
.btn[aria-disabled="true"] { opacity:.5; }
/* left rail */
.manifest { display:flex; flex-direction:column; gap:var(--s-1); padding:var(--s-2) var(--s-3);
border-radius:var(--r-sm); border:1px solid transparent; }
.manifest.active { border-color:var(--accent); background:var(--accent-surface); }
.manifest .t { font-weight:var(--w-med); display:flex; justify-content:space-between; align-items:center;}
.manifest .d { color:var(--muted-foreground); font-size:var(--fs-xs); }
.kv { display:flex; justify-content:space-between; gap:var(--s-2); padding:var(--s-1) 0;
font-size:var(--fs-xs); }
.kv .k { color:var(--muted-foreground); }
.goal { border-left:3px solid var(--accent); background:var(--accent-surface);
border-radius:var(--r-sm); padding:var(--s-2) var(--s-3); font-size:var(--fs-xs); }
.tree { list-style:none; margin:0; padding:0; font-size:var(--fs-xs); }
.tree li { display:flex; align-items:center; gap:var(--s-2); padding:var(--s-1) var(--s-2);
border-radius:var(--r-sm);}
.tree li.cur { background:var(--muted); font-weight:var(--w-med); }
.tree li.child { padding-left:var(--s-6); }
/* transcript */
.chat { display:flex; flex-direction:column; min-height:34rem; }
.stream { flex:1; display:flex; flex-direction:column; gap:var(--s-3); padding:var(--s-4); }
.msg { max-width:85%; }
.msg.user { align-self:flex-end; background:var(--primary); color:var(--primary-foreground);
border-radius:var(--r-md); padding:var(--s-2) var(--s-3); }
.msg.assistant { align-self:stretch; max-width:100%; }
.item-label { font-size:var(--fs-xs); color:var(--muted-foreground); margin-bottom:var(--s-1);
display:flex; gap:var(--s-2); align-items:center;}
details.fold { border:1px solid var(--border); border-radius:var(--r-sm);
background:var(--surface-raised); font-size:var(--fs-xs); }
details.fold summary { list-style:none; cursor:default; padding:var(--s-2) var(--s-3);
display:flex; align-items:center; gap:var(--s-2); color:var(--muted-foreground);}
details.fold pre { margin:0; padding:var(--s-2) var(--s-3); border-top:1px solid var(--border);
overflow:auto; font-size:var(--fs-xs); }
.chip { display:inline-flex; align-items:center; gap:var(--s-2); border-radius:999px;
padding:var(--s-1) var(--s-3); font-size:var(--fs-xs); border:1px dashed var(--border);
color:var(--muted-foreground); align-self:flex-end; }
.chip.ack { border-style:solid; border-color:var(--success); color:var(--success);
background:var(--success-surface);}
.chip.rej { border-style:solid; border-color:var(--danger); color:var(--danger);
background:var(--danger-surface);}
.divider { display:flex; align-items:center; gap:var(--s-2); color:var(--warning);
font-size:var(--fs-xs); }
.divider::before,.divider::after { content:""; flex:1; border-top:1px dashed var(--warning); }
/* request card */
.req { border:1px solid var(--border); border-left:3px solid var(--accent);
border-radius:var(--r-sm); background:var(--card); }
.req .rh { display:flex; gap:var(--s-2); align-items:center; padding:var(--s-2) var(--s-3);
font-weight:var(--w-med); font-size:var(--fs-sm);}
.req .rid { margin-left:auto; color:var(--muted-foreground); }
.req pre { margin:0 var(--s-3); padding:var(--s-2) var(--s-3); background:var(--muted);
border-radius:var(--r-sm); overflow:auto;}
.req .ra { display:flex; gap:var(--s-2); padding:var(--s-3); flex-wrap:wrap;}
.req.resolved { border-left-color:var(--success); }
.req.resolved .rh { color:var(--muted-foreground); font-weight:var(--w-med); }
.streaming-cursor { display:inline-block; width:.5em; height:1em; background:var(--accent);
vertical-align:text-bottom; }
.jump { position:sticky; bottom:5.5rem; align-self:center; }
/* composer */
.composer { border-top:1px solid var(--border); padding:var(--s-3) var(--s-4); display:flex;
flex-direction:column; gap:var(--s-2); background:var(--surface-raised);
border-radius:0 0 var(--r-md) var(--r-md); }
.composer .row { display:flex; gap:var(--s-3); align-items:flex-end; }
.composer textarea { flex:1; min-height:2.5rem; resize:none; border:1px solid var(--border);
border-radius:var(--r-sm); padding:var(--s-2) var(--s-3); font:inherit; background:var(--card);}
.status-line { font-size:var(--fs-xs); color:var(--muted-foreground); display:flex; gap:var(--s-2);
align-items:center;}
.banner { display:flex; gap:var(--s-2); align-items:center; padding:var(--s-2) var(--s-4);
font-size:var(--fs-xs); border-top:1px solid var(--border); }
.banner.pending { background:var(--warning-surface); color:var(--warning); }
kbd { font-family:var(--font-mono); font-size:var(--fs-xs); background:var(--muted);
border:1px solid var(--border); border-bottom-width:2px; border-radius:var(--r-sm);
padding:0 var(--s-1); }
/* inspector */
.tabs { display:flex; gap:var(--s-1); padding:var(--s-2) var(--s-3) 0; border-bottom:1px solid var(--border);}
.tab { padding:var(--s-1) var(--s-3); border-radius:var(--r-sm) var(--r-sm) 0 0; font-size:var(--fs-xs);
color:var(--muted-foreground);}
.tab.on { background:var(--muted); color:var(--foreground); font-weight:var(--w-med); }
.evt { display:grid; grid-template-columns:2.5rem 1fr auto; gap:var(--s-2); padding:var(--s-1) var(--s-3);
font-size:var(--fs-xs); font-family:var(--font-mono); border-bottom:1px solid var(--border); }
.evt .seq { color:var(--muted-foreground); }
.evt.hl { background:var(--accent-surface); }
.cap { display:flex; justify-content:space-between; gap:var(--s-2); padding:var(--s-1) var(--s-3);
font-size:var(--fs-xs); border-bottom:1px solid var(--border);}
/* annotation markers */
.anno { position:relative; }
.pin { position:absolute; z-index:5; width:1.4rem; height:1.4rem; border-radius:999px;
background:var(--accent); color:#fff; font-size:var(--fs-xs); font-weight:var(--w-semi);
display:flex; align-items:center; justify-content:center; box-shadow:0 0 0 3px var(--accent-surface); }
.legend { grid-column:1/-1; }
.legend ol { margin:var(--s-2) 0 0; padding-left:1.2rem; font-size:var(--fs-xs);
columns:2; column-gap:var(--s-8); }
.legend li { margin-bottom:var(--s-1); }
/* mobile */
.seg { display:none; }
@media (max-width: 64rem) {
.console { grid-template-columns: minmax(0,1fr); padding:var(--s-3); }
.rail-left, .rail-right { display:none; }
header.top { flex-wrap:wrap; row-gap:var(--s-2); }
header.top h1 { font-size:var(--fs-md); }
header.top .btn.ghost, header.top .badge.mono { display:none; }
.eyebrow { letter-spacing:.04em; }
.seg { display:flex; gap:var(--s-1); background:var(--muted); border-radius:var(--r-sm);
padding:var(--s-1); }
.seg span { flex:1; text-align:center; padding:var(--s-1) 0; border-radius:var(--r-sm);
font-size:var(--fs-xs); color:var(--muted-foreground);}
.seg span.on { background:var(--card); color:var(--foreground); font-weight:var(--w-med);
box-shadow:var(--shadow-sm);}
.legend ol { columns:1; }
.msg { max-width:92%; }
.btn { height:2.75rem; } /* V2: 44px touch targets */
}
</style>
</head>
<body>
<div class="console anno">
<header class="top">
<div>
<div class="eyebrow">Paperclip Runner Protocol · Phase 4b</div>
<h1>Live Codex console</h1>
</div>
<span class="badge info"><span class="dot run"></span> connected</span>
<span class="badge mono">session prp-7f3a…c2</span>
<button class="btn">Goal ▾</button>
<button class="btn ghost">Inspector</button>
</header>
<!-- LEFT RAIL -->
<aside class="rail-left" style="position:relative">
<span class="pin" style="top:-0.5rem; left:-0.5rem">9</span>
<section class="panel">
<div class="panel-h">Demo chats</div>
<div class="panel-b" style="display:flex;flex-direction:column;gap:var(--s-1)">
<div class="manifest active">
<div class="t">Steer &amp; interrupt <span class="badge info">running</span></div>
<div class="d">Mid-turn steering ack, stale rejection, 3 interrupt races</div>
</div>
<div class="manifest">
<div class="t">Approvals <span class="badge ok">passed</span></div>
<div class="d">Command, file-change, permission, input requests</div>
</div>
<div class="manifest">
<div class="t">Reconnect &amp; replay <span class="badge">not run</span></div>
<div class="d">Drop socket, refresh, byte-identical replay</div>
</div>
<button class="btn ghost sm">Reset demo state…</button>
</div>
</section>
<section class="panel" style="position:relative">
<span class="pin" style="top:-0.5rem; right:-0.5rem">5</span>
<div class="panel-h">Session</div>
<div class="panel-b">
<div class="kv"><span class="k">Connection</span><span><span class="dot ok"></span> connected</span></div>
<div class="kv"><span class="k">Turn</span><span class="mono">turn-19 · active</span></div>
<div class="kv"><span class="k">Pending requests</span><span class="badge warn">1</span></div>
<div class="goal" style="margin-top:var(--s-2)">
<strong>Goal · active</strong><br/>
Ship the fixture validator with tests
<div style="margin-top:var(--s-1)"><button class="btn ghost sm">Pause</button>
<button class="btn ghost sm" aria-disabled="true" title="Goal is active">Resume</button></div>
</div>
</div>
</section>
<section class="panel" style="position:relative">
<span class="pin" style="top:-0.5rem; right:-0.5rem">6</span>
<div class="panel-h">Threads</div>
<div class="panel-b">
<ul class="tree">
<li class="cur"><span class="dot run"></span> <span class="mono">root · thr-a91f</span></li>
<li class="child"><span class="dot run"></span> <span class="mono">child · thr-b02c</span> <span class="badge info">running</span></li>
<li class="child"><span class="dot mut"></span> <span class="mono">child · thr-9d44</span> <span class="badge ok">done</span></li>
</ul>
</div>
</section>
</aside>
<!-- CENTER: transcript + composer -->
<main class="panel chat anno" style="position:relative">
<span class="pin" style="top:-0.5rem; left:-0.5rem">1</span>
<div class="seg" role="tablist" aria-label="Console sections">
<span class="on">Chat</span><span>Session</span><span>Inspector</span>
</div>
<div class="stream" role="log" aria-label="Session transcript">
<div class="msg user">Validate the fixture corpus and summarize gaps.</div>
<div class="msg assistant">
<div class="item-label"><span class="badge">reasoning</span> 2.1s</div>
<details class="fold"><summary>▸ Planned validation order and edge cases…</summary></details>
</div>
<div class="msg assistant" style="position:relative">
<span class="pin" style="top:-0.6rem; right:0">4</span>
<div class="req">
<div class="rh"><span class="badge warn">Command</span> Approve running this command?
<span class="rid mono">req-77b1…</span></div>
<pre>pnpm vitest run protocol/fixtures --reporter=json</pre>
<div class="ra">
<button class="btn primary">Approve</button>
<button class="btn">Approve for session</button>
<button class="btn danger">Reject</button>
</div>
</div>
</div>
<div class="msg assistant">
<div class="item-label"><span class="badge ok">tool · resolved</span> validate_fixtures</div>
<details class="fold"><summary>▸ 37 fixtures validated · 0 gaps</summary></details>
</div>
<div class="divider" style="position:relative">
<span class="pin" style="top:-1.4rem; left:40%">3</span>
turn-18 interrupted — partial output preserved
</div>
<div class="chip ack" style="position:relative">
<span class="pin" style="top:-1.2rem; right:-0.5rem">2</span>
✓ Steering acknowledged · “prefer the JSON reporter”
</div>
<div class="chip rej">✕ Steering rejected — turn already ended · <u>Send as new message</u></div>
<div class="msg assistant">
<div class="item-label"><span class="badge info">assistant · streaming</span></div>
<p>Re-running with the JSON reporter. Two fixtures need regenerated goldens: <code>source-gap</code> and <code>duplicate-event</code>. Starting with<span class="streaming-cursor"></span></p>
</div>
<button class="btn sm jump">↓ Jump to latest · 2 new</button>
</div>
<div class="banner pending" style="position:relative">
<span class="pin" style="top:-0.7rem; left:30%">4</span>
⏳ 1 request waiting — the turn is paused until you respond. <u>Review</u>
</div>
<div class="composer anno" style="position:relative">
<span class="pin" style="top:-0.7rem; right:-0.5rem">2</span>
<div class="status-line" aria-live="polite"><span class="dot run"></span> Turn running · steering will apply to <span class="mono">turn-19</span></div>
<div class="row">
<textarea aria-label="Steer the active turn">Focus only on the two failing goldens.</textarea>
<button class="btn primary">Steer</button>
<button class="btn danger" aria-label="Stop turn">■ Stop</button>
</div>
<div class="status-line"><kbd>Enter</kbd> steer · <kbd>Shift</kbd>+<kbd>Enter</kbd> newline · <kbd>End</kbd> jump to latest</div>
</div>
</main>
<!-- RIGHT RAIL: inspector -->
<aside class="rail-right anno" style="position:relative">
<span class="pin" style="top:-0.5rem; left:-0.5rem">8</span>
<section class="panel">
<div class="panel-h">Protocol inspector <span class="badge">live</span></div>
<div class="tabs">
<span class="tab on">Events</span><span class="tab">Requests</span>
<span class="tab">Capabilities</span><span class="tab">Session</span>
</div>
<div>
<div class="evt"><span class="seq">41</span><span>turn.started</span><span class="mono">turn-19</span></div>
<div class="evt hl"><span class="seq">42</span><span>item.delta</span><span class="mono">item-63</span></div>
<div class="evt"><span class="seq">43</span><span>runtime_request.created</span><span class="mono">req-77b1</span></div>
<div class="evt"><span class="seq">44</span><span>item.delta</span><span class="mono">item-63</span></div>
</div>
<div class="panel-b" style="border-top:1px solid var(--border)">
<div class="eyebrow" style="margin-bottom:var(--s-1)">Capabilities</div>
<div class="cap"><span>turn/steer</span><span class="badge ok">supported</span></div>
<div class="cap"><span>turn/interrupt</span><span class="badge ok">supported</span></div>
<div class="cap" style="position:relative"><span class="pin" style="right:-0.9rem; top:-0.2rem">5</span>
<span>goals/pause</span><span class="badge err" title="app-server 0.132.0 does not advertise goals.pause">unsupported</span></div>
</div>
</section>
<section class="panel">
<div class="panel-b" style="font-size:var(--fs-xs); color:var(--muted-foreground)">
<strong style="color:var(--foreground)">Replay state</strong>
<div class="kv"><span class="k">Cursor</span><span class="mono">seq 44 · no gaps</span></div>
<div class="kv"><span class="k">Mode</span><span><span class="badge info">LIVE</span> <span style="position:relative"><span class="pin" style="top:-1rem; right:-1.4rem">7</span></span></span></div>
</div>
</section>
</aside>
<!-- LEGEND -->
<section class="panel legend">
<div class="panel-b">
<div class="eyebrow">Annotations — interaction map sections (docs/design/phase-4b-interaction-map.md)</div>
<ol>
<li><strong>§1 Transcript & composer</strong> — reducer-ordered `role="log"`, streaming affordance, jump-to-latest pill, composer tri-state (Send → Steer/Stop).</li>
<li><strong>§2 Steering</strong> — pending chip → acknowledged / stale-rejected with “Send as new message” recovery; `expectedTurnId` shown in status line.</li>
<li><strong>§3 Interruption</strong> — interrupted divider preserves partial output; three races get distinct terminal states; session never replaced.</li>
<li><strong>§4 Request cards</strong> — inline at timeline position + blocking banner; actions only as offered upstream; single-response guarantee.</li>
<li><strong>§5 Goal capability</strong> — durable banner + fixed five-verb menu; unsupported verbs disabled with the exact upstream diagnostic.</li>
<li><strong>§6 Lineage</strong> — parent/child tree from upstream identities; child steering shown as unsupported, never emulated.</li>
<li><strong>§7 Reconnect/replay</strong> — connection status, replayed-to-here divider, REPLAY vs LIVE eyebrow badge, cursor/gap state.</li>
<li><strong>§8 Protocol inspector</strong> — canonical events + raw upstream names, requests, capabilities, identities; redaction markers verbatim.</li>
<li><strong>§9 Demo manifests</strong> — scenario list with expected observations, per-manifest outcome, reset with explicit destruction copy.</li>
</ol>
</div>
</section>
</div>
</body>
</html>
@@ -1,329 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Phase 7 scenario explorer — annotated mockup (ap-mcp-gate-01)</title>
<style>
/* Static mockup only. Values mirror the package --pcr-* token layer;
the implementation must consume the real tokens, never these copies. */
:root {
--bg: #f7f7f5; --fg: #191917; --card: #ffffff; --muted: #efefeb;
--muted-fg: #66665f; --border: #deded7; --primary: #242420; --primary-fg: #fff;
--success: #17633a; --success-surface: #e9f5ed;
--warning: #8a5a10; --warning-surface: #fff4dc;
--danger: #a02f2f; --danger-surface: #fceaea;
--accent: #2f5fa8; --accent-fg: #fff; --accent-surface: #e9f0fa;
--raised: #fcfcfa; --ring: #66665f;
--sans: Inter, ui-sans-serif, system-ui, sans-serif;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--r-sm: 0.375rem; --r-md: 0.625rem;
--shadow: 0 0.0625rem 0.125rem rgb(0 0 0 / 0.05);
}
* { box-sizing: border-box; min-width: 0; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 400 0.875rem/1.5 var(--sans); }
.mono { font-family: var(--mono); }
.xs { font-size: 0.75rem; }
header.app {
display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 1rem;
background: var(--raised); border-bottom: 1px solid var(--border);
}
header.app h1 { font-size: 0.875rem; font-weight: 600; margin: 0; }
header.app .sub { color: var(--muted-fg); font-size: 0.75rem; }
.corpus { margin-left: auto; color: var(--muted-fg); font-size: 0.75rem; }
.shell { display: grid; grid-template-columns: 17rem minmax(0,1fr) 24rem; gap: 0; height: calc(100vh - 2.6rem); }
/* ---------- left rail ---------- */
nav.picker { border-right: 1px solid var(--border); background: var(--raised); overflow-y: auto; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.75rem; }
.search { display: block; width: 100%; height: 2rem; border: 1px solid var(--border); border-radius: var(--r-sm); padding: 0 0.5rem; font: inherit; background: var(--card); color: var(--muted-fg); }
.facet h3 { margin: 0 0 0.25rem; font-size: 0.75rem; font-weight: 600; color: var(--muted-fg); text-transform: uppercase; letter-spacing: 0.04em; }
.chips { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.chip { border: 1px solid var(--border); background: var(--card); border-radius: 999px; padding: 0.05rem 0.5rem; font-size: 0.75rem; color: var(--fg); }
.chip.on { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.chip.off { color: var(--muted-fg); opacity: 0.55; }
.active-filters { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; padding: 0.375rem; background: var(--accent-surface); border-radius: var(--r-sm); }
.active-filters .clear { margin-left: auto; font-size: 0.75rem; color: var(--accent); background: none; border: 0; cursor: pointer; }
.group-h { font-size: 0.75rem; font-weight: 600; color: var(--muted-fg); margin: 0.5rem 0 0.25rem; display: flex; justify-content: space-between; }
ul.cases { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.case { display: grid; grid-template-columns: 1fr auto; gap: 0 0.5rem; padding: 0.375rem 0.5rem; border-radius: var(--r-sm); border-left: 3px solid transparent; }
.case .id { font-family: var(--mono); font-size: 0.72rem; color: var(--muted-fg); }
.case .t { font-size: 0.78rem; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.case.sel { background: var(--accent-surface); border-left-color: var(--accent); }
.dot { width: 0.55rem; height: 0.55rem; border-radius: 999px; align-self: center; }
.dot.pass { background: var(--success); }
.dot.fail { background: var(--danger); }
.badge { display: inline-flex; align-items: center; gap: 0.25rem; border-radius: 999px; padding: 0.05rem 0.5rem; font-size: 0.7rem; font-weight: 500; border: 1px solid var(--border); background: var(--muted); color: var(--fg); white-space: nowrap; }
.badge.cp { background: var(--muted); color: var(--muted-fg); }
.badge.always { background: var(--accent-surface); color: var(--accent); border-color: transparent; }
.badge.optional { background: var(--warning-surface); color: var(--warning); border-color: transparent; }
.badge.grp { background: var(--card); }
.badge.deny { background: var(--danger-surface); color: var(--danger); border-color: transparent; }
.badge.allow { background: var(--success-surface); color: var(--success); border-color: transparent; }
/* ---------- center ---------- */
main.run { overflow-y: auto; padding: 1rem 1.25rem; display: flex; flex-direction: column; gap: 0.75rem; max-width: 48rem; width: 100%; margin: 0 auto; }
.runhead { display: flex; flex-direction: column; gap: 0.5rem; }
.runhead .row1 { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.runhead h2 { margin: 0; font-size: 1rem; font-weight: 600; }
.banner { display: flex; gap: 0.5rem; align-items: center; border-radius: var(--r-md); padding: 0.5rem 0.75rem; font-weight: 500; }
.banner.pass { background: var(--success-surface); color: var(--success); }
.controls { display: flex; gap: 0.5rem; align-items: center; }
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden; }
.seg button { border: 0; background: var(--card); padding: 0.25rem 0.75rem; font: inherit; font-size: 0.78rem; color: var(--muted-fg); }
.seg button.on { background: var(--primary); color: var(--primary-fg); }
.btn { border: 1px solid var(--border); background: var(--card); border-radius: var(--r-sm); padding: 0.3rem 0.9rem; font: inherit; font-size: 0.8rem; font-weight: 500; }
.btn.primary { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.relay-note { color: var(--muted-fg); font-size: 0.72rem; }
.transcript { display: flex; flex-direction: column; gap: 0.5rem; }
.cpa { background: var(--muted); border-radius: var(--r-md); padding: 0.5rem 0.75rem; display: flex; gap: 0.5rem; align-items: flex-start; }
.cpa .glyph { color: var(--muted-fg); font-size: 0.8rem; line-height: 1.4; }
.cpa p { margin: 0; font-size: 0.8rem; }
.cpa .why { color: var(--muted-fg); font-size: 0.72rem; }
.agent-turn { background: var(--card); border: 1px solid var(--border); border-radius: var(--r-md); padding: 0.5rem 0.75rem; box-shadow: var(--shadow); }
.agent-turn p { margin: 0.15rem 0; font-size: 0.82rem; }
.toolcall { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--raised); }
.toolcall summary, .toolcall .sumline { display: flex; gap: 0.5rem; align-items: center; padding: 0.45rem 0.75rem; font-size: 0.8rem; cursor: default; flex-wrap: wrap; }
.toolcall .op { font-family: var(--mono); font-weight: 600; font-size: 0.78rem; }
.toolcall .outcome { color: var(--muted-fg); font-size: 0.75rem; }
.toolcall .body { border-top: 1px solid var(--border); padding: 0.5rem 0.75rem; display: grid; gap: 0.4rem; }
.kv { display: grid; grid-template-columns: 7rem 1fr; gap: 0.15rem 0.5rem; font-size: 0.75rem; }
.kv dt { color: var(--muted-fg); }
.kv dd { margin: 0; font-family: var(--mono); font-size: 0.72rem; overflow-wrap: anywhere; }
pre.json { margin: 0; background: var(--muted); border-radius: var(--r-sm); padding: 0.4rem 0.5rem; font-family: var(--mono); font-size: 0.7rem; line-height: 1.45; overflow-x: auto; }
.toolcall.denied { border-color: var(--danger); }
.toolcall.denied .sumline { background: var(--danger-surface); border-radius: var(--r-md) var(--r-md) 0 0; }
.deny-detail { padding: 0.5rem 0.75rem; border-top: 1px solid var(--border); font-size: 0.78rem; }
.deny-detail strong { color: var(--danger); }
.redact { display: inline-flex; align-items: center; gap: 0.25rem; background: var(--muted); border: 1px dashed var(--muted-fg); border-radius: var(--r-sm); padding: 0 0.35rem; font-family: var(--mono); font-size: 0.7rem; color: var(--muted-fg); }
.sysnote { text-align: center; color: var(--muted-fg); font-size: 0.75rem; padding: 0.25rem; }
/* ---------- inspector ---------- */
aside.inspector { border-left: 1px solid var(--border); background: var(--raised); overflow-y: auto; display: flex; flex-direction: column; }
.tabs { display: flex; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--raised); }
.tabs button { flex: 1; border: 0; background: none; padding: 0.5rem 0.25rem; font: inherit; font-size: 0.72rem; color: var(--muted-fg); border-bottom: 2px solid transparent; display: inline-flex; justify-content: center; gap: 0.25rem; align-items: center; white-space: nowrap; }
.tabs button.on { color: var(--fg); font-weight: 600; border-bottom-color: var(--primary); }
.tab-chip { background: var(--danger-surface); color: var(--danger); border-radius: 999px; font-size: 0.65rem; padding: 0 0.35rem; font-weight: 600; }
.pane { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }
table.auth { width: 100%; border-collapse: collapse; font-size: 0.72rem; table-layout: fixed; }
table.auth td, table.auth th { overflow-wrap: anywhere; }
table.auth th:nth-child(1) { width: 1.2rem; }
table.auth th:nth-child(2) { width: 34%; }
table.auth th:nth-child(3) { width: 3.6rem; }
table.auth th { text-align: left; color: var(--muted-fg); font-weight: 500; padding: 0.25rem 0.35rem; border-bottom: 1px solid var(--border); }
table.auth td { padding: 0.3rem 0.35rem; border-bottom: 1px solid var(--border); vertical-align: top; }
table.auth tr.deny { background: var(--danger-surface); }
.authcard { border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--card); padding: 0.5rem; font-size: 0.72rem; display: grid; gap: 0.3rem; }
/* annotations */
.ann { position: absolute; background: #1d4ed8; color: #fff; font: 600 0.68rem/1.2 var(--sans); border-radius: 0.25rem; padding: 0.15rem 0.4rem; z-index: 50; box-shadow: 0 1px 3px rgb(0 0 0 / 0.3); }
.wrap { position: relative; }
/* segmented mobile control hidden on desktop */
.mseg { display: none; }
@media (max-width: 63.99rem) {
.shell { display: block; height: auto; }
nav.picker, aside.inspector { border: 0; }
nav.picker { display: none; }
.mseg { display: flex; position: sticky; top: 0; z-index: 10; background: var(--raised); border-bottom: 1px solid var(--border); }
.mseg button { flex: 1; border: 0; background: none; padding: 0.6rem 0; font: inherit; font-size: 0.8rem; color: var(--muted-fg); min-height: 2.75rem; }
.mseg button.on { color: var(--fg); font-weight: 600; box-shadow: inset 0 -2px 0 var(--primary); }
main.run { padding: 0.75rem; }
aside.inspector { display: none; }
.corpus { display: none; }
.shell > .ann { display: none; }
.seg button { font-size: 0.72rem; padding: 0.25rem 0.5rem; white-space: nowrap; }
main.run { padding-top: 0.75rem !important; }
}
</style>
</head>
<body>
<header class="app">
<h1>Phase 7 scenario explorer</h1>
<span class="sub">mock control plane · no Paperclip services · no credentials</span>
<span class="corpus">106 cases · 16 groups · fake agent</span>
</header>
<div class="mseg" role="tablist" aria-label="Explorer sections">
<button>Scenarios</button><button class="on">Run</button><button>Inspect</button>
</div>
<div class="shell wrap">
<span class="ann" style="top:0.4rem; left:1rem;">1 · picker: search + 6 facets + grouped case list</span>
<span class="ann" style="top:0.4rem; left:19rem;">2 · run view: verdict banner + mode + transcript</span>
<span class="ann" style="top:0.4rem; right:1rem;">3 · inspector: Context · Authorization · Diff · Parity</span>
<nav class="picker" aria-label="Scenario picker" style="padding-top:2.2rem;">
<input class="search" value="" placeholder="Search 106 cases…" aria-label="Search cases" />
<div class="active-filters">
<span class="chip on">group: ap ×</span>
<button class="clear">Clear filters</button>
</div>
<div class="facet">
<h3>Group</h3>
<div class="chips">
<span class="chip">hb 5</span><span class="chip">co 6</span><span class="chip">st 8</span>
<span class="chip">cm 6</span><span class="chip">se 4</span><span class="chip">su 4</span>
<span class="chip">bl 5</span><span class="chip">dp 3</span><span class="chip">ix 9</span>
<span class="chip on">ap 6</span><span class="chip">ar 4</span><span class="chip">er 9</span>
<span class="chip">rf 22</span><span class="chip">mh 4</span><span class="chip">rs 3</span>
<span class="chip">wk 8</span>
</div>
</div>
<div class="facet">
<h3>Disposition</h3>
<div class="chips"><span class="chip">Control plane 1</span><span class="chip">Always tool 0</span><span class="chip">Optional tool 5</span></div>
</div>
<div class="facet">
<h3>Role</h3>
<div class="chips"><span class="chip">ic 5</span><span class="chip">reviewer 1</span><span class="chip off">manager 0</span></div>
</div>
<div class="facet">
<h3>Claim</h3>
<div class="chips"><span class="chip mono">approval:read 6</span><span class="chip mono off">routine:admin 0</span></div>
</div>
<div class="facet">
<h3>Parity</h3>
<div class="chips"><span class="chip">Pass 6</span><span class="chip off">Fail 0</span><span class="chip off">Gap 0</span></div>
</div>
<div class="group-h"><span>ap — approvals</span><span>6</span></div>
<ul class="cases" role="listbox" aria-label="Cases">
<li class="case"><span class="id">ap-approval-deny-01</span><span class="dot pass"></span><span class="t">A denied approval leaves the task open…</span></li>
<li class="case"><span class="id">ap-approval-wake-01</span><span class="dot pass"></span><span class="t">Approval wake reviews the approval…</span></li>
<li class="case"><span class="id">ap-board-approval-01</span><span class="dot pass"></span><span class="t">Spend needs a board approval, then waiting…</span></li>
<li class="case"><span class="id">ap-mcp-expiry-01</span><span class="dot pass"></span><span class="t">Expired MCP approval: one fresh re-call…</span></li>
<li class="case sel" aria-selected="true"><span class="id">ap-mcp-gate-01</span><span class="dot pass"></span><span class="t">Pending MCP tool approval means review posture…</span></li>
<li class="case"><span class="id">ap-mcp-pathmissing-01</span><span class="dot pass"></span><span class="t">approval_path_missing means stop and reroute…</span></li>
</ul>
</nav>
<main class="run" style="padding-top:2.4rem;">
<div class="runhead">
<div class="row1">
<h2><span class="mono">ap-mcp-gate-01</span></h2>
<span class="badge grp">ap</span>
<span class="badge optional">Optional tool</span>
<span class="badge">Authorization</span>
<span class="badge">Multi-hop</span>
</div>
<div class="row1" style="font-size:0.8rem; color:var(--muted-fg);">Pending MCP tool approval means review posture — no retry, no done.</div>
<div class="banner pass">✓ Parity: pass — 9 assertions · 9 pass · 0 gaps</div>
<div class="controls">
<div class="seg"><button class="on">Fake agent</button><button disabled>Codex (bounded)</button></div>
<button class="btn primary">Re-run</button>
<span class="relay-note">Codex disabled: provider relay not running — see tutorial §4</span>
</div>
</div>
<div class="transcript wrap" data-run-state="settled">
<span class="ann" style="top:-0.2rem; right:0;">4 · control-plane channel: muted, “no tool exists”</span>
<div class="cpa">
<span class="glyph">⛭</span>
<div>
<p>Control plane routed scoped wake and checked out task <span class="mono">MCK-102</span>.</p>
<p class="why">No agent tool exists for checkout — control-plane owned. <a href="#">Decision record</a></p>
</div>
</div>
<div class="agent-turn">
<p><strong>Agent</strong> — I need the workspace runtime status before restarting the flaky service.</p>
</div>
<details class="toolcall" open>
<summary class="sumline">
<span class="op">get_task_context</span>
<span class="badge always">Always tool</span>
<span class="badge allow">Allow</span>
<span class="outcome">committed · 41ms fixture-time</span>
</summary>
<div class="body">
<dl class="kv">
<dt>Operation</dt><dd>get_task_context · v1</dd>
<dt>Claims</dt><dd>(task-scoped, none required)</dd>
<dt>Idempotency</dt><dd>read-only</dd>
</dl>
<pre class="json">{ "task": "MCK-102", "wake": "scoped", "ancestors": 2, "documents": 1 }</pre>
</div>
</details>
<div class="toolcall denied wrap">
<span class="ann" style="top:-0.55rem; right:0;">5 · typed denial: missing claim, no protected state</span>
<div class="sumline">
<span class="op">control_workspace_service</span>
<span class="badge optional">Optional tool</span>
<span class="badge deny">Deny</span>
<span class="outcome">policy denial — approval gate pending</span>
</div>
<div class="deny-detail">
<strong>Denied:</strong> requires claim <span class="mono">workspace:control</span> — MCP approval
<span class="mono">apr-0117</span> is pending. Typed denial only; no workspace state returned.
</div>
</div>
<div class="agent-turn">
<p><strong>Agent</strong> — Approval is pending, so I will not retry. Parking the task for review with a durable explanation.</p>
</div>
<details class="toolcall" open>
<summary class="sumline">
<span class="op">request_review</span>
<span class="badge always">Always tool</span>
<span class="badge allow">Allow</span>
<span class="outcome">committed · task → in_review</span>
</summary>
<div class="body">
<dl class="kv">
<dt>Operation</dt><dd>request_review · v1</dd>
<dt>Redactions</dt><dd><span class="redact">••• api-key</span> in explanation draft, rule <span class="mono">secret-value</span></dd>
</dl>
<pre class="json">{ "disposition": "in_review", "reason": "mcp_approval_pending", "approval": "apr-0117" }</pre>
</div>
</details>
<div class="sysnote">No further operations — 3 forbidden operations, none invoked · <a href="#">Parity</a></div>
</div>
</main>
<aside class="inspector" aria-label="Inspector" style="padding-top:1.8rem;">
<div class="tabs" role="tablist">
<button>Context</button>
<button class="on">Authorization <span class="tab-chip">1 deny</span></button>
<button>State diff</button>
<button>Parity</button>
</div>
<div class="pane wrap">
<span class="ann" style="top:-0.1rem; right:0.5rem;">6 · authorization records, verbatim from mock core</span>
<table class="auth">
<thead><tr><th>#</th><th>Operation</th><th>Decision</th><th>Reason</th></tr></thead>
<tbody>
<tr><td>1</td><td class="mono">get_task_context</td><td><span class="badge allow">Allow</span></td><td>task-scoped read</td></tr>
<tr class="deny"><td>2</td><td class="mono">control_workspace_service</td><td><span class="badge deny">Deny</span></td><td>claim <span class="mono">workspace:control</span> gated by pending approval <span class="mono">apr-0117</span></td></tr>
<tr><td>3</td><td class="mono">request_review</td><td><span class="badge allow">Allow</span></td><td>always catalog · redaction <span class="mono">secret-value</span> applied</td></tr>
</tbody>
</table>
<div class="authcard">
<strong>Record 2 — full decision</strong>
<dl class="kv">
<dt>Claims considered</dt><dd>workspace:control (absent), task:active (present)</dd>
<dt>Redactions</dt><dd>none</dd>
<dt>State change</dt><dd>none — denial produced no mutation</dd>
<dt>Audit ref</dt><dd>authz-000214</dd>
</dl>
</div>
<div class="authcard">
<strong>Exposure summary</strong>
<dl class="kv">
<dt>Always tools</dt><dd>7 visible</dd>
<dt>Optional tools</dt><dd>2 visible · via approval:read, workspace:read</dd>
<dt>Control plane</dt><dd>9 owned actions — no tool</dd>
</dl>
</div>
</div>
</aside>
</div>
</body>
</html>
@@ -1,424 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Phase 7I scenario chat — annotated mockup (ix-checkbox-01)</title>
<style>
/* Static mockup only. Values mirror the package --pcr-* token layer;
the implementation must consume the real tokens, never these copies.
Query params: ?view=activity (mobile Activity segment), ?state=streaming. */
:root {
--bg: #f7f7f5; --fg: #191917; --card: #ffffff; --muted: #efefeb;
--muted-fg: #66665f; --border: #deded7; --primary: #242420; --primary-fg: #fff;
--success: #17633a; --success-surface: #e9f5ed;
--warning: #8a5a10; --warning-surface: #fff4dc;
--danger: #a02f2f; --danger-surface: #fceaea;
--accent: #2f5fa8; --accent-fg: #fff; --accent-surface: #e9f0fa;
--raised: #fcfcfa; --ring: #66665f;
--sans: Inter, ui-sans-serif, system-ui, sans-serif;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--r-sm: 0.375rem; --r-md: 0.625rem;
--shadow: 0 0.0625rem 0.125rem rgb(0 0 0 / 0.05);
--touch: 2.75rem;
}
* { box-sizing: border-box; min-width: 0; }
html, body { height: 100%; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 400 0.875rem/1.5 var(--sans);
display: flex; flex-direction: column; }
header.app, .legend, .segbar { flex: none; }
.mono { font-family: var(--mono); }
.xs { font-size: 0.75rem; }
.muted-fg { color: var(--muted-fg); }
/* annotation callouts */
.co { display: inline-flex; align-items: center; justify-content: center; width: 1.05rem; height: 1.05rem;
border-radius: 999px; background: var(--accent); color: var(--accent-fg); font-size: 0.68rem;
font-weight: 600; flex: none; vertical-align: text-bottom; }
.legend { display: flex; flex-wrap: wrap; gap: 0.3rem 0.75rem; padding: 0.3rem 1rem;
background: var(--accent-surface); border-bottom: 1px solid var(--border); font-size: 0.7rem; }
.legend span.item { display: inline-flex; gap: 0.25rem; align-items: center; color: var(--fg); }
header.app { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 1rem;
background: var(--raised); border-bottom: 1px solid var(--border); }
header.app h1 { font-size: 0.875rem; font-weight: 600; margin: 0; }
header.app .corpus { margin-left: auto; color: var(--muted-fg); font-size: 0.75rem; }
.badge { display: inline-flex; align-items: center; gap: 0.25rem; border-radius: 999px; padding: 0.05rem 0.5rem;
font-size: 0.7rem; font-weight: 500; border: 1px solid var(--border); background: var(--muted);
color: var(--fg); white-space: nowrap; }
.badge.cp { background: var(--muted); color: var(--muted-fg); }
.badge.always { background: var(--accent-surface); color: var(--accent); border-color: transparent; }
.badge.optional { background: var(--warning-surface); color: var(--warning); border-color: transparent; }
.badge.deny { background: var(--danger-surface); color: var(--danger); border-color: transparent; }
.badge.allow { background: var(--success-surface); color: var(--success); border-color: transparent; }
.badge.live { background: var(--accent-surface); color: var(--accent); border-color: transparent; }
.badge.grp { background: var(--card); }
.btn { border: 1px solid var(--border); background: var(--card); border-radius: var(--r-sm);
padding: 0.3rem 0.9rem; font: inherit; font-size: 0.8rem; font-weight: 500; min-height: 2rem; }
.btn.primary { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.btn:disabled { opacity: 0.5; }
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden; }
.seg button { border: 0; background: var(--card); padding: 0.25rem 0.75rem; font: inherit; font-size: 0.78rem; color: var(--muted-fg); }
.seg button.on { background: var(--primary); color: var(--primary-fg); }
.shell { display: grid; grid-template-columns: 17rem minmax(0,1fr) 26rem; flex: 1; min-height: 0; }
/* ---------- left rail: reused 7F picker (slim render) ---------- */
nav.picker { border-right: 1px solid var(--border); background: var(--raised); overflow-y: auto;
padding: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }
.search { display: block; width: 100%; height: 2rem; border: 1px solid var(--border); border-radius: var(--r-sm);
padding: 0 0.5rem; font: inherit; background: var(--card); color: var(--muted-fg); }
.group-h { font-size: 0.75rem; font-weight: 600; color: var(--muted-fg); margin: 0.4rem 0 0.1rem;
display: flex; justify-content: space-between; }
ul.cases { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.case { display: grid; gap: 0 0.5rem; padding: 0.375rem 0.5rem; border-radius: var(--r-sm); border-left: 3px solid transparent; }
.case .id { font-family: var(--mono); font-size: 0.72rem; color: var(--muted-fg); }
.case .t { font-size: 0.78rem; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.case.sel { background: var(--accent-surface); border-left-color: var(--accent); }
.facet-note { font-size: 0.72rem; color: var(--muted-fg); border: 1px dashed var(--border);
border-radius: var(--r-sm); padding: 0.35rem 0.5rem; }
/* ---------- center: chat ---------- */
main.chat { display: flex; flex-direction: column; overflow: hidden; }
.chat-scroll { overflow-y: auto; padding: 1rem 1.25rem 0.5rem; display: flex; flex-direction: column;
gap: 0.6rem; max-width: 48rem; width: 100%; margin: 0 auto; flex: 1; }
.sess { display: flex; flex-direction: column; gap: 0.4rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--border); }
.sess .row1 { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.sess h2 { margin: 0; font-size: 1rem; font-weight: 600; }
.sess .controls { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.cpa { background: var(--muted); border-radius: var(--r-md); padding: 0.5rem 0.75rem; display: flex;
gap: 0.5rem; align-items: flex-start; }
.cpa .glyph { color: var(--muted-fg); font-size: 0.8rem; line-height: 1.4; }
.cpa p { margin: 0; font-size: 0.8rem; }
.cpa .why { color: var(--muted-fg); font-size: 0.72rem; }
.u-turn { align-self: flex-end; max-width: 85%; background: var(--accent-surface); border-radius: var(--r-md);
padding: 0.45rem 0.75rem; font-size: 0.85rem; }
.m-turn { background: var(--card); border: 1px solid var(--border); border-radius: var(--r-md);
padding: 0.5rem 0.75rem; box-shadow: var(--shadow); max-width: 95%; }
.m-turn p { margin: 0.15rem 0; font-size: 0.85rem; }
.caret::after { content: "▌"; color: var(--muted-fg); animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
.toolcall { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--raised);
margin-left: 1.25rem; }
.toolcall .sumline { display: flex; gap: 0.5rem; align-items: center; padding: 0.45rem 0.75rem;
font-size: 0.8rem; flex-wrap: wrap; min-height: var(--touch); }
.toolcall .op { font-family: var(--mono); font-weight: 600; font-size: 0.78rem; }
.toolcall .outcome { color: var(--muted-fg); font-size: 0.75rem; }
.toolcall .body { border-top: 1px solid var(--border); padding: 0.5rem 0.75rem; display: grid; gap: 0.4rem; }
.kv { display: grid; grid-template-columns: 6.5rem 1fr; gap: 0.15rem 0.5rem; font-size: 0.75rem; margin: 0; }
.kv dt { color: var(--muted-fg); }
.kv dd { margin: 0; font-family: var(--mono); font-size: 0.72rem; overflow-wrap: anywhere; }
pre.json { margin: 0; background: var(--muted); border-radius: var(--r-sm); padding: 0.4rem 0.5rem;
font-family: var(--mono); font-size: 0.7rem; line-height: 1.45; overflow-x: auto; }
.toolcall.denied { border-color: var(--danger); }
.toolcall.denied .sumline { background: var(--danger-surface); border-radius: var(--r-md) var(--r-md) 0 0; }
.deny-detail { padding: 0.5rem 0.75rem; border-top: 1px solid var(--border); font-size: 0.78rem; }
.deny-detail strong { color: var(--danger); }
.redaction { display: inline-flex; gap: 0.25rem; align-items: center; background: var(--muted);
border-radius: 999px; padding: 0 0.4rem; font-size: 0.7rem; color: var(--muted-fg); }
.turn-strip { display: flex; gap: 0.5rem; align-items: center; width: 100%; text-align: left;
border: 1px solid var(--border); background: var(--card); border-radius: var(--r-md);
padding: 0.45rem 0.75rem; font: inherit; font-size: 0.75rem; color: var(--muted-fg);
min-height: var(--touch); cursor: pointer; flex-wrap: wrap; }
.turn-strip .lbl { font-weight: 600; color: var(--fg); }
.turn-strip .ok { color: var(--success); }
.turn-strip .bad { color: var(--danger); }
.composer { border-top: 1px solid var(--border); background: var(--raised); padding: 0.6rem 1.25rem 0.75rem; }
.composer-inner { max-width: 48rem; margin: 0 auto; display: flex; flex-direction: column; gap: 0.3rem; }
.composer-row { display: flex; gap: 0.5rem; align-items: flex-end; }
.composer textarea { flex: 1; resize: none; border: 1px solid var(--border); border-radius: var(--r-md);
font: inherit; font-size: 0.85rem; padding: 0.5rem 0.6rem; min-height: var(--touch);
background: var(--card); }
.composer .hint { font-size: 0.7rem; color: var(--muted-fg); }
/* ---------- right rail: activity stream ---------- */
aside.activity { border-left: 1px solid var(--border); background: var(--raised); overflow-y: auto;
padding: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }
.act-head { display: flex; align-items: baseline; gap: 0.5rem; }
.act-head h3 { margin: 0; font-size: 0.8rem; font-weight: 600; }
.filters { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.fchip { border: 1px solid var(--border); background: var(--card); border-radius: 999px;
padding: 0.15rem 0.6rem; font: inherit; font-size: 0.72rem; color: var(--muted-fg); min-height: 1.75rem; }
.fchip.on { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.turn-group { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--card); }
.tg-head { display: flex; gap: 0.5rem; align-items: center; padding: 0.5rem 0.65rem; font-size: 0.78rem;
font-weight: 600; flex-wrap: wrap; min-height: var(--touch); }
.tg-head .sum { font-weight: 400; color: var(--muted-fg); font-size: 0.72rem; }
.tg-body { border-top: 1px solid var(--border); padding: 0.5rem 0.65rem; display: flex; flex-direction: column; gap: 0.55rem; }
.sect h4 { margin: 0 0 0.2rem; font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.04em; color: var(--muted-fg); }
.row { display: flex; gap: 0.4rem; align-items: baseline; font-size: 0.75rem; padding: 0.15rem 0; flex-wrap: wrap; }
.row .mono { font-size: 0.72rem; }
.row.deny { background: var(--danger-surface); border-radius: var(--r-sm); padding: 0.25rem 0.4rem; }
.diff-domain { display: flex; justify-content: space-between; font-size: 0.75rem; padding: 0.15rem 0; gap: 0.5rem; }
.diff-field { font-family: var(--mono); font-size: 0.7rem; overflow-wrap: anywhere; color: var(--fg);
background: var(--muted); border-radius: var(--r-sm); padding: 0.2rem 0.4rem; margin: 0.15rem 0; }
.unchanged { color: var(--muted-fg); font-size: 0.72rem; }
.verdict { display: flex; gap: 0.4rem; align-items: center; font-size: 0.78rem; font-weight: 600;
color: var(--success); }
.verdict.session { border-top: 1px dashed var(--border); padding-top: 0.4rem; }
/* ---------- mobile ---------- */
.segbar { display: none; }
@media (max-width: 63.98rem) {
.shell { display: flex; flex-direction: column; }
nav.picker, aside.activity { display: none; }
body.view-activity main.chat { display: none; }
body.view-activity aside.activity { display: flex; border-left: 0; flex: 1; }
.segbar { display: flex; position: sticky; top: 0; z-index: 5; gap: 0.25rem; padding: 0.4rem 0.75rem;
background: var(--raised); border-bottom: 1px solid var(--border); }
.segbar button { flex: 1; min-height: var(--touch); border: 1px solid var(--border); background: var(--card);
border-radius: var(--r-sm); font: inherit; font-size: 0.8rem; display: inline-flex;
align-items: center; justify-content: center; gap: 0.35rem; }
.segbar button.on { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.chat-scroll { padding: 0.75rem 0.75rem 0.5rem; }
.composer { padding: 0.5rem 0.75rem 0.65rem; }
.toolcall { margin-left: 0.5rem; }
header.app .corpus { display: none; }
}
/* ---------- streaming variant ---------- */
.streaming-only { display: none; }
body.state-streaming .streaming-only { display: block; }
body.state-streaming .settled-only { display: none; }
</style>
</head>
<body>
<header class="app">
<h1>Phase 7 scenario chat — mock control plane</h1>
<span class="corpus">106 scenarios · 16 groups · mock adapter only, no credentials in the browser</span>
</header>
<div class="legend" aria-label="Annotation legend">
<span class="item"><span class="co">1</span> session header + status + reset/replay</span>
<span class="item"><span class="co">2</span> control-plane strip (turn 0 seed)</span>
<span class="item"><span class="co">3</span> user turn</span>
<span class="item"><span class="co">4</span> model output</span>
<span class="item"><span class="co">5</span> semantic tool call (expanded)</span>
<span class="item"><span class="co">6</span> denied operation</span>
<span class="item"><span class="co">7</span> turn activity strip</span>
<span class="item"><span class="co">8</span> composer</span>
<span class="item"><span class="co">9</span> per-turn activity group</span>
<span class="item"><span class="co">10</span> mobile segments + deny chip</span>
</div>
<div class="segbar" role="radiogroup" aria-label="Chat sections">
<button type="button" id="seg-chat" role="radio" aria-checked="true"><span class="co">10</span> Chat</button>
<button type="button" id="seg-activity" role="radio" aria-checked="false">Activity <span class="badge deny">1 deny</span></button>
</div>
<div class="shell">
<nav class="picker" aria-label="Scenario picker">
<input class="search" value="" placeholder="Search 106 scenarios…" aria-label="Search scenarios" />
<div class="facet-note">7F picker reused as-is: group · case · disposition · role · claim · parity facets</div>
<div class="group-h"><span>Interactions <span class="mono">ix</span></span><span>9</span></div>
<ul class="cases">
<li class="case"><span class="id">ix-questions-01</span><span class="t">Structured questions use ask-user form</span></li>
<li class="case sel"><span class="id">ix-checkbox-01</span><span class="t">Subset selection from a known list is a checkbox confirmation</span></li>
<li class="case"><span class="id">ix-confirmation-01</span><span class="t">Yes/no plan approval binds the revision</span></li>
</ul>
<div class="group-h"><span>Approvals <span class="mono">ap</span></span><span>6</span></div>
<ul class="cases">
<li class="case"><span class="id">ap-mcp-gate-01</span><span class="t">Gated tool call opens one approval card</span></li>
</ul>
</nav>
<main class="chat" aria-label="Scenario chat">
<div class="chat-scroll">
<div class="sess">
<div class="row1">
<span class="co">1</span>
<span class="mono xs muted-fg">ix-checkbox-01</span>
<h2>Subset selection from a known list is a checkbox confirmation</h2>
</div>
<div class="row1">
<span class="badge grp">Interactions <span class="mono">ix</span></span>
<span class="badge always">Always tool</span>
<span class="badge live">Live · Codex (bounded)</span>
<span class="badge allow">✓ Parity · 2 turns pass</span>
</div>
<div class="controls">
<span class="seg" role="radiogroup" aria-label="Session mode">
<button type="button">Replay (scripted)</button>
<button type="button" class="on">Codex (bounded)</button>
</span>
<button type="button" class="btn settled-only">Reset session…</button>
<button type="button" class="btn settled-only">Replay</button>
<button type="button" class="btn streaming-only" disabled title="Disabled while a turn is running">Reset session…</button>
<span class="streaming-only xs muted-fg">Controls disabled while turn 3 is running</span>
</div>
</div>
<div class="cpa">
<span class="glyph">⌘</span>
<div>
<p><span class="co">2</span> <strong>Control plane</strong> seeded <span class="mono">ix-checkbox-01</span>, routed the scoped wake, and checked out task <span class="mono">MCK-102</span> — no agent tool exists for this.</p>
<p class="why">Session start · 4 events · exposure: 8 always · 2 optional · 5 control-plane (no tool)</p>
</div>
</div>
<div class="u-turn"><span class="co">3</span> Read the task and confirm which files I should delete before you act.</div>
<div class="m-turn">
<p><span class="co">4</span> I read task <span class="mono">MCK-102</span>. It asks for a cleanup of generated fixture files. Before touching anything I need your selection, so I'm raising a checkbox confirmation with the candidate files.</p>
</div>
<div class="toolcall">
<div class="sumline">
<span class="co">5</span>
<span class="op">request_human_input</span>
<span class="badge always">Always tool</span>
<span class="outcome">kind=checkbox · committed</span>
</div>
<div class="body">
<dl class="kv">
<dt>operation</dt><dd>request_human_input@1</dd>
<dt>claims</dt><dd>(always catalog — none required)</dd>
<dt>idempotency</dt><dd>checkbox:MCK-102:cleanup:rev-3 · first commit</dd>
<dt>redactions</dt><dd><span class="redaction">••• redacted secret-hygiene</span></dd>
</dl>
<pre class="json">{ "kind": "checkbox", "prompt": "Check the files to delete.",
"options": [ { "id": "draft-report-march" }, { "id": "tmp-export-2025" } ],
"minSelected": 0 }</pre>
</div>
</div>
<div class="toolcall denied">
<div class="sumline">
<span class="co">6</span>
<span class="op">decide_approval</span>
<span class="badge optional">Optional tool</span>
<span class="badge deny">✕ Denied</span>
</div>
<div class="deny-detail">
<strong>Denied:</strong> missing claim <span class="mono">approval:decide</span>.
The authorization engine rejected the call before any state was read — no protected
state is included in this record.
</div>
</div>
<div class="cpa">
<span class="glyph">⌘</span>
<div>
<p><strong>Control plane</strong> registered the pending interaction and scheduled the continuation wake — interaction continuations are routed by the control plane, not requested again.</p>
<p class="why">state change: interactions +1 · wake scheduled (fixture time t+3)</p>
</div>
</div>
<button type="button" class="turn-strip">
<span class="co">7</span>
<span class="lbl">Turn 2 activity</span>
<span>2 tools</span> · <span class="bad">✕ 1 denied</span> · <span>2 control plane</span> ·
<span>diff 2 domains</span> · <span class="ok">✓ parity pass</span>
</button>
<div class="m-turn streaming-only">
<p class="caret">Checking the interaction result before finishing the task</p>
</div>
</div>
<div class="composer">
<div class="composer-inner">
<div class="composer-row settled-only">
<span class="co">8</span>
<textarea rows="2" aria-label="Message">Once I answer the checkbox, finish the task.</textarea>
<button type="button" class="btn primary">Send</button>
</div>
<p class="hint settled-only">Enter sends. Shift and Enter add a line.</p>
<div class="composer-row streaming-only">
<span class="co">8</span>
<textarea rows="2" aria-label="Message" placeholder="Steer the running turn…"></textarea>
<button type="button" class="btn primary">Steer</button>
<button type="button" class="btn">Stop</button>
</div>
<p class="hint streaming-only">Enter steers the running turn. Escape moves focus to Stop.</p>
</div>
</div>
</main>
<aside class="activity" aria-label="Mock activity stream">
<div class="act-head"><h3>Activity</h3><span class="xs muted-fg">every record emitted by the mock core</span></div>
<div class="filters">
<button type="button" class="fchip on">All</button>
<button type="button" class="fchip">Tools</button>
<button type="button" class="fchip">Authorization</button>
<button type="button" class="fchip">Control plane</button>
<button type="button" class="fchip">Diffs</button>
<button type="button" class="fchip">Parity</button>
</div>
<div class="turn-group">
<div class="tg-head">Session start <span class="sum">4 control-plane events · exposure 8 / 2 / 5</span></div>
</div>
<div class="turn-group">
<div class="tg-head">Turn 1 <span class="sum">1 tool · 1 allow · diff 0 · ✓ pass</span></div>
</div>
<div class="turn-group">
<div class="tg-head"><span class="co">9</span> Turn 2 <span class="sum">2 tools · 1 denied · 2 control plane · ✓ pass</span></div>
<div class="tg-body">
<div class="sect">
<h4>Exposed tools</h4>
<div class="row"><span class="badge always">Always · 8</span><span class="badge optional">Optional · 2</span><span class="badge cp">Control plane — no tool · 5</span></div>
</div>
<div class="sect">
<h4>Calls and results</h4>
<div class="row"><span class="mono">request_human_input</span><span class="badge allow">✓ committed</span></div>
<div class="row deny"><span class="mono">decide_approval</span><span class="badge deny">✕ denied</span></div>
</div>
<div class="sect">
<h4>Authorization</h4>
<div class="row"><span class="badge allow">✓ Allow</span><span class="mono">request_human_input</span><span class="xs muted-fg">always catalog</span></div>
<div class="row deny"><span class="badge deny">✕ Deny</span><span class="mono">decide_approval</span><span class="xs">missing <span class="mono">approval:decide</span></span></div>
</div>
<div class="sect">
<h4>Control plane</h4>
<div class="row"><span class="badge cp">Control plane</span><span class="xs">interaction registered · continuation wake scheduled (t+3)</span></div>
</div>
<div class="sect">
<h4>State diff — turn 2</h4>
<div class="diff-domain"><span>interactions</span><span class="xs muted-fg">1 added</span></div>
<div class="diff-field">interactions[itx-91].status: (none) → pending</div>
<div class="diff-domain"><span>tasks</span><span class="xs muted-fg">1 changed</span></div>
<div class="diff-field">tasks[MCK-102].status: in_progress → in_review</div>
<div class="diff-domain"><span>comments · documents · approvals · artifacts · blockers · workspace · budget · run</span><span class="unchanged">unchanged</span></div>
</div>
<div class="sect">
<h4>Parity — turn 2</h4>
<div class="verdict">✓ Pass · 3 assertions · 3 pass</div>
</div>
</div>
</div>
<div class="turn-group streaming-only">
<div class="tg-head">Turn 3 <span class="sum">running — records append as the mock core emits them</span></div>
</div>
<div class="verdict session settled-only">✓ Session parity · Pass · 6 assertions · 6 pass · 0 forbidden operations invoked</div>
</aside>
</div>
<script>
// Deterministic mockup states for capture: ?view=activity, ?state=streaming
const params = new URLSearchParams(location.search);
if (params.get("view") === "activity") {
document.body.classList.add("view-activity");
document.getElementById("seg-chat").setAttribute("aria-checked", "false");
const seg = document.getElementById("seg-activity");
seg.setAttribute("aria-checked", "true");
seg.classList.add("on");
} else {
document.getElementById("seg-chat").classList.add("on");
}
if (params.get("state") === "streaming") document.body.classList.add("state-streaming");
</script>
</body>
</html>
@@ -1,297 +0,0 @@
# Phase 5 Component and SDK Surface Plan — Reference Console and Reusable Contracts
Status: **approved** (UX gate PAP-16842, 2026-08-08). This is the binding
UX/component plan for the Phase 5 implementation (PAP-16843). It extends the
approved [Phase 4b interaction map](phase-4b-interaction-map.md) and
[Phase 4b component decision record](phase-4b-component-decisions.md); where
this document is silent, those documents govern.
Inputs reviewed for this gate: the accepted Phase 4b live console
(`devtools/browser/`, shipped `96092b832d`, corrected `cda1fb03ab`, QA
evidence `09b91b63f7` — 23 accepted screenshots in
`knowledge/evidence/phase-04b/` at 1440×900 and 390×844), the Phase 4b
reducer/protocol contracts (`src/reducer/session-reducer.ts`,
`src/protocol/`), and the live client layer
(`devtools/browser/src/live/{client,protocol,transcript-model,use-live-console}.ts`).
## 0. Scope and boundary
- Everything lands under `packages/paperclip-runner/` on branch
`PAP-16679-paperclip-runner`. **Do not modify or import production
`server/`, `ui/`, `packages/db/`, routes, persistence, or legacy
adapters.**
- Extraction is **copy-and-freeze, not move**: the Phase 4b demo console at
`devtools/browser/` and its Phase 1–3 surfaces stay untouched — their
screenshots are frozen QA evidence. The SDK is a new, versioned copy with
stable names.
- The canonical PRP event stream and `SessionSnapshot` reducer remain the
only UI data contract (Phase 4b ground rule §1.1 carries forward
unchanged).
## 1. Public package surface
Add subpath exports to `@paperclipai/paperclip-runner`:
| Subpath | Contents | Dependencies |
|---|---|---|
| `.` | existing node/driver contracts (unchanged) | none |
| `./browser` | framework-free session client: HTTP + SSE transport functions (today's `live/client.ts`), wire types (today's `live/protocol.ts`), transcript model, re-exported reducer | none |
| `./react` | hooks + components (today's `use-live-console.ts` + `components/ui/*` + `live/*` views) | `react`/`react-dom` as **peer** deps only |
| `./styles.css` | token sheet + component styles, scoped under `.pcr-root` | none |
Rules:
1. **Zero new runtime dependencies** (carried from Phase 4b §1.3). `react`
and `react-dom` become declared peer dependencies of the `./react`
subpath; nothing else is added. No Tailwind, radix, cva, AI SDK, zod,
shiki, streamdown — same ban list as Phase 4b §6.
2. **Public naming drops `live`/`phase4b`.** Renames (shape unchanged):
`useLiveConsole` → `useRunnerConsole`, `LiveConsole` (interface) →
`RunnerConsole`, `LiveSessionState` → `RunnerSessionState`,
`LiveConnectionStatus` → `ConnectionStatus`, `LiveManifestSummary` →
`ManifestSummary`, `LiveConsoleError` → `RunnerClientError`.
`SessionSnapshot`, `SessionItemSnapshot`, `SessionRequestSnapshot`,
`SequenceGap`, `PrpEvent` are already canonical and keep their names.
3. **The hook contract is frozen as shipped.** `useRunnerConsole` keeps the
accepted `RunnerConsole` shape: `manifests`, `snapshot`, `transcript`,
`connection`, `reconnectAttempt`, `replayParity`, `composer`,
`steeringChips`, `announcement`, `error`, `busy`, thread selection, the
`replay` control group, and the verbs `start/send/steer/interrupt/
resolve/goal/dropConnection/retryNow/reset/dismissSteeringChip`. The
`announcement` string is a load-bearing accessibility channel (map §10
A-criteria) and must remain in the public contract.
4. **Semver discipline.** Any change to an exported prop shape, class name,
token name, or keyboard behavior after extraction requires a decision
recorded in this document's follow-ups, not a silent edit.
## 2. Component contracts
All components: plain function components, `data-slot` attributes, semantic
class names, every visual value from the token layer. SDK class names and
tokens use the **`pcr-` prefix** (see §4) so they cannot collide in consumer
apps; the frozen `ui-*` demo classes stay behind in `devtools/browser/`.
Promoted primitives (from Phase 4b REUSE/ADAPT, contracts unchanged):
| Component | Props typed against | Keyboard contract |
|---|---|---|
| `Button` (default/danger/ghost) | native button props | native |
| `Badge` | tone + children | n/a |
| `Card` | native section props | n/a |
| `Textarea` | native | native |
| `Tabs` | items + selected + onSelect | WAI-ARIA tabs, roving tabindex, arrow keys (map K) |
| `Menu` (menu-button) | fixed item list + onSelect | menu-button pattern: arrows, Home/End, Escape |
| `Dialog` | open/onClose/title/children | native `<dialog>` `showModal()`: focus trap + Escape |
| `Tooltip` | content mirrored to `aria-describedby` | focus-visible trigger; never the only channel (A5) |
| `Banner` | tone (info/warning/danger/success) + actions | actions are real buttons in tab order |
Promoted console components:
| Component | Props typed against | Notes |
|---|---|---|
| `Conversation` | `TranscriptEntry[]` + unseen count | `role="log"`, stick-to-bottom + "jump to latest" as accepted |
| `Message` | `SessionItemSnapshot` | roles: user/assistant/reasoning/tool/system |
| `Composer` | `ComposerState` + submit/steer/stop verbs | Enter submits, Shift+Enter newline; Send/Steer/Stop tri-state per map §1–§3 |
| `ReasoningItem` | `SessionItemSnapshot` | `<details>` disclosure, auto-open while streaming |
| `ToolItem` | `SessionItemSnapshot` | header/input/output sections, status from canonical events |
| `RequestCard` | `SessionRequestSnapshot` + resolve verb | one card per pending request kind; terminal states preserved (PAP-16839 semantics) |
| `SessionTimeline` (today's `SessionPanel`) | `SessionSnapshot` | timeline, lineage tree, goal state |
| `Inspector` | `SessionSnapshot` + `PrpEvent[]` | Events/Session tabs; copy affordance stays local |
| `ReplayControls` | `RunnerConsole["replay"]` | scrub/step/play; arrow-key step |
| `ConnectionBanner` | `ConnectionStatus` + retry verb | reconnect/backoff states as accepted |
Layout components (`.pcr-root` shell, three-pane grid, mobile segments) are
exported as the **reference console** (`RunnerConsoleApp`) — see §8. Grid
tracks that hold identifiers must use `minmax(0, 1fr)` with `min-width: 0`
at every level (measured Phase 4b regression; do not rely on CSS reasoning —
verify with `getBoundingClientRect` in the browser tests).
## 3. Extension points (exactly these five)
1. **Item body renderer.** `Conversation`/`Message`/`ReasoningItem`/
`ToolItem` accept optional `renderItemBody?(item: SessionItemSnapshot) =>
ReactNode`. Default remains exact plain text. This is where consumers add
markdown or syntax highlighting — which is why `Response` and `CodeBlock`
stay out of the core (§9).
2. **Request detail renderer.** `RequestCard` accepts optional
`renderRequestDetail?(request: SessionRequestSnapshot) => ReactNode` for
the payload section only. Status header, resolution controls, and
terminal-state behavior are not overridable.
3. **Composer slots.** Optional `leadingActions`/`trailingActions`
ReactNode slots in the action row. Submit semantics, tri-state button,
and keyboard behavior are fixed.
4. **Token theming.** Consumers restyle exclusively by overriding `--pcr-*`
custom properties on `.pcr-root` (or a descendant scope). Class names and
DOM structure are not a supported theming API.
5. **Transport injection.** The `./browser` client accepts `{ baseUrl,
fetchImpl?, eventSourceFactory? }` so consumers can add auth headers,
proxies, or test doubles without forking the client.
Anything not listed is **not** an extension point. No render-prop shells, no
headless mode, no component-swap registry in Phase 5.
## 4. Design-token rules
1. The Phase 4b token inventory (color/status pairs, accent, surfaces, type
scale, spacing, radii, shadow, motion, z-index, layout, touch-target)
carries over verbatim, renamed `--background` → `--pcr-background` etc.,
declared on `.pcr-root` (not `:root`) in the shipped `./styles.css`.
2. `color-scheme: light` is declared on `.pcr-root`. **Dark mode is a
non-goal** for Phase 5 and is recorded as such (§9).
3. No inline visual values in any SDK component — extend
`check:browser-tokens` to cover the SDK source directories.
4. New tokens are a system change: add to the sheet and record the addition
in this document in the same commit (Phase 4b §5 rule).
5. Motion tokens stay gated by `prefers-reduced-motion`.
6. Shipped default pairs must keep ≥ 4.5:1 contrast (map V1). The package
docs must list the foreground/surface pairs so a consumer who overrides
tokens knows exactly which combinations to re-verify; contrast for
overridden values is the consumer's responsibility and the docs say so.
## 5. Responsive states
- Reference console: three-pane (rail / transcript / inspector) at desktop,
single-pane with segment tabs at mobile, exactly as accepted in Phase 4b
(map §0). Canonical review viewports stay 1440×900 and 390×844.
- **Single-tree rule (binding):** one rendered layout selected via a
`matchMedia` hook (breakpoint constant 900px). Never render both layouts
and hide one with CSS — it duplicates every `data-testid`, control, and
ARIA landmark (Phase 4b lesson, recorded in the 4b journal).
- Component minimum widths (document in package docs): `Conversation` and
`Inspector` ≥ 320px, `RequestCard` ≥ 280px, `Composer` ≥ 320px. Components
must not overflow their container at these widths with realistic long
identifiers (verify in-browser, not by CSS inspection).
- Touch targets ≥ `--pcr-touch-target` (2.75rem) for all mobile controls.
## 6. Keyboard behavior
Map §10 K1–K9 carry forward as SDK acceptance criteria, owned per component:
- `Composer`: Enter submits (Send or Steer per state), Shift+Enter inserts
newline, disabled states never trap focus.
- `Tabs`: arrow-key roving tabindex, Home/End.
- `Menu`: menu-button pattern — Enter/Space/ArrowDown opens, arrows cycle,
Escape closes and restores focus to the trigger.
- `Dialog`: native `showModal()` focus trap, Escape closes, focus returns to
the invoking control.
- `ReasoningItem`/`ToolItem`: `<details>` summary toggles with Enter/Space.
- `ReplayControls`: Left/Right step one event, Space toggles play when the
scrubber has focus.
- Banners: focus is moved to a banner only when it blocks the primary action
(reconnect-failed); informational banners never steal focus.
## 7. Accessibility acceptance criteria
Map §10 A1–A7 and V1–V5 are the SDK acceptance bar, verified on the
reference console **and** the mini consumer:
1. Transcript is `role="log"`; state changes surface through the hook's
`announcement` channel into a single `aria-live="polite"` region.
2. Every interactive control has an accessible name; tooltips mirror to
`aria-describedby` and are never the only channel (A5).
3. Status is never color-only — badge text/icon carries the distinction
(V-criteria).
4. Rebuilt-on-native patterns (dialog, menu, tabs, disclosure) meet the
WAI-ARIA pattern they replace; "the library would have done it" remains
unavailable as an excuse (Phase 4b §1.4).
5. Components keep their ARIA and keyboard contracts under any token
override — theming can never remove semantics.
6. `prefers-reduced-motion` disables all non-essential motion.
## 8. Reference console and the second SDK consumer
**Reference console** (`RunnerConsoleApp`): a standalone app entry that
composes only public SDK exports and reproduces the accepted Phase 4b
lifecycle. It is the first consumer and the visual-truth surface for review
screenshots. It must not import from `devtools/browser/` demo internals.
**Mini consumer** (the tracer-bullet gate): a deliberately minimal second
app (suggested location `examples/mini-consumer/`) that proves the SDK
surface is sufficient and the extension points are real:
- Imports **only** `@paperclipai/paperclip-runner/browser`, `/react`, and
`/styles.css` — extend `check:forbidden-imports` to fail on any deep or
demo-internal import from either consumer.
- Single-column layout using only `Conversation`, `Composer`, `RequestCard`,
`ConnectionBanner`, and `ReplayControls`. It is intentionally not a second
console: no rail, no inspector, no goal menu.
- Scripted flow (against the fake scripted driver and the real Codex
driver): list manifests → create session → send a turn → steer mid-turn →
interrupt → resolve one command request → set and clear a goal via the
hook verb → drop connection → automatic reconnect with gap recovery →
enter replay and scrub → show `replayParity`.
- Exercises extension points: a custom `renderItemBody` (e.g. wraps assistant
text with a visible marker) and at least one `--pcr-*` token override,
both visible in screenshots.
## 9. Component decision record — Phase 5
Carried ground rules: protocol authority (§1.1), source adaptation over
dependency adoption (§1.2), zero new runtime deps (§1.3), accessibility
parity (§1.4), token gaps are system changes (§1.5).
**PROMOTE to SDK (copy from accepted Phase 4b, rename classes/tokens to
`pcr-`):** Button, Badge, Card, Textarea, Tabs, Menu, Dialog, Tooltip,
Banner, Conversation, Message, Composer, ReasoningItem, ToolItem,
RequestCard, SessionTimeline, Inspector, ReplayControls, ConnectionBanner,
plus the `useRunnerConsole` hook, transcript model, and browser client.
**Phase 4b "Phase 5 candidates" — re-decided for Phase 5:**
| Candidate | Decision | Reason |
|---|---|---|
| `Response` (streamed markdown) | **REJECT in core** | markdown pipelines pull deps and can hide protocol truth; served by the `renderItemBody` extension point instead |
| `CodeBlock` (shiki) | **REJECT in core** | same; `<pre>` + mono token remains the default, consumers plug highlighters via `renderItemBody` |
| `Context` (token/cost meter) | **REJECT as persistent meter** | usage stays plain data in the Inspector Session tab; a meter overweights cost in a protocol surface |
| `Command` palette | **REJECT** | goal vocabulary is still five fixed verbs; plain menu wins on Hick's law and zero deps |
**NEW REJECT (Phase 5):**
| Proposal | Reason |
|---|---|
| Headless/unstyled distribution | doubles the API surface and QA matrix before there is a second real consumer; token theming covers stated needs |
| Dark theme | no consuming surface requires it yet; a second palette doubles every V-criterion — record as explicit non-goal, revisit at product integration (Phase 6) |
| Tailwind/radix port | contradicts the zero-dep rule and the accepted idiom |
| Toast/notification layer | failures must live in the transcript record, not ephemeral toasts (map §1.5) |
| Virtualized transcript | no evidence of need at accepted session sizes; adds focus/scroll a11y risk. Revisit with data if real sessions exceed ~2k items |
## 10. Review evidence requirements (binding on PAP-16843)
1. Implementation handoff must attach screenshots at **1440×900 and
390×844**: reference console in idle/empty, streaming turn, steering
acknowledged, interrupt, request pending, request resolved, goal set,
reconnect banner, replay mode, and failed turn; mini consumer in idle,
streaming, request pending, reconnect, replay — including one shot each
proving the custom `renderItemBody` and the token override.
2. Real-driver evidence: at least one reference-console screenshot from a
live Codex session (Phase 4b QA recipe applies).
3. The implementer creates a **UX re-review child issue assigned to
UXDesigner** with those screenshots before the Phase 5 contract review
(PAP-16845) is asked to approve. Code inspection alone is not a UX pass.
## 11. Acceptance criteria for PAP-16843 (UX gate)
- AC1 Subpath exports `.`, `./browser`, `./react`, `./styles.css` exist with
the dependency rules of §1; public names per §1.2; hook contract per §1.3.
- AC2 All §2 components exported with props typed against reducer/protocol
shapes; `data-slot` attributes and `pcr-` classes throughout.
- AC3 Exactly the five §3 extension points, demonstrated in the mini
consumer.
- AC4 Token sheet scoped to `.pcr-root`, `pcr-`-prefixed, light-only,
lint-enforced; contrast pairs documented.
- AC5 Responsive rules of §5 hold, verified by in-browser measurement at
both canonical viewports.
- AC6 Keyboard contracts of §6 and accessibility criteria of §7 pass on both
consumers (component + browser tests reference K/A/V ids).
- AC7 Mini consumer runs the full §8 flow against fake and real drivers with
no demo-internal imports (lint-enforced).
- AC8 Reference console reproduces the accepted Phase 4b lifecycle from
public APIs only; Phase 4b demo and Phase 1–3 surfaces are byte-untouched.
- AC9 Evidence per §10 attached; UX re-review issue created.
## 12. Non-goals
Production `ui/` integration (Phase 6, gated on board acceptance + CTO
design approval), dark mode, i18n, SSR support (document client-only), npm
publishing/registry work, and any second event model or client-side session
cache (map §7.2 stands).
@@ -1,20 +1,19 @@
# Phase 7I Interaction Map — Mobile Chat with Visible Mock Activity
# Scenario chat Interaction Map — Mobile Chat with Visible Mock Activity
Status: **approved UX contract**, implemented in PAP-16916 (2026-08-09).
Deviations taken during implementation are recorded in §11a; the §11 matrix is
recorded by `pnpm run record:phase7i`.
recorded by `pnpm run record:scenario-chat`.
Owner: UXDesigner. Implementer: 7I implementation (PAP-16916), consuming the
existing 7C mock adapter, 7D semantic catalog, and 7F explorer components.
Companion records: [Phase 7 interaction map](phase-7-scenario-explorer-ux.md)
Companion records: [Capability interaction map](capability-scenario-explorer-ux.md)
(the 7F contract — everything not overridden here is inherited verbatim),
[Phase 4b interaction map](phase-4b-interaction-map.md) (chat console
baseline), [Phase 5 component decisions](phase-5-component-decisions.md).
Mockup: [`mockups/phase-7i-mobile-chat.html`](mockups/phase-7i-mobile-chat.html)
(annotated desktop + mobile renders in `knowledge/evidence/phase-07/`,
`phase-7i-*` slugs).
[Live console interaction map](live-console-interaction-map.md) (chat console
baseline), [SDK component decisions](sdk-component-decisions.md).
(annotated desktop + mobile renders in `knowledge/evidence/capability/`,
`scenario-chat-*` slugs).
Phase 7I adds a **Codex-style interactive chat** over the same package-local
Phase 7 runner and mock `ControlPlanePort`. The board opens a scenario, chats
Scenario chat adds a **Codex-style interactive chat** over the same package-local
Capability runner and mock `ControlPlanePort`. The board opens a scenario, chats
with the agent, and — this is the point of the phase — **sees the mock
Paperclip activity for every turn** instead of having it hidden behind chat.
The 7F explorer remains unchanged as the corpus-review surface; the chat is a
@@ -28,7 +27,7 @@ stream.
## 0. Naming, vocabulary, and the turn model
- Surface name in the UI: **Scenario chat** (header: “Phase 7 scenario chat —
- Surface name in the UI: **Scenario chat** (header: “Capability scenario chat —
mock control plane”).
- All 7F §0 vocabulary carries over exactly: **task** in copy; machine values
(case IDs, `MCK-102`, grants, JSON) verbatim in `--pcr-font-mono`;
@@ -45,7 +44,7 @@ stream.
## 1. Shell and information architecture
Same Vite entry, same app shell as 7F (`examples/phase7-explorer/`), new
Same Vite entry, same app shell as 7F (`examples/scenario-explorer/`), new
routes under `#/chat/…` (§9). No second app, no second token sheet.
Desktop (≥ 64rem), three regions, one React tree:
@@ -111,7 +110,7 @@ must attribute any row to its actor in two seconds):
1. **User turns** — right-aligned message bubbles on `--pcr-accent-surface`.
2. **Model output** — left-aligned message cards on `--pcr-card` with the
standard streaming affordances (token reveal, jump-to-latest, auto-follow
inherited verbatim from Phase 4b §1; reduced-motion renders instantly).
inherited verbatim from Live console §1; reduced-motion renders instantly).
Reasoning summaries, when the driver emits them, use `reasoning-item`
behind its existing disclosure.
3. **Agent tool work** — semantic calls render inline in the conversation as
@@ -210,7 +209,7 @@ defect to fix upstream, not a UI guess.
`••• redacted <rule>` inline, no tooltip-only disclosure). Additionally for
chat: the composer submits user text to the package-local runner only; in
Codex mode the provider credential stays server-side behind the same relay
boundary proven in Phase 5, and no user-typed text is ever written to
boundary proven in SDK, and no user-typed text is ever written to
`localStorage` (a page reload may drop an unsent draft; that is the accepted
trade against persisting conversation content).
@@ -246,7 +245,7 @@ trade against persisting conversation content).
operation, parity pass”); a denial announces assertively once, when it
lands (upgrading 7F revision 10’s interim).
- Streaming, flash cues, and auto-follow honor `prefers-reduced-motion`
(instant/static variants, Phase 5 rule). Contrast from existing token
(instant/static variants, SDK rule). Contrast from existing token
pairs; every status icon + text.
## 9. Routes and determinism
@@ -303,7 +302,7 @@ Hash routing, extending the 7F scheme:
per-turn verdicts itself — if the runtime can’t supply a per-turn record,
extend the runtime.
- **Driver seam:** Replay mode consumes recorded fixtures; Codex mode uses
the bounded driver through the Phase 5 relay. The chat UI is
the bounded driver through the SDK relay. The chat UI is
driver-agnostic — it renders whatever the artifact stream says happened.
- **Testing hooks 7I must ship:** component tests for `TurnStrip`,
`ActivityStream` grouping/filtering, and per-turn diff/parity rendering;
@@ -314,8 +313,8 @@ Hash routing, extending the 7F scheme:
## 11. Screenshot acceptance matrix
Evidence lands in `knowledge/evidence/phase-07/` as
`phase-7i-<slug>-<viewport>.png` at **1440×900** and **390×844**, captured
Evidence lands in `knowledge/evidence/capability/` as
`scenario-chat-<slug>-<viewport>.png` at **1440×900** and **390×844**, captured
from §9 routes after `data-chat-state="settled"`. Mobile captures must show
the correct active segment and no horizontal scrollbar.
@@ -392,7 +391,7 @@ made from §1–§11 is recorded here rather than left for a reviewer to discove
## 11b. Acceptance review rulings (PAP-16920, UXDesigner, 2026-08-09)
Review method: all ten §11 captures inspected at full resolution, plus a live
pass against `demo:phase7` at 1440×900 and 390×844 (scripted composer driven
pass against `demo:scenarios` at 1440×900 and 390×844 (scripted composer driven
interactively, streaming route inspected in-DOM). Shots 1–4 are **accepted**.
Shot 5 is **rejected pending revisions 12–14 below**; only shot 5 needs
re-capture.
@@ -464,7 +463,7 @@ shot 5 is gated on):
settles."), wired via `aria-describedby`; keep the `title` if you like,
it just can't be the only channel.
Re-acceptance: re-run `record:phase7i` and hand back only the two
Re-acceptance: re-run `record:scenario-chat` and hand back only the two
`chat-streaming` captures for review; shots 1–4 stand.
## 11c. Re-acceptance ruling (PAP-16920 closing, UXDesigner, 2026-08-09)
@@ -486,7 +485,7 @@ code:
on all four mid-turn-disabled controls; `title` retained as a secondary
channel.
Reproducibility spot-check in the review run: `record:phase7i` re-run
Reproducibility spot-check in the review run: `record:scenario-chat` re-run
produced **byte-identical** `chat-streaming` PNGs at both viewports.
15. **Post-capture copy change (demo-hardening commits `0566a9f3ca`,
@@ -1,14 +1,14 @@
# Phase 5 SDK Extraction Decision Record
# SDK Extraction Decision Record
Date: 2026-08-08
Status: implemented from the approved [Phase 5 component plan](phase-5-component-plan.md)
Status: implemented
## Outcome
The accepted Phase 4b transport and reducer contracts were copied into a
The accepted Live console transport and reducer contracts were copied into a
versioned `0.1.2` public surface. The reference console and a deliberately
small second consumer import only public package subpaths. Phase 4b and the
Phase 1–3 surfaces remain the comparison baseline.
small second consumer import only public package subpaths. Live console and the
Replay–3 surfaces remain the comparison baseline.
## Promoted contracts
@@ -88,10 +88,10 @@ forward-compatible; the schema and shared reducer remain authoritative.
The reference console opens in a normal Codex chat mode. This mode sends plain
user text and permits follow-up turns in the same provider thread. It does not
add the Phase 4 task envelope, semantic completion tools, or output schema.
add the Codex task envelope, semantic completion tools, or output schema.
The workspace sandbox and server-only provider authentication stay active.
The deterministic Phase 4 fixtures remain selectable for protocol tests. The
The deterministic Codex fixtures remain selectable for protocol tests. The
chat layout removes decorative panel outlines and keeps the protocol inspector
available on demand. The header includes a package version and an iteration
emoji so a reviewer can identify the deployed build.
@@ -1,8 +1,8 @@
# Phase 6 Thin Paperclip Adapter Boundary
# Standalone Thin Paperclip Adapter Boundary
> **Scope correction — 2026-08-09:** The board superseded real Paperclip
> installation and live-instance execution for the current checkpoint. Active
> Phase 6 work is limited to the standalone demo runner and demo page under
> Standalone work is limited to the standalone demo runner and demo page under
> `packages/paperclip-runner/`. Product/server/database sections below are
> retained as historical design context, not current execution instructions.
@@ -46,14 +46,14 @@ provider, reducer, outbox, reconnect, or process-supervision logic.
This remains a narrow, default-off tracer: it does not enable native execution
by default, migrate legacy runs, or add browser UI. “Thin” limits where runner
behavior lives; it does not remove the normative completion contract. Phase 6
behavior lives; it does not remove the normative completion contract. Standalone
is complete only when the additive native finalizer applies the complete
Section 18 arbitration contract required by the spike exit criteria and NR-006.
## Why the current sketches need one package-local reconciliation
The original `ControlPlanePort` and `NativeRunEvent` sketch proves the Phase 0
lifecycle, while the accepted Phase 1-5 path uses `PrpEvent`,
The original `ControlPlanePort` and `NativeRunEvent` sketch proves the Conformance
lifecycle, while the accepted Replay-5 path uses `PrpEvent`,
`PrpStructuredRunResult`, durable source sequences, and replay. Before the real
adapter is added, the package contract must be reconciled without importing
Paperclip types:
@@ -82,7 +82,7 @@ interface ControlPlanePort {
```
The exact exported names may stay source-compatible through overloads, but the
observable contract above is required. The Phase 0 fixture may be adapted
observable contract above is required. The Conformance fixture may be adapted
inside the package; no production adapter may invent a second event type.
`NativeSessionBackend` remains the control-plane-facing session contract. A
@@ -124,7 +124,7 @@ profile plus that typed discriminator; exit code and model-authored fields in
## Durable native run envelope and recovery boundary
Phase 6 uses first-class columns and the Section 18 tables, not keys hidden in
Standalone uses first-class columns and the Section 18 tables, not keys hidden in
`context_snapshot` or `result_json`.
`heartbeat_runs` gains these exact fields:
@@ -349,7 +349,7 @@ Threats and required responses:
## Governance and status authority
PRP events and `reportedWorkDisposition` are reports, not organizational
commands. The Phase 6 tracer does not add a runner-accessible issue status API.
commands. The Standalone tracer does not add a runner-accessible issue status API.
- Existing board/user issue routes remain authorized organizational command
paths. For native finalization, `StatusDecisionCommitter` is the only
@@ -373,10 +373,10 @@ versioned `StatusArbiter`, serialized `StatusDecisionCommitter`, durable effect
ledger, and `NativeFinalizationReconciler`.
Shadow computation remains mandatory at MIG-04/MIG-05, but it is a rollout
stage, not the Phase 6 scope or exit state. Phase 6 evidence must proceed through
stage, not the Standalone scope or exit state. Standalone evidence must proceed through
MIG-06 for one allowlisted internal company/agent and prove an applied decision.
Rollout can then be disabled again; already-selected native rows still reconcile
as native. If the complete corpus or application gate cannot pass, Phase 6 stays
as native. If the complete corpus or application gate cannot pass, Standalone stays
blocked rather than shipping a contradictory shadow-only substitute.
The arbiter may choose `done`, `in_review`, `blocked`, continued `in_progress`,
@@ -389,7 +389,7 @@ superseding assessment instead of overwriting organizational authority.
## Existing lifecycle mapping
| Paperclip concern | Phase 6 mapping | Owner |
| Paperclip concern | Standalone mapping | Owner |
|---|---|---|
| Checkout and issue execution lock | Unchanged; resolved before native selection | Existing issue/heartbeat services |
| Budget and pause gate | Unchanged pre-dispatch check | Existing budget/invokability services |
@@ -620,17 +620,13 @@ packages/paperclip-runner/src/backends/harness-driver-backend.ts (n
packages/paperclip-runner/src/backends/harness-driver-backend.test.ts (new)
packages/paperclip-runner/src/conformance/control-plane-port.ts (new)
packages/paperclip-runner/src/conformance/control-plane-port.test.ts (new)
packages/paperclip-runner/src/cli/phase6-paperclip.ts (new)
packages/paperclip-runner/protocol/fixtures/phase-06/* (new)
packages/paperclip-runner/docs/phase-06-thin-paperclip-adapter.md (new)
packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md
packages/paperclip-runner/src/cli/paperclip-adapter.ts (new)
packages/paperclip-runner/protocol/fixtures/standalone/* (new)
packages/paperclip-runner/docs/standalone-thin-paperclip-adapter.md (new)
packages/paperclip-runner/docs/tutorials/standalone-thin-paperclip-adapter.md
packages/paperclip-runner/docs/tutorials/end-to-end.md
packages/paperclip-runner/docs/index.md
packages/paperclip-runner/docs/architecture.md
packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md (new)
packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06*.md
packages/paperclip-runner/knowledge/journal/index.md
packages/paperclip-runner/knowledge/evidence/index.md
packages/paperclip-runner/knowledge/log.md
```
@@ -679,7 +675,7 @@ server/src/services/heartbeat.ts
server/src/routes/issues.ts
server/src/routes/agents.ts
server/src/routes/openapi.ts
server/src/__tests__/native-runner-phase6.integration.test.ts (new)
server/src/__tests__/native-runner-standalone.integration.test.ts (new)
server/src/__tests__/heartbeat-native-runner-selection.test.ts (new)
server/src/__tests__/heartbeat-native-runner-cancellation.test.ts (new)
server/src/__tests__/heartbeat-run-event-sequencing.test.ts (new)
@@ -693,11 +689,11 @@ server/src/__tests__/legacy-finalization-regression.test.ts (new)
No other file is pre-approved. The migration owns the universal
`status_version` increment (a database trigger on an actual status change), so
Phase 6 does not scatter versioning edits across unrelated writers. The native
Standalone does not scatter versioning edits across unrelated writers. The native
committer calls transaction-aware existing issue, blocker, review, interaction,
recovery, wake, activity, and live-event entry points; if one of those files
must change to expose such an entry point, implementation pauses for an
allowlist amendment and CTO review. In particular, Phase 6 must not change a
allowlist amendment and CTO review. In particular, Standalone must not change a
concrete legacy adapter, approval authority, browser UI, workspace policy,
budget policy, or activity semantics merely to make the tracer pass.
@@ -712,7 +708,7 @@ packages/paperclip-runner/src/native-session-runtime.test.ts
packages/paperclip-runner/src/contracts/native-execution.ts
packages/paperclip-runner/src/contracts/native-execution.test.ts
packages/paperclip-runner/src/backends/codex-native-backend.ts
packages/paperclip-runner/spec/fixtures/status-authority-phase5.json
packages/paperclip-runner/spec/fixtures/status-authority-sdk.json
packages/adapter-utils/package.json
packages/shared/src/index.ts
packages/shared/src/types/index.ts
@@ -723,7 +719,7 @@ server/src/services/native-runtime/evidence-classifier.test.ts
server/src/services/native-runtime/paperclip-control-plane-port.test.ts
server/src/services/native-runtime/status-arbiter.test.ts
server/src/services/recovery/service.ts
packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md
packages/paperclip-runner/docs/design/standalone-thin-paperclip-adapter.md
```
The package runtime, closed native-input contract, and Codex backend files own
@@ -938,10 +934,10 @@ additional resolver kinds remain deferred.
The provisional list and seam amendments above remain the decision history.
For final diff auditing, their authoritative union is the following exact
89-file allowlist. It matches the Phase 6 branch diff from the parent of the
89-file allowlist. It matches the Standalone branch diff from the parent of the
authorization commit through this remediation, including the package README,
the Section 18.13 checker, package/runtime tests, `canonical.ts`, and
`runtime-mode.test.ts`. No file outside this list is authorized as Phase 6 work.
`runtime-mode.test.ts`. No file outside this list is authorized as Standalone work.
```text
packages/adapter-utils/package.json
@@ -960,21 +956,17 @@ packages/db/src/schema/status_decision_effects.ts
packages/db/src/schema/status_decisions.ts
packages/db/src/schema/work_assessments.ts
packages/paperclip-runner/README.md
packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md
packages/paperclip-runner/docs/design/standalone-thin-paperclip-adapter.md
packages/paperclip-runner/docs/index.md
packages/paperclip-runner/docs/phase-06-thin-paperclip-adapter.md
packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md
packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md
packages/paperclip-runner/knowledge/evidence/index.md
packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md
packages/paperclip-runner/knowledge/journal/index.md
packages/paperclip-runner/docs/standalone-thin-paperclip-adapter.md
packages/paperclip-runner/docs/tutorials/standalone-thin-paperclip-adapter.md
packages/paperclip-runner/knowledge/log.md
packages/paperclip-runner/package.json
packages/paperclip-runner/spec/fixtures/status-authority-phase5.json
packages/paperclip-runner/spec/fixtures/status-authority-sdk.json
packages/paperclip-runner/src/backends/codex-native-backend.ts
packages/paperclip-runner/src/backends/harness-driver-backend.test.ts
packages/paperclip-runner/src/backends/harness-driver-backend.ts
packages/paperclip-runner/src/cli/phase6-paperclip.ts
packages/paperclip-runner/src/cli/standalone-paperclip.ts
packages/paperclip-runner/src/conformance/control-plane-port.test.ts
packages/paperclip-runner/src/conformance/control-plane-port.ts
packages/paperclip-runner/src/contracts/control-plane-port.ts
@@ -993,7 +985,7 @@ packages/shared/src/types/native-finalization.ts
packages/shared/src/validators/index.ts
packages/shared/src/validators/instance.ts
packages/shared/src/validators/native-finalization.ts
scripts/check-runner-phase5-spec.mjs
scripts/check-runner-sdk-spec.mjs
server/package.json
server/src/__tests__/heartbeat-native-runner-cancellation.test.ts
server/src/__tests__/heartbeat-native-runner-selection.test.ts
@@ -1004,7 +996,7 @@ server/src/__tests__/native-finalization-recovery.test.ts
server/src/__tests__/native-interaction-bridge.test.ts
server/src/__tests__/native-run-finalizer.test.ts
server/src/__tests__/native-runner-input-boundary.test.ts
server/src/__tests__/native-runner-phase6.integration.test.ts
server/src/__tests__/native-runner-standalone.integration.test.ts
server/src/__tests__/native-session-resumption.test.ts
server/src/__tests__/native-status-arbiter-corpus.test.ts
server/src/services/heartbeat-run-events.ts
@@ -1043,13 +1035,13 @@ do not exist yet in this design-only task.
Deterministic package/mock proof:
```sh
pnpm check:runner-phase5-spec
pnpm check:runner-sdk-spec
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/conformance/control-plane-port.test.ts \
src/backends/harness-driver-backend.test.ts
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- \
pnpm --filter @paperclipai/paperclip-runner trace:standalone -- \
--target mock --scenario happy-path
```
@@ -1057,7 +1049,7 @@ First real Paperclip tracer and inspection (against an isolated local dev
instance with the five `PAPERCLIP_*` identifiers/auth variables already set):
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- \
pnpm --filter @paperclipai/paperclip-runner trace:standalone -- \
--target paperclip --scenario happy-path
PAPERCLIP_API_BASE="${PAPERCLIP_API_URL%/}"
@@ -1072,7 +1064,7 @@ Targeted real integration proof:
```sh
pnpm --filter @paperclipai/server exec vitest run \
src/__tests__/native-runner-phase6.integration.test.ts \
src/__tests__/native-runner-standalone.integration.test.ts \
src/__tests__/heartbeat-native-runner-selection.test.ts \
src/__tests__/heartbeat-native-runner-cancellation.test.ts \
src/__tests__/heartbeat-run-event-sequencing.test.ts \
@@ -1095,11 +1087,11 @@ pnpm --filter @paperclipai/server exec vitest run \
Legacy fallback proof after disabling the flag:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- \
pnpm --filter @paperclipai/paperclip-runner trace:standalone -- \
--target paperclip --scenario legacy-fallback
pnpm --filter @paperclipai/server exec vitest run \
src/__tests__/native-runner-phase6.integration.test.ts \
src/__tests__/native-runner-standalone.integration.test.ts \
-t "uses the unchanged legacy path when the kill switch is off"
```
@@ -1116,7 +1108,7 @@ host path.
## Tutorial outline
The implementation fills in the package-local
[Phase 6 tutorial](../tutorials/phase-06-thin-paperclip-adapter.md) with:
[Standalone tutorial](../tutorials/standalone-thin-paperclip-adapter.md) with:
1. prerequisites and an isolated local instance;
2. mock conformance first;
@@ -1134,11 +1126,11 @@ The implementation fills in the package-local
No production credentials or destructive cleanup command belongs in the
tutorial.
## Evidence plan
## Acceptance evidence
`knowledge/evidence/2026-08-09-phase-06-verification.md` is the human-readable
index and records git revision, migration revision, exact commands/exit codes,
flag/profile scope, and links to these redacted machine artifacts:
The package verification run records the git revision, migration revision,
exact commands and exit codes, flag/profile scope, and these redacted machine
artifacts:
- one Section 18.13 result per `{corpusRevision, gitRevision, fixtureId,
consumer}` with observed digests and semantic row/effect counts;
@@ -1163,7 +1155,7 @@ implementation handoff.
## Browser UI decision
No browser component changes are required for the Phase 6 tracer. The Section
No browser component changes are required for the Standalone tracer. The Section
18 company-authorized read models/routes for completion contracts, native
finalization, assessments/decisions, and compact run/issue summaries plus the
package tracer provide inspectable evidence. The flag is server/API-only and
@@ -1181,6 +1173,6 @@ The CTO can approve or reject this record by deciding these five points:
unique canonical sequence preferable to a second event store?
4. Does the restored Section 18 native finalizer/arbitration scope, including
the MIG-04/MIG-05 shadow stages and MIG-06 internal application proof,
satisfy the normative Phase 6 gate?
satisfy the normative Standalone gate?
5. Are the allowed files, conformance matrix, and exact tracer/fallback commands
sufficient to begin implementation?
@@ -1,6 +1,6 @@
# Phase 3 Durable Transport and Recovery
# Durable recovery Durable Transport and Recovery
Phase 3 adds a real outbound WebSocket path to the standalone package. The
Durable recovery adds a real outbound WebSocket path to the standalone package. The
Rust `paperclip-runnerd` process is the client. The TypeScript mock core is the
remote peer. Neither side imports Paperclip server, UI, database, or shared
control-plane code.
@@ -164,7 +164,7 @@ Rust process receives a fresh ticket, reads the same state file, emits a P0
`runner.reconciled` event, and continues the same runner, session, turn, item,
command, and source-event identities.
The harness-restart fault starts the real Phase 2 `fake-harness` child, waits
The harness-restart fault starts the real Local runner `fake-harness` child, waits
for its ready message, terminates its process group, and starts a second child.
The runner then emits `harness.exited`, `harness.ready`, and
`session.reconciled` events with the same normalized session, turn, and item
@@ -211,7 +211,7 @@ unacknowledged facts.
Run a fault and print operator-safe diagnostics:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault lost-ack
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault lost-ack
```
Supported fault names are:
@@ -240,5 +240,5 @@ They never show bootstrap tickets or connection lease tokens.
Regenerate the checked fault matrix and every exact per-fault trace with:
```sh
pnpm --filter @paperclipai/paperclip-runner record:phase3
pnpm --filter @paperclipai/paperclip-runner record:durable-recovery
```
+45 -48
View File
@@ -1,62 +1,59 @@
# Native Runner Tutorials
The tutorials are cumulative and are always run from the repository root.
Each tutorial starts with an explanation of what the phase is and what the phase proves.
Each tutorial explains the capability it exercises and the contract it proves.
## Implemented phases
## Implemented capabilities
- [Phase 0: standalone tracer](tutorials/phase-00-standalone-tracer.md) — install the workspace, validate its Rust/TypeScript boundary and knowledge bundle, then run the deterministic Rust mock-core path and cross-language parity check.
- [Phase 1: static PRP replay](tutorials/phase-01-static-replay.md) — validate the shared schema/fixture corpus, reduce a fixture in the CLI, and inspect the same final snapshot in the standalone browser page.
- [Phase 2: local runner and fake harness](tutorials/phase-02-local-runner.md) — run a supervised Rust process, exercise scripted live scenarios, resolve requests, interrupt a turn, and confirm live/replay parity.
- [Phase 3: break recovery on purpose](tutorials/phase-03-break-recovery.md) — lose an ACK, drop the socket, restart the runner side, replay durable events, and inspect recovery diagnostics.
- [Phase 4: run the skillless Codex driver](tutorials/phase-04-skillless-codex.md) — run a safe real-model task, inspect the exact context boundary, steer or interrupt it, and confirm one replay-safe result.
- [Phase 4b: run the protocol demo server](tutorials/phase-04b-protocol-server.md) — exercise the server-only Codex boundary, canonical replay, typed controls, and reconnect with `curl`.
- [Phase 4b: run the live Codex protocol console](tutorials/phase-04b-live-console.md) — chat with a live session in the browser, steer it, stop it three ways, answer its requests, change its goal, break its connection, and replay the record.
- [Phase 5: run the SDK console and mini consumer](tutorials/phase-05-sdk-console.md) — exercise the versioned public browser/React surface in two independent consumers against fake and real drivers.
- [Phase 6: run the thin Paperclip adapter](tutorials/phase-06-thin-paperclip-adapter.md) — prove mock/real conformance, run one feature-flagged local task, inspect replay/finalization, disable the kill switch, and verify legacy fallback.
- [Phase 7: the Paperclip-style issue thread over a mock control plane](tutorials/phase-07-issue-thread.md) — the canonical clean-start tutorial for the final build: prove the 106-case conformance suite, open the issue thread in deterministic fake mode, record the byte-stable screenshot matrix, and optionally drive a real Codex turn — all from a clean checkout with no Paperclip service.
- [Phase 7: explore the capability contract and scenario explorer](tutorials/phase-07-scenario-explorer.md) — companion tour of the read-only browser explorer over the mock control plane and the focused verification set.
- [Phase 7I: chat with the mock control plane](tutorials/phase-07i-scenario-chat.md) — the scenario-explorer chat surface: send prompts to a Phase 7 scenario and watch the mock Paperclip activity for every turn: exposure, typed calls, denials, control-plane-owned actions, state diffs, wakes, and per-turn parity.
- [Phase 7: chat with real Codex in a clean room](tutorials/phase-07-clean-room-chat.md) — the second primary path: open a blank chat on a freshly minted mock tenant, send a free-form message to real Codex through real runnerd, and inspect the tool, policy, and state evidence on demand.
- [Cumulative end-to-end tutorial](tutorials/end-to-end.md) — the shortest complete workflow available at the current phase.
- [Conformance: standalone tracer](tutorials/conformance-standalone-tracer.md) — install the workspace, validate its Rust/TypeScript boundary and knowledge bundle, then run the deterministic Rust mock-core path and cross-language parity check.
- [Replay: static PRP replay](tutorials/replay.md) — validate the shared schema/fixture corpus, reduce a fixture in the CLI, and inspect the same final snapshot in the standalone browser page.
- [Local runner: local runner and fake harness](tutorials/local-runner.md) — run a supervised Rust process, exercise scripted live scenarios, resolve requests, interrupt a turn, and confirm live/replay parity.
- [Durable recovery: break recovery on purpose](tutorials/durable-recovery.md) — lose an ACK, drop the socket, restart the runner side, replay durable events, and inspect recovery diagnostics.
- [Codex: run the skillless Codex driver](tutorials/codex.md) — run a safe real-model task, inspect the exact context boundary, steer or interrupt it, and confirm one replay-safe result.
- [Live console: run the protocol demo server](tutorials/live-console-protocol-server.md) — exercise the server-only Codex boundary, canonical replay, typed controls, and reconnect with `curl`.
- [Live console: run the live Codex protocol console](tutorials/live-console.md) — chat with a live session in the browser, steer it, stop it three ways, answer its requests, change its goal, break its connection, and replay the record.
- [SDK: run the SDK console and mini consumer](tutorials/sdk-console.md) — exercise the versioned public browser/React surface in two independent consumers against fake and real drivers.
- [Standalone: run the thin Paperclip adapter](tutorials/standalone-thin-paperclip-adapter.md) — prove mock/real conformance, run one feature-flagged local task, inspect replay/finalization, disable the kill switch, and verify legacy fallback.
- [Capability: the Paperclip-style issue thread over a mock control plane](tutorials/capability-issue-thread.md) — the canonical clean-start tutorial for the final build: prove the 106-case conformance suite, open the issue thread in deterministic fake mode, record the byte-stable screenshot matrix, and optionally drive a real Codex turn — all from a clean checkout with no Paperclip service.
- [Capability: explore the capability contract and scenario explorer](tutorials/capability-scenario-explorer.md) — companion tour of the read-only browser explorer over the mock control plane and the focused verification set.
- [Scenario chat: chat with the mock control plane](tutorials/scenario-chat.md) — the scenario-explorer chat surface: send prompts to a Capability scenario and watch the mock Paperclip activity for every turn: exposure, typed calls, denials, control-plane-owned actions, state diffs, wakes, and per-turn parity.
- [Capability: chat with real Codex in a clean room](tutorials/capability-clean-room-chat.md) — the second primary path: open a blank chat on a freshly minted mock tenant, send a free-form message to real Codex through real runnerd, and inspect the tool, policy, and state evidence on demand.
- [Cumulative end-to-end tutorial](tutorials/end-to-end.md) — the shortest complete workflow for the current package.
## Reference
- [Architecture and dependency boundary](architecture.md)
- [PRP compatibility and versioning policy](protocol-compatibility.md)
- [Phase 2 local protocol and supervision](phase-02-local-protocol.md)
- [Phase 3 durable transport and recovery](phase-03-durable-transport.md)
- [Phase 4 skillless Codex driver](phase-04-skillless-codex-driver.md)
- [Phase 4b protocol and demo server](phase-04b-protocol-server.md)
- [Phase 4b live console](phase-04b-live-console.md)
- [Phase 5 browser SDK and reference console](phase-05-sdk.md)
- [Phase 6 thin Paperclip adapter](phase-06-thin-paperclip-adapter.md)
- [Phase 7 capability contract (generated)](phase-07-capability-contract.md)
- [Phase 7 capability disposition](phase-07-capability-disposition.md)
- [Phase 7 mock ControlPlanePort](phase-07-mock-control-plane-port.md)
- [Phase 7 semantic catalog and authorization](phase-07-semantic-catalog.md)
- [Phase 7 semantic tool catalog](phase-07-semantic-tools.md)
- [Phase 7 authorization and exposure](phase-07-authorization-and-exposure.md)
- [Phase 7 eval-derived conformance](phase-07-eval-conformance.md)
- [Phase 7 browser scenario explorer](phase-07-scenario-explorer.md)
- [Phase 7 live runnerd/Codex loop](phase-07-live-runnerd-codex.md)
- [Phase 7 execution modes and identity (fake vs real Codex)](phase-07-execution-modes.md)
- [Phase 7 Paperclip-style issue-thread UI](phase-07-issue-thread-ui.md)
- [Phase 7 clean-room live chat](phase-07-clean-room-chat.md)
- [Phase 7I interactive scenario chat](phase-07i-scenario-chat.md)
- [Phase 7 final evidence manifest (Revision 3/4)](../knowledge/evidence/2026-08-10-phase-07i-final-evidence.md)
- [Phase 7 future binding boundary (Phase 8 / ACPX)](phase-07-future-binding-boundary.md)
- [Phase 7 verification commands](phase-07-verification-commands.md)
- [Phase 7 scenario explorer UX interaction map](design/phase-7-scenario-explorer-ux.md)
- [Phase 7I mobile chat UX interaction map](design/phase-7i-mobile-chat-ux.md)
- [Engineering journal guide](journal.md)
- [Local protocol and supervision](local-runner.md)
- [Durable transport and recovery](durable-recovery.md)
- [Codex skillless Codex driver](codex-driver.md)
- [Live console protocol and demo server](live-console-protocol-server.md)
- [Live console](live-console.md)
- [SDK browser SDK and reference console](sdk.md)
- [Standalone thin Paperclip adapter](standalone-thin-paperclip-adapter.md)
- [Capability contract (generated)](capability-contract.md)
- [Capability disposition](capability-disposition.md)
- [Capability mock ControlPlanePort](capability-mock-control-plane-port.md)
- [Capability semantic catalog and authorization](capability-semantic-catalog.md)
- [Capability semantic tool catalog](capability-semantic-tools.md)
- [Capability authorization and exposure](capability-authorization-and-exposure.md)
- [Capability eval-derived conformance](capability-eval-conformance.md)
- [Capability browser scenario explorer](capability-scenario-explorer.md)
- [Capability live runnerd/Codex loop](capability-live-runnerd-codex.md)
- [Capability execution modes and identity (fake vs real Codex)](capability-execution-modes.md)
- [Capability Paperclip-style issue-thread UI](capability-issue-thread-ui.md)
- [Capability clean-room live chat](capability-clean-room-chat.md)
- [Scenario chat interactive scenario chat](scenario-chat.md)
- [Capability future binding boundary (future upload integration / ACPX)](capability-future-binding-boundary.md)
- [Capability verification commands](capability-verification-commands.md)
- [Capability scenario explorer UX interaction map](design/capability-scenario-explorer-ux.md)
- [Scenario chat mobile chat UX interaction map](design/scenario-chat-ux.md)
- [Dated shadcn/ui and AI Elements compatibility note](research/2026-08-07-ui-library-compatibility.md)
- [Phase 4b live-console interaction map](design/phase-4b-interaction-map.md)
- [Phase 4b component decision record (shadcn/ui, AI Elements)](design/phase-4b-component-decisions.md)
- [Phase 5 component and SDK surface plan](design/phase-5-component-plan.md)
- [Phase 5 SDK extraction decision record](design/phase-5-component-decisions.md)
- [Phase 6 thin Paperclip adapter boundary](design/phase-6-thin-paperclip-adapter.md)
- [Live console live-console interaction map](design/live-console-interaction-map.md)
- [Live console component decision record (shadcn/ui, AI Elements)](design/live-console-component-decisions.md)
- [SDK extraction decision record](design/sdk-component-decisions.md)
- [Standalone thin Paperclip adapter boundary](design/standalone-thin-paperclip-adapter.md)
- [Package README](../README.md)
Phase 6 is implemented as a default-off server integration at the public
Standalone is implemented as a default-off server integration at the public
package boundary. Production Paperclip UI integration remains deferred.
-81
View File
@@ -1,81 +0,0 @@
# Engineering Journal Guide
The `knowledge/` directory is a package-local Google Open Knowledge Format
(OKF) v0.2 bundle. It uses ordinary Markdown concept files with YAML
frontmatter, progressive-disclosure indexes, and a chronological `log.md`.
## Bundle layout
```text
knowledge/
index.md
log.md
journal/
index.md
YYYY-MM-DD-phase-NN.md
evidence/
index.md
YYYY-MM-DD-phase-NN-verification.md
templates/
index.md
journal-entry.md
```
## Add an entry
1. Open the [journal entry template](../knowledge/templates/journal-entry.md).
2. Copy the fenced template into `knowledge/journal/YYYY-MM-DD-topic.md`.
3. Set a descriptive `type`; phase journals use
`Engineering Journal Entry` plus `phase` and `entry_kind` extension fields.
4. Record `generated` with the OKF actor convention and an ISO 8601 UTC instant.
5. Include decisions, evidence, failures, known gaps, and follow-up questions.
6. Link the entry from `knowledge/journal/index.md` and update
`knowledge/log.md`.
7. Run `pnpm --filter @paperclipai/paperclip-runner docs:validate`.
OKF v0.2 requires only `type`, but this bundle also requires `title`,
`description`, and `generated` so engineering entries remain searchable and
auditable. Unknown producer-defined fields remain valid.
## Query entries
List journal concepts:
```sh
rg -l '^type: Engineering Journal Entry$' packages/paperclip-runner/knowledge
```
Find Phase 0 entries:
```sh
rg -l '^phase: "0"$' packages/paperclip-runner/knowledge
```
Find unresolved questions or failures:
```sh
rg -n '^## (Failures|Follow-up questions)$' packages/paperclip-runner/knowledge/journal
```
Find concepts by tag:
```sh
rg -l '^tags: .*boundary' packages/paperclip-runner/knowledge
```
Indexes are the human and agent entry point; `rg` is the deterministic local
query path. No database or Paperclip service is required.
## Validation contract
The OKF validator enforces:
- root `okf_version: "0.2"` and reserved index/log rules;
- required concept frontmatter and valid lifecycle values;
- OKF actor and UTC timestamp syntax in `generated`;
- typed phase journal extension fields;
- ISO date headings in `log.md`;
- index coverage for concepts and child directories.
The [OKF v0.2 specification](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md)
remains authoritative; the local requirements only tighten authoring quality.
@@ -1,8 +1,8 @@
# Phase 4b Protocol and Demo Server
# Live console Protocol and Demo Server
## Scope
This is the lowest Phase 4b layer. It extends the package-local Codex driver,
This is the lowest Live console layer. It extends the package-local Codex driver,
typed harness contract, deterministic fixtures, and mock-core demo server. It
does not import or change Paperclip server, UI, database, or production routes.
@@ -13,7 +13,7 @@ threads. No provider or Paperclip credential is serialized to browser state.
## Deterministic fixture
`protocol/fixtures/phase-04b/driver-conformance.json` records the exact Codex
`protocol/fixtures/codex-driver/driver-conformance.json` records the exact Codex
0.132.0 wire shapes used by conformance tests:
- command, file, permission, user-input, and MCP elicitation requests;
@@ -79,7 +79,7 @@ notifications are ignored instead of being attached to the active session.
Start it with:
```sh
pnpm --filter @paperclipai/paperclip-runner demo:phase4b -- \
pnpm --filter @paperclipai/paperclip-runner demo:live-console -- \
--host 127.0.0.1 --port 4174
```
@@ -93,17 +93,17 @@ it is never returned by an API response or browser state.
Important routes:
- `GET /api/phase4b/health`
- `POST /api/phase4b/sessions`
- `GET /api/phase4b/sessions/:id`
- `GET /api/phase4b/sessions/:id/events?after=:cursor`
- `GET /api/phase4b/sessions/:id/stream?after=:cursor`
- `POST /api/phase4b/sessions/:id/steer`
- `POST /api/phase4b/sessions/:id/interrupt`
- `POST /api/phase4b/sessions/:id/requests/:requestId/resolve`
- `POST /api/phase4b/sessions/:id/goal/:operation`
- `POST /api/phase4b/sessions/:id/reconnect`
- `POST /api/phase4b/sessions/:id/close`
- `GET /api/liveConsole/health`
- `POST /api/liveConsole/sessions`
- `GET /api/liveConsole/sessions/:id`
- `GET /api/liveConsole/sessions/:id/events?after=:cursor`
- `GET /api/liveConsole/sessions/:id/stream?after=:cursor`
- `POST /api/liveConsole/sessions/:id/steer`
- `POST /api/liveConsole/sessions/:id/interrupt`
- `POST /api/liveConsole/sessions/:id/requests/:requestId/resolve`
- `POST /api/liveConsole/sessions/:id/goal/:operation`
- `POST /api/liveConsole/sessions/:id/reconnect`
- `POST /api/liveConsole/sessions/:id/close`
The server chooses the working directory. A create body cannot override it.
Provider, Paperclip, cookie, and bearer credential fields in create bodies are
@@ -131,9 +131,8 @@ Run deterministic conformance first, then the real Codex boundary:
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/drivers/codex/codex-app-server-driver.test.ts \
src/mock-core/phase4b-demo-server.test.ts
pnpm --filter @paperclipai/paperclip-runner record:phase4b
src/mock-core/live-console-demo-server.test.ts
pnpm --filter @paperclipai/paperclip-runner record:live-console
```
See the [verification record](../knowledge/evidence/2026-08-08-phase-04b-protocol-server-verification.md)
and [real server trace](../knowledge/evidence/phase-04b-real-codex-server.json).
See the [real server trace](../knowledge/evidence/live-console-real-codex-server.json).
@@ -1,12 +1,12 @@
# Phase 4b Live Console
# Live console Live Console
## Scope
This is the browser layer of Phase 4b. It adds a **Live console** mode to the
This is the browser layer of Live console. It adds a **Live console** mode to the
existing standalone devtool in `devtools/browser/`. It builds on the
[protocol and demo server](phase-04b-protocol-server.md) and follows the
approved [interaction map](design/phase-4b-interaction-map.md) and
[component decision record](design/phase-4b-component-decisions.md).
[protocol and demo server](live-console-protocol-server.md) and follows the
approved [interaction map](design/live-console-interaction-map.md) and
[component decision record](design/live-console-component-decisions.md).
Every file lives under `packages/paperclip-runner/`. The console does not
import or change Paperclip server, UI, database, or control-plane code.
@@ -14,12 +14,12 @@ import or change Paperclip server, UI, database, or control-plane code.
## Start it
```sh
pnpm --filter @paperclipai/paperclip-runner console:phase4b
pnpm --filter @paperclipai/paperclip-runner console:live-console
```
Then open `http://127.0.0.1:4180/` and choose **Live console**.
Phase 4b is deliberately loopback-only. A wildcard or LAN `--host` fails
Live console is deliberately loopback-only. A wildcard or LAN `--host` fails
startup because this developer console can start provider-backed turns and
resolve approvals. There is no unauthenticated remote mode.
@@ -27,8 +27,8 @@ Two environment variables change the driver behind the console:
| Variable | Default | Effect |
| --- | --- | --- |
| `PAPERCLIP_PHASE4B_DRIVER` | `demo` | `codex` starts the real `codex app-server` driver behind the same routes |
| `PAPERCLIP_PHASE4B_CHUNK_DELAY_MS` | `45` | Milliseconds between streamed chunks in the demo driver |
| `PAPERCLIP_LIVE_CONSOLE_DRIVER` | `demo` | `codex` starts the real `codex app-server` driver behind the same routes |
| `PAPERCLIP_LIVE_CONSOLE_CHUNK_DELAY_MS` | `45` | Milliseconds between streamed chunks in the demo driver |
## Where state comes from
@@ -77,7 +77,7 @@ directly, because a destructive action needs an explicit activation.
## Demo chats
The demo server owns the manifest catalogue and serves it at
`GET /api/phase4b/manifests`. Each manifest names the scenario it proves and
`GET /api/liveConsole/manifests`. Each manifest names the scenario it proves and
the observations a human should confirm; the console renders that list as a
checklist rather than restating it in the UI.
@@ -131,13 +131,12 @@ component file carries a raw colour, pixel, or font value.
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/mock-core/phase4b-scripted-driver.test.ts \
src/mock-core/phase4b-demo-server.test.ts \
src/mock-core/live-console-scripted-driver.test.ts \
src/mock-core/live-console-demo-server.test.ts \
devtools/browser/src/live/transcript-model.test.ts
pnpm --filter @paperclipai/paperclip-runner test:browser
pnpm --filter @paperclipai/paperclip-runner check:browser-tokens
pnpm --filter @paperclipai/paperclip-runner check:forbidden-imports
```
See the [verification record](../knowledge/evidence/2026-08-08-phase-04b-live-console-verification.md)
and the [tutorial](tutorials/phase-04b-live-console.md).
See the [tutorial](tutorials/live-console.md).
@@ -1,8 +1,8 @@
# Phase 2 Local Protocol and Supervision Reference
# Local runner Local Protocol and Supervision Reference
## Scope
Phase 2 proves one native local session. It uses no Paperclip service, database,
Local runner proves one native local session. It uses no Paperclip service, database,
network provider, or model. The TypeScript mock core, Rust runner, and Rust fake
harness are all inside `packages/paperclip-runner/`.
@@ -39,13 +39,13 @@ sequence, and timestamp. Script delay can be overridden for fast tests.
- The same ID and same content returns `duplicate` without repeating an effect.
- The same ID with different content is rejected.
- A sequence gap is rejected.
- Phase 2 supports prepare, open, turn start, request resolution, interruption,
- Local runner supports prepare, open, turn start, request resolution, interruption,
stop, close, and cancel command shapes. A script consumes only the commands it
expects.
## Fake-driver scripts
Scripts live under `protocol/fixtures/phase-02/scripts/`.
Scripts live under `protocol/fixtures/local-runner/scripts/`.
| Script | Proof |
|---|---|
@@ -103,8 +103,8 @@ succeeded.
The Vite middleware starts the same TypeScript mock-core controller used by
tests. It streams history plus new events as NDJSON. The browser validates each
event with the Phase 1 schema, checks run/session binding, and applies the Phase
1 reducer. At completion it replays the full event list and compares both final
event with the replay schema, checks run/session binding, and applies the
session reducer. At completion it replays the full event list and compares both final
snapshots.
The browser reuses package-local shadcn-style Button, Badge, Card, and Textarea
@@ -114,7 +114,7 @@ components. All visual values remain in `styles.css`.
The browser server is a developer-only loopback transport, not an authenticated
Paperclip API. Vite dev and preview default to `127.0.0.1`. Every
`/api/phase2/*` request is checked before route lookup or process startup:
`/api/localRunner/*` request is checked before route lookup or process startup:
- both ends of the accepted socket and the HTTP `Host` must be loopback;
- an `Origin`, when a browser supplies one, must exactly match the request
@@ -126,12 +126,12 @@ Paperclip API. Vite dev and preview default to `127.0.0.1`. Every
run.
These are secure defaults plus defense in depth: even if Vite is accidentally
started with a broad `--host`, non-loopback Phase 2 API traffic is rejected.
started with a broad `--host`, non-loopback Local runner API traffic is rejected.
There is intentionally no user identity, session cookie, or reusable bearer
credential in this spike.
Residual risk: another process running as the same local user is inside this
trust boundary and can make loopback requests. The scripted Phase 2 event set is
trust boundary and can make loopback requests. The scripted Local runner event set is
finite, but the retained event history inside one active run is not yet a
general-purpose per-session byte budget. A later real harness/network phase must
add authenticated capabilities, per-session event/byte budgets, durable
@@ -140,6 +140,6 @@ transport.
## Deferred work
Phase 2 has no durable outbox, ACK, reconnect, runner restart recovery, real
Local runner has no durable outbox, ACK, reconnect, runner restart recovery, real
harness, production Paperclip bridge, or browser-to-runner connection. Those
items require later authorized phases.
@@ -1,54 +0,0 @@
# Phase 7 Verification Commands
Every command here runs from the repository root, is offline and deterministic,
starts no Paperclip service, and holds no credential. All are prefixed
`pnpm --filter @paperclipai/paperclip-runner`. The observed results for the
acceptance record are in
[the Phase 7 verification evidence](../knowledge/evidence/2026-08-09-phase-07-verification.md).
| Surface | Command | Expected result |
| --- | --- | --- |
| Capability contract completeness and drift | `check:phase7-inventory` | `Phase 7 inventory completeness and generated-output checks passed.` |
| Contract validator negatives | `test:phase7-inventory` | 4 tests pass |
| Mock control plane + shared port | `exec vitest run src/conformance/control-plane-port.test.ts src/mock-core/phase7-mock-control-plane-adapter.test.ts` | 9 tests pass |
| Semantic tools + authorization/redaction | `exec vitest run src/tools/phase7-semantic-tools.test.ts` | 9 tests pass |
| 106-case conformance | `test:phase7-evals` | 1 file (all 106 cases) passes |
| Fake-agent matrix + bounded Codex + parity report | `report:phase7-evals` | `Phase 7 eval conformance passed: 106 cases across 16 groups.` |
| Scenario runtime + explorer + clean room + routes | `test:phase7` | 159 tests pass |
| Browser IA, accessibility, determinism, boundary | `test:browser:phase7` | Playwright suites pass (60 in the issue-thread/clean-room suite) |
| Screenshot acceptance set | `record:phase7` | 24 deterministic images |
| Doc links + OKF bundle | `docs:validate` | 73 files, 28 OKF concepts, 5 indexes |
## Live commands (not offline)
The clean-room chat has no fake or replay path, so its end-to-end proof needs a
Rust toolchain and a locally authenticated Codex. These are the only Phase 7
commands that start a provider.
| Surface | Command | Expected result |
| --- | --- | --- |
| Real Codex through real runnerd on a fresh mock tenant | `smoke:phase7:cleanroom` | every assertion `true`; two `MCK-` identifiers, one per chat |
| Preset issue thread against real Codex | `smoke:phase7:ui` | every assertion `true` |
| Clean-room screenshots | `record:phase7:cleanroom` | 7 images; intentionally not byte-stable |
## Notes
- **No Rust toolchain is needed** for any Phase 7 command. The full
`verify` target still builds the Rust workspace, but nothing above does.
- **Determinism.** Fake-mode runs render fixture time only, so repeat runs and
repeat screenshots are byte-identical.
- **The parity report is generated on demand.** `report:phase7-evals` writes
`knowledge/evidence/phase-07/eval-parity-report.{json,md}`, which are not
committed. Because the `.md` carries no OKF frontmatter, run `docs:validate`
**before** generating the report, or delete the report first. This dirty-tree
interaction is tracked for the Phase 7H QA gate (`PAP-16907`).
- **Browser libraries.** On minimal or rootless hosts, extract Playwright's
browser libraries first; the package's `verify:rootless` target shows the
pattern.
## Related
- [Clean-start tutorial](tutorials/phase-07-scenario-explorer.md)
- [Eval conformance](phase-07-eval-conformance.md)
- [Scenario explorer](phase-07-scenario-explorer.md)
- [Clean-room live chat](phase-07-clean-room-chat.md)
@@ -3,20 +3,20 @@
## Authority
The JSON Schema files in [`protocol/schemas/`](../protocol/schemas/) are the
language-neutral source of truth for the Phase 1 and Phase 2 executable contract. The
language-neutral source of truth for the Replay and Local runner executable contract. The
generated TypeScript schema module is checked against those files before every
TypeScript typecheck. Rust consumes the same fixtures and must produce the same
golden parity summaries.
The broader normative protocol remains the
[native-runner spike specification](../spec/paperclip-native-runner-spike-spec.md).
Phase 2 reuses this contract for local live events. It adds package-local stdio
[native-runner spike specification](../spec/native-runner-contract.md).
Local runner reuses this contract for local live events. It adds package-local stdio
and stream envelopes, but it does not add durable transport, persistence, or
production control-plane behavior.
## Version fields
| Field | Phase 1 support | Compatibility rule |
| Field | Replay support | Compatibility rule |
|---|---:|---|
| `protocolVersion` | `1` | Required. Negotiate the highest overlapping version; no overlap fails closed. |
| `fixtureVersion` | `1` | Required by the conformance corpus. Unknown values fail closed. |
@@ -56,11 +56,11 @@ required v2 protocol cannot be replayed by this consumer.
out of order.
- Replaying an already-applied batch leaves the snapshot unchanged.
The CLI and browser import the same `replayPhase1FixtureText` function, so
The CLI and browser import the same `replayReplayFixtureText` function, so
validation, compatibility errors, and final snapshots cannot drift between the
two surfaces.
## Phase 2 local envelope rules
## Local envelope rules
- Mock-core commands use `paperclip.prp.command.v1` over stdin JSONL.
- Runner output uses `paperclip.runner.stream.v1` over stdout JSONL.
@@ -75,9 +75,9 @@ two surfaces.
- The live browser rejects an event with an invalid schema, run ID, or session
ID before it reaches the reducer.
These envelopes are local Phase 2 implementation contracts.
These envelopes are local Local runner implementation contracts.
## Phase 3 durable wire rules
## Durable wire rules
- The runner opens an outbound WebSocket and sends PRP v1 `hello` before any
command result or event.
@@ -97,7 +97,7 @@ These envelopes are local Phase 2 implementation contracts.
- Frames are bounded at 1 MiB and upgrade headers at 16 KiB. Unknown or invalid
required protocol data fails closed; malformed JSON is a bounded diagnostic.
These are package-local Phase 3 rules. Production TLS, control-plane admission,
These are package-local Durable recovery rules. Production TLS, control-plane admission,
and deployment policy remain separately reviewed work.
## Change policy
@@ -2,7 +2,7 @@
## Decision
Phase 0 adds no UI runtime and no shadcn/ui, AI Elements, React, Tailwind, Next.js,
Conformance adds no UI runtime and no shadcn/ui, AI Elements, React, Tailwind, Next.js,
or AI SDK dependency to `@paperclipai/paperclip-runner`. A later browser phase
should adapt source components selectively after protocol and reducer contracts
exist.
@@ -15,7 +15,7 @@ Sources were checked on 2026-08-07.
|---|---|---|
| shadcn/ui | Official documentation describes open-code component distribution and provides first-class Vite and monorepo setup paths. | Compatible with Paperclip's Vite UI model. Reuse or adapt checked-in source while keeping Paperclip tokens and accessibility rules authoritative. |
| AI Elements | Official documentation describes a registry built on shadcn/ui, targets React 19 and Tailwind CSS 4, and lists Next.js plus AI SDK as prerequisites. | Source shapes are promising, but the documented turnkey setup does not match a standalone Vite runner console. Evaluate individual source components; do not adopt the Next.js/AI SDK runtime stack by default. |
| Phase 0 package | This phase has no browser surface. | Keep the dependency graph empty at runtime and defer all component selection. |
| Conformance package | This phase has no browser surface. | Keep the dependency graph empty at runtime and defer all component selection. |
## Candidate later evaluation
@@ -38,4 +38,4 @@ against these gates:
- [Vercel AI Elements introduction and prerequisites](https://elements.ai-sdk.dev/docs)
This note is a compatibility record, not authorization to add a UI surface in
Phase 0.
Conformance.
@@ -1,16 +1,16 @@
# Phase 7I Scenario Chat
# Scenario chat Scenario Chat
The scenario chat is an interactive surface over the same package-local Phase 7
runner and [mock control plane](phase-07-mock-control-plane-port.md) that the
[scenario explorer](phase-07-scenario-explorer.md) reads. You pick a scenario,
The scenario chat is an interactive surface over the same package-local Capability
runner and [mock control plane](capability-mock-control-plane-port.md) that the
[scenario explorer](capability-scenario-explorer.md) reads. You pick a scenario,
send prompts, and watch the mock Paperclip activity for every turn instead of
having it hidden behind the conversation. It contacts no Paperclip service,
holds no credential, and introduces no ACPX implementation.
Sources: `src/phase7/chat-session.ts`, `src/phase7/chat-script.ts`,
`src/phase7/scenario-execution.ts`, and `examples/phase7-explorer/`.
Sources: `src/scenarios/chat-session.ts`, `src/scenarios/chat-script.ts`,
`src/scenarios/scenario-execution.ts`, and `examples/scenario-explorer/`.
Interaction contract:
[Phase 7I mobile chat UX](design/phase-7i-mobile-chat-ux.md).
[Scenario chat mobile chat UX](design/scenario-chat-ux.md).
## The turn model
@@ -38,7 +38,7 @@ boundary, a per-turn diff, or a per-turn verdict itself.
| Control-plane actions with audit and decision references | the 7C mock core |
| State diff over the ten entity domains | immutable fixture snapshots |
| Wake / reconciliation events | the 7C mock core's own scheduled wakes |
| Turn parity verdict | `phase7TurnParity` |
| Turn parity verdict | `capabilityTurnParity` |
A turn is judged on what that turn did. A session-level expectation about work
the board has not asked for yet would read as a failure of turn 1 rather than
@@ -96,10 +96,10 @@ session** returns to the seeded turn 0 and stays there.
## Commands
```sh
pnpm --filter @paperclipai/paperclip-runner run demo:phase7 # open the surface
pnpm --filter @paperclipai/paperclip-runner run test:phase7 # runtime + component tests
pnpm --filter @paperclipai/paperclip-runner run test:browser:phase7
pnpm --filter @paperclipai/paperclip-runner run record:phase7i # §11 screenshot matrix
pnpm --filter @paperclipai/paperclip-runner run demo:scenarios # open the surface
pnpm --filter @paperclipai/paperclip-runner run test:scenarios # runtime + component tests
pnpm --filter @paperclipai/paperclip-runner run test:browser:scenarios
pnpm --filter @paperclipai/paperclip-runner run record:scenario-chat # §11 screenshot matrix
```
Walkthrough: [Phase 7I scenario chat tutorial](tutorials/phase-07i-scenario-chat.md).
Walkthrough: [Scenario chat scenario chat tutorial](tutorials/scenario-chat.md).
@@ -1,12 +1,12 @@
# Phase 5 Browser SDK and Reference Console
# SDK Browser SDK and Reference Console
## Public package surface
Phase 5 freezes the browser SDK as package version `0.1.2`.
SDK freezes the browser SDK as package version `0.1.2`.
Version `0.1.1` adds direct chat mode. The browser sends the operator message
as plain Codex input. The protocol inspector still shows the event stream,
reducer state, requests, and replay data. The Phase 4 task fixtures remain
reducer state, requests, and replay data. The Codex task fixtures remain
available from the session controls.
Version `0.1.2` adds the canonical events for each transcript item to the
@@ -33,7 +33,7 @@ than render it on the server.
```ts
import { createRunnerClient } from "@paperclipai/paperclip-runner/browser";
const client = createRunnerClient({ baseUrl: "/api/phase4b" });
const client = createRunnerClient({ baseUrl: "/api/liveConsole" });
const manifests = await client.fetchManifests();
```
@@ -52,7 +52,7 @@ identities, or removes duplicate events before the shared reducer sees them.
import { useRunnerConsole } from "@paperclipai/paperclip-runner/react";
function Console() {
const runner = useRunnerConsole({ baseUrl: "/api/phase4b" });
const runner = useRunnerConsole({ baseUrl: "/api/liveConsole" });
return <p>{runner.connection}: {runner.replayParity ? "match" : "waiting"}</p>;
}
```
@@ -81,7 +81,7 @@ lineage, goal, connection, and replay states come only from canonical events
or public session state.
The reference server has two driver modes behind the same HTTP/SSE contract.
Deterministic manifests use `Phase4bScriptedDriver`. Real chat mode uses
Deterministic manifests use `LiveConsoleScriptedDriver`. Real chat mode uses
`CodexAppServerDriver`, which starts a real local `codex app-server` process.
The Node server owns provider authentication and the disposable working
directory; browser code receives only canonical runner events.
@@ -167,5 +167,5 @@ exports. `examples/mini-consumer/` imports only the three public SDK subpaths
and visibly exercises every extension point. Both run against the package fake
driver or the real Codex driver through the same server-only adapter.
See the [hand-run tutorial](tutorials/phase-05-sdk-console.md) and the
[implementation decision record](design/phase-5-component-decisions.md).
See the [hand-run tutorial](tutorials/sdk-console.md) and the
[implementation decision record](design/sdk-component-decisions.md).
@@ -1,6 +1,6 @@
# Phase 6 Standalone Adapter Demo
# Standalone Standalone Adapter Demo
Phase 6 currently proves the thin adapter boundary as a standalone package demo.
Standalone currently proves the thin adapter boundary as a standalone package demo.
It does not install or enable the runner in a Paperclip instance. All active
implementation, documentation, tutorial, and evidence work remains under
`packages/paperclip-runner/`.
@@ -18,13 +18,13 @@ enabled and the kill switch is disabled. Both paths use the public
```sh
# Default legacy path
pnpm --filter @paperclipai/paperclip-runner trace:phase6
pnpm --filter @paperclipai/paperclip-runner trace:standalone
# Native standalone path
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --feature-flag enabled
pnpm --filter @paperclipai/paperclip-runner trace:standalone -- --feature-flag enabled
# Kill-switch rollback
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --feature-flag enabled --kill-switch enabled
pnpm --filter @paperclipai/paperclip-runner trace:standalone -- --feature-flag enabled --kill-switch enabled
```
## Standalone page
@@ -32,10 +32,10 @@ pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --feature-flag enabl
Run:
```sh
pnpm --filter @paperclipai/paperclip-runner demo:phase6
pnpm --filter @paperclipai/paperclip-runner demo:standalone
```
Then open `http://127.0.0.1:4182/phase6-standalone/`. The page exposes mode
Then open `http://127.0.0.1:4182/standalone-demo/`. The page exposes mode
selection, contract conformance, reducer projection, replay, finalization, and
native/legacy invocation counters. It makes no network call to Paperclip.
@@ -51,8 +51,7 @@ The package-local tracer verifies:
Company auth, budgets, approvals, audit, workspaces, and product persistence are
not claimed by this standalone checkpoint. Testing those concerns in a real
Paperclip instance is explicitly deferred until the board authorizes a separate
integration phase.
Paperclip instance remains outside this package and requires a separately
reviewed integration.
See the [runnable tutorial](tutorials/phase-06-thin-paperclip-adapter.md) and the
[verification record](../knowledge/evidence/2026-08-09-phase-06-verification.md).
See the [runnable tutorial](tutorials/standalone-thin-paperclip-adapter.md).
@@ -1,4 +1,4 @@
# Phase 7 Tutorial: Chat With Real Codex In A Clean Room
# Capability Tutorial: Chat With Real Codex In A Clean Room
**Time to first success: about 3 minutes**, once the package is installed. You
open a blank chat, type whatever you like, and watch a real Codex session work a
@@ -6,10 +6,10 @@ brand-new mock Paperclip issue through a real `paperclip-runnerd` process. No
scenario to pick, no recorded transcript, and no real Paperclip service anywhere
in the picture.
This is the second primary Phase 7 path. The first — the preset scenario
This is the second primary Capability path. The first — the preset scenario
explorer — is covered by the
[issue-thread tutorial](phase-07-issue-thread.md) and stays available and
unchanged. Read [the clean-room reference](../phase-07-clean-room-chat.md) for
[issue-thread tutorial](capability-issue-thread.md) and stays available and
unchanged. Read [the clean-room reference](../capability-clean-room-chat.md) for
the seed, exposure profile, bounds, and route contract behind what you see here.
## What you need
@@ -34,7 +34,7 @@ build tooling lives there.
## 1. Prove the path without a browser (about 1 minute)
```sh
pnpm --filter @paperclipai/paperclip-runner smoke:phase7:cleanroom -- --json
pnpm --filter @paperclipai/paperclip-runner smoke:capability:cleanroom -- --json
```
This builds the TypeScript output and `paperclip-runnerd`, starts the package
@@ -43,7 +43,7 @@ the same HTTP routes the browser uses. Expected shape:
```json
{
"schema": "paperclip.phase7.clean-room-smoke.v1",
"schema": "paperclip.capability.clean-room-smoke.v1",
"firstIssue": "MCK-2114",
"newChatIssue": "MCK-9539",
"turns": 2,
@@ -139,9 +139,9 @@ http://127.0.0.1:4184/#/issue/hb-baseline?shot=denial-optional-tool&capture=1
## Where to go next
- [Clean-room chat reference](../phase-07-clean-room-chat.md) — seed, exposure
- [Clean-room chat reference](../capability-clean-room-chat.md) — seed, exposure
profile, session routes, and bounds.
- [Execution modes and identity](../phase-07-execution-modes.md) — why a
- [Execution modes and identity](../capability-execution-modes.md) — why a
`mode=fake` artifact can never satisfy a live criterion.
- [Verification commands](../phase-07-verification-commands.md) — the full
- [Verification commands](../capability-verification-commands.md) — the full
command set, including the browser and screenshot gates.
@@ -1,4 +1,4 @@
# Phase 7 Clean-Start Tutorial: The Paperclip-Style Issue Thread Over a Mock Control Plane
# Capability Clean-Start Tutorial: The Paperclip-Style Issue Thread Over a Mock Control Plane
**Time to first success: about 5 minutes.** Two commands take you from a clean
checkout to 106 passing conformance cases and a Paperclip-style issue thread you
@@ -7,19 +7,19 @@ root. It starts no Paperclip service, contacts no Paperclip control plane, clone
no external eval repository, and holds no provider credential. Everything the
default path needs is checked in under `packages/paperclip-runner/`.
This is the canonical Phase 7 tutorial for the final build. It covers both agent
This is the canonical Capability tutorial for the final build. It covers both agent
modes: **scripted (deterministic) fake mode**, which is fully offline, and
**Codex (bounded) live mode**, which is optional and needs a locally
authenticated Codex. The two modes share one mock control plane, one semantic
tool catalog, one authorization engine, and one view contract. See
[execution modes and identity](../phase-07-execution-modes.md) for the rules that
separate them, and [the future binding boundary](../phase-07-future-binding-boundary.md)
for why none of this touches real Paperclip — that is Phase 8 and needs separate
[execution modes and identity](../capability-execution-modes.md) for the rules that
separate them, and [the future binding boundary](../capability-future-binding-boundary.md)
for why none of this touches real Paperclip — that is future upload integration and needs separate
approval.
For a deeper tour of the read-only scenario explorer and the capability
contract, see the companion
[scenario-explorer tutorial](phase-07-scenario-explorer.md); this page focuses
[scenario-explorer tutorial](capability-scenario-explorer.md); this page focuses
on the issue-thread surface and the live loop.
## What you need
@@ -46,7 +46,7 @@ inherits `NODE_ENV=production`.
## 1. Prove the 106-case conformance suite (about 1 minute)
```sh
pnpm --filter @paperclipai/paperclip-runner test:phase7-evals
pnpm --filter @paperclipai/paperclip-runner test:capability-evals
```
This drives all 106 eval-derived cases across the 16 groups entirely in-process
@@ -61,23 +61,23 @@ For the per-group counts, assertion classes, the 18-operation fake-agent matrix,
and the 16-case bounded Codex binding sample, generate the parity report:
```sh
pnpm --filter @paperclipai/paperclip-runner report:phase7-evals
pnpm --filter @paperclipai/paperclip-runner report:capability-evals
```
Expected final line:
```
Phase 7 eval conformance passed: 106 cases across 16 groups.
Capability eval conformance passed: 106 cases across 16 groups.
```
The report is written to `knowledge/evidence/phase-07/eval-parity-report.{json,md}`.
The report is written to `knowledge/evidence/capability/eval-parity-report.{json,md}`.
It is generated on demand and is not committed; delete it before running
`docs:validate`, or run validation first.
## 2. Run the issue-thread contract and UI tests (about 1 minute)
```sh
pnpm --filter @paperclipai/paperclip-runner test:phase7
pnpm --filter @paperclipai/paperclip-runner test:scenarios
```
Expected final lines:
@@ -88,7 +88,7 @@ Expected final lines:
```
This covers the mock `ControlPlanePort`, the semantic tools with their
authorization and redaction rules, and the `Phase7IssueThreadSnapshot`
authorization and redaction rules, and the `CapabilityIssueThreadSnapshot`
view-model contract — including that every one of the twelve deterministic
screenshot states builds a schema-valid snapshot with no credential anywhere in
the projected view.
@@ -110,7 +110,7 @@ collapsed on the right.
Swap the `shot` value to seed any of the twelve contract states — each one is a
different item type or interaction state from the
[issue-thread UX contract](../design/phase-7-issue-thread-ux-contract.md):
[issue-thread UX contract](../design/capability-issue-thread-ux-contract.md):
| `shot=` | What it shows |
| --- | --- |
@@ -142,12 +142,12 @@ never scrolls horizontally.
## 4. Record the deterministic screenshot matrix (about 1 minute)
```sh
pnpm --filter @paperclipai/paperclip-runner record:phase7:ui
pnpm --filter @paperclipai/paperclip-runner check:phase7:ui
pnpm --filter @paperclipai/paperclip-runner record:capability:ui
pnpm --filter @paperclipai/paperclip-runner check:capability:ui
```
The first writes 24 images — the twelve slugs at 1440×900 and 390×844 — to
`knowledge/evidence/phase-07/ui/`. Every mobile capture asserts no horizontal
`knowledge/evidence/capability/ui/`. Every mobile capture asserts no horizontal
scroll before the shot. The second re-records and confirms all 24 reproduce
byte-for-byte, which is the property that makes the fake-mode matrix a stable
acceptance artifact. Both modes render fixture time only, so a fresh checkout
@@ -164,7 +164,7 @@ installed** — nothing above depends on it.
Headless smoke over the package server:
```sh
pnpm --filter @paperclipai/paperclip-runner smoke:phase7:ui
pnpm --filter @paperclipai/paperclip-runner smoke:capability:ui
```
This creates a session, runs live turns, and asserts the identity reads
@@ -175,10 +175,10 @@ withheld from the agent, and that no credential appears in the view.
Capture the live browser frames (a real browser driving a real Codex turn):
```sh
pnpm --filter @paperclipai/paperclip-runner record:phase7:ui:live
pnpm --filter @paperclipai/paperclip-runner record:capability:ui:live
```
The frames land in `knowledge/evidence/phase-07/ui-live/`. They are intentionally
The frames land in `knowledge/evidence/capability/ui-live/`. They are intentionally
**not** byte-stable — a real provider turn varies — so they are excluded from the
determinism gate and are the mode that satisfies the final live acceptance
criteria. A scripted (`mode=fake`) frame cannot.
@@ -186,7 +186,7 @@ criteria. A scripted (`mode=fake`) frame cannot.
Prove the process and network boundary directly:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase7 -- --json
pnpm --filter @paperclipai/paperclip-runner trace:live-runner -- --json
```
This checks a real semantic-tool mutation, a typed result, a same-thread second
@@ -212,12 +212,11 @@ and the runner session, not on the agent:
## What this does not do
- No ACPX implementation and no real control-plane integration. Real Paperclip
binding is Phase 8 and requires separate approval.
binding is future upload integration and requires separate approval.
- No provider, runner, or control-plane credential in the browser, ever.
- Nothing you type is persisted beyond the mock session. A reload drops an
unsent draft — the accepted trade against storing conversation content.
References: [issue-thread UI](../phase-07-issue-thread-ui.md),
[live runnerd and Codex loop](../phase-07-live-runnerd-codex.md),
[execution modes and identity](../phase-07-execution-modes.md),
[final evidence manifest](../../knowledge/evidence/2026-08-10-phase-07i-final-evidence.md).
References: [issue-thread UI](../capability-issue-thread-ui.md),
[live runnerd and Codex loop](../capability-live-runnerd-codex.md),
[execution modes and identity](../capability-execution-modes.md).
@@ -1,4 +1,4 @@
# Phase 7 Clean-Start Tutorial: Capability Contract, Mock Control Plane, and Scenario Explorer
# Capability Clean-Start Tutorial: Capability Contract, Mock Control Plane, and Scenario Explorer
**Time to first success: about 5 minutes.** Two commands take you from a clean
checkout to 106 passing conformance cases and a browser you can click through.
@@ -7,13 +7,13 @@ service, contacts no Paperclip control plane, clones no external eval
repository, and holds no provider credential. Everything it needs is checked in
under `packages/paperclip-runner/`.
Phase 7 does not integrate the runner into Paperclip. It builds a package-local
Capability does not integrate the runner into Paperclip. It builds a package-local
model of what a native Paperclip run *would* do — a deterministic mock control
plane, a transport-neutral semantic tool catalog, an authorization engine, a
106-case conformance suite derived from the Paperclip Evals corpus, and a
read-only browser explorer over all of it. Real integration is Phase 8 and
read-only browser explorer over all of it. Real integration is future upload integration and
requires separate approval. See
[the future binding boundary reference](../phase-07-future-binding-boundary.md).
[the future binding boundary reference](../capability-future-binding-boundary.md).
## What you need
@@ -32,7 +32,7 @@ pnpm install --filter @paperclipai/paperclip-runner --lockfile=false --offline -
## 1. Prove the 106-case conformance suite (about 1 minute)
```sh
pnpm --filter @paperclipai/paperclip-runner test:phase7-evals
pnpm --filter @paperclipai/paperclip-runner test:capability-evals
```
This runs the eval-derived conformance suite entirely in-process against the
@@ -48,17 +48,17 @@ per-group counts, assertion classes, the fake-agent operation matrix, and the
bounded Codex binding sample, generate the parity report:
```sh
pnpm --filter @paperclipai/paperclip-runner report:phase7-evals
pnpm --filter @paperclipai/paperclip-runner report:capability-evals
```
Expected final line:
```
Phase 7 eval conformance passed: 106 cases across 16 groups.
Capability eval conformance passed: 106 cases across 16 groups.
```
The report is written to
`knowledge/evidence/phase-07/eval-parity-report.{json,md}`. It is a
`knowledge/evidence/capability/eval-parity-report.{json,md}`. It is a
generated-on-demand artifact, not a committed file; a clean checkout does not
contain it. Delete it before you run `pnpm --filter @paperclipai/paperclip-runner docs:validate`
(the report carries no OKF frontmatter and would otherwise fail the knowledge
@@ -76,10 +76,10 @@ The 16 groups and case counts are fixed by the capability contract:
## 2. Open the scenario explorer (about 2 minutes)
```sh
pnpm --filter @paperclipai/paperclip-runner demo:phase7
pnpm --filter @paperclipai/paperclip-runner demo:scenarios
```
Open `http://127.0.0.1:4183/phase7-explorer/`. The explorer is read-only. It
Open `http://127.0.0.1:4183/scenario-explorer/`. The explorer is read-only. It
runs each scenario in the browser against the same mock control plane and
renders the run artifact; it never re-judges parity and never leaves its own
origin. Walk this path:
@@ -99,88 +99,85 @@ origin. Walk this path:
makes no request beyond its own assets.
The explorer imports the frozen `0.1.2` SDK through the package-local
`@paperclip-runner-local/phase7` alias — deliberately not the published package
name. See [the browser explorer reference](../phase-07-scenario-explorer.md).
`@paperclip-runner-local/capability` alias — deliberately not the published package
name. See [the browser explorer reference](../capability-scenario-explorer.md).
## 3. Read what each surface guarantees
The explorer renders four surfaces produced by the runtime. Each has a
package-local reference page:
- [Capability disposition](../phase-07-capability-disposition.md) — how every
- [Capability disposition](../capability-disposition.md) — how every
skill/reference behavior and eval case is classified as control-plane-owned,
always-agent-tool, or optional-agent-tool, and how the generated contract is
produced and checked.
- [Mock ControlPlanePort](../phase-07-mock-control-plane-port.md) — the
- [Mock ControlPlanePort](../capability-mock-control-plane-port.md) — the
deterministic in-memory adapter, its entity domains, and its boundary.
- [Semantic tool catalog](../phase-07-semantic-tools.md) — the
- [Semantic tool catalog](../capability-semantic-tools.md) — the
transport-neutral tools an agent may call.
- [Authorization and exposure](../phase-07-authorization-and-exposure.md) —
- [Authorization and exposure](../capability-authorization-and-exposure.md) —
grants, typed denials, and secret redaction.
- [Eval conformance](../phase-07-eval-conformance.md) — how the 106 cases are
- [Eval conformance](../capability-eval-conformance.md) — how the 106 cases are
derived from the checked-in traceability derivative with no external repo.
## 4. Run the full focused verification set
Every command below is offline, deterministic, and package-local. The
[verification commands reference](../phase-07-verification-commands.md) lists
[verification commands reference](../capability-verification-commands.md) lists
each one with its purpose and expected result.
```sh
# Capability contract: completeness, uniqueness, one-to-one MCP folds, no drift.
pnpm --filter @paperclipai/paperclip-runner check:phase7-inventory
pnpm --filter @paperclipai/paperclip-runner test:phase7-inventory
pnpm --filter @paperclipai/paperclip-runner check:capability-inventory
pnpm --filter @paperclipai/paperclip-runner test:capability-inventory
# Mock control plane adapter and shared ControlPlanePort conformance.
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/conformance/control-plane-port.test.ts \
src/mock-core/phase7-mock-control-plane-adapter.test.ts
src/mock-core/capability-mock-control-plane-adapter.test.ts
# Semantic tool catalog and authorization/redaction engine.
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/tools/phase7-semantic-tools.test.ts
src/tools/capability-semantic-tools.test.ts
# 106-case conformance and the parity/fake-agent/bounded-Codex report.
pnpm --filter @paperclipai/paperclip-runner test:phase7-evals
pnpm --filter @paperclipai/paperclip-runner report:phase7-evals
pnpm --filter @paperclipai/paperclip-runner test:capability-evals
pnpm --filter @paperclipai/paperclip-runner report:capability-evals
# Scenario runtime, explorer components, and route determinism (49 tests).
pnpm --filter @paperclipai/paperclip-runner test:phase7
pnpm --filter @paperclipai/paperclip-runner test:scenarios
# Browser information architecture, accessibility, determinism, and boundary.
pnpm --filter @paperclipai/paperclip-runner test:browser:phase7
pnpm --filter @paperclipai/paperclip-runner test:browser:scenarios
# Deterministic 24-image screenshot acceptance set.
pnpm --filter @paperclipai/paperclip-runner record:phase7
pnpm --filter @paperclipai/paperclip-runner record:capability
# Documentation links and the OKF knowledge bundle (run with no generated
# report present).
pnpm --filter @paperclipai/paperclip-runner docs:validate
```
The exact results captured for the acceptance record are in the
[Phase 7 verification evidence](../../knowledge/evidence/2026-08-09-phase-07-verification.md).
## Known gaps and boundaries
- **The parity report breaks the OKF bundle if left in place.**
`report:phase7-evals` writes `eval-parity-report.md` into the knowledge
`report:capability-evals` writes `eval-parity-report.md` into the knowledge
bundle without OKF frontmatter, so `docs:validate` fails while it is present.
Delete it before validating. A clean checkout never contains it. This is
recorded for the Phase 7H QA gate.
recorded for the Capability QA gate.
- **Codex mode is disabled.** The explorer offers a bounded real-Codex binding
sample only through `report:phase7-evals`; the browser holds no credential
sample only through `report:capability-evals`; the browser holds no credential
and the in-page Codex mode is inert with a stated reason.
- **No Paperclip integration.** Nothing here touches a real control plane,
database, or provider. Phase 8 (ACPX) binds the real adapter behind the same
database, or provider. future upload integration (ACPX) binds the real adapter behind the same
seam and requires separate CTO approval.
## Where this sits
- Plan: Phase 7 plan, issue `PAP-16897` (see its `#document-plan` document).
- Plan: Capability plan, issue `PAP-16897` (see its `#document-plan` document).
- Child issues: 7A capability inventory (PAP-16898), 7B UX map (PAP-16899),
7C mock adapter (PAP-16900), 7D semantic tools and authorization (PAP-16901,
security gate PAP-16902, remediation PAP-16909), 7E conformance suite
(PAP-16903), 7F browser explorer (PAP-16904, UX gate PAP-16905), 7G docs and
evidence (PAP-16906), 7H clean-room QA (PAP-16907), Phase 7 checkpoint
evidence (PAP-16906), 7H clean-room QA (PAP-16907), Capability checkpoint
(PAP-16908).
@@ -1,8 +1,8 @@
# Phase 4: Run the Skillless Codex Driver
# Codex: Run the Skillless Codex Driver
## What this phase is
Phase 4 connects the mock core to a real Codex app-server session.
Codex connects the mock core to a real Codex app-server session.
Codex gets one small task envelope. Codex does not get the Paperclip skill.
Codex does not get a Paperclip API key or API instructions.
@@ -54,11 +54,11 @@ Confirm that the driver tests pass and the package boundary check says
Choose a temporary directory and trace file:
```sh
phase4_workspace="$(mktemp -d)"
phase4_trace="$phase4_workspace/trace.json"
pnpm --filter @paperclipai/paperclip-runner trace:phase4 -- \
--working-directory "$phase4_workspace" \
--output "$phase4_trace"
codex_workspace="$(mktemp -d)"
codex_trace="$codex_workspace/trace.json"
pnpm --filter @paperclipai/paperclip-runner trace:codex -- \
--working-directory "$codex_workspace" \
--output "$codex_trace"
```
Confirm:
@@ -68,7 +68,7 @@ Confirm:
- the file has the exact text:
```sh
test "$(cat "$phase4_workspace/hello.txt")" = "hello from phase 4"
test "$(cat "$codex_workspace/hello.txt")" = "hello from Codex runner"
```
## Step 3: Inspect the exact model boundary
@@ -89,7 +89,7 @@ jq '.context | {
dynamicToolNames,
modelInputKinds,
envelope
}' "$phase4_trace"
}' "$codex_trace"
```
Confirm:
@@ -111,7 +111,7 @@ Confirm:
Check that no control-plane route or bearer credential appears:
```sh
if rg -ni 'authorization: bearer|paperclip_api_key|/api/issues/' "$phase4_trace"; then
if rg -ni 'authorization: bearer|paperclip_api_key|/api/issues/' "$codex_trace"; then
echo "FAIL: forbidden control-plane context found"
exit 1
fi
@@ -121,8 +121,8 @@ fi
```sh
jq '[.events[].eventType] | group_by(.) | map({type: .[0], count: length})' \
"$phase4_trace"
jq '.assertions' "$phase4_trace"
"$codex_trace"
jq '.assertions' "$codex_trace"
```
Confirm:
@@ -140,7 +140,7 @@ and may not need human input for this small task.
To record the authenticated isolation proof, run:
```sh
pnpm --filter @paperclipai/paperclip-runner record:phase4
pnpm --filter @paperclipai/paperclip-runner record:codex
```
The recorder selects every existing readable host Codex credential/config file
@@ -161,9 +161,9 @@ attempts to consume absent outputs.
Use a new directory:
```sh
phase4_steer_workspace="$(mktemp -d)"
pnpm --filter @paperclipai/paperclip-runner trace:phase4 -- \
--working-directory "$phase4_steer_workspace" \
codex_steer_workspace="$(mktemp -d)"
pnpm --filter @paperclipai/paperclip-runner trace:codex -- \
--working-directory "$codex_steer_workspace" \
--steer "Keep the answer short and verify the exact file text."
```
@@ -175,9 +175,9 @@ still returns one result.
Use a new directory:
```sh
phase4_interrupt_workspace="$(mktemp -d)"
pnpm --filter @paperclipai/paperclip-runner trace:phase4 -- \
--working-directory "$phase4_interrupt_workspace" \
codex_interrupt_workspace="$(mktemp -d)"
pnpm --filter @paperclipai/paperclip-runner trace:codex -- \
--working-directory "$codex_interrupt_workspace" \
--interrupt
```
@@ -195,10 +195,7 @@ This command is the package acceptance path. The real Codex example remains a
separate command because automated environments may not have a signed-in Codex
session.
## Step 8: Read the evidence
## Step 8: Read the retained trace
- [Phase 4 driver reference](../phase-04-skillless-codex-driver.md)
- [Phase 4 verification evidence](../../knowledge/evidence/2026-08-08-phase-04-verification.md)
- [Phase 4 journal](../../knowledge/journal/2026-08-08-phase-04.md)
Phase 5 is not part of this tutorial and remains uncreated.
- [Codex driver reference](../codex-driver.md)
- [Codex normalized trace](../../knowledge/evidence/codex-trace.json)
@@ -1,8 +1,8 @@
# Phase 0: Run the Standalone Tracer
# Conformance: Run the Standalone Tracer
## What this phase is
Phase 0 is the smallest standalone runner path. It uses a mock control plane and does not start Paperclip.
Conformance is the smallest standalone runner path. It uses a mock control plane and does not start Paperclip.
## What this phase proves
@@ -27,7 +27,7 @@ pnpm install --filter @paperclipai/paperclip-runner --lockfile=false --offline -
```
`--lockfile=false` follows this repository's policy that automation owns the
root lockfile. `--offline` proves Phase 0 needs no newly downloaded package.
root lockfile. `--offline` proves Conformance needs no newly downloaded package.
`--dev` makes the package tooling explicit even when the calling shell has
`NODE_ENV=production`.
@@ -64,13 +64,13 @@ dates, the root version declaration, and index coverage.
## 5. Run the tracer
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase0
pnpm --filter @paperclipai/paperclip-runner trace:conformance
```
Expected final line:
```json
{"schemaVersion":"paperclip.runner.phase0.output.v1","runIdentity":{"runId":"run_phase0_0001","sessionId":"session_phase0_0001"},"result":{"status":"succeeded","summary":"Standalone Phase 0 fixture accepted."}}
{"schemaVersion":"paperclip.runner.conformance.output.v1","runIdentity":{"runId":"run_conformance_0001","sessionId":"session_conformance_0001"},"result":{"status":"succeeded","summary":"Standalone Conformance fixture accepted."}}
```
The command exits successfully after the mock core stops. No service remains
@@ -79,21 +79,18 @@ The default tracer is Rust. Prove that the TypeScript reference produces the
same bytes with:
```sh
pnpm --filter @paperclipai/paperclip-runner check:phase0-parity
pnpm --filter @paperclipai/paperclip-runner check:conformance-parity
```
Expected result: `Rust and TypeScript Phase 0 tracer output matches the shared
Expected result: `Rust and TypeScript Conformance tracer output matches the shared
golden fixture.`
## 6. Inspect and query the journal
## 6. Inspect the conformance fixture
```sh
sed -n '1,240p' packages/paperclip-runner/knowledge/journal/2026-08-07-phase-00.md
rg -l '^type: Engineering Journal Entry$' packages/paperclip-runner/knowledge
sed -n '1,240p' packages/paperclip-runner/protocol/fixtures/conformance-minimal-run.json
```
For more query and authoring examples, see the [journal guide](../journal.md).
## One-command rerun
After installation, the same checks can be repeated with:
@@ -1,8 +1,8 @@
# Phase 3: Break It on Purpose
# Durable recovery: Break It on Purpose
## What this phase is
Phase 3 adds a durable outbound WebSocket connection.
Durable recovery adds a durable outbound WebSocket connection.
The Rust runner connects to a package-local mock core. The runner stores events
before it sends them. It stores each command result before it replies.
@@ -27,7 +27,7 @@ From the repository root, build the package binaries:
```sh
pnpm --filter @paperclipai/paperclip-runner build:typescript
pnpm --filter @paperclipai/paperclip-runner build:phase2-binaries
pnpm --filter @paperclipai/paperclip-runner build:runner-binaries
```
The commands below use only loopback network access. They create temporary
@@ -39,7 +39,7 @@ removes that state after each successful proof.
Run:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault socket-drop
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault socket-drop
```
Confirm:
@@ -54,7 +54,7 @@ Confirm:
Run:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault lost-ack
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault lost-ack
```
Confirm:
@@ -73,7 +73,7 @@ logical event and advances one cumulative cursor.
Run:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault runner-restart
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault runner-restart
```
Confirm:
@@ -92,7 +92,7 @@ continues the same durable source cursor.
Run:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault harness-restart
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault harness-restart
```
Confirm:
@@ -109,7 +109,7 @@ second child. The normalized session, turn, and item IDs do not change.
Run:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault duplicate-command
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault duplicate-command
```
Confirm:
@@ -123,7 +123,7 @@ Confirm:
Run:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault storage-pressure
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault storage-pressure
```
Confirm:
@@ -139,9 +139,9 @@ Confirm:
Run:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault lease-expiry
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault drain
pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault revoke
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault lease-expiry
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault drain
pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault revoke
```
Confirm:
@@ -161,7 +161,7 @@ pnpm --filter @paperclipai/paperclip-runner browser:dev --host 127.0.0.1 --port
```
Open `http://127.0.0.1:4179/` and select **Recovery**. Use the **Fault** selector
to run these cases with **Run Phase 3 recovery**:
to run these cases with **Run Durable recovery**:
1. **Normal connection**
2. **Socket drop**
@@ -202,14 +202,10 @@ pnpm --filter @paperclipai/paperclip-runner verify
The verification path builds TypeScript, Rust, and the browser. It runs unit,
integration, browser, boundary, documentation, parity, and tracer checks for
Phases 0 through 3.
conformance, replay, local execution, and durable recovery.
## Evidence
- [Phase 3 verification record](../../knowledge/evidence/2026-08-07-phase-03-verification.md)
- [Phase 3 recovery screenshot](../../knowledge/evidence/phase-03-recovery-diagnostics.png)
- [Phase 3 OKF journal entry](../../knowledge/journal/2026-08-07-phase-03.md)
- [Phase 3 transport reference](../phase-03-durable-transport.md)
Phase 4 remains uncreated. A human must accept the Phase 3 checkpoint before
later work starts.
- [Durable recovery fault matrix](../../knowledge/evidence/durable-recovery-fault-matrix.json)
- [Durable recovery trace](../../knowledge/evidence/durable-recovery-trace.json)
- [Durable recovery transport reference](../durable-recovery.md)
@@ -3,13 +3,13 @@
## What this tutorial is
This tutorial combines each implemented Native Runner phase into one procedure.
It currently includes Phase 0 through the Phase 6 Paperclip adapter tracer.
It currently includes Conformance through the Standalone Paperclip adapter tracer.
## What this tutorial proves
This tutorial proves that the standalone package boundary, static replay path,
local live-run path, durable recovery path, and direct Codex driver work
together. It does not use the Paperclip control plane. Phase 4 uses a real
together. It does not use the Paperclip control plane. Codex uses a real
local Codex session through the mock core.
The current system includes the Rust mock-core tracer, shared protocol fixtures,
@@ -18,59 +18,44 @@ replay modes. It also includes the Rust outbound WebSocket client, durable
outbox, package-local mock core, recovery CLI, and recovery browser view.
The final phase adds a skillless task envelope, direct app-server driver,
semantic completion tools, and the same reducer/replay proof used by fixtures.
Phase 5 freezes that browser transport and reducer projection as public SDK
SDK freezes that browser transport and reducer projection as public SDK
subpaths, then proves them with a reference console and a second consumer.
Phase 6 consumes the public runner contract from Paperclip behind a default-off
Standalone consumes the public runner contract from Paperclip behind a default-off
flag while preserving server-owned workspace, governance, and status authority.
## Current end-to-end path
1. Follow [Phase 0: Run the Standalone Tracer](phase-00-standalone-tracer.md).
2. Confirm the final JSON contains `run_phase0_0001`,
`session_phase0_0001`, and `succeeded`.
1. Follow [Conformance: Run the Standalone Tracer](conformance-standalone-tracer.md).
2. Confirm the final JSON contains `run_conformance_0001`,
`session_conformance_0001`, and `succeeded`.
3. Confirm the cross-language parity check passes.
4. Confirm the shell prompt returns and no Paperclip service was started.
5. Open the [Phase 0 journal entry](../../knowledge/journal/2026-08-07-phase-00.md)
and its linked verification evidence.
6. Follow [Phase 1: Validate and Replay a PRP Fixture](phase-01-static-replay.md).
7. Compare the happy-path CLI snapshot with the browser page.
8. Exercise the duplicate, gap, unknown-field, and unsupported-version fixtures.
9. Open the [Phase 1 journal entry](../../knowledge/journal/2026-08-07-phase-01.md)
and its linked verification evidence.
10. Follow [Phase 2: Run the Local Runner and Fake Harness](phase-02-local-runner.md).
11. Run the happy, permission/input, interruption, error, and duplicate-terminal scenarios.
12. Open the browser live mode and confirm the completed run says `Match` for live and replay output.
13. Open the [Phase 2 journal entry](../../knowledge/journal/2026-08-07-phase-02.md)
and its linked verification evidence and screenshots.
14. Follow [Phase 3: Break Recovery on Purpose](phase-03-break-recovery.md).
15. Confirm that the recovered runner and session IDs stay the same.
16. Confirm that the outbox is empty after replay and cumulative acknowledgement.
17. Open the [Phase 3 journal entry](../../knowledge/journal/2026-08-07-phase-03.md).
18. Follow [Phase 4: Run the Skillless Codex Driver](phase-04-skillless-codex.md).
19. Inspect the exact model-context snapshot and confirm that it has no
5. Follow [Replay: Validate and Replay a PRP Fixture](replay.md).
6. Compare the happy-path CLI snapshot with the browser page and exercise the
duplicate, gap, unknown-field, and unsupported-version fixtures.
7. Follow [Local runner: Run the Local Runner and Fake Harness](local-runner.md).
8. Run the happy, permission/input, interruption, error, and duplicate-terminal scenarios.
9. Open the browser live mode and confirm the completed run says `Match` for live and replay output.
10. Follow [Durable recovery: Break Recovery on Purpose](durable-recovery.md).
11. Confirm that the recovered runner and session IDs stay the same and that
the outbox is empty after replay and cumulative acknowledgement.
12. Follow [Codex: Run the Skillless Codex Driver](codex.md).
13. Inspect the exact model-context snapshot and confirm that it has no
Paperclip instructions, bearer credentials, or unrelated skills.
20. Run the safe task, then steer and interrupt separate sessions. Confirm
14. Run the safe task, then steer and interrupt separate sessions. Confirm
stable session identities and exactly one result and terminal event.
21. Open the [Phase 4 journal entry](../../knowledge/journal/2026-08-08-phase-04.md)
and its linked real-session trace and verification evidence.
22. Follow [Phase 4b: Run the Protocol Demo Server](phase-04b-protocol-server.md).
23. Confirm requests stay pending for a typed browser decision, stale steering
15. Follow [Live console: Run the Protocol Demo Server](live-console-protocol-server.md).
16. Confirm requests stay pending for a typed browser decision, stale steering
is rejected, and reconnect keeps the same run and session identities.
24. Open the [Phase 4b protocol/server journal](../../knowledge/journal/2026-08-08-phase-04b-protocol-server.md)
and its linked deterministic and real-Codex evidence.
25. Follow [Phase 5: Run the SDK Console and Mini Consumer](phase-05-sdk-console.md).
26. Run the fake lifecycle in both consumers, then confirm the mini consumer
17. Follow [SDK: Run the SDK Console and Mini Consumer](sdk-console.md).
18. Run the fake lifecycle in both consumers, then confirm the mini consumer
reaches `Replay parity: match` after reconnect and replay.
27. Run the safe real-Codex browser smoke and inspect the Phase 5 screenshots.
28. Open the [Phase 5 journal](../../knowledge/journal/2026-08-08-phase-05-sdk.md)
and its linked package-acceptance evidence.
29. Follow [Phase 6: Run the Thin Paperclip Adapter](phase-06-thin-paperclip-adapter.md).
30. Run the unchanged port conformance suite against mock and database-backed
19. Run the safe real-Codex browser smoke.
20. Follow [Standalone: Run the Thin Paperclip Adapter](standalone-thin-paperclip-adapter.md).
21. Run the unchanged port conformance suite against mock and database-backed
Paperclip ports, then inspect one local feature-flagged task.
31. Disable the flag and confirm a fresh task selects legacy while persisted
22. Disable the flag and confirm a fresh task selects legacy while persisted
native finalization remains native.
32. Open the [Phase 6 implementation journal](../../knowledge/journal/2026-08-09-phase-06-implementation.md)
and its linked verification evidence.
The one-command form after installation is:
@@ -104,7 +89,7 @@ pnpm --filter @paperclipai/paperclip-runner verify:rootless
- process exit is recorded separately from the structured semantic result;
- bounded logs retain only their configured tail;
- exactly one terminal event closes every completed local trace;
- every live browser event passes the Phase 1 validator and reducer before display;
- every live browser event passes the Replay validator and reducer before display;
- replaying the completed live event list produces the same final snapshot.
- a lost cumulative ACK replays the same durable event ID without a second
logical event;
@@ -163,22 +148,22 @@ pnpm --filter @paperclipai/paperclip-runner verify:rootless
## Step 6: Chat with a live session in the browser
```sh
pnpm --filter @paperclipai/paperclip-runner console:phase4b
pnpm --filter @paperclipai/paperclip-runner console:live-console
```
Open `http://127.0.0.1:4180/` and press **Live console**. Work through the
[Phase 4b live console tutorial](phase-04b-live-console.md) to reach every
[Live console tutorial](live-console.md) to reach every
state above from the eleven deterministic demo chats. Add
`PAPERCLIP_PHASE4B_DRIVER=codex` to run the identical screens against a real
`PAPERCLIP_LIVE_CONSOLE_DRIVER=codex` to run the identical screens against a real
Codex session.
## Step 7: Run the reusable SDK consumers
```sh
pnpm --filter @paperclipai/paperclip-runner console:phase5
pnpm --filter @paperclipai/paperclip-runner console:sdk
```
Open `http://127.0.0.1:4181/reference-console/` and
`http://127.0.0.1:4181/mini-consumer/`. Follow the
[Phase 5 tutorial](phase-05-sdk-console.md) for the deterministic lifecycle,
[SDK tutorial](sdk-console.md) for the deterministic lifecycle,
real-Codex smoke, keyboard checks, and package acceptance command.
@@ -1,8 +1,8 @@
# Phase 4b: Run the Protocol Demo Server
# Live console: Run the Protocol Demo Server
## What this is
This tutorial runs the Phase 4b server layer. It has no browser UI yet. You can
This tutorial runs the Live console server layer. It has no browser UI yet. You can
use `curl` to act like the browser.
The server starts Codex. Codex login data stays on the server. The JSON replies
@@ -15,7 +15,7 @@ From the repository root, run:
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/drivers/codex/codex-app-server-driver.test.ts \
src/mock-core/phase4b-demo-server.test.ts
src/mock-core/live-console-demo-server.test.ts
```
Expected result: both files pass. The tests cover requests, goals, lineage,
@@ -26,11 +26,11 @@ steering, interrupt races, reconnect, replay, and redaction.
Use an empty directory outside your Codex home:
```sh
phase4b_workspace="$(mktemp -d)"
pnpm --filter @paperclipai/paperclip-runner demo:phase4b -- \
liveConsole_workspace="$(mktemp -d)"
pnpm --filter @paperclipai/paperclip-runner demo:live-console -- \
--host 127.0.0.1 \
--port 4174 \
--working-directory "$phase4b_workspace"
--working-directory "$liveConsole_workspace"
```
Keep this terminal open. The first output line is JSON. It names the host,
@@ -42,7 +42,7 @@ port, and server-owned working directory. It also says
In a second terminal, run:
```sh
curl -s http://127.0.0.1:4174/api/phase4b/health \
curl -s http://127.0.0.1:4174/api/liveConsole/health \
-H 'Origin: http://127.0.0.1:4174' \
-H 'Sec-Fetch-Site: same-origin' | jq
```
@@ -57,12 +57,12 @@ Expected facts:
## Step 4: Start one safe session
```sh
curl -s -X POST http://127.0.0.1:4174/api/phase4b/sessions \
curl -s -X POST http://127.0.0.1:4174/api/liveConsole/sessions \
-H 'Origin: http://127.0.0.1:4174' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'content-type: application/json' \
--data '{"objective":"Create demo.txt with exactly: phase4b demo","message":"Complete and verify the exact file task."}' \
| tee "$phase4b_workspace/session.json" | jq
--data '{"objective":"Create demo.txt with exactly: liveConsole demo","message":"Complete and verify the exact file task."}' \
| tee "$liveConsole_workspace/session.json" | jq
```
Copy `sessionId` and `activeTurnId` from the reply. The server ignores a
@@ -72,8 +72,8 @@ fields.
## Step 5: Read live or replayed events
```sh
session_id="$(jq -r .sessionId "$phase4b_workspace/session.json")"
curl -s "http://127.0.0.1:4174/api/phase4b/sessions/$session_id/events?after=0" \
session_id="$(jq -r .sessionId "$liveConsole_workspace/session.json")"
curl -s "http://127.0.0.1:4174/api/liveConsole/sessions/$session_id/events?after=0" \
-H 'Origin: http://127.0.0.1:4174' \
-H 'Sec-Fetch-Site: same-origin' | jq
```
@@ -85,7 +85,7 @@ view and replay.
For a live stream, run:
```sh
curl -N "http://127.0.0.1:4174/api/phase4b/sessions/$session_id/stream?after=0" \
curl -N "http://127.0.0.1:4174/api/liveConsole/sessions/$session_id/stream?after=0" \
-H 'Origin: http://127.0.0.1:4174' \
-H 'Sec-Fetch-Site: same-origin'
```
@@ -94,7 +94,7 @@ curl -N "http://127.0.0.1:4174/api/phase4b/sessions/$session_id/stream?after=0"
```sh
curl -s -X POST \
"http://127.0.0.1:4174/api/phase4b/sessions/$session_id/reconnect" \
"http://127.0.0.1:4174/api/liveConsole/sessions/$session_id/reconnect" \
-H 'Origin: http://127.0.0.1:4174' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'content-type: application/json' --data '{}' | jq
@@ -114,7 +114,7 @@ approval policy remain the final execution boundary.
Stop the manual server. Then run:
```sh
pnpm --filter @paperclipai/paperclip-runner record:phase4b
pnpm --filter @paperclipai/paperclip-runner record:live-console
```
This starts an ephemeral loopback server, runs one safe real Codex task,
@@ -123,7 +123,6 @@ checks the exact file, and writes the redacted evidence JSON.
Read:
- `packages/paperclip-runner/knowledge/evidence/phase-04b-real-codex-server.json`
- `packages/paperclip-runner/knowledge/evidence/2026-08-08-phase-04b-protocol-server-verification.md`
- `packages/paperclip-runner/knowledge/evidence/live-console-real-codex-server.json`
The deterministic fixture is the conformance authority. A real model can vary.
@@ -1,9 +1,9 @@
# Phase 4b: Run the Live Codex Protocol Console
# Live console: Run the Live Codex Protocol Console
## What this is
This tutorial runs the Phase 4b browser console. The
[protocol demo server](phase-04b-protocol-server.md) tutorial used `curl`. This
This tutorial runs the Live console browser console. The
[protocol demo server](live-console-protocol-server.md) tutorial used `curl`. This
one uses the real user interface.
## What this proves
@@ -22,8 +22,8 @@ for this tutorial. The default driver replays deterministic demo chats.
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/mock-core/phase4b-scripted-driver.test.ts \
src/mock-core/phase4b-demo-server.test.ts \
src/mock-core/live-console-scripted-driver.test.ts \
src/mock-core/live-console-demo-server.test.ts \
devtools/browser/src/live/transcript-model.test.ts
```
@@ -33,7 +33,7 @@ routes, and the pure transcript model the console renders.
## Step 2: Start the console
```sh
pnpm --filter @paperclipai/paperclip-runner console:phase4b
pnpm --filter @paperclipai/paperclip-runner console:live-console
```
Keep this terminal open. Open `http://127.0.0.1:4180/` in a browser and press
@@ -183,8 +183,8 @@ Check the **Credentials in browser** row. It must read `no`.
This step needs a working Codex login on the machine.
```sh
PAPERCLIP_PHASE4B_DRIVER=codex \
pnpm --filter @paperclipai/paperclip-runner console:phase4b
PAPERCLIP_LIVE_CONSOLE_DRIVER=codex \
pnpm --filter @paperclipai/paperclip-runner console:live-console
```
The routes and the screens are the same. The demo chat list still appears, but
@@ -199,5 +199,5 @@ temporary directory and is not part of your repository.
## Where to go next
- [Cumulative end-to-end tutorial](end-to-end.md)
- [Phase 4b live console reference](../phase-04b-live-console.md)
- [Phase 4b interaction map](../design/phase-4b-interaction-map.md)
- [Live console reference](../live-console.md)
- [Live console interaction map](../design/live-console-interaction-map.md)
@@ -1,8 +1,8 @@
# Phase 2: Run the Local Runner and Fake Harness
# Local runner: Run the Local Runner and Fake Harness
## What this phase is
Phase 2 is a local live-run path. A TypeScript mock core starts a Rust runner.
Local runner is a local live-run path. A TypeScript mock core starts a Rust runner.
The Rust runner starts a scripted fake harness. The processes use JSON lines over
stdio.
@@ -51,13 +51,13 @@ pnpm --filter @paperclipai/paperclip-runner verify
```
Expected: Rust, TypeScript, protocol, boundary, documentation, and browser
checks pass. The final Phase 2 line reports one terminal event and a successful
checks pass. The final Local runner line reports one terminal event and a successful
semantic result.
## 2. Run the happy path in the CLI
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase2 -- --scenario happy-path --quiet
pnpm --filter @paperclipai/paperclip-runner trace:local-runner -- --scenario happy-path --quiet
```
Expected summary facts:
@@ -70,20 +70,20 @@ Expected summary facts:
Capture the live trace as a validated replay fixture:
```sh
pnpm --filter @paperclipai/paperclip-runner record:phase2
pnpm --filter @paperclipai/paperclip-runner record:local-runner
```
The command writes
`packages/paperclip-runner/knowledge/evidence/phase-02-happy-path.json`.
`packages/paperclip-runner/knowledge/evidence/local-runner-happy-path.json`.
## 3. Run the scripted control flows
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase2 -- --scenario permission-input --quiet
pnpm --filter @paperclipai/paperclip-runner trace:phase2 -- --scenario interrupted --quiet
pnpm --filter @paperclipai/paperclip-runner trace:phase2 -- --scenario error --quiet
pnpm --filter @paperclipai/paperclip-runner trace:phase2 -- --scenario duplicate-terminal --quiet
pnpm --filter @paperclipai/paperclip-runner trace:phase2 -- --scenario happy-path --duplicate-turn-command --quiet
pnpm --filter @paperclipai/paperclip-runner trace:local-runner -- --scenario permission-input --quiet
pnpm --filter @paperclipai/paperclip-runner trace:local-runner -- --scenario interrupted --quiet
pnpm --filter @paperclipai/paperclip-runner trace:local-runner -- --scenario error --quiet
pnpm --filter @paperclipai/paperclip-runner trace:local-runner -- --scenario duplicate-terminal --quiet
pnpm --filter @paperclipai/paperclip-runner trace:local-runner -- --scenario happy-path --duplicate-turn-command --quiet
```
Expected:
@@ -116,21 +116,18 @@ Open `http://127.0.0.1:4179`, then follow these steps:
button becomes active.
8. Confirm the terminal badge says `cancelled`, the semantic result says
`yielded`, and the timeline contains one `run.terminal` event.
9. Switch to **Static replay** and confirm the Phase 1 fixture path still works.
9. Switch to **Static replay** and confirm the Replay fixture path still works.
Stop Vite with `Ctrl+C`.
The browser regression suite writes its temporary screenshots under the
ignored `packages/paperclip-runner/test-results/` directory. It never rewrites
the committed evidence images linked below.
the committed protocol fixtures.
## 5. Inspect the evidence
## 5. Inspect the fixtures
- [Complete live run](../../knowledge/evidence/phase-02-live-complete.png)
- [Permission request](../../knowledge/evidence/phase-02-live-permission.png)
- [Interrupted run](../../knowledge/evidence/phase-02-live-interrupted.png)
- [Exact verification record](../../knowledge/evidence/2026-08-07-phase-02-verification.md)
- [Engineering journal](../../knowledge/journal/2026-08-07-phase-02.md)
- [Local-runner fixtures](../../protocol/fixtures/local-runner/)
- [Local-runner protocol reference](../local-runner.md)
## What this proves
@@ -139,7 +136,7 @@ the committed evidence images linked below.
- logs are bounded;
- process exit and semantic result stay separate;
- duplicate commands and terminal proposals do not repeat effects;
- every browser event passes the Phase 1 validator and reducer;
- every browser event passes the Replay validator and reducer;
- replay after completion produces the same final snapshot.
Phase 3 must not begin until the Phase 2 human checkpoint is accepted.
Durable recovery must not begin until the Local runner human checkpoint is accepted.
@@ -1,8 +1,8 @@
# Phase 1: Validate and Replay a PRP Fixture
# Replay: Validate and Replay a PRP Fixture
## What this phase is
Phase 1 is a static replay path for a Paperclip Runner Protocol fixture. It uses the same reducer in the CLI and browser.
Replay is a static replay path for a Paperclip Runner Protocol fixture. It uses the same reducer in the CLI and browser.
## What this phase proves
@@ -36,8 +36,8 @@ pnpm --filter @paperclipai/paperclip-runner exec playwright install-deps chromiu
```sh
pnpm --filter @paperclipai/paperclip-runner typecheck
pnpm --filter @paperclipai/paperclip-runner check:phase1-goldens
pnpm --filter @paperclipai/paperclip-runner check:phase1-parity
pnpm --filter @paperclipai/paperclip-runner check:replay-goldens
pnpm --filter @paperclipai/paperclip-runner check:replay-parity
pnpm --filter @paperclipai/paperclip-runner check:forbidden-imports
```
@@ -47,7 +47,7 @@ same fixtures, all parity summaries match, and the standalone boundary passes.
## 2. Replay the happy path in the CLI
```sh
pnpm --filter @paperclipai/paperclip-runner replay:phase1
pnpm --filter @paperclipai/paperclip-runner replay:fixture
```
Expected JSON facts:
@@ -60,7 +60,7 @@ Expected JSON facts:
Pass a different fixture path to inspect another case:
```sh
pnpm --filter @paperclipai/paperclip-runner replay:phase1 packages/paperclip-runner/protocol/fixtures/phase-01/source-gap.json
pnpm --filter @paperclipai/paperclip-runner replay:fixture packages/paperclip-runner/protocol/fixtures/replay/source-gap.json
```
Expected: `snapshot.integrity` is `gap_detected` and the missing source sequence
@@ -69,7 +69,7 @@ is `3`.
An unsupported required version exits non-zero and returns a structured error:
```sh
pnpm --filter @paperclipai/paperclip-runner replay:phase1 packages/paperclip-runner/protocol/fixtures/phase-01/unsupported-required-version.json
pnpm --filter @paperclipai/paperclip-runner replay:fixture packages/paperclip-runner/protocol/fixtures/replay/unsupported-required-version.json
```
## 3. Open the standalone replay page
@@ -111,5 +111,5 @@ suite does not rewrite committed evidence images.
- No Paperclip server, UI, CLI, adapter, or production database module starts or
imports into the standalone path.
Phase 2 is outside this tutorial and must not start before the Phase 1 human
Local runner is outside this tutorial and must not start before the Replay human
checkpoint.
@@ -1,14 +1,14 @@
# Phase 7I Tutorial: Chat With the Mock Control Plane
# Scenario chat Tutorial: Chat With the Mock Control Plane
**Time to first success: about 3 minutes.** One command opens a chat where you
send prompts to a Phase 7 scenario and watch exactly how the mock Paperclip
send prompts to a Capability scenario and watch exactly how the mock Paperclip
control plane was used on every turn. The tutorial runs from the repository
root. It starts no Paperclip service, contacts no Paperclip control plane, and
holds no provider credential.
Phase 7I extends the Phase 7 browser surface; it does not integrate the runner
into Paperclip. Real integration is Phase 8 and requires separate approval. See
[the future binding boundary reference](../phase-07-future-binding-boundary.md).
Scenario chat extends the Capability browser surface; it does not integrate the runner
into Paperclip. Real integration is future upload integration and requires separate approval. See
[the future binding boundary reference](../capability-future-binding-boundary.md).
## What you need
@@ -24,10 +24,10 @@ pnpm install --filter @paperclipai/paperclip-runner --lockfile=false --offline -
## 1. Open the chat (about 1 minute)
```sh
pnpm --filter @paperclipai/paperclip-runner run demo:phase7
pnpm --filter @paperclipai/paperclip-runner run demo:scenarios
```
Open <http://127.0.0.1:4183/phase7-explorer/#/chat/ap-mcp-gate-01>.
Open <http://127.0.0.1:4183/scenario-explorer/#/chat/ap-mcp-gate-01>.
Before you type anything, the conversation already has content: a **session
seed** strip reading "Control plane checked out the task and opened the run — no
@@ -78,7 +78,7 @@ run simply does not hold `governance:approvals:request`. Open the turn's
## 4. See control-plane-owned work with no agent tool
Open <http://127.0.0.1:4183/phase7-explorer/#/chat/ix-checkbox-01?replay=fake>.
Open <http://127.0.0.1:4183/scenario-explorer/#/chat/ix-checkbox-01?replay=fake>.
`replay=fake` plays the recorded three-turn conversation.
Turn 2 opens a checkbox interaction. Turn 3 is the board answering it — and
@@ -107,14 +107,14 @@ confirmation: it reproduces an identical timeline.
## 6. Run the tests and record the evidence
```sh
pnpm --filter @paperclipai/paperclip-runner run test:phase7
pnpm --filter @paperclipai/paperclip-runner run test:browser:phase7
pnpm --filter @paperclipai/paperclip-runner run record:phase7i
pnpm --filter @paperclipai/paperclip-runner run test:scenarios
pnpm --filter @paperclipai/paperclip-runner run test:browser:scenarios
pnpm --filter @paperclipai/paperclip-runner run record:scenario-chat
```
The first covers the session data contract and the chat components, the second
drives the surface in a real browser at both viewports, and the third writes the
ten acceptance screenshots to `knowledge/evidence/phase-07/` — failing if any
ten acceptance screenshots to `knowledge/evidence/capability/` — failing if any
route scrolls horizontally.
## What this does not do
@@ -126,4 +126,4 @@ route scrolls horizontally.
- Nothing you type is persisted. A reload drops an unsent draft, which is the
accepted trade against storing conversation content.
Reference: [Phase 7I scenario chat](../phase-07i-scenario-chat.md).
Reference: [Scenario chat scenario chat](../scenario-chat.md).
@@ -1,8 +1,8 @@
# Phase 5: Run the SDK Console and Mini Consumer
# SDK: Run the SDK Console and Mini Consumer
## What this phase is
Phase 5 makes the accepted browser tracer reusable. It has a browser client,
SDK makes the accepted browser tracer reusable. It has a browser client,
a React hook, reusable components, one reference console, and one small second
consumer.
@@ -26,7 +26,7 @@ Run commands from the repository root.
## 1. Run the small SDK tests
```sh
pnpm --filter @paperclipai/paperclip-runner test:phase5
pnpm --filter @paperclipai/paperclip-runner test:sdk
pnpm --filter @paperclipai/paperclip-runner check:browser-tokens
pnpm --filter @paperclipai/paperclip-runner check:forbidden-imports
```
@@ -36,7 +36,7 @@ These tests check reducer parity, components, tokens, and package boundaries.
## 2. Open both apps with the fake driver
```sh
pnpm --filter @paperclipai/paperclip-runner console:phase5
pnpm --filter @paperclipai/paperclip-runner console:sdk
```
Open these pages:
@@ -103,14 +103,14 @@ feature.
On a machine with Playwright system libraries:
```sh
pnpm --filter @paperclipai/paperclip-runner test:browser:phase5
pnpm --filter @paperclipai/paperclip-runner test:browser:sdk
```
On a minimal Debian or Ubuntu machine without root access:
```sh
cd packages/paperclip-runner
bash scripts/verify-rootless-linux.sh pnpm run test:browser:phase5
bash scripts/verify-rootless-linux.sh pnpm run test:browser:sdk
```
This runs keyboard, accessibility, reconnect, gap-recovery, replay, fake
@@ -122,7 +122,7 @@ Use a machine where the local Codex CLI is already authenticated.
```sh
cd packages/paperclip-runner
bash scripts/verify-rootless-linux.sh pnpm run record:phase5:codex
bash scripts/verify-rootless-linux.sh pnpm run record:sdk:codex
```
The test runs a safe completion through both public consumers. It checks that
@@ -1,6 +1,6 @@
# Phase 6 Standalone Adapter Demo
# Standalone Standalone Adapter Demo
This tutorial exercises the Phase 6 tracer entirely inside
This tutorial exercises the Standalone tracer entirely inside
`packages/paperclip-runner/`. It does not install the runner into Paperclip,
call a Paperclip API, change an instance flag, edit an agent profile, create a
Paperclip task, or use the repository server/database integration.
@@ -10,7 +10,7 @@ Paperclip task, or use the repository server/database integration.
From the repository root:
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase6
pnpm --filter @paperclipai/paperclip-runner trace:standalone
```
The JSON trace must report:
@@ -24,7 +24,7 @@ The JSON trace must report:
## 2. Enable the native standalone path
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --feature-flag enabled
pnpm --filter @paperclipai/paperclip-runner trace:standalone -- --feature-flag enabled
```
The trace must report `resolvedMode: "native"`. Both paths execute the same
@@ -35,7 +35,7 @@ inspectable.
## 3. Prove the kill switch
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --feature-flag enabled --kill-switch enabled
pnpm --filter @paperclipai/paperclip-runner trace:standalone -- --feature-flag enabled --kill-switch enabled
```
The trace must return to `resolvedMode: "legacy"` with
@@ -45,10 +45,10 @@ allowed after the kill switch resolves the path.
## 4. Open the standalone demo page
```sh
pnpm --filter @paperclipai/paperclip-runner demo:phase6
pnpm --filter @paperclipai/paperclip-runner demo:standalone
```
Open `http://127.0.0.1:4182/phase6-standalone/`. The page starts on the legacy
Open `http://127.0.0.1:4182/standalone-demo/`. The page starts on the legacy
path. Toggle the native demo flag, inspect contract/reducer/finalization state,
then enable the kill switch and confirm the page returns to legacy. Expand the
trace JSON to inspect replay and idempotency details.
@@ -57,9 +57,9 @@ trace JSON to inspect replay and idempotency details.
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/standalone/phase6-demo.test.ts \
src/standalone/standalone-demo.test.ts \
src/conformance/control-plane-port.test.ts
pnpm --filter @paperclipai/paperclip-runner run build:phase6
pnpm --filter @paperclipai/paperclip-runner run build:standalone
```
These checks require no running Paperclip service or credentials.
@@ -24,7 +24,7 @@ function MiniConsumer() {
// fifth extension point without adding provider credentials to component
// props or browser state.
const console_ = useRunnerConsole({
baseUrl: "/api/phase4b",
baseUrl: "/api/liveConsole",
fetchImpl: injectedFetch,
eventSourceFactory: injectedEventSource,
});
@@ -1,9 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Paperclip Runner Phase 6 standalone demo</title>
</head>
<body><div id="root"></div><script type="module" src="/phase6-standalone/src/main.tsx"></script></body>
</html>
@@ -1,9 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Phase 7 scenario explorer — mock control plane</title>
</head>
<body><div id="root"></div><script type="module" src="/phase7-explorer/src/main.tsx"></script></body>
</html>
@@ -1,18 +0,0 @@
/// <reference types="vite/client" />
/**
* Declared build-time inputs for the explorer, supplied by
* `scripts/phase7-explorer-fixtures.mjs`. The explorer is an example consumer
* and reaches package or spec files only through these entry points.
*/
declare module "virtual:phase7-scenario-manifest" {
/** The checked-in Phase 7A traceability derivative, verbatim. */
const manifestSource: string;
export default manifestSource;
}
declare module "virtual:phase7-eval-report" {
/** Phase 7E's parity report, or `null` when it has not been generated. */
const evalReport: unknown;
export default evalReport;
}
@@ -2,7 +2,7 @@ import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: ".",
testMatch: "phase5.spec.ts",
testMatch: "sdk.spec.ts",
fullyParallel: false,
workers: 1,
timeout: 120_000,
@@ -12,10 +12,10 @@ export default defineConfig({
viewport: { width: 1440, height: 900 },
},
webServer: {
command: "pnpm exec vite --config vite.phase5.config.ts --host 127.0.0.1 --port 4192",
command: "pnpm exec vite --config vite.sdk.config.ts --host 127.0.0.1 --port 4192",
env: {
PAPERCLIP_PHASE4B_CHUNK_DELAY_MS: "250",
PAPERCLIP_PHASE4B_MAX_SESSIONS: "64",
PAPERCLIP_LIVE_CONSOLE_CHUNK_DELAY_MS: "250",
PAPERCLIP_LIVE_CONSOLE_MAX_SESSIONS: "64",
},
url: "http://127.0.0.1:4192/reference-console/",
reuseExistingServer: false,
@@ -2,7 +2,7 @@ import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: ".",
testMatch: "phase5-real.spec.ts",
testMatch: "sdk-real.spec.ts",
fullyParallel: false,
workers: 1,
timeout: 240_000,
@@ -12,10 +12,10 @@ export default defineConfig({
viewport: { width: 1440, height: 900 },
},
webServer: {
command: "pnpm exec vite --config vite.phase5.config.ts --host 127.0.0.1 --port 4193",
command: "pnpm exec vite --config vite.sdk.config.ts --host 127.0.0.1 --port 4193",
env: {
PAPERCLIP_PHASE4B_DRIVER: "codex",
PAPERCLIP_PHASE4B_MAX_SESSIONS: "8",
PAPERCLIP_LIVE_CONSOLE_DRIVER: "codex",
PAPERCLIP_LIVE_CONSOLE_MAX_SESSIONS: "8",
// The browser middleware creates its disposable working directory below
// this run-owned root. Tell the nested Codex process about that exact
// assignment so its workspace-boundary guard remains fail closed.
@@ -2,7 +2,7 @@ import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: ".",
testMatch: /phase7-(explorer|chat)\.spec\.ts/,
testMatch: /scenario-(explorer|chat)\.spec\.ts/,
fullyParallel: false,
workers: 1,
timeout: 120_000,
@@ -12,8 +12,8 @@ export default defineConfig({
viewport: { width: 1440, height: 900 },
},
webServer: {
command: "pnpm exec vite --config vite.phase7.config.ts --host 127.0.0.1 --port 4193",
url: "http://127.0.0.1:4193/phase7-explorer/",
command: "pnpm exec vite --config vite.scenarios.config.ts --host 127.0.0.1 --port 4193",
url: "http://127.0.0.1:4193/scenario-explorer/",
reuseExistingServer: false,
timeout: 60_000,
},
@@ -1,16 +1,16 @@
import { expect, test, type Page } from "@playwright/test";
/**
* Phase 7I browser acceptance.
* Scenario chat browser acceptance.
*
* The chat is where the board actually uses the mock control plane, so these
* cover the paths a person takes: send a prompt, get a denial, follow a turn to
* its evidence, reset, and do it again on a phone. Screen captures for the §11
* matrix are recorded by `scripts/record-phase7i-evidence.mjs`, which reuses
* matrix are recorded by `scripts/record-scenario-chat-evidence.mjs`, which reuses
* these routes.
*/
const BASE = "/phase7-explorer/";
const BASE = "/scenario-explorer/";
const MOBILE = { width: 390, height: 844 };
async function open(page: Page, hash: string, state = "settled"): Promise<void> {
@@ -1,15 +1,15 @@
import { expect, test, type Page } from "@playwright/test";
/**
* Phase 7F browser acceptance.
* Capability browser acceptance.
*
* Covers the interaction map's determinism contract (§7), the keyboard paths
* and landmark structure (§6), and the credential/policy boundary (§5). Screen
* captures for the §9 acceptance matrix are recorded by
* `scripts/record-phase7-evidence.mjs`, which reuses the same routes.
* `scripts/record-capability-evidence.mjs`, which reuses the same routes.
*/
const BASE = "/phase7-explorer/";
const BASE = "/scenario-explorer/";
async function open(page: Page, hash: string): Promise<void> {
await page.goto(`${BASE}${hash}`);
@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Capability scenario explorer — mock control plane</title>
</head>
<body><div id="root"></div><script type="module" src="/scenario-explorer/src/main.tsx"></script></body>
</html>
@@ -2,14 +2,14 @@ import * as React from "react";
import { Tabs, type TabDefinition } from "@paperclipai/paperclip-runner/react";
import {
phase7RunScenario,
type Phase7ChatSessionArtifact,
type Phase7EvalSuiteLookup,
type Phase7ParityStatus,
type Phase7RunArtifact,
type Phase7ScenarioIndex,
type Phase7ScenarioIndexEntry,
} from "@paperclip-runner-local/phase7";
capabilityRunScenario,
type CapabilityChatSessionArtifact,
type CapabilityEvalSuiteLookup,
type CapabilityParityStatus,
type CapabilityRunArtifact,
type CapabilityScenarioIndex,
type CapabilityScenarioIndexEntry,
} from "@paperclip-runner-local/capability";
import {
AuthorizationPanel,
@@ -18,14 +18,14 @@ import {
ParityPanel,
StateDiffPanel,
} from "./components/inspector-panels.js";
import { buildFacets, ScenarioPicker, type Phase7PickerFacets } from "./components/scenario-picker.js";
import { buildFacets, ScenarioPicker, type CapabilityPickerFacets } from "./components/scenario-picker.js";
import { RunHeader } from "./components/run-header.js";
import { ScenarioTranscript } from "./components/scenario-transcript.js";
import { EmptyState, Mono } from "./components/primitives.js";
import {
ChatSurface,
type OpenPhase7ChatSession,
type Phase7ChatState,
type OpenCapabilityChatSession,
type CapabilityChatState,
} from "./components/chat-surface.js";
import {
ActivityEmptyState,
@@ -35,16 +35,16 @@ import {
import { DISPOSITION_LABEL } from "./labels.js";
import {
EMPTY_FILTERS,
parsePhase7Route,
serializePhase7Route,
type Phase7ActivityFilter,
type Phase7Filters,
type Phase7InspectorView,
type Phase7Route,
parseCapabilityRoute,
serializeCapabilityRoute,
type CapabilityActivityFilter,
type CapabilityFilters,
type ScenarioChatnspectorView,
type CapabilityRoute,
} from "./route.js";
/**
* Phase 7 scenario explorer shell.
* Capability scenario explorer shell.
*
* One React tree, three regions: picker (nav), run view (main), inspector
* (complementary). The shell owns route and view state only — never protocol
@@ -52,15 +52,15 @@ import {
*/
export interface ExplorerAppProps {
index: Phase7ScenarioIndex;
evalSuite?: Phase7EvalSuiteLookup;
index: CapabilityScenarioIndex;
evalSuite?: CapabilityEvalSuiteLookup;
/** Injected so tests can drive a settled state without a live relay probe. */
codexAvailable?: boolean;
initialRoute?: Phase7Route;
initialRoute?: CapabilityRoute;
/** Test seam: run scenarios synchronously through an injected runner. */
runScenario?: typeof phase7RunScenario;
runScenario?: typeof capabilityRunScenario;
/** Test seam: open chat sessions through an injected session factory. */
openSession?: OpenPhase7ChatSession;
openSession?: OpenCapabilityChatSession;
}
type RunState = "idle" | "pending" | "settled" | "failed";
@@ -94,22 +94,22 @@ export function ExplorerApp({
evalSuite,
codexAvailable = false,
initialRoute,
runScenario = phase7RunScenario,
runScenario = capabilityRunScenario,
openSession,
}: ExplorerAppProps) {
const [route, setRoute] = React.useState<Phase7Route>(
() => initialRoute ?? parsePhase7Route(typeof window === "undefined" ? "#/" : window.location.hash),
const [route, setRoute] = React.useState<CapabilityRoute>(
() => initialRoute ?? parseCapabilityRoute(typeof window === "undefined" ? "#/" : window.location.hash),
);
const [artifacts, setArtifacts] = React.useState<Record<string, Phase7RunArtifact>>({});
const [artifacts, setArtifacts] = React.useState<Record<string, CapabilityRunArtifact>>({});
const [runState, setRunState] = React.useState<RunState>("idle");
const [mode, setMode] = React.useState<"fake" | "codex">(route.run ?? "fake");
const [segment, setSegment] = React.useState<Segment>(() => defaultSegment(route));
const [replayPosition, setReplayPosition] = React.useState(0);
const [highlight, setHighlight] = React.useState<number | null>(null);
const [runFocusRequest, setRunFocusRequest] = React.useState(0);
const [chatArtifact, setChatArtifact] = React.useState<Phase7ChatSessionArtifact | null>(null);
const [chatArtifact, setChatArtifact] = React.useState<CapabilityChatSessionArtifact | null>(null);
const [chatRevealedSequence, setChatRevealedSequence] = React.useState(0);
const [chatState, setChatState] = React.useState<Phase7ChatState>("seeding");
const [chatState, setChatState] = React.useState<CapabilityChatState>("seeding");
const [stripFocusRequest, setStripFocusRequest] = React.useState({ turn: 0, nonce: 0 });
const shellRef = React.useRef<HTMLDivElement>(null);
const runHeaderRef = React.useRef<HTMLElement>(null);
@@ -117,16 +117,16 @@ export function ExplorerApp({
const selected = route.caseId === null ? null : findEntry(index, route.caseId);
const artifact = selected === null ? null : (artifacts[selected.id] ?? null);
const navigate = React.useCallback((next: Phase7Route) => {
const navigate = React.useCallback((next: CapabilityRoute) => {
setRoute(next);
if (typeof window !== "undefined") {
window.history.replaceState(null, "", serializePhase7Route(next));
window.history.replaceState(null, "", serializeCapabilityRoute(next));
}
}, []);
React.useEffect(() => {
if (typeof window === "undefined") return undefined;
const onHashChange = (): void => setRoute(parsePhase7Route(window.location.hash));
const onHashChange = (): void => setRoute(parseCapabilityRoute(window.location.hash));
window.addEventListener("hashchange", onHashChange);
return () => window.removeEventListener("hashchange", onHashChange);
}, []);
@@ -175,7 +175,7 @@ export function ExplorerApp({
}, []);
const run = React.useCallback(
async (entry: Phase7ScenarioIndexEntry) => {
async (entry: CapabilityScenarioIndexEntry) => {
setRunState("pending");
try {
const next = await runScenario(entry, { mode: "fake", evalSuite });
@@ -198,7 +198,7 @@ export function ExplorerApp({
}, [artifacts, route.run, run, runState, selected]);
const parityStatus = React.useCallback(
(id: string): Phase7ParityStatus => artifacts[id]?.parity.verdict ?? "not_run",
(id: string): CapabilityParityStatus => artifacts[id]?.parity.verdict ?? "not_run",
[artifacts],
);
@@ -207,10 +207,10 @@ export function ExplorerApp({
[index.entries, parityStatus, route.filters],
);
const facets = React.useMemo<Phase7PickerFacets>(
const facets = React.useMemo<CapabilityPickerFacets>(
() =>
buildFacets(index.entries, (key, value) => {
const probe: Phase7Filters = {
const probe: CapabilityFilters = {
...route.filters,
[key === "parity" ? "parity" : key]: value,
};
@@ -229,7 +229,7 @@ export function ExplorerApp({
? "settled"
: "pending";
const view: Phase7InspectorView = route.view;
const view: ScenarioChatnspectorView = route.view;
const inspectorTab = view === "transcript" ? "context" : view;
function selectCase(id: string): void {
@@ -243,12 +243,12 @@ export function ExplorerApp({
setRunFocusRequest((request) => request + 1);
}
function setView(next: Phase7InspectorView): void {
function setView(next: ScenarioChatnspectorView): void {
navigate({ ...route, view: next });
if (next !== "transcript") setSegment("inspect");
}
function setFilters(patch: Partial<Phase7Filters>): void {
function setFilters(patch: Partial<CapabilityFilters>): void {
// Narrowing the picker must not end a live chat session, so the chat
// surface keeps its case while the explorer still returns to the corpus.
const caseId = route.surface === "chat" ? route.caseId : null;
@@ -459,7 +459,7 @@ export function ExplorerApp({
artifact={chatArtifact}
revealedSequence={chatRevealedSequence}
filter={route.activityFilter}
onFilterChange={(filter: Phase7ActivityFilter) =>
onFilterChange={(filter: CapabilityActivityFilter) =>
navigate({ ...route, activityFilter: filter })
}
openTurn={route.turn}
@@ -485,7 +485,7 @@ export function ExplorerApp({
: tab,
)}
selected={inspectorTab}
onSelect={(value) => setView(value as Phase7InspectorView)}
onSelect={(value) => setView(value as ScenarioChatnspectorView)}
label="Scenario inspector"
>
{inspectorTab === "context" ? (
@@ -547,7 +547,7 @@ function IntroCard({
chat,
onSelect,
}: {
index: Phase7ScenarioIndex;
index: CapabilityScenarioIndex;
chat: boolean;
onSelect: (id: string) => void;
}) {
@@ -557,7 +557,7 @@ function IntroCard({
return (
<section className="pcr7-intro" data-testid="explorer-intro">
<p className="pcr-eyebrow">
Phase 7 scenario {chat ? "chat" : "explorer"} — mock control plane
Capability scenario {chat ? "chat" : "explorer"} — mock control plane
</p>
<h1>{index.entries.length} scenarios across {index.groups.length} eval groups</h1>
<p>
@@ -587,7 +587,7 @@ function IntroCard({
* The inspector before a scenario is picked. Corpus stats keep the panel
* useful instead of leaving a dead column (UX map §6, "no dead panels").
*/
function CorpusSummary({ index }: { index: Phase7ScenarioIndex }) {
function CorpusSummary({ index }: { index: CapabilityScenarioIndex }) {
const byDisposition = (["control_plane_owned", "always_agent_tool", "optional_agent_tool"] as const).map(
(disposition) => ({
disposition,
@@ -630,19 +630,19 @@ function CorpusSummary({ index }: { index: Phase7ScenarioIndex }) {
* link that says "activity" has to move the segment too, or the route claims
* one thing while the screen shows another.
*/
function defaultSegment(route: Phase7Route): Segment {
function defaultSegment(route: CapabilityRoute): Segment {
if (route.caseId === null) return "scenarios";
if (route.surface === "chat") return route.chatView === "activity" ? "activity" : "chat";
return route.view === "transcript" ? "run" : "inspect";
}
function findEntry(index: Phase7ScenarioIndex, id: string): Phase7ScenarioIndexEntry | null {
function findEntry(index: CapabilityScenarioIndex, id: string): CapabilityScenarioIndexEntry | null {
return index.entries.find((entry) => entry.id === id) ?? null;
}
export function matches(
entry: Phase7ScenarioIndexEntry,
filters: Phase7Filters,
entry: CapabilityScenarioIndexEntry,
filters: CapabilityFilters,
parityStatus: (id: string) => string,
): boolean {
if (filters.group !== null && filters.group.length > 0 && entry.group !== filters.group) return false;
Loaded 100 of 574 files, more files were not shown because too many files have changed in this diff. Show more