mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
refactor(runner): organize package by responsibility
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
ef2d8c6e03
commit
59e57017b1
574 files changed
+16232
-28863
No files matched your search
+2
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 & 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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+9
-9
@@ -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)
|
||||
+23
-23
@@ -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.
|
||||
+4
-4
@@ -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
|
||||
|
||||
+13
-13
@@ -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)
|
||||
+16
-16
@@ -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)
|
||||
+6
-8
@@ -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.
|
||||
+16
-16
@@ -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)
|
||||
+24
-24
@@ -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.
|
||||
+11
-11
@@ -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
|
||||
+12
-12
@@ -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)
|
||||
+16
-17
@@ -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)
|
||||
+9
-9
@@ -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`.
|
||||
+12
-12
@@ -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)
|
||||
+7
-7
@@ -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
|
||||
+9
-9
@@ -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.
|
||||
|
||||
+32
-33
@@ -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.**
|
||||
|
||||
+14
-16
@@ -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/).
|
||||
+5
-7
@@ -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
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 153 KiB |
BIN
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 & 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 & 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).
|
||||
+20
-21
@@ -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`,
|
||||
+7
-7
@@ -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.
|
||||
+42
-50
@@ -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?
|
||||
+5
-5
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
+18
-19
@@ -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).
|
||||
+13
-14
@@ -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).
|
||||
+10
-10
@@ -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.
|
||||
+13
-13
@@ -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).
|
||||
+8
-8
@@ -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).
|
||||
+10
-11
@@ -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).
|
||||
+9
-9
@@ -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.
|
||||
+24
-25
@@ -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).
|
||||
+33
-36
@@ -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).
|
||||
+22
-25
@@ -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)
|
||||
+9
-12
@@ -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:
|
||||
+17
-21
@@ -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.
|
||||
+16
-17
@@ -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.
|
||||
+10
-10
@@ -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)
|
||||
+18
-21
@@ -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.
|
||||
+8
-8
@@ -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.
|
||||
+13
-13
@@ -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).
|
||||
+7
-7
@@ -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
|
||||
+9
-9
@@ -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.
|
||||
|
||||
+3
-3
@@ -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,
|
||||
},
|
||||
+3
-3
@@ -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> {
|
||||
+3
-3
@@ -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>
|
||||
+48
-48
@@ -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
Reference in new issue
Block a user