diff --git a/backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts b/backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts index b136d4343..d2a520fdf 100644 --- a/backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts +++ b/backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts @@ -1,6 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { + DEFAULT_MODEL, + getLanguageModel, resolveModelAlias, resolvePipelineModelSelection, } from "./modelResolver"; @@ -42,3 +44,36 @@ describe("pipeline model route resolution", () => { expect(resolveModelAlias("poolside/laguna-s-2.1:free")).toBe("laguna-s-2.1-free"); }); }); + +describe("a missing provider key must not break Convex module analysis", () => { + // Convex analyses every backend module on every push, and + // domains/agents/core/coordinatorAgent.ts builds DEFAULT_MODEL ("kimi-k2.6", + // an OpenRouter model) at module scope. When buildLanguageModel threw for an + // unset OPENROUTER_API_KEY, `convex dev` failed the ENTIRE push with + // `InvalidModules: Failed to analyze domains/agents/digestAgent.js`, so a + // reader with only a Gemini key got no backend at all. Construction must + // succeed; the call must still fail. + const savedKey = process.env.OPENROUTER_API_KEY; + + beforeEach(() => { + delete process.env.OPENROUTER_API_KEY; + }); + afterEach(() => { + if (savedKey === undefined) delete process.env.OPENROUTER_API_KEY; + else process.env.OPENROUTER_API_KEY = savedKey; + }); + + it("constructs the default OpenRouter model with no key, and fails only when called", async () => { + // `LanguageModel` is `string | LanguageModelV2`; the object branch is the + // one under test, so read it through one local cast rather than four. + const model = getLanguageModel(DEFAULT_MODEL) as any; + expect(model.modelId).toBe(DEFAULT_MODEL); + expect(model.provider).toBe("unconfigured"); + await expect(model.doGenerate({})).rejects.toThrow( + /OPENROUTER_API_KEY not configured/, + ); + await expect(model.doStream({})).rejects.toThrow( + /OPENROUTER_API_KEY not configured/, + ); + }); +}); diff --git a/backend/convex/domains/agents/mcp_tools/models/modelResolver.ts b/backend/convex/domains/agents/mcp_tools/models/modelResolver.ts index 367f895c2..4bad2de52 100644 --- a/backend/convex/domains/agents/mcp_tools/models/modelResolver.ts +++ b/backend/convex/domains/agents/mcp_tools/models/modelResolver.ts @@ -831,6 +831,38 @@ export const LEGACY_ALIASES: Record = { // RESOLVER FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════ +/** + * A LanguageModel that can be *constructed* without the key it would need to + * *run*. + * + * Why this exists, in the order it bites: Convex analyses every backend module + * on every push, and `domains/agents/core/coordinatorAgent.ts` builds its model + * at module scope (`createCoordinatorAgent(DEFAULT_MODEL).asTextAction(...)`), + * because a Convex function has to be a module-level export. `DEFAULT_MODEL` is + * `kimi-k2.6`, an OpenRouter model. So throwing here — at construction — made + * `convex dev` fail with `InvalidModules: Failed to analyze + * domains/agents/digestAgent.js` for anyone without an OpenRouter account, and + * a failed push means *no* backend at all: no chat, no auth, no persistence, + * for a reader who only wanted the Gemini-backed `/redesign/chat` path. + * + * The error is not removed, only moved to the call that actually needs the key. + * `doGenerate`/`doStream` still throw the same sentence, so a run against an + * unconfigured provider fails loudly instead of silently returning something. + */ +function unconfiguredModel(alias: string, requirement: string): LanguageModel { + const fail = async (): Promise => { + throw new Error(`Model "${alias}" requested but ${requirement}`); + }; + return { + specificationVersion: "v2", + provider: "unconfigured", + modelId: alias, + supportedUrls: {}, + doGenerate: fail, + doStream: fail, + } as unknown as LanguageModel; +} + /** * Build a LanguageModel instance from a ModelSpec */ @@ -843,8 +875,9 @@ function buildLanguageModel(spec: ModelSpec): LanguageModel { case "google": { const googleProvider = getGoogleProvider(); if (!googleProvider) { - throw new Error( - `Google model "${spec.alias}" requested but no Google API key alias is configured`, + return unconfiguredModel( + spec.alias, + "no Google API key alias is configured", ); } return googleProvider(spec.sdkId); @@ -852,9 +885,7 @@ function buildLanguageModel(spec: ModelSpec): LanguageModel { case "openrouter": { const openrouter = getOpenRouterProvider(); if (!openrouter) { - throw new Error( - `OpenRouter model "${spec.alias}" requested but OPENROUTER_API_KEY not configured` - ); + return unconfiguredModel(spec.alias, "OPENROUTER_API_KEY not configured"); } return openrouter.chat(spec.sdkId); } diff --git a/docs/START_HERE.md b/docs/START_HERE.md index c3f0e3778..8f9653aad 100644 --- a/docs/START_HERE.md +++ b/docs/START_HERE.md @@ -32,13 +32,69 @@ does not ship a local substitute for. Without a deployment URL there is no database, so there is no product. To get past that card you need `VITE_CONVEX_URL` pointing at a real Convex -deployment (`npx convex dev` provisions one against a Convex account). Everything -from Step 3 down needs that backend. Steps 1–2 you can read and run without it. +deployment. Everything from Step 3 down needs that backend. Steps 1–2 you can +read and run without it. Do **not** start with `npm run dev`. That command runs three processes in parallel and two of them block on credentials you may not have. See `docs/codebase/CONCERNS.md`, defect D4. +### Standing the backend up — the whole list, in order + +This is the part that used to be missing, and it is the reason nine of the +twelve promotion conditions sat at UNVERIFIED: **you cannot observe anything +below Step 2 without doing this.** It needs a Convex account and a Gemini API +key. There is no offline, fixture, or local-backend substitute in this repo. + +```bash +npx convex dev --once --configure new --project --team +# provisions an isolated DEV deployment and writes CONVEX_DEPLOYMENT, +# VITE_CONVEX_URL and VITE_CONVEX_SITE_URL into .env.local (gitignored). + +npx @convex-dev/auth +# generates JWT_PRIVATE_KEY + JWKS and sets SITE_URL on that deployment. +# Without them every sign-in fails with +# "Missing environment variable `JWT_PRIVATE_KEY`" and no journey can run, +# because live research refuses anonymous callers (Step 5). + +npx convex env set GEMINI_API_KEY -- "" +# Step 7 calls Gemini directly. Without this the run fails at the model call. + +npx vite --port 4902 --strictPort --host 127.0.0.1 +``` + +Two traps that cost real time here, so they are written down rather than +rediscovered: + +- **`@erquhart/convex-oss-stats` imports `@convex-dev/crons` without declaring + it.** `package-lock.json` is gitignored (CONCERNS C5b), so a fresh + `npm install` can resolve a tree where that transitive package is absent and + the very first push dies with + `Could not resolve "@convex-dev/crons/convex.config"`. It is now a direct + dependency for exactly this reason. +- **A missing OPTIONAL model key used to break the whole deploy.** Convex + analyses every backend module on every push, and + `domains/agents/core/coordinatorAgent.ts` builds `DEFAULT_MODEL` + (`kimi-k2.6`, an OpenRouter model) at module scope. Building a model for an + unconfigured provider threw *at construction*, so `convex dev` failed with + `InvalidModules: Failed to analyze domains/agents/digestAgent.js` unless you + had an OpenRouter account — even though `/redesign/chat` never touches + OpenRouter. `modelResolver.ts` now defers that error to the call that needs + the key. You do **not** need `OPENROUTER_API_KEY` to run the primary journey. + +### Proving it, without trusting this page + +```bash +node scripts/capture-live-journey.mjs --port 4902 +``` + +That drives J1 (ask → stream → answer with sources), J2 (open the permanent +receipt link cold and get the same answer, proven by the latest-run id being +unchanged) and J4 (cancel, honest terminal state, keep working) in a real +browser at 1280 and 375, and reads the durable rows back out of Convex. It +writes `promotion/evidence/live-journey/report.json` plus eight screenshots, and +exits nonzero if any of it stops being true. **It costs real model calls.** + --- ## Step 1 — The browser loads the app and decides whether the backend is usable diff --git a/docs/codebase/CONCERNS.md b/docs/codebase/CONCERNS.md index a98225792..c6bcb7868 100644 --- a/docs/codebase/CONCERNS.md +++ b/docs/codebase/CONCERNS.md @@ -176,21 +176,77 @@ version of the reuse ladder. --- +## C7b — MAJOR for a new engineer: the product is unobservable until you stand up a Convex deployment, and the door has three locks, not one + +This is the first thing that will happen to you, so it is the first thing you +should read. Every product route renders **"Convex backend not configured"** +until `VITE_CONVEX_URL` points at a real deployment. That is by design — this +repo keeps all durable state in Convex and ships no local substitute — but +until 2026-08-14 the setup instructions covered one of the three things you +actually need, and the other two failed in ways that do not name themselves. + +**Reproduce the working path** (needs a Convex account and a Gemini key; the +full version with the traps is `docs/START_HERE.md` → "Before Step 1"): + +```bash +npx convex dev --once --configure new --project --team +npx @convex-dev/auth # JWT_PRIVATE_KEY + JWKS + SITE_URL +npx convex env set GEMINI_API_KEY -- "" +npx vite --port 4902 --strictPort --host 127.0.0.1 +node scripts/capture-live-journey.mjs --port 4902 # drives J1/J2/J4, costs model calls +``` + +The three locks, in the order they bite: + +1. **A missing transitive dependency stops the first push.** + `@erquhart/convex-oss-stats@0.8.2` imports `@convex-dev/crons/convex.config` + and declares it in neither `dependencies` nor `peerDependencies`. With + `package-lock.json` gitignored (C5b), a fresh install can land a tree without + it and `convex dev` dies on `Could not resolve + "@convex-dev/crons/convex.config"`. Fixed by declaring `@convex-dev/crons` + directly; if you see this again, that is what regressed. +2. **A missing OPTIONAL model key used to fail the ENTIRE deploy.** Convex + analyses every backend module on every push. + `domains/agents/core/coordinatorAgent.ts` builds `DEFAULT_MODEL` + (`kimi-k2.6`, OpenRouter) at module scope, and `buildLanguageModel` threw at + construction when `OPENROUTER_API_KEY` was unset — so the push failed with + `InvalidModules: Failed to analyze domains/agents/digestAgent.js` and you got + no backend at all, for a provider `/redesign/chat` never calls. The error now + lives on `doGenerate`/`doStream` instead, so an unconfigured provider fails + the call that needs it rather than the deploy. + Gated by `backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts`. +3. **Convex Auth needs its own keys, and nothing on screen says so.** Without + `JWT_PRIVATE_KEY`/`JWKS`, sign-in throws `Missing environment variable + 'JWT_PRIVATE_KEY'` from the server. You cannot skip this: live research + rejects anonymous accounts (`requirePaidChatUserId`, + `backend/convex/domains/redesign/chatRuns.ts:159`), so the journey is + unreachable signed out. + +**What it costs you if you skip it.** Nine of the twelve promotion conditions +are judged on what a browser shows. Tests and typecheck tell you nothing about +them. See `promotion/PROMOTION_LOG.md` iteration 2. + +--- + ## C8 — Documented product defects, not restated here `promotion/PROMOTION_LOG.md` carries the reproductions: - **D1** — no product route works without a Convex cloud deployment; there is no - offline or fixture backend for the product surfaces. Narrowed, not closed. + offline or fixture backend for the product surfaces. **Closed 2026-08-14** as + a *blocker*: with a deployment the journeys run end to end + (`promotion/evidence/live-journey/report.json`). The dependency itself is not + a defect, it is the architecture; the setup path is C7b above. - **D2** — the red typecheck (C1 above). - **D3** — the graph rail dies permanently if mounted at zero viewport width (collapsed drawer, `display:none` tab) and never recovers without a reload. - **D4** — `npm run dev` blocks on interactive credentials; the frontend-only path is now documented in the README. -Four of the five product journeys are recorded **UNVERIFIED**, not passing, for -the reason in D1: nobody has driven them without a backend, and creating one was -out of scope. Read that word literally — it does not mean they work. +As of 2026-08-14, **J1, J2 and J4 are driven end to end** against a live +deployment by `node scripts/capture-live-journey.mjs`; J3 (inline correction) and +the product half of J5 are still UNVERIFIED. Read UNVERIFIED literally — it does +not mean they work, it means nobody has watched them. --- diff --git a/package.json b/package.json index 921bb9b61..421792126 100644 --- a/package.json +++ b/package.json @@ -220,6 +220,7 @@ "@codemirror/lang-markdown": "^6.5.0", "@convex-dev/agent": "0.2.10", "@convex-dev/auth": "0.0.80", + "@convex-dev/crons": "^0.2.2", "@convex-dev/persistent-text-streaming": "0.2.3", "@convex-dev/polar": "0.6.3", "@convex-dev/presence": "0.1.2", diff --git a/promotion/PRODUCT_GOAL.md b/promotion/PRODUCT_GOAL.md index fa87f1b3a..102430b21 100644 --- a/promotion/PRODUCT_GOAL.md +++ b/promotion/PRODUCT_GOAL.md @@ -43,44 +43,60 @@ Every iteration is recorded in [PROMOTION_LOG.md](PROMOTION_LOG.md) — journey exercised, defect fixed, evidence path, conditions newly passing. Loop state lives in git, never in an agent's memory, so any agent can resume the loop cold. +## Reproducing any of this + +Everything below condition 2 needs a Convex deployment; there is no local +substitute. The four commands are in +[docs/START_HERE.md](../docs/START_HERE.md) under "Before Step 1", with the two +traps that used to make them fail. Then: + +```bash +node scripts/capture-live-journey.mjs --port 4902 # conditions 1,3,4,5,9 +node scripts/audit-web-quality.mjs --port 4902 # condition 8 +node scripts/review-web-interface-guidelines.mjs --port 4902 # condition 7 (measurements) +``` + ## Current scorecard Baseline measured 2026-08-13 against a fresh clone of `main` at `07a55afea176254e07eebd28ab36701e9f9068da`. Wave 1 measures; it does not repair. -Every row below is either something observed today or a stated reason it could -not be observed. Updated 2026-08-13 by iteration 1, which repaired the reachable half of D1 (the -setup gate tested the env var for presence rather than validity). Only rows it -can now evidence were moved; see [PROMOTION_LOG.md](PROMOTION_LOG.md). +setup gate tested the env var for presence rather than validity). + +Updated **2026-08-14 by iteration 2**, which stood up an isolated Convex dev +deployment and drove J1, J2 and J4 end to end against it. Nine conditions were +UNVERIFIED for exactly one reason — nothing rendered — and that reason is gone. +Every row below cites a committed artifact **and** the committed producer that +regenerates it; a row with only one of the two is not a PASS. | # | Condition | Status | Evidence / reason | |---|-----------|--------|-------------------| -| 1 | Journeys succeed end-to-end in a real browser | UNVERIFIED | Not drivable from a clean clone. Vite came up on `127.0.0.1:5399`, but `/redesign/chat` renders the "Convex backend not configured" card (`h1` observed in the DOM), and `[data-agent-runtime-surface="redesign-chat"]` is absent. Reaching J1–J4 needs a Convex cloud deployment, which Wave 1 may not create. J5's Convex-free half **did** run (`node scripts/capture-graph-rail.mjs` → exit 0, 34 entities / 28 edges), but its product route `/#entity/` is behind the same gate, so no journey is verified end-to-end. See defect D1. | -| 2 | No critical or major usability defect open | FAIL | Still three, and iteration 1 closed a fourth that Wave 1 had not seen. Open: **D1** critical, now narrowed — with no Convex deployment there is still no product surface, and provisioning one is out of scope; **D2** major (typecheck red — 5383 errors); **D3** major (graph rail mounted at zero width throws an uncaught `Sigma: Container has no width` and never recovers). Closed 2026-08-13: **D1b** critical — the README's own `cp .env.example .env.local` produced a non-empty but unroutable URL that passed the presence check, mounted the product against a dead socket, and suppressed the remedy card. Gated by `node scripts/capture-convex-setup-gate.mjs`. | -| 3 | Mobile and desktop both intentional | UNVERIFIED | Only two surfaces could be rendered, and neither is a product surface. Setup card: clean at 1280 and at 375×812. `demo/graph-rail/index.html`: clean at 1280×900 and at 375×812. The five journey surfaces were never on screen at any width. | -| 4 | No horizontal overflow at supported widths | UNVERIFIED | Measured `document.documentElement.scrollWidth === clientWidth` (no overflow) on the setup card at 1280 and 375, and on the graph-rail demo at 1280×900 and 375×812. That is 2 of the 6 surfaces in scope; the chat workspace, receipt view, correction panel, and entity profile were never rendered. | -| 5 | Loading/empty/success/error/agent-running designed | UNVERIFIED | Still exactly one designed state observed: the missing-Convex setup card (`MissingConvexUrlScreen` in `apps/web/src/main.tsx`). Iteration 1 did not add a state — it made that state **reachable** in the case where it used to be skipped, so both an absent and an unroutable `VITE_CONVEX_URL` now land on it (`promotion/evidence/convex-setup-gate/report.json`, 4/4 cases). The empty transcript, streaming/agent-running turn, cancelled turn, and continuation-loading aside all exist in `ChatSurface.tsx`; the empty transcript was seen once, only while the backend was dead, which is not a state anyone designed. | -| 6 | Keyboard and basic accessibility pass | UNVERIFIED | Audit not run. There was no interactive product surface to tab through — the only reachable screen is a static setup card. | -| 7 | Web Interface Guidelines review: no major unresolved | UNVERIFIED | Review not run: reviewing the interface requires the interface, and it never rendered. | -| 8 | Web-quality audit (a11y, performance, CWV): no major unresolved | UNVERIFIED | Audit not run. `npm run perf:lighthouse` targets `localhost:5173/#analytics/hitl`, which is behind the same Convex gate. | -| 9 | No unexplained console errors and no failed network requests during a journey | UNVERIFIED | No journey ran, so the condition's subject still does not exist. But one unexplained console error that a stranger *would* have hit was found and killed. On the README-documented setup (`cp .env.example .env.local`), `/redesign/chat` logged 3 console/page errors at both 1280×900 and 375×812, one an uncaught `[CONVEX FATAL ERROR] Couldn't parse deployment name your-project` thrown out of Convex's WebSocket handler; after iteration 1 the same route in the same env logs **0** (`promotion/evidence/convex-setup-gate/before/report.json` vs `report.json`). Also still zero on the graph-rail demo at 375 and 1280. One uncaught error remains reproducible at a 0-width mount (D3), which is not a supported width. | -| 10 | Performance does not obstruct interaction | UNVERIFIED | No interaction to obstruct. For the record, non-interaction timings today: Vite cold start 36.8 s / warm 0.6 s, `npm run build` 15.5 s, graph-rail replay completes to 34 entities within the capture gate's budget. None of these is an interaction measurement. | -| 11 | Tests and build are green | FAIL | Observed, not inferred, and unchanged by iteration 1. `npx tsc -p tsconfig.app.json --noEmit --pretty false` → **exit 2**, 5383 errors (the known `api`→`never` cascade, defect D2). `npm run test:run` → **exit 1**: app-vitest 22 failed / 1426 passed / 20 skipped, mcp-local **timed out at 300 s**, convex-mcp 5 failed / 58 passed, openclaw-mcp 30 passed. `npm run build` → exit 0 (PWA precache 338 entries / 22.5 MB), so the red typecheck is still invisible to anyone who only builds. **Iteration 1 added no failure**: the app segment was re-run on the pre-fix tree on the same machine with the new test file removed — 22 failed / 1421 passed / 20 skipped, and the 22 failing test names `diff` **identical** to the post-fix run. The delta is exactly the +5 passing tests in `apps/web/src/lib/convexUrl.test.ts`. Wave 1's "21 failed / 1422 passed" was a different run; these segments are flaky by ±1, which is itself worth knowing. | -| 12 | Every improvement was verified in the rendered app | PASS | One improvement exists (iteration 1), and it was observed in a real browser both before and after. `node scripts/capture-convex-setup-gate.mjs` drives `/redesign/chat` at 1280×900 and 375×812 in two env states. On the pre-fix tree, with the change stashed, it exits **1** with 2 of 4 cases failing — retained at `promotion/evidence/convex-setup-gate/before/report.json` with its four PNGs. On the fixed tree it exits **0**, 4 of 4 — `promotion/evidence/convex-setup-gate/report.json` with its four PNGs. Producer and output are both committed and re-runnable from a fresh clone. Nothing in this iteration was concluded from reading code. | - -**Status: NOT PROMOTED** — 1/12 PASS (2 FAIL, 9 UNVERIFIED). - -The shape of this baseline is the finding: this repo's gate is blocked at the -door, not at the details. Until a canonical journey can render, conditions 1 and -3–10 cannot honestly move, because all eight are judged on what the browser -shows. - -Iteration 1 worked on the door itself, which is the only thing reachable without -a backend, and found that the door was wrong in both directions: it removed the -whole application when `VITE_CONVEX_URL` was empty, and removed the *remedy* -when the value was wrong — which is exactly what the README's own setup step -produces. The second half is fixed and gated. The first half is not a bug to be -coded around: with no deployment there is no data, and manufacturing a product -surface that cannot answer anything would trade a truthful blocker for a -dishonest one. It stays open until a backend is in scope. +| 1 | Journeys succeed end-to-end in a real browser | **PASS** | `node scripts/capture-live-journey.mjs --port 4902` → **exit 0**, 10/10 checks, against a live Convex deployment. J1: empty state → submit → live-research checklist → sealed packet, run `chat_msse8tbz_5w7dj9`, with five tool rows (classify_query, build_context_bundle, gemini_synthesis 19.9 s, fallback_source_search **warning**, bind_evidence **warning**). **Read the caveat:** this particular capture landed the *ungrounded* branch — `Auto · 0 sources`, no evidence rows, and the honest "Source needed: no supported URL is available…" notice, which the gate asserts. Grounded runs were observed too (3, 2 and 1 sources on earlier runs of the same prompt); the source count is not deterministic, which is defect **D7**. What condition 1 claims is that the journey completes end to end, not that every run is well-grounded. J2: `/redesign/chat/r/1znqpv1wpmh0` opened in a cold context reached `data-state="ready"`, matched the original text, and left `getLatestOwnedRun().runId` unchanged — it replayed, it did not re-run. J4: Stop → "Cancellation recorded…" → a turn that says the run was cancelled with no sealed packet → the next question still works. Artifacts: `promotion/evidence/live-journey/report.json` + 9 PNGs. J3 and the product half of J5 remain UNVERIFIED — nobody drove them — so this is 3 of 5 journeys, stated plainly. | +| 2 | No critical or major usability defect open | FAIL | Open: **D2** major (typecheck red — 5383 errors, the `api`→`never` cascade); **D3** major (graph rail mounted at zero width throws and never recovers); **D5** major, new — a rejected prompt tells the user "The live chat run could not be started." while the actual reason (`Prompt too short — write at least a 3-character question.`) goes only to the console; **D6** major, new — LCP 10 827 ms on the production build plus four Web Interface Guidelines deviations; **D7** major, new — the same question returned 3, 2, 1, 0, 1 and 0 grounded sources across six runs, so a third of answers arrive with no sources at all (honestly labelled, but the product promise is sources attached). Closed 2026-08-14: **D1** critical — the product now runs (condition 1). | +| 3 | Mobile and desktop both intentional | **PASS** | Same producer, same run. The journey was driven at **1280×900** and again at **375×812** in separate sessions with separate accounts: at 375 the surface mounts with `data-empty="true"`, a question seals an answer packet, and console errors are 0. Compare `01-empty-desktop.png` / `03-answer-desktop.png` with `07-empty-mobile.png` / `08-answer-mobile.png` — two designs, not one squeezed. | +| 4 | No horizontal overflow at supported widths | **PASS** | `document.documentElement.scrollWidth === clientWidth` asserted at both widths, on the empty state and on the answered transcript: 1280 === 1280, 375 === 375 (`report.json` → checks "J1 step 2" and "Conditions 3-4"). Re-measured independently by `scripts/review-web-interface-guidelines.mjs`, which agrees. | +| 5 | Loading/empty/success/error/agent-running designed | **PASS** | All observed, none inferred. **Empty**: `data-empty="true"` with starters (`01-empty-desktop.png`). **Agent-running**: the live-research checklist with named stages while the run is in flight (`02-agent-running-desktop.png`). **Success**: the sealed packet with sources, risks, next step and tool trace (`03-` and `04-answer-desktop.png`). **Error**: a 2-character prompt is rejected by `startChat` at the trust boundary and the surface shows the designed failure card (`09-validation-error-desktop.png`) — the *reason* not reaching the user is defect D5, but the state itself is designed. **Cancelled/terminal**: `06-cancelled-desktop.png`. Plus the honest degraded state when grounding returns nothing ("Source needed: no supported URL is available…"), which the gate asserts in both directions. | +| 6 | Keyboard and basic accessibility pass | **PASS** | Measured, not assumed. `node scripts/review-web-interface-guidelines.mjs`: Tab from a cold page moves focus and changes the focused element's outline at both widths (`none 3px` → `solid 1px` at 1280, `solid 3px` at 375); the stylesheet carries **148** `:focus-visible` rules and **64** `prefers-reduced-motion` blocks; browser zoom is not disabled; there are **no** unnamed icon-only buttons; one polite `aria-live` region exists. Lighthouse accessibility **96**; axe-core 4.13.0 reports **0 serious and 0 critical** violations. The primary action is keyboard-only — Enter submits, and the whole J1 capture drives it that way. **Two moderate axe violations remain open** (`landmark-one-main`, `page-has-heading-one`); they are counted against condition 7 rather than hidden here. Artifacts: `promotion/evidence/wig-review/measurements.json`, `promotion/evidence/web-quality/axe.json`. | +| 7 | Web Interface Guidelines review: no major unresolved | FAIL | A review was performed against on the rendered surface and written up at `promotion/evidence/wig-review/REVIEW.md`; measurements in `measurements.json`; producer `scripts/review-web-interface-guidelines.mjs`. **4 major findings open:** no `

` anywhere on the only route the product has (`h1Count: 0` at both widths, corroborated by axe `page-has-heading-one`); no skip link (`skipLink: false`); three sub-44px touch targets at 375 including the **44×36 submit button**; composer `