diff --git a/packages/paperclip-runner/docs/design/mockups/capture-issue-thread-mockup.mjs b/packages/paperclip-runner/docs/design/mockups/capture-issue-thread-mockup.mjs new file mode 100644 index 00000000000..ba465feb1c4 --- /dev/null +++ b/packages/paperclip-runner/docs/design/mockups/capture-issue-thread-mockup.mjs @@ -0,0 +1,44 @@ +import { chromium } from "@playwright/test"; +import { mkdirSync } from "node:fs"; + +const root = new URL("./packages/paperclip-runner/docs/design/mockups/phase-7-issue-thread.html", import.meta.url).href; +const outDir = process.env.OUT_DIR; +mkdirSync(outDir, { recursive: true }); + +const shots = [ + ["thread-pending-confirmation", "?state=pending"], + ["thread-baseline", "?state=baseline"], + ["thread-resolved-denial-panel", "?state=resolved&panel=open"], + ["turn-streaming", "?state=streaming"], + ["replay-mode", "?state=replay"], +]; +const viewports = { + desktop: { width: 1440, height: 900 }, + mobile: { width: 390, height: 844 }, +}; + +const browser = await chromium.launch({ + executablePath: process.env.CHROMIUM_PATH || undefined, +}); +for (const [vpName, vp] of Object.entries(viewports)) { + const page = await browser.newPage({ viewport: vp }); + for (const [slug, params] of shots) { + let p = params; + // mobile: evidence panel state uses the segment instead of the side panel + if (vpName === "mobile" && slug === "thread-resolved-denial-panel") p = "?state=resolved&seg=evidence"; + await page.goto(root + p); + await page.waitForSelector('html[data-thread-state="settled"]'); + const hscroll = await page.evaluate(() => { + const el = document.scrollingElement; + return el.scrollWidth - el.clientWidth; + }); + if (vp.width === 390 && hscroll > 0) { + console.error(`FAIL h-scroll ${slug} mobile: overflow ${hscroll}px`); + process.exitCode = 1; + } + await page.screenshot({ path: `${outDir}/${slug}--${vpName}.png` }); + console.log(`ok ${slug}--${vpName} (hscroll=${hscroll})`); + } + await page.close(); +} +await browser.close(); diff --git a/packages/paperclip-runner/docs/design/mockups/phase-7-issue-thread.html b/packages/paperclip-runner/docs/design/mockups/phase-7-issue-thread.html new file mode 100644 index 00000000000..1fb895acc9b --- /dev/null +++ b/packages/paperclip-runner/docs/design/mockups/phase-7-issue-thread.html @@ -0,0 +1,373 @@ + + + + + +MCK-31 — Phase 7 issue thread (mockup) + + + + +
+
+ MCK-31 +

Ship the retry queue for outbound webhooks

+ In progress + ▲ High +
+ + + + + + +
+
+
+ ◆ Fake agent + In-process runner + ⛁ Mock Paperclip + assignee mock-agent-01 · run turn 3 idle +
+ + +
+
+
+ + + +
+
+
+
+ +
⚙ Wake: issue_blockers_resolved → checkout by mock-agent-01 · 09:14:02
+ +
Turn 1 · fake · 2 tool calls · 09:14:05
+ +
+
You (board user)09:14:05
+ Please pick up MCK-31, summarize the current state, and update the plan document before you start. +
+ +
✓get_task_contextread issue, 4 comments, plan r3, 1 blockerView in Evidence›
+ +
+
Fake agent09:14:07
+ The retry queue work is unblocked — MCK-29 (schema change) is done. I'll revise the plan to cover backoff policy and dead-letter handling, then report progress. +
+ +
✓write_documentplan r3 → r4 · “Add exponential backoff + DLQ section”View in Evidence›
+ +
+
Recorded to mock thread
+
mock-agent-01progress · 09:14:09
+ Plan updated to r4: added backoff policy, DLQ routing, and a rollout gate. Remaining: confirm the plan, then implement the queue worker. Next owner: board confirmation. +
+ +
Turn 2 · fake · 2 tool calls · 09:15:20
+ + + + +
+
+ Confirmation requested + ⌘ plan · r4 + ● Waiting for you +
+ Accept the revised implementation plan? Backoff policy caps at 6 retries over 32 minutes; failures beyond that route to the dead-letter queue for manual review. + +
+ + +
+
+ + + + + + + + + + + +
+
+ +
+
+
+ + +
+ +
+
+
+ + +
+ + + + diff --git a/packages/paperclip-runner/docs/design/phase-7-issue-thread-ux-contract.md b/packages/paperclip-runner/docs/design/phase-7-issue-thread-ux-contract.md new file mode 100644 index 00000000000..bb9695bd836 --- /dev/null +++ b/packages/paperclip-runner/docs/design/phase-7-issue-thread-ux-contract.md @@ -0,0 +1,391 @@ +# Phase 7B — Issue-Thread UX Contract + +Status: **binding** for track 7G (Paperclip-style Web UI) and reviewable by tracks 7E/7K. +Source of authority: Phase 7 plan (PAP-16897 `plan` document), Revision 3 §"Page information +architecture", §"Native issue-thread interactions", §"Live execution contract", and Revision 4 +§"Final evidence eligibility". Capability vocabulary: Phase 7A generated contract +(`generated/phase-07/capabilities.yaml`, `mcp-tool-map.yaml`, `eval-traceability.yaml`, +`contract-schema.json` — PAP-16939). + +If a corner of this contract is underspecified, 7G comments on PAP-16940 and waits for a +revision. 7G does not diverge silently. Deviations are either fixed in the implementation or +written back into this document as an explicit revision. + +--- + +## 0. Vocabulary (normative) + +All UI copy and all implementation identifiers use the 7A terms. The UI never invents +capability language. + +- **Dispositions** (closed enum): `control_plane_owned`, `always_agent_tool`, + `optional_agent_tool`. +- **Always semantic operations** (thread-visible verbs): `get_task_context`, + `report_progress`, `answer_status_question`, `finish_task`, `block_task`, `request_review`, + `write_document` (+ revision reads), `request_human_input` (kinds `questions`, + `confirmation`, `checkbox`, `suggest_tasks`, `item_verdicts`), `register_deliverable`. +- **Optional operation families**: discovery (`scoped_discovery`, `search_tasks`, + `list_agents`, …), delegation/dependencies (`create_task`, `delegate_task`, + `set_dependencies`, `create_blocked_task`), approvals (`request_approval`, + `decide_approval`, …), cases, workspace runtime, routines, company skills, secrets, admin, + escape hatch. Grants render exactly as 7A writes them (e.g. `rf:read_or_write`). +- **Mock-state kinds**: `operation_result`, `runtime_decision_record`, `active_task_context`. +- **Eval keys**: case `id` (e.g. `ix-confirmation-plan-01`), `group` (16 groups), + `fixtureProfile` (e.g. `hb-baseline`), `browserEvidenceRecipe` (e.g. `hb/hb-context-01`), + `expectedSemanticOperations`, `forbiddenOperations`. +- **Modes**: `live` (real Codex via real runnerd), `fake` (deterministic fake agent), + `replay` (recorded canonical events). A surface never shows an unlabeled mode. + +Human-readable labels (UI copy) for the dispositions are fixed: `Agent tool — always`, +`Agent tool — granted`, `Control plane`. The machine term appears in the debug panel and in +tooltips, never invented synonyms ("system tool", "built-in", etc. are forbidden). + +## 1. Identity and mode banner (always visible) + +The page must answer, at all times and on every viewport, four questions: who is the agent, +what runs it, what control plane it talks to, and whether this session is live. + +1. **Three identity chips**, rendered in the issue header and never hidden by scroll + (header is sticky): + - `Real Codex` — cyan chip, only when the active session mode is `live`. In `fake` mode the + chip reads `Fake agent`; in `replay` it reads `Replay`. + - `Real runnerd` — neutral chip with a live process dot (pulse while a runnerd session is + attached; static gray when detached). Shown in `live` mode only; `fake`/`replay` show + `In-process runner`. + - `Mock Paperclip` — amber chip, shown in **all** modes. Tooltip: "All issue records are + mock. No real Paperclip API is reachable." +2. **Mode is data, not styling**: the root element carries `data-session-mode="live|fake|replay"` + and the chips render from the server-reported session record. A session artifact with + `mode=fake` must be visually distinguishable from `live` in every screenshot (Revision 4 + evidence eligibility). +3. **Mock identifier scheme**: mock issues use a reserved prefix that cannot be confused with + a real company (fixture default: `MCK-`, e.g. `MCK-31`). The UI never renders a + `/PAP/...`-style link to a real control plane; mock entity links navigate inside the + explorer only. +4. **No credential surface**: no header, panel, tooltip, error, or diff may contain a token, + key, or `Authorization` value. Redacted fields render as `••• redacted` with the redaction + rule name from the authorization record. + +## 2. Page information architecture + +### 2.1 Desktop (reference viewport 1440×900) + +``` +┌──────────────────────────────────────────────────────────────┬───────────────┐ +│ Issue header (sticky): │ │ +│ MCK-31 · title · StatusBadge · PriorityIcon · assignee │ Debug panel │ +│ [Fake agent|Real Codex] [Real runnerd] [Mock Paperclip] │ (collapsed │ +│ run state · Scenario ▾ · Replay · Reset · Stop │ by default; │ +├──────────────────────────────────────────────────────────────┤ resizable │ +│ Thread column (centered, max-width 760px, single column): │ 320–640px) │ +│ turn groups: user message → agent activity → responses │ │ +│ comment / interaction / document / deliverable / │ │ +│ disposition cards in chronological order │ │ +│ system notices (wakes, reconciliation) as one-line rows │ │ +├──────────────────────────────────────────────────────────────┤ │ +│ Composer (pinned bottom of thread column) │ │ +└──────────────────────────────────────────────────────────────┴───────────────┘ +``` + +- The **thread is the primary readable surface**. Default panel state is **collapsed**; the + user's open/width choice persists per browser (`localStorage`), and deep links may force it + (§10). The page never leads with raw protocol events. +- Thread column measure: max-width 760px, centered in the remaining space; `text-sm` body + scale per the Paperclip type ramp. +- Debug panel: right side, resizable 320–640px with a keyboard-operable splitter + (`role="separator"`, `aria-valuenow`), collapse toggle in the header (`Evidence` button with + open/closed state). +- Below 1100px viewport width the panel switches from side-by-side to an overlay sheet from + the right (same content, same tab order), so the thread column never drops below ~600px. + +### 2.2 Mobile (reference viewport 390×844) + +- One column. Sticky condensed header: row 1 = `MCK-31` + StatusBadge + overflow menu `⋯` + (Scenario, Replay, Reset inside the menu; **Stop stays outside the menu** whenever a turn is + active). Row 2 = the three identity chips, wrapping to a second line if needed — chips never + cause horizontal page scroll. +- A **segmented control** with exactly two segments — `Thread` and `Evidence` — sits under the + header (pattern validated in the Phase 7I mobile review; a drawer was rejected there and + stays rejected). Thread is default. Badge on `Evidence` shows the current turn's + authorization-denial count when nonzero. +- Composer is fixed to the bottom of the `Thread` segment, above the keyboard inset + (`env(safe-area-inset-bottom)`). +- **No horizontal page scroll at 390px** (`document.scrollingElement.scrollWidth <= + clientWidth` is an automated acceptance check). `pre`/code/diff blocks wrap or scroll inside + their own container only. +- Touch targets ≥ 44×44 CSS px for every actionable element, including chip tooltips + (tap-to-toggle on touch), accordion headers, and interaction-card controls. + +## 3. Thread item taxonomy + +Every thread item renders from a **mock-core record or canonical event** — the browser holds +no state authority, computes no claim/policy/parity result, and never mutates mock state +outside an interaction response (§5). Items in chronological order, grouped by turn: + +| # | Item | Source | Anatomy | +|---|------|--------|---------| +| T1 | **User message** | thread record | Right-aligned bubble style is **not** used; Paperclip comment card with author "You (board user)", timestamp, markdown body. | +| T2 | **Agent response** | model output items | Comment card, author = agent identity chip (`Fake agent` / `Real Codex`), streaming state per §6. Model prose only — never confused with durable records (see T3). | +| T3 | **Durable progress comment** | `report_progress` / `answer_status_question` `operation_result` | Distinct comment card with a `Recorded to mock thread` marker (filled corner tag + tooltip naming the semantic operation). This is the visual boundary between ephemeral model text (T2) and durable mock records. | +| T4 | **Tool activity strip** | semantic call + typed result | One line per call inside the turn group: status glyph (`✓ ok`, `✕ denied`, `⏳ running`), operation id (`write_document`), one-line human summary, `›` expander. Expanded: request args (redacted per rules), typed result, and a `View in Evidence` link that opens the debug panel pre-filtered to that call. Strips are collapsed by default; a turn shows at most 3 strips + `N more…` expander (progressive disclosure). | +| T5 | **Interaction card** | `request_human_input` record | §5. Rendered at its chronological position. | +| T6 | **Document card** | `write_document` result | Document key + title, revision chain (`r3 → r4`), author, `View diff` (opens Evidence → State), and stale marker when a later revision exists. | +| T7 | **Deliverable card** | `register_deliverable` result | File/ref name, kind (attachment bytes / external ref / workspace file), size, registered-by, download affordance for attachment-backed deliverables. | +| T8 | **Dependency / delegation card** | optional-op results (`create_task`, `set_dependencies`, …) | Created mock child issues with identifiers and blocker edges (`MCK-32 blocks MCK-31`). | +| T9 | **Disposition card** | `finish_task` / `block_task` / `request_review` result | Terminal banner card: new status via StatusBadge, explanation body, named blocker owner where applicable. After a terminal disposition the composer enters `disabled` (§6). | +| T10 | **Denial notice** | typed policy denial (`operation_result` with deny) | Inline red-bordered strip variant of T4: `✕ create_task — denied: missing grant su:read_or_write`. Shows deny reason **from the authorization record verbatim**; never leaks protected state or credentials. | +| T11 | **System notice** | `runtime_decision_record` (wake, checkout, reconciliation, budget stop, session events) | One-line, muted, icon + text (e.g. `⚙ Wake: issue_blockers_resolved → turn 3 started`). Progressive disclosure into Evidence → Control plane. Never a card; system notices must read quieter than work content. | + +Turn grouping: each turn renders a hairline group header `Turn N · · tool calls · +` binding T2/T4/T5-T10 items produced within it. Thread auto-follows the newest item +only while the user is at the bottom; a `Jump to latest` pill appears when scrolled up +(≥ 300px) or when new items arrive off-screen. + +## 4. Composer contract + +States (mutually exclusive; `data-composer-state` attribute is the test/screenshot hook): + +| State | Trigger | Visual | Controls | +|-------|---------|--------|----------| +| `ready` | session attached, no active turn, issue non-terminal | normal input, `Send` primary | Send (Cmd/Ctrl+Enter), attach disabled in Phase 7 | +| `sending` | message posted, turn not yet streaming | input cleared, inline spinner on Send | Send disabled | +| `streaming` | active turn | input stays **editable** (steer), primary button becomes `Stop` (destructive-outline), helper text `Codex is working — send to steer, or stop the turn.` | Send = steer (queued as next user input), Stop | +| `waiting` | pending interaction card requires the user | input disabled, helper `Answer the pending request above to continue.` with an anchor link that scrolls to and focuses the pending card | none | +| `reconnecting` | transport lost | input disabled, helper `Reconnecting… your session is preserved.` | Retry now | +| `disabled` | terminal disposition, replay mode, or budget stop | input disabled with reason line (`Issue is done`, `Replay is read-only`, `Budget limit reached`) | New scenario / Reset | + +Draft text survives refresh (localStorage per session id). Stop never discards the transcript; +a stopped turn's partial output stays in the thread with a `Stopped by user` marker on the +turn header. + +## 5. Interaction cards (native lifecycle) + +All five `request_human_input` kinds render as native cards using the typed payload's prompt, +options, labels, and validation — never as markdown asking the user to type an answer. + +Kinds and their answer controls: + +- `questions` (ask_user_questions): typed form — per-question control (radio/select/short + text), one submit. +- `confirmation` (request_confirmation): target summary + Accept / Reject buttons; reject + reason textarea when the payload requires it. **Revision-bound**: the card shows the target + (`plan · r4`) and links the exact revision. +- `checkbox` (request_checkbox_confirmation): checkbox list with min/max enforcement, + default-selected ids, Accept label / Reject label from payload. +- `suggest_tasks`: proposed-task list (title + description); board accepts a subset; accepted + tasks appear as T8 cards afterwards. +- `item_verdicts` (request_item_verdicts): per-item Approve / Reject / Defer segmented + buttons; reason required per `requireReasonOn`; supports partial submit (submitted items + lock, remaining stay editable, card stays `pending`). + +**Response authority path (normative):** the card's submit posts the typed response to the +package server; the **mock control plane stores the response before the runner receives it**, +and only then does the same Codex session resume with the typed result. The card's UI state +moves `pending → submitting → resolved` only on server acknowledgment (no optimistic +resolution). This is the *only* browser-initiated mock mutation; there is no other write path +from the page. + +**State matrix (all states are required and visually distinct):** + +| State | Store shape | Card treatment | +|-------|------------|----------------| +| `pending` | status `pending` | Accent left border (violet), controls enabled, `Waiting for you` chip, focus lands on first control when the card is the reason the composer is `waiting`. | +| `submitting` | in flight | Controls disabled, inline spinner. | +| `accepted` | status `accepted` | Green check chip `Accepted`, chosen values summarized inline, controls collapse to read-only summary. | +| `answered` | status `answered` | Same as accepted with `Answered` chip (questions / verdicts complete). | +| `rejected` | status `rejected` | Red chip `Changes requested`, reason quoted in the card. | +| `stale_target` | status `expired`, `result.outcome=stale_target` | Gray chip `Stale — plan moved to r5`, link to the superseding revision, controls removed, body dimmed (60% opacity). | +| `superseded_by_comment` | status `expired`, `result.outcome=superseded_by_comment` | Gray chip `Superseded by a later comment`, link to that comment. | +| `expired` | status `expired`, no outcome | Gray chip `Expired `. | +| `withdrawn` | status `cancelled`, `result.outcome=withdrawn` | Gray chip `Withdrawn`, optional reason. | +| `issue_closed` | status `cancelled`/`expired`, `result.outcome=issue_closed` | Gray chip `Issue closed`. | + +Resolved/expired cards are durable history: they stay in the thread at their chronological +position, are keyboard-reachable, and expose `View request evidence` linking the debug panel +to the related request, policy decision, mock mutation, wake, and resume events (the plan's +required debug linkage). Pending cards survive refresh and reconnect (§6) — rehydrated from +mock state, not from browser memory. + +## 6. Session lifecycle behaviors + +- **Refresh (F5)**: full restore from server state — same session, same turn ids, transcript, + pending interaction cards, composer state, and mode chips. The session **never restarts + because the browser reconnected**. Scroll restores to latest; a `Restored session` system + notice (T11) is *not* emitted (silent restore) — the evidence panel's session record is the + proof. +- **Reconnect**: on transport drop the composer enters `reconnecting`, a slim amber banner + pins under the header (`Connection lost — retrying (attempt n)`), and streaming indicators + freeze with a `paused` glyph. On reconnect, missed canonical events replay by ordinal (no + duplicates, no gaps) and the banner resolves to a 3s `Reconnected` confirmation. + `prefers-reduced-motion` replaces the banner slide with opacity. +- **Stop**: header Stop and composer Stop are the same action — bounded cancel of the active + turn. Post-state: partial output retained + `Stopped by user` turn marker; session and + pending interactions unaffected; composer returns to `ready`. +- **Reset**: destructive — always behind a confirm dialog (`Reset scenario? This clears the + mock state and starts a clean session. The transcript will be lost.`; confirm button + `Reset scenario`, destructive style; cancel is default focus). Reset re-seeds the fixture, + rotates/clears session authority (Revision 4), and lands on a clean thread with a fresh + `Turn 0` seeded context. Reset affects only the current browser session's scenario instance. +- **Replay**: mode `replay` re-renders a recorded run from canonical events. Composer + `disabled` (`Replay is read-only`), identity chip row shows `Replay` + `Mock Paperclip`, + and a top progress strip allows step/next-turn/play-all with a deterministic `?at=` + deep-link parameter. Replay of a `fake` recording must still be labeled as fake-derived + (chip `Replay · fake source`) so replay evidence can never satisfy a live criterion. +- **Stop/Reset/Replay/Scenario controls** live in the header on desktop; on mobile + Replay/Reset/Scenario collapse into `⋯`, Stop stays exposed while a turn is active (§2.2). + +## 7. Side debug panel (Evidence) + +Named **Evidence** in UI copy. Content scope: a **turn selector** at the top (`Turn N ▾`, +default = latest; `All turns` option for the state and parity sections). Below it, eight +accordion sections in this order (accordion, not tabs — >7 categories, and multiple sections +must be open simultaneously for review): + +1. **Tools exposed** — the turn's visible tool list grouped `Agent tool — always`, then + `Agent tool — granted` (each with its grant, e.g. `rf:read_or_write`), then a separated + muted list `Control plane (not exposed to the agent)` naming `control_plane_owned` + operations relevant to the fixture. Negative evidence is first-class: the control-plane + list exists precisely to show what the model *cannot* call. +2. **Calls & results** — chronological semantic calls: raw Codex request → dispatched command + → typed result/denial, with operation id, version, and redaction annotations. +3. **Authorization** — one record per decision: operation, claims considered, allow/deny + + reason, redactions applied, resulting state change ref. Deny records use the same red + accent as T10. +4. **Control plane** — `runtime_decision_record` stream: checkout/lock, wake scheduling, + budget, idempotency/retry, reconciliation, session lifecycle. +5. **Runner & events** — canonical PRP events and runnerd/Codex process diagnostics (session + id, thread id, process state, cleanup evidence). +6. **State diff** — before/after per entity class (tasks, comments, documents, interactions, + approvals, artifacts, blockers, workspace, budget, run). Per-turn by default; `All turns` + shows fixture-seed → current. Rendered from immutable snapshots; the browser never + computes a diff from its own bookkeeping. +7. **Traceability** — the fixture's 7A anchors: capability rows (id + `sourceAnchor`), eval + case id/group/`browserEvidenceRecipe`, `expectedSemanticOperations`, + `forbiddenOperations`, `requiredCapabilityGrants`. +8. **Parity** — assertion list with verdict chips (`pass` green / `fail` red / + `intentional gap` gray + note), summarized as `n/m` in the section header. + +Cross-linking contract: every T4 strip, T10 denial, and interaction card deep-links into the +matching Evidence record (`View in Evidence`), and every Evidence record links back to its +thread anchor. Deep-link target = section + record id, e.g. `?panel=authorization&rec=`. + +Mobile: the same eight sections render inside the `Evidence` segment, full-width accordions, +turn selector pinned under the segmented control. + +## 8. Visual language + +Follow the Paperclip design language without importing the product `ui/` package: + +- Dark theme default, OKLCH neutral grays; semantic tokens only (background/card/muted/ + accent/destructive/border/ring equivalents defined package-locally). No raw hex in + components. +- Type ramp: page title `text-xl font-bold`; card titles `text-sm font-medium`; body + `text-sm`; metadata `text-xs text-muted-foreground`; identifiers and operation ids + `font-mono text-xs`. +- Status/priority renders with StatusBadge/StatusIcon-equivalent components using the + product's status hue table (todo blue, in_progress indigo, in_review violet, done green, + blocked red, backlog/cancelled gray). +- Radii ≤ `rounded-xl`; shadows ≤ `shadow-sm`; density = product issue page, not a marketing + layout. +- Interaction cards use the product interaction-card anatomy (title row + prompt + controls + + state chip) so the mock thread reads as a Paperclip issue thread (Jakob's Law is the point + of this phase's demo). + +## 9. Accessibility acceptance (blocking) + +7G is not acceptable until all of these pass; 7K re-verifies them clean-room: + +1. **Keyboard tour** (documented, testable): Tab order = header controls → thread (each card + is a focusable group; Enter expands) → composer → Evidence toggle → panel. The splitter is + arrow-key resizable. Pending interaction controls are reachable without pointer; Escape + closes the mobile `⋯` menu and the desktop overlay sheet. +2. **Focus management**: opening Evidence moves focus to its heading; resolving a card moves + focus to the card's state chip; `waiting` composer's anchor link moves focus to the pending + card's first control. Focus ring = 3px ring token, never suppressed. +3. **Live regions**: streaming agent text in `aria-live="polite"` chunk announcements (throttled + ≥ 2s); turn completion, denial notices, and interaction resolution announce via a single + polite status region; reconnect banner is `role="status"`, Stop confirmation `role="alert"`. +4. **Structure**: one `h1` (issue title), landmarks `header/main/complementary` (Evidence), + `form` for composer; interaction cards are `section`s labeled by their prompt; tool strips + are disclosure buttons with `aria-expanded`. +5. **Color independence**: every state chip pairs color with a glyph + text (`✓ Accepted`, + `✕ Denied`, `⏳`); parity verdicts likewise. Contrast ≥ 4.5:1 for text, ≥ 3:1 for UI + glyphs, verified in dark theme. +6. **Reduced motion**: `prefers-reduced-motion` disables pulse dots, banner slides, streaming + shimmer, and smooth scrolling (instant jumps). +7. **Automated gate**: axe (or equivalent) run against every screenshot route in §10 with + zero serious/critical violations, executed in CI alongside screenshot capture. + +## 10. Deterministic screenshot contract + +### 10.1 Route scheme + +- Base route: `#/issue/` (e.g. `#/issue/hb-baseline`). Scenario/fixture ids + come from 7A `fixtureProfile`; per-case evidence uses the 7A `browserEvidenceRecipe` path + (`/`) as the canonical evidence id. +- Screenshot state param: `?shot=` seeds the named deterministic state below in `fake` + mode with: fixed clock (all timestamps render from fixture time), animations/caret/pulse + disabled, network idle, fonts loaded. +- Panel/deep-link params: `?panel=
[&rec=]`, `?at=` (replay), + `?seg=thread|evidence` (mobile segment). +- Settle signal: the root element sets `data-thread-state="settled"` when hydration, fixture + load, and auto-scroll are complete. Capture tooling waits for it — never for timeouts. + +### 10.2 Required matrix (12 slugs × 2 viewports = 24 PNGs) + +Viewports: desktop `1440×900`, mobile `390×844`. Output path: +`knowledge/evidence/phase-07/ui/--.png` (package-local). Every capture +also asserts `scrollWidth <= clientWidth` on the scrolling element at 390×844. + +| Slug | Seeded state | Must be visible | +|------|--------------|-----------------| +| `thread-baseline` | settled 3-turn fake run on `hb-baseline` | header w/ 3 identity chips + mode; T1/T2/T3/T4 items; turn headers; composer `ready` | +| `turn-streaming` | mid-turn stream | streaming indicator, composer `streaming` w/ Stop, editable steer input | +| `interaction-question-pending` | `ix-questions-01` pending card | typed question form, `Waiting for you` chip, composer `waiting` w/ anchor helper | +| `interaction-confirmation-pending` | `ix-confirmation-plan-01` pending | revision-bound target (`plan · r4`) on card, Accept/Reject, reject-reason affordance | +| `interaction-resolved-mixed` | history incl. accepted + rejected + `stale_target` + `superseded_by_comment` | four visually distinct resolved/expired treatments per §5 | +| `denial-optional-tool` | denied `create_task` (missing `su:read_or_write`) | T10 denial strip w/ verbatim deny reason; Evidence badge increment | +| `document-revision` | `dp-plan-doc-01` after `write_document` | T6 card w/ revision chain `r3 → r4` + View diff | +| `deliverable-registered` | `ar-upload-before-done-01` | T7 deliverable card w/ kind + registered-by | +| `disposition-terminal` | `st-done-comment-01` finished | T9 terminal card, StatusBadge `done`, composer `disabled` w/ reason | +| `debug-panel-open` | baseline + `?panel=authorization` | desktop: panel open at 384px w/ 8 sections, authorization records; mobile: `Evidence` segment active | +| `reconnect-banner` | forced transport drop | amber reconnect banner, composer `reconnecting`, frozen stream glyph | +| `replay-mode` | replay of recorded fake run `?at=12` | `Replay · fake source` chip, read-only composer, progress strip | + +Determinism rule: two captures of the same slug/viewport from a clean checkout must be +pixel-identical (the Phase 7I byte-identical bar). Anything time-, random-, or +locale-dependent renders from fixture data. + +### 10.3 Evidence naming + +Per-eval-case evidence (7F/7K scope) reuses `browserEvidenceRecipe` verbatim: +`knowledge/evidence/phase-07/cases//--.png`. The §10.2 matrix is +the UI acceptance set; case evidence is additive and follows the same settle/determinism +rules. + +## 11. Authority and safety rules (UI-side restatement) + +- The browser renders mock-core records, snapshots, and canonical events. It computes no + claim, policy, diff, or parity result client-side. UI-side state math is a defect. +- The only browser-initiated mock mutation is an interaction response (§5). Composer messages + go to the runner session, not to mock state. +- No provider, runner, or control-plane credential ever reaches the browser; redactions render + by rule name. Real Paperclip URLs/API paths never appear. +- Policy and state authority live in the package server + mock `ControlPlanePort`; refresh + and reconnect re-derive everything from them. + +## 12. 7G handoff checklist + +Implementation acceptance (UXDesigner review) requires: + +1. All §3 item types and all §5 interaction states implemented and reachable via fixtures. +2. All §4 composer states with `data-composer-state` hooks. +3. §6 behaviors demonstrated: refresh restore, reconnect replay, stop, reset confirm, replay + read-only. +4. §7 Evidence panel with all eight sections, turn selector, and bidirectional deep links. +5. §10 matrix: 24 deterministic PNGs recorded at the named routes, byte-stable across two + clean runs, plus the 390px no-horizontal-scroll assertion per capture. +6. §9 accessibility gate green (axe + documented keyboard tour). +7. Screenshot review posted to the 7G issue for UXDesigner acceptance before 7G closes. + +Questions or gaps → comment on PAP-16940.