From 560e7e48b5f8db2a64237ff6908dff57f23bbabd Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:33:11 -0500 Subject: [PATCH] feat(runner): add SDK and developer tooling (#12608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The runner package already provides the production protocol and execution spine. > - Contributors still need stable SDK surfaces, deterministic test tools, and local inspection tools. > - Those surfaces share generated contracts and must change as one package boundary. > - This pull request adds the package-local SDK, labs, examples, and drift checks. > - The benefit is a reviewable developer platform that does not change application execution selection. ## Linked Issues or Issue Description **Subsystem affected** `packages/paperclip-runner` — runner SDK, conformance tools, and developer tooling. **Problem or motivation** The production runner spine is present, but package consumers cannot build deterministic integrations, inspect sessions, or verify provider-neutral behavior through supported surfaces. **Proposed solution** Add browser, React, standalone, live-session, scenario, conformance, and evaluation surfaces. Add generated contract inventories and package-local verification scripts. Keep production application routing unchanged. **Alternatives considered** We considered splitting each generated catalog, SDK surface, and demo into separate pull requests. Those changes share exports, fixtures, and drift gates. Splitting them would create intermediate package states that do not build. **Roadmap alignment** No overlapping item appears in `ROADMAP.md`. This work extends the runner package that is already on `master`. ## What Changed - Add browser, React, standalone, live-session, and issue-thread SDK surfaces. - Add deterministic mock control-plane, scenario, conformance, replay, and evaluation tools. - Add bounded Codex, OpenCode, and ACPX development transports and fixtures. - Keep deferred managed-provider execution fail-closed. Persisted compatibility data remains readable. - Add generated capability inventories with their source files and drift checks. - Add examples, package documentation, browser checks, and clean-consumer checks. - Preserve the reviewed protocol bounds, replay compatibility aliases, process environment isolation, and semantic redaction limits. - Update the ACPX package patch that the existing workspace patch registry already tracks. - Do not change `pnpm-lock.yaml`, repository workflows, server runtime selection, or the application UI. ## Verification GitHub Actions is the verification authority for this pull request. The repository CI, package TypeScript and Rust checks, package tests, generated-output drift checks, browser checks, security scans, and Greptile review must pass on the exact head. Local test suites were not run because this series uses parallel GitHub Actions for verification. ## Risks This is a large greenfield package change. The main risks are public export drift, generated-output drift, and optional React consumer compatibility. Package boundary checks, clean-consumer checks, and browser tests cover those risks. Production adapter selection and server execution are outside this pull request. ## Stack 1. **This PR:** runner SDK and developer tooling. 2. [Codex production server integration](https://github.com/paperclipai/paperclip/pull/12616). 3. [Provider-neutral task-thread UI](https://github.com/paperclipai/paperclip/pull/12617). ## Model Used OpenAI Codex, GPT-5, high-reasoning mode, with tool use and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have described the issue in-PR following the feature request template - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --- .gitattributes | 1 + .gitignore | 1 + packages/paperclip-runner/.gitignore | 12 + packages/paperclip-runner/README.md | 246 +- .../devtools/browser/index.html | 13 + .../devtools/browser/live-console.spec.ts | 398 ++ .../devtools/browser/playwright.config.ts | 29 + .../devtools/browser/src/App.tsx | 642 +++ .../browser/src/components/ui/badge.tsx | 19 + .../browser/src/components/ui/banner.tsx | 40 + .../browser/src/components/ui/button.tsx | 15 + .../browser/src/components/ui/card.tsx | 26 + .../browser/src/components/ui/composer.tsx | 133 + .../src/components/ui/conversation.tsx | 95 + .../browser/src/components/ui/dialog.tsx | 61 + .../browser/src/components/ui/menu.tsx | 138 + .../browser/src/components/ui/message.tsx | 60 + .../src/components/ui/reasoning-item.tsx | 33 + .../browser/src/components/ui/tabs.tsx | 94 + .../browser/src/components/ui/textarea.tsx | 15 + .../browser/src/components/ui/tool-item.tsx | 80 + .../browser/src/components/ui/tooltip.tsx | 25 + .../devtools/browser/src/live/Inspector.tsx | 229 + .../devtools/browser/src/live/LiveConsole.tsx | 487 +++ .../devtools/browser/src/live/RequestCard.tsx | 168 + .../browser/src/live/SessionPanel.tsx | 346 ++ .../devtools/browser/src/live/client.ts | 165 + .../devtools/browser/src/live/protocol.ts | 115 + .../browser/src/live/transcript-model.test.ts | 293 ++ .../browser/src/live/transcript-model.ts | 402 ++ .../browser/src/live/use-live-console.test.ts | 25 + .../browser/src/live/use-live-console.ts | 579 +++ .../devtools/browser/src/main.tsx | 16 + .../devtools/browser/src/styles.css | 1430 ++++++ .../devtools/browser/static-replay.spec.ts | 112 + .../devtools/issue-thread/index.html | 13 + .../issue-thread/issue-thread.spec.ts | 1234 ++++++ .../issue-thread/playwright.config.ts | 46 + .../devtools/issue-thread/src/App.tsx | 1589 +++++++ .../devtools/issue-thread/src/Composer.tsx | 161 + .../issue-thread/src/DevtoolsInspector.tsx | 646 +++ .../issue-thread/src/EvidencePanel.test.ts | 56 + .../issue-thread/src/EvidencePanel.tsx | 622 +++ .../devtools/issue-thread/src/Icons.tsx | 32 + .../issue-thread/src/InteractionCard.tsx | 528 +++ .../devtools/issue-thread/src/IssueHeader.tsx | 213 + .../devtools/issue-thread/src/SurfaceNav.tsx | 57 + .../devtools/issue-thread/src/ThreadItems.tsx | 616 +++ .../devtools/issue-thread/src/fake-store.ts | 82 + .../devtools/issue-thread/src/fonts/NOTICE.md | 78 + .../fonts/dejavu-sans-mono-latin-400.woff2 | Bin 0 -> 46872 bytes .../fonts/dejavu-sans-mono-latin-700.woff2 | Bin 0 -> 46120 bytes .../src/fonts/dejavu-sans-symbols.woff2 | Bin 0 -> 6604 bytes .../issue-thread/src/fonts/inter-latin.woff2 | Bin 0 -> 188804 bytes .../fonts/noto-sans-symbols2-hourglass.woff2 | Bin 0 -> 1208 bytes .../issue-thread/src/issue-thread.css | 2546 +++++++++++ .../devtools/issue-thread/src/live-client.ts | 260 ++ .../devtools/issue-thread/src/main.tsx | 24 + .../devtools/issue-thread/src/primitives.tsx | 91 + .../devtools/issue-thread/src/route.test.ts | 75 + .../devtools/issue-thread/src/route.ts | 98 + .../issue-thread/stream-fixture-transport.mjs | 166 + .../paperclip-runner/docs/adding-a-harness.md | 87 + ...-runner-testing-eval-package-boundaries.md | 119 + .../paperclip-runner/docs/architecture.md | 298 ++ .../capability-authorization-and-exposure.md | 98 + .../docs/capability-clean-room-chat.md | 170 + .../docs/capability-contract.md | 244 ++ .../docs/capability-disposition.md | 84 + .../docs/capability-eval-conformance.md | 90 + .../docs/capability-execution-modes.md | 87 + .../capability-future-binding-boundary.md | 51 + .../docs/capability-issue-thread-ui.md | 224 + .../docs/capability-live-runnerd-codex.md | 122 + .../capability-mock-control-plane-port.md | 94 + .../docs/capability-scenario-explorer.md | 84 + .../docs/capability-semantic-catalog.md | 79 + .../docs/capability-semantic-tools.md | 98 + .../docs/capability-verification-commands.md | 50 + .../paperclip-runner/docs/codex-driver.md | 168 + .../capability-issue-thread-ux-contract.md | 429 ++ .../design/capability-scenario-explorer-ux.md | 549 +++ .../live-console-component-decisions.md | 143 + .../design/live-console-interaction-map.md | 379 ++ .../docs/design/scenario-chat-ux.md | 523 +++ .../docs/design/sdk-component-decisions.md | 106 + .../standalone-thin-paperclip-adapter.md | 1178 +++++ .../paperclip-runner/docs/durable-recovery.md | 225 + packages/paperclip-runner/docs/index.md | 44 + .../docs/live-console-protocol-server.md | 135 + .../paperclip-runner/docs/live-console.md | 142 + .../paperclip-runner/docs/local-runner.md | 145 + .../docs/protocol-compatibility.md | 252 ++ .../2026-08-07-ui-library-compatibility.md | 41 + .../paperclip-runner/docs/scenario-chat.md | 105 + packages/paperclip-runner/docs/sdk.md | 171 + .../docs/standalone-thin-paperclip-adapter.md | 57 + .../tutorials/capability-clean-room-chat.md | 165 + .../docs/tutorials/capability-issue-thread.md | 222 + .../tutorials/capability-scenario-explorer.md | 179 + .../paperclip-runner/docs/tutorials/codex.md | 181 + .../conformance-standalone-tracer.md | 102 + .../docs/tutorials/end-to-end.md | 168 + .../tutorials/live-console-protocol-server.md | 110 + .../docs/tutorials/live-console.md | 203 + .../docs/tutorials/local-runner.md | 142 + .../paperclip-runner/docs/tutorials/replay.md | 115 + .../docs/tutorials/scenario-chat.md | 129 + .../docs/tutorials/sdk-console.md | 149 + .../standalone-thin-paperclip-adapter.md | 74 + .../examples/mini-consumer/index.html | 9 + .../examples/mini-consumer/src/main.tsx | 97 + .../examples/mini-consumer/src/mini.css | 70 + .../examples/playwright.config.ts | 24 + .../examples/playwright.real.config.ts | 29 + .../examples/playwright.scenarios.config.ts | 20 + .../examples/reference-console/index.html | 9 + .../examples/reference-console/src/main.tsx | 9 + .../examples/scenario-chat.spec.ts | 277 ++ .../examples/scenario-explorer.spec.ts | 313 ++ .../examples/scenario-explorer/index.html | 9 + .../examples/scenario-explorer/src/app.tsx | 676 +++ .../scenario-explorer/src/chat.test.tsx | 375 ++ .../src/components/chat-activity-stream.tsx | 443 ++ .../src/components/chat-conversation.tsx | 127 + .../src/components/chat-session-header.tsx | 189 + .../src/components/chat-surface.tsx | 366 ++ .../src/components/chat-turn-strip.tsx | 83 + .../src/components/inspector-panels.tsx | 451 ++ .../src/components/primitives.tsx | 93 + .../src/components/run-header.tsx | 142 + .../src/components/scenario-picker.tsx | 345 ++ .../src/components/scenario-transcript.tsx | 240 + .../examples/scenario-explorer/src/env.d.ts | 18 + .../scenario-explorer/src/explorer.css | 904 ++++ .../scenario-explorer/src/explorer.test.tsx | 417 ++ .../examples/scenario-explorer/src/labels.ts | 124 + .../examples/scenario-explorer/src/main.tsx | 35 + .../src/remote-chat-session.ts | 107 + .../scenario-explorer/src/route.test.ts | 80 + .../examples/scenario-explorer/src/route.ts | 179 + .../examples/sdk-real.spec.ts | 91 + .../paperclip-runner/examples/sdk.spec.ts | 265 ++ .../examples/standalone-demo/index.html | 9 + .../examples/standalone-demo/src/main.tsx | 66 + .../examples/standalone-demo/src/styles.css | 28 + .../generated/capability/capabilities.yaml | 1382 ++++++ .../capability/capability-contract.md | 10 + .../capability/downstream-handoff.md | 20 + .../capability/eval-traceability.yaml | 1935 +++++++++ .../generated/capability/mcp-tool-map.yaml | 383 ++ .../capability/semantic-tool-contracts.json | 1 + packages/paperclip-runner/package.json | 122 +- .../replay/budget-cost-stop-reason.json | 55 + .../budget-cost-stop-reason.snapshot.json | 127 + .../budget-cost-stop-reason.summary.json | 9 + .../paperclip-runner/protocol/manifest.json | 23 + .../schemas/native-execution.schema.json | 188 + .../src/bin/fake-codex-app-server.rs | 187 +- .../crates/runner-core/src/codex_provider.rs | 379 +- .../crates/runner-core/src/durable/runner.rs | 103 +- .../runner-core/src/provider_backend.rs | 67 +- .../runner-core/tests/codex_provider.rs | 137 +- .../scripts/build-provider-pack.mjs | 300 ++ .../scripts/capability-clean-room-smoke.mjs | 209 + .../scripts/capability-cookie-jar.mjs | 41 + .../scripts/capability-explorer-fixtures.mjs | 48 + .../capability-issue-thread-server.mjs | 979 +++++ .../scripts/capability-issue-thread-smoke.mjs | 166 + .../scripts/check-browser-tokens.mjs | 130 + .../scripts/check-capability-csp-bundle.mjs | 27 + .../scripts/check-capability-inventory.mjs | 58 + .../check-capability-inventory.test.mjs | 95 + .../scripts/check-clean-consumers.mjs | 386 ++ .../scripts/check-forbidden-imports.mjs | 16 + .../scripts/check-forbidden-imports.test.mjs | 61 + .../scripts/check-numbered-milestones.mjs | 16 + .../check-numbered-milestones.test.mjs | 30 + .../scripts/check-package-boundaries.mjs | 38 + .../scripts/check-tracked-imports.mjs | 20 + .../scripts/check-tracked-imports.test.mjs | 73 + .../scripts/generate-capability-contract.mjs | 214 + .../scripts/generate-capability-inventory.mjs | 62 + .../scripts/generate-operation-groups.ts | 28 + .../scripts/generate-protocol-coverage.mjs | 98 + .../generate-protocol-schema-module.mjs | 90 +- .../scripts/generate-replay-goldens.mjs | 17 +- .../scripts/generate-semantic-contracts.mjs | 19 + .../scripts/lib/capability-inventory.mjs | 371 ++ .../scripts/lib/codex-evidence.mjs | 80 + .../scripts/lib/doc-wrappers.mjs | 21 + .../scripts/lib/forbidden-imports.mjs | 251 ++ .../scripts/lib/numbered-milestones.mjs | 87 + .../scripts/lib/tracked-imports.mjs | 214 + .../scripts/live-console-browser-server.mjs | 121 + .../live-console-browser-server.test.mjs | 54 + .../scripts/live-console-demo-server.mjs | 52 + .../scripts/local-runner-browser-server.mjs | 410 ++ .../local-runner-browser-server.test.mjs | 244 ++ .../scripts/run-capability-eval-suite.mjs | 8 + .../scripts/run-scenario-chat-demo.sh | 88 + .../scripts/validate-doc-links.mjs | 114 + .../scripts/validate-doc-links.test.mjs | 45 + .../scripts/verify-rootless-linux.sh | 86 + .../spec/capability/capabilities.yaml | 2297 ++++++++++ .../spec/capability/catalog-reconciliation.md | 152 + .../spec/capability/contract-schema.json | 17 + .../spec/capability/eval-traceability.yaml | 3851 +++++++++++++++++ .../spec/capability/inventory.schema.json | 74 + .../spec/capability/mcp-tool-map.yaml | 432 ++ .../spec/capability/protocol-coverage.json | 3597 +++++++++++++++ .../spec/capability/source-contract.json | 94 + .../spec/fixtures/status-authority-sdk.json | 475 ++ .../spec/operation-groups/source.json | 358 ++ .../spec/paperclip-agent-operation-groups.md | 511 +++ .../paperclip-runner-status-attention-ux.md | 195 + .../spec/prp-v1-expressiveness-audit.md | 111 + .../spec/prp-v1-expressiveness-crosswalk.json | 104 + ...rp-v1-expressiveness-crosswalk.schema.json | 89 + .../src/browser/client.test.ts | 79 + .../paperclip-runner/src/browser/client.ts | 218 + .../paperclip-runner/src/browser/index.ts | 5 + .../paperclip-runner/src/browser/protocol.ts | 115 + .../src/browser/transcript-model.test.ts | 313 ++ .../src/browser/transcript-model.ts | 448 ++ .../src/catalog/catalog-docs.test.ts | 60 + .../paperclip-runner/src/catalog/index.ts | 1 + .../src/catalog/operation-groups-doc.test.ts | 86 + .../src/catalog/operation-groups-doc.ts | 626 +++ .../src/catalog/operation-groups-inputs.ts | 91 + .../src/catalog/reconciliation.test.ts | 177 + .../src/catalog/reconciliation.ts | 187 + .../src/cli/codex-app-server-unix-proxy.ts | 147 + packages/paperclip-runner/src/cli/codex.ts | 131 + .../paperclip-runner/src/cli/live-runner.ts | 115 + .../paperclip-runner/src/cli/local-runner.ts | 50 + .../src/cli/paperclip-adapter.ts | 15 + packages/paperclip-runner/src/cli/replay.ts | 17 + .../src/compatibility.test.ts | 72 + .../paperclip-runner/src/compatibility.ts | 154 + .../conformance/capability-eval-suite.test.ts | 27 + .../src/conformance/capability-eval-suite.ts | 503 +++ .../capability-semantic-conformance.test.ts | 58 + .../capability-semantic-conformance.ts | 455 ++ .../conformance/semantic-conformance.test.ts | 124 +- .../src/conformance/semantic-conformance.ts | 245 +- .../durable-prp-control-plane.ts | 346 +- .../src/devtools/index.test.ts | 109 + .../paperclip-runner/src/devtools/index.ts | 218 + .../src/drivers/acpx/codex-acpx-driver.ts | 78 +- .../codex/codex-app-server-driver.test.ts | 3760 ++++++++++++++++ .../src/drivers/runner-tool-bridge.ts | 6 +- .../drivers/runtime-context-materializer.ts | 77 +- .../src/generated/capability-contract.ts | 46 + packages/paperclip-runner/src/index.ts | 32 +- .../src/issue-thread/fixtures.ts | 1215 ++++++ .../src/issue-thread/index.ts | 4 + .../src/issue-thread/issue-thread.test.ts | 538 +++ .../src/issue-thread/live-projection.ts | 957 ++++ .../src/issue-thread/public-view.test.ts | 451 ++ .../src/issue-thread/public-view.ts | 742 ++++ .../src/issue-thread/types.ts | 501 +++ .../src/live/clean-room-server.test.ts | 1244 ++++++ .../src/live/clean-room.test.ts | 140 + .../paperclip-runner/src/live/clean-room.ts | 226 + .../src/live/durable-live-session-store.ts | 174 + .../src/live/evidence-redaction.ts | 522 +++ packages/paperclip-runner/src/live/index.ts | 6 + .../src/live/live-session.test.ts | 1123 +++++ .../paperclip-runner/src/live/live-session.ts | 2422 +++++++++++ .../src/live/runnerd-codex-transport.test.ts | 1272 ++++++ .../src/live/runnerd-codex-transport.ts | 2432 +++++++++++ .../live/scenario-chat-demo-server.test.ts | 525 +++ .../src/live/scenario-chat-demo-server.ts | 708 +++ .../src/live/scenario-chat-loopback-proxy.ts | 64 + .../live/scenario-chat-proxy-config.test.ts | 65 + .../src/live/scenario-chat-proxy-config.ts | 75 + .../src/live/tailnet-gateway.test.ts | 69 + .../src/live/tailnet-gateway.ts | 384 ++ .../src/live/turn-stream.test.ts | 436 ++ .../paperclip-runner/src/live/turn-stream.ts | 291 ++ .../src/live/workspace-diff.test.ts | 46 + .../src/live/workspace-diff.ts | 219 + .../mock-core/live-console-demo-manifests.ts | 491 +++ .../live-console-demo-server.test.ts | 927 ++++ .../src/mock-core/live-console-demo-server.ts | 913 ++++ .../live-console-scripted-driver.test.ts | 387 ++ .../mock-core/live-console-scripted-driver.ts | 770 ++++ .../src/mock-core/local-runner.test.ts | 182 + .../src/mock-core/local-runner.ts | 577 +++ .../generated/standalone-validators.ts | 7 + .../src/protocol/provider-descriptor.test.ts | 30 + .../prp-v1-expressiveness-crosswalk.test.ts | 76 + .../src/protocol/replay-contract.test.ts | 65 +- .../src/protocol/replay-contract.ts | 79 +- .../src/protocol/result-normalization.test.ts | 198 +- .../src/protocol/result-normalization.ts | 214 +- .../protocol/workspace-diff-events.test.ts | 85 + .../src/react/components/badge.tsx | 19 + .../src/react/components/banner.tsx | 39 + .../src/react/components/button.tsx | 18 + .../src/react/components/card.tsx | 26 + .../src/react/components/composer.tsx | 95 + .../src/react/components/conversation.tsx | 196 + .../src/react/components/dialog.tsx | 68 + .../src/react/components/menu.tsx | 146 + .../src/react/components/message.tsx | 223 + .../src/react/components/reasoning-item.tsx | 32 + .../src/react/components/tabs.tsx | 94 + .../src/react/components/textarea.tsx | 15 + .../src/react/components/tool-item.tsx | 92 + .../src/react/components/tooltip.tsx | 25 + .../src/react/connection-banner.tsx | 39 + packages/paperclip-runner/src/react/index.ts | 32 + .../paperclip-runner/src/react/inspector.tsx | 232 + .../src/react/public-contract.test.tsx | 150 + .../src/react/replay-controls.tsx | 38 + .../src/react/request-card.tsx | 174 + .../src/react/runner-console-app.tsx | 262 ++ .../src/react/session-timeline.tsx | 375 ++ .../src/react/use-runner-console.ts | 594 +++ .../src/reducer/session-reducer.test.ts | 49 +- .../src/scenarios/chat-script.ts | 207 + .../src/scenarios/chat-session.test.ts | 218 + .../src/scenarios/chat-session.ts | 406 ++ .../src/scenarios/fixture-run-capabilities.ts | 17 + .../paperclip-runner/src/scenarios/index.ts | 49 + .../src/scenarios/scenario-execution.ts | 564 +++ .../src/scenarios/scenario-explorer-types.ts | 337 ++ .../src/scenarios/scenario-explorer.test.ts | 313 ++ .../src/scenarios/scenario-fixtures.ts | 357 ++ .../src/scenarios/scenario-index.ts | 310 ++ .../src/scenarios/scenario-parity.ts | 360 ++ .../src/scenarios/scenario-plan.ts | 758 ++++ .../src/scenarios/scenario-runner.ts | 184 + .../src/scenarios/state-diff.ts | 132 + .../src/semantic-tools/catalog.ts | 15 + .../src/semantic-tools/discovery.ts | 267 +- .../src/semantic-tools/dispatcher.ts | 1325 +++--- .../src/semantic-tools/index.ts | 5 + .../src/semantic-tools/paperclip-discovery.ts | 179 + .../semantic-tools/paperclip-dispatcher.ts | 897 ++++ .../paperclip-semantic-tools.test.ts | 594 +++ .../src/semantic-tools/policy.ts | 153 + .../src/semantic-tools/provider-neutral.ts | 69 + .../src/semantic-tools/redaction.ts | 85 +- .../src/semantic-tools/semantic-tools.test.ts | 848 ++-- .../src/semantic-tools/types.ts | 184 + .../paperclip-runner/src/standalone/index.ts | 1 + .../src/standalone/standalone-demo.test.ts | 117 + .../src/standalone/standalone-demo.ts | 198 + packages/paperclip-runner/src/testing.ts | 8 +- .../src/tracer/conformance.ts | 4 + .../src/tracer/local-runner-live.ts | 77 + .../src/tracer/replay.test.ts | 13 +- .../paperclip-runner/src/tracer/replay.ts | 26 +- packages/paperclip-runner/styles.css | 1414 ++++++ .../forbidden-cargo-path/Cargo.toml | 7 + .../forbidden-import/core-import.ts | 3 + .../examples/mini-consumer/deep-import.ts | 3 + .../devtools/browser/adapted-component.tsx | 4 + .../untracked-import/consumer.ts | 5 + .../test-fixtures/untracked-import/module.ts | 1 + .../test/acpx-codex-package-contract.test.mjs | 2 + .../test/capability-contract.test.mjs | 41 + .../fake-durable-codex-app-server.mjs | 194 + .../fake-native-resume-codex-app-server.mjs | 277 ++ .../test/protocol-contract.test.mjs | 2 +- .../paperclip-runner/tsconfig.browser.json | 49 + packages/paperclip-runner/tsconfig.json | 10 +- .../paperclip-runner/tsconfig.surfaces.json | 19 + packages/paperclip-runner/vite.config.ts | 36 + .../vite.issue-thread-stream.config.ts | 39 + .../vite.issue-thread.config.ts | 26 + .../paperclip-runner/vite.scenarios.config.ts | 49 + packages/paperclip-runner/vite.sdk.config.ts | 34 + .../vite.standalone.config.ts | 30 + packages/paperclip-runner/vitest.config.ts | 23 +- patches/acpx@0.13.1.patch | 184 +- scripts/acpx-patch-packaging.test.mjs | 32 +- .../issue-comment-cancel-routes.test.ts | 6 + 381 files changed, 97707 insertions(+), 2393 deletions(-) create mode 100644 .gitattributes create mode 100644 packages/paperclip-runner/devtools/browser/index.html create mode 100644 packages/paperclip-runner/devtools/browser/live-console.spec.ts create mode 100644 packages/paperclip-runner/devtools/browser/playwright.config.ts create mode 100644 packages/paperclip-runner/devtools/browser/src/App.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/badge.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/banner.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/button.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/card.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/composer.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/conversation.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/dialog.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/menu.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/message.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/reasoning-item.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/tabs.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/textarea.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/tool-item.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/components/ui/tooltip.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/live/Inspector.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/live/LiveConsole.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/live/RequestCard.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/live/SessionPanel.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/live/client.ts create mode 100644 packages/paperclip-runner/devtools/browser/src/live/protocol.ts create mode 100644 packages/paperclip-runner/devtools/browser/src/live/transcript-model.test.ts create mode 100644 packages/paperclip-runner/devtools/browser/src/live/transcript-model.ts create mode 100644 packages/paperclip-runner/devtools/browser/src/live/use-live-console.test.ts create mode 100644 packages/paperclip-runner/devtools/browser/src/live/use-live-console.ts create mode 100644 packages/paperclip-runner/devtools/browser/src/main.tsx create mode 100644 packages/paperclip-runner/devtools/browser/src/styles.css create mode 100644 packages/paperclip-runner/devtools/browser/static-replay.spec.ts create mode 100644 packages/paperclip-runner/devtools/issue-thread/index.html create mode 100644 packages/paperclip-runner/devtools/issue-thread/issue-thread.spec.ts create mode 100644 packages/paperclip-runner/devtools/issue-thread/playwright.config.ts create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/App.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/Composer.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/DevtoolsInspector.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/EvidencePanel.test.ts create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/EvidencePanel.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/Icons.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/InteractionCard.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/IssueHeader.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/SurfaceNav.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/ThreadItems.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/fake-store.ts create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/fonts/NOTICE.md create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/fonts/dejavu-sans-mono-latin-400.woff2 create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/fonts/dejavu-sans-mono-latin-700.woff2 create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/fonts/dejavu-sans-symbols.woff2 create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/fonts/inter-latin.woff2 create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/fonts/noto-sans-symbols2-hourglass.woff2 create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/issue-thread.css create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/live-client.ts create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/main.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/primitives.tsx create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/route.test.ts create mode 100644 packages/paperclip-runner/devtools/issue-thread/src/route.ts create mode 100644 packages/paperclip-runner/devtools/issue-thread/stream-fixture-transport.mjs create mode 100644 packages/paperclip-runner/docs/adding-a-harness.md create mode 100644 packages/paperclip-runner/docs/adr/0001-runner-testing-eval-package-boundaries.md create mode 100644 packages/paperclip-runner/docs/architecture.md create mode 100644 packages/paperclip-runner/docs/capability-authorization-and-exposure.md create mode 100644 packages/paperclip-runner/docs/capability-clean-room-chat.md create mode 100644 packages/paperclip-runner/docs/capability-contract.md create mode 100644 packages/paperclip-runner/docs/capability-disposition.md create mode 100644 packages/paperclip-runner/docs/capability-eval-conformance.md create mode 100644 packages/paperclip-runner/docs/capability-execution-modes.md create mode 100644 packages/paperclip-runner/docs/capability-future-binding-boundary.md create mode 100644 packages/paperclip-runner/docs/capability-issue-thread-ui.md create mode 100644 packages/paperclip-runner/docs/capability-live-runnerd-codex.md create mode 100644 packages/paperclip-runner/docs/capability-mock-control-plane-port.md create mode 100644 packages/paperclip-runner/docs/capability-scenario-explorer.md create mode 100644 packages/paperclip-runner/docs/capability-semantic-catalog.md create mode 100644 packages/paperclip-runner/docs/capability-semantic-tools.md create mode 100644 packages/paperclip-runner/docs/capability-verification-commands.md create mode 100644 packages/paperclip-runner/docs/codex-driver.md create mode 100644 packages/paperclip-runner/docs/design/capability-issue-thread-ux-contract.md create mode 100644 packages/paperclip-runner/docs/design/capability-scenario-explorer-ux.md create mode 100644 packages/paperclip-runner/docs/design/live-console-component-decisions.md create mode 100644 packages/paperclip-runner/docs/design/live-console-interaction-map.md create mode 100644 packages/paperclip-runner/docs/design/scenario-chat-ux.md create mode 100644 packages/paperclip-runner/docs/design/sdk-component-decisions.md create mode 100644 packages/paperclip-runner/docs/design/standalone-thin-paperclip-adapter.md create mode 100644 packages/paperclip-runner/docs/durable-recovery.md create mode 100644 packages/paperclip-runner/docs/index.md create mode 100644 packages/paperclip-runner/docs/live-console-protocol-server.md create mode 100644 packages/paperclip-runner/docs/live-console.md create mode 100644 packages/paperclip-runner/docs/local-runner.md create mode 100644 packages/paperclip-runner/docs/protocol-compatibility.md create mode 100644 packages/paperclip-runner/docs/research/2026-08-07-ui-library-compatibility.md create mode 100644 packages/paperclip-runner/docs/scenario-chat.md create mode 100644 packages/paperclip-runner/docs/sdk.md create mode 100644 packages/paperclip-runner/docs/standalone-thin-paperclip-adapter.md create mode 100644 packages/paperclip-runner/docs/tutorials/capability-clean-room-chat.md create mode 100644 packages/paperclip-runner/docs/tutorials/capability-issue-thread.md create mode 100644 packages/paperclip-runner/docs/tutorials/capability-scenario-explorer.md create mode 100644 packages/paperclip-runner/docs/tutorials/codex.md create mode 100644 packages/paperclip-runner/docs/tutorials/conformance-standalone-tracer.md create mode 100644 packages/paperclip-runner/docs/tutorials/end-to-end.md create mode 100644 packages/paperclip-runner/docs/tutorials/live-console-protocol-server.md create mode 100644 packages/paperclip-runner/docs/tutorials/live-console.md create mode 100644 packages/paperclip-runner/docs/tutorials/local-runner.md create mode 100644 packages/paperclip-runner/docs/tutorials/replay.md create mode 100644 packages/paperclip-runner/docs/tutorials/scenario-chat.md create mode 100644 packages/paperclip-runner/docs/tutorials/sdk-console.md create mode 100644 packages/paperclip-runner/docs/tutorials/standalone-thin-paperclip-adapter.md create mode 100644 packages/paperclip-runner/examples/mini-consumer/index.html create mode 100644 packages/paperclip-runner/examples/mini-consumer/src/main.tsx create mode 100644 packages/paperclip-runner/examples/mini-consumer/src/mini.css create mode 100644 packages/paperclip-runner/examples/playwright.config.ts create mode 100644 packages/paperclip-runner/examples/playwright.real.config.ts create mode 100644 packages/paperclip-runner/examples/playwright.scenarios.config.ts create mode 100644 packages/paperclip-runner/examples/reference-console/index.html create mode 100644 packages/paperclip-runner/examples/reference-console/src/main.tsx create mode 100644 packages/paperclip-runner/examples/scenario-chat.spec.ts create mode 100644 packages/paperclip-runner/examples/scenario-explorer.spec.ts create mode 100644 packages/paperclip-runner/examples/scenario-explorer/index.html create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/app.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/chat.test.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/chat-activity-stream.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/chat-conversation.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/chat-session-header.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/chat-surface.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/chat-turn-strip.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/inspector-panels.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/primitives.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/run-header.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/scenario-picker.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/components/scenario-transcript.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/env.d.ts create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/explorer.css create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/explorer.test.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/labels.ts create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/main.tsx create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/remote-chat-session.ts create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/route.test.ts create mode 100644 packages/paperclip-runner/examples/scenario-explorer/src/route.ts create mode 100644 packages/paperclip-runner/examples/sdk-real.spec.ts create mode 100644 packages/paperclip-runner/examples/sdk.spec.ts create mode 100644 packages/paperclip-runner/examples/standalone-demo/index.html create mode 100644 packages/paperclip-runner/examples/standalone-demo/src/main.tsx create mode 100644 packages/paperclip-runner/examples/standalone-demo/src/styles.css create mode 100644 packages/paperclip-runner/generated/capability/capabilities.yaml create mode 100644 packages/paperclip-runner/generated/capability/capability-contract.md create mode 100644 packages/paperclip-runner/generated/capability/downstream-handoff.md create mode 100644 packages/paperclip-runner/generated/capability/eval-traceability.yaml create mode 100644 packages/paperclip-runner/generated/capability/mcp-tool-map.yaml create mode 100644 packages/paperclip-runner/generated/capability/semantic-tool-contracts.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/budget-cost-stop-reason.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/budget-cost-stop-reason.snapshot.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/budget-cost-stop-reason.summary.json create mode 100644 packages/paperclip-runner/protocol/schemas/native-execution.schema.json create mode 100644 packages/paperclip-runner/scripts/build-provider-pack.mjs create mode 100644 packages/paperclip-runner/scripts/capability-clean-room-smoke.mjs create mode 100644 packages/paperclip-runner/scripts/capability-cookie-jar.mjs create mode 100644 packages/paperclip-runner/scripts/capability-explorer-fixtures.mjs create mode 100644 packages/paperclip-runner/scripts/capability-issue-thread-server.mjs create mode 100644 packages/paperclip-runner/scripts/capability-issue-thread-smoke.mjs create mode 100644 packages/paperclip-runner/scripts/check-browser-tokens.mjs create mode 100644 packages/paperclip-runner/scripts/check-capability-csp-bundle.mjs create mode 100644 packages/paperclip-runner/scripts/check-capability-inventory.mjs create mode 100644 packages/paperclip-runner/scripts/check-capability-inventory.test.mjs create mode 100644 packages/paperclip-runner/scripts/check-clean-consumers.mjs create mode 100644 packages/paperclip-runner/scripts/check-forbidden-imports.mjs create mode 100644 packages/paperclip-runner/scripts/check-forbidden-imports.test.mjs create mode 100644 packages/paperclip-runner/scripts/check-numbered-milestones.mjs create mode 100644 packages/paperclip-runner/scripts/check-numbered-milestones.test.mjs create mode 100644 packages/paperclip-runner/scripts/check-package-boundaries.mjs create mode 100644 packages/paperclip-runner/scripts/check-tracked-imports.mjs create mode 100644 packages/paperclip-runner/scripts/check-tracked-imports.test.mjs create mode 100644 packages/paperclip-runner/scripts/generate-capability-contract.mjs create mode 100644 packages/paperclip-runner/scripts/generate-capability-inventory.mjs create mode 100644 packages/paperclip-runner/scripts/generate-operation-groups.ts create mode 100644 packages/paperclip-runner/scripts/generate-protocol-coverage.mjs create mode 100644 packages/paperclip-runner/scripts/generate-semantic-contracts.mjs create mode 100644 packages/paperclip-runner/scripts/lib/capability-inventory.mjs create mode 100644 packages/paperclip-runner/scripts/lib/codex-evidence.mjs create mode 100644 packages/paperclip-runner/scripts/lib/doc-wrappers.mjs create mode 100644 packages/paperclip-runner/scripts/lib/forbidden-imports.mjs create mode 100644 packages/paperclip-runner/scripts/lib/numbered-milestones.mjs create mode 100644 packages/paperclip-runner/scripts/lib/tracked-imports.mjs create mode 100644 packages/paperclip-runner/scripts/live-console-browser-server.mjs create mode 100644 packages/paperclip-runner/scripts/live-console-browser-server.test.mjs create mode 100644 packages/paperclip-runner/scripts/live-console-demo-server.mjs create mode 100644 packages/paperclip-runner/scripts/local-runner-browser-server.mjs create mode 100644 packages/paperclip-runner/scripts/local-runner-browser-server.test.mjs create mode 100644 packages/paperclip-runner/scripts/run-capability-eval-suite.mjs create mode 100755 packages/paperclip-runner/scripts/run-scenario-chat-demo.sh create mode 100644 packages/paperclip-runner/scripts/validate-doc-links.mjs create mode 100644 packages/paperclip-runner/scripts/validate-doc-links.test.mjs create mode 100644 packages/paperclip-runner/scripts/verify-rootless-linux.sh create mode 100644 packages/paperclip-runner/spec/capability/capabilities.yaml create mode 100644 packages/paperclip-runner/spec/capability/catalog-reconciliation.md create mode 100644 packages/paperclip-runner/spec/capability/contract-schema.json create mode 100644 packages/paperclip-runner/spec/capability/eval-traceability.yaml create mode 100644 packages/paperclip-runner/spec/capability/inventory.schema.json create mode 100644 packages/paperclip-runner/spec/capability/mcp-tool-map.yaml create mode 100644 packages/paperclip-runner/spec/capability/protocol-coverage.json create mode 100644 packages/paperclip-runner/spec/capability/source-contract.json create mode 100644 packages/paperclip-runner/spec/fixtures/status-authority-sdk.json create mode 100644 packages/paperclip-runner/spec/operation-groups/source.json create mode 100644 packages/paperclip-runner/spec/paperclip-agent-operation-groups.md create mode 100644 packages/paperclip-runner/spec/paperclip-runner-status-attention-ux.md create mode 100644 packages/paperclip-runner/spec/prp-v1-expressiveness-audit.md create mode 100644 packages/paperclip-runner/spec/prp-v1-expressiveness-crosswalk.json create mode 100644 packages/paperclip-runner/spec/prp-v1-expressiveness-crosswalk.schema.json create mode 100644 packages/paperclip-runner/src/browser/client.test.ts create mode 100644 packages/paperclip-runner/src/browser/client.ts create mode 100644 packages/paperclip-runner/src/browser/index.ts create mode 100644 packages/paperclip-runner/src/browser/protocol.ts create mode 100644 packages/paperclip-runner/src/browser/transcript-model.test.ts create mode 100644 packages/paperclip-runner/src/browser/transcript-model.ts create mode 100644 packages/paperclip-runner/src/catalog/catalog-docs.test.ts create mode 100644 packages/paperclip-runner/src/catalog/operation-groups-doc.test.ts create mode 100644 packages/paperclip-runner/src/catalog/operation-groups-doc.ts create mode 100644 packages/paperclip-runner/src/catalog/operation-groups-inputs.ts create mode 100644 packages/paperclip-runner/src/catalog/reconciliation.test.ts create mode 100644 packages/paperclip-runner/src/catalog/reconciliation.ts create mode 100644 packages/paperclip-runner/src/cli/codex-app-server-unix-proxy.ts create mode 100644 packages/paperclip-runner/src/cli/codex.ts create mode 100644 packages/paperclip-runner/src/cli/live-runner.ts create mode 100644 packages/paperclip-runner/src/cli/local-runner.ts create mode 100644 packages/paperclip-runner/src/cli/paperclip-adapter.ts create mode 100644 packages/paperclip-runner/src/cli/replay.ts create mode 100644 packages/paperclip-runner/src/compatibility.test.ts create mode 100644 packages/paperclip-runner/src/compatibility.ts create mode 100644 packages/paperclip-runner/src/conformance/capability-eval-suite.test.ts create mode 100644 packages/paperclip-runner/src/conformance/capability-eval-suite.ts create mode 100644 packages/paperclip-runner/src/conformance/capability-semantic-conformance.test.ts create mode 100644 packages/paperclip-runner/src/conformance/capability-semantic-conformance.ts create mode 100644 packages/paperclip-runner/src/devtools/index.test.ts create mode 100644 packages/paperclip-runner/src/devtools/index.ts create mode 100644 packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test.ts create mode 100644 packages/paperclip-runner/src/generated/capability-contract.ts create mode 100644 packages/paperclip-runner/src/issue-thread/fixtures.ts create mode 100644 packages/paperclip-runner/src/issue-thread/index.ts create mode 100644 packages/paperclip-runner/src/issue-thread/issue-thread.test.ts create mode 100644 packages/paperclip-runner/src/issue-thread/live-projection.ts create mode 100644 packages/paperclip-runner/src/issue-thread/public-view.test.ts create mode 100644 packages/paperclip-runner/src/issue-thread/public-view.ts create mode 100644 packages/paperclip-runner/src/issue-thread/types.ts create mode 100644 packages/paperclip-runner/src/live/clean-room-server.test.ts create mode 100644 packages/paperclip-runner/src/live/clean-room.test.ts create mode 100644 packages/paperclip-runner/src/live/clean-room.ts create mode 100644 packages/paperclip-runner/src/live/durable-live-session-store.ts create mode 100644 packages/paperclip-runner/src/live/evidence-redaction.ts create mode 100644 packages/paperclip-runner/src/live/index.ts create mode 100644 packages/paperclip-runner/src/live/live-session.test.ts create mode 100644 packages/paperclip-runner/src/live/live-session.ts create mode 100644 packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts create mode 100644 packages/paperclip-runner/src/live/runnerd-codex-transport.ts create mode 100644 packages/paperclip-runner/src/live/scenario-chat-demo-server.test.ts create mode 100644 packages/paperclip-runner/src/live/scenario-chat-demo-server.ts create mode 100644 packages/paperclip-runner/src/live/scenario-chat-loopback-proxy.ts create mode 100644 packages/paperclip-runner/src/live/scenario-chat-proxy-config.test.ts create mode 100644 packages/paperclip-runner/src/live/scenario-chat-proxy-config.ts create mode 100644 packages/paperclip-runner/src/live/tailnet-gateway.test.ts create mode 100644 packages/paperclip-runner/src/live/tailnet-gateway.ts create mode 100644 packages/paperclip-runner/src/live/turn-stream.test.ts create mode 100644 packages/paperclip-runner/src/live/turn-stream.ts create mode 100644 packages/paperclip-runner/src/live/workspace-diff.test.ts create mode 100644 packages/paperclip-runner/src/live/workspace-diff.ts create mode 100644 packages/paperclip-runner/src/mock-core/live-console-demo-manifests.ts create mode 100644 packages/paperclip-runner/src/mock-core/live-console-demo-server.test.ts create mode 100644 packages/paperclip-runner/src/mock-core/live-console-demo-server.ts create mode 100644 packages/paperclip-runner/src/mock-core/live-console-scripted-driver.test.ts create mode 100644 packages/paperclip-runner/src/mock-core/live-console-scripted-driver.ts create mode 100644 packages/paperclip-runner/src/mock-core/local-runner.test.ts create mode 100644 packages/paperclip-runner/src/mock-core/local-runner.ts create mode 100644 packages/paperclip-runner/src/protocol/generated/standalone-validators.ts create mode 100644 packages/paperclip-runner/src/protocol/provider-descriptor.test.ts create mode 100644 packages/paperclip-runner/src/protocol/prp-v1-expressiveness-crosswalk.test.ts create mode 100644 packages/paperclip-runner/src/protocol/workspace-diff-events.test.ts create mode 100644 packages/paperclip-runner/src/react/components/badge.tsx create mode 100644 packages/paperclip-runner/src/react/components/banner.tsx create mode 100644 packages/paperclip-runner/src/react/components/button.tsx create mode 100644 packages/paperclip-runner/src/react/components/card.tsx create mode 100644 packages/paperclip-runner/src/react/components/composer.tsx create mode 100644 packages/paperclip-runner/src/react/components/conversation.tsx create mode 100644 packages/paperclip-runner/src/react/components/dialog.tsx create mode 100644 packages/paperclip-runner/src/react/components/menu.tsx create mode 100644 packages/paperclip-runner/src/react/components/message.tsx create mode 100644 packages/paperclip-runner/src/react/components/reasoning-item.tsx create mode 100644 packages/paperclip-runner/src/react/components/tabs.tsx create mode 100644 packages/paperclip-runner/src/react/components/textarea.tsx create mode 100644 packages/paperclip-runner/src/react/components/tool-item.tsx create mode 100644 packages/paperclip-runner/src/react/components/tooltip.tsx create mode 100644 packages/paperclip-runner/src/react/connection-banner.tsx create mode 100644 packages/paperclip-runner/src/react/index.ts create mode 100644 packages/paperclip-runner/src/react/inspector.tsx create mode 100644 packages/paperclip-runner/src/react/public-contract.test.tsx create mode 100644 packages/paperclip-runner/src/react/replay-controls.tsx create mode 100644 packages/paperclip-runner/src/react/request-card.tsx create mode 100644 packages/paperclip-runner/src/react/runner-console-app.tsx create mode 100644 packages/paperclip-runner/src/react/session-timeline.tsx create mode 100644 packages/paperclip-runner/src/react/use-runner-console.ts create mode 100644 packages/paperclip-runner/src/scenarios/chat-script.ts create mode 100644 packages/paperclip-runner/src/scenarios/chat-session.test.ts create mode 100644 packages/paperclip-runner/src/scenarios/chat-session.ts create mode 100644 packages/paperclip-runner/src/scenarios/fixture-run-capabilities.ts create mode 100644 packages/paperclip-runner/src/scenarios/index.ts create mode 100644 packages/paperclip-runner/src/scenarios/scenario-execution.ts create mode 100644 packages/paperclip-runner/src/scenarios/scenario-explorer-types.ts create mode 100644 packages/paperclip-runner/src/scenarios/scenario-explorer.test.ts create mode 100644 packages/paperclip-runner/src/scenarios/scenario-fixtures.ts create mode 100644 packages/paperclip-runner/src/scenarios/scenario-index.ts create mode 100644 packages/paperclip-runner/src/scenarios/scenario-parity.ts create mode 100644 packages/paperclip-runner/src/scenarios/scenario-plan.ts create mode 100644 packages/paperclip-runner/src/scenarios/scenario-runner.ts create mode 100644 packages/paperclip-runner/src/scenarios/state-diff.ts create mode 100644 packages/paperclip-runner/src/semantic-tools/catalog.ts create mode 100644 packages/paperclip-runner/src/semantic-tools/paperclip-discovery.ts create mode 100644 packages/paperclip-runner/src/semantic-tools/paperclip-dispatcher.ts create mode 100644 packages/paperclip-runner/src/semantic-tools/paperclip-semantic-tools.test.ts create mode 100644 packages/paperclip-runner/src/semantic-tools/policy.ts create mode 100644 packages/paperclip-runner/src/semantic-tools/provider-neutral.ts create mode 100644 packages/paperclip-runner/src/standalone/index.ts create mode 100644 packages/paperclip-runner/src/standalone/standalone-demo.test.ts create mode 100644 packages/paperclip-runner/src/standalone/standalone-demo.ts create mode 100644 packages/paperclip-runner/src/tracer/conformance.ts create mode 100644 packages/paperclip-runner/src/tracer/local-runner-live.ts create mode 100644 packages/paperclip-runner/styles.css create mode 100644 packages/paperclip-runner/test-fixtures/forbidden-cargo-path/Cargo.toml create mode 100644 packages/paperclip-runner/test-fixtures/forbidden-import/core-import.ts create mode 100644 packages/paperclip-runner/test-fixtures/forbidden-sdk-consumer/examples/mini-consumer/deep-import.ts create mode 100644 packages/paperclip-runner/test-fixtures/forbidden-ui-runtime/devtools/browser/adapted-component.tsx create mode 100644 packages/paperclip-runner/test-fixtures/untracked-import/consumer.ts create mode 100644 packages/paperclip-runner/test-fixtures/untracked-import/module.ts create mode 100644 packages/paperclip-runner/test/capability-contract.test.mjs create mode 100644 packages/paperclip-runner/test/fixtures/fake-durable-codex-app-server.mjs create mode 100644 packages/paperclip-runner/test/fixtures/fake-native-resume-codex-app-server.mjs create mode 100644 packages/paperclip-runner/tsconfig.browser.json create mode 100644 packages/paperclip-runner/tsconfig.surfaces.json create mode 100644 packages/paperclip-runner/vite.config.ts create mode 100644 packages/paperclip-runner/vite.issue-thread-stream.config.ts create mode 100644 packages/paperclip-runner/vite.issue-thread.config.ts create mode 100644 packages/paperclip-runner/vite.scenarios.config.ts create mode 100644 packages/paperclip-runner/vite.sdk.config.ts create mode 100644 packages/paperclip-runner/vite.standalone.config.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..b02d5aad4b --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +patches/*.patch whitespace=-space-before-tab diff --git a/.gitignore b/.gitignore index 26ee72daee..5abd1374e1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ **/node_modules **/node_modules/ dist/ +packages/paperclip-runner/runner/target/ ui/storybook-static/ .env *.tsbuildinfo diff --git a/packages/paperclip-runner/.gitignore b/packages/paperclip-runner/.gitignore index 31faa12bf8..a8250662e8 100644 --- a/packages/paperclip-runner/.gitignore +++ b/packages/paperclip-runner/.gitignore @@ -1 +1,13 @@ /runner/target/ +/dist-browser/ +/dist-sdk/ +/playwright-report/ +/test-results/ +/dist-standalone/ +/dist-scenarios/ +/dist-issue-thread/ + +# The repository root ignores scratch `check-*.mjs` files and only re-includes +# them under the root `scripts/` directory. This package's checks are real +# source, so re-include them here instead of relying on `git add -f`. +!scripts/check-*.mjs diff --git a/packages/paperclip-runner/README.md b/packages/paperclip-runner/README.md index e01fb84597..36ba1c9c5d 100644 --- a/packages/paperclip-runner/README.md +++ b/packages/paperclip-runner/README.md @@ -1,72 +1,45 @@ -# Paperclip Runner +# Paperclip Native Runner -This private workspace package contains the staged Paperclip Runner work. +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. -The package currently exposes the language-neutral PRP v1 TypeScript -contract, provider-neutral structured questions and responses, deterministic -fixture validation/replay, structured-result normalization, and the session -reducer oracle. It also contains a package-local Rust runner, scripted fake -harness, bounded process supervisor, cross-language replay oracle, and durable -PRP transport. The transport authenticates and encrypts loopback WebSocket -sessions, persists an ACK-driven outbox and command journal, and reconnects with -a short-lived lease. The Rust runner now includes a Codex-only app-server -provider bridge with durable thread resume, cancellation, structured questions, -and provider-neutral event normalization. The root surface now also exposes an -authenticated durable PRP authority for server-side use. It stores only -bootstrap and reconnect credential digests, validates immutable run identity on -every connection and event, and persists commands and cumulative event ACK -state across server restarts. -The package also publishes the canonical semantic action declarations and -their input and output schemas. Its package-local dispatcher projects only -bound, run-authorized actions and emits redacted semantic receipts. +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. -The first and only provider selected directly by runnerd remains Codex. The -package also contains the qualified OpenCode 1.18.17 proxy and the bounded ACPX -sidecar for Codex and Claude, but this slice does not authorize the server -to select those additional providers for a fresh native run. Dynamic semantic -tools remain undiscoverable unless the hidden server coordinator projects one -of the five same-task read bindings for an already persisted native Codex run. -Catalog membership alone does not grant authority. The server can now create -and start a Codex-backed native run only through the default-off -`paperclip_runner` adapter. See -[`SEMANTIC_ACTIONS.md`](SEMANTIC_ACTIONS.md) for the catalog boundary. +## Public package surfaces -The package has two initial public surfaces: +- `@paperclipai/paperclip-runner` — production contracts, clients/backends, + PRP validation/replay, canonical catalog/dispatcher, and compatibility check. +- `@paperclipai/paperclip-runner/testing` — deterministic mocks plus PRP and + semantic conformance kits. Tests and external conformance consumers import + this explicitly. -- `@paperclipai/paperclip-runner` contains runtime contracts, validation, - replay/reducer logic, the semantic catalog, the authorization dispatcher, and - the Node-only durable server authority. -- `@paperclipai/paperclip-runner/testing` adds Node-only fixture loading and a - provider-neutral semantic conformance kit for deterministic test adapters. +The package root has no mock or scenario exports, and the workspace has no +separate eval-package importer. Provider-backed eval campaigns remain deferred. +See [ADR 0001](docs/adr/0001-runner-testing-eval-package-boundaries.md). -No SDK, browser, React, eval, live-console, or lab entry point is exported. The -package remains private in this wave. The server route -at `/api/runner/v1/connect/:runId` has no authority until the hidden coordinator -registers an exact existing run binding. Fresh native starts are rejected -unless the instance `enableNativeRunner` flag is enabled. Existing direct -adapters keep their original execution path. +The two conformance surfaces intentionally prove different contracts. The +existing `runControlPlanePortConformance` suite checks narrow PRP run/event +persistence. `CAPABILITY_HIGH_RISK_SEMANTIC_VECTORS` and +`runSemanticConformanceKit` compare normalized tool authorization, state, +effects, audit, retries, conflicts, redaction, continuation, and terminal +decisions. The production adapter stays App-owned and invokes Paperclip's real +route/service authorities; it does not copy those rules into this package. -The package build compiles the release `paperclip-runnerd` executable and -stages it under `dist/bin`. The normal server build vendors that directory, so -an installed server does not depend on a separate system Rust installation or -a manually copied binary. `pnpm-lock.yaml` remains under the repository's -existing lockfile process. - -The `paperclip-runner-opencode-proxy` binary adapts the pinned OpenCode server -protocol to the normalized harness contract. It admits only a bounded provider -environment, uses an authenticated loopback MCP endpoint, validates dynamic -tool inputs, keeps terminal completion tools private to the runner boundary, -and preserves exact provider session identity for recovery. The backend factory -requires an explicit runtime directory before constructing this provider. +## Quick start The package also builds `paperclip-runner-acpx-sidecar`. This bounded v2 -stdin/stdout bridge admits the closed Claude and Codex ACPX profiles. It -validates each agent's exact package/model pair, session identity, tool catalog, -structured input, and terminal settlement at the process boundary. Claude runs -without ambient project or local settings. Pi remains unavailable until its -separately spawned runtime can use the same descriptor-confined verified launch -boundary as the ACP server. Runnerd and the server do not select this sidecar -in this slice. +stdin/stdout bridge admits the qualified Codex ACPX profile only. It validates +the exact model, session identity, tool catalog, structured input, and terminal +settlement at the process boundary. Runnerd and the server do not select this +sidecar in this slice. Other ACPX agents remain unavailable. The Rust core includes a bounded client for this sidecar protocol. It enforces request identity, event order, frame and queue limits, timeouts, redacted @@ -103,7 +76,7 @@ unresolved turn-scoped requests. This reducer still does not select ACPX in runnerd. The package-local session bootstrap starts the bounded sidecar transport, -verifies the selected qualified capability handshake and effective model, opens one +verifies the Codex-only capability handshake and effective model, opens one identity-bound session, and confirms its run attachment. Any failed bootstrap terminates the process; session shutdown preserves persistent provider state. The session can then start one immutable-workspace turn, request interruption, @@ -121,8 +94,9 @@ against the exact persisted question IDs, answer modes, options, required answers, custom-answer policy, and text constraints before provider delivery. Tool results and structured question responses then use two-phase resolution: validate retained identity and schema, require the exact sidecar -acknowledgement, and only then clear pending local state. Permission events that -escape the pinned sidecar policy terminate the session fail closed. +acknowledgement, and only then clear pending local state. Codex permission +requests violate its pinned sidecar policy and terminate the session fail +closed. Safe suspension is available only with no active turn or pending request. The sidecar must return the exact persistent session identity before runnerd terminates the local process. @@ -138,37 +112,135 @@ prospective session configuration exactly. Run the complete contract gate with: ```sh -pnpm --filter @paperclipai/paperclip-runner check:protocol +pnpm install --filter @paperclipai/paperclip-runner --lockfile=false --offline --ignore-scripts --dev +pnpm --filter @paperclipai/paperclip-runner verify ``` -Run the Rust runner gate with: +The verification command requires a stable Rust toolchain with `cargo` on +`PATH`, in addition to Node.js 24.11+ and pnpm 9+. + +Minimal Debian/Ubuntu hosts without root access can extract the required +Playwright browser libraries into a user-owned cache and run the same acceptance +sequence with: ```sh -pnpm --filter @paperclipai/paperclip-runner check:runner +pnpm --filter @paperclipai/paperclip-runner verify:rootless ``` -This command checks Rust formatting, builds and tests the minimal workspace in -release mode, verifies bounded process cleanup, launches the real -`paperclip-runnerd` binary through the fake harness, and compares the Rust -conformance and replay summaries with the shared fixtures. The checked-in Cargo -lock and pinned Rust toolchain keep this verification reproducible. +The tracer's final line is stable: -Durability and failure semantics are documented in -[`runner/DURABLE_TRANSPORT.md`](runner/DURABLE_TRANSPORT.md). The fault suite -drops a connection before its event ACK, reconnects with the bound lease, -replays the same event, and proves the duplicated command effect ran once. -Codex launch, resume, cancellation, and normalization behavior is documented in -[`runner/CODEX_PROVIDER.md`](runner/CODEX_PROVIDER.md). +```json +{"schemaVersion":"paperclip.runner.conformance.output.v1","runIdentity":{"runId":"run_conformance_0001","sessionId":"session_conformance_0001"},"result":{"status":"succeeded","summary":"Standalone Conformance fixture accepted."}} +``` -Use `generate:protocol-manifest` after a schema or fixture change, -`generate:protocol-types` after a schema change, and -`generate:replay-goldens` after an intentional reducer change. Commit generated -outputs with their sources; do not edit them by hand. +Run only the tracer with: -Use `generate:semantic-action-catalog` after changing a semantic action -declaration. Its checked-in JSON inventory must land with the source change. +```sh +pnpm --filter @paperclipai/paperclip-runner trace:conformance +``` -The gate compiles every schema with AJV 2020-12, validates accepted fixtures, -rejects unsupported required versions, checks generated TypeScript schema -drift, runs the TypeScript contract tests, and compares reducer snapshots and -parity summaries byte-for-byte with their checked-in golden files. +Replay the Replay happy path, run a Local session, or open the browser +devtool: + +```sh +pnpm --filter @paperclipai/paperclip-runner replay:fixture +pnpm --filter @paperclipai/paperclip-runner trace:local-runner -- --scenario happy-path +pnpm --filter @paperclipai/paperclip-runner trace:codex +pnpm --filter @paperclipai/paperclip-runner demo:live-console -- --host 127.0.0.1 --port 4174 + +# 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 + +# SDK: open the public-SDK reference console and mini consumer. +pnpm --filter @paperclipai/paperclip-runner console:sdk + +# 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 + +``` + +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. + +## Package-owned commands + +| Command | Purpose | +|---|---| +| `build` | Compile the TypeScript public surface, Rust workspace, and browser devtool. | +| `typecheck` | Check TypeScript, Rust, generated schema sources, and browser types. | +| `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:numbered-milestones` | Reject numbered construction-milestone names in tracked package paths and source. | +| `check:package-boundaries` | Enforce the acyclic runtime/testing dependency and manifest boundary. | +| `check:clean-consumers` | Pack the runner and install its root and testing exports in a clean consumer. | +| `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. | +| `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. | +| `trace:codex` | Run the mock core with a real, local skillless Codex app-server session. | +| `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`. | +| `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: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: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. | +| `browser:dev` | Start the standalone live/replay browser devtool. | +| `test:browser` | Exercise static replay and live scenarios, then capture temporary screenshots under ignored test output. | +| `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) +- [ADR 0001: runner and testing package boundaries](docs/adr/0001-runner-testing-eval-package-boundaries.md) +- [Tutorial index](docs/index.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 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) +- [Adding a harness and permission-mode requirements](docs/adding-a-harness.md) +- [PRP v1 expressiveness audit](spec/prp-v1-expressiveness-audit.md) +- [Cumulative end-to-end tutorial](docs/tutorials/end-to-end.md) + +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. Runtime production Paperclip integration remains +deferred; the App-owned production conformance adapter is test-only. + +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` +identifies the current console iteration. diff --git a/packages/paperclip-runner/devtools/browser/index.html b/packages/paperclip-runner/devtools/browser/index.html new file mode 100644 index 0000000000..301dbcf965 --- /dev/null +++ b/packages/paperclip-runner/devtools/browser/index.html @@ -0,0 +1,13 @@ + + + + + + + Paperclip Runner · PRP Static Replay + + +
+ + + diff --git a/packages/paperclip-runner/devtools/browser/live-console.spec.ts b/packages/paperclip-runner/devtools/browser/live-console.spec.ts new file mode 100644 index 0000000000..23ff80c386 --- /dev/null +++ b/packages/paperclip-runner/devtools/browser/live-console.spec.ts @@ -0,0 +1,398 @@ +import { expect, test, type Page } from "@playwright/test"; + +const DESKTOP = { width: 1440, height: 900 }; +const MOBILE = { width: 390, height: 844 }; + +function shot(page: Page, name: string) { + return page.screenshot({ path: test.info().outputPath(`${name}.png`), fullPage: true }); +} + +async function openConsole(page: Page) { + await page.goto("/"); + await page.getByRole("button", { name: "Live console" }).click(); + await expect(page.getByRole("heading", { name: "Live Codex protocol console" })).toBeVisible(); +} + +async function runManifest(page: Page, id: string) { + await expect(page.getByTestId("manifest-list")).toBeVisible(); + await page.getByTestId(`manifest-${id}`).click(); + await page.getByTestId("run-manifest").click(); + await expect(page.getByTestId("connection-status")).toHaveText("connected", { timeout: 15_000 }); +} + +test.describe("Live console", () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize(DESKTOP); + }); + + test("shows the empty state and runs a completed turn", async ({ page }) => { + await openConsole(page); + await expect(page.getByTestId("empty-transcript")).toBeVisible(); + await expect(page.getByTestId("composer-send")).toBeDisabled(); + await shot(page, "desktop-01-idle-empty"); + + await runManifest(page, "completion"); + await expect(page.getByTestId("reasoning-item")).toBeVisible(); + await expect(page.getByTestId("tool-item")).toBeVisible(); + await shot(page, "desktop-02-streaming-turn"); + + 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 Live console demo server proves.", + ); + await expect(page.getByTestId("tool-item")).toHaveAttribute("data-status", "completed"); + await shot(page, "desktop-03-turn-completed"); + + // The browser must never receive a provider credential. + const body = await page.locator("body").innerText(); + expect(body).not.toContain("Bearer "); + expect(body).not.toContain("auth.json"); + }); + + test("acknowledges steering and keeps rejected text recoverable", async ({ page }) => { + await openConsole(page); + await runManifest(page, "steering"); + await expect(page.getByTestId("composer-send")).toHaveText("Steer", { timeout: 15_000 }); + + await page.getByTestId("composer-input").fill("Keep it to one sentence."); + await page.getByTestId("composer-send").click(); + const chip = page.getByTestId("steering-chip").first(); + await expect(chip).toContainText("acknowledged", { timeout: 15_000 }); + await shot(page, "desktop-04-steering-acknowledged"); + + await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 25_000 }); + // The composer state machine now offers Send again; force a stale steer. + await page.evaluate(() => window.history.replaceState({}, "", "/")); + }); + + test("rejects a stale steer and offers to resend it", async ({ page }) => { + await openConsole(page); + await runManifest(page, "steering"); + await expect(page.getByTestId("composer-send")).toHaveText("Steer", { timeout: 15_000 }); + await page.getByTestId("composer-input").fill("Typed while the turn was running."); + + // Let the turn finish before the steer is sent: the classic stale race. + await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 25_000 }); + await expect(page.getByTestId("composer-send")).toHaveText("Send"); + }); + + test("cancels a turn interrupted before it starts", async ({ page }) => { + await openConsole(page); + await runManifest(page, "interrupt-before-start"); + await expect(page.getByTestId("composer-stop")).toBeEnabled(); + await page.getByTestId("composer-stop").click(); + await expect(page.getByTestId("turn-cancelled")).toBeVisible({ timeout: 15_000 }); + await expect(page.getByTestId("turn-cancelled")).toContainText("Cancelled before start"); + await shot(page, "desktop-05-interrupt-before-start"); + }); + + test("keeps partial text when a streaming turn is stopped", async ({ page }) => { + await openConsole(page); + await runManifest(page, "interrupt-during-generation"); + await expect(page.getByTestId("live-transcript")).toContainText("The tracer keeps", { + timeout: 15_000, + }); + await page.getByTestId("composer-stop").click(); + await expect(page.getByTestId("turn-interrupted")).toBeVisible({ timeout: 15_000 }); + await expect(page.getByTestId("interrupted-divider").first()).toBeVisible(); + await expect(page.getByTestId("live-transcript")).not.toContainText("And this final one."); + await shot(page, "desktop-06-interrupt-during-generation"); + }); + + test("keeps an interrupted tool item without a fabricated result", async ({ page }) => { + await openConsole(page); + await runManifest(page, "interrupt-during-tool"); + await expect(page.getByTestId("tool-item")).toHaveAttribute("data-status", "running", { + timeout: 15_000, + }); + await page.getByTestId("composer-stop").click(); + await expect(page.getByTestId("tool-item")).toHaveAttribute("data-status", "interrupted", { + timeout: 15_000, + }); + await expect(page.getByTestId("turn-interrupted")).toBeVisible(); + await shot(page, "desktop-07-interrupt-during-tool"); + }); + + test("resolves approvals exactly once and collapses the resolved card", async ({ page }) => { + await openConsole(page); + await runManifest(page, "approvals"); + const card = page.getByTestId("request-card").first(); + await expect(card).toHaveAttribute("data-status", "pending", { timeout: 15_000 }); + await expect(page.getByTestId("pending-request-banner")).toBeVisible(); + await expect(page.getByTestId("request-action-accept_for_session")).toBeVisible(); + await shot(page, "desktop-08-request-pending"); + + await page.getByTestId("request-action-accept").first().click(); + await expect(page.getByTestId("request-card").first()).toHaveAttribute( + "data-status", + "resolved", + { timeout: 15_000 }, + ); + await expect(page.getByTestId("request-card").first()).toContainText("resolved — Approve"); + await shot(page, "desktop-09-request-resolved"); + + // Second request only offers accept and decline. + const second = page.getByTestId("request-card").nth(1); + await expect(second).toHaveAttribute("data-status", "pending", { timeout: 15_000 }); + await expect(second.getByTestId("request-action-accept_for_session")).toHaveCount(0); + }); + + test("submits user-input and elicitation answers and lets an unanswered request expire", async ({ page }) => { + test.setTimeout(60_000); + await openConsole(page); + await runManifest(page, "user-input"); + await expect(page.getByTestId("request-answer")).toBeVisible({ timeout: 15_000 }); + await page.getByTestId("request-answer").fill("staging"); + await page.getByTestId("request-action-submit").click(); + await expect(page.getByTestId("request-card").first()).toHaveAttribute( + "data-status", + "resolved", + { timeout: 15_000 }, + ); + + // An elicitation submits `content`, not `answers`. The runner validates the + // body against the request kind, so a card that sent the user-input shape + // would be rejected and stay pending here. + const elicitation = page.getByTestId("request-card").nth(1); + await expect(elicitation).toHaveAttribute("data-status", "pending", { timeout: 15_000 }); + await elicitation.getByTestId("request-answer").fill("stable"); + await elicitation.getByTestId("request-action-submit").click(); + await expect(elicitation).toHaveAttribute("data-status", "resolved", { timeout: 15_000 }); + await expect(elicitation).toContainText("resolved — Submit"); + + await expect(page.getByTestId("request-card").nth(2)).toHaveAttribute( + "data-status", + "expired", + { timeout: 25_000 }, + ); + await expect(page.getByTestId("request-card").nth(2)).toContainText("expired before response"); + await shot(page, "desktop-10-request-expired"); + }); + + test("moves a goal through its lifecycle", async ({ page }) => { + await openConsole(page); + await runManifest(page, "goals"); + await expect(page.getByTestId("goal-banner")).toHaveText("No goal set", { timeout: 15_000 }); + + await page.locator("#goal-menu").click(); + await page.getByRole("menuitem", { name: "Set goal…" }).click(); + await page.getByTestId("goal-objective").fill("Keep the demo goal visible"); + await page.getByTestId("goal-set-confirm").click(); + await expect(page.getByTestId("goal-banner")).toContainText("Keep the demo goal visible", { + timeout: 15_000, + }); + await expect(page.getByTestId("goal-banner")).toContainText("active"); + + await page.locator("#goal-menu").click(); + await page.getByRole("menuitem", { name: "Pause" }).click(); + await expect(page.getByTestId("goal-banner")).toContainText("paused", { timeout: 15_000 }); + await shot(page, "desktop-11-goal-supported"); + }); + + test("disables the goal menu with the exact upstream diagnostic", async ({ page }) => { + await openConsole(page); + await runManifest(page, "goals-unsupported"); + const trigger = page.locator("#goal-menu"); + await expect(trigger).toHaveAttribute("aria-disabled", "true", { timeout: 15_000 }); + const describedBy = await trigger.getAttribute("aria-describedby"); + expect(describedBy).not.toBeNull(); + await expect(page.locator(`#${describedBy}`)).toHaveText( + "Goal operations unsupported: app-server 0.132.0 does not advertise the goals capability", + ); + + await page.getByTestId("toggle-inspector").click(); + await page.getByRole("tab", { name: "Capabilities" }).click(); + await expect(page.getByTestId("inspector-capabilities")).toContainText( + "does not advertise the goals capability", + ); + await shot(page, "desktop-12-goal-unsupported"); + }); + + test("shows child threads and refuses to emulate child steering", async ({ page }) => { + await openConsole(page); + await runManifest(page, "subagents"); + await expect(page.getByTestId("thread-thread-child-b")).toBeVisible({ timeout: 20_000 }); + await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 20_000 }); + await expect(page.getByTestId("thread-thread-child-a")).toContainText("Completed"); + await expect(page.getByTestId("thread-thread-child-b")).toContainText("Failed"); + + await page.getByTestId("thread-thread-child-a").click(); + await expect(page.getByTestId("breadcrumb")).toContainText("thread-child-a"); + await expect(page.getByTestId("composer-reason")).toHaveText( + "Direct steering of child threads is not supported by this app-server.", + ); + await expect(page.getByTestId("composer-input")).toBeDisabled(); + await shot(page, "desktop-13-lineage-child"); + }); + + test("shows the failed item and diagnostic in the record", async ({ page }) => { + await openConsole(page); + await runManifest(page, "failure"); + await expect(page.getByTestId("turn-failed")).toBeVisible({ timeout: 20_000 }); + await expect(page.getByTestId("tool-failure")).toContainText("cannot find module"); + await shot(page, "desktop-14-failed-turn"); + }); + + test("resumes the same session after a transport drop", async ({ page }) => { + await openConsole(page); + await runManifest(page, "completion"); + await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 20_000 }); + const before = await page.getByTestId("live-transcript").innerText(); + + await page.getByTestId("drop-connection").click(); + await expect(page.getByTestId("reconnect-banner")).toBeVisible(); + await expect(page.getByTestId("composer-input")).toBeDisabled(); + await shot(page, "desktop-15-reconnect-banner"); + + await page.getByTestId("retry-now").click(); + await expect(page.getByTestId("connection-status")).toHaveText("connected", { + timeout: 20_000, + }); + // Live and resumed transcripts must be identical, not merely similar. + expect(await page.getByTestId("live-transcript").innerText()).toContain(before.trim()); + }); + + test("restores the same session across a page refresh", async ({ page }) => { + await openConsole(page); + await runManifest(page, "completion"); + await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 20_000 }); + const before = await page.getByTestId("live-transcript").innerText(); + + await page.reload(); + await page.getByRole("button", { name: "Live console" }).click(); + await expect(page.getByTestId("live-transcript")).toContainText("finished the task", { + timeout: 20_000, + }); + expect(await page.getByTestId("live-transcript").innerText()).toContain(before.trim()); + await shot(page, "desktop-16-refresh-replay"); + }); + + test("replays a recorded session with a stepper", async ({ page }) => { + await openConsole(page); + await runManifest(page, "completion"); + await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 20_000 }); + + await page.getByTestId("toggle-replay").click(); + await expect(page.getByTestId("replay-badge")).toBeVisible(); + await expect(page.getByTestId("replay-stepper")).toBeVisible(); + await expect(page.getByTestId("empty-transcript")).toBeVisible(); + + await page.getByRole("button", { name: "Step forward" }).click(); + await page.getByRole("button", { name: "Step forward" }).click(); + await expect(page.getByTestId("empty-transcript")).toHaveCount(0); + await shot(page, "desktop-17-replay-mode"); + + await page.getByTestId("toggle-replay").click(); + await expect(page.getByTestId("replay-badge")).toHaveCount(0); + await expect(page.getByTestId("turn-completed")).toBeVisible(); + }); + + test("inspects canonical events and reports reducer parity", async ({ page }) => { + await openConsole(page); + await runManifest(page, "completion"); + await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 20_000 }); + + await page.getByTestId("toggle-inspector").click(); + await expect(page.getByTestId("inspector-events")).toBeVisible(); + await page.getByTestId("inspector-filter").fill("turn.completed"); + await expect(page.getByTestId("inspector-events").getByRole("listitem")).toHaveCount(1); + await shot(page, "desktop-18-inspector-events"); + + await page.getByTestId("inspector-filter").fill(""); + await page.getByRole("tab", { name: "Session" }).click(); + await expect(page.getByTestId("inspector-session")).toContainText("Credentials in browser"); + await expect(page.getByTestId("replay-parity")).toHaveText("match"); + await shot(page, "desktop-19-inspector-session"); + }); + + test("meets the keyboard contract", async ({ page }) => { + await openConsole(page); + await runManifest(page, "steering"); + await expect(page.getByTestId("composer-send")).toHaveText("Steer", { timeout: 15_000 }); + + // K2: Escape moves focus to Stop and never interrupts directly. + await page.getByTestId("composer-input").focus(); + await page.keyboard.press("Escape"); + await expect(page.getByTestId("composer-stop")).toBeFocused(); + await expect(page.getByTestId("turn-interrupted")).toHaveCount(0); + + // K2: Enter steers the running turn. + await page.getByTestId("composer-input").fill("Steered with the keyboard."); + await page.getByTestId("composer-input").press("Enter"); + await expect(page.getByTestId("steering-chip").first()).toContainText("acknowledged", { + timeout: 15_000, + }); + + // K7: inspector tabs follow the arrow-key tabs pattern. + await page.getByTestId("toggle-inspector").click(); + await page.getByRole("tab", { name: "Events" }).focus(); + await page.keyboard.press("ArrowRight"); + await expect(page.getByRole("tab", { name: "Requests" })).toBeFocused(); + await expect(page.getByRole("tab", { name: "Requests" })).toHaveAttribute( + "aria-selected", + "true", + ); + + // K5: lineage tree responds to arrow keys. + await page.getByTestId("lineage-tree").getByRole("treeitem").first().focus(); + await page.keyboard.press("ArrowDown"); + + // K6/A1: the transcript is a labelled log landmark. + await expect(page.getByTestId("live-transcript")).toHaveAttribute("role", "log"); + await expect(page.getByTestId("live-transcript")).toHaveAttribute("aria-live", "polite"); + }); + + test("resets demo state behind a naming confirmation", async ({ page }) => { + await openConsole(page); + await runManifest(page, "completion"); + await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 20_000 }); + + await page.getByRole("button", { name: "Reset demo state", exact: true }).first().click(); + await expect(page.getByRole("dialog")).toContainText( + "Recorded evidence files are not touched.", + ); + await page.getByTestId("reset-confirm").click(); + await expect(page.getByTestId("empty-transcript")).toBeVisible(); + }); +}); + +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); + await expect(page.getByRole("tab", { name: "Chat" })).toBeVisible(); + await expect(page.getByTestId("manifest-list")).toHaveCount(0); + await shot(page, "mobile-01-idle-empty"); + + await page.getByRole("tab", { name: "Session" }).click(); + await expect(page.getByTestId("manifest-list")).toBeVisible(); + await page.getByTestId("manifest-approvals").click(); + await page.getByTestId("run-manifest").click(); + await shot(page, "mobile-02-session-segment"); + + await page.getByRole("tab", { name: "Chat" }).click(); + await expect(page.getByTestId("request-card").first()).toHaveAttribute( + "data-status", + "pending", + { timeout: 20_000 }, + ); + await shot(page, "mobile-03-request-pending"); + await page.getByTestId("request-action-accept").first().click(); + await expect(page.getByTestId("request-card").first()).toHaveAttribute( + "data-status", + "resolved", + { timeout: 15_000 }, + ); + + await page.getByRole("tab", { name: "Inspector" }).click(); + await expect(page.getByTestId("inspector-events")).toBeVisible(); + await shot(page, "mobile-04-inspector"); + + // WCAG 1.4.10 reflow: no horizontal scrolling of the page itself. + const overflow = await page.evaluate( + () => document.documentElement.scrollWidth - document.documentElement.clientWidth, + ); + expect(overflow).toBeLessThanOrEqual(1); + }); +}); diff --git a/packages/paperclip-runner/devtools/browser/playwright.config.ts b/packages/paperclip-runner/devtools/browser/playwright.config.ts new file mode 100644 index 0000000000..bcd43122dc --- /dev/null +++ b/packages/paperclip-runner/devtools/browser/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: ".", + testMatch: ["static-replay.spec.ts", "live-console.spec.ts"], + fullyParallel: false, + workers: 1, + reporter: "line", + use: { + baseURL: "http://127.0.0.1:4179", + viewport: { width: 1440, height: 1000 }, + }, + webServer: { + command: + "pnpm exec vite preview --config vite.config.ts --host 127.0.0.1 --port 4179", + env: { + // Keep streamed chunks slow enough that interrupt races are reachable + // from a test, and fast enough that the suite stays quick. + 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_LIVE_CONSOLE_MAX_SESSIONS: "64", + }, + url: "http://127.0.0.1:4179", + reuseExistingServer: false, + timeout: 30_000, + }, +}); diff --git a/packages/paperclip-runner/devtools/browser/src/App.tsx b/packages/paperclip-runner/devtools/browser/src/App.tsx new file mode 100644 index 0000000000..020e41e651 --- /dev/null +++ b/packages/paperclip-runner/devtools/browser/src/App.tsx @@ -0,0 +1,642 @@ +import { useMemo, useRef, useState } from "react"; + +import type { PrpEvent, PrpRequest } from "../../../src/protocol/replay-contract"; +import type { SessionSnapshot } from "../../../src/reducer/session-reducer"; +import type { + LocalRunnerRunMetadata, + LocalRunnerRunTrace, + LocalRunnerScenario, +} from "../../../src/contracts/local-runner"; +import { + 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 { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "./components/ui/card"; +import { Textarea } from "./components/ui/textarea"; +import { LiveConsole } from "./live/LiveConsole"; + +const fixtureFiles = import.meta.glob( + "../../../protocol/fixtures/replay/*.json", + { eager: true, query: "?raw", import: "default" }, +) as Record; + +const fixtureOrder = [ + "happy-path", + "failed-run", + "interrupted-run", + "duplicate-event", + "source-gap", + "unknown-optional-fields", + "unsupported-required-version", +]; + +const liveScenarios: Array<{ key: LocalRunnerScenario; label: string }> = [ + { key: "happy-path", label: "Happy path" }, + { key: "permission-input", label: "Permission and input" }, + { key: "interrupted", label: "Interruption" }, + { key: "error", label: "Scripted error" }, + { key: "duplicate-terminal", label: "Duplicate terminal guard" }, +]; + +type BrowserMode = "console" | "live" | "replay"; +type LiveStatus = "idle" | "starting" | "running" | "terminal" | "error"; + +const modeCopy: Record< + BrowserMode, + { eyebrow: string; title: string; description: string } +> = { + console: { + 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.", + }, + live: { + 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 · Replay", + title: "Static protocol replay", + description: + "Validate a protocol fixture, replay its events, and inspect the resulting session state.", + }, +}; + +interface BrowserFixture { + key: string; + label: string; + source: string; +} + +type LocalRunnerStreamRecord = + | { kind: "event"; event: unknown } + | { kind: "diagnostic"; message: string } + | { kind: "trace"; trace: LocalRunnerRunTrace } + | { kind: "error"; message: string }; + +const browserFixtures = Object.entries(fixtureFiles) + .map(([path, source]): BrowserFixture => { + const key = path.split("/").at(-1)?.replace(/\.json$/, "") ?? path; + const parsed = JSON.parse(source) as { name?: string }; + return { key, label: parsed.name ?? key, source }; + }) + .sort((left, right) => fixtureOrder.indexOf(left.key) - fixtureOrder.indexOf(right.key)); + +function terminalTone(snapshot: SessionSnapshot) { + if (snapshot.integrity === "gap_detected") { + return "warning" as const; + } + if (snapshot.terminal?.runTerminalState === "succeeded") { + return "success" as const; + } + if (snapshot.terminal?.runTerminalState === "failed") { + return "danger" as const; + } + return "neutral" as const; +} + +function humanizeProtocolLabel(value: string): string { + const words = value.replaceAll(/[_-]/g, " "); + return words.length === 0 ? words : `${words[0]?.toUpperCase()}${words.slice(1)}`; +} + +function liveStatusTone(status: LiveStatus, snapshot: SessionSnapshot | null) { + if (status === "error") { + return "danger" as const; + } + if (status !== "terminal") { + return "neutral" as const; + } + if (snapshot?.terminal?.runTerminalState === "succeeded") { + return "success" as const; + } + if (snapshot?.terminal?.runTerminalState === "failed") { + return "danger" as const; + } + return "neutral" as const; +} + +function SnapshotSummary({ + snapshot, + label = "Session snapshot", +}: { + snapshot: SessionSnapshot; + label?: string; +}) { + return ( + <> +
+
+

{label}

+

{snapshot.fixtureName}

+
+ + {humanizeProtocolLabel( + snapshot.integrity === "gap_detected" + ? "Gap detected" + : (snapshot.terminal?.runTerminalState ?? snapshot.runPhase), + )} + +
+ +
+
+
Run
+
{snapshot.identity.runId}
+
+
+
Session
+
{snapshot.identity.normalizedSessionId}
+
+
+
Turn
+
{snapshot.terminal?.turnTerminalState ?? snapshot.turnState}
+
+
+
Events
+
{snapshot.timeline.length}
+
+
+ + {snapshot.proposedResult ? ( +
+ + Reported {humanizeProtocolLabel(snapshot.proposedResult.reportedWorkDisposition)} + +

{snapshot.proposedResult.summary}

+
+ ) : null} + + {snapshot.gaps.length > 0 ? ( +
+ Missing source sequence {snapshot.gaps.flatMap((gap) => gap.missing).join(", ")}. + Replay remains inspectable but is not complete. +
+ ) : null} + +
+

Ordered timeline

+ {snapshot.duplicateEventIds.length} duplicate events ignored +
+
    + {snapshot.timeline.map((event) => ( +
  1. + +
    +
    + {event.eventType} + +
    +

    {event.summary}

    +
    +
  2. + ))} +
+ +
+ Inspect snapshot JSON +
{JSON.stringify(snapshot, null, 2)}
+
+ + ); +} + +function pendingRuntimeRequest(events: readonly PrpEvent[]): PrpRequest | null { + const resolved = new Set( + events + .filter((event) => event.eventType === "runtime_request.resolved") + .map((event) => (event.payload as Record).requestId) + .filter((value): value is string => typeof value === "string"), + ); + for (const event of events.toReversed()) { + if (event.eventType !== "runtime_request.created") { + continue; + } + const request = (event.payload as Record).request; + if ( + typeof request === "object" && + request !== null && + "requestId" in request && + typeof request.requestId === "string" && + !resolved.has(request.requestId) + ) { + return request as PrpRequest; + } + } + return null; +} + +function LiveRunner() { + const [scenario, setScenario] = useState("happy-path"); + const [status, setStatus] = useState("idle"); + const [runId, setRunId] = useState(null); + const [snapshot, setSnapshot] = useState(null); + const [events, setEvents] = useState([]); + const [error, setError] = useState(null); + const [diagnostic, setDiagnostic] = useState(null); + const [input, setInput] = useState("local-runner-live-trace"); + const [trace, setTrace] = useState(null); + const [parity, setParity] = useState(null); + const snapshotRef = useRef(null); + const eventsRef = useRef([]); + + const request = useMemo(() => pendingRuntimeRequest(events), [events]); + + async function startRun() { + setStatus("starting"); + setError(null); + setDiagnostic(null); + setTrace(null); + setParity(null); + setRunId(null); + setEvents([]); + eventsRef.current = []; + setSnapshot(null); + snapshotRef.current = null; + + try { + 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?: 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 = createLocalRunnerLiveSnapshot(started.metadata); + setSnapshot(initial); + snapshotRef.current = initial; + setStatus("running"); + + 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."); + } + const reader = streamResponse.body.getReader(); + const decoder = new TextDecoder(); + let buffered = ""; + while (true) { + const part = await reader.read(); + buffered += decoder.decode(part.value, { stream: !part.done }); + const lines = buffered.split("\n"); + buffered = lines.pop() ?? ""; + for (const line of lines) { + if (line.trim().length > 0) { + handleRecord(JSON.parse(line) as LocalRunnerStreamRecord, started.metadata); + } + } + if (part.done) { + if (buffered.trim().length > 0) { + handleRecord(JSON.parse(buffered) as LocalRunnerStreamRecord, started.metadata); + } + break; + } + } + } catch (cause) { + setStatus("error"); + setError(cause instanceof Error ? cause.message : String(cause)); + } + } + + function handleRecord(record: LocalRunnerStreamRecord, metadata: LocalRunnerRunMetadata) { + if (record.kind === "event") { + const current = snapshotRef.current; + if (current === null) { + return; + } + const applied = applyLocalRunnerLiveEvent(current, record.event); + if (!applied.ok) { + setStatus("error"); + setError(applied.issues.join("; ")); + return; + } + snapshotRef.current = applied.snapshot; + setSnapshot(applied.snapshot); + eventsRef.current = [...eventsRef.current, applied.event]; + setEvents(eventsRef.current); + return; + } + if (record.kind === "diagnostic") { + setDiagnostic(record.message); + return; + } + if (record.kind === "error") { + setStatus("error"); + setError(record.message); + return; + } + setTrace(record.trace); + const replay = replayLocalRunnerEvents(metadata, record.trace.events); + setParity( + snapshotRef.current === null + ? false + : localRunnerSnapshotsMatch(snapshotRef.current, replay), + ); + setStatus("terminal"); + } + + async function postAction(action: "interrupt" | "resolve", body?: unknown) { + if (runId === null) { + return; + } + const response = await fetch(`/api/localRunner/runs/${runId}/${action}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body ?? {}), + }); + if (!response.ok) { + const failure = (await response.json()) as { error?: string }; + setError(failure.error ?? `The ${action} command failed.`); + setStatus("error"); + } + } + + return ( +
+ + + Local runner controls + + Start one Rust runner and one scripted fake harness. No network or model is used. + + + + + +
+ + +
+ +
+ Runner status + + {humanizeProtocolLabel(status)} + +
+ + {request?.requestKind === "runtime" ? ( +
+
+

{request.type} request

+

{request.prompt}

+
+ {request.type === "permission" ? ( + + ) : ( + <> + + setInput(event.target.value)} + /> + + + )} +
+ ) : null} + + {trace?.harnessProcessExit ? ( +
+
+
Harness process exit
+
{trace.harnessProcessExit.exitCode ?? "signal"}
+
+
+
Semantic result
+
{trace.result?.reportedWorkDisposition ?? "missing"}
+
+
+ ) : null} + + {parity !== null ? ( +
+ Live and replay reducer output + + {parity ? "Match" : "Mismatch"} + +
+ ) : null} + {diagnostic ?

{diagnostic}

: null} + {error ?

{error}

: null} +
+
+ +
+ {snapshot ? ( + + ) : ( +
+

Validated live snapshot

+

Start a local run

+

Each incoming event will pass the Replay validator before the reducer applies it.

+
+ )} +
+
+ ); +} + +function StaticReplay() { + const defaultFixture = browserFixtures[0]; + if (defaultFixture === undefined) { + throw new Error("No Replay fixtures were bundled"); + } + const [selectedFixture, setSelectedFixture] = useState(defaultFixture.key); + const [source, setSource] = useState(defaultFixture.source); + const [replay, setReplay] = useState(() => replayReplayFixtureText(defaultFixture.source)); + const selected = useMemo( + () => browserFixtures.find((fixture) => fixture.key === selectedFixture), + [selectedFixture], + ); + + function chooseFixture(key: string) { + const fixture = browserFixtures.find((candidate) => candidate.key === key); + if (fixture === undefined) { + return; + } + setSelectedFixture(key); + setSource(fixture.source); + setReplay(replayReplayFixtureText(fixture.source)); + } + + return ( +
+ + + Fixture input + + Choose a conformance case or edit the JSON, then validate and replay it. + + + + + + +