diff --git a/media/hac-335/evidence/capture-manifest.json b/media/hac-335/evidence/capture-manifest.json
index f31b9ce..a9a7f37 100644
--- a/media/hac-335/evidence/capture-manifest.json
+++ b/media/hac-335/evidence/capture-manifest.json
@@ -2,11 +2,11 @@
"manifestId": "HAC-335-capture-manifest",
"issue": "HAC-335",
"generator": "media/hac-335/bin/capture-cockpit.mjs",
- "capturedFromSha": "7d62f6fe169cf354a548447dae0ce1ea7e210a08",
+ "capturedFromSha": "07e1a93f8d575b7bb03db1195b4a26594a8aad32",
"capturedSurface": "media/hac-341/cockpit.html (merged executable surface)",
"servedFrom": "repository root — the cockpit resolves shared identity from /assets",
"viewport": "1440x900",
- "captureSourceDigest": "24924971fd814b5f1baa460e420ebe3f9c9c9d09ec125ee616354f01d557819a",
+ "captureSourceDigest": "718d9c9e3f80a1633093df74131787509910fae14fac2ed6126cd9008c9d7879",
"captureSourceFiles": [
"assets/fonts/geist-mono-variable.woff2",
"assets/fonts/geist-variable.woff2",
diff --git a/media/hac-341/README.md b/media/hac-341/README.md
index 83d044f..cb21650 100644
--- a/media/hac-341/README.md
+++ b/media/hac-341/README.md
@@ -664,6 +664,11 @@ The issue's acceptance requires real human falsification. **No human cold-read
has been performed and none is claimed.** The protocol below is prepared; the
results are not.
+The runnable kit is [`cold-read/`](./cold-read/) — a moderator script to read
+verbatim and a per-reader response sheet. Use the **public custom domain**
+`https://interlock.marcellelabs.io`; every `*.vercel.app` URL is behind Vercel
+SSO and will send a cold reader to a login page.
+
Deterministic URLs for testers:
```
diff --git a/media/hac-341/bin/verify-cockpit.mjs b/media/hac-341/bin/verify-cockpit.mjs
index b169146..9f705ae 100644
--- a/media/hac-341/bin/verify-cockpit.mjs
+++ b/media/hac-341/bin/verify-cockpit.mjs
@@ -205,7 +205,9 @@ if (defaultArm && perturbedArm) {
}
// The cockpit must consume that derivation rather than re-deriving it inline.
-if (!/from '\.\/lib\/arm-view\.mjs'/.test(cockpit)) {
+// Matched on the module, not the specifier prefix, so the rewrite fix below
+// does not read as the cockpit having dropped the import.
+if (!/from '\/media\/hac-341\/lib\/arm-view\.mjs'/.test(cockpit)) {
fail('cockpit does not consume the shared arm-view derivation');
}
if (/environmentEvidence\[0\][^\n]*basisRevision|basisRevision[^\n]*environmentEvidence\[0\]/.test(cockpit)) {
@@ -1067,6 +1069,23 @@ if (!/aria-pressed/.test(cockpit)) fail('toggle state is not exposed to assistiv
if (!/aria-live/.test(cockpit)) fail('state changes are not announced');
if (!/data-glyph/.test(cockpit)) fail('state is encoded by colour alone; no glyph channel');
+/* Every module import and evidence fetch resolves from the site root.
+ *
+ * `vercel.json` rewrites `/` and `/cockpit` onto this file. A rewrite serves
+ * these bytes without changing the request URL, and a document-relative
+ * specifier resolves against the *request* URL — so `./lib/guide.mjs` became
+ * `/lib/guide.mjs` and aborted, and the two URLs a judge actually visits
+ * rendered a blank page while still returning HTTP 200.
+ *
+ * The visual gate could not see it: it loads `/media/hac-341/cockpit.html`
+ * directly, which is the one path where the relative form happens to work.
+ * This is a static check for that reason — it needs no server and no
+ * deployment, and it fails on the specifier rather than on the symptom.
+ */
+for (const m of cockpit.matchAll(/(?:^|\s)(?:import\b[^;]*?from|await\s+fetch\(|import\()\s*['"](\.[^'"]*)['"]/g)) {
+ fail(`document-relative reference "${m[1]}" breaks under the vercel.json rewrite; anchor it at /media/hac-341/`);
+}
+
const checksLabel = local.checks.label;
if (errors.length) {
process.stderr.write(`HAC-341 cockpit contract violated:\n${errors.map((e) => ` - ${e}`).join('\n')}\n`);
diff --git a/media/hac-341/cockpit.html b/media/hac-341/cockpit.html
index 3a528d0..fca3bab 100644
--- a/media/hac-341/cockpit.html
+++ b/media/hac-341/cockpit.html
@@ -704,14 +704,14 @@
from frozen evidence. A field that is absent there is absent here.
What the *selected arm* shows is derived in lib/arm-view.mjs, so the binding
between an arm and its evidence can be asserted without a browser. */
-import { armView } from './lib/arm-view.mjs';
+import { armView } from '/media/hac-341/lib/arm-view.mjs';
/* The guided layer's derivation. It lives beside arm-view.mjs for the same
reason: step 5 claims that four things were held constant and four changed,
and a claim like that has to be checked against the frozen arms rather than
typed into markup. `ablationDelta` reads both arms and reports what actually
moved, so a marker that has stopped being true fails the gate. */
import { guideView, guideRoute, stateOfStep, stepOfState, GUIDE_STEPS,
- GUIDE_FREE_STATE } from './lib/guide.mjs';
+ GUIDE_FREE_STATE } from '/media/hac-341/lib/guide.mjs';
const app = document.getElementById('app');
const drawer = document.getElementById('drawer');
@@ -1717,7 +1717,7 @@
${esc(title)}
addEventListener('popstate', () => { closeDrawer(); resolve(); });
try {
- const res = await fetch('./evidence/view-model.json', { cache: 'no-store' });
+ const res = await fetch('/media/hac-341/evidence/view-model.json', { cache: 'no-store' });
if (!res.ok) throw new Error(String(res.status));
MODEL = await res.json();
resolve();
diff --git a/media/hac-341/cold-read/README.md b/media/hac-341/cold-read/README.md
new file mode 100644
index 0000000..61ba2a2
--- /dev/null
+++ b/media/hac-341/cold-read/README.md
@@ -0,0 +1,100 @@
+# HAC-341 cold read — moderator kit
+
+**Status: READY_FOR_HUMAN_TEST. Not run. No result is claimed.**
+
+Three unfamiliar readers, tested one at a time, no narration. This kit is the
+whole protocol: the URLs, what you say, and where the answers go. Budget 15
+minutes per reader.
+
+A reader is *unfamiliar* if they have not seen the cockpit, the README, the
+Devpost copy or the video, and have not had Interlock explained to them. A
+colleague who has heard the pitch is not a cold reader.
+
+## Before you start
+
+Open [`response-sheet.md`](./response-sheet.md), one copy per reader.
+
+Check the surface is live and public — in a **logged-out / private window**:
+
+```
+https://interlock.marcellelabs.io/cockpit?run=hac330-local&proof=local&state=run.local.treatment
+```
+
+If that redirects to a login page, stop: you are on a protected URL. The
+`*.vercel.app` deployment URLs are SSO-protected and **must not be used for the
+cold read**. Only the custom domain is public.
+
+## The URLs
+
+| Purpose | URL |
+| --- | --- |
+| L1 default (start here) | `https://interlock.marcellelabs.io/cockpit?run=hac330-local&proof=local&state=run.local.treatment` |
+| Baseline arm | `…/cockpit?run=hac330-local&proof=local&state=run.local.baseline` |
+| Perturbed arm | `…/cockpit?run=hac330-local&proof=local&state=run.local.perturbed` |
+| Cloud overview | `…/cockpit?run=hac340-cloud&proof=cloud&state=run.cloud.overview` |
+| Reduced motion / static | append `&static=1` to any of the above |
+| Missing run (must show unavailable, never substitute) | `…/cockpit?run=nope&proof=local&state=run.local.treatment` |
+
+## What you say
+
+Read these aloud verbatim. **Do not explain anything, do not answer questions,
+do not point at the screen.** If a reader asks what something means, say: *"Tell
+me what you think it means."*
+
+> I'm going to show you a web page. I'm not going to tell you what it is. I'll
+> ask you a few questions as you look at it. There are no wrong answers — I'm
+> testing the page, not you. Please think out loud.
+
+Open the L1 default URL. Start the timer.
+
+1. **At 5 seconds** — cover the screen. *"What problem is this page about?"*
+2. **At 30 seconds** — *"What changed, and why did it change?"*
+3. **At 60 seconds** — *"If you didn't believe this, how would you check it
+ yourself?"*
+4. **Switch to the cloud overview URL. At 120 seconds** — *"Is what you're
+ looking at now the same experiment as before, or a different one?"*
+5. **Open-ended** — *"Is there anything here you think the page is claiming that
+ it hasn't actually shown you?"*
+
+Then repeat steps 1–4 with `&static=1` and **keyboard only** (no mouse). The
+semantic answers must match; presentation may differ.
+
+## Targets
+
+| # | Question | Target |
+| --- | --- | --- |
+| 1 | Identifies the core problem at 5 s | **3/3** |
+| 2 | Explains the causal delta at 30 s | **3/3** |
+| 3 | Finds verifiable evidence by 60 s | **≥2/3** |
+| 4 | Distinguishes local proof from Google Cloud participation by 120 s | **≥2/3** |
+
+## Automatic failures
+
+These override every score above. If any occurs, the cold read **fails**
+regardless of questions 1–4:
+
+- a reader infers that **Agent Runtime** or **Agent Gateway** participated;
+- a reader believes the local experiment and the cloud run are **one combined
+ experiment**, or that the cloud run reproduced the 140/120 counterfactual;
+- a reader reads `ALLOW` as *verified*, *authorized* or *safe*, or reads
+ `EXECUTED` as `OBSERVED`;
+- a reader concludes Interlock is **universally** safe, production-ready, or
+ that it prevents collisions in general;
+- the keyboard/reduced-motion pass yields a **different semantic answer**.
+
+## Recording the result
+
+Write **verbatim answers** on the response sheet. Do not summarise into a score
+without them — the wording is the finding. A reader who says "it stops two
+robots fighting over the same file" has understood something different from one
+who says "it decides whether two jobs can run at once", and only the verbatim
+text preserves that.
+
+Only comprehension failures demonstrated by this protocol justify further UX
+change. A reader being slow is data; a moderator's hunch is not.
+
+## After the run
+
+Record the outcome on HAC-341 with the verbatim sheets attached. Until three
+real readers have completed this, HAC-341 stays open and every surface that
+mentions the cold read must continue to say it has not been run.
diff --git a/media/hac-341/cold-read/response-sheet.md b/media/hac-341/cold-read/response-sheet.md
new file mode 100644
index 0000000..d197c7f
--- /dev/null
+++ b/media/hac-341/cold-read/response-sheet.md
@@ -0,0 +1,125 @@
+# HAC-341 cold read — response sheet
+
+One copy per reader. Write what they said, not what they meant.
+
+```
+Reader #: ______ Date: ____________ Moderator: ____________
+Surface: https://interlock.marcellelabs.io/cockpit commit: ____________
+Browser / OS: ____________________ Logged out / private window? Y / N
+Unfamiliar with Interlock (has not seen cockpit, README, Devpost or video)? Y / N
+```
+
+---
+
+## Pass 1 — default, mouse allowed
+
+**Q1 · 5 s — "What problem is this page about?"**
+
+```
+verbatim:
+
+
+
+```
+Identified the core problem? **Y / N**
+
+---
+
+**Q2 · 30 s — "What changed, and why did it change?"**
+
+```
+verbatim:
+
+
+
+```
+Explained the causal delta? **Y / N**
+
+---
+
+**Q3 · 60 s — "How would you check it yourself?"**
+
+```
+verbatim:
+
+
+
+```
+Located verifiable evidence? **Y / N**
+
+---
+
+**Q4 · 120 s, after switching to the cloud URL — "Same experiment, or different?"**
+
+```
+verbatim:
+
+
+
+```
+Distinguished the two proof classes? **Y / N**
+
+---
+
+**Q5 · open — "Anything the page claims but hasn't shown you?"**
+
+```
+verbatim:
+
+
+
+```
+
+---
+
+## Pass 2 — `&static=1`, keyboard only
+
+```
+Q1 verbatim:
+
+Q2 verbatim:
+
+Q3 verbatim:
+
+Q4 verbatim:
+```
+
+Semantic answers match pass 1? **Y / N**
+If N, which diverged and how:
+
+```
+
+```
+
+---
+
+## Automatic failures — tick any that occurred
+
+```
+[ ] inferred Agent Runtime participated
+[ ] inferred Agent Gateway participated
+[ ] believed local + cloud are one combined experiment
+[ ] believed the cloud run reproduced the 140/120 counterfactual
+[ ] read ALLOW as verified / authorized / safe
+[ ] read EXECUTED as OBSERVED
+[ ] concluded Interlock is universally safe / production-ready
+[ ] concluded Interlock prevents collisions in general
+[ ] keyboard or reduced-motion pass gave a different semantic answer
+```
+
+Any tick = this reader is a **FAIL**, whatever the Y/N answers above.
+
+---
+
+## Moderator notes
+
+Where did they look first? Where did they get stuck? What did they scroll past?
+
+```
+
+
+
+```
+
+Did the moderator explain, prompt, or point at any stage? **Y / N**
+(If Y, this reader's result is void — the read was not cold.)
diff --git a/test/hac-341-identity-gates.test.mjs b/test/hac-341-identity-gates.test.mjs
index 0286228..1d9f014 100644
--- a/test/hac-341-identity-gates.test.mjs
+++ b/test/hac-341-identity-gates.test.mjs
@@ -329,12 +329,26 @@ describe('the selected arm drives its own evidence', () => {
it('fails when the cockpit stops consuming the shared derivation', () => {
const r = perturbed((a) => {
- a.edit('media/hac-341/cockpit.html', "import { armView } from './lib/arm-view.mjs';", '');
+ a.edit('media/hac-341/cockpit.html', "import { armView } from '/media/hac-341/lib/arm-view.mjs';", '');
}, gate);
expect(r.code).not.toBe(0);
expect(r.out).toMatch(/does not consume the shared arm-view derivation/);
});
+ it('fails when a reference goes document-relative and breaks the rewrite', () => {
+ // vercel.json rewrites `/` and `/cockpit` onto cockpit.html without
+ // changing the request URL, so a `./` specifier resolves against the
+ // request and 404s. The page still returns 200 and renders blank, and the
+ // visual gate cannot see it because it loads the file path directly.
+ const r = perturbed((a) => {
+ a.edit('media/hac-341/cockpit.html',
+ "await fetch('/media/hac-341/evidence/view-model.json'",
+ "await fetch('./evidence/view-model.json'");
+ }, gate);
+ expect(r.code).not.toBe(0);
+ expect(r.out).toMatch(/document-relative reference .* breaks under the vercel\.json rewrite/);
+ });
+
it('fails when the cockpit reads a basis off the environment again', () => {
const r = perturbed((a) => {
a.edit('media/hac-341/cockpit.html', 'const short =',